├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── Design ├── Asset Generator App Icon.sketch └── Asset Generator UI Design.sketch ├── LICENSE ├── README.md ├── XCAssetGenerator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── XCAssetGenerator.xccheckout └── xcuserdata │ └── pranavshah.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── XCAssetGenerator.xcscheme │ └── xcschememanagement.plist ├── XCAssetGenerator ├── AppDelegate.swift ├── AssetAttribute.swift ├── AssetCatalog.swift ├── AssetDiff.swift ├── AssetGenerationController.swift ├── AssetGenerator.swift ├── AssetGeneratorInputValidator.swift ├── AssetGeneratorWindowController.swift ├── AssetType.swift ├── AssetWindowViewModel.swift ├── Base.lproj │ └── Main.storyboard ├── BookmarkResolver.swift ├── CABasicAnimation+Extensions.swift ├── DropView.swift ├── FileSystemHelper.swift ├── FileSystemObserver.h ├── FileSystemObserver.m ├── FileSystemObserverSignal.swift ├── ImageSelection.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── iconArrow.imageset │ │ ├── Contents.json │ │ ├── iconArrow.png │ │ └── iconArrow@2x.png │ ├── uiBottomBar.imageset │ │ ├── uiBottomBar.png │ │ └── uiBottomBar@2x.png │ ├── uiBottombar.imageset │ │ └── Contents.json │ └── uiWell.imageset │ │ ├── Contents.json │ │ ├── uiWell.png │ │ └── uiWell@2x.png ├── ImagesDropView.xib ├── ImagesDropViewController.swift ├── ImagesGroupViewModel.swift ├── Info.plist ├── Keyword Constants.swift ├── Localizable.strings ├── NSColor+Extensions.swift ├── NSImage+Extensions.swift ├── NSLayoutConstraint+Extensions.swift ├── Optionals+Operators.swift ├── PathQuery.swift ├── Pluralize.swift ├── ProgressIndicationViewModel.swift ├── ProgressLineView.swift ├── ProgressViewController.swift ├── ProjectDropView.xib ├── ProjectDropViewController.swift ├── ProjectSelectionViewModel.swift ├── ProjectSelector.swift ├── RoundedDropView.swift ├── Serializable.swift ├── StatusCrafter.swift ├── Storage.swift ├── StringExtension.swift ├── Validator.swift ├── XCAssetGenerator-Bridging-Header.h ├── XCAssetsJSON.swift ├── XCProject.swift ├── XcodeFileValidator.swift └── main.swift └── XCAssetGeneratorTests ├── Info.plist └── XCAssetGeneratorTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode # 2 | ######### 3 | *.xcuserdata 4 | *.xcuserdatad/ 5 | *.xcworkspace 6 | !default.xcworkspace 7 | *.pbxuser 8 | DerivedData 9 | build/ 10 | *.ipa 11 | *.moved-aside 12 | *.mode1v3 13 | *.mode2v3 14 | *.perspectivev3 15 | 16 | # OS generated files # 17 | ###################### 18 | .DS_Store 19 | .DS_Store? 20 | .Spotlight-V100 21 | .Trashes 22 | thumbs.db 23 | Thumbs.db -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveCocoa/ReactiveCocoa" "swift-development" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "robrix/Box" "1.2.2" 2 | github "antitypical/Result" "0.4.3" 3 | github "ReactiveCocoa/ReactiveCocoa" "12110113b02b22c7d3a1e7aa423d76340eb19157" 4 | -------------------------------------------------------------------------------- /Design/Asset Generator App Icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/Design/Asset Generator App Icon.sketch -------------------------------------------------------------------------------- /Design/Asset Generator UI Design.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/Design/Asset Generator UI Design.sketch -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Sourcebits 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What Is It? 2 | 3 | Asset Generator is a Mac app which takes design assets and adds them to your Xcode project's [asset catalog](https://developer.apple.com/library/ios/recipes/xcode_help-image_catalog-1.0/Recipe.html#//apple_ref/doc/uid/TP40013303-CH1-SW1). Its goal is to bridge the gap between designers and developers: the former can pull project repositories and update assets (in a hopefully intuitive way) without bothering the latter. 4 | 5 | # How to Use? 6 | 7 | Simply drag a folder with images or multiple image files onto the left well, drag an Xcodeproject file onto the right well and hit **Build**. 8 | 9 | ![Asset Generator Screenshot](http://imgur.com/SPz0i7K.jpg "Asset Generator Screenshot") 10 | 11 | # Download 12 | 13 | [Grab the latest build here.](https://github.com/sourcebitsllc/Asset-Generator-Mac/releases) 14 | 15 | # Features 16 | 17 | - Automatically detects asset types based on [keywords](#keywords) and image metadata. 18 | - Supports iOS and Mac assets (including icons, launch images, Spotlight and settings assets, and more). 19 | - Merges new assets with existing catalog data so you can incrementally build assets as you go in a safe manner. 20 | - Preserves content created through Xcode such as slicing information and size classes. 21 | - Dynamically tracks location of both source and destinations when moved. 22 | 23 | # Keywords 24 | 25 | Keywords are tags added to the image filename that help the app determine the proper information of the image. The good news is, if you follow Apple's naming convention you're already done! If not, it's very simple. Asset Generator keywords take the following form: 26 | `.` 27 | 28 | where: 29 | 30 | - `` is either _@2x_, _@3x_ or blank for _@1x_. 31 | - `` specifies the target device which can be either _~iphone_, _~ipad_, _~mac_, or blank for universal. 32 | - `` are the support image extensions which are _png_, _jpg_ and _jpeg_. 33 | 34 | ### App Icons 35 | 36 | - For iOS icons, the `` must start with either _**"AppIcon"**_ or _**"Icon"**_ and Asset Generator takes care of the rest. 37 | - Mac OS icons must start with ***"icon_"*** and must follow Apple's naming convention [found here](https://developer.apple.com/library/mac/documentation/UserExperience/Conceptual/OSXHIGuidelines/Designing.html). 38 | - More information about iOS icons can be found [here](https://developer.apple.com/library/ios/qa/qa1686/_index.html) and [here](https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html#//apple_ref/doc/uid/TP40006556-CH27-SW2). 39 | 40 | ### Launch Images 41 | 42 | - For launch images, the `` must start with either _**"Default"**_ or _**"LaunchImage"**_ and Asset Generator takes care of the rest. 43 | - More information about launch images can be found [here](https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/MobileHIG/LaunchImages.html#//apple_ref/doc/uid/TP40006556-CH22-SW1) and [here](https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html#//apple_ref/doc/uid/TP40006556-CH27-SW2). 44 | 45 | ### General Images 46 | 47 | For all other assets, you need to provide all the keywords mentioned above and the app will parse the data as such. For example `Button@3x~iphone.png` is a @3x iPhone button image and `Spinner@2x.png` is a @2x universal spinner image. 48 | 49 | # Notes 50 | 51 | - Asset Generator does **not** scale or compress your assets. You need to prepare all different dimensions yourself. 52 | - You need to have an asset catalog in your project to use Asset Generator. 53 | - If you have multiple catalogs in your project, Asset Generator will use the first one (alphabetically). 54 | 55 | # How to Build 56 | 57 | 1. Clone the repo into your machine. 58 | 2. To build the project dependencies, install [Carthage](http://github.com/Carthage/Carthage/) with [Homebrew](http://brew.sh/) as follows: 59 | 60 | ```bash 61 | $ brew update 62 | $ brew install carthage 63 | ``` 64 | 3. Run `carthage update` to setup the dependencies. 65 | 66 | # About 67 | 68 | Asset Generator is a collaboration between [Bader Alabdulrazzaq](https://twitter.com/BHAlRezzaga), iOS and Mac OS engineer who graciously greeted Sourcebits for an internship in late 2014, and Sourcebits' Chief Innovation Officer [Piotr Gajos](https://twitter.com/Pe8er). App icon was designed by Sourcebits' senior interaction designer, Rick Patrick. 69 | 70 | The app was designed in Sketch and coded in Swift. 71 | 72 | We would like to thank all Sourcebits developers and designers who helped us with feature ideas and relentless bug reports. 73 | 74 | # License 75 | 76 | Asset Generator is released under the MIT license. See [LICENSE](LICENSE) for details. 77 | -------------------------------------------------------------------------------- /XCAssetGenerator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1E17A3A11ACF637700785DB2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E17A3A01ACF637700785DB2 /* Localizable.strings */; }; 11 | 1E2D9EE31B26620A009A2745 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ED9A1461B1F983B000A41B9 /* Main.storyboard */; }; 12 | 1E48EF431B31861E00A63FBF /* AssetAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E48EF421B31861E00A63FBF /* AssetAttribute.swift */; }; 13 | 1E7602D21B4E47140096DEF3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7602D11B4E47140096DEF3 /* Storage.swift */; }; 14 | 1E8D12F01B4363ED004A3FCB /* FileSystemObserverSignal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8D12EF1B4363ED004A3FCB /* FileSystemObserverSignal.swift */; }; 15 | 1E946FD51B46007600133459 /* FileSystemObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E946FD41B46007600133459 /* FileSystemObserver.m */; }; 16 | 1E95006C1B44F1F200D7632D /* ImagesDropView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1E95006B1B44F1F200D7632D /* ImagesDropView.xib */; }; 17 | 1EAC3B1F1B3C3EA600B75DD6 /* CABasicAnimation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC3B1E1B3C3EA600B75DD6 /* CABasicAnimation+Extensions.swift */; }; 18 | 1EAC3B211B3CC7D300B75DD6 /* ImageSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC3B201B3CC7D300B75DD6 /* ImageSelection.swift */; }; 19 | 1EC4854C1B44DCE300784756 /* ProjectDropView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1EC4854B1B44DCE300784756 /* ProjectDropView.xib */; }; 20 | 1ED096DC1B2D53D8005337A9 /* AssetDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED096DB1B2D53D8005337A9 /* AssetDiff.swift */; }; 21 | 1ED885CB1B27F11200197226 /* Pluralize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED885CA1B27F11200197226 /* Pluralize.swift */; }; 22 | 1ED885CE1B280F8200197226 /* NSColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED885CD1B280F8200197226 /* NSColor+Extensions.swift */; }; 23 | 1ED885D01B28F51B00197226 /* ProgressLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED885CF1B28F51B00197226 /* ProgressLineView.swift */; }; 24 | 1ED9A0F41B1F97CE000A41B9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0F31B1F97CE000A41B9 /* AppDelegate.swift */; }; 25 | 1ED9A11D1B1F9810000A41B9 /* AssetCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0F51B1F9810000A41B9 /* AssetCatalog.swift */; }; 26 | 1ED9A11E1B1F9810000A41B9 /* AssetGenerationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0F61B1F9810000A41B9 /* AssetGenerationController.swift */; }; 27 | 1ED9A11F1B1F9810000A41B9 /* AssetGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0F71B1F9810000A41B9 /* AssetGenerator.swift */; }; 28 | 1ED9A1201B1F9810000A41B9 /* AssetGeneratorInputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0F81B1F9810000A41B9 /* AssetGeneratorInputValidator.swift */; }; 29 | 1ED9A1211B1F9810000A41B9 /* AssetGeneratorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0F91B1F9810000A41B9 /* AssetGeneratorWindowController.swift */; }; 30 | 1ED9A1221B1F9810000A41B9 /* AssetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0FA1B1F9810000A41B9 /* AssetType.swift */; }; 31 | 1ED9A1231B1F9810000A41B9 /* AssetWindowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0FB1B1F9810000A41B9 /* AssetWindowViewModel.swift */; }; 32 | 1ED9A1241B1F9810000A41B9 /* BookmarkResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0FC1B1F9810000A41B9 /* BookmarkResolver.swift */; }; 33 | 1ED9A1261B1F9810000A41B9 /* DropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A0FE1B1F9810000A41B9 /* DropView.swift */; }; 34 | 1ED9A1291B1F9810000A41B9 /* FileSystemHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1011B1F9810000A41B9 /* FileSystemHelper.swift */; }; 35 | 1ED9A12C1B1F9810000A41B9 /* ImagesDropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1051B1F9810000A41B9 /* ImagesDropViewController.swift */; }; 36 | 1ED9A12D1B1F9810000A41B9 /* ImagesGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1061B1F9810000A41B9 /* ImagesGroupViewModel.swift */; }; 37 | 1ED9A12E1B1F9810000A41B9 /* XCAssetsJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1071B1F9810000A41B9 /* XCAssetsJSON.swift */; }; 38 | 1ED9A12F1B1F9810000A41B9 /* Keyword Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1081B1F9810000A41B9 /* Keyword Constants.swift */; }; 39 | 1ED9A1301B1F9810000A41B9 /* NSImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1091B1F9810000A41B9 /* NSImage+Extensions.swift */; }; 40 | 1ED9A1311B1F9810000A41B9 /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A10A1B1F9810000A41B9 /* NSLayoutConstraint+Extensions.swift */; }; 41 | 1ED9A1321B1F9810000A41B9 /* Optionals+Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A10B1B1F9810000A41B9 /* Optionals+Operators.swift */; }; 42 | 1ED9A1331B1F9810000A41B9 /* PathQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A10C1B1F9810000A41B9 /* PathQuery.swift */; }; 43 | 1ED9A1341B1F9810000A41B9 /* ProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A10D1B1F9810000A41B9 /* ProgressViewController.swift */; }; 44 | 1ED9A1351B1F9810000A41B9 /* ProgressIndicationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A10E1B1F9810000A41B9 /* ProgressIndicationViewModel.swift */; }; 45 | 1ED9A1371B1F9810000A41B9 /* ProjectDropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1101B1F9810000A41B9 /* ProjectDropViewController.swift */; }; 46 | 1ED9A1381B1F9810000A41B9 /* ProjectSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1111B1F9810000A41B9 /* ProjectSelector.swift */; }; 47 | 1ED9A13B1B1F9810000A41B9 /* RoundedDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1141B1F9810000A41B9 /* RoundedDropView.swift */; }; 48 | 1ED9A13C1B1F9810000A41B9 /* ProjectSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1151B1F9810000A41B9 /* ProjectSelectionViewModel.swift */; }; 49 | 1ED9A13D1B1F9810000A41B9 /* Serializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1161B1F9810000A41B9 /* Serializable.swift */; }; 50 | 1ED9A13E1B1F9810000A41B9 /* StatusCrafter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1171B1F9810000A41B9 /* StatusCrafter.swift */; }; 51 | 1ED9A13F1B1F9810000A41B9 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A1181B1F9810000A41B9 /* StringExtension.swift */; }; 52 | 1ED9A1411B1F9810000A41B9 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A11A1B1F9810000A41B9 /* Validator.swift */; }; 53 | 1ED9A1421B1F9810000A41B9 /* XcodeFileValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A11B1B1F9810000A41B9 /* XcodeFileValidator.swift */; }; 54 | 1ED9A1431B1F9810000A41B9 /* XCProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED9A11C1B1F9810000A41B9 /* XCProject.swift */; }; 55 | 1EFC5ABA1B2661B300F7400A /* Box.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ED9A1491B1F9B40000A41B9 /* Box.framework */; }; 56 | 1EFC5ABB1B2661B300F7400A /* Box.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1ED9A1491B1F9B40000A41B9 /* Box.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 57 | 1EFC5ABC1B2661B300F7400A /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ED9A14A1B1F9B40000A41B9 /* ReactiveCocoa.framework */; }; 58 | 1EFC5ABD1B2661B300F7400A /* ReactiveCocoa.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1ED9A14A1B1F9B40000A41B9 /* ReactiveCocoa.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 59 | 1EFC5ABE1B2661B300F7400A /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ED9A14B1B1F9B40000A41B9 /* Result.framework */; }; 60 | 1EFC5ABF1B2661B300F7400A /* Result.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1ED9A14B1B1F9B40000A41B9 /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 61 | DD09CBE81989A46C002DD2FB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09CBE71989A46C002DD2FB /* main.swift */; }; 62 | DD09CBEE1989A46C002DD2FB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD09CBED1989A46C002DD2FB /* Images.xcassets */; }; 63 | DD09CBFD1989A46C002DD2FB /* XCAssetGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09CBFC1989A46C002DD2FB /* XCAssetGeneratorTests.swift */; }; 64 | /* End PBXBuildFile section */ 65 | 66 | /* Begin PBXContainerItemProxy section */ 67 | DD09CBF71989A46C002DD2FB /* PBXContainerItemProxy */ = { 68 | isa = PBXContainerItemProxy; 69 | containerPortal = DD09CBDA1989A46C002DD2FB /* Project object */; 70 | proxyType = 1; 71 | remoteGlobalIDString = DD09CBE11989A46C002DD2FB; 72 | remoteInfo = XCAssetGenerator; 73 | }; 74 | /* End PBXContainerItemProxy section */ 75 | 76 | /* Begin PBXCopyFilesBuildPhase section */ 77 | 1EDAD8951AF0D669002E2394 /* Embed Frameworks */ = { 78 | isa = PBXCopyFilesBuildPhase; 79 | buildActionMask = 2147483647; 80 | dstPath = ""; 81 | dstSubfolderSpec = 10; 82 | files = ( 83 | 1EFC5ABF1B2661B300F7400A /* Result.framework in Embed Frameworks */, 84 | 1EFC5ABB1B2661B300F7400A /* Box.framework in Embed Frameworks */, 85 | 1EFC5ABD1B2661B300F7400A /* ReactiveCocoa.framework in Embed Frameworks */, 86 | ); 87 | name = "Embed Frameworks"; 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXCopyFilesBuildPhase section */ 91 | 92 | /* Begin PBXFileReference section */ 93 | 1E17A3A01ACF637700785DB2 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 94 | 1E48EF421B31861E00A63FBF /* AssetAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttribute.swift; sourceTree = ""; }; 95 | 1E7602D11B4E47140096DEF3 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 96 | 1E8D12EB1B436324004A3FCB /* XCAssetGenerator-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCAssetGenerator-Bridging-Header.h"; sourceTree = ""; }; 97 | 1E8D12EF1B4363ED004A3FCB /* FileSystemObserverSignal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemObserverSignal.swift; sourceTree = ""; }; 98 | 1E946FD31B46007600133459 /* FileSystemObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileSystemObserver.h; sourceTree = ""; }; 99 | 1E946FD41B46007600133459 /* FileSystemObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileSystemObserver.m; sourceTree = ""; }; 100 | 1E95006B1B44F1F200D7632D /* ImagesDropView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ImagesDropView.xib; sourceTree = ""; }; 101 | 1EAC3B1E1B3C3EA600B75DD6 /* CABasicAnimation+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CABasicAnimation+Extensions.swift"; sourceTree = ""; }; 102 | 1EAC3B201B3CC7D300B75DD6 /* ImageSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelection.swift; sourceTree = ""; }; 103 | 1EC4854B1B44DCE300784756 /* ProjectDropView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProjectDropView.xib; sourceTree = ""; }; 104 | 1ED096DB1B2D53D8005337A9 /* AssetDiff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDiff.swift; sourceTree = ""; }; 105 | 1ED885CA1B27F11200197226 /* Pluralize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pluralize.swift; sourceTree = ""; }; 106 | 1ED885CD1B280F8200197226 /* NSColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSColor+Extensions.swift"; sourceTree = ""; }; 107 | 1ED885CF1B28F51B00197226 /* ProgressLineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressLineView.swift; sourceTree = ""; }; 108 | 1ED9A0F31B1F97CE000A41B9 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 109 | 1ED9A0F51B1F9810000A41B9 /* AssetCatalog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetCatalog.swift; sourceTree = ""; }; 110 | 1ED9A0F61B1F9810000A41B9 /* AssetGenerationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetGenerationController.swift; sourceTree = ""; }; 111 | 1ED9A0F71B1F9810000A41B9 /* AssetGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetGenerator.swift; sourceTree = ""; }; 112 | 1ED9A0F81B1F9810000A41B9 /* AssetGeneratorInputValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetGeneratorInputValidator.swift; sourceTree = ""; }; 113 | 1ED9A0F91B1F9810000A41B9 /* AssetGeneratorWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetGeneratorWindowController.swift; sourceTree = ""; }; 114 | 1ED9A0FA1B1F9810000A41B9 /* AssetType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetType.swift; sourceTree = ""; }; 115 | 1ED9A0FB1B1F9810000A41B9 /* AssetWindowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetWindowViewModel.swift; sourceTree = ""; }; 116 | 1ED9A0FC1B1F9810000A41B9 /* BookmarkResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkResolver.swift; sourceTree = ""; }; 117 | 1ED9A0FE1B1F9810000A41B9 /* DropView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropView.swift; sourceTree = ""; }; 118 | 1ED9A1011B1F9810000A41B9 /* FileSystemHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemHelper.swift; sourceTree = ""; }; 119 | 1ED9A1051B1F9810000A41B9 /* ImagesDropViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesDropViewController.swift; sourceTree = ""; }; 120 | 1ED9A1061B1F9810000A41B9 /* ImagesGroupViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesGroupViewModel.swift; sourceTree = ""; }; 121 | 1ED9A1071B1F9810000A41B9 /* XCAssetsJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCAssetsJSON.swift; sourceTree = ""; }; 122 | 1ED9A1081B1F9810000A41B9 /* Keyword Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Keyword Constants.swift"; sourceTree = ""; }; 123 | 1ED9A1091B1F9810000A41B9 /* NSImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+Extensions.swift"; sourceTree = ""; }; 124 | 1ED9A10A1B1F9810000A41B9 /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = ""; }; 125 | 1ED9A10B1B1F9810000A41B9 /* Optionals+Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Optionals+Operators.swift"; sourceTree = ""; }; 126 | 1ED9A10C1B1F9810000A41B9 /* PathQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathQuery.swift; sourceTree = ""; }; 127 | 1ED9A10D1B1F9810000A41B9 /* ProgressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewController.swift; sourceTree = ""; }; 128 | 1ED9A10E1B1F9810000A41B9 /* ProgressIndicationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicationViewModel.swift; sourceTree = ""; }; 129 | 1ED9A1101B1F9810000A41B9 /* ProjectDropViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectDropViewController.swift; sourceTree = ""; }; 130 | 1ED9A1111B1F9810000A41B9 /* ProjectSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSelector.swift; sourceTree = ""; }; 131 | 1ED9A1141B1F9810000A41B9 /* RoundedDropView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedDropView.swift; sourceTree = ""; }; 132 | 1ED9A1151B1F9810000A41B9 /* ProjectSelectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSelectionViewModel.swift; sourceTree = ""; }; 133 | 1ED9A1161B1F9810000A41B9 /* Serializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serializable.swift; sourceTree = ""; }; 134 | 1ED9A1171B1F9810000A41B9 /* StatusCrafter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusCrafter.swift; sourceTree = ""; }; 135 | 1ED9A1181B1F9810000A41B9 /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 136 | 1ED9A11A1B1F9810000A41B9 /* Validator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; 137 | 1ED9A11B1B1F9810000A41B9 /* XcodeFileValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeFileValidator.swift; sourceTree = ""; }; 138 | 1ED9A11C1B1F9810000A41B9 /* XCProject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCProject.swift; sourceTree = ""; }; 139 | 1ED9A1471B1F983B000A41B9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 140 | 1ED9A1491B1F9B40000A41B9 /* Box.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Box.framework; path = Carthage/Build/Mac/Box.framework; sourceTree = ""; }; 141 | 1ED9A14A1B1F9B40000A41B9 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveCocoa.framework; path = Carthage/Build/Mac/ReactiveCocoa.framework; sourceTree = ""; }; 142 | 1ED9A14B1B1F9B40000A41B9 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = Carthage/Build/Mac/Result.framework; sourceTree = ""; }; 143 | DD09CBE21989A46C002DD2FB /* Asset Generator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Asset Generator.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 144 | DD09CBE61989A46C002DD2FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 145 | DD09CBE71989A46C002DD2FB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 146 | DD09CBED1989A46C002DD2FB /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 147 | DD09CBF61989A46C002DD2FB /* XCAssetGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XCAssetGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 148 | DD09CBFB1989A46C002DD2FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 149 | DD09CBFC1989A46C002DD2FB /* XCAssetGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCAssetGeneratorTests.swift; sourceTree = ""; }; 150 | /* End PBXFileReference section */ 151 | 152 | /* Begin PBXFrameworksBuildPhase section */ 153 | DD09CBDF1989A46C002DD2FB /* Frameworks */ = { 154 | isa = PBXFrameworksBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 1EFC5ABE1B2661B300F7400A /* Result.framework in Frameworks */, 158 | 1EFC5ABA1B2661B300F7400A /* Box.framework in Frameworks */, 159 | 1EFC5ABC1B2661B300F7400A /* ReactiveCocoa.framework in Frameworks */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | DD09CBF31989A46C002DD2FB /* Frameworks */ = { 164 | isa = PBXFrameworksBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXFrameworksBuildPhase section */ 171 | 172 | /* Begin PBXGroup section */ 173 | 1EAC3B221B3CC92E00B75DD6 /* Services */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 1E7602D11B4E47140096DEF3 /* Storage.swift */, 177 | 1ED9A0F61B1F9810000A41B9 /* AssetGenerationController.swift */, 178 | 1ED9A0F71B1F9810000A41B9 /* AssetGenerator.swift */, 179 | 1ED096DB1B2D53D8005337A9 /* AssetDiff.swift */, 180 | 1ED9A1171B1F9810000A41B9 /* StatusCrafter.swift */, 181 | 1E8D12EF1B4363ED004A3FCB /* FileSystemObserverSignal.swift */, 182 | 1ED9A1111B1F9810000A41B9 /* ProjectSelector.swift */, 183 | ); 184 | name = Services; 185 | sourceTree = ""; 186 | }; 187 | 1ED885CC1B27F3F400197226 /* Helpers */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | 1ED9A1071B1F9810000A41B9 /* XCAssetsJSON.swift */, 191 | 1ED9A0FC1B1F9810000A41B9 /* BookmarkResolver.swift */, 192 | 1ED9A1011B1F9810000A41B9 /* FileSystemHelper.swift */, 193 | 1ED885CA1B27F11200197226 /* Pluralize.swift */, 194 | 1ED9A0F81B1F9810000A41B9 /* AssetGeneratorInputValidator.swift */, 195 | 1ED9A10C1B1F9810000A41B9 /* PathQuery.swift */, 196 | 1ED9A11B1B1F9810000A41B9 /* XcodeFileValidator.swift */, 197 | 1ED9A11A1B1F9810000A41B9 /* Validator.swift */, 198 | ); 199 | name = Helpers; 200 | sourceTree = ""; 201 | }; 202 | 1ED9A1511B1F9FA7000A41B9 /* ViewModels */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 1ED9A0FB1B1F9810000A41B9 /* AssetWindowViewModel.swift */, 206 | 1ED9A1061B1F9810000A41B9 /* ImagesGroupViewModel.swift */, 207 | 1ED9A1151B1F9810000A41B9 /* ProjectSelectionViewModel.swift */, 208 | 1ED9A10E1B1F9810000A41B9 /* ProgressIndicationViewModel.swift */, 209 | ); 210 | name = ViewModels; 211 | sourceTree = ""; 212 | }; 213 | 1ED9A1521B1F9FBE000A41B9 /* Views */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 1ED9A0F91B1F9810000A41B9 /* AssetGeneratorWindowController.swift */, 217 | 1ED9A1051B1F9810000A41B9 /* ImagesDropViewController.swift */, 218 | 1E95006B1B44F1F200D7632D /* ImagesDropView.xib */, 219 | 1ED9A1101B1F9810000A41B9 /* ProjectDropViewController.swift */, 220 | 1EC4854B1B44DCE300784756 /* ProjectDropView.xib */, 221 | 1ED9A1141B1F9810000A41B9 /* RoundedDropView.swift */, 222 | 1ED9A0FE1B1F9810000A41B9 /* DropView.swift */, 223 | 1ED9A10D1B1F9810000A41B9 /* ProgressViewController.swift */, 224 | 1ED885CF1B28F51B00197226 /* ProgressLineView.swift */, 225 | ); 226 | name = Views; 227 | sourceTree = ""; 228 | }; 229 | 1ED9A1531B1F9FEC000A41B9 /* Models */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | 1ED9A11C1B1F9810000A41B9 /* XCProject.swift */, 233 | 1ED9A0F51B1F9810000A41B9 /* AssetCatalog.swift */, 234 | 1EAC3B201B3CC7D300B75DD6 /* ImageSelection.swift */, 235 | 1ED9A0FA1B1F9810000A41B9 /* AssetType.swift */, 236 | 1E48EF421B31861E00A63FBF /* AssetAttribute.swift */, 237 | 1ED9A1161B1F9810000A41B9 /* Serializable.swift */, 238 | ); 239 | name = Models; 240 | sourceTree = ""; 241 | }; 242 | 1ED9A1541B1FA013000A41B9 /* Extensions and Operators */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 1ED9A10B1B1F9810000A41B9 /* Optionals+Operators.swift */, 246 | 1EAC3B1E1B3C3EA600B75DD6 /* CABasicAnimation+Extensions.swift */, 247 | 1ED9A1091B1F9810000A41B9 /* NSImage+Extensions.swift */, 248 | 1ED9A10A1B1F9810000A41B9 /* NSLayoutConstraint+Extensions.swift */, 249 | 1ED9A1181B1F9810000A41B9 /* StringExtension.swift */, 250 | 1ED885CD1B280F8200197226 /* NSColor+Extensions.swift */, 251 | ); 252 | name = "Extensions and Operators"; 253 | sourceTree = ""; 254 | }; 255 | DD09CBD91989A46C002DD2FB = { 256 | isa = PBXGroup; 257 | children = ( 258 | 1ED9A1491B1F9B40000A41B9 /* Box.framework */, 259 | 1ED9A14A1B1F9B40000A41B9 /* ReactiveCocoa.framework */, 260 | 1ED9A14B1B1F9B40000A41B9 /* Result.framework */, 261 | DD09CBE41989A46C002DD2FB /* XCAssetGenerator */, 262 | DD09CBF91989A46C002DD2FB /* XCAssetGeneratorTests */, 263 | DD09CBE31989A46C002DD2FB /* Products */, 264 | ); 265 | sourceTree = ""; 266 | }; 267 | DD09CBE31989A46C002DD2FB /* Products */ = { 268 | isa = PBXGroup; 269 | children = ( 270 | DD09CBE21989A46C002DD2FB /* Asset Generator.app */, 271 | DD09CBF61989A46C002DD2FB /* XCAssetGeneratorTests.xctest */, 272 | ); 273 | name = Products; 274 | sourceTree = ""; 275 | }; 276 | DD09CBE41989A46C002DD2FB /* XCAssetGenerator */ = { 277 | isa = PBXGroup; 278 | children = ( 279 | 1ED9A0F31B1F97CE000A41B9 /* AppDelegate.swift */, 280 | 1ED9A1531B1F9FEC000A41B9 /* Models */, 281 | 1ED9A1511B1F9FA7000A41B9 /* ViewModels */, 282 | 1ED9A1521B1F9FBE000A41B9 /* Views */, 283 | 1EAC3B221B3CC92E00B75DD6 /* Services */, 284 | 1ED9A1541B1FA013000A41B9 /* Extensions and Operators */, 285 | 1ED885CC1B27F3F400197226 /* Helpers */, 286 | 1ED9A1081B1F9810000A41B9 /* Keyword Constants.swift */, 287 | 1E946FD31B46007600133459 /* FileSystemObserver.h */, 288 | 1E946FD41B46007600133459 /* FileSystemObserver.m */, 289 | 1ED9A1461B1F983B000A41B9 /* Main.storyboard */, 290 | DD09CBED1989A46C002DD2FB /* Images.xcassets */, 291 | DD09CBE51989A46C002DD2FB /* Supporting Files */, 292 | 1E8D12EB1B436324004A3FCB /* XCAssetGenerator-Bridging-Header.h */, 293 | ); 294 | path = XCAssetGenerator; 295 | sourceTree = ""; 296 | }; 297 | DD09CBE51989A46C002DD2FB /* Supporting Files */ = { 298 | isa = PBXGroup; 299 | children = ( 300 | DD09CBE61989A46C002DD2FB /* Info.plist */, 301 | 1E17A3A01ACF637700785DB2 /* Localizable.strings */, 302 | DD09CBE71989A46C002DD2FB /* main.swift */, 303 | ); 304 | name = "Supporting Files"; 305 | sourceTree = ""; 306 | }; 307 | DD09CBF91989A46C002DD2FB /* XCAssetGeneratorTests */ = { 308 | isa = PBXGroup; 309 | children = ( 310 | DD09CBFC1989A46C002DD2FB /* XCAssetGeneratorTests.swift */, 311 | DD09CBFA1989A46C002DD2FB /* Supporting Files */, 312 | ); 313 | path = XCAssetGeneratorTests; 314 | sourceTree = ""; 315 | }; 316 | DD09CBFA1989A46C002DD2FB /* Supporting Files */ = { 317 | isa = PBXGroup; 318 | children = ( 319 | DD09CBFB1989A46C002DD2FB /* Info.plist */, 320 | ); 321 | name = "Supporting Files"; 322 | sourceTree = ""; 323 | }; 324 | /* End PBXGroup section */ 325 | 326 | /* Begin PBXNativeTarget section */ 327 | DD09CBE11989A46C002DD2FB /* Asset Generator */ = { 328 | isa = PBXNativeTarget; 329 | buildConfigurationList = DD09CC001989A46C002DD2FB /* Build configuration list for PBXNativeTarget "Asset Generator" */; 330 | buildPhases = ( 331 | DD09CBDE1989A46C002DD2FB /* Sources */, 332 | DD09CBDF1989A46C002DD2FB /* Frameworks */, 333 | DD09CBE01989A46C002DD2FB /* Resources */, 334 | 1EDAD8951AF0D669002E2394 /* Embed Frameworks */, 335 | ); 336 | buildRules = ( 337 | ); 338 | dependencies = ( 339 | ); 340 | name = "Asset Generator"; 341 | productName = XCAssetGenerator; 342 | productReference = DD09CBE21989A46C002DD2FB /* Asset Generator.app */; 343 | productType = "com.apple.product-type.application"; 344 | }; 345 | DD09CBF51989A46C002DD2FB /* XCAssetGeneratorTests */ = { 346 | isa = PBXNativeTarget; 347 | buildConfigurationList = DD09CC031989A46C002DD2FB /* Build configuration list for PBXNativeTarget "XCAssetGeneratorTests" */; 348 | buildPhases = ( 349 | DD09CBF21989A46C002DD2FB /* Sources */, 350 | DD09CBF31989A46C002DD2FB /* Frameworks */, 351 | DD09CBF41989A46C002DD2FB /* Resources */, 352 | ); 353 | buildRules = ( 354 | ); 355 | dependencies = ( 356 | DD09CBF81989A46C002DD2FB /* PBXTargetDependency */, 357 | ); 358 | name = XCAssetGeneratorTests; 359 | productName = XCAssetGeneratorTests; 360 | productReference = DD09CBF61989A46C002DD2FB /* XCAssetGeneratorTests.xctest */; 361 | productType = "com.apple.product-type.bundle.unit-test"; 362 | }; 363 | /* End PBXNativeTarget section */ 364 | 365 | /* Begin PBXProject section */ 366 | DD09CBDA1989A46C002DD2FB /* Project object */ = { 367 | isa = PBXProject; 368 | attributes = { 369 | LastSwiftUpdateCheck = 0700; 370 | LastUpgradeCheck = 0600; 371 | ORGANIZATIONNAME = "Bader Alabdulrazzaq"; 372 | TargetAttributes = { 373 | DD09CBE11989A46C002DD2FB = { 374 | CreatedOnToolsVersion = 6.0; 375 | SystemCapabilities = { 376 | com.apple.Sandbox = { 377 | enabled = 0; 378 | }; 379 | }; 380 | }; 381 | DD09CBF51989A46C002DD2FB = { 382 | CreatedOnToolsVersion = 6.0; 383 | TestTargetID = DD09CBE11989A46C002DD2FB; 384 | }; 385 | }; 386 | }; 387 | buildConfigurationList = DD09CBDD1989A46C002DD2FB /* Build configuration list for PBXProject "XCAssetGenerator" */; 388 | compatibilityVersion = "Xcode 3.2"; 389 | developmentRegion = English; 390 | hasScannedForEncodings = 0; 391 | knownRegions = ( 392 | en, 393 | Base, 394 | ); 395 | mainGroup = DD09CBD91989A46C002DD2FB; 396 | productRefGroup = DD09CBE31989A46C002DD2FB /* Products */; 397 | projectDirPath = ""; 398 | projectRoot = ""; 399 | targets = ( 400 | DD09CBE11989A46C002DD2FB /* Asset Generator */, 401 | DD09CBF51989A46C002DD2FB /* XCAssetGeneratorTests */, 402 | ); 403 | }; 404 | /* End PBXProject section */ 405 | 406 | /* Begin PBXResourcesBuildPhase section */ 407 | DD09CBE01989A46C002DD2FB /* Resources */ = { 408 | isa = PBXResourcesBuildPhase; 409 | buildActionMask = 2147483647; 410 | files = ( 411 | 1EC4854C1B44DCE300784756 /* ProjectDropView.xib in Resources */, 412 | 1E95006C1B44F1F200D7632D /* ImagesDropView.xib in Resources */, 413 | 1E2D9EE31B26620A009A2745 /* Main.storyboard in Resources */, 414 | DD09CBEE1989A46C002DD2FB /* Images.xcassets in Resources */, 415 | 1E17A3A11ACF637700785DB2 /* Localizable.strings in Resources */, 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | }; 419 | DD09CBF41989A46C002DD2FB /* Resources */ = { 420 | isa = PBXResourcesBuildPhase; 421 | buildActionMask = 2147483647; 422 | files = ( 423 | ); 424 | runOnlyForDeploymentPostprocessing = 0; 425 | }; 426 | /* End PBXResourcesBuildPhase section */ 427 | 428 | /* Begin PBXSourcesBuildPhase section */ 429 | DD09CBDE1989A46C002DD2FB /* Sources */ = { 430 | isa = PBXSourcesBuildPhase; 431 | buildActionMask = 2147483647; 432 | files = ( 433 | 1ED9A12F1B1F9810000A41B9 /* Keyword Constants.swift in Sources */, 434 | 1ED885CB1B27F11200197226 /* Pluralize.swift in Sources */, 435 | 1ED885CE1B280F8200197226 /* NSColor+Extensions.swift in Sources */, 436 | 1ED9A1291B1F9810000A41B9 /* FileSystemHelper.swift in Sources */, 437 | 1ED9A1351B1F9810000A41B9 /* ProgressIndicationViewModel.swift in Sources */, 438 | 1ED9A13F1B1F9810000A41B9 /* StringExtension.swift in Sources */, 439 | 1ED9A1421B1F9810000A41B9 /* XcodeFileValidator.swift in Sources */, 440 | 1ED9A1411B1F9810000A41B9 /* Validator.swift in Sources */, 441 | 1ED9A13B1B1F9810000A41B9 /* RoundedDropView.swift in Sources */, 442 | 1ED9A12D1B1F9810000A41B9 /* ImagesGroupViewModel.swift in Sources */, 443 | 1ED9A1381B1F9810000A41B9 /* ProjectSelector.swift in Sources */, 444 | 1ED885D01B28F51B00197226 /* ProgressLineView.swift in Sources */, 445 | 1E946FD51B46007600133459 /* FileSystemObserver.m in Sources */, 446 | 1E8D12F01B4363ED004A3FCB /* FileSystemObserverSignal.swift in Sources */, 447 | 1ED096DC1B2D53D8005337A9 /* AssetDiff.swift in Sources */, 448 | 1E48EF431B31861E00A63FBF /* AssetAttribute.swift in Sources */, 449 | 1EAC3B1F1B3C3EA600B75DD6 /* CABasicAnimation+Extensions.swift in Sources */, 450 | 1EAC3B211B3CC7D300B75DD6 /* ImageSelection.swift in Sources */, 451 | 1ED9A1231B1F9810000A41B9 /* AssetWindowViewModel.swift in Sources */, 452 | 1ED9A1311B1F9810000A41B9 /* NSLayoutConstraint+Extensions.swift in Sources */, 453 | 1ED9A1241B1F9810000A41B9 /* BookmarkResolver.swift in Sources */, 454 | 1ED9A1201B1F9810000A41B9 /* AssetGeneratorInputValidator.swift in Sources */, 455 | 1ED9A11F1B1F9810000A41B9 /* AssetGenerator.swift in Sources */, 456 | 1ED9A1261B1F9810000A41B9 /* DropView.swift in Sources */, 457 | 1ED9A13E1B1F9810000A41B9 /* StatusCrafter.swift in Sources */, 458 | 1ED9A1331B1F9810000A41B9 /* PathQuery.swift in Sources */, 459 | 1ED9A1301B1F9810000A41B9 /* NSImage+Extensions.swift in Sources */, 460 | 1ED9A12E1B1F9810000A41B9 /* XCAssetsJSON.swift in Sources */, 461 | 1ED9A0F41B1F97CE000A41B9 /* AppDelegate.swift in Sources */, 462 | 1E7602D21B4E47140096DEF3 /* Storage.swift in Sources */, 463 | 1ED9A13D1B1F9810000A41B9 /* Serializable.swift in Sources */, 464 | DD09CBE81989A46C002DD2FB /* main.swift in Sources */, 465 | 1ED9A1341B1F9810000A41B9 /* ProgressViewController.swift in Sources */, 466 | 1ED9A1221B1F9810000A41B9 /* AssetType.swift in Sources */, 467 | 1ED9A11E1B1F9810000A41B9 /* AssetGenerationController.swift in Sources */, 468 | 1ED9A12C1B1F9810000A41B9 /* ImagesDropViewController.swift in Sources */, 469 | 1ED9A1211B1F9810000A41B9 /* AssetGeneratorWindowController.swift in Sources */, 470 | 1ED9A11D1B1F9810000A41B9 /* AssetCatalog.swift in Sources */, 471 | 1ED9A1371B1F9810000A41B9 /* ProjectDropViewController.swift in Sources */, 472 | 1ED9A13C1B1F9810000A41B9 /* ProjectSelectionViewModel.swift in Sources */, 473 | 1ED9A1321B1F9810000A41B9 /* Optionals+Operators.swift in Sources */, 474 | 1ED9A1431B1F9810000A41B9 /* XCProject.swift in Sources */, 475 | ); 476 | runOnlyForDeploymentPostprocessing = 0; 477 | }; 478 | DD09CBF21989A46C002DD2FB /* Sources */ = { 479 | isa = PBXSourcesBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | DD09CBFD1989A46C002DD2FB /* XCAssetGeneratorTests.swift in Sources */, 483 | ); 484 | runOnlyForDeploymentPostprocessing = 0; 485 | }; 486 | /* End PBXSourcesBuildPhase section */ 487 | 488 | /* Begin PBXTargetDependency section */ 489 | DD09CBF81989A46C002DD2FB /* PBXTargetDependency */ = { 490 | isa = PBXTargetDependency; 491 | target = DD09CBE11989A46C002DD2FB /* Asset Generator */; 492 | targetProxy = DD09CBF71989A46C002DD2FB /* PBXContainerItemProxy */; 493 | }; 494 | /* End PBXTargetDependency section */ 495 | 496 | /* Begin PBXVariantGroup section */ 497 | 1ED9A1461B1F983B000A41B9 /* Main.storyboard */ = { 498 | isa = PBXVariantGroup; 499 | children = ( 500 | 1ED9A1471B1F983B000A41B9 /* Base */, 501 | ); 502 | name = Main.storyboard; 503 | sourceTree = ""; 504 | }; 505 | /* End PBXVariantGroup section */ 506 | 507 | /* Begin XCBuildConfiguration section */ 508 | DD09CBFE1989A46C002DD2FB /* Debug */ = { 509 | isa = XCBuildConfiguration; 510 | buildSettings = { 511 | ALWAYS_SEARCH_USER_PATHS = NO; 512 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 513 | CLANG_CXX_LIBRARY = "libc++"; 514 | CLANG_ENABLE_MODULES = YES; 515 | CLANG_ENABLE_OBJC_ARC = YES; 516 | CLANG_WARN_BOOL_CONVERSION = YES; 517 | CLANG_WARN_CONSTANT_CONVERSION = YES; 518 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 519 | CLANG_WARN_EMPTY_BODY = YES; 520 | CLANG_WARN_ENUM_CONVERSION = YES; 521 | CLANG_WARN_INT_CONVERSION = YES; 522 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 523 | CLANG_WARN_UNREACHABLE_CODE = YES; 524 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 525 | CODE_SIGN_IDENTITY = "-"; 526 | COPY_PHASE_STRIP = NO; 527 | ENABLE_STRICT_OBJC_MSGSEND = YES; 528 | GCC_C_LANGUAGE_STANDARD = gnu99; 529 | GCC_DYNAMIC_NO_PIC = NO; 530 | GCC_OPTIMIZATION_LEVEL = 0; 531 | GCC_PREPROCESSOR_DEFINITIONS = ( 532 | "DEBUG=1", 533 | "$(inherited)", 534 | ); 535 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 536 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 537 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 538 | GCC_WARN_UNDECLARED_SELECTOR = YES; 539 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 540 | GCC_WARN_UNUSED_FUNCTION = YES; 541 | GCC_WARN_UNUSED_VARIABLE = YES; 542 | MACOSX_DEPLOYMENT_TARGET = 10.10; 543 | MTL_ENABLE_DEBUG_INFO = YES; 544 | ONLY_ACTIVE_ARCH = YES; 545 | PRODUCT_NAME = "Asset Generator"; 546 | SDKROOT = macosx; 547 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 548 | }; 549 | name = Debug; 550 | }; 551 | DD09CBFF1989A46C002DD2FB /* Release */ = { 552 | isa = XCBuildConfiguration; 553 | buildSettings = { 554 | ALWAYS_SEARCH_USER_PATHS = NO; 555 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 556 | CLANG_CXX_LIBRARY = "libc++"; 557 | CLANG_ENABLE_MODULES = YES; 558 | CLANG_ENABLE_OBJC_ARC = YES; 559 | CLANG_WARN_BOOL_CONVERSION = YES; 560 | CLANG_WARN_CONSTANT_CONVERSION = YES; 561 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 562 | CLANG_WARN_EMPTY_BODY = YES; 563 | CLANG_WARN_ENUM_CONVERSION = YES; 564 | CLANG_WARN_INT_CONVERSION = YES; 565 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 566 | CLANG_WARN_UNREACHABLE_CODE = YES; 567 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 568 | CODE_SIGN_IDENTITY = "-"; 569 | COPY_PHASE_STRIP = YES; 570 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 571 | ENABLE_NS_ASSERTIONS = NO; 572 | ENABLE_STRICT_OBJC_MSGSEND = YES; 573 | GCC_C_LANGUAGE_STANDARD = gnu99; 574 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 575 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 576 | GCC_WARN_UNDECLARED_SELECTOR = YES; 577 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 578 | GCC_WARN_UNUSED_FUNCTION = YES; 579 | GCC_WARN_UNUSED_VARIABLE = YES; 580 | MACOSX_DEPLOYMENT_TARGET = 10.10; 581 | MTL_ENABLE_DEBUG_INFO = NO; 582 | PRODUCT_NAME = "Asset Generator"; 583 | SDKROOT = macosx; 584 | }; 585 | name = Release; 586 | }; 587 | DD09CC011989A46C002DD2FB /* Debug */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 591 | CLANG_ENABLE_MODULES = YES; 592 | CODE_SIGN_ENTITLEMENTS = ""; 593 | COMBINE_HIDPI_IMAGES = YES; 594 | FRAMEWORK_SEARCH_PATHS = ( 595 | "$(inherited)", 596 | "$(PROJECT_DIR)/Carthage/Build/Mac", 597 | ); 598 | INFOPLIST_FILE = XCAssetGenerator/Info.plist; 599 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 600 | MACOSX_DEPLOYMENT_TARGET = 10.10; 601 | PRODUCT_NAME = "Asset Generator"; 602 | SDKROOT = macosx; 603 | SWIFT_INSTALL_OBJC_HEADER = NO; 604 | SWIFT_OBJC_BRIDGING_HEADER = "XCAssetGenerator/XCAssetGenerator-Bridging-Header.h"; 605 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 606 | }; 607 | name = Debug; 608 | }; 609 | DD09CC021989A46C002DD2FB /* Release */ = { 610 | isa = XCBuildConfiguration; 611 | buildSettings = { 612 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 613 | CLANG_ENABLE_MODULES = YES; 614 | CODE_SIGN_ENTITLEMENTS = ""; 615 | COMBINE_HIDPI_IMAGES = YES; 616 | FRAMEWORK_SEARCH_PATHS = ( 617 | "$(inherited)", 618 | "$(PROJECT_DIR)/Carthage/Build/Mac", 619 | ); 620 | INFOPLIST_FILE = XCAssetGenerator/Info.plist; 621 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 622 | MACOSX_DEPLOYMENT_TARGET = 10.10; 623 | ONLY_ACTIVE_ARCH = YES; 624 | PRODUCT_NAME = "Asset Generator"; 625 | SDKROOT = macosx; 626 | SWIFT_INSTALL_OBJC_HEADER = NO; 627 | SWIFT_OBJC_BRIDGING_HEADER = "XCAssetGenerator/XCAssetGenerator-Bridging-Header.h"; 628 | }; 629 | name = Release; 630 | }; 631 | DD09CC041989A46C002DD2FB /* Debug */ = { 632 | isa = XCBuildConfiguration; 633 | buildSettings = { 634 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/XCAssetGenerator.app/Contents/MacOS/XCAssetGenerator"; 635 | COMBINE_HIDPI_IMAGES = YES; 636 | FRAMEWORK_SEARCH_PATHS = ( 637 | "$(DEVELOPER_FRAMEWORKS_DIR)", 638 | "$(inherited)", 639 | ); 640 | GCC_PREPROCESSOR_DEFINITIONS = ( 641 | "DEBUG=1", 642 | "$(inherited)", 643 | ); 644 | INFOPLIST_FILE = XCAssetGeneratorTests/Info.plist; 645 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 646 | PRODUCT_NAME = "$(TARGET_NAME)"; 647 | TEST_HOST = "$(BUNDLE_LOADER)"; 648 | }; 649 | name = Debug; 650 | }; 651 | DD09CC051989A46C002DD2FB /* Release */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/XCAssetGenerator.app/Contents/MacOS/XCAssetGenerator"; 655 | COMBINE_HIDPI_IMAGES = YES; 656 | FRAMEWORK_SEARCH_PATHS = ( 657 | "$(DEVELOPER_FRAMEWORKS_DIR)", 658 | "$(inherited)", 659 | ); 660 | INFOPLIST_FILE = XCAssetGeneratorTests/Info.plist; 661 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 662 | PRODUCT_NAME = "$(TARGET_NAME)"; 663 | TEST_HOST = "$(BUNDLE_LOADER)"; 664 | }; 665 | name = Release; 666 | }; 667 | /* End XCBuildConfiguration section */ 668 | 669 | /* Begin XCConfigurationList section */ 670 | DD09CBDD1989A46C002DD2FB /* Build configuration list for PBXProject "XCAssetGenerator" */ = { 671 | isa = XCConfigurationList; 672 | buildConfigurations = ( 673 | DD09CBFE1989A46C002DD2FB /* Debug */, 674 | DD09CBFF1989A46C002DD2FB /* Release */, 675 | ); 676 | defaultConfigurationIsVisible = 0; 677 | defaultConfigurationName = Release; 678 | }; 679 | DD09CC001989A46C002DD2FB /* Build configuration list for PBXNativeTarget "Asset Generator" */ = { 680 | isa = XCConfigurationList; 681 | buildConfigurations = ( 682 | DD09CC011989A46C002DD2FB /* Debug */, 683 | DD09CC021989A46C002DD2FB /* Release */, 684 | ); 685 | defaultConfigurationIsVisible = 0; 686 | defaultConfigurationName = Release; 687 | }; 688 | DD09CC031989A46C002DD2FB /* Build configuration list for PBXNativeTarget "XCAssetGeneratorTests" */ = { 689 | isa = XCConfigurationList; 690 | buildConfigurations = ( 691 | DD09CC041989A46C002DD2FB /* Debug */, 692 | DD09CC051989A46C002DD2FB /* Release */, 693 | ); 694 | defaultConfigurationIsVisible = 0; 695 | defaultConfigurationName = Release; 696 | }; 697 | /* End XCConfigurationList section */ 698 | }; 699 | rootObject = DD09CBDA1989A46C002DD2FB /* Project object */; 700 | } 701 | -------------------------------------------------------------------------------- /XCAssetGenerator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XCAssetGenerator.xcodeproj/project.xcworkspace/xcshareddata/XCAssetGenerator.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | D4A41BE2-5C29-46AD-B49E-14B8259EB7A9 9 | IDESourceControlProjectName 10 | XCAssetGenerator 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | A6FD430031B564906AE889503C9BFEEA3B9C193C 14 | https://github.com/sourcebitsllc/Asset-Generator-Mac 15 | 16 | IDESourceControlProjectPath 17 | XCAssetGenerator.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | A6FD430031B564906AE889503C9BFEEA3B9C193C 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/sourcebitsllc/Asset-Generator-Mac 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | A6FD430031B564906AE889503C9BFEEA3B9C193C 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | A6FD430031B564906AE889503C9BFEEA3B9C193C 36 | IDESourceControlWCCName 37 | Asset-Generator-Mac 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /XCAssetGenerator.xcodeproj/xcuserdata/pranavshah.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /XCAssetGenerator.xcodeproj/xcuserdata/pranavshah.xcuserdatad/xcschemes/XCAssetGenerator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /XCAssetGenerator.xcodeproj/xcuserdata/pranavshah.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | XCAssetGenerator.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | DD09CBE11989A46C002DD2FB 16 | 17 | primary 18 | 19 | 20 | DD09CBF51989A46C002DD2FB 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /XCAssetGenerator/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 7/30/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | var windowController: NSWindowController! 13 | 14 | func applicationDidFinishLaunching(aNotification: NSNotification) { 15 | 16 | let viewModel = AssetWindowViewModel() 17 | windowController = AssetGeneratorWindowController.instantiate(viewModel) 18 | 19 | windowController.window?.setFrame(NSRect(x: 0, y: 0, width: 450, height: 300), display: true) 20 | windowController.window?.center() 21 | windowController.showWindow(nil) 22 | windowController.window?.makeKeyAndOrderFront(nil) 23 | } 24 | 25 | func applicationWillTerminate(aNotification: NSNotification) { 26 | // Insert code here to tear down your application 27 | } 28 | 29 | func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool { 30 | return true; 31 | } 32 | 33 | func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply { 34 | return NSApplicationTerminateReply.TerminateNow 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /XCAssetGenerator/AssetAttribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetAttributeProcessor.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/17/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | /// A JSON representation of the values in an individual Asset Catalog JSON. 12 | /// Will always contains a minimum of the following keys: .Scale and .Idiom 13 | /// Other available keys as specified in XCAssetsJSONKeys. 14 | typealias XCAssetsJSON = [String: AnyObject] 15 | 16 | /// Represents the outer JSON of the XCAssetsJSON values. 17 | /// Will always contain the following keys: "info", "images". 18 | typealias XCAssetsJSONWrapper = [String: AnyObject] 19 | 20 | 21 | func sanitizeJSON(json: [XCAssetsJSON]) -> [XCAssetsJSON] { 22 | func removeUnwanted(dict: [String: AnyObject]) -> XCAssetsJSON { 23 | var clean = dict 24 | clean.removeValueForKey(XCAssetsJSONKeys.Unassigned) 25 | return clean 26 | } 27 | 28 | return json.map(removeUnwanted) 29 | } 30 | 31 | struct AssetAttribute: Serializable { 32 | var filename: String? 33 | let scale: String 34 | let idiom: String 35 | let size: String? 36 | let extent: String? 37 | let subtype: String? 38 | let orientation: String? 39 | let minimumSystemVersion: String? 40 | 41 | // MARK: - Initializers 42 | 43 | private init (filename: String?, scale: String, idiom: String, size: String? = nil, subtype: String? = nil, orientation: String? = nil, minimumSystemVersion: String? = nil, extent: String? = nil) { 44 | self.filename = filename 45 | self.scale = scale 46 | self.idiom = idiom 47 | self.size = size 48 | self.extent = extent 49 | self.subtype = subtype 50 | self.orientation = orientation 51 | self.minimumSystemVersion = minimumSystemVersion 52 | } 53 | 54 | // MARK: - Serializable 55 | 56 | typealias Serialized = XCAssetsJSON 57 | var serialized: Serialized { 58 | var s = [XCAssetsJSONKeys.Scale: scale, XCAssetsJSONKeys.Idiom: idiom] 59 | if let filename = filename { 60 | s[XCAssetsJSONKeys.Filename] = filename 61 | } 62 | if let size = size { 63 | s[XCAssetsJSONKeys.Size] = size 64 | } 65 | if let extent = extent { 66 | s[XCAssetsJSONKeys.Extent] = extent 67 | } 68 | if let subtype = subtype { 69 | s[XCAssetsJSONKeys.Subtype] = subtype 70 | } 71 | if let orientation = orientation { 72 | s[XCAssetsJSONKeys.Orientation] = orientation 73 | } 74 | if let minimumSystemVersion = minimumSystemVersion { 75 | s[XCAssetsJSONKeys.MinimumSystemVersion] = minimumSystemVersion 76 | } 77 | return s 78 | } 79 | } 80 | 81 | struct AssetAttributeProcessor { 82 | 83 | static func withAsset(path: Path) -> AssetAttribute { 84 | let name = path.lastPathComponent 85 | 86 | let is2x = name.contains(GenerationKeywords.PPI2x) 87 | let is3x = name.contains(GenerationKeywords.PPI3x) 88 | let scale = is2x ? "2x" : is3x ? "3x" : "1x" 89 | 90 | let isiPhone = name.contains(GenerationKeywords.iPhone) 91 | let isiPad = name.contains(GenerationKeywords.iPad) 92 | let isMac = name.contains(GenerationKeywords.Mac) 93 | let idiom = isiPhone ? "iphone" : isiPad ? "ipad" : isMac ? "mac" : "universal" 94 | 95 | return AssetAttribute(filename: name, scale: scale, idiom: idiom) 96 | } 97 | 98 | static func withMacIcon(path: Path) -> AssetAttribute { 99 | let imgURL = NSURL(fileURLWithPath: path) 100 | let src = CGImageSourceCreateWithURL(imgURL, nil) 101 | let result = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as NSDictionary 102 | 103 | let name = path.lastPathComponent 104 | let width = result[kCGImagePropertyPixelWidth as String]! as! Int 105 | 106 | let is2x = name.contains("@2x") 107 | let idiom = "mac" 108 | var scale = is2x ? "2x" : "1x" 109 | var size = "16x16" 110 | 111 | switch width { 112 | case 16: 113 | break 114 | case 32: 115 | size = is2x ? "16x16" : "32x32" 116 | case 64: 117 | scale = "2x" 118 | size = "32x32" 119 | case 128: 120 | scale = "1x" 121 | size = "128x128" 122 | case 256: 123 | size = is2x ? "128x128" : "256x256" 124 | case 512: 125 | size = is2x ? "256x256" : "512x512" 126 | case 1024: 127 | scale = "2x" 128 | size = "512x512" 129 | default: 130 | break 131 | 132 | } 133 | 134 | return AssetAttribute(filename: name, scale: scale, idiom: idiom, size: size) 135 | } 136 | 137 | static func withIcon(path: Path) -> AssetAttribute { 138 | let name = path.lastPathComponent 139 | 140 | if AssetType.isMacIcon(name) { 141 | return AssetAttributeProcessor.withMacIcon(path) 142 | } 143 | 144 | let imgURL = NSURL(fileURLWithPath: path) 145 | let src = CGImageSourceCreateWithURL(imgURL, nil) 146 | let result = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as NSDictionary 147 | 148 | 149 | let width = result[kCGImagePropertyPixelWidth as String]! as! Int 150 | 151 | var idiom = "iphone" 152 | var scale = "1x" 153 | var size = "60x60" 154 | 155 | switch width { 156 | case 60: 157 | break 158 | case 120: 159 | idiom = "iphone" 160 | let spotlight = name.contains("@3x") || name.contains("Icon-Small") // Spotlight 40@3x or iPhone icon 60@2x 161 | scale = spotlight ? "3x" : "2x" 162 | size = spotlight ? "40x40" : "60x60" 163 | case 180: 164 | scale = "3x" 165 | case 76: 166 | idiom = "ipad" 167 | size = "76x76" 168 | case 152: 169 | idiom = "ipad" 170 | scale = "2x" 171 | size = "76x76" 172 | case 40: 173 | idiom = "ipad" 174 | scale = "1x" 175 | size = "40x40" 176 | case 80: 177 | idiom = name.contains(GenerationKeywords.iPad) ? "ipad" : "iphone" 178 | scale = "2x" 179 | size = "40x40" 180 | case 29: 181 | idiom = "ipad" 182 | scale = "1x" 183 | size = "29x29" 184 | case 58: 185 | idiom = name.contains(GenerationKeywords.iPad) ? "ipad" : "iphone" 186 | scale = "2x" 187 | size = "29x29" 188 | case 87: 189 | idiom = "iphone" 190 | scale = "3x" 191 | size = "29x29" 192 | default: 193 | break 194 | } 195 | 196 | return AssetAttribute(filename: name, scale: scale, idiom: idiom, size: size) 197 | } 198 | 199 | static func withLaunchImage(path: Path) -> AssetAttribute { 200 | let imgURL = NSURL(fileURLWithPath: path) 201 | let src = CGImageSourceCreateWithURL(imgURL, nil) 202 | let result = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as NSDictionary 203 | 204 | let name = path.lastPathComponent 205 | let width = result[kCGImagePropertyPixelWidth as String]! as! Float 206 | 207 | var idiom = "iphone" 208 | var scale = "1x" 209 | var subtype = "" 210 | var orientation = "portrait" 211 | var minimumVersion = "7.0" 212 | let extent = "full-screen" 213 | 214 | switch width { 215 | case 320: 216 | break 217 | case 640: 218 | scale = "2x" 219 | let height = result[kCGImagePropertyPixelHeight as String]! as! Float 220 | if (height == 1136) { subtype = "retina4" } 221 | case 1242: 222 | scale = "3x" 223 | subtype = "736h" 224 | minimumVersion = "8.0" 225 | case 750: 226 | scale = "2x" 227 | subtype = "667h" 228 | minimumVersion = "8.0" 229 | case 2208: 230 | scale = "3x" 231 | subtype = "736h" 232 | minimumVersion = "8.0" 233 | case 768: 234 | idiom = "ipad" 235 | case 1536: 236 | idiom = "ipad" 237 | scale = "2x" 238 | case 1024: 239 | idiom = "ipad" 240 | orientation = "landscape" 241 | case 2048: 242 | idiom = "ipad" 243 | scale = "2x" 244 | orientation = "landscape" 245 | default: 246 | break 247 | } 248 | 249 | return AssetAttribute(filename: name, scale: scale, idiom: idiom, extent: extent, subtype: subtype, orientation: orientation, minimumSystemVersion: minimumVersion) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /XCAssetGenerator/AssetCatalog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetCatalog.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 10/6/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct AssetCatalog: Printable { 12 | let path: Path 13 | 14 | init(path: Path) { 15 | self.path = path 16 | } 17 | 18 | var title: String { 19 | return path.lastPathComponent 20 | } 21 | 22 | // MARK: - Printable 23 | 24 | var description: String { 25 | return path 26 | } 27 | } 28 | 29 | extension AssetCatalog: Serializable { 30 | 31 | // MARK: - Serializable 32 | 33 | typealias Serialized = Bookmark 34 | 35 | var serialized: Serialized { 36 | return BookmarkResolver.resolveBookmarkFromPath(path)! 37 | } 38 | } -------------------------------------------------------------------------------- /XCAssetGenerator/AssetDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetDiff.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/14/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | struct AssetDiff { 12 | 13 | enum DiffOperation { 14 | // Assets found both in source and destination. 15 | case CommonAssets 16 | // Assets found in source but not destination. 17 | case NewAssets 18 | // Assets found in destination but not source. 19 | case MissingAssets 20 | } 21 | 22 | static func diffWithOperation(source: [Asset], catalog: AssetCatalog)(operation: DiffOperation) -> Int { 23 | let s = source.map { Comparison(path: $0.fullPath, key: $0.fullPath.remove([$0.ancestor.removeTrailingSlash()])) } 24 | 25 | let c = PathQuery.availableImages(from: catalog.path).map { path in 26 | return Comparison(path: path, key: path.remove([catalog.path]).removeAssetSetsComponent()) 27 | } 28 | 29 | switch operation { 30 | case .NewAssets: 31 | return new(s, target: c) 32 | case .CommonAssets: 33 | return common(s, target: c) 34 | case .MissingAssets: 35 | return missing(s, target: c) 36 | } 37 | 38 | } 39 | 40 | // MARK - Private 41 | private struct Comparison { 42 | let path: Path 43 | let key: String 44 | } 45 | 46 | private static func common(source: [Comparison], target: [Comparison]) -> Int { 47 | var total = 0 48 | for c in target { 49 | total += source.filter { $0.key == c.key && FileSystem.equal($0.path, c.path) }.count 50 | } 51 | return total 52 | } 53 | 54 | private static func new(source: [Comparison], target: [Comparison]) -> Int { 55 | var new = 0 56 | // Inefficient. I'm too lazy to write an imparative loop right now. // TODO: Swift 2.0, indexOf {} 57 | for s in source { 58 | new += target.filter { $0.key == s.key && FileSystem.equal($0.path, s.path) }.count == 0 ? 1 : 0 59 | } 60 | return new 61 | } 62 | 63 | private static func missing(source: [Comparison], target: [Comparison]) -> Int { 64 | var new = 0 65 | // Inefficient. I'm too lazy to write an imparative loop right now. // TODO: Swift 2.0, indexOf {} 66 | for s in target { 67 | new += source.filter { $0.key == s.key }.count == 0 ? 1 : 0 68 | } 69 | return new 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /XCAssetGenerator/AssetGenerationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetGenerationController.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 9/25/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Result 11 | import ReactiveCocoa 12 | 13 | enum GenerationState { 14 | case Progress(Float) 15 | case Assets(Int) 16 | } 17 | 18 | enum AssetGeneratorError: ErrorType { 19 | case InvalidSource 20 | case InvalidCatalog 21 | 22 | var nsError: NSError { 23 | return NSError() 24 | } 25 | } 26 | 27 | class AssetGenerationController { 28 | 29 | private let assetGenerator: AssetGenerator 30 | let running: MutableProperty = MutableProperty(false) 31 | 32 | init() { 33 | assetGenerator = AssetGenerator() 34 | } 35 | 36 | func assetGenerationProducer(assets: [Asset]?, destination: Path?) -> SignalProducer { 37 | return SignalProducer { (sink, disposable) in 38 | self.running.put(true) 39 | 40 | // TODO: Swift 2.0 guard + defer. 41 | if assets == nil { 42 | sendError(sink, .InvalidSource) 43 | self.running.put(false) 44 | return 45 | } 46 | 47 | if destination == nil { 48 | sendError(sink, .InvalidCatalog) 49 | self.running.put(false) 50 | return 51 | } 52 | 53 | self.assetGenerator.generateAssets(assets!, target: destination!)(observer: sink) { 54 | self.running.put(false) 55 | } 56 | } 57 | } 58 | 59 | func prepare(path: Path) -> Path { 60 | if !path.hasSuffix("/") { 61 | return path + "/" 62 | } 63 | return path 64 | } 65 | 66 | func intermedieteDirectory() -> Path { 67 | let cacheDir = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true).first as! Path 68 | return cacheDir + "/.XCAssetTemp/" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /XCAssetGenerator/AssetGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetGenerator.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/3/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | class AssetGenerator { 12 | typealias AssetGeneratorObserver = Signal.Observer 13 | 14 | func generateAssets(source: [Asset], target: Path)(observer: AssetGeneratorObserver, completion: () -> ()) { 15 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) { 16 | sendNext(observer, .Progress(10)) 17 | let report = self.moveAssetsAndPrepareReport(source, destination: target) 18 | sendNext(observer, .Progress(50)) 19 | self.reticulateReportAndDeploy(report) 20 | sendNext(observer, .Progress(95)) 21 | sendNext(observer, .Assets(source.count)) 22 | sendCompleted(observer) 23 | completion() 24 | } 25 | } 26 | 27 | /// :param: key: Path the assets' `AssetSet` destination. 28 | /// :param: value: [Asset] The assets belonging to the said deestination. 29 | private typealias AssetsReport = [Path: [Asset]] 30 | 31 | private func reticulateReportAndDeploy(rep: AssetsReport) { 32 | for (path, assets) in rep { 33 | let destinationJSON = path + "Contents.json" 34 | 35 | if var json = JSON.readJSON(destinationJSON) as? XCAssetsJSONWrapper, 36 | let imagesJSON = json["images"] as? [XCAssetsJSON] { 37 | sanitizeJSON(imagesJSON) 38 | |> updateAttributesWithAssets(assets) 39 | |> XCAssetsJSONHelper.updateImagesValue(json) 40 | |> JSON.writeJSON(to: destinationJSON) 41 | 42 | } else { 43 | assets.map { AssetMetaData.create($0).attributes.serialized } 44 | |> XCAssetsJSONHelper.createJSONDefaultWrapper 45 | |> JSON.writeJSON(to: destinationJSON) 46 | } 47 | } 48 | } 49 | 50 | 51 | 52 | private func moveAssetsAndPrepareReport(assets: [Asset], destination: Path) -> AssetsReport { 53 | // Find all images in our source folder. 54 | var assetsPerDestination: [Path: [Asset]] = [:] 55 | for asset in assets { 56 | let path = computeEnclosingSet(asset, target: destination) 57 | assetsPerDestination[path] = (assetsPerDestination[path] ?? []) + [asset] 58 | 59 | //If .* doesnt exist, create it. 60 | FileSystem.createDirectoryIfMissing(path) 61 | //Compute the images' final location and proceed to copy. 62 | let finalImageDestination = path + asset.name 63 | FileSystem.copy(file: asset.fullPath, toLocation: finalImageDestination) 64 | } 65 | 66 | return assetsPerDestination 67 | } 68 | 69 | func computeEnclosingSet(asset: Asset, target: Path) -> Path { 70 | switch asset.type { 71 | case .Image: 72 | let subddirectory = asset.relativePath.stringByDeletingLastPathComponent 73 | let cleanSubDirectory = subddirectory.replace([".", ":"], withCharacter: "_") 74 | return String.pathWithComponents([target, cleanSubDirectory, asset.enclosingSet]) + "/" 75 | case .LaunchImage, .Icon: 76 | return String.pathWithComponents([target, asset.enclosingSet]) + "/" 77 | } 78 | } 79 | 80 | 81 | private func updateAttributesWithAssets(assets: [Asset])(assetsJSON: [XCAssetsJSON]) -> [XCAssetsJSON] { 82 | var newJSON = assetsJSON 83 | for i in assets { 84 | let image = AssetMetaData.create(i) 85 | let attributes = image.attributes 86 | let comparator = image.comparator 87 | 88 | // If we find a "matching" entry for image, update its name to new image. If not, add new image to json. 89 | if var entry = assetsJSON.filter(comparator).first, 90 | let index = find(newJSON as [NSDictionary], entry) { 91 | newJSON.removeAtIndex(index) 92 | entry[XCAssetsJSONKeys.Filename] = attributes.filename 93 | newJSON.insert(entry, atIndex: index) 94 | } else { 95 | newJSON.append(attributes.serialized) 96 | } 97 | } 98 | 99 | return newJSON 100 | } 101 | } -------------------------------------------------------------------------------- /XCAssetGenerator/AssetGeneratorInputValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetGeneratorInputValidator.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct AssetGeneratorInputValidator { 12 | 13 | static func validateSource(selection: [Asset]?) -> Bool { 14 | return selection?.count > 0 15 | } 16 | 17 | static func validateTarget(catalog: AssetCatalog?) -> Bool { 18 | return catalog.map { PathValidator.directoryExists(path: $0.path) } ?? false 19 | } 20 | } -------------------------------------------------------------------------------- /XCAssetGenerator/AssetGeneratorWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetGeneratorWindowController.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/11/15. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ReactiveCocoa 11 | 12 | class AssetGeneratorWindowController: NSWindowController { 13 | 14 | var statusLabel: NSTextField! 15 | @IBOutlet var generateButton: NSButton! 16 | 17 | var viewModel: AssetWindowViewModel! 18 | 19 | // MARK: ViewControllers. Btw, i dont need to reference them but they kinda make for a nice little documentation snippet. 20 | var progressController: ProgressViewController! 21 | var imagesViewController: ImagesDropViewController! 22 | var projectViewController: ProjectDropViewController! 23 | 24 | /// MARK: Initializers 25 | 26 | static func instantiate(viewModel: AssetWindowViewModel) -> AssetGeneratorWindowController { 27 | let controller = NSStoryboard(name: "Main", bundle: nil)!.instantiateControllerWithIdentifier("MainWindowController") as! AssetGeneratorWindowController 28 | controller.viewModel = viewModel 29 | controller.setup() 30 | return controller 31 | } 32 | 33 | /// MARK:- Setup Methods. 34 | 35 | func setup() { 36 | super.windowDidLoad() 37 | 38 | layoutVibrancy() 39 | layoutTopbarElements() 40 | layoutSeperator() 41 | 42 | // Bottom bar and status label. 43 | layoutBottomElements() 44 | 45 | createImagesArea() 46 | createProjectArea() 47 | 48 | // RAC Binding. 49 | viewModel.canGenerate.producer 50 | |> observeOn(QueueScheduler.mainQueueScheduler) 51 | |> start(next: { enabled in 52 | self.generateButton.enabled = enabled 53 | }) 54 | 55 | viewModel.statusLabel.producer 56 | |> observeOn(QueueScheduler.mainQueueScheduler) 57 | |> start(next: { label in 58 | self.statusLabel.stringValue = label 59 | }) 60 | 61 | viewModel.generateTitle.producer 62 | |> observeOn(QueueScheduler.mainQueueScheduler) 63 | |> start(next: { title in 64 | self.generateButton.title = title 65 | }) 66 | 67 | } 68 | 69 | @IBAction func generateButtonPressed(sneder: AnyObject!) { 70 | // TODO: Options -> Pushed to 2.0 71 | viewModel.generateAssets() 72 | } 73 | 74 | /// MARK:- UI Setup Helpers. 75 | 76 | private func createImagesArea() { 77 | let imagesViewModel = viewModel.viewModelForImagesGroup() 78 | // imagesViewController = ImagesDropViewController.instantiate(imagesViewModel) 79 | imagesViewController = ImagesDropViewController(viewModel: imagesViewModel) 80 | imagesViewController.view.translatesAutoresizingMaskIntoConstraints = false 81 | 82 | contentViewController?.view.addSubview(imagesViewController.view) 83 | // 84 | let centerFileX = NSLayoutConstraint(item: imagesViewController.view, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.CenterX, multiplier: 0.5, constant: 0) 85 | let centerFileY = NSLayoutConstraint(item: imagesViewController.view, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.CenterY, multiplier: 0.8, constant: 0) 86 | let widthFile = NSLayoutConstraint(item: imagesViewController.view, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.Width, multiplier: 0.5, constant: 0) 87 | let heightFile = NSLayoutConstraint(item: imagesViewController.view, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.Height, multiplier: 1, constant: 0) 88 | 89 | NSLayoutConstraint.activateConstraints([centerFileX, centerFileY, widthFile, heightFile]) 90 | } 91 | 92 | private func createProjectArea() { 93 | let projectViewModel = viewModel.viewModelForSelectedProject() 94 | // projectViewController = ProjectDropViewController.instantiate(projectViewModel) 95 | projectViewController = ProjectDropViewController(viewModel: projectViewModel) 96 | projectViewController.view.translatesAutoresizingMaskIntoConstraints = false 97 | contentViewController?.view.addSubview(projectViewController.view) 98 | 99 | let centerProjX = NSLayoutConstraint(item: projectViewController.view, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.5, constant: 0) 100 | let centerProjY = NSLayoutConstraint(item: projectViewController.view, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.CenterY, multiplier: 0.8, constant: 0) 101 | let widthProj = NSLayoutConstraint(item: projectViewController.view, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.Width, multiplier: 0.5, constant: 0) 102 | let heightProj = NSLayoutConstraint(item: projectViewController.view, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: contentViewController?.view, attribute: NSLayoutAttribute.Height, multiplier: 1, constant: 0) 103 | 104 | NSLayoutConstraint.activateConstraints([centerProjX, centerProjY, widthProj, heightProj]) 105 | } 106 | 107 | private func layoutTopbarElements() { 108 | window!.titleVisibility = NSWindowTitleVisibility.Hidden 109 | 110 | // Progress bar. 111 | let progressViewModel = viewModel.viewModelForProgressIndication() 112 | progressController = ProgressViewController(viewModel: progressViewModel, width: window!.frame.width) 113 | self.window?.standardWindowButton(NSWindowButton.CloseButton)?.superview?.addSubview(progressController.view) 114 | 115 | // Faux Title. 116 | let title = setupLabel("Asset Generator") 117 | title.translatesAutoresizingMaskIntoConstraints = false 118 | self.window?.standardWindowButton(NSWindowButton.CloseButton)?.superview?.addSubview(title) // I... sigh. 119 | let titleConstraints = NSLayoutConstraint.centeringConstraints(title, into: title.superview) 120 | NSLayoutConstraint.activateConstraints(titleConstraints) 121 | } 122 | 123 | private func layoutBottomElements() { 124 | // Bottom translucent bar. 125 | let bar = NSImage(named: "uiBottomBar") 126 | let bottomBar = NSImageView() 127 | bottomBar.translatesAutoresizingMaskIntoConstraints = false 128 | bottomBar.image = bar 129 | contentViewController?.view.addSubview(bottomBar) 130 | 131 | // Status label. 132 | statusLabel = setupLabel("Drop a folder with slices you'd like to add to your Xcode project") 133 | statusLabel.textColor = NSColor(calibratedWhite: 0.4, alpha: 1) 134 | bottomBar.addSubview(statusLabel) 135 | 136 | let posStatusX = NSLayoutConstraint(item: statusLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: statusLabel.superview, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0) 137 | let posStatusY = NSLayoutConstraint(item: statusLabel, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: statusLabel.superview, attribute: NSLayoutAttribute.CenterY, multiplier: 1.1, constant: 0) 138 | 139 | NSLayoutConstraint.activateConstraints([posStatusX, posStatusY]) 140 | } 141 | 142 | /// MARK: Visual layout. 143 | 144 | private func layoutVibrancy() { 145 | let visualEffectView = NSVisualEffectView() 146 | visualEffectView.material = NSVisualEffectMaterial.Titlebar 147 | visualEffectView.blendingMode = NSVisualEffectBlendingMode.BehindWindow 148 | visualEffectView.state = NSVisualEffectState.Active 149 | visualEffectView.wantsLayer = true 150 | visualEffectView.translatesAutoresizingMaskIntoConstraints = false 151 | contentViewController?.view.addSubview(visualEffectView) 152 | let vibrancyConstraints = NSLayoutConstraint.fittingConstraints(visualEffectView, into: contentViewController?.view) 153 | NSLayoutConstraint.activateConstraints(vibrancyConstraints) 154 | } 155 | 156 | private func layoutSeperator() { 157 | let seperatorView = NSImageView() 158 | seperatorView.translatesAutoresizingMaskIntoConstraints = false 159 | seperatorView.image = NSImage(named: "iconArrow") 160 | contentViewController?.view.addSubview(seperatorView) 161 | 162 | let centerSeperatorX: NSLayoutConstraint = NSLayoutConstraint(item: seperatorView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: seperatorView.superview, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0) 163 | 164 | let centerSeperatorY: NSLayoutConstraint = NSLayoutConstraint(item: seperatorView, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: seperatorView.superview, attribute: NSLayoutAttribute.CenterY, multiplier: 0.8, constant: 0) 165 | 166 | NSLayoutConstraint.activateConstraints([centerSeperatorX, centerSeperatorY]) 167 | 168 | } 169 | 170 | // MARK:- Helpers 171 | private func setupLabel(string: String) -> NSTextField { 172 | let field = NSTextField() 173 | field.translatesAutoresizingMaskIntoConstraints = false 174 | field.editable = false 175 | field.backgroundColor = NSColor.controlColor() 176 | field.bordered = false 177 | field.alignment = .CenterTextAlignment 178 | field.font = NSFont.systemFontOfSize(13) 179 | field.stringValue = string 180 | return field 181 | } 182 | } 183 | 184 | -------------------------------------------------------------------------------- /XCAssetGenerator/AssetType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAssetsImage.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/3/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum AssetType { 12 | case Image 13 | case Icon 14 | case LaunchImage 15 | 16 | static func type(#path: Path) -> AssetType { 17 | return type(name: path.lastPathComponent) 18 | } 19 | 20 | static func type(#name: String) -> AssetType { 21 | if isIcon(name) { 22 | return .Icon 23 | } else if isLaunchImage(name) { 24 | return .LaunchImage 25 | } else { 26 | return .Image 27 | } 28 | } 29 | 30 | static func isIcon(name: String) -> Bool { 31 | return name.hasPrefix("AppIcon") || name.hasPrefix("Icon") || isMacIcon(name) 32 | } 33 | static func isMacIcon(name: String) -> Bool { 34 | return name.hasPrefix("icon_") 35 | } 36 | 37 | static func isLaunchImage(name: String) -> Bool { 38 | return name.hasPrefix("LaunchImage") || name.hasPrefix("Default") 39 | } 40 | } 41 | 42 | struct AssetMetaData { 43 | let attributes: AssetAttribute 44 | let type: AssetType 45 | 46 | private init(path: Path,type: AssetType) { 47 | self.type = type 48 | switch self.type { 49 | case .Image: 50 | self.attributes = AssetAttributeProcessor.withAsset(path) 51 | case .Icon: 52 | self.attributes = AssetAttributeProcessor.withIcon(path) 53 | case .LaunchImage: 54 | self.attributes = AssetAttributeProcessor.withLaunchImage(path) 55 | } 56 | } 57 | 58 | static func create(path: Path) -> AssetMetaData { 59 | let type = AssetType.type(path: path) 60 | return AssetMetaData(path: path, type: type) 61 | } 62 | 63 | static func create(asset: Asset) -> AssetMetaData { 64 | return create(asset.fullPath) 65 | } 66 | 67 | typealias AssetComparator = (XCAssetsJSON -> Bool) 68 | var comparator: AssetComparator { 69 | let attribute = attributes 70 | 71 | switch type { 72 | case .Image: 73 | return { dict in 74 | let sameIdiom = dict[XCAssetsJSONKeys.Idiom] as! String? == attribute.idiom 75 | let sameScale = dict[XCAssetsJSONKeys.Scale] as! String? == attribute.scale 76 | return sameIdiom && sameScale 77 | } 78 | case .Icon: 79 | return { dict in 80 | let sameIdiom = dict[XCAssetsJSONKeys.Idiom] as! String? == attribute.idiom 81 | let sameScale = dict[XCAssetsJSONKeys.Scale] as! String? == attribute.scale 82 | let sameSize = dict[XCAssetsJSONKeys.Size] as! String? == attribute.size 83 | return sameIdiom && sameScale && sameSize 84 | } 85 | case .LaunchImage: 86 | return { dict in 87 | let sameIdiom = dict[XCAssetsJSONKeys.Idiom] as! String? == attribute.idiom 88 | let sameScale = dict[XCAssetsJSONKeys.Scale] as! String? == attribute.scale 89 | let sameSubtype = dict[XCAssetsJSONKeys.Subtype] as! String? == attribute.subtype 90 | let sameOrientation = dict[XCAssetsJSONKeys.Orientation] as! String? == attribute.orientation 91 | return sameIdiom && sameScale && sameOrientation && sameSubtype 92 | } 93 | } 94 | } 95 | } 96 | 97 | struct Asset: Printable { 98 | let type: AssetType 99 | 100 | let ancestor: Path 101 | let fullPath: Path 102 | 103 | var name: Path { 104 | return fullPath.lastPathComponent 105 | } 106 | 107 | var description: String { 108 | return fullPath 109 | } 110 | 111 | /// Return path relative to its ancestor. 112 | /// Basically fullPath - ancestor 113 | var relativePath: Path { 114 | return fullPath.remove([ancestor]) 115 | } 116 | 117 | // MARK: - Initializers 118 | 119 | init(fullPath path: Path, ancestor: Path) { 120 | self.fullPath = path 121 | self.ancestor = ancestor 122 | self.type = AssetType.type(path: path) 123 | } 124 | 125 | 126 | // MARK: - Properties 127 | 128 | var enclosingSet: Path { 129 | switch type { 130 | case .Image: 131 | return stripKeywords(fullPath) + ".imageset" 132 | case .Icon: 133 | return "AppIcon.appiconset" 134 | case .LaunchImage: 135 | return "LaunchImage.launchimage" 136 | } 137 | } 138 | 139 | // MARK: - 140 | 141 | /// 142 | /// Removes the various keywords used in Asset Generator. 143 | /// 144 | /// scale: @1x, @2x, @3x 145 | /// idiom: ~iphone, ~ipad 146 | /// extention: .png, .jpg, etc. 147 | private func stripKeywords(path: Path) -> Path { 148 | var name = path.lastPathComponent.stringByDeletingPathExtension.remove([GenerationKeywords.PPI2x, 149 | GenerationKeywords.PPI3x, 150 | GenerationKeywords.PPI1x, 151 | GenerationKeywords.iPhone, 152 | GenerationKeywords.iPad, 153 | GenerationKeywords.Mac ]) 154 | return name 155 | } 156 | } 157 | 158 | enum Device { 159 | case iPhone 160 | case iPad 161 | case Universal 162 | case Mac 163 | case Watch 164 | case NotYetKnownLol 165 | } 166 | -------------------------------------------------------------------------------- /XCAssetGenerator/AssetWindowViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetWindowViewModel.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | class AssetWindowViewModel { 12 | let statusLabel: MutableProperty 13 | let canGenerate = MutableProperty(false) 14 | let generateTitle = MutableProperty("Build") 15 | 16 | private let imagesViewModel: ImagesGroupViewModel 17 | private let projectViewModel: ProjectSelectionViewModel 18 | private var progressViewModel: ProgressIndicationViewModel 19 | private let assetGenerator: AssetGenerationController 20 | 21 | // RAC3 TODO: Main WindowController Initialization. 22 | init() { 23 | imagesViewModel = ImagesGroupViewModel() 24 | projectViewModel = ProjectSelectionViewModel() 25 | progressViewModel = ProgressIndicationViewModel() 26 | assetGenerator = AssetGenerationController() 27 | 28 | statusLabel = MutableProperty("") 29 | 30 | let notCurrentlyGenerating: () -> Bool = { 31 | return self.progressViewModel.animating.value == false 32 | } 33 | 34 | let inputsSignal = combineLatest(imagesViewModel.selectionSignal, projectViewModel.selectionSignal) 35 | let inputsContentSignal = combineLatest(imagesViewModel.contentSignal, projectViewModel.contentSignal) 36 | 37 | statusLabel <~ inputsSignal 38 | |> map { assets, project in 39 | return StatusCrafter.status(assets: assets, target: project) 40 | } 41 | 42 | statusLabel <~ inputsContentSignal 43 | |> filter { _ in notCurrentlyGenerating() } 44 | |> map { _ in 45 | let assets = self.imagesViewModel.assetRepresentation() 46 | let catalog = self.projectViewModel.currentCatalog 47 | return StatusCrafter.status(assets: assets, target: catalog) 48 | } 49 | 50 | // RAC3 TODO: 51 | canGenerate <~ combineLatest(inputsSignal, assetGenerator.running.producer) 52 | // |> throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler) 53 | |> map { input, running in 54 | let validSource = AssetGeneratorInputValidator.validateSource(input.0) 55 | let validTarget = AssetGeneratorInputValidator.validateTarget(input.1) 56 | let notRunning = running == false 57 | return validSource && validTarget && notRunning 58 | } 59 | 60 | generateTitle <~ combineLatest(inputsSignal, inputsContentSignal) 61 | |> filter { _ in notCurrentlyGenerating() } 62 | |> map { _ in "Build" } 63 | } 64 | 65 | func viewModelForImagesGroup() -> ImagesGroupViewModel { 66 | return imagesViewModel 67 | } 68 | 69 | func viewModelForSelectedProject() -> ProjectSelectionViewModel { 70 | return projectViewModel 71 | } 72 | 73 | func viewModelForProgressIndication() -> ProgressIndicationViewModel { 74 | return progressViewModel 75 | } 76 | 77 | func generateAssets() { 78 | // Wut 79 | let assets = imagesViewModel.assetRepresentation() 80 | let catalog = projectViewModel.currentCatalog 81 | // End Wut 82 | assetGenerator.assetGenerationProducer(assets, destination: catalog?.path) 83 | |> startOn(QueueScheduler.mainQueueScheduler) 84 | |> on(started: { 85 | self.progressViewModel.progressStarted() 86 | }, completed: { 87 | self.generateTitle.put("Build Again") 88 | self.progressViewModel.progressFinished() 89 | }, next: { report in 90 | switch report { 91 | case .Progress(let p): 92 | self.progressViewModel.updateProgress(p) 93 | case .Assets(let a): 94 | StatusCrafter.postGeneration(catalog!.title, amount: a) |> self.statusLabel.put 95 | } 96 | }) 97 | |> start() 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /XCAssetGenerator/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /XCAssetGenerator/BookmarkResolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkResolver.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 10/30/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias Path = String 12 | typealias Bookmark = NSData 13 | 14 | struct BookmarkResolver { 15 | 16 | static func resolvePathFromBookmark(data: Bookmark) -> Path? { 17 | let url = NSURL(byResolvingBookmarkData: data, options: NSURLBookmarkResolutionOptions.WithoutMounting, relativeToURL: nil, bookmarkDataIsStale: nil, error: nil) 18 | return url?.path ?? nil 19 | } 20 | 21 | static func resolveBookmarkFromPath(path: Path) -> Bookmark? { 22 | let url: NSURL = NSURL(fileURLWithPath: path)! 23 | return resolveBookmarkFromURL(url) 24 | } 25 | 26 | // TODO: Potential bugs here. Reduce the amount 27 | static func resolveBookmarkFromURL(url: NSURL) -> Bookmark? { 28 | return url.bookmarkDataWithOptions(NSURLBookmarkCreationOptions.SuitableForBookmarkFile, includingResourceValuesForKeys: nil, relativeToURL: nil, error: nil) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /XCAssetGenerator/CABasicAnimation+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CABasicAnimation+Extensions.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/25/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension CABasicAnimation { 12 | static func shakeAnimation(#magnitude: Float) -> CABasicAnimation { 13 | let anim = CABasicAnimation(keyPath: "position.x") 14 | anim.duration = 0.05 15 | anim.repeatCount = 3 16 | anim.autoreverses = true 17 | anim.fromValue = magnitude 18 | anim.toValue = -magnitude 19 | anim.additive = true 20 | return anim 21 | } 22 | } -------------------------------------------------------------------------------- /XCAssetGenerator/DropView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropView.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 9/11/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol DropViewDelegate { 12 | func dropViewShouldAcceptDraggedPath(dropView: DropView, paths: [Path]) -> Bool 13 | func dropViewDidDropFileToView(dropView: DropView, paths: [Path]) 14 | func dropViewDidDragValidFileIntoView(dropView: DropView) // Should be called when folder enters drag areas. 15 | func dropViewDidDragInvalidFileIntoView(dropView: DropView) 16 | func dropViewDidDragFileOutOfView(dropView: DropView) 17 | func dropViewNumberOfAcceptableItems(dropView: DropView, items: [Path]) -> Int 18 | } 19 | 20 | protocol DropViewMouseDelegate { 21 | func dropViewDidRightClick(dropView: DropView, event: NSEvent) 22 | } 23 | 24 | 25 | class DropView: NSView { 26 | 27 | var delegate: DropViewDelegate? 28 | var mouse: DropViewMouseDelegate? 29 | // MARK:- Initializers 30 | 31 | override init(frame frameRect: NSRect) { 32 | super.init(frame: frameRect) 33 | setup() 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | super.init(coder: coder) 38 | setup() 39 | } 40 | 41 | convenience init() { 42 | self.init() 43 | setup() 44 | } 45 | 46 | func setup() { 47 | registerForDraggedTypes([NSFilenamesPboardType]) 48 | self.wantsLayer = true 49 | } 50 | 51 | 52 | // MARK:- Drag Handlers. 53 | 54 | override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation { 55 | let paths = sender.draggingPasteboard().propertyListForType(NSFilenamesPboardType) as! [String] 56 | let acceptDrag = delegate?.dropViewShouldAcceptDraggedPath(self, paths: paths) ?? false 57 | if acceptDrag { 58 | sender.numberOfValidItemsForDrop = delegate?.dropViewNumberOfAcceptableItems(self, items: paths) 59 | ?? sender.numberOfValidItemsForDrop 60 | delegate?.dropViewDidDragValidFileIntoView(self) 61 | return NSDragOperation.Copy 62 | } else { 63 | delegate?.dropViewDidDragInvalidFileIntoView(self) 64 | return NSDragOperation.None 65 | } 66 | } 67 | 68 | override func draggingExited(sender: NSDraggingInfo?) { 69 | delegate?.dropViewDidDragFileOutOfView(self) 70 | } 71 | // override func draggingExited(sender: NSDraggingInfo?) { 72 | // self.delegate?.dropViewDidDragFileOutOfView(self) 73 | // } 74 | 75 | override func prepareForDragOperation(sender: NSDraggingInfo) -> Bool { 76 | return true 77 | } 78 | 79 | override func performDragOperation(sender: NSDraggingInfo) -> Bool { 80 | return true 81 | } 82 | 83 | override func concludeDragOperation(sender: NSDraggingInfo?) { 84 | let filenames = sender!.draggingPasteboard().propertyListForType(NSFilenamesPboardType) as! [String] 85 | delegate?.dropViewDidDropFileToView(self, paths: filenames) 86 | } 87 | 88 | } 89 | 90 | extension DropView { 91 | override func rightMouseDown(theEvent: NSEvent) { 92 | mouse?.dropViewDidRightClick(self, event: theEvent) 93 | } 94 | 95 | // HACK: `MenuForEvent` for some reason does not propogate properly on CTRL+Click (but does on right click...) so work around it. 96 | override func mouseDown(theEvent: NSEvent) { 97 | if (theEvent.type == NSEventType.LeftMouseDown) && (theEvent.modifierFlags.rawValue & NSEventModifierFlags.ControlKeyMask.rawValue != 0) { 98 | mouse?.dropViewDidRightClick(self, event: theEvent) 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /XCAssetGenerator/FileSystemHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSystemHelper.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/3/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct FileSystem { 12 | 13 | static func copy(#file: Path, toLocation location: Path) -> Bool { 14 | if NSFileManager.defaultManager().fileExistsAtPath(location) { 15 | // TODO: Swift 2.0 16 | if equal(file, location) { 17 | return false 18 | } 19 | 20 | NSFileManager.defaultManager().removeItemAtPath(location, error: nil) 21 | } 22 | let success = NSFileManager.defaultManager().copyItemAtPath(file, toPath: location, error: nil) 23 | return success 24 | } 25 | 26 | 27 | static func deleteDirectory(path: Path) -> Bool { 28 | if PathValidator.directoryExists(path: path) { 29 | let status = NSFileManager.defaultManager().removeItemAtPath(path, error: nil) 30 | return status 31 | } 32 | 33 | return false 34 | } 35 | 36 | static func createDirectoryIfMissing(path: Path) -> Bool { 37 | if !PathValidator.directoryExists(path: path) { 38 | let status = NSFileManager.defaultManager().createDirectoryAtPath(path, withIntermediateDirectories: true, attributes: nil, error: nil) 39 | return status 40 | } 41 | return false 42 | } 43 | 44 | static func equal(first: Path, _ second: Path) -> Bool { 45 | return NSFileManager.defaultManager().contentsEqualAtPath(first, andPath: second) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /XCAssetGenerator/FileSystemObserver.h: -------------------------------------------------------------------------------- 1 | // 2 | // FileSystemObserver.h 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 7/1/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol FileSystemObserverDelegate 12 | @required 13 | - (void)FileSystemDirectoryDeleted:(NSString *)path; 14 | - (void)FileSystemDirectory:(NSString *)oldPath renamedTo:(NSString *)newPath; 15 | - (void)FileSystemDirectoryError:(NSError *)error; 16 | - (void)FileSystemDirectoryContentChanged:(NSString *)root; 17 | 18 | @end 19 | 20 | 21 | @interface FileSystemObserver : NSObject 22 | 23 | @property FSEventStreamRef rootStream; 24 | @property FSEventStreamRef contentStream; 25 | @property BOOL ignoreHiddenItems; 26 | 27 | - (void)addObserver:(id)observer forFileSystemPath:(NSString *)path ignoreContents:(BOOL)ignore; 28 | 29 | - (void)addObserver:(id)observer forFileSystemPaths:(NSArray *)paths ignoreContents:(BOOL)ignore; 30 | 31 | - (void)replacePathForObserversFrom:(NSString *)originalPath To:(NSString *)newPath; 32 | - (void)removeObserverForPath:(NSString *)path; 33 | - (void)removeObserverForPath:(NSString *)path restartStream:(BOOL)restart; 34 | - (void)removeAllObservers; 35 | - (void)stopStream; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /XCAssetGenerator/FileSystemObserver.m: -------------------------------------------------------------------------------- 1 | // 2 | // FileSystemObserver.m 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 7/1/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /* 12 | <<>><<>><<>><<>><<>> Sunset this whole mess in Swift 2.0 <<>><<>><<>><<>><<>> 13 | */ 14 | 15 | #import "FileSystemObserver.h" 16 | 17 | static void fs_callback(ConstFSEventStreamRef ref, void * data, size_t events, void * paths, const FSEventStreamEventFlags flags[], const FSEventStreamEventId ids[]); 18 | 19 | 20 | @interface FileSystemObserver () 21 | @property (nonatomic, strong) NSMutableDictionary *observers; // dictionary of paths: Array 22 | @property (nonatomic, strong) NSMutableDictionary *pathFileDescriptors; 23 | @property (nonatomic, strong) NSMutableArray *rootsToObserve; 24 | @property (nonatomic, strong) NSMutableArray *contentsToObserve; 25 | @end 26 | 27 | @implementation FileSystemObserver 28 | 29 | #pragma mark - Initialization 30 | - (id)init 31 | { 32 | self = [super init]; 33 | if (self == nil) 34 | return nil; 35 | 36 | // _pathsToObserve = [[NSMutableArray alloc] init]; 37 | _observers = [[NSMutableDictionary alloc] init]; 38 | _pathFileDescriptors = [[NSMutableDictionary alloc] init]; 39 | 40 | _ignoreHiddenItems = YES; 41 | _rootsToObserve = [[NSMutableArray alloc] init]; 42 | _contentsToObserve = [[NSMutableArray alloc] init]; 43 | return self; 44 | } 45 | 46 | - (void)dealloc 47 | { 48 | _observers = nil; 49 | _pathFileDescriptors = nil; 50 | _rootsToObserve = nil; 51 | _contentsToObserve = nil; 52 | 53 | if (self.rootStream == NULL && self.contentStream == NULL) return; 54 | 55 | FSEventStreamStop(self.rootStream); 56 | FSEventStreamUnscheduleFromRunLoop(self.rootStream, 57 | CFRunLoopGetCurrent(), 58 | kCFRunLoopDefaultMode); 59 | FSEventStreamInvalidate(self.rootStream); 60 | FSEventStreamRelease(self.rootStream); 61 | 62 | FSEventStreamStop(self.contentStream); 63 | FSEventStreamUnscheduleFromRunLoop(self.contentStream, 64 | CFRunLoopGetCurrent(), 65 | kCFRunLoopDefaultMode); 66 | FSEventStreamInvalidate(self.contentStream); 67 | FSEventStreamRelease(self.contentStream); 68 | 69 | 70 | } 71 | 72 | #pragma MARK - Observers Management 73 | 74 | - (void)addObserver:(id)observer forFileSystemPaths:(NSArray *)paths ignoreContents:(BOOL)ignore 75 | { 76 | [paths enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 77 | [self addObserver:observer forFileSystemPath:obj ignoreContents:ignore]; 78 | }]; 79 | } 80 | 81 | - (void)addObserver:(id)observer forFileSystemPath:(NSString *)path ignoreContents:(BOOL)ignore 82 | { 83 | const char *cStringPath = [path cStringUsingEncoding:NSASCIIStringEncoding]; 84 | int fd = open(cStringPath, 0); 85 | 86 | if (fd == -1) { 87 | // Initialization failed error. 88 | [observer FileSystemDirectoryError:nil]; 89 | return ; 90 | } 91 | 92 | if (!ignore && self.contentStream != NULL) 93 | [self invalidateContentStream]; 94 | 95 | if (self.rootStream != NULL) 96 | [self invalidateStream]; 97 | 98 | 99 | if (!ignore && ![self.contentsToObserve containsObject:path]) 100 | [self.contentsToObserve addObject:path]; 101 | 102 | if (![self.rootsToObserve containsObject:path]) { 103 | [self.rootsToObserve addObject:path]; 104 | } 105 | 106 | [self.pathFileDescriptors setObject:@(fd) forKey:path]; 107 | [self.observers setValue:@[observer] forKey:path]; 108 | 109 | [self createStream]; 110 | } 111 | 112 | - (void)removeObserverForPath:(NSString *)path 113 | { 114 | [self removeObserverForPath:path restartStream:YES]; 115 | } 116 | 117 | - (void)removeObserverForPath:(NSString *)path restartStream:(BOOL)restart 118 | { 119 | [self.observers removeObjectForKey:path]; 120 | [self.rootsToObserve removeObject:path]; 121 | [self.contentsToObserve removeObject:path]; 122 | [self.pathFileDescriptors removeObjectForKey:path]; 123 | 124 | if (restart) 125 | [self restartStream]; 126 | } 127 | 128 | - (void)removeAllObservers 129 | { 130 | [self.rootsToObserve removeAllObjects]; 131 | [self.contentsToObserve removeAllObjects]; 132 | [self.observers removeAllObjects]; 133 | [self.pathFileDescriptors removeAllObjects]; 134 | // [self invalidateStream]; 135 | } 136 | 137 | #pragma MARK - Stream Managements 138 | 139 | 140 | - (void)createStream 141 | { 142 | FSEventStreamContext context = { 0, (__bridge void *)self, NULL, NULL, NULL }; 143 | 144 | self.rootStream = FSEventStreamCreate(nil, 145 | &fs_callback, 146 | &context, 147 | (__bridge CFArrayRef)self.rootsToObserve, 148 | kFSEventStreamEventIdSinceNow, 149 | (CFAbsoluteTime)0.2, 150 | kFSEventStreamCreateFlagWatchRoot); 151 | 152 | if (self.rootStream != NULL) { 153 | // start the stream on the main event loop IFF stream was successfully created. 154 | FSEventStreamScheduleWithRunLoop(self.rootStream, 155 | CFRunLoopGetCurrent(), 156 | kCFRunLoopDefaultMode); 157 | FSEventStreamStart(self.rootStream); 158 | 159 | } 160 | 161 | FSEventStreamContext context2 = { 0, (__bridge void *)self, NULL, NULL, NULL }; 162 | if (self.contentsToObserve.count) { 163 | self.contentStream = FSEventStreamCreate(NULL, 164 | &fs_callback, 165 | &context2, 166 | (__bridge CFArrayRef)self.contentsToObserve, 167 | kFSEventStreamEventIdSinceNow, 168 | (CFAbsoluteTime)0.2, 169 | kFSEventStreamCreateFlagFileEvents); 170 | 171 | if (self.contentStream != NULL) { 172 | // start the stream on the main event loop IFF stream was successfully created. 173 | FSEventStreamScheduleWithRunLoop(self.contentStream, 174 | CFRunLoopGetCurrent(), 175 | kCFRunLoopDefaultMode); 176 | FSEventStreamStart(self.contentStream); 177 | } 178 | } 179 | 180 | } 181 | 182 | 183 | - (void)restartStream 184 | { 185 | 186 | if (self.rootStream != nil) 187 | [self invalidateStream]; 188 | if (self.contentStream != nil) 189 | [self invalidateStream]; 190 | 191 | [self createStream]; 192 | } 193 | 194 | - (void)stopStream 195 | { 196 | [self invalidateStream]; 197 | } 198 | 199 | - (void)invalidateContentStream 200 | { 201 | // TODO: 202 | if (self.contentStream == NULL) return; 203 | 204 | FSEventStreamStop(self.contentStream); 205 | FSEventStreamUnscheduleFromRunLoop(self.contentStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 206 | FSEventStreamInvalidate(self.contentStream); 207 | FSEventStreamRelease(self.contentStream); 208 | 209 | self.contentStream = NULL; 210 | } 211 | 212 | -(void)invalidateStream 213 | { 214 | if (self.rootStream == NULL) return; 215 | 216 | FSEventStreamStop(self.rootStream); 217 | FSEventStreamUnscheduleFromRunLoop(self.rootStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 218 | FSEventStreamInvalidate(self.rootStream); 219 | FSEventStreamRelease(self.rootStream); 220 | 221 | self.rootStream = NULL; 222 | 223 | [self invalidateContentStream]; 224 | } 225 | 226 | 227 | // Returns the main directory that we are observing -- given a subpath 228 | - (NSString *)observedPathForSubdirectory:(NSString *)subpath 229 | { 230 | __block NSString *matchingPath = nil; 231 | [self.rootsToObserve enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 232 | if ([subpath containsString:obj]) { 233 | matchingPath = obj; 234 | *stop = YES; 235 | } 236 | }]; 237 | return matchingPath; 238 | } 239 | 240 | - (NSString *)parentDirectoryForSubdirectory:(NSString *)sub fromDirectories:(NSArray *)directories 241 | { 242 | __block NSString *matchingPath = nil; 243 | [directories enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 244 | if ([sub containsString:obj]) { 245 | matchingPath = obj; 246 | *stop = YES; 247 | } 248 | }]; 249 | 250 | return matchingPath; 251 | 252 | } 253 | 254 | //-(void)debugPathsBeingObserved 255 | //{ 256 | // NSArray *pathsBeingObserved = (__bridge NSArray *)(FSEventStreamCopyPathsBeingWatched(self.fileStream)); 257 | // NSLog(@"%@",pathsBeingObserved); 258 | //} 259 | 260 | - (void)replacePathForObserversFrom:(NSString *)originalPath To:(NSString *)newPath 261 | { 262 | NSArray *observersOfOriginalPath = [self.observers objectForKey:originalPath]; 263 | BOOL ignore = [self.contentsToObserve containsObject:originalPath] ? NO : YES; 264 | if (observersOfOriginalPath != nil) { 265 | [self removeObserverForPath:originalPath restartStream:NO]; 266 | [observersOfOriginalPath enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 267 | [self addObserver:obj forFileSystemPath:newPath ignoreContents:ignore]; 268 | }]; 269 | 270 | } 271 | } 272 | 273 | BOOL isHidden(NSString *path) 274 | { 275 | NSArray *components = path.pathComponents; 276 | __block BOOL isHidden = NO; 277 | [components enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 278 | if ([obj hasPrefix:@"."]) { 279 | isHidden = YES; 280 | *stop = YES; 281 | } 282 | }]; 283 | return isHidden; 284 | } 285 | 286 | static void 287 | fs_callback(ConstFSEventStreamRef ref, void * data, size_t events, void * paths, const FSEventStreamEventFlags flags[], const FSEventStreamEventId ids[]) 288 | { 289 | FileSystemObserver *theObserver = (__bridge FileSystemObserver *)data; 290 | size_t i; 291 | char ** _paths = paths; 292 | NSString *path = [NSString stringWithFormat:@"%s", _paths[0]]; 293 | NSArray *pathsBeingObserved = (__bridge NSArray *)(FSEventStreamCopyPathsBeingWatched(ref)); 294 | BOOL contentChanged = NO; 295 | 296 | for (i = 0; i < events; ++i) { 297 | 298 | if ( ref == theObserver.contentStream && (flags[i] & kFSEventStreamEventFlagItemRenamed || flags[i] & kFSEventStreamEventFlagItemRemoved || flags[i] & kFSEventStreamEventFlagItemCreated)) { 299 | // Ignore the TEMP file that we create. 300 | if (theObserver.ignoreHiddenItems) { 301 | // if (!isHidden(path)) // Sometimes .DS_Store eats copy events. So check everything until we find a better way. 302 | contentChanged = YES; 303 | 304 | } else { 305 | contentChanged = YES; 306 | } 307 | } 308 | 309 | if (flags[i] & kFSEventStreamEventFlagRootChanged) { 310 | NSString *thePath = [theObserver parentDirectoryForSubdirectory:path 311 | fromDirectories:pathsBeingObserved]; 312 | 313 | int descriptor = ((NSNumber *)theObserver.pathFileDescriptors[thePath]).intValue; 314 | char filePath[PATH_MAX]; 315 | NSArray *observers = [theObserver.observers objectForKey:thePath]; 316 | if (observers) { 317 | // Resolve the path using its filde. 318 | if (fcntl(descriptor, F_GETPATH, filePath) != -1) 319 | { 320 | NSString *newPath = [NSString stringWithUTF8String:filePath]; 321 | BOOL isTrashed = [newPath containsString:@"/.Trash"]; 322 | BOOL isOldPathEqualToNewPath = [path isEqualToString:newPath]; // This can happen when we "rm". 323 | // No other notifications are sent except a rename with non-changed filename 324 | 325 | if (isOldPathEqualToNewPath || isTrashed) { 326 | [observers enumerateObjectsUsingBlock:^(id observer, NSUInteger idx, BOOL *stop) { 327 | [observer FileSystemDirectoryDeleted:path]; 328 | }]; 329 | } 330 | 331 | else { 332 | [observers enumerateObjectsUsingBlock:^(id observer, NSUInteger idx, BOOL *stop) { 333 | [observer FileSystemDirectory:path renamedTo:newPath]; 334 | }]; 335 | } 336 | } else { 337 | // Cannot resolve the proper filde. Jump ship. 338 | [observers enumerateObjectsUsingBlock:^(id observer, NSUInteger idx, BOOL *stop) { 339 | [observer FileSystemDirectoryError:nil]; 340 | }]; 341 | } 342 | 343 | } 344 | } 345 | 346 | } // End Events loop 347 | 348 | // Notify the observers if content changed. 349 | if (contentChanged) { 350 | NSString *thePath = [theObserver parentDirectoryForSubdirectory:path 351 | fromDirectories:pathsBeingObserved]; 352 | NSArray *observers = [theObserver.observers objectForKey:thePath]; 353 | [observers enumerateObjectsUsingBlock:^(id observer, NSUInteger idx, BOOL *stop) { 354 | [observer FileSystemDirectoryContentChanged:thePath]; 355 | }]; 356 | } 357 | 358 | 359 | 360 | } 361 | 362 | @end -------------------------------------------------------------------------------- /XCAssetGenerator/FileSystemObserverSignal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSystemSignal.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 7/1/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | import ReactiveCocoa 12 | 13 | /* 14 | This is gonna get the Sunset treatment soon. 15 | */ 16 | 17 | class FileSystemProjectObserver: NSObject, FileSystemObserverDelegate { 18 | var observer: FileSystemObserver 19 | 20 | let projectSignal: Signal 21 | private let projectSink: Signal.Observer 22 | 23 | let catalogSignal: Signal 24 | private let catalogSink: Signal.Observer 25 | 26 | let catalogContentSignal: Signal 27 | private let catalogContentSink: Signal.Observer 28 | 29 | override init() { 30 | observer = FileSystemObserver() 31 | 32 | (projectSignal, projectSink) = Signal.pipe() 33 | (catalogSignal, catalogSink) = Signal.pipe() 34 | (catalogContentSignal, catalogContentSink) = Signal.pipe() 35 | } 36 | 37 | func observe(project: XCProject?) -> Bool { 38 | if let project = project, catalog = project.catalog { 39 | observer.addObserver(self, forFileSystemPath: project.path, ignoreContents: true) 40 | observer.addObserver(self, forFileSystemPath: catalog.path, ignoreContents: false) 41 | return true 42 | } else { 43 | stop() 44 | return false 45 | } 46 | } 47 | 48 | func stop() { 49 | observer.removeAllObservers() 50 | } 51 | 52 | 53 | @objc func FileSystemDirectory(oldPath: String!, renamedTo newPath: String!) { 54 | if oldPath.isXCProject() && newPath.isXCProject() { 55 | sendNext(projectSink, XCProject(path: newPath)) 56 | } 57 | 58 | if oldPath.isAssetCatalog() && newPath.isAssetCatalog() { 59 | sendNext(catalogSink, AssetCatalog(path: newPath)) 60 | } 61 | } 62 | 63 | @objc func FileSystemDirectoryContentChanged(root: String!) { 64 | 65 | sendNext(catalogContentSink, Void()) 66 | } 67 | 68 | @objc func FileSystemDirectoryDeleted(path: String!) { 69 | sendNext(projectSink, nil) 70 | } 71 | 72 | @objc func FileSystemDirectoryError(error: NSError!) { 73 | // TODO: 74 | } 75 | 76 | } 77 | 78 | class FileSystemImagesObserver: NSObject, FileSystemObserverDelegate { 79 | var observer: FileSystemObserver 80 | 81 | private var current: ImageSelection 82 | 83 | let selectionSignal: Signal 84 | private let selectionChangedSink: Signal.Observer 85 | 86 | let contentChangedSignal: Signal 87 | private let contentChangedSink: Signal.Observer 88 | 89 | override init() { 90 | observer = FileSystemObserver() 91 | current = .None 92 | 93 | (selectionSignal, selectionChangedSink) = Signal.pipe() 94 | (contentChangedSignal, contentChangedSink) = Signal.pipe() 95 | } 96 | 97 | func observe(selection: ImageSelection) { 98 | current = selection 99 | stop() 100 | switch current { 101 | case .None: 102 | break 103 | case .Folder(let path): 104 | observer.addObserver(self, forFileSystemPath: path, ignoreContents: false) 105 | case .Images(let paths): 106 | observer.addObserver(self, forFileSystemPaths: paths, ignoreContents: true) 107 | } 108 | } 109 | 110 | func stop() { 111 | observer.removeAllObservers() 112 | } 113 | 114 | func FileSystemDirectory(oldPath: String!, renamedTo newPath: String!) { 115 | 116 | switch current { 117 | case .Folder: 118 | sendNext(selectionChangedSink, ImageSelection.create(newPath)) 119 | case .Images(var paths): 120 | if let idx = find(paths, oldPath) { 121 | paths.removeAtIndex(idx) 122 | paths.append(newPath) 123 | sendNext(selectionChangedSink, ImageSelection.create(paths)) 124 | } 125 | case .None: 126 | break 127 | } 128 | } 129 | 130 | func FileSystemDirectoryContentChanged(root: String!) { 131 | sendNext(contentChangedSink, Void()) 132 | } 133 | 134 | func FileSystemDirectoryDeleted(path: String!) { 135 | switch current { 136 | case .Folder: 137 | current = .None 138 | sendNext(selectionChangedSink, .None) 139 | case .Images(var paths): 140 | if let idx = find(paths, path) { 141 | paths.removeAtIndex(idx) 142 | current = ImageSelection.create(paths) 143 | sendNext(selectionChangedSink, current) 144 | } 145 | case .None: 146 | break 147 | } 148 | } 149 | 150 | func FileSystemDirectoryError(error: NSError!) { 151 | // TODO: 152 | } 153 | 154 | } 155 | 156 | -------------------------------------------------------------------------------- /XCAssetGenerator/ImageSelection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSelection.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | enum ImageSelection: Printable, Serializable { 10 | case Images([Path]) 11 | case Folder(Path) 12 | case None 13 | 14 | // TODO: Documentation 15 | static func create(path: Path) -> ImageSelection { 16 | return isSupportedImage(path) ? .Images([path]) : PathValidator.directoryExists(path: path) ? .Folder(path) : .None 17 | } 18 | 19 | static func create(paths: [Path]) -> ImageSelection { 20 | if paths.count == 1 { 21 | return create(paths[0]) 22 | } 23 | let acceptable = paths.filter(isSupportedImage) 24 | return acceptable.count > 0 ? .Images(acceptable) : .None 25 | } 26 | 27 | func analysis(@noescape #ifNone: () -> T, @noescape ifImages: [Path] -> T, @noescape ifFolder: Path -> T) -> T { 28 | switch self { 29 | case .None: 30 | return ifNone() 31 | case .Images(let images): 32 | return ifImages(images) 33 | case .Folder(let folder): 34 | return ifFolder(folder) 35 | } 36 | } 37 | 38 | func asAssets() -> [Asset]? { 39 | return analysis( 40 | ifNone: { nil }, 41 | ifImages: { $0.map { Asset(fullPath: $0, ancestor: $0.stringByDeletingLastPathComponent) } }, 42 | ifFolder: { folder in PathQuery.availableImages(from: folder).map { Asset(fullPath: $0, ancestor: folder)}}) 43 | } 44 | 45 | // MARK: - Printable 46 | 47 | var description: String { 48 | return analysis( 49 | ifNone: { "None:" }, 50 | ifImages: { "Image: \($0)" }, 51 | ifFolder: { "Folder: \($0)" }) 52 | } 53 | 54 | 55 | // MARK: - Serializable 56 | 57 | typealias Serialized = [Bookmark]? 58 | 59 | var serialized: Serialized { 60 | return analysis( 61 | ifNone: { nil }, 62 | ifImages: { paths in 63 | // Swift 2.0 TODO: 64 | let valid = paths.map(BookmarkResolver.resolveBookmarkFromPath).flatMap { $0 != nil ? [$0!] : [] } 65 | return valid.count > 0 ? valid : nil }, 66 | ifFolder: { folder in 67 | let valid = BookmarkResolver.resolveBookmarkFromPath(folder) 68 | return valid != nil ? [valid!] : nil }) 69 | } 70 | 71 | static func deserialize(serial: Serialized) -> ImageSelection { 72 | if let s = serial { 73 | let paths = s.map(BookmarkResolver.resolvePathFromBookmark).filter { $0 != nil }.map{ $0! } as [Path] 74 | return create(paths) 75 | } else { 76 | return .None 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/iconArrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "iconArrow.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "iconArrow@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/iconArrow.imageset/iconArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/iconArrow.imageset/iconArrow.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/iconArrow.imageset/iconArrow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/iconArrow.imageset/iconArrow@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/uiBottomBar.imageset/uiBottomBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/uiBottomBar.imageset/uiBottomBar.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/uiBottomBar.imageset/uiBottomBar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/uiBottomBar.imageset/uiBottomBar@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/uiBottombar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "uiBottomBar.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "uiBottomBar@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/uiWell.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "uiWell.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "uiWell@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/uiWell.imageset/uiWell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/uiWell.imageset/uiWell.png -------------------------------------------------------------------------------- /XCAssetGenerator/Images.xcassets/uiWell.imageset/uiWell@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebitsllc/Asset-Generator-Mac/862dd71e066101de402275bc742557f80c24455a/XCAssetGenerator/Images.xcassets/uiWell.imageset/uiWell@2x.png -------------------------------------------------------------------------------- /XCAssetGenerator/ImagesDropView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /XCAssetGenerator/ImagesDropViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagesDropViewController.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/11/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | // TODO: Swift 2.0: Protocol extension 12 | enum DropState { 13 | case None 14 | case Hovering 15 | case Accepted 16 | case Rejected 17 | } 18 | 19 | let thin: (border: CGFloat, width: CGFloat) = (border: 1, width: 125) 20 | let fat: (border: CGFloat, width: CGFloat) = (border: 3, width: 130) 21 | 22 | class ImagesDropViewController: NSViewController { 23 | 24 | @IBOutlet var dropView: RoundedDropView! 25 | @IBOutlet var dropImageView: NSImageView! 26 | @IBOutlet var well: NSImageView! 27 | @IBOutlet var label: NSTextField! 28 | @IBOutlet var heightConstraint: NSLayoutConstraint! 29 | @IBOutlet var widthConstraint: NSLayoutConstraint! 30 | 31 | let viewModel: ImagesGroupViewModel 32 | 33 | init?(viewModel: ImagesGroupViewModel) { 34 | self.viewModel = viewModel 35 | super.init(nibName: "ImagesDropView", bundle: nil) 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | dropView.delegate = self 46 | dropView.mouse = self 47 | 48 | dropImageView.unregisterDraggedTypes() 49 | well.unregisterDraggedTypes() 50 | 51 | viewModel.currentSelectionValid.producer 52 | |> on(next: { valid in 53 | self.layoutUI(valid ? .Accepted : .None) }) 54 | |> start() 55 | 56 | viewModel.label.producer 57 | |> on(next: { label in 58 | self.label.stringValue = label }) 59 | |> start() 60 | } 61 | 62 | 63 | private func layoutUI(state: DropState) { 64 | switch state { 65 | case .Hovering: 66 | dropView.layer?.borderWidth = fat.border 67 | heightConstraint.constant = fat.width 68 | widthConstraint.constant = fat.width 69 | dropView.layer?.borderColor = NSColor.dropViewHoveringColor().CGColor 70 | case .Accepted: 71 | dropView.layer?.borderWidth = fat.border 72 | heightConstraint.constant = fat.width 73 | widthConstraint.constant = fat.width 74 | dropView.layer?.borderColor = NSColor.dropViewAcceptedColor().CGColor 75 | dropView.layer?.backgroundColor = NSColor.whiteColor().CGColor 76 | well.hidden = true 77 | dropImageView.image = self.viewModel.systemImageForCurrentPath() 78 | case .None: 79 | dropView.layer?.borderWidth = thin.border 80 | heightConstraint.constant = thin.width 81 | widthConstraint.constant = thin.width 82 | dropView.layer?.borderColor = NSColor(calibratedRed: 0.652 , green: 0.673, blue: 0.696, alpha: 1).CGColor 83 | dropView.layer?.backgroundColor = NSColor.clearColor().CGColor 84 | well.hidden = false 85 | dropImageView.image = nil 86 | case .Rejected: 87 | dropView.layer?.borderWidth = fat.border 88 | heightConstraint.constant = fat.width 89 | widthConstraint.constant = fat.width 90 | dropView.layer?.borderColor = NSColor.dropViewRejectedColor().CGColor 91 | } 92 | } 93 | } 94 | 95 | // MARK:- DropView drag delegate 96 | extension ImagesDropViewController: DropViewDelegate { 97 | func dropViewDidDragFileOutOfView(dropView: DropView) { 98 | viewModel.isCurrentSelectionValid() ? layoutUI(.Accepted) : layoutUI(.None) 99 | } 100 | 101 | func dropViewDidDragInvalidFileIntoView(dropView: DropView) { 102 | layoutUI(.Rejected) 103 | let anim = CABasicAnimation.shakeAnimation(magnitude: 10) 104 | view.layer?.addAnimation(anim, forKey: "x") 105 | } 106 | 107 | func dropViewDidDragValidFileIntoView(dropView: DropView) { 108 | layoutUI(.Hovering) 109 | } 110 | 111 | func dropViewDidDropFileToView(dropView: DropView, paths: [Path]) { 112 | viewModel.newPathSelected(paths) 113 | } 114 | 115 | func dropViewShouldAcceptDraggedPath(dropView: DropView, paths: [Path]) -> Bool { 116 | return viewModel.shouldAcceptSelection(paths) 117 | } 118 | 119 | func dropViewNumberOfAcceptableItems(dropView: DropView, items: [Path]) -> Int { 120 | return viewModel.acceptableItemsOfSelection(items) 121 | } 122 | } 123 | 124 | // MARK:- DropView mouse delegate 125 | extension ImagesDropViewController: DropViewMouseDelegate { 126 | func dropViewDidRightClick(dropView: DropView, event: NSEvent) { 127 | let enabled = viewModel.isCurrentSelectionValid() 128 | 129 | let reveal = NSMenuItem(title: "Show in Finder", action:Selector("revealMenuPressed"), keyEquivalent: "") 130 | reveal.enabled = enabled 131 | 132 | let clear = NSMenuItem(title: "Clear Selection", action: Selector("clearMenuPressed"), keyEquivalent: "") 133 | clear.enabled = enabled 134 | 135 | let menu = NSMenu(title: "Asset Generator") 136 | menu.autoenablesItems = false 137 | menu.insertItem(reveal, atIndex: 0) 138 | menu.insertItem(clear, atIndex: 1) 139 | NSMenu.popUpContextMenu(menu, withEvent: event, forView: self.view) 140 | } 141 | 142 | func clearMenuPressed() { 143 | viewModel.clearSelection() 144 | } 145 | 146 | func revealMenuPressed() { 147 | let items = viewModel.urlRepresentation() 148 | NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs(items) 149 | } 150 | } -------------------------------------------------------------------------------- /XCAssetGenerator/ImagesGroupViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagesGroupViewModel.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | class ImagesGroupViewModel { 13 | private let selection: MutableProperty 14 | private let contentChanged: MutableProperty 15 | private let storage: ImagesStorage 16 | private let observer: FileSystemImagesObserver 17 | 18 | let label: MutableProperty 19 | let currentSelectionValid: MutableProperty 20 | 21 | var selectionSignal: SignalProducer<[Asset]?, NoError> { 22 | return selection.producer 23 | |> map { $0.asAssets() } 24 | } 25 | 26 | var contentSignal: SignalProducer { 27 | return contentChanged.producer 28 | } 29 | 30 | init() { 31 | self.storage = ImagesStorage() 32 | self.selection = MutableProperty(storage.load()) 33 | self.label = MutableProperty("Xcode Slices") 34 | self.currentSelectionValid = MutableProperty(false) 35 | self.contentChanged = MutableProperty() 36 | self.observer = FileSystemImagesObserver() 37 | 38 | currentSelectionValid <~ selection.producer |> map { _ in return self.isCurrentSelectionValid() } 39 | label <~ selection.producer |> map { _ in return self.labelForCurrentSelection() } 40 | 41 | selection <~ observer.selectionSignal 42 | contentChanged <~ observer.contentChangedSignal 43 | 44 | selection.producer 45 | |> throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler) 46 | |> on(next: { s in 47 | self.storage.store(s) 48 | self.observer.observe(s) 49 | }) 50 | |> start() 51 | } 52 | 53 | func labelForCurrentSelection() -> String { 54 | return selection.value.analysis( 55 | ifNone: { "Image assets" }, 56 | ifImages: { $0.count == 1 ? $0[0].lastPathComponent : "Multiple Images" }, 57 | ifFolder: { $0.lastPathComponent }) 58 | } 59 | 60 | func shouldAcceptSelection(paths: [Path]) -> Bool { 61 | if paths.count == 1 { 62 | return paths.filter { isSupportedImage($0) || self.isValidPath($0) }.count > 0 63 | } else { 64 | return paths.filter(isSupportedImage).count > 0 65 | } 66 | } 67 | 68 | func acceptableItemsOfSelection(path: [Path]) -> Int { 69 | if path.count == 1 { 70 | return 1 71 | } 72 | return path.filter(isSupportedImage).count 73 | } 74 | 75 | func assetRepresentation() -> [Asset]? { 76 | return selection.value.asAssets() 77 | } 78 | 79 | private func isValidPath(path: Path) -> Bool { 80 | return PathValidator.directoryExists(path: path) && !path.isXCProject() 81 | } 82 | 83 | func newPathSelected(paths: [Path]) { 84 | // Which is more readable? 85 | // selection.put(.create(paths)) 86 | paths |> ImageSelection.create |> selection.put 87 | } 88 | 89 | func isCurrentSelectionValid() -> Bool { 90 | return selection.value.analysis( 91 | ifNone: { _ in false }, 92 | ifImages: { _ in true }, 93 | ifFolder: { _ in true }) 94 | } 95 | 96 | func systemImageForCurrentPath() -> NSImage? { 97 | return selection.value.analysis( 98 | ifNone: { nil }, 99 | ifImages: { NSImage.systemImageForGroup($0) }, 100 | ifFolder: { NSImage.systemImage($0) }) 101 | } 102 | 103 | func clearSelection() { 104 | selection.put(.None) 105 | } 106 | 107 | func urlRepresentation() -> [NSURL] { 108 | return selection.value.analysis( 109 | ifNone: { [] }, 110 | ifImages: { $0.map { NSURL(fileURLWithPath: $0)! }}, 111 | ifFolder: { [NSURL(fileURLWithPath: $0)!] }) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /XCAssetGenerator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.sourcebits.assetgenerator 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | LSApplicationCategoryType 24 | 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2015 Sourcebits Inc. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /XCAssetGenerator/Keyword Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keywords.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/8/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GenerationKeywords { 12 | static let PPI1x = "@1x" 13 | static let PPI2x = "@2x" 14 | static let PPI3x = "@3x" 15 | 16 | static let iPhone = "~iphone" 17 | static let iPad = "~ipad" 18 | static let Mac = "~mac" 19 | static let Watch = "~watch" 20 | } 21 | 22 | struct XCAssetsJSONKeys { 23 | static let Filename = "filename" 24 | static let Scale = "scale" 25 | static let Idiom = "idiom" 26 | 27 | static let Size = "size" 28 | 29 | static let Orientation = "orientation" 30 | static let Extent = "extent" 31 | static let Subtype = "subtype" 32 | static let MinimumSystemVersion = "minimum-system-version" 33 | 34 | static let WidthClass = "widthClass" 35 | static let HeightClass = "heightClass" 36 | 37 | static let Alignment = "alignment-insets" 38 | static let Slicing = "resizing" 39 | 40 | // watch 41 | static let ScreenWidth = "screenWidth" 42 | static let Role = "role" 43 | 44 | static let Unassigned = "unassigned" 45 | } 46 | -------------------------------------------------------------------------------- /XCAssetGenerator/Localizable.strings: -------------------------------------------------------------------------------- 1 | // 2 | // Localizable.strings 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/4/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | /* Generate button. */ 10 | "Generate" = "Create"; 11 | 12 | /* File Drop Initial State. */ 13 | "Drop a folder with slices here." = "Drop a folder with slices here."; 14 | 15 | /* File Drop Successful Drop State. */ 16 | "Hit Create button to add your slices to the project." = "Hit Create button to add your slices to the project."; 17 | 18 | /* File Drop Does Not Contains Images State. */ 19 | "Drop a folder that contains slice-able images." = "Drop a folder that contains slice-able images."; 20 | 21 | /* File Drop Invalid State. */ 22 | "Drop a folder that contains slice-able images." = "Drop a folder that contains slice-able images."; 23 | 24 | /* File Drop Deleted State. */ 25 | "Seems like your folder has disappeared! Select it again." = "Seems like your folder has disappeared! Select it again."; 26 | 27 | /* Recent project list. */ 28 | "Select a project…" = "Select a project…"; 29 | "Recent Projects" = "Recent Projects"; 30 | 31 | /* Project selection errors */ 32 | "The selected folder does not contain an Xcode Project." = "The selected folder does not contain an Xcode Project."; 33 | "The selected project does not contain a valid xcassets folder." = "The selected project (\(project)) does not contain a valid xcassets folder."; 34 | 35 | /* Assets folder */ 36 | "Invalid Asset Title" = ""; 37 | -------------------------------------------------------------------------------- /XCAssetGenerator/NSColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+Extensions.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/10/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSColor { 12 | static func dropViewAcceptedColor() -> NSColor { 13 | return NSColor(calibratedRed: 0.233, green: 0.819 , blue: 0.251, alpha: 1) 14 | } 15 | 16 | static func dropViewHoveringColor() -> NSColor { 17 | return NSColor(calibratedRed: 0.185, green: 0.390 , blue: 0.820, alpha: 1) 18 | } 19 | 20 | static func dropViewRejectedColor() -> NSColor { 21 | return NSColor(calibratedRed: 0.986, green: 0 , blue: 0.093, alpha: 1) 22 | } 23 | } -------------------------------------------------------------------------------- /XCAssetGenerator/NSImage+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+Extensions.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/20/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import QuickLook 11 | 12 | extension NSImage { 13 | static func systemImage(path: Path, size: NSSize = NSSize(width: 80, height: 80)) -> NSImage { 14 | let image = NSWorkspace.sharedWorkspace().iconForFile(path) 15 | image.size = size 16 | return image 17 | } 18 | 19 | static func systemImageForGroup(paths: [Path], size: NSSize = NSSize(width: 80, height: 80)) -> NSImage { 20 | let image = NSWorkspace.sharedWorkspace().iconForFiles(paths)! 21 | image.size = size 22 | return image 23 | } 24 | } -------------------------------------------------------------------------------- /XCAssetGenerator/NSLayoutConstraint+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraint+Extensions.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/27/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSLayoutConstraint { 12 | static func centeringConstraints(view: NSView, into superView: NSView?) -> [NSLayoutConstraint] { 13 | let centerX: NSLayoutConstraint = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: superView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0) 14 | 15 | let centerY: NSLayoutConstraint = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: superView, attribute: NSLayoutAttribute.CenterY, multiplier: 1, constant: 0) 16 | 17 | return [centerX, centerY] 18 | } 19 | 20 | static func centeringConstraints(view: NSView, into superView: NSView?, size: NSSize) -> [NSLayoutConstraint] { 21 | let centerX = NSLayoutConstraint(item: view, attribute: .CenterX, relatedBy: .Equal, toItem: superView, attribute: .CenterX, multiplier: 1, constant: 0) 22 | let centerY = NSLayoutConstraint(item: view, attribute: .CenterY, relatedBy: .Equal, toItem: superView, attribute: .CenterY, multiplier: 1, constant: 0) 23 | let widthAL = NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .Equal, toItem: superView, attribute: .Width, multiplier: 0, constant: size.width) 24 | let heightAL = NSLayoutConstraint(item: view, attribute: .Height, relatedBy: .Equal, toItem: superView, attribute: .Height, multiplier: 0, constant: size.height) 25 | return [centerX, centerY, widthAL, heightAL] 26 | } 27 | 28 | static func fittingConstraints(view: NSView, into superView: NSView?) -> [NSLayoutConstraint] { 29 | let centerX = NSLayoutConstraint(item: view, attribute: .CenterX, relatedBy: .Equal, toItem: superView, attribute: .CenterX, multiplier: 1, constant: 0) 30 | let centerY = NSLayoutConstraint(item: view, attribute: .CenterY, relatedBy: .Equal, toItem: superView, attribute: .CenterY, multiplier: 1, constant: 0) 31 | let widthAL = NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .Equal, toItem: superView, attribute: .Width, multiplier: 1, constant: 0) 32 | let heightAL = NSLayoutConstraint(item: view, attribute: .Height, relatedBy: .Equal, toItem: superView, attribute: .Height, multiplier: 1, constant: 0) 33 | 34 | return [centerX, centerY, widthAL, heightAL] 35 | } 36 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Optionals+Operators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optionals.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/29/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | infix operator <^> { associativity left } 12 | infix operator <*> { associativity left } 13 | infix operator >>- { associativity left precedence 150 } 14 | infix operator |> { associativity left precedence 95 } 15 | infix operator <| { associativity left precedence 95 } 16 | 17 | public func <^>(f: T -> U, x: T?) -> U? { 18 | return x.map(f) 19 | } 20 | 21 | public func <*>(f: (T -> U)?, x: T?) -> U? { 22 | if let f = f { 23 | return x.map(f) 24 | } else { 25 | return .None 26 | } 27 | } 28 | 29 | public func >>-(x: T?, f: T -> U?) -> U? { 30 | return x.flatMap(f) 31 | } 32 | 33 | public func |> (left: T, @noescape right: T -> U) -> U { 34 | return right(left) 35 | } 36 | 37 | public func <| (@noescape left: T -> U, right: T) -> U { 38 | return left(right) 39 | } 40 | -------------------------------------------------------------------------------- /XCAssetGenerator/PathQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathQuery.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/5/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func isSupportedImage(path: Path) -> Bool { 12 | return path.hasSuffix(".png") || path.hasSuffix(".jpg") || path.hasSuffix(".jpeg") 13 | } 14 | 15 | struct PathQuery { 16 | 17 | static func availableImages(from path: Path) -> [Path] { 18 | return queryWith(path, searchOption: NSDirectoryEnumerationOptions.SkipsHiddenFiles) { 19 | return isSupportedImage($0) 20 | } 21 | } 22 | 23 | static func availableAssetSets(from path: Path) -> [Path] { 24 | return queryWith(path, searchOption: NSDirectoryEnumerationOptions.SkipsPackageDescendants) { 25 | return $0.isAssetSet() 26 | } 27 | } 28 | 29 | static func availableAssetCatalogs(from path: Path) -> [Path] { 30 | return queryWith(path, searchOption: NSDirectoryEnumerationOptions.SkipsPackageDescendants) { 31 | return $0.isAssetCatalog() 32 | } 33 | } 34 | 35 | private static func queryWith(path: Path, searchOption: NSDirectoryEnumerationOptions, query: Path -> Bool) -> [Path] { 36 | let url = NSURL(fileURLWithPath: path, isDirectory: true) 37 | let generator = NSFileManager.defaultManager().enumeratorAtURL(url!, includingPropertiesForKeys: [NSURLIsDirectoryKey], options: searchOption , errorHandler: nil) 38 | 39 | let list = generator?.allObjects as? [NSURL] 40 | let result = list?.map{$0.path!}.filter(query) 41 | return result ?? [] 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Pluralize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pluralize.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/10/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func pluralize(amount: Int, #singular: String, #plural: String) -> String { 12 | switch amount { 13 | case 1: 14 | return "1 \(singular)" 15 | case let a: 16 | return "\(a) \(plural)" 17 | } 18 | } -------------------------------------------------------------------------------- /XCAssetGenerator/ProgressIndicationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressIndicationViewModel.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | enum Progress { 12 | case Started 13 | case Ongoing(Float) 14 | case Finished 15 | } 16 | 17 | class ProgressIndicationViewModel { 18 | let animating: MutableProperty 19 | 20 | let progress: Signal 21 | private let sink: SinkOf> 22 | 23 | let lineWidth: CGFloat = 3 24 | let color: MutableProperty 25 | 26 | init() { 27 | self.animating = MutableProperty(false) 28 | self.color = MutableProperty(NSColor(calibratedRed: 0.047, green: 0.261, blue: 0.993, alpha: 1)) 29 | (self.progress, self.sink) = Signal.pipe() 30 | } 31 | 32 | func updateProgress(amount: Float) { 33 | sendNext(sink, .Ongoing(amount)) 34 | } 35 | 36 | func progressFinished() { 37 | sendNext(sink, .Finished) 38 | } 39 | 40 | func progressStarted() { 41 | sendNext(sink, .Started) 42 | } 43 | 44 | private func isAnimating(progress: Progress) -> Bool { 45 | switch progress { 46 | case .Finished: 47 | return false 48 | case _: 49 | return true 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /XCAssetGenerator/ProgressLineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressLineView.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 6/11/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ProgressLineView: NSView { 12 | private let line: NSView 13 | 14 | var lineWidth: CGFloat = 3 { 15 | didSet { 16 | line.frame.size.height = lineWidth 17 | } 18 | } 19 | 20 | var color: NSColor = NSColor.blackColor() { 21 | didSet { 22 | line.layer?.backgroundColor = color.CGColor 23 | } 24 | } 25 | 26 | init(width: CGFloat) { 27 | let frame = NSRect(x: 0, y: 0, width: width, height: lineWidth) 28 | line = NSView(frame: NSRect(x: 0, y: 0, width: 0, height: lineWidth)) 29 | super.init(frame: frame) 30 | line.wantsLayer = true 31 | line.layer?.masksToBounds = true 32 | line.layer?.backgroundColor = color.CGColor 33 | addSubview(line) 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | func animateTo(#progress: Float) { 41 | let width = bounds.size.width * (CGFloat(progress) / 100) 42 | line.animator().frame.size.width = width 43 | } 44 | 45 | func forceAnimateFullProgress() { 46 | line.animator().frame.size.width = bounds.size.width 47 | } 48 | 49 | func animateFadeOut() { 50 | line.animator().alphaValue = 0 51 | } 52 | 53 | func resetProgress() { 54 | line.animator().frame.size.width = 0 55 | } 56 | 57 | func initiateProgress() { 58 | line.animator().frame.size.width = 0 59 | line.alphaValue = 1 60 | } 61 | } -------------------------------------------------------------------------------- /XCAssetGenerator/ProgressViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressController.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/18/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | import ReactiveCocoa 12 | 13 | class ProgressViewController: NSViewController { 14 | var progressView: ProgressLineView 15 | let viewModel: ProgressIndicationViewModel 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | init?(viewModel: ProgressIndicationViewModel, width: CGFloat) { 22 | self.viewModel = viewModel 23 | progressView = ProgressLineView(width: width) 24 | let lineWidth = viewModel.lineWidth // TODO: right now this is ignored. 25 | super.init(nibName: nil, bundle: nil) 26 | 27 | view = progressView 28 | 29 | /// RAC3 30 | viewModel.progress 31 | |> observeOn(QueueScheduler.mainQueueScheduler) 32 | |> observe(next: { progress in 33 | self.viewModel.animating.put(true) 34 | switch progress { 35 | case .Ongoing(let amount): 36 | self.progressView.animateTo(progress: amount) 37 | case .Finished: 38 | self.resetUI() 39 | case .Started: 40 | self.progressView.initiateProgress() 41 | } 42 | 43 | }) 44 | 45 | viewModel.color.producer 46 | |> observeOn(QueueScheduler.mainQueueScheduler) 47 | |> start(next: { color in 48 | self.progressView.color = color 49 | }) 50 | } 51 | 52 | 53 | func resetProgress(completion: () -> Void) { 54 | NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) -> Void in 55 | self.progressView.forceAnimateFullProgress() 56 | }, completionHandler: { () -> Void in 57 | 58 | NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) -> Void in 59 | self.progressView.animateFadeOut() 60 | }, completionHandler: { () -> Void in 61 | self.progressView.resetProgress() 62 | completion() 63 | }) 64 | }) 65 | 66 | } 67 | 68 | func resetUI() { 69 | NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) -> Void in 70 | self.progressView.forceAnimateFullProgress() 71 | }, completionHandler: { () -> Void in 72 | 73 | NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) -> Void in 74 | self.progressView.animateFadeOut() 75 | }, completionHandler: { () -> Void in 76 | self.progressView.resetProgress() 77 | self.viewModel.animating.put(false) 78 | }) 79 | }) 80 | } 81 | } -------------------------------------------------------------------------------- /XCAssetGenerator/ProjectDropView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /XCAssetGenerator/ProjectDropViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectDropViewController.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/11/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | class ProjectDropViewController: NSViewController { 12 | 13 | @IBOutlet var dropView: RoundedDropView! 14 | @IBOutlet var dropImageView: NSImageView! 15 | @IBOutlet var well: NSImageView! 16 | @IBOutlet var label: NSTextField! 17 | @IBOutlet var heightConstraint: NSLayoutConstraint! 18 | @IBOutlet var widthConstraint: NSLayoutConstraint! 19 | 20 | let viewModel: ProjectSelectionViewModel 21 | 22 | init?(viewModel: ProjectSelectionViewModel) { 23 | self.viewModel = viewModel 24 | super.init(nibName: "ProjectDropView", bundle: nil) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | dropView.delegate = self 36 | dropView.mouse = self 37 | 38 | dropImageView.unregisterDraggedTypes() 39 | well.unregisterDraggedTypes() 40 | 41 | viewModel.label.producer 42 | |> start(next: { label in 43 | self.label.stringValue = label 44 | }) 45 | 46 | viewModel.currentSelectionValid.producer 47 | |> on(next: { valid in 48 | self.layoutUI(valid ? .Accepted : .None) }) 49 | |> start() 50 | 51 | } 52 | 53 | private func layoutUI(state: DropState) { 54 | switch state { 55 | case .Hovering: 56 | dropView.animator().layer?.borderWidth = fat.border 57 | heightConstraint.constant = fat.width 58 | widthConstraint.constant = fat.width 59 | dropView.layer?.borderColor = NSColor.dropViewHoveringColor().CGColor 60 | case .Accepted: 61 | dropView.layer?.borderWidth = fat.border 62 | heightConstraint.constant = fat.width 63 | widthConstraint.constant = fat.width 64 | dropView.layer?.borderColor = NSColor.dropViewAcceptedColor().CGColor 65 | dropView.layer?.backgroundColor = NSColor.whiteColor().CGColor 66 | well.hidden = true 67 | dropImageView.image = self.viewModel.systemImageForCurrentPath() 68 | case .None: 69 | dropView.layer?.borderWidth = thin.border 70 | heightConstraint.constant = thin.width 71 | widthConstraint.constant = thin.width 72 | dropView.layer?.borderColor = NSColor(calibratedRed: 0.652 , green: 0.673, blue: 0.696, alpha: 1).CGColor 73 | dropView.layer?.backgroundColor = NSColor.clearColor().CGColor 74 | well.hidden = false 75 | dropImageView.image = nil 76 | case .Rejected: 77 | dropView.layer?.borderWidth = fat.border 78 | heightConstraint.constant = fat.width 79 | widthConstraint.constant = fat.width 80 | dropView.layer?.borderColor = NSColor.dropViewRejectedColor().CGColor 81 | } 82 | } 83 | } 84 | 85 | // MARK:- DropView drag delegate 86 | extension ProjectDropViewController: DropViewDelegate { 87 | func dropViewDidDragFileOutOfView(dropView: DropView) { 88 | viewModel.isCurrentSelectionValid() ? layoutUI(.Accepted) : layoutUI(.None) 89 | } 90 | 91 | func dropViewDidDragInvalidFileIntoView(dropView: DropView) { 92 | layoutUI(.Rejected) 93 | let anim = CABasicAnimation.shakeAnimation(magnitude: 10) 94 | view.layer?.addAnimation(anim, forKey: "x") 95 | } 96 | 97 | func dropViewDidDragValidFileIntoView(dropView: DropView) { 98 | layoutUI(.Hovering) 99 | } 100 | 101 | func dropViewDidDropFileToView(dropView: DropView, paths: [Path]) { 102 | viewModel.newPathSelected(paths[0]) 103 | } 104 | 105 | func dropViewShouldAcceptDraggedPath(dropView: DropView, paths: [String]) -> Bool { 106 | return viewModel.shouldAcceptPath(paths) 107 | } 108 | 109 | func dropViewNumberOfAcceptableItems(dropView: DropView, items: [Path]) -> Int { 110 | return 1 111 | } 112 | } 113 | 114 | // MARK:- DropView mouse delegate 115 | extension ProjectDropViewController: DropViewMouseDelegate { 116 | func dropViewDidRightClick(dropView: DropView, event: NSEvent) { 117 | let enabled = viewModel.isCurrentSelectionValid() 118 | 119 | let reveal = NSMenuItem(title: "Show in Finder", action:Selector("revealMenuPressed"), keyEquivalent: "") 120 | reveal.enabled = enabled 121 | 122 | let clear = NSMenuItem(title: "Clear Selection", action: Selector("clearMenuPressed"), keyEquivalent: "") 123 | clear.enabled = enabled 124 | 125 | let menu = NSMenu(title: "Asset Generator") 126 | menu.autoenablesItems = false 127 | menu.insertItem(reveal, atIndex: 0) 128 | menu.insertItem(clear, atIndex: 1) 129 | NSMenu.popUpContextMenu(menu, withEvent: event, forView: self.view) 130 | } 131 | 132 | func clearMenuPressed() { 133 | viewModel.clearSelection() 134 | } 135 | 136 | func revealMenuPressed() { 137 | if let item = viewModel.urlRepresentation() { 138 | NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs([item]) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /XCAssetGenerator/ProjectSelectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectSelectionViewModel.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | class ProjectSelectionViewModel { 13 | private let project: MutableProperty 14 | private let contentChanged: MutableProperty 15 | private let storage: ProjectStorage 16 | private let observer: FileSystemProjectObserver 17 | 18 | let label: MutableProperty 19 | let currentSelectionValid: MutableProperty 20 | 21 | var selectionSignal: SignalProducer { 22 | return project.producer |> map { $0?.catalog } 23 | } 24 | 25 | var contentSignal: SignalProducer { 26 | return contentChanged.producer 27 | } 28 | 29 | var currentCatalog: AssetCatalog? { 30 | return project.value?.catalog 31 | } 32 | 33 | init() { 34 | storage = ProjectStorage() 35 | project = MutableProperty(storage.load()) 36 | currentSelectionValid = MutableProperty(false) 37 | label = MutableProperty("Xcode Project") 38 | 39 | contentChanged = MutableProperty() 40 | observer = FileSystemProjectObserver() 41 | 42 | 43 | currentSelectionValid <~ project.producer |> map { $0 != nil } 44 | 45 | project <~ observer.projectSignal 46 | project <~ observer.catalogSignal |> map { catalog in 47 | if let project = self.project.value where project.ownsCatalog(catalog) { 48 | return XCProject(path: project.path) 49 | } else { return nil } 50 | } 51 | 52 | contentChanged <~ observer.catalogContentSignal 53 | 54 | project.producer 55 | |> throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler) 56 | |> on(next: { project in 57 | self.storage.store(project) 58 | self.observer.observe(project) 59 | }) 60 | |> start() 61 | 62 | 63 | label <~ project.producer |> map { $0?.title ?? "Xcode Project" } 64 | } 65 | 66 | 67 | func shouldAcceptPath(path: [Path]) -> Bool { 68 | return path.count == 1 && path[0].isXCProject() && PathValidator.directoryContainsXCAsset(directory: path[0].stringByDeletingLastPathComponent + ("/")) 69 | } 70 | 71 | private func isValidSelection(project: XCProject) -> Bool { 72 | return ProjectValidator.isProjectValid(project) && project.hasValidAssetsPath() 73 | } 74 | 75 | func isCurrentSelectionValid() -> Bool { 76 | return project.value.map(isValidSelection) ?? false 77 | } 78 | 79 | func systemImageForCurrentPath() -> NSImage? { 80 | return project.value.flatMap { NSImage.systemImage($0.path) } 81 | } 82 | 83 | private func forceSyncSelectionValidity() { 84 | currentSelectionValid.put(currentSelectionValid.value) 85 | } 86 | 87 | func newPathSelected(path: Path) { 88 | SignalProducer(result: ProjectSelector.excavateProject(path)) 89 | |> startOn(QueueScheduler(priority: DISPATCH_QUEUE_PRIORITY_DEFAULT, name: "StoreAndObserveQueue")) 90 | |> observeOn(QueueScheduler.mainQueueScheduler) 91 | |> start(error: { error in 92 | self.forceSyncSelectionValidity() 93 | }, next: { project in 94 | self.project.put(project) 95 | }) 96 | } 97 | 98 | 99 | func clearSelection() { 100 | project.put(nil) 101 | } 102 | 103 | func urlRepresentation() -> NSURL? { 104 | return project.value.flatMap { NSURL(fileURLWithPath: $0.path) } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /XCAssetGenerator/ProjectSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectSelection.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/29/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Result 10 | import ReactiveCocoa 11 | 12 | enum ProjectSelectionError: ErrorType { 13 | 14 | case AssetNotFound(String) 15 | case ProjectNotFound 16 | 17 | var nsError: NSError { 18 | let message: String 19 | switch self { 20 | case .AssetNotFound(let project): 21 | message = "The selected project (\(project)) does not contain a valid xcassets folder." 22 | case .ProjectNotFound: 23 | message = NSLocalizedString("The selected folder does not contain an Xcode Project.",comment: "") 24 | } 25 | return NSError(domain: "com.sourcebits.assetgenerator", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) 26 | } 27 | } 28 | 29 | struct ProjectSelector { 30 | 31 | /// Given a URL, find a project with an AssetCatalog. Return ProjectSelectionError if none. 32 | static func excavateProject(url: Path) -> Result { 33 | return projectFromPath(url) >>- assetFromProject 34 | } 35 | 36 | private static func projectFromPath(path: Path) -> Result { 37 | return asProject(path).map { Result.success($0) } ?? retrieveProject(path) 38 | } 39 | 40 | private static func asProject(url: Path) -> Path? { 41 | return url.isXCProject() ? url : nil 42 | } 43 | 44 | private static func retrieveProject(directory: Path) -> Result { 45 | let project = PathValidator.retreiveProject(directory) 46 | return project.map(Result.success) ?? Result.failure(ProjectSelectionError.ProjectNotFound) 47 | } 48 | 49 | private static func assetFromProject(url: Path) -> Result { 50 | let directory = url.stringByDeletingLastPathComponent + ("/") 51 | let hasAsset = PathValidator.directoryContainsXCAsset(directory: directory) 52 | let name = url.lastPathComponent 53 | return (hasAsset) ? Result.success(XCProject(path: url)) : Result.failure(ProjectSelectionError.AssetNotFound(name)) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /XCAssetGenerator/RoundedDropView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundDropView.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/11/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | 13 | class RoundedDropView : DropView { 14 | 15 | required init?(coder: NSCoder) { 16 | super.init(coder: coder) 17 | setupRoundedness() 18 | } 19 | 20 | override init(frame frameRect: NSRect) { 21 | super.init(frame: frameRect) 22 | setupRoundedness() 23 | } 24 | 25 | func setupRoundedness() { 26 | self.layer?.cornerRadius = self.frame.size.width / 2 27 | self.layer?.masksToBounds = true 28 | } 29 | 30 | override func layoutSubtreeIfNeeded() { 31 | setupRoundedness() 32 | } 33 | } -------------------------------------------------------------------------------- /XCAssetGenerator/Serializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Serializable.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/9/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Serializable { 12 | typealias Serialized 13 | 14 | /// Return a serialized representation of value 15 | /// 16 | var serialized: Serialized { get } 17 | 18 | /// Create new value from Serialized parameter 19 | /// 20 | /// :param: 21 | /// :returns: Deserialzed value 22 | // func create(from: Serialized) -> Self 23 | } -------------------------------------------------------------------------------- /XCAssetGenerator/StatusCrafter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusViewModel.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 5/26/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias Status = String 12 | 13 | struct StatusCrafter { 14 | 15 | static func postGeneration(catalog: Path, amount: Int) -> Status { 16 | let s = pluralize(amount, singular: "asset was", plural: "assets were") 17 | return "\(s) added to \(catalog)" 18 | } 19 | 20 | static func status(#assets: [Asset]?, target: AssetCatalog?) -> Status { 21 | switch (assets, target) { 22 | case (.None, _): 23 | return "Drop a folder with slices you'd like to add to your Xcode project" 24 | case (.Some(let a), .None): 25 | let end = (a.count > 0) ? pluralize(a.count, singular: "asset", plural: "assets") : "assets" 26 | return "Choose an Xcode project to add " + end 27 | case (.Some(let a), .Some(let catalog)) where a.count == 0: 28 | return "Add slices to the folder in order to build assets" 29 | case (.Some(let a), .Some(let catalog)): 30 | return newAssetsStatus(a, catalog: catalog) 31 | default: 32 | return "" 33 | } 34 | } 35 | 36 | private static func newAssetsStatus(assets: [Asset], catalog: AssetCatalog) -> Status { 37 | let total = AssetDiff.diffWithOperation(assets, catalog: catalog)(operation: .NewAssets) 38 | let n = pluralize(total, singular: "new asset", plural: "new assets") 39 | return "Hit Build to add \(n) to your project" 40 | } 41 | 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /XCAssetGenerator/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storage.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 7/9/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | // TODO: Swift 2.0 10 | 11 | // Segfaults 12 | protocol Storage { 13 | typealias T: Serializable 14 | func store(item: T) 15 | 16 | func load() -> T 17 | } 18 | 19 | struct ProjectStorage { 20 | private let ProjectKey = "RecentlySelectedProject" 21 | 22 | typealias T = XCProject? 23 | 24 | func load() -> T { 25 | let projectDict = NSUserDefaults.standardUserDefaults().objectForKey(ProjectKey) as? [String: NSData] 26 | var project: XCProject? = nil 27 | func validProject(dict: [String: NSData]) -> Bool { 28 | let validPath = BookmarkResolver.isBookmarkValid(dict[PathKey]) 29 | let validAsset = BookmarkResolver.isBookmarkValid(dict[AssetPathsKey]) 30 | return validPath && validAsset 31 | } 32 | if let dict = projectDict where validProject(dict) { 33 | project = dict |> XCProject.projectFromDictionary 34 | } 35 | 36 | store(project) 37 | return project 38 | } 39 | 40 | func store(item: T) { 41 | if let serialized = item?.serialized { 42 | NSUserDefaults.standardUserDefaults().setObject(serialized, forKey: ProjectKey) 43 | } else { 44 | NSUserDefaults.standardUserDefaults().removeObjectForKey(ProjectKey) 45 | } 46 | } 47 | } 48 | 49 | struct ImagesStorage { 50 | 51 | private let SelectionKey = "RecentlySelectedAssets" 52 | typealias T = ImageSelection 53 | 54 | func store(item: T) { 55 | if let serialized = item.serialized { 56 | NSUserDefaults.standardUserDefaults().setObject(serialized, forKey: SelectionKey) 57 | } else { 58 | NSUserDefaults.standardUserDefaults().removeObjectForKey(SelectionKey) 59 | } 60 | } 61 | 62 | func load() -> T { 63 | let srlz = NSUserDefaults.standardUserDefaults().objectForKey(SelectionKey) as? [Bookmark] 64 | let selection = ImageSelection.deserialize(srlz) 65 | store(selection) 66 | return selection 67 | } 68 | } -------------------------------------------------------------------------------- /XCAssetGenerator/StringExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtension.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/8/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func remove(strings: [String]) -> String { 13 | var v = self 14 | for s in strings { 15 | v = v.stringByReplacingOccurrencesOfString(s, withString: "") 16 | } 17 | return v 18 | } 19 | // TODO: Swift 2.0 20 | func replace(characters: [Character], withCharacter character: Character) -> String { 21 | return String(map(self) { find(characters, $0) == nil ? $0 : character }) 22 | } 23 | 24 | func contains(substring: String) -> Bool { 25 | return self.rangeOfString(substring) != nil 26 | } 27 | 28 | func removeAssetSetsComponent() -> String { 29 | return self.pathComponents.filter { !$0.isAssetSet() } 30 | |> String.pathWithComponents 31 | } 32 | 33 | func removeTrailingSlash() -> String { 34 | var v = self 35 | if v.hasSuffix("/") { 36 | v.removeAtIndex(v.endIndex.predecessor()) 37 | } 38 | return v 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /XCAssetGenerator/Validator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Validator.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 3/12/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension BookmarkResolver { 12 | 13 | static func isBookmarkValid(bookmark: Bookmark?) -> Bool { 14 | if let b = bookmark, let path = resolvePathFromBookmark(b) { 15 | return PathValidator.directoryExists(path: path) 16 | } else { 17 | return false 18 | } 19 | } 20 | } 21 | 22 | struct ProjectValidator { 23 | static func isProjectValid(project: XCProject) -> Bool { 24 | return PathValidator.directoryExists(path: project.path) 25 | } 26 | } 27 | 28 | 29 | struct PathValidator { 30 | 31 | static func directoryContainsInvalidCharacters(#path: Path, options: AnyObject?) -> Bool { 32 | return directoryWith(path, searchOption: NSDirectoryEnumerationOptions.SkipsHiddenFiles) { (url, isDirectory) -> Bool? in 33 | if isDirectory && (contains(url.path!, ".") || contains(url.path!, ":")) { return true } 34 | return nil 35 | } ?? false 36 | } 37 | 38 | static func directoryContainsXCAsset(#directory: Path) -> Bool { 39 | return directoryWith(directory, searchOption: NSDirectoryEnumerationOptions.SkipsHiddenFiles) { (url, isDirectory) -> Bool? in 40 | if isDirectory && url.path!.isAssetCatalog() { return true } 41 | return nil 42 | } ?? false 43 | } 44 | 45 | 46 | static func retreiveProject(path: Path) -> Path? { 47 | return directoryWith(path, searchOption: NSDirectoryEnumerationOptions.SkipsSubdirectoryDescendants) { (url, isDirectory) -> Path? in 48 | if isDirectory && url.path!.isXCProject() { return url.path } 49 | return nil 50 | } 51 | } 52 | 53 | 54 | static func directoryContainsImages(#path: Path) -> Bool { 55 | return directoryWith(path, searchOption: NSDirectoryEnumerationOptions.SkipsHiddenFiles) { (url, isDirectory) -> Bool? in 56 | let isImage = url.path!.hasSuffix(".png") || url.path!.hasSuffix(".jpg") || url.path!.hasSuffix(".jpeg") 57 | if !isDirectory && isImage { return true } 58 | else { return nil } 59 | } ?? false 60 | } 61 | 62 | 63 | private static func directoryWith(path: Path, searchOption: NSDirectoryEnumerationOptions, f: (NSURL, Bool) -> T?) -> T? { 64 | let url = NSURL(fileURLWithPath: path, isDirectory: true) 65 | 66 | let generator = NSFileManager.defaultManager().enumeratorAtURL(url!, includingPropertiesForKeys: [NSURLIsDirectoryKey], options: searchOption, errorHandler: nil) 67 | 68 | while let element = generator?.nextObject() as? NSURL { 69 | 70 | var d: AnyObject? = nil 71 | element.getResourceValue(&d, forKey: NSURLIsDirectoryKey, error: nil) 72 | let isDirectory: Bool = (d as? Bool) ?? false 73 | 74 | if let asset = element.path { 75 | let t = f(element, isDirectory) 76 | if (t != nil) { return t } 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | 84 | static func directoryExists(#path: Path) -> Bool { 85 | var isDirectory: ObjCBool = ObjCBool(false) 86 | if path.rangeOfString("/.Trash") == nil && !path.isEmpty { 87 | NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDirectory) 88 | } 89 | return isDirectory.boolValue 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /XCAssetGenerator/XCAssetGenerator-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "FileSystemObserver.h" -------------------------------------------------------------------------------- /XCAssetGenerator/XCAssetsJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 4/3/15. 6 | // Copyright (c) 2015 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // NOPE 12 | // TODO: 13 | typealias JSONDictionary = NSDictionary 14 | typealias MutableJSONDictionary = NSMutableDictionary 15 | 16 | struct XCAssetsJSONHelper { 17 | static func createJSONDefaultWrapper(images: [XCAssetsJSON]) -> JSONDictionary { 18 | let info = ["version": "1", "author": "Asset Generator"] 19 | let json: JSONDictionary = ["images": images, "info": info] 20 | return json 21 | } 22 | 23 | static func updateImagesValue(json: XCAssetsJSONWrapper)(value: [XCAssetsJSON]) -> JSONDictionary { 24 | var copy = json 25 | copy["images"] = value 26 | return copy 27 | } 28 | 29 | } 30 | 31 | struct JSON { 32 | 33 | static func writeJSON(json: JSONDictionary, toFile file: Path) { 34 | let outputStream = NSOutputStream(toFileAtPath: file, append: false) 35 | outputStream?.open() 36 | NSJSONSerialization.writeJSONObject(json, toStream: outputStream!, options: NSJSONWritingOptions.PrettyPrinted, error: nil) 37 | outputStream?.close() 38 | } 39 | 40 | static func writeJSON(to file: Path)(withJSON json: JSONDictionary) { 41 | let outputStream = NSOutputStream(toFileAtPath: file, append: false) 42 | outputStream?.open() 43 | NSJSONSerialization.writeJSONObject(json, toStream: outputStream!, options: NSJSONWritingOptions.PrettyPrinted, error: nil) 44 | outputStream?.close() 45 | } 46 | 47 | static func readJSON(path: Path) -> JSONDictionary? { 48 | return NSData(contentsOfFile: path).flatMap { NSJSONSerialization.JSONObjectWithData($0, options: NSJSONReadingOptions.MutableContainers, error: nil) as? JSONDictionary } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /XCAssetGenerator/XCProject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCProject.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 9/19/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let PathKey = "XCAssetGeneratorXcodeProjectPath" 12 | let AssetPathsKey = "XCAssetGeneratorXcodeAssetsPath" 13 | 14 | // MARK:- 15 | struct XCProject: Printable { 16 | 17 | let path: Path 18 | private var xcassets: AssetCatalog? 19 | 20 | // MARK:- Initializers 21 | 22 | internal init(path: Path) { 23 | self.path = path 24 | let found = PathQuery.availableAssetCatalogs(from: currentWorkingDirectory) 25 | self.xcassets = found.count > 0 ? AssetCatalog(path: found.first!) : nil 26 | } 27 | 28 | internal init(path: Path, catalogs: AssetCatalog?) { 29 | self.path = path 30 | self.xcassets = catalogs 31 | } 32 | 33 | private var currentWorkingDirectory: Path { 34 | return path.stringByDeletingLastPathComponent + ("/") 35 | } 36 | 37 | var title: String { 38 | return path.lastPathComponent 39 | } 40 | 41 | // MARK: - Printable 42 | 43 | var description: String { 44 | return "path: \(path) -> assets: \(xcassets)" 45 | } 46 | } 47 | 48 | // MARK: - Serializable. 49 | extension XCProject: Serializable { 50 | 51 | /// For the key "PathKey", the NSData is the project bookmark. 52 | /// For the key "AssetPathsKey", the NSData is an array of asset bookmarks. 53 | /// TODO: 2.0: Store all found AssetFolders of the project. (We only store the "selected" one right now. Keep code messy and revisit later). 54 | typealias Serialized = [String: NSData] 55 | 56 | var serialized: Serialized { 57 | get { 58 | let bookmark = BookmarkResolver.resolveBookmarkFromPath(path)! 59 | let assets = xcassets?.serialized ?? Bookmark() 60 | return [PathKey: bookmark, AssetPathsKey: assets] 61 | } 62 | } 63 | 64 | static func projectFromDictionary(dictionary: Serialized) -> XCProject { 65 | 66 | let path = BookmarkResolver.resolvePathFromBookmark(dictionary[PathKey]!)! 67 | var catalogs: AssetCatalog? = nil 68 | if let data = dictionary[AssetPathsKey], let catalog = BookmarkResolver.resolvePathFromBookmark(data) { 69 | catalogs = AssetCatalog(path: catalog) 70 | } 71 | return XCProject(path: path, catalogs: catalogs) 72 | } 73 | } 74 | 75 | 76 | // MARK:- XCProject Assets Public Query Interface 77 | extension XCProject { 78 | 79 | var catalog: AssetCatalog? { 80 | return xcassets 81 | } 82 | 83 | // A project will have a valid assets path if it contains an asset and if the asset path is not empty. 84 | func hasValidAssetsPath() -> Bool { 85 | return xcassets.map { PathValidator.directoryExists(path: $0.path) } ?? false 86 | } 87 | 88 | func ownsCatalog(catalog: AssetCatalog) -> Bool { 89 | return catalog.path.hasPrefix(currentWorkingDirectory) 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /XCAssetGenerator/XcodeFileValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeFileValidatorProtocol.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 11/11/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol XcodeFileValidator { 12 | func isXCProject() -> Bool 13 | func isAssetCatalog() -> Bool 14 | } 15 | 16 | // TODO: We should probably check if its a directory too. 17 | extension String: XcodeFileValidator { 18 | 19 | /* 20 | Determines whether the string is a project or asset. 21 | NOTE: It does not determine whether that associated objects actually exist. 22 | */ 23 | func isXCProject() -> Bool { 24 | return self.hasSuffix(".xcodeproj") 25 | } 26 | 27 | func isAssetCatalog() -> Bool { 28 | return self.hasSuffix(".xcassets") 29 | } 30 | 31 | func isAssetSet() -> Bool { 32 | return PathValidator.isAssetSet(self) 33 | } 34 | } 35 | 36 | extension PathValidator { 37 | static func isAssetSet(path: Path) -> Bool { 38 | return path.hasSuffix(".imageset") || path.hasSuffix(".appiconset") || path.hasSuffix(".launchimage") 39 | } 40 | } -------------------------------------------------------------------------------- /XCAssetGenerator/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // XCAssetGenerator 4 | // 5 | // Created by Bader on 7/30/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | NSApplicationMain(Process.argc, Process.unsafeArgv) 12 | -------------------------------------------------------------------------------- /XCAssetGeneratorTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.sourcebits.assetgenerator.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /XCAssetGeneratorTests/XCAssetGeneratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCAssetGeneratorTests.swift 3 | // XCAssetGeneratorTests 4 | // 5 | // Created by Bader on 7/30/14. 6 | // Copyright (c) 2014 Bader Alabdulrazzaq. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import XCTest 11 | 12 | class XCAssetGeneratorTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------