diff --git a/Assets/Demo.gif b/Assets/Demo.gif
new file mode 100644
index 0000000..92c3ed7
Binary files /dev/null and b/Assets/Demo.gif differ
diff --git a/Assets/Logo.png b/Assets/Logo.png
new file mode 100644
index 0000000..e9b9456
Binary files /dev/null and b/Assets/Logo.png differ
diff --git a/README.md b/README.md
index c1eb693..342ac5c 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,14 @@
-# XcodeCleaner
-MacOS application that allows you to analyze and free up disk space used by XCODE.
+
+
+
+
+
+
+
+## Installation
+To install Xcode Cleaner, go to [releases](https://github.com/IrelDev/XcodeCleaner/releases) page and download the `Xcode Cleaner.app` you need. If you prefer to build the project yourself just clone the `master` branch.
+
+## Demo
+
+
+
diff --git a/XcodeCleaner.xcodeproj/project.pbxproj b/XcodeCleaner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..788698e
--- /dev/null
+++ b/XcodeCleaner.xcodeproj/project.pbxproj
@@ -0,0 +1,603 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 9DCE1C0B24B582D200E49E6D /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCE1C0A24B582D200E49E6D /* CoreDataManager.swift */; };
+ 9DD3BA7024B4639400EDA308 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA6F24B4639400EDA308 /* AppDelegate.swift */; };
+ 9DD3BA7224B4639400EDA308 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA7124B4639400EDA308 /* ContentView.swift */; };
+ 9DD3BA7524B4639400EDA308 /* XcodeCleaner.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA7324B4639400EDA308 /* XcodeCleaner.xcdatamodeld */; };
+ 9DD3BA7724B4639700EDA308 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9DD3BA7624B4639700EDA308 /* Assets.xcassets */; };
+ 9DD3BA7A24B4639700EDA308 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9DD3BA7924B4639700EDA308 /* Preview Assets.xcassets */; };
+ 9DD3BA7D24B4639700EDA308 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9DD3BA7B24B4639700EDA308 /* Main.storyboard */; };
+ 9DD3BA8B24B465B800EDA308 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 9DD3BA8924B465B800EDA308 /* README.md */; };
+ 9DD3BA8C24B465B800EDA308 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9DD3BA8A24B465B800EDA308 /* LICENSE */; };
+ 9DD3BA8E24B478AE00EDA308 /* DirectoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA8D24B478AE00EDA308 /* DirectoryManager.swift */; };
+ 9DD3BA9024B478B600EDA308 /* DirectoryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA8F24B478B600EDA308 /* DirectoryType.swift */; };
+ 9DD3BA9424B478C900EDA308 /* DirectoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA9224B478C900EDA308 /* DirectoryModel.swift */; };
+ 9DD3BA9524B478C900EDA308 /* StatisticModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA9324B478C900EDA308 /* StatisticModel.swift */; };
+ 9DD3BA9E24B4791900EDA308 /* DropDownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA9B24B4791900EDA308 /* DropDownView.swift */; };
+ 9DD3BA9F24B4791900EDA308 /* BodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA9C24B4791900EDA308 /* BodyView.swift */; };
+ 9DD3BAA024B4791900EDA308 /* DirectoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BA9D24B4791900EDA308 /* DirectoryListView.swift */; };
+ 9DD3BAA224B4793900EDA308 /* FooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAA124B4793900EDA308 /* FooterView.swift */; };
+ 9DD3BAA424B4794300EDA308 /* DividerButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAA324B4794300EDA308 /* DividerButtonView.swift */; };
+ 9DD3BAA624B4794800EDA308 /* StatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAA524B4794800EDA308 /* StatisticView.swift */; };
+ 9DD3BAA824B4794C00EDA308 /* ScanProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAA724B4794C00EDA308 /* ScanProgressView.swift */; };
+ 9DD3BAAE24B4796C00EDA308 /* StatisticViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAAA24B4796C00EDA308 /* StatisticViewModel.swift */; };
+ 9DD3BAAF24B4796C00EDA308 /* PieChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAAB24B4796C00EDA308 /* PieChartViewModel.swift */; };
+ 9DD3BAB024B4796C00EDA308 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAAC24B4796C00EDA308 /* ViewModel.swift */; };
+ 9DD3BAB124B4796C00EDA308 /* DirectoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAAD24B4796C00EDA308 /* DirectoryListViewModel.swift */; };
+ 9DD3BAB724B4798E00EDA308 /* DirectoryListViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAB324B4798E00EDA308 /* DirectoryListViewModelProtocol.swift */; };
+ 9DD3BAB824B4798E00EDA308 /* PieChartViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAB424B4798E00EDA308 /* PieChartViewModelProtocol.swift */; };
+ 9DD3BAB924B4798E00EDA308 /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAB524B4798E00EDA308 /* ViewModelProtocol.swift */; };
+ 9DD3BABA24B4798E00EDA308 /* StatisticViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAB624B4798E00EDA308 /* StatisticViewModelProtocol.swift */; };
+ 9DD3BABD24B479BF00EDA308 /* BytesToStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BABC24B479BF00EDA308 /* BytesToStringFormatter.swift */; };
+ 9DD3BAC024B479DF00EDA308 /* PieChartSliceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BABF24B479DF00EDA308 /* PieChartSliceFactory.swift */; };
+ 9DD3BAC524B479ED00EDA308 /* PieChartSubSliceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAC224B479ED00EDA308 /* PieChartSubSliceView.swift */; };
+ 9DD3BAC624B479ED00EDA308 /* PieChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAC324B479ED00EDA308 /* PieChartView.swift */; };
+ 9DD3BAC724B479ED00EDA308 /* PieChartSliceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAC424B479ED00EDA308 /* PieChartSliceView.swift */; };
+ 9DD3BACB24B47A0400EDA308 /* PieChartSubSliceShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAC924B47A0400EDA308 /* PieChartSubSliceShape.swift */; };
+ 9DD3BACC24B47A0400EDA308 /* PieChartSliceShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BACA24B47A0400EDA308 /* PieChartSliceShape.swift */; };
+ 9DD3BAD224B47A1E00EDA308 /* PieChartSliceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BACE24B47A1D00EDA308 /* PieChartSliceModel.swift */; };
+ 9DD3BAD324B47A1E00EDA308 /* PieChartObservableItemsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BACF24B47A1E00EDA308 /* PieChartObservableItemsModel.swift */; };
+ 9DD3BAD424B47A1E00EDA308 /* PieChartItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAD024B47A1E00EDA308 /* PieChartItemModel.swift */; };
+ 9DD3BAD524B47A1E00EDA308 /* PieChartSubSliceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAD124B47A1E00EDA308 /* PieChartSubSliceModel.swift */; };
+ 9DD3BAD824B47A4800EDA308 /* PieChartAnySliceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD3BAD724B47A4800EDA308 /* PieChartAnySliceProtocol.swift */; };
+ 9DDFA81D24B58F47008C2C2A /* DateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DDFA81C24B58F47008C2C2A /* DateManager.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 9DCE1C0A24B582D200E49E6D /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; };
+ 9DD3BA6C24B4639400EDA308 /* XcodeCleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XcodeCleaner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9DD3BA6F24B4639400EDA308 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 9DD3BA7124B4639400EDA308 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 9DD3BA7424B4639400EDA308 /* XcodeCleaner.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = XcodeCleaner.xcdatamodel; sourceTree = ""; };
+ 9DD3BA7624B4639700EDA308 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 9DD3BA7924B4639700EDA308 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 9DD3BA7C24B4639700EDA308 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 9DD3BA7E24B4639700EDA308 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9DD3BA7F24B4639700EDA308 /* XcodeCleaner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = XcodeCleaner.entitlements; sourceTree = ""; };
+ 9DD3BA8924B465B800EDA308 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
+ 9DD3BA8A24B465B800EDA308 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
+ 9DD3BA8D24B478AE00EDA308 /* DirectoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryManager.swift; sourceTree = ""; };
+ 9DD3BA8F24B478B600EDA308 /* DirectoryType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryType.swift; sourceTree = ""; };
+ 9DD3BA9224B478C900EDA308 /* DirectoryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryModel.swift; sourceTree = ""; };
+ 9DD3BA9324B478C900EDA308 /* StatisticModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticModel.swift; sourceTree = ""; };
+ 9DD3BA9B24B4791900EDA308 /* DropDownView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDownView.swift; sourceTree = ""; };
+ 9DD3BA9C24B4791900EDA308 /* BodyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BodyView.swift; sourceTree = ""; };
+ 9DD3BA9D24B4791900EDA308 /* DirectoryListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryListView.swift; sourceTree = ""; };
+ 9DD3BAA124B4793900EDA308 /* FooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FooterView.swift; sourceTree = ""; };
+ 9DD3BAA324B4794300EDA308 /* DividerButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DividerButtonView.swift; sourceTree = ""; };
+ 9DD3BAA524B4794800EDA308 /* StatisticView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticView.swift; sourceTree = ""; };
+ 9DD3BAA724B4794C00EDA308 /* ScanProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanProgressView.swift; sourceTree = ""; };
+ 9DD3BAAA24B4796C00EDA308 /* StatisticViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticViewModel.swift; sourceTree = ""; };
+ 9DD3BAAB24B4796C00EDA308 /* PieChartViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartViewModel.swift; sourceTree = ""; };
+ 9DD3BAAC24B4796C00EDA308 /* ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; };
+ 9DD3BAAD24B4796C00EDA308 /* DirectoryListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryListViewModel.swift; sourceTree = ""; };
+ 9DD3BAB324B4798E00EDA308 /* DirectoryListViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryListViewModelProtocol.swift; sourceTree = ""; };
+ 9DD3BAB424B4798E00EDA308 /* PieChartViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartViewModelProtocol.swift; sourceTree = ""; };
+ 9DD3BAB524B4798E00EDA308 /* ViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; };
+ 9DD3BAB624B4798E00EDA308 /* StatisticViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticViewModelProtocol.swift; sourceTree = ""; };
+ 9DD3BABC24B479BF00EDA308 /* BytesToStringFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BytesToStringFormatter.swift; sourceTree = ""; };
+ 9DD3BABF24B479DF00EDA308 /* PieChartSliceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSliceFactory.swift; sourceTree = ""; };
+ 9DD3BAC224B479ED00EDA308 /* PieChartSubSliceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSubSliceView.swift; sourceTree = ""; };
+ 9DD3BAC324B479ED00EDA308 /* PieChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartView.swift; sourceTree = ""; };
+ 9DD3BAC424B479ED00EDA308 /* PieChartSliceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSliceView.swift; sourceTree = ""; };
+ 9DD3BAC924B47A0400EDA308 /* PieChartSubSliceShape.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSubSliceShape.swift; sourceTree = ""; };
+ 9DD3BACA24B47A0400EDA308 /* PieChartSliceShape.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSliceShape.swift; sourceTree = ""; };
+ 9DD3BACE24B47A1D00EDA308 /* PieChartSliceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSliceModel.swift; sourceTree = ""; };
+ 9DD3BACF24B47A1E00EDA308 /* PieChartObservableItemsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartObservableItemsModel.swift; sourceTree = ""; };
+ 9DD3BAD024B47A1E00EDA308 /* PieChartItemModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartItemModel.swift; sourceTree = ""; };
+ 9DD3BAD124B47A1E00EDA308 /* PieChartSubSliceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartSubSliceModel.swift; sourceTree = ""; };
+ 9DD3BAD724B47A4800EDA308 /* PieChartAnySliceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartAnySliceProtocol.swift; sourceTree = ""; };
+ 9DDFA81C24B58F47008C2C2A /* DateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateManager.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 9DD3BA6924B4639400EDA308 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9DD3BA6324B4639400EDA308 = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA8A24B465B800EDA308 /* LICENSE */,
+ 9DD3BA8924B465B800EDA308 /* README.md */,
+ 9DD3BA6E24B4639400EDA308 /* XcodeCleaner */,
+ 9DD3BA6D24B4639400EDA308 /* Products */,
+ );
+ sourceTree = "";
+ };
+ 9DD3BA6D24B4639400EDA308 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA6C24B4639400EDA308 /* XcodeCleaner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 9DD3BA6E24B4639400EDA308 /* XcodeCleaner */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA6F24B4639400EDA308 /* AppDelegate.swift */,
+ 9DD3BA8F24B478B600EDA308 /* DirectoryType.swift */,
+ 9DD3BA8D24B478AE00EDA308 /* DirectoryManager.swift */,
+ 9DCE1C0A24B582D200E49E6D /* CoreDataManager.swift */,
+ 9DD3BA9624B478CC00EDA308 /* Views */,
+ 9DD3BA9124B478C100EDA308 /* Models */,
+ 9DD3BAA924B4795A00EDA308 /* ViewModels */,
+ 9DD3BAB224B4798200EDA308 /* Protocols */,
+ 9DD3BABB24B479B300EDA308 /* Helpers */,
+ 9DD3BABE24B479C900EDA308 /* PieChart */,
+ 9DD3BA7624B4639700EDA308 /* Assets.xcassets */,
+ 9DD3BA7B24B4639700EDA308 /* Main.storyboard */,
+ 9DD3BA7E24B4639700EDA308 /* Info.plist */,
+ 9DD3BA7F24B4639700EDA308 /* XcodeCleaner.entitlements */,
+ 9DD3BA7324B4639400EDA308 /* XcodeCleaner.xcdatamodeld */,
+ 9DD3BA7824B4639700EDA308 /* Preview Content */,
+ );
+ path = XcodeCleaner;
+ sourceTree = "";
+ };
+ 9DD3BA7824B4639700EDA308 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA7924B4639700EDA308 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 9DD3BA9124B478C100EDA308 /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA9224B478C900EDA308 /* DirectoryModel.swift */,
+ 9DD3BA9324B478C900EDA308 /* StatisticModel.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ 9DD3BA9624B478CC00EDA308 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA7124B4639400EDA308 /* ContentView.swift */,
+ 9DD3BA9924B478FD00EDA308 /* BodyViews */,
+ 9DD3BA9A24B4790200EDA308 /* FooterViews */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ 9DD3BA9924B478FD00EDA308 /* BodyViews */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BA9C24B4791900EDA308 /* BodyView.swift */,
+ 9DD3BA9D24B4791900EDA308 /* DirectoryListView.swift */,
+ 9DD3BA9B24B4791900EDA308 /* DropDownView.swift */,
+ );
+ path = BodyViews;
+ sourceTree = "";
+ };
+ 9DD3BA9A24B4790200EDA308 /* FooterViews */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BAA124B4793900EDA308 /* FooterView.swift */,
+ 9DD3BAA324B4794300EDA308 /* DividerButtonView.swift */,
+ 9DD3BAA524B4794800EDA308 /* StatisticView.swift */,
+ 9DD3BAA724B4794C00EDA308 /* ScanProgressView.swift */,
+ );
+ path = FooterViews;
+ sourceTree = "";
+ };
+ 9DD3BAA924B4795A00EDA308 /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BAAC24B4796C00EDA308 /* ViewModel.swift */,
+ 9DD3BAAD24B4796C00EDA308 /* DirectoryListViewModel.swift */,
+ 9DD3BAAB24B4796C00EDA308 /* PieChartViewModel.swift */,
+ 9DD3BAAA24B4796C00EDA308 /* StatisticViewModel.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ 9DD3BAB224B4798200EDA308 /* Protocols */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BAB524B4798E00EDA308 /* ViewModelProtocol.swift */,
+ 9DD3BAB424B4798E00EDA308 /* PieChartViewModelProtocol.swift */,
+ 9DD3BAB324B4798E00EDA308 /* DirectoryListViewModelProtocol.swift */,
+ 9DD3BAB624B4798E00EDA308 /* StatisticViewModelProtocol.swift */,
+ );
+ path = Protocols;
+ sourceTree = "";
+ };
+ 9DD3BABB24B479B300EDA308 /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BABC24B479BF00EDA308 /* BytesToStringFormatter.swift */,
+ 9DDFA81C24B58F47008C2C2A /* DateManager.swift */,
+ );
+ path = Helpers;
+ sourceTree = "";
+ };
+ 9DD3BABE24B479C900EDA308 /* PieChart */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BABF24B479DF00EDA308 /* PieChartSliceFactory.swift */,
+ 9DD3BAC124B479E500EDA308 /* Views */,
+ 9DD3BAC824B479F300EDA308 /* Shapes */,
+ 9DD3BACD24B47A1000EDA308 /* Models */,
+ 9DD3BAD624B47A2300EDA308 /* Protocols */,
+ );
+ path = PieChart;
+ sourceTree = "";
+ };
+ 9DD3BAC124B479E500EDA308 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BAC424B479ED00EDA308 /* PieChartSliceView.swift */,
+ 9DD3BAC224B479ED00EDA308 /* PieChartSubSliceView.swift */,
+ 9DD3BAC324B479ED00EDA308 /* PieChartView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ 9DD3BAC824B479F300EDA308 /* Shapes */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BACA24B47A0400EDA308 /* PieChartSliceShape.swift */,
+ 9DD3BAC924B47A0400EDA308 /* PieChartSubSliceShape.swift */,
+ );
+ path = Shapes;
+ sourceTree = "";
+ };
+ 9DD3BACD24B47A1000EDA308 /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BAD024B47A1E00EDA308 /* PieChartItemModel.swift */,
+ 9DD3BACF24B47A1E00EDA308 /* PieChartObservableItemsModel.swift */,
+ 9DD3BACE24B47A1D00EDA308 /* PieChartSliceModel.swift */,
+ 9DD3BAD124B47A1E00EDA308 /* PieChartSubSliceModel.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ 9DD3BAD624B47A2300EDA308 /* Protocols */ = {
+ isa = PBXGroup;
+ children = (
+ 9DD3BAD724B47A4800EDA308 /* PieChartAnySliceProtocol.swift */,
+ );
+ path = Protocols;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 9DD3BA6B24B4639400EDA308 /* XcodeCleaner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 9DD3BA8224B4639700EDA308 /* Build configuration list for PBXNativeTarget "XcodeCleaner" */;
+ buildPhases = (
+ 9DD3BA6824B4639400EDA308 /* Sources */,
+ 9DD3BA6924B4639400EDA308 /* Frameworks */,
+ 9DD3BA6A24B4639400EDA308 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = XcodeCleaner;
+ productName = XcodeCleaner;
+ productReference = 9DD3BA6C24B4639400EDA308 /* XcodeCleaner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 9DD3BA6424B4639400EDA308 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1150;
+ LastUpgradeCheck = 1150;
+ ORGANIZATIONNAME = "Kirill Pustovalov";
+ TargetAttributes = {
+ 9DD3BA6B24B4639400EDA308 = {
+ CreatedOnToolsVersion = 11.5;
+ };
+ };
+ };
+ buildConfigurationList = 9DD3BA6724B4639400EDA308 /* Build configuration list for PBXProject "XcodeCleaner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 9DD3BA6324B4639400EDA308;
+ productRefGroup = 9DD3BA6D24B4639400EDA308 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 9DD3BA6B24B4639400EDA308 /* XcodeCleaner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 9DD3BA6A24B4639400EDA308 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9DD3BA7D24B4639700EDA308 /* Main.storyboard in Resources */,
+ 9DD3BA7A24B4639700EDA308 /* Preview Assets.xcassets in Resources */,
+ 9DD3BA8B24B465B800EDA308 /* README.md in Resources */,
+ 9DD3BA7724B4639700EDA308 /* Assets.xcassets in Resources */,
+ 9DD3BA8C24B465B800EDA308 /* LICENSE in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 9DD3BA6824B4639400EDA308 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9DD3BA7224B4639400EDA308 /* ContentView.swift in Sources */,
+ 9DD3BA9524B478C900EDA308 /* StatisticModel.swift in Sources */,
+ 9DD3BAD424B47A1E00EDA308 /* PieChartItemModel.swift in Sources */,
+ 9DD3BAC624B479ED00EDA308 /* PieChartView.swift in Sources */,
+ 9DD3BA9F24B4791900EDA308 /* BodyView.swift in Sources */,
+ 9DD3BA9024B478B600EDA308 /* DirectoryType.swift in Sources */,
+ 9DD3BACC24B47A0400EDA308 /* PieChartSliceShape.swift in Sources */,
+ 9DD3BAA824B4794C00EDA308 /* ScanProgressView.swift in Sources */,
+ 9DD3BAC524B479ED00EDA308 /* PieChartSubSliceView.swift in Sources */,
+ 9DD3BA8E24B478AE00EDA308 /* DirectoryManager.swift in Sources */,
+ 9DD3BAB024B4796C00EDA308 /* ViewModel.swift in Sources */,
+ 9DD3BA9424B478C900EDA308 /* DirectoryModel.swift in Sources */,
+ 9DD3BAA224B4793900EDA308 /* FooterView.swift in Sources */,
+ 9DD3BAC724B479ED00EDA308 /* PieChartSliceView.swift in Sources */,
+ 9DD3BA9E24B4791900EDA308 /* DropDownView.swift in Sources */,
+ 9DD3BABD24B479BF00EDA308 /* BytesToStringFormatter.swift in Sources */,
+ 9DD3BABA24B4798E00EDA308 /* StatisticViewModelProtocol.swift in Sources */,
+ 9DD3BAAE24B4796C00EDA308 /* StatisticViewModel.swift in Sources */,
+ 9DD3BAD524B47A1E00EDA308 /* PieChartSubSliceModel.swift in Sources */,
+ 9DD3BAB924B4798E00EDA308 /* ViewModelProtocol.swift in Sources */,
+ 9DD3BACB24B47A0400EDA308 /* PieChartSubSliceShape.swift in Sources */,
+ 9DD3BAA624B4794800EDA308 /* StatisticView.swift in Sources */,
+ 9DD3BAA424B4794300EDA308 /* DividerButtonView.swift in Sources */,
+ 9DD3BA7024B4639400EDA308 /* AppDelegate.swift in Sources */,
+ 9DD3BAA024B4791900EDA308 /* DirectoryListView.swift in Sources */,
+ 9DDFA81D24B58F47008C2C2A /* DateManager.swift in Sources */,
+ 9DD3BAAF24B4796C00EDA308 /* PieChartViewModel.swift in Sources */,
+ 9DD3BAD224B47A1E00EDA308 /* PieChartSliceModel.swift in Sources */,
+ 9DD3BAB824B4798E00EDA308 /* PieChartViewModelProtocol.swift in Sources */,
+ 9DD3BAB124B4796C00EDA308 /* DirectoryListViewModel.swift in Sources */,
+ 9DD3BAC024B479DF00EDA308 /* PieChartSliceFactory.swift in Sources */,
+ 9DD3BAD324B47A1E00EDA308 /* PieChartObservableItemsModel.swift in Sources */,
+ 9DD3BA7524B4639400EDA308 /* XcodeCleaner.xcdatamodeld in Sources */,
+ 9DD3BAB724B4798E00EDA308 /* DirectoryListViewModelProtocol.swift in Sources */,
+ 9DCE1C0B24B582D200E49E6D /* CoreDataManager.swift in Sources */,
+ 9DD3BAD824B47A4800EDA308 /* PieChartAnySliceProtocol.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 9DD3BA7B24B4639700EDA308 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 9DD3BA7C24B4639700EDA308 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 9DD3BA8024B4639700EDA308 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.16;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 9DD3BA8124B4639700EDA308 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.16;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 9DD3BA8324B4639700EDA308 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = XcodeCleaner/XcodeCleaner.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ DEVELOPMENT_ASSET_PATHS = "\"XcodeCleaner/Preview Content\"";
+ DEVELOPMENT_TEAM = A46C2G8GT3;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = XcodeCleaner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ PRODUCT_BUNDLE_IDENTIFIER = com.ireldev.XcodeCleaner.XcodeCleaner;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 9DD3BA8424B4639700EDA308 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = XcodeCleaner/XcodeCleaner.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ DEVELOPMENT_ASSET_PATHS = "\"XcodeCleaner/Preview Content\"";
+ DEVELOPMENT_TEAM = A46C2G8GT3;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = XcodeCleaner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ PRODUCT_BUNDLE_IDENTIFIER = com.ireldev.XcodeCleaner.XcodeCleaner;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 9DD3BA6724B4639400EDA308 /* Build configuration list for PBXProject "XcodeCleaner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9DD3BA8024B4639700EDA308 /* Debug */,
+ 9DD3BA8124B4639700EDA308 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 9DD3BA8224B4639700EDA308 /* Build configuration list for PBXNativeTarget "XcodeCleaner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9DD3BA8324B4639700EDA308 /* Debug */,
+ 9DD3BA8424B4639700EDA308 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCVersionGroup section */
+ 9DD3BA7324B4639400EDA308 /* XcodeCleaner.xcdatamodeld */ = {
+ isa = XCVersionGroup;
+ children = (
+ 9DD3BA7424B4639400EDA308 /* XcodeCleaner.xcdatamodel */,
+ );
+ currentVersion = 9DD3BA7424B4639400EDA308 /* XcodeCleaner.xcdatamodel */;
+ path = XcodeCleaner.xcdatamodeld;
+ sourceTree = "";
+ versionGroupType = wrapper.xcdatamodel;
+ };
+/* End XCVersionGroup section */
+ };
+ rootObject = 9DD3BA6424B4639400EDA308 /* Project object */;
+}
diff --git a/XcodeCleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/XcodeCleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..b9aaed9
--- /dev/null
+++ b/XcodeCleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/XcodeCleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/XcodeCleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/XcodeCleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/XcodeCleaner.xcodeproj/xcuserdata/revilarva.xcuserdatad/xcschemes/xcschememanagement.plist b/XcodeCleaner.xcodeproj/xcuserdata/revilarva.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..a5817c0
--- /dev/null
+++ b/XcodeCleaner.xcodeproj/xcuserdata/revilarva.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ SchemeUserState
+
+ XcodeCleaner.xcscheme_^#shared#^_
+
+ orderHint
+ 0
+
+
+
+
diff --git a/XcodeCleaner/AppDelegate.swift b/XcodeCleaner/AppDelegate.swift
new file mode 100644
index 0000000..a827def
--- /dev/null
+++ b/XcodeCleaner/AppDelegate.swift
@@ -0,0 +1,137 @@
+//
+// AppDelegate.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 07.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Cocoa
+import SwiftUI
+
+@NSApplicationMain
+class AppDelegate: NSObject, NSApplicationDelegate {
+
+ var window: NSWindow!
+
+
+ func applicationDidFinishLaunching(_ aNotification: Notification) {
+ // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
+ // Add `@Environment(\.managedObjectContext)` in the views that will need the context.
+ let viewModel = ViewModel()
+ let contentView = ContentView().environment(\.managedObjectContext, persistentContainer.viewContext).environmentObject(viewModel)
+
+ // Create the window and set the content view.
+ window = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
+ styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
+ backing: .buffered, defer: false)
+ window.center()
+ window.setFrameAutosaveName("Main Window")
+ window.contentView = NSHostingView(rootView: contentView)
+ window.makeKeyAndOrderFront(nil)
+ }
+
+ func applicationWillTerminate(_ aNotification: Notification) {
+ // Insert code here to tear down your application
+ }
+
+ // MARK: - Core Data stack
+
+ lazy var persistentContainer: NSPersistentContainer = {
+ /*
+ The persistent container for the application. This implementation
+ creates and returns a container, having loaded the store for the
+ application to it. This property is optional since there are legitimate
+ error conditions that could cause the creation of the store to fail.
+ */
+ let container = NSPersistentContainer(name: "XcodeCleaner")
+ container.loadPersistentStores(completionHandler: { (storeDescription, error) in
+ if let error = error {
+ // Replace this implementation with code to handle the error appropriately.
+ // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
+
+ /*
+ Typical reasons for an error here include:
+ * The parent directory does not exist, cannot be created, or disallows writing.
+ * The persistent store is not accessible, due to permissions or data protection when the device is locked.
+ * The device is out of space.
+ * The store could not be migrated to the current model version.
+ Check the error message to determine what the actual problem was.
+ */
+ fatalError("Unresolved error \(error)")
+ }
+ })
+ return container
+ }()
+
+ // MARK: - Core Data Saving and Undo support
+
+ @IBAction func saveAction(_ sender: AnyObject?) {
+ // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user.
+ let context = persistentContainer.viewContext
+
+ if !context.commitEditing() {
+ NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving")
+ }
+ if context.hasChanges {
+ do {
+ try context.save()
+ } catch {
+ // Customize this code block to include application-specific recovery steps.
+ let nserror = error as NSError
+ NSApplication.shared.presentError(nserror)
+ }
+ }
+ }
+
+ func windowWillReturnUndoManager(window: NSWindow) -> UndoManager? {
+ // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application.
+ return persistentContainer.viewContext.undoManager
+ }
+
+ func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
+ // Save changes in the application's managed object context before the application terminates.
+ let context = persistentContainer.viewContext
+
+ if !context.commitEditing() {
+ NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing to terminate")
+ return .terminateCancel
+ }
+
+ if !context.hasChanges {
+ return .terminateNow
+ }
+
+ do {
+ try context.save()
+ } catch {
+ let nserror = error as NSError
+
+ // Customize this code block to include application-specific recovery steps.
+ let result = sender.presentError(nserror)
+ if (result) {
+ return .terminateCancel
+ }
+
+ let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message")
+ let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info");
+ let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title")
+ let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title")
+ let alert = NSAlert()
+ alert.messageText = question
+ alert.informativeText = info
+ alert.addButton(withTitle: quitButton)
+ alert.addButton(withTitle: cancelButton)
+
+ let answer = alert.runModal()
+ if answer == .alertSecondButtonReturn {
+ return .terminateCancel
+ }
+ }
+ // If we got here, it is time to quit.
+ return .terminateNow
+ }
+
+}
+
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/Contents.json b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..6adbbd7
--- /dev/null
+++ b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "filename" : "mac-16x16.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "filename" : "mac-16x16@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "filename" : "mac-32x32.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "filename" : "mac-32x32@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "filename" : "mac-128x128.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "filename" : "mac-128x128@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "filename" : "mac-256x256.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "mac-256x256@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "mac-512x512.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "filename" : "mac-512x512@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-128x128.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-128x128.png
new file mode 100644
index 0000000..b552560
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-128x128.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-128x128@2x.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-128x128@2x.png
new file mode 100644
index 0000000..f003af9
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-128x128@2x.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-16x16.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-16x16.png
new file mode 100644
index 0000000..091a722
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-16x16.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-16x16@2x.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-16x16@2x.png
new file mode 100644
index 0000000..13628b2
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-16x16@2x.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-256x256.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-256x256.png
new file mode 100644
index 0000000..f003af9
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-256x256.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-256x256@2x.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-256x256@2x.png
new file mode 100644
index 0000000..5622065
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-256x256@2x.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-32x32.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-32x32.png
new file mode 100644
index 0000000..13628b2
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-32x32.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-32x32@2x.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-32x32@2x.png
new file mode 100644
index 0000000..2587a8d
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-32x32@2x.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-512x512.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-512x512.png
new file mode 100644
index 0000000..5622065
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-512x512.png differ
diff --git a/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-512x512@2x.png b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-512x512@2x.png
new file mode 100644
index 0000000..b6ee3f4
Binary files /dev/null and b/XcodeCleaner/Assets.xcassets/AppIcon.appiconset/mac-512x512@2x.png differ
diff --git a/XcodeCleaner/Assets.xcassets/Contents.json b/XcodeCleaner/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/XcodeCleaner/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/XcodeCleaner/Base.lproj/Main.storyboard b/XcodeCleaner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..57e74f8
--- /dev/null
+++ b/XcodeCleaner/Base.lproj/Main.storyboard
@@ -0,0 +1,683 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XcodeCleaner/CoreDataManager.swift b/XcodeCleaner/CoreDataManager.swift
new file mode 100644
index 0000000..4213356
--- /dev/null
+++ b/XcodeCleaner/CoreDataManager.swift
@@ -0,0 +1,76 @@
+//
+// CoreDataManager.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 08.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import AppKit
+import CoreData
+
+struct CoreDataManager {
+ static let shared = CoreDataManager()
+
+ func getStatistic() -> StatisticModel {
+ var statisticModel = StatisticModel()
+ fetchStatisticIn(statisticModel: &statisticModel)
+
+ return statisticModel
+ }
+ func saveStatistic(statistic: StatisticModel) {
+ let newTotalCleanedValue = statistic.totalCleaned
+ let newDate = statistic.lastTimeCleaned
+
+ saveStatistic(newValue: newTotalCleanedValue, newDate: newDate)
+ }
+ private func fetchStatisticIn(statisticModel: inout StatisticModel) {
+ guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
+
+ let managedContext = appDelegate.persistentContainer.viewContext
+ let fetchRequest = NSFetchRequest(entityName: "StatisticEntity")
+
+ do {
+ let fetchRequestResults = try managedContext.fetch(fetchRequest)
+
+ let totalCleaned = fetchRequestResults.first?.value(forKey: "totalSize") as? Int64
+ let date = fetchRequestResults.first?.value(forKey: "date") as? Date
+
+ let modelFromFetchRequest = StatisticModel(totalCleaned: totalCleaned, lastTimeCleaned: date)
+ statisticModel = modelFromFetchRequest
+
+ } catch {
+ print("could not fetch statistic \(error.localizedDescription)")
+ }
+ }
+ private func saveStatistic(newValue: Int64?, newDate: Date?) {
+ guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
+ let managedContext = appDelegate.persistentContainer.viewContext
+ let entity = NSEntityDescription.entity(forEntityName: "StatisticEntity", in: managedContext)!
+
+ let fetchRequest = NSFetchRequest(entityName: "StatisticEntity")
+
+ do {
+ let fetchRequestResults = try managedContext.fetch(fetchRequest)
+ if fetchRequestResults.count == 0 {
+ let statistic = NSManagedObject(entity: entity, insertInto: managedContext)
+
+ statistic.setValue(newValue, forKeyPath: "totalSize")
+ statistic.setValue(newDate, forKeyPath: "date")
+ } else {
+ let previousStatistic = fetchRequestResults.first!
+
+ let previousTotalSizeValue = previousStatistic.value(forKey: "totalSize") as! Int64
+ let value = (newValue ?? 0) + previousTotalSizeValue
+
+ previousStatistic.setValue(newDate, forKey: "date")
+ previousStatistic.setValue(value, forKey: "totalSize")
+ }
+
+ try managedContext.save()
+ } catch {
+ print("Error, could not save statistic \(error.localizedDescription)")
+ }
+ }
+}
+
diff --git a/XcodeCleaner/DirectoryManager.swift b/XcodeCleaner/DirectoryManager.swift
new file mode 100644
index 0000000..857905a
--- /dev/null
+++ b/XcodeCleaner/DirectoryManager.swift
@@ -0,0 +1,130 @@
+//
+// DirectoryManager.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 04.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Cocoa
+
+struct DirectoryManager {
+ func getValidHomeDirectory() -> String {
+ var homeDirectory = NSHomeDirectory()
+ let prefix = "/Library/Containers/"
+
+ if homeDirectory.contains(prefix) {
+ if let range = homeDirectory.range(of: prefix) {
+ homeDirectory = String(homeDirectory[.. String {
+ let homeDirectory = getValidHomeDirectory()
+ let xcodePath = "/Library/Developer/Xcode/"
+ return "\(homeDirectory + xcodePath)"
+ }
+ func getDerivedDataPath() -> String {
+ let xcodePath = getXcodeDefaultPath()
+ let derivedDataPath = "DerivedData/"
+ return "\(xcodePath + derivedDataPath)"
+ }
+ func getDeviceSupportPath() -> String {
+ let xcodePath = getXcodeDefaultPath()
+ let deviceSupportPath = "iOS DeviceSupport/"
+ return "\(xcodePath + deviceSupportPath)"
+ }
+ func getArchivesPath() -> String {
+ let xcodePath = getXcodeDefaultPath()
+ let deviceSupportPath = "Archives/"
+ return "\(xcodePath + deviceSupportPath)"
+ }
+ func getSubDirectoriesForPath(path: String) -> [String] {
+ let fileManager = FileManager.default
+
+ var subDirectories: [String] = []
+ do {
+ let directories = try fileManager.contentsOfDirectory(atPath: path)
+
+ for directory in directories {
+ let subDirectoryPath = path + directory
+ subDirectories.append(subDirectoryPath)
+ }
+ } catch {
+ print(error.localizedDescription)
+ }
+
+ return subDirectories
+ }
+ func getDirectorySize(path: String, completion: @escaping () -> Void = { }) -> Int64 {
+ let fileManager = FileManager.default
+ var directorySize: Int64 = 0
+
+ let directories = fileManager.subpaths(atPath: path)
+ guard directories != nil else {
+ completion()
+ return 0
+ }
+
+ for directory in directories! {
+ do {
+ let attributes = try fileManager.attributesOfItem(atPath: path + directory)
+ directorySize += attributes[FileAttributeKey.size] as! Int64
+ } catch {
+ print(error.localizedDescription)
+ }
+ }
+ completion()
+
+ return directorySize
+ }
+ func normalizeDirectoryPath(directory: String) -> String {
+ var newDirectoryPath = directory
+
+ if !newDirectoryPath.hasSuffix("/") {
+ newDirectoryPath += "/"
+ }
+ return newDirectoryPath
+ }
+ func normalizeDirectoryPathForDisplay(directory: String, forType type: DirectoryType) -> String {
+ var result = directory
+ var prefix: String
+
+ switch type {
+ case .derivedData:
+ prefix = getDerivedDataPath()
+ case .deviceSupport:
+ prefix = getDeviceSupportPath()
+ case .archives:
+ prefix = getArchivesPath()
+ }
+
+ if directory.contains(prefix) {
+ result = directory.replacingOccurrences(of: prefix, with: "")
+ }
+
+ return result
+ }
+ func cleanDirectory(forType type: DirectoryType) {
+ let fileManager = FileManager.default
+
+ var directoryPath: String
+ switch type {
+ case .derivedData:
+ directoryPath = getDerivedDataPath()
+ case .deviceSupport:
+ directoryPath = getDeviceSupportPath()
+ case .archives:
+ directoryPath = getArchivesPath()
+ }
+
+ let directoryURL = URL(fileURLWithPath: normalizeDirectoryPath(directory: directoryPath))
+
+ do {
+ try fileManager.removeItem(at: directoryURL)
+ } catch {
+ print(error.localizedDescription)
+ }
+ }
+}
diff --git a/XcodeCleaner/DirectoryType.swift b/XcodeCleaner/DirectoryType.swift
new file mode 100644
index 0000000..fd23be4
--- /dev/null
+++ b/XcodeCleaner/DirectoryType.swift
@@ -0,0 +1,15 @@
+//
+// DirectoryType.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 05.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+public enum DirectoryType {
+ case derivedData
+ case deviceSupport
+ case archives
+}
diff --git a/XcodeCleaner/Helpers/BytesToStringFormatter.swift b/XcodeCleaner/Helpers/BytesToStringFormatter.swift
new file mode 100644
index 0000000..56dc1b8
--- /dev/null
+++ b/XcodeCleaner/Helpers/BytesToStringFormatter.swift
@@ -0,0 +1,18 @@
+//
+// BytesToStringFormatter.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+struct BytesToStringFormatter {
+ static func format(size: Int64, allowedUnits: ByteCountFormatter.Units = [.useGB, .useMB]) -> String {
+ let byteCountFormatter = ByteCountFormatter()
+ byteCountFormatter.allowedUnits = allowedUnits
+
+ return byteCountFormatter.string(fromByteCount: size)
+ }
+}
diff --git a/XcodeCleaner/Helpers/DateManager.swift b/XcodeCleaner/Helpers/DateManager.swift
new file mode 100644
index 0000000..a2355c0
--- /dev/null
+++ b/XcodeCleaner/Helpers/DateManager.swift
@@ -0,0 +1,21 @@
+//
+// DateManager.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 08.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+struct DateManager {
+ static func getCurrentDate() -> Date {
+ Date()
+ }
+ static func getStringDate(date: Date) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = .medium
+ dateFormatter.timeStyle = .none
+ return dateFormatter.string(from: date)
+ }
+}
diff --git a/XcodeCleaner/Info.plist b/XcodeCleaner/Info.plist
new file mode 100644
index 0000000..c68cd26
--- /dev/null
+++ b/XcodeCleaner/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSApplicationCategoryType
+ public.app-category.developer-tools
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ Copyright © 2020 Kirill Pustovalov. All rights reserved.
+ NSMainStoryboardFile
+ Main
+ NSPrincipalClass
+ NSApplication
+ NSSupportsAutomaticTermination
+
+ NSSupportsSuddenTermination
+
+
+
diff --git a/XcodeCleaner/Models/DirectoryModel.swift b/XcodeCleaner/Models/DirectoryModel.swift
new file mode 100644
index 0000000..a0e3fc2
--- /dev/null
+++ b/XcodeCleaner/Models/DirectoryModel.swift
@@ -0,0 +1,14 @@
+//
+// DirectoryModel.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 04.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+struct DirectoryModel {
+ var name: String
+ var size: Int64
+}
diff --git a/XcodeCleaner/Models/StatisticModel.swift b/XcodeCleaner/Models/StatisticModel.swift
new file mode 100644
index 0000000..ce49743
--- /dev/null
+++ b/XcodeCleaner/Models/StatisticModel.swift
@@ -0,0 +1,14 @@
+//
+// StatisticModel.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+struct StatisticModel {
+ var totalCleaned: Int64?
+ var lastTimeCleaned: Date?
+}
diff --git a/XcodeCleaner/PieChart/Models/PieChartItemModel.swift b/XcodeCleaner/PieChart/Models/PieChartItemModel.swift
new file mode 100644
index 0000000..7343dab
--- /dev/null
+++ b/XcodeCleaner/PieChart/Models/PieChartItemModel.swift
@@ -0,0 +1,22 @@
+//
+// PieChartItemModel.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+public struct PieChartItemModel {
+ public var value: Double
+ public var color: Color
+ public var subItems: [PieChartItemModel]
+
+ public init(value: Double, color: Color, subItems: [PieChartItemModel] = []) {
+ self.value = value
+ self.color = color
+
+ self.subItems = subItems
+ }
+}
diff --git a/XcodeCleaner/PieChart/Models/PieChartObservableItemsModel.swift b/XcodeCleaner/PieChart/Models/PieChartObservableItemsModel.swift
new file mode 100644
index 0000000..ddbc7c8
--- /dev/null
+++ b/XcodeCleaner/PieChart/Models/PieChartObservableItemsModel.swift
@@ -0,0 +1,21 @@
+//
+// PieChartObservableItemsModel.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+public class PCItems: ObservableObject {
+ @Published public var items: [PieChartItemModel]
+ public init(items: [PieChartItemModel]) {
+ self.items = items
+ }
+ public init(data: [Double], chartColor: Color) {
+ self.items = data.map {
+ PieChartItemModel(value: $0, color: chartColor)
+ }
+ }
+}
diff --git a/XcodeCleaner/PieChart/Models/PieChartSliceModel.swift b/XcodeCleaner/PieChart/Models/PieChartSliceModel.swift
new file mode 100644
index 0000000..95af123
--- /dev/null
+++ b/XcodeCleaner/PieChart/Models/PieChartSliceModel.swift
@@ -0,0 +1,29 @@
+//
+// PieChartSliceModel.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartSliceModel: PieChartAnySliceProtocol {
+ var value: Double
+ var color: Color
+
+ var startDegree: Double
+ var endDegree: Double
+
+ var subSlices: [PieChartSubSliceModel]
+
+ public init(value: Double, color: Color, startDegree: Double, endDegree: Double, subSlices: [PieChartSubSliceModel] = []) {
+ self.value = value
+ self.color = color
+
+ self.startDegree = startDegree
+ self.endDegree = endDegree
+ self.subSlices = subSlices
+ }
+}
+
diff --git a/XcodeCleaner/PieChart/Models/PieChartSubSliceModel.swift b/XcodeCleaner/PieChart/Models/PieChartSubSliceModel.swift
new file mode 100644
index 0000000..33bebce
--- /dev/null
+++ b/XcodeCleaner/PieChart/Models/PieChartSubSliceModel.swift
@@ -0,0 +1,26 @@
+//
+// PieChartSubSliceModel.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartSubSliceModel: PieChartAnySliceProtocol {
+ var value: Double
+ var color: Color
+
+ var startDegree: Double
+ var endDegree: Double
+
+ public init(value: Double, color: Color, startDegree: Double, endDegree: Double) {
+ self.value = value
+ self.color = color
+
+ self.startDegree = startDegree
+ self.endDegree = endDegree
+ }
+}
+
diff --git a/XcodeCleaner/PieChart/PieChartSliceFactory.swift b/XcodeCleaner/PieChart/PieChartSliceFactory.swift
new file mode 100644
index 0000000..da3802f
--- /dev/null
+++ b/XcodeCleaner/PieChart/PieChartSliceFactory.swift
@@ -0,0 +1,55 @@
+//
+// PieChartSliceFactory.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+class PieChartSliceFactory {
+ func createPieChartSlicesFromItems(items: [PieChartItemModel], maxShapeDegree: Double, initialDegree: Double = 0.0) -> [PieChartSliceModel] {
+ var slices: [PieChartSliceModel] = []
+ var previousSliceEndDegree = initialDegree
+
+ let maxSumSliceValue = items.reduce(0) { $0 + $1.value }
+
+ for itemIndex in 0 ..< items.count {
+ let item = items[itemIndex]
+
+ let sliceStartDegree = previousSliceEndDegree
+ let proportionalValue = item.value / maxSumSliceValue
+
+ let sliceEndDegree = sliceStartDegree + proportionalValue * maxShapeDegree
+
+ var subslices: [PieChartSubSliceModel] = []
+ if item.subItems.count > 0 {
+ let sliceIndex = itemIndex + 1
+ subslices = createPieChartSubSlicesFromItems(items: item.subItems, initialDegree: previousSliceEndDegree, maxShapeDegree: sliceEndDegree, withIndex: sliceIndex)
+ }
+ let slice = PieChartSliceModel(value: item.value, color: item.color, startDegree: sliceStartDegree, endDegree: sliceEndDegree, subSlices: subslices)
+ slices.append(slice)
+ previousSliceEndDegree = sliceEndDegree
+ }
+ return slices
+ }
+ func createPieChartSubSlicesFromItems(items: [PieChartItemModel], initialDegree: Double, maxShapeDegree: Double, withIndex sliceIndex: Int = 1) -> [PieChartSubSliceModel] {
+ var subSlices: [PieChartSubSliceModel] = []
+ var previousSliceEndDegree = initialDegree
+
+ let maxSumSliceValue = items.reduce(0) { $0 + $1.value }
+
+ for itemIndex in 0 ..< items.count {
+ let item = items[itemIndex]
+
+ let sliceStartDegree = previousSliceEndDegree
+ let proportionalValue = item.value / maxSumSliceValue
+
+ let sliceEndDegree = sliceStartDegree + proportionalValue * maxShapeDegree / Double(sliceIndex)
+
+ let slice = PieChartSubSliceModel(value: item.value, color: item.color, startDegree: sliceStartDegree, endDegree: sliceEndDegree)
+ subSlices.append(slice)
+ previousSliceEndDegree = sliceEndDegree
+ }
+ return subSlices
+ }
+}
diff --git a/XcodeCleaner/PieChart/Protocols/PieChartAnySliceProtocol.swift b/XcodeCleaner/PieChart/Protocols/PieChartAnySliceProtocol.swift
new file mode 100644
index 0000000..44ac5c2
--- /dev/null
+++ b/XcodeCleaner/PieChart/Protocols/PieChartAnySliceProtocol.swift
@@ -0,0 +1,18 @@
+//
+// PieChartAnySliceProtocol.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+protocol PieChartAnySliceProtocol {
+ var value: Double { get set }
+ var color: Color { get set }
+
+ var startDegree: Double { get set }
+ var endDegree: Double { get set }
+}
+
diff --git a/XcodeCleaner/PieChart/Shapes/PieChartSliceShape.swift b/XcodeCleaner/PieChart/Shapes/PieChartSliceShape.swift
new file mode 100644
index 0000000..c8c2f21
--- /dev/null
+++ b/XcodeCleaner/PieChart/Shapes/PieChartSliceShape.swift
@@ -0,0 +1,42 @@
+//
+// PieChartSliceShape.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartSliceShape: Shape {
+ var startAngle: Angle
+ var endAngle: Angle
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let center = CGPoint(x: rect.midX, y: rect.midY)
+ let size = min(rect.width / 2, rect.height / 2)
+
+ let radius = size
+
+ path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
+ path.addLine(to: center)
+
+ path.closeSubpath()
+
+ return path
+ }
+}
+
+struct PieChartSliceShape_Previews: PreviewProvider {
+ static var previews: some View {
+ let startAngle = Angle(degrees: 0)
+ let endAngle = Angle(degrees: 90)
+
+ return PieChartSliceShape(startAngle: startAngle, endAngle: endAngle)
+ .fill()
+ .foregroundColor(.orange)
+ .frame(width: 150, height: 150)
+ }
+}
+
diff --git a/XcodeCleaner/PieChart/Shapes/PieChartSubSliceShape.swift b/XcodeCleaner/PieChart/Shapes/PieChartSubSliceShape.swift
new file mode 100644
index 0000000..787273e
--- /dev/null
+++ b/XcodeCleaner/PieChart/Shapes/PieChartSubSliceShape.swift
@@ -0,0 +1,41 @@
+//
+// PieChartSubSliceShape.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartSubSliceShape: Shape {
+ var startAngle: Angle
+ var endAngle: Angle
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let center = CGPoint(x: rect.midX, y: rect.midY)
+ let size = min(rect.width / 2, rect.height / 2)
+
+ let firstRadius = size
+ let secondRadius = size / 2
+
+ path.addArc(center: center, radius: firstRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
+ path.addArc(center: center, radius: secondRadius, startAngle: endAngle, endAngle: startAngle, clockwise: true)
+ path.closeSubpath()
+
+ return path
+ }
+}
+
+struct PieChartSubSliceShape_Previews: PreviewProvider {
+ static var previews: some View {
+ let startAngle = Angle(degrees: 0)
+ let endAngle = Angle(degrees: 90)
+
+ return PieChartSubSliceShape(startAngle: startAngle, endAngle: endAngle)
+ .fill()
+ .foregroundColor(.orange)
+ .frame(width: 150, height: 150)
+ }
+}
diff --git a/XcodeCleaner/PieChart/Views/PieChartSliceView.swift b/XcodeCleaner/PieChart/Views/PieChartSliceView.swift
new file mode 100644
index 0000000..9b570b8
--- /dev/null
+++ b/XcodeCleaner/PieChart/Views/PieChartSliceView.swift
@@ -0,0 +1,57 @@
+//
+// PieChartSliceView.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartSliceView: View {
+ var rect: CGRect
+
+ let slice: PieChartSliceModel
+ let sliceSeparatorColor: Color
+
+ @State var isVisible: Bool = false
+
+ var body: some View {
+ let startAngle = Angle(degrees: slice.startDegree)
+ let endAngle = Angle(degrees: slice.endDegree)
+
+ let sliceShape = PieChartSliceShape(startAngle: startAngle, endAngle: endAngle)
+
+ return Group { sliceShape
+ .fill()
+ .overlay(sliceShape.stroke(sliceSeparatorColor, lineWidth: 2))
+ .foregroundColor(slice.color)
+ .scaleEffect(isVisible ? 1: 0)
+ .animation(Animation.easeIn)
+ .onAppear {
+ self.isVisible.toggle()
+ }
+ if self.slice.subSlices.count > 0 {
+ GeometryReader { geometryReader in
+ ForEach(0 ..< self.slice.subSlices.count, id: \.self) { subSliceIndex in
+ PieChartSubSliceView(rect: geometryReader.frame(in: .local), subSlice: self.slice.subSlices[subSliceIndex], sliceSeparatorColor: self.sliceSeparatorColor)
+ .padding(0 - min(geometryReader.size.width, geometryReader.size.height) / 2)
+ }
+ }
+ }
+ }
+ }
+}
+
+struct PieSliceView_Previews: PreviewProvider {
+ static var previews: some View {
+ let pieSlice = PieChartSliceModel(value: .zero, color: .orange, startDegree: 50, endDegree: 130)
+ let sliceSeparatorColor = Color.black
+
+ return GeometryReader { geometryReader in
+ PieChartSliceView(rect: geometryReader.frame(in: .local), slice: pieSlice, sliceSeparatorColor: sliceSeparatorColor)
+ }
+ .frame(width: 150, height: 150)
+ }
+}
+
diff --git a/XcodeCleaner/PieChart/Views/PieChartSubSliceView.swift b/XcodeCleaner/PieChart/Views/PieChartSubSliceView.swift
new file mode 100644
index 0000000..c0d1883
--- /dev/null
+++ b/XcodeCleaner/PieChart/Views/PieChartSubSliceView.swift
@@ -0,0 +1,48 @@
+//
+// PieChartSubSliceView.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartSubSliceView: View {
+ var rect: CGRect
+
+ let subSlice: PieChartSubSliceModel
+ let sliceSeparatorColor: Color
+
+ @State var isVisible: Bool = false
+
+ var body: some View {
+ let startAngle = Angle(degrees: subSlice.startDegree)
+ let endAngle = Angle(degrees: subSlice.endDegree)
+
+ let sliceShape = PieChartSubSliceShape(startAngle: startAngle, endAngle: endAngle)
+
+ return Group { sliceShape
+ .fill()
+ .overlay(sliceShape.stroke(sliceSeparatorColor, lineWidth: 2))
+ .foregroundColor(subSlice.color)
+ .scaleEffect(isVisible ? 1: 0)
+ .animation(Animation.easeIn)
+ .onAppear {
+ self.isVisible.toggle()
+ }
+ }
+ }
+}
+
+struct PieChartSubSliceView_Previews: PreviewProvider {
+ static var previews: some View {
+ let pieSlice = PieChartSubSliceModel(value: .zero, color: .orange, startDegree: 50, endDegree: 130)
+ let sliceSeparatorColor = Color.black
+
+ return GeometryReader { geometryReader in
+ PieChartSubSliceView(rect: geometryReader.frame(in: .local), subSlice: pieSlice, sliceSeparatorColor: sliceSeparatorColor)
+ }
+ .frame(width: 150, height: 150)
+ }
+}
diff --git a/XcodeCleaner/PieChart/Views/PieChartView.swift b/XcodeCleaner/PieChart/Views/PieChartView.swift
new file mode 100644
index 0000000..cc21a20
--- /dev/null
+++ b/XcodeCleaner/PieChart/Views/PieChartView.swift
@@ -0,0 +1,69 @@
+//
+// PieChartView.swift
+// PieChartSwiftUI
+//
+// Created by Kirill Pustovalov on 28.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+public struct PieChartView: View {
+ @ObservedObject var items: PCItems
+ var sliceSeparatorColor: Color
+
+ public init(items: PCItems, sliceSeparatorColor: Color = .white) {
+ self.items = items
+ self.sliceSeparatorColor = sliceSeparatorColor
+ }
+
+ public var body: some View {
+ let sliceFactory = PieChartSliceFactory()
+ let circleShapeMaxDegree = 360.0
+
+ let slices = sliceFactory.createPieChartSlicesFromItems(items: items.items, maxShapeDegree: circleShapeMaxDegree)
+
+ return Group {
+ GeometryReader { geometryReader in
+ if slices.count == 1 {
+ Circle()
+ .fill(slices.first!.color)
+ .overlay(Circle().stroke(self.sliceSeparatorColor, lineWidth: 1))
+ .frame(width: geometryReader.size.width, height: geometryReader.size.height)
+
+ ForEach(0 ..< slices.first!.subSlices.count, id: \.self) { subSliceIndex in
+ PieChartSubSliceView(rect: geometryReader.frame(in: .local), subSlice: (slices.first!.subSlices[subSliceIndex]), sliceSeparatorColor: self.sliceSeparatorColor)
+ }
+ } else {
+ ForEach(0 ..< slices.count, id: \.self) { sliceIndex in
+ PieChartSliceView(rect: geometryReader.frame(in: .local), slice: slices[sliceIndex], sliceSeparatorColor: self.sliceSeparatorColor)
+ }
+ }
+ }
+ }
+ }
+}
+
+struct PieChartView_Previews: PreviewProvider {
+ static var previews: some View {
+ let pieChartItemOne = PieChartItemModel(value: 25, color: .pink)
+ let pieChartItemTwo = PieChartItemModel(value: 25, color: .orange)
+ let pieChartItemThree = PieChartItemModel(value: 25, color: .red)
+
+ let items = PCItems(items: [pieChartItemOne, pieChartItemTwo, pieChartItemThree])
+ let itemsFromData = PCItems(data: [25, 50, 25], chartColor: .black)
+
+ return VStack {
+ Spacer()
+
+ PieChartView(items: items, sliceSeparatorColor: .black)
+ .frame(width: 100, height: 100, alignment: .center)
+ Spacer()
+
+ PieChartView(items: itemsFromData, sliceSeparatorColor: .white)
+ .frame(width: 100, height: 100, alignment: .center)
+ Spacer()
+
+ }
+ }
+}
diff --git a/XcodeCleaner/Preview Content/Preview Assets.xcassets/Contents.json b/XcodeCleaner/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/XcodeCleaner/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/XcodeCleaner/Protocols/DirectoryListViewModelProtocol.swift b/XcodeCleaner/Protocols/DirectoryListViewModelProtocol.swift
new file mode 100644
index 0000000..928ddc2
--- /dev/null
+++ b/XcodeCleaner/Protocols/DirectoryListViewModelProtocol.swift
@@ -0,0 +1,18 @@
+//
+// DirectoryListViewModelProtocol.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+protocol DirectoryListViewModelProtocol {
+ var directoryName: String { get set }
+ var totalSize: Int64 { get set }
+ var directories: [DirectoryModel] { get set }
+ var circleColor: Color { get set }
+
+ mutating func calculateTotalSize()
+}
diff --git a/XcodeCleaner/Protocols/PieChartViewModelProtocol.swift b/XcodeCleaner/Protocols/PieChartViewModelProtocol.swift
new file mode 100644
index 0000000..7611bb9
--- /dev/null
+++ b/XcodeCleaner/Protocols/PieChartViewModelProtocol.swift
@@ -0,0 +1,16 @@
+//
+// PieChartViewModelProtocol.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 07.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+protocol PieChartViewModelProtocol {
+ var items: [PieChartItemModel] { get set }
+
+ mutating func createItems(derivedData: [DirectoryModel], deviceSupport: [DirectoryModel], archives: [DirectoryModel])
+ func getPCItems() -> PCItems
+}
diff --git a/XcodeCleaner/Protocols/StatisticViewModelProtocol.swift b/XcodeCleaner/Protocols/StatisticViewModelProtocol.swift
new file mode 100644
index 0000000..2aa50b9
--- /dev/null
+++ b/XcodeCleaner/Protocols/StatisticViewModelProtocol.swift
@@ -0,0 +1,16 @@
+//
+// StatisticViewModelProtocol.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+protocol StatisticViewModelProtocol {
+ var statistic: StatisticModel { get set }
+
+ func getLastDate() -> String
+ func getTotalSize() -> String
+}
diff --git a/XcodeCleaner/Protocols/ViewModelProtocol.swift b/XcodeCleaner/Protocols/ViewModelProtocol.swift
new file mode 100644
index 0000000..73d2f91
--- /dev/null
+++ b/XcodeCleaner/Protocols/ViewModelProtocol.swift
@@ -0,0 +1,36 @@
+//
+// ViewModelProtocol.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+protocol ViewModelProtocol {
+ var isScanStarted: Bool { get set }
+
+ var directoriesCount: Int { get set }
+ var analyzedDirectoriesCount: Int { get set }
+ var totalSize: Int64 { get set }
+
+ var derivedData: [DirectoryModel] { get set }
+ var deviceSupport: [DirectoryModel] { get set }
+ var archives: [DirectoryModel] { get set }
+
+ var scanProgress: Double { get }
+
+ var isReadyToBeCleaned: Bool { get set }
+
+ var isAlertPresented: Bool { get set }
+
+ func startScan()
+ func calculateSize(ofDirectory: inout [DirectoryModel], subDirectories: [String], type: DirectoryType)
+ func getViewModelForItemList(forType type: DirectoryType) -> DirectoryListViewModelProtocol
+ func cleanBeforeScan()
+ func getViewModelForPieChart() -> PieChartViewModelProtocol
+ func getViewModelForStatistic() -> StatisticViewModelProtocol
+
+ func startClean()
+}
diff --git a/XcodeCleaner/ViewModels/DirectoryListViewModel.swift b/XcodeCleaner/ViewModels/DirectoryListViewModel.swift
new file mode 100644
index 0000000..fb316fb
--- /dev/null
+++ b/XcodeCleaner/ViewModels/DirectoryListViewModel.swift
@@ -0,0 +1,26 @@
+//
+// DirectoryListViewModel.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct DirectoryListViewModel: DirectoryListViewModelProtocol {
+ var directoryName: String
+ var directories: [DirectoryModel]
+
+ var totalSize: Int64 = 0
+ var circleColor: Color
+
+ mutating func calculateTotalSize() {
+ var size: Int64 = 0
+
+ directories.forEach { directory in
+ size += directory.size
+ }
+ totalSize = size
+ }
+}
diff --git a/XcodeCleaner/ViewModels/PieChartViewModel.swift b/XcodeCleaner/ViewModels/PieChartViewModel.swift
new file mode 100644
index 0000000..933a1c7
--- /dev/null
+++ b/XcodeCleaner/ViewModels/PieChartViewModel.swift
@@ -0,0 +1,68 @@
+//
+// PieChartViewModel.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 07.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct PieChartViewModel: PieChartViewModelProtocol {
+ var items: [PieChartItemModel] = []
+
+ mutating func createItems(derivedData: [DirectoryModel], deviceSupport: [DirectoryModel], archives: [DirectoryModel]) {
+ var derivedDataSize: Int64 = 0
+ derivedData.forEach {
+ derivedDataSize += $0.size
+ }
+
+ var deviceSupportSize: Int64 = 0
+ deviceSupport.forEach {
+ deviceSupportSize += $0.size
+ }
+
+ var archivesSize: Int64 = 0
+ archives.forEach {
+ archivesSize += $0.size
+ }
+
+ var derivedData: PieChartItemModel
+ var deviceSupport: PieChartItemModel
+ var archives: PieChartItemModel
+
+ if derivedDataSize == 0 && deviceSupportSize == 0 && archivesSize == 0 {
+ let defaultValue = 1.0
+
+ derivedData = PieChartItemModel(value: defaultValue, color: .pink)
+ deviceSupport = PieChartItemModel(value: defaultValue, color: Color(.cyan))
+ archives = PieChartItemModel(value: defaultValue, color: .orange)
+
+ let items = [archives, deviceSupport, derivedData]
+ self.items = items
+ } else {
+ derivedData = PieChartItemModel(value: Double(derivedDataSize), color: .pink)
+ deviceSupport = PieChartItemModel(value: Double(deviceSupportSize), color: Color(.cyan))
+ archives = PieChartItemModel(value: Double(archivesSize), color: .orange)
+
+ let items = [archives, deviceSupport, derivedData]
+
+ var normalizedItems: [PieChartItemModel] = []
+
+ items.forEach {
+ if $0.value != 0 {
+ normalizedItems.append($0)
+ }
+ }
+ self.items = normalizedItems
+ }
+ }
+ func getPCItems() -> PCItems {
+ let pcItems = PCItems(items: [])
+
+ items.forEach { item in
+ pcItems.items.append(item)
+ }
+ return pcItems
+ }
+}
diff --git a/XcodeCleaner/ViewModels/StatisticViewModel.swift b/XcodeCleaner/ViewModels/StatisticViewModel.swift
new file mode 100644
index 0000000..df58d19
--- /dev/null
+++ b/XcodeCleaner/ViewModels/StatisticViewModel.swift
@@ -0,0 +1,22 @@
+//
+// StatisticViewModel.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 06.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+
+struct StatisticViewModel: StatisticViewModelProtocol {
+ var statistic: StatisticModel
+
+ func getLastDate() -> String {
+ guard let date = statistic.lastTimeCleaned else { return "none" }
+ return DateManager.getStringDate(date: date)
+ }
+ func getTotalSize() -> String {
+ guard let size = statistic.totalCleaned else { return "none" }
+ return BytesToStringFormatter.format(size: size)
+ }
+}
diff --git a/XcodeCleaner/ViewModels/ViewModel.swift b/XcodeCleaner/ViewModels/ViewModel.swift
new file mode 100644
index 0000000..5ccf4cb
--- /dev/null
+++ b/XcodeCleaner/ViewModels/ViewModel.swift
@@ -0,0 +1,145 @@
+//
+// ViewModel.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 05.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import Foundation
+import SwiftUI
+
+class ViewModel: ObservableObject, ViewModelProtocol {
+ private let directoryManager = DirectoryManager()
+
+ @Published var isScanStarted = false
+
+ @Published var directoriesCount: Int = 0
+ @Published var analyzedDirectoriesCount: Int = 0
+ @Published var totalSize: Int64 = 0
+
+ @Published var derivedData: [DirectoryModel] = []
+ @Published var deviceSupport: [DirectoryModel] = []
+ @Published var archives: [DirectoryModel] = []
+
+ @Published var isReadyToBeCleaned = false
+
+ @Published var isAlertPresented = false
+
+ var scanProgress: Double {
+ directoriesCount == 0 ? 0: Double(analyzedDirectoriesCount) / Double(directoriesCount)
+ }
+ func startScan() {
+ guard !isScanStarted else { return }
+ cleanBeforeScan()
+
+ isScanStarted.toggle()
+
+ let derivedDataDirectories = directoryManager.getSubDirectoriesForPath(path: directoryManager.getDerivedDataPath())
+ directoriesCount += derivedDataDirectories.count
+
+ let deviceSupportDirectories = directoryManager.getSubDirectoriesForPath(path: directoryManager.getDeviceSupportPath())
+ directoriesCount += deviceSupportDirectories.count
+
+ let archivesDirectories = directoryManager.getSubDirectoriesForPath(path: directoryManager.getArchivesPath())
+ directoriesCount += archivesDirectories.count
+
+ DispatchQueue.global(qos: .userInitiated).async {
+ self.calculateSize(ofDirectory: &self.derivedData, subDirectories: derivedDataDirectories, type: .derivedData)
+ self.calculateSize(ofDirectory: &self.deviceSupport, subDirectories: deviceSupportDirectories, type: .deviceSupport)
+ self.calculateSize(ofDirectory: &self.archives, subDirectories: archivesDirectories, type: .archives)
+
+ self.isScanStarted.toggle()
+ self.isReadyToBeCleaned.toggle()
+
+ DispatchQueue.main.async {
+ self.objectWillChange.send()
+ }
+ }
+ }
+ func calculateSize(ofDirectory: inout [DirectoryModel], subDirectories: [String], type: DirectoryType) {
+ ofDirectory = subDirectories.map { directory in
+ let normalizedDirectoryPathForDisplay = self.directoryManager.normalizeDirectoryPathForDisplay(directory: directory, forType: type)
+ let normalizedDirectoryPath = self.directoryManager.normalizeDirectoryPath(directory: directory)
+
+
+ let directorySize = self.directoryManager.getDirectorySize(path: normalizedDirectoryPath) {
+ self.analyzedDirectoriesCount += 1
+ }
+
+ DispatchQueue.main.async {
+ self.totalSize += directorySize
+ self.objectWillChange.send()
+ }
+
+ return DirectoryModel(name: normalizedDirectoryPathForDisplay, size: directorySize)
+ }
+ }
+ func getViewModelForItemList(forType type: DirectoryType) -> DirectoryListViewModelProtocol {
+ var directoryName: String
+ var directories: [DirectoryModel]
+ var circleColor: Color
+
+ switch type {
+ case .derivedData:
+ directories = derivedData
+ directoryName = "Derived Data"
+ circleColor = .pink
+ case .deviceSupport:
+ directories = deviceSupport
+ directoryName = "Device Support"
+ circleColor = Color(.cyan)
+ case .archives:
+ directories = archives
+ directoryName = "Archives"
+ circleColor = .orange
+ }
+
+ var viewModel = DirectoryListViewModel(directoryName: directoryName, directories: directories, circleColor: circleColor)
+ viewModel.calculateTotalSize()
+
+ return viewModel
+ }
+ func cleanBeforeScan() {
+ directoriesCount = 0
+ analyzedDirectoriesCount = 0
+ totalSize = 0
+
+ derivedData.removeAll()
+ deviceSupport.removeAll()
+ archives.removeAll()
+
+ objectWillChange.send()
+ }
+ func getViewModelForPieChart() -> PieChartViewModelProtocol {
+ var viewModel = PieChartViewModel()
+ viewModel.createItems(derivedData: derivedData, deviceSupport: deviceSupport, archives: archives)
+
+ return viewModel
+ }
+ //add chosen type
+ func startClean() {
+ DispatchQueue.global(qos: .userInitiated).async {
+ self.directoryManager.cleanDirectory(forType: .derivedData)
+ self.directoryManager.cleanDirectory(forType: .deviceSupport)
+ self.directoryManager.cleanDirectory(forType: .archives)
+
+ DispatchQueue.main.async {
+ let statistic = StatisticModel(totalCleaned: self.totalSize, lastTimeCleaned: DateManager.getCurrentDate())
+
+ CoreDataManager.shared.saveStatistic(statistic: statistic)
+
+ self.isAlertPresented.toggle()
+ self.objectWillChange.send()
+
+ self.isReadyToBeCleaned.toggle()
+ }
+ }
+ }
+ func getViewModelForStatistic() -> StatisticViewModelProtocol {
+ let statistic = CoreDataManager.shared.getStatistic()
+ let viewModel = StatisticViewModel(statistic: statistic)
+
+ return viewModel
+ }
+}
diff --git a/XcodeCleaner/Views/BodyViews/BodyView.swift b/XcodeCleaner/Views/BodyViews/BodyView.swift
new file mode 100644
index 0000000..bab24e5
--- /dev/null
+++ b/XcodeCleaner/Views/BodyViews/BodyView.swift
@@ -0,0 +1,37 @@
+//
+// BodyView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 02.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct BodyView: View {
+ var viewModel: PieChartViewModelProtocol
+
+ var body: some View {
+ HStack {
+ DirectoryListView()
+ Spacer()
+ GeometryReader { geometryReader in
+ VStack {
+ PieChartView(items: self.viewModel.getPCItems(), sliceSeparatorColor: .black)
+ .frame(width: geometryReader.size.width / 2, height: geometryReader.size.height / 2)
+ .animation(.easeIn(duration: 1.0))
+ }
+ }
+ }
+ }
+}
+
+struct MainView_Previews: PreviewProvider {
+ static var previews: some View {
+ var viewModel = PieChartViewModel()
+ viewModel.createItems(derivedData: [DirectoryModel(name: "Test", size: 2)], deviceSupport: [DirectoryModel(name: "Test", size: 2)], archives: [DirectoryModel(name: "Test", size: 2)])
+
+ return BodyView(viewModel: viewModel)
+ .environmentObject(ViewModel())
+ }
+}
diff --git a/XcodeCleaner/Views/BodyViews/DirectoryListView.swift b/XcodeCleaner/Views/BodyViews/DirectoryListView.swift
new file mode 100644
index 0000000..b800fdd
--- /dev/null
+++ b/XcodeCleaner/Views/BodyViews/DirectoryListView.swift
@@ -0,0 +1,37 @@
+//
+// DirectoryListView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 24.06.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct DirectoryListView: View {
+ @EnvironmentObject var viewModel: ViewModel
+ var body: some View {
+ VStack(alignment: .leading) {
+ Spacer()
+
+ DropDownView(viewModel: viewModel.getViewModelForItemList(forType: .derivedData))
+ .padding()
+ Spacer()
+
+ DropDownView(viewModel: viewModel.getViewModelForItemList(forType: .deviceSupport))
+ .padding()
+ Spacer()
+
+ DropDownView(viewModel: viewModel.getViewModelForItemList(forType: .archives))
+ .padding()
+ Spacer()
+ }
+ }
+}
+
+struct ItemsListView_Previews: PreviewProvider {
+ static var previews: some View {
+ DirectoryListView()
+ .environmentObject(ViewModel())
+ }
+}
diff --git a/XcodeCleaner/Views/BodyViews/DropDownView.swift b/XcodeCleaner/Views/BodyViews/DropDownView.swift
new file mode 100644
index 0000000..2cfb0db
--- /dev/null
+++ b/XcodeCleaner/Views/BodyViews/DropDownView.swift
@@ -0,0 +1,67 @@
+//
+// DropDownView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 04.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct DropDownView: View {
+ @State var isExpanded = false
+ var viewModel: DirectoryListViewModelProtocol
+
+ var body: some View {
+ VStack(spacing: 5) {
+ HStack {
+ HStack {
+ Text("\(viewModel.directories.count == 0 ? "": isExpanded ? "▲": "▼") \(viewModel.directoryName)")
+ .font(.largeTitle)
+ .fontWeight(.heavy)
+
+ Text("\(BytesToStringFormatter.format(size: viewModel.totalSize))")
+ .foregroundColor(viewModel.circleColor)
+ .font(.largeTitle)
+ .fontWeight(.heavy)
+
+ Circle()
+ .fill(viewModel.circleColor)
+ .frame(width: 30, height: 30)
+ }
+ }.onTapGesture {
+ self.isExpanded.toggle()
+ }
+ if viewModel.directories.count > 0 {
+ if isExpanded {
+ VStack {
+ GeometryReader { geometryReader in
+ ScrollView {
+ VStack {
+ ForEach(0 ..< self.viewModel.directories.count) { index in
+ HStack {
+ Text(self.viewModel.directories[index].name)
+ .lineLimit(1)
+ Spacer()
+ Text("\(BytesToStringFormatter.format(size: self.viewModel.directories[index].size))")
+ .foregroundColor(self.viewModel.circleColor)
+ }
+ }
+ }
+ .padding(.top, geometryReader.size.height / 2)
+ }
+ .font(.title)
+ }
+ }
+ }
+ }
+ }
+ .animation(.easeIn(duration: 0.3))
+ }
+}
+
+struct DropDownView_Previews: PreviewProvider {
+ static var previews: some View {
+ DropDownView(viewModel: DirectoryListViewModel(directoryName: "Test", directories: [DirectoryModel(name: "/Url", size: 15)], circleColor: .green))
+ }
+}
diff --git a/XcodeCleaner/Views/ContentView.swift b/XcodeCleaner/Views/ContentView.swift
new file mode 100644
index 0000000..a92fa53
--- /dev/null
+++ b/XcodeCleaner/Views/ContentView.swift
@@ -0,0 +1,32 @@
+//
+// ContentView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 07.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct ContentView: View {
+ @EnvironmentObject var viewModel: ViewModel
+ var body: some View {
+ VStack {
+ BodyView(viewModel: viewModel.getViewModelForPieChart())
+ FooterView()
+ }
+ .alert(isPresented: $viewModel.isAlertPresented) {
+ Alert(title: Text("Clean finished"), message: Text("\(BytesToStringFormatter.format(size: viewModel.totalSize)) was successfully cleaned!"), dismissButton: .default(
+ Text("Ok"), action: {
+ self.viewModel.cleanBeforeScan()
+ }))}
+ .frame(minWidth: 800, minHeight: 550)
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ .environmentObject(ViewModel())
+ }
+}
diff --git a/XcodeCleaner/Views/FooterViews/DividerButtonView.swift b/XcodeCleaner/Views/FooterViews/DividerButtonView.swift
new file mode 100644
index 0000000..7817968
--- /dev/null
+++ b/XcodeCleaner/Views/FooterViews/DividerButtonView.swift
@@ -0,0 +1,32 @@
+//
+// DividerButtonView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 03.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct DividerButtonView: View {
+ var divider = VStack { Divider() }
+ @EnvironmentObject var viewModel: ViewModel
+
+ var body: some View {
+ HStack {
+ divider
+ Button("\(viewModel.isReadyToBeCleaned ? "Clean": "Scan")") {
+ self.viewModel.isReadyToBeCleaned ? self.viewModel.startClean(): self.viewModel.startScan()
+ }
+ .cornerRadius(25)
+ divider
+ }
+ }
+}
+
+struct DividerButtonView_Previews: PreviewProvider {
+ static var previews: some View {
+ DividerButtonView()
+ .environmentObject(ViewModel())
+ }
+}
diff --git a/XcodeCleaner/Views/FooterViews/FooterView.swift b/XcodeCleaner/Views/FooterViews/FooterView.swift
new file mode 100644
index 0000000..4663a88
--- /dev/null
+++ b/XcodeCleaner/Views/FooterViews/FooterView.swift
@@ -0,0 +1,42 @@
+//
+// FooterView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 02.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct FooterView: View {
+ @EnvironmentObject var viewModel: ViewModel
+
+ var body: some View {
+ return VStack {
+ DividerButtonView()
+ .offset(y: 16)
+
+ HStack {
+ ScanProgressView(progress: CGFloat(viewModel.scanProgress))
+ .padding(10)
+
+ Text("\(BytesToStringFormatter.format(size: viewModel.totalSize, allowedUnits: [.useGB]))")
+ .font(.title)
+ .fontWeight(.heavy)
+ .foregroundColor(.pink)
+ .frame(width: 100)
+ .lineLimit(1)
+
+ StatisticView(viewModel: viewModel.getViewModelForStatistic())
+ .padding(10)
+ }
+ }
+ }
+}
+
+struct FooterView_Previews: PreviewProvider {
+ static var previews: some View {
+ FooterView()
+ .environmentObject(ViewModel())
+ }
+}
diff --git a/XcodeCleaner/Views/FooterViews/ScanProgressView.swift b/XcodeCleaner/Views/FooterViews/ScanProgressView.swift
new file mode 100644
index 0000000..1bcae2c
--- /dev/null
+++ b/XcodeCleaner/Views/FooterViews/ScanProgressView.swift
@@ -0,0 +1,50 @@
+//
+// ScanProgressView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 03.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct ScanProgressView: View {
+ var progress: CGFloat = 0.0
+ var height: CGFloat = 8
+ var startPoint = CGPoint(x: 0, y: 0)
+
+ var body: some View {
+ GeometryReader { geometryReader in
+ ZStack {
+ Path { path in
+ path.move(to: self.startPoint)
+ path.addRoundedRect(
+ in: CGRect(x: self.startPoint.x, y: self.startPoint.y, width: geometryReader.size.width, height: self.height),
+ cornerSize: CGSize(width: self.height / 2 , height: self.height / 2),
+ style: .circular)
+ }
+
+ Path { path in
+ path.move(to: self.startPoint)
+ path.addRoundedRect(
+ in: CGRect(x: self.startPoint.x, y: self.startPoint.y, width: geometryReader.size.width * self.progress, height: self.height),
+ cornerSize: CGSize(width: self.height / 2, height: self.height / 2),
+ style: .circular)
+ }
+ .fill(LinearGradient(gradient: Gradient(colors: [Color(.cyan), .pink, .orange]), startPoint: .leading, endPoint: .trailing))
+ .animation(.easeInOut)
+ }
+ .frame(width: geometryReader.size.width, height: self.height)
+ }
+ .frame(height: self.height)
+ }
+}
+
+struct ScanProgressView_Previews: PreviewProvider {
+ static var previews: some View {
+ VStack {
+ ScanProgressView()
+ ScanProgressView(progress: 1.0)
+ }
+ }
+}
diff --git a/XcodeCleaner/Views/FooterViews/StatisticView.swift b/XcodeCleaner/Views/FooterViews/StatisticView.swift
new file mode 100644
index 0000000..c8123b9
--- /dev/null
+++ b/XcodeCleaner/Views/FooterViews/StatisticView.swift
@@ -0,0 +1,27 @@
+//
+// StatisticView.swift
+// XcodeCleaner
+//
+// Created by Kirill Pustovalov on 02.07.2020.
+// Copyright © 2020 Kirill Pustovalov. All rights reserved.
+//
+
+import SwiftUI
+
+struct StatisticView: View {
+ var viewModel: StatisticViewModelProtocol
+ var body: some View {
+ VStack(alignment: .trailing) {
+ Text("Total cleaned: \(viewModel.getTotalSize())")
+ Text("Last cleanup: \(viewModel.getLastDate())")
+ Text("Github: IrelDev")
+ }
+ .font(.footnote)
+ }
+}
+
+struct StatisticView_Previews: PreviewProvider {
+ static var previews: some View {
+ StatisticView(viewModel: StatisticViewModel(statistic: StatisticModel(totalCleaned: 250000, lastTimeCleaned: Date())))
+ }
+}
diff --git a/XcodeCleaner/XcodeCleaner.entitlements b/XcodeCleaner/XcodeCleaner.entitlements
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/XcodeCleaner/XcodeCleaner.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/XcodeCleaner/XcodeCleaner.xcdatamodeld/.xccurrentversion b/XcodeCleaner/XcodeCleaner.xcdatamodeld/.xccurrentversion
new file mode 100644
index 0000000..6384fd4
--- /dev/null
+++ b/XcodeCleaner/XcodeCleaner.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+
+
+
+
+ _XCCurrentVersionName
+ XcodeCleaner.xcdatamodel
+
+
diff --git a/XcodeCleaner/XcodeCleaner.xcdatamodeld/XcodeCleaner.xcdatamodel/contents b/XcodeCleaner/XcodeCleaner.xcdatamodeld/XcodeCleaner.xcdatamodel/contents
new file mode 100644
index 0000000..daf8bb4
--- /dev/null
+++ b/XcodeCleaner/XcodeCleaner.xcdatamodeld/XcodeCleaner.xcdatamodel/contents
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file