diff --git a/Examples/Annotated Configuration.json b/Examples/Annotated Configuration.json new file mode 100644 index 0000000..f6166f5 --- /dev/null +++ b/Examples/Annotated Configuration.json @@ -0,0 +1,42 @@ +{ + /* Skip images */ + "skip-images" : { + "patterns" : ["Watch App .*", "Bed.*"] + }, + + + /* Set which images to include. Defaults to ["png"] */ + "valid-image-extensions" : ["png", "jpeg", "jpg", "tiff"], + + // Set the base rendering options + "base" : { + "template-rendering-intent" : "template" + }, + + /* Apply properties based on the target device */ + "devices" : [ + { + "device-type" : "watch", + "properties" : { + "template-rendering-intent" : "template" + } + } + ], + + /* By default images following the AppIcon-{size}.png naming convention + will be treated as prerendered app icons and won't be exposed to Swift */ + "app-icon" : { + "pattern" : "Custom App Icon Name.*", + "prerendered" : false /* Defaults to true */ + }, + + /* Apply custom sets of properties to images matched by the given patterns */ + "custom" : [ + { + "patterns" : [".*Preview.*", "SleepDuration.*", "WakeTime.*"], + "properties" : { + "template-rendering-intent" : "original" + } + } + ] +} \ No newline at end of file diff --git a/XCAssetPacker.xcodeproj/project.pbxproj b/XCAssetPacker.xcodeproj/project.pbxproj index 24b946c..154e9a5 100644 --- a/XCAssetPacker.xcodeproj/project.pbxproj +++ b/XCAssetPacker.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ 4AD71E911EA4D13400016A1A /* AssetCatalogGenerator+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD71E8F1EA4D13400016A1A /* AssetCatalogGenerator+Creation.swift */; }; 4AD71E951EA4DD2700016A1A /* Complication Rules.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD71E941EA4DD2700016A1A /* Complication Rules.json */; }; 4AD71E971EA4EEEC00016A1A /* Skip Circles Rules.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD71E961EA4EEEC00016A1A /* Skip Circles Rules.json */; }; + 4AFFC5911F5D8E35000FDCFD /* DeviceIdiom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFFC5901F5D8E35000FDCFD /* DeviceIdiom.swift */; }; + 4AFFC5931F5D8E83000FDCFD /* DeviceIdiom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFFC5921F5D8E83000FDCFD /* DeviceIdiom.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -62,6 +64,8 @@ 4AD71E8F1EA4D13400016A1A /* AssetCatalogGenerator+Creation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AssetCatalogGenerator+Creation.swift"; sourceTree = ""; }; 4AD71E941EA4DD2700016A1A /* Complication Rules.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Complication Rules.json"; sourceTree = ""; }; 4AD71E961EA4EEEC00016A1A /* Skip Circles Rules.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Skip Circles Rules.json"; sourceTree = ""; }; + 4AFFC5901F5D8E35000FDCFD /* DeviceIdiom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceIdiom.swift; sourceTree = ""; }; + 4AFFC5921F5D8E83000FDCFD /* DeviceIdiom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceIdiom.swift; path = XCAssetPacker/DeviceIdiom.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,9 +89,9 @@ 4A56D13E1DE5B573000B40D8 = { isa = PBXGroup; children = ( + 4AFFC5921F5D8E83000FDCFD /* DeviceIdiom.swift */, 4A56D1491DE5B573000B40D8 /* XCAssetPacker */, 4A56D1571DE5B5AB000B40D8 /* CommandLine */, - 4AD71E771EA4CC7300016A1A /* Tests */, 4AD71E7D1EA4CCBA00016A1A /* XCAssetPacker Tests */, 4A56D1481DE5B573000B40D8 /* Products */, ); @@ -110,6 +114,7 @@ 4AD71E8F1EA4D13400016A1A /* AssetCatalogGenerator+Creation.swift */, 4A56D1581DE5BA85000B40D8 /* AssetCatalogGenerator.swift */, 4A07727D1DF963C70068A929 /* ImageProperties.swift */, + 4AFFC5901F5D8E35000FDCFD /* DeviceIdiom.swift */, 4A07727B1DF9634A0068A929 /* Extensions.swift */, 4AD71E731EA4899500016A1A /* SwiftGeneration.swift */, ); @@ -127,14 +132,6 @@ path = XCAssetPacker; sourceTree = ""; }; - 4AD71E771EA4CC7300016A1A /* Tests */ = { - isa = PBXGroup; - children = ( - ); - name = Tests; - path = XCAssetPacker; - sourceTree = ""; - }; 4AD71E7D1EA4CCBA00016A1A /* XCAssetPacker Tests */ = { isa = PBXGroup; children = ( @@ -243,6 +240,7 @@ buildActionMask = 2147483647; files = ( 4A0772821DF9731B0068A929 /* CommandLine.swift in Sources */, + 4AFFC5911F5D8E35000FDCFD /* DeviceIdiom.swift in Sources */, 4A5F178F1EA3EDE8007C37DE /* Constants.swift in Sources */, 4A07727C1DF9634A0068A929 /* Extensions.swift in Sources */, 4A07727E1DF963C70068A929 /* ImageProperties.swift in Sources */, @@ -266,6 +264,7 @@ 4AD71E871EA4CD0800016A1A /* Extensions.swift in Sources */, 4AD71E881EA4CD0800016A1A /* SwiftGeneration.swift in Sources */, 4AD71E8A1EA4CD0800016A1A /* CommandLine.swift in Sources */, + 4AFFC5931F5D8E83000FDCFD /* DeviceIdiom.swift in Sources */, 4AD71E8B1EA4CD0800016A1A /* Option.swift in Sources */, 4AD71E8C1EA4CD0800016A1A /* StringExtensions.swift in Sources */, 4AD71E911EA4D13400016A1A /* AssetCatalogGenerator+Creation.swift in Sources */, @@ -445,6 +444,7 @@ 4AD71E831EA4CCBA00016A1A /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/XCAssetPacker/AssetCatalogGenerator.swift b/XCAssetPacker/AssetCatalogGenerator.swift index fda08e6..15dab61 100644 --- a/XCAssetPacker/AssetCatalogGenerator.swift +++ b/XCAssetPacker/AssetCatalogGenerator.swift @@ -97,6 +97,7 @@ class Node { return sourceURL == nil } + var pathComponents: [String] { var currentNode: Node? = self var pathComponents: [String] = [] @@ -110,19 +111,6 @@ class Node { } -// var folderPathComponents: [String] { -// var currentNode: Node? = self.parent -// var pathComponents: [String] = [] -// -// while let node = currentNode, node.parent != nil { -// pathComponents.append(node.name) -// currentNode = node.parent -// } -// -// return pathComponents -// } - - func printTree(depth: Int = 0) { let depthPadding = (0...depth).reduce("") { (existing, _) -> String in return existing + " " @@ -144,6 +132,7 @@ class AssetCatalogGenerator { let swiftFileURL: URL? let swiftTarget: SwiftTarget let shouldOverwrite: Bool + let baseIdiom: DeviceIdiom? let rootNode: Node let numberOfRootPathComponents: Int @@ -162,6 +151,13 @@ class AssetCatalogGenerator { self.shouldOverwrite = shouldOverwrite self.configuration = configuration + + if let base = configuration.value(for: .base) as? [String: Any], + let idiom = base.value(for: .idiom) as? String { + baseIdiom = DeviceIdiom(idiom) + } else { + baseIdiom = nil + } } @@ -272,7 +268,26 @@ class AssetCatalogGenerator { let imageFileName = sourceURL.lastPathComponent let assetDestinationURL = destinationURL.appending(pathComponents: node.pathComponents).appendingPathComponent(imageFileName) - images.append(imageDictionary(for: imageFileName, properties: properties)) + // Xcode 9 seperates Notification, Settings and Spotlight images for iPhone and iPad + // if no base idiom is supplied then add both images + // + // This is rather messy, so would ideally be tidied up + switch properties.type { + case .notification, .settings, .spotlight: + if let idiom = baseIdiom { + images.append(imageDictionary(for: imageFileName, properties: properties, customIdiom: idiom.idiomString)) + } else { + let targetIdioms: [DeviceIdiom] = [.iPhone, .iPad] + + for targetIdiom in targetIdioms { + images.append(imageDictionary(for: imageFileName, properties: properties, customIdiom: targetIdiom.idiomString)) + } + } + + default: + images.append(imageDictionary(for: imageFileName, properties: properties)) + } + copies.append((sourceURL, assetDestinationURL)) } } @@ -320,12 +335,14 @@ class AssetCatalogGenerator { // MARK: Generate json for each component within the .xcassets package - func imageDictionary(for imageName: String, properties: ImageProperties) -> [String: Any] { + + func imageDictionary(for imageName: String, properties: ImageProperties, customIdiom: String? = nil) -> [String: Any] { var imageDictionary: [String: Any] = [:] - imageDictionary[Configuration.filename.rawValue] = imageName - if let idiom = properties.idiom { + if let idiom = customIdiom { + imageDictionary[Configuration.idiom.rawValue] = idiom + } else if let idiom = properties.idiom { imageDictionary[Configuration.idiom.rawValue] = idiom } diff --git a/XCAssetPacker/DeviceIdiom.swift b/XCAssetPacker/DeviceIdiom.swift new file mode 100644 index 0000000..721dfaf --- /dev/null +++ b/XCAssetPacker/DeviceIdiom.swift @@ -0,0 +1,51 @@ +// +// DeviceIdiom.swift +// XCAssetPacker +// +// Created by Harry Jordan on 04/09/2017. +// Copyright © 2017 Inquisitive Software. All rights reserved. +// + +import Foundation + + +public enum DeviceIdiom: String { + case watch, iPhone, iPad + + static var all: [DeviceIdiom] = [watch, iPhone, iPad] + + + init?(_ idiomString: String) { + let idiom = DeviceIdiom.all.first(where: { + $0.idiomString.caseInsensitiveCompare(idiomString) == .orderedSame + }) + + if let idiom = idiom { + self = idiom + } else { + return nil + } + } + + + var idiomString: String { + // Keys used for xcasset properties + switch self { + case .watch: + return "watch" + + case .iPhone: + return "iphone" + + case .iPad: + return "ipad" + } + } + + + var configurationKey: String { + // Keys used to match strings in the configuration .json + return self.rawValue + } + +} diff --git a/XCAssetPacker/ImageProperties.swift b/XCAssetPacker/ImageProperties.swift index 3a12c0c..f7d1edc 100644 --- a/XCAssetPacker/ImageProperties.swift +++ b/XCAssetPacker/ImageProperties.swift @@ -119,8 +119,9 @@ struct ImageProperties { } if let base = configuration.value(for: .base) as? [String: Any], - let idiomString = base.value(for: .idiom) as? String { - return idiomString + let idiomString = base.value(for: .idiom) as? String, + let deviceIdiom = DeviceIdiom(idiomString) { + return deviceIdiom.idiomString } return Configuration.universal.rawValue @@ -139,19 +140,23 @@ enum ImageType { var idiom: String? { + let idiom: DeviceIdiom? + switch self { case .watch, .watch38, .watch42: - return "watch" + idiom = .watch case .iPhoneAppIcon: - return "iphone" + idiom = .iPhone case .iPadAppIcon, .iPadProAppIcon: - return "ipad" + idiom = .iPad default: - return nil + idiom = nil } + + return idiom?.idiomString } @@ -176,13 +181,13 @@ enum ImageType { // Used for matching with the configuration file switch self { case .watch, .watch38, .watch42: - return "watch" + return DeviceIdiom.watch.configurationKey case .iPhoneAppIcon: - return "iPhone" + return DeviceIdiom.iPhone.configurationKey case .iPadAppIcon, .iPadProAppIcon: - return "iPad" + return DeviceIdiom.iPad.configurationKey default: return "universal" diff --git a/XCAssetPacker/main.swift b/XCAssetPacker/main.swift index 1142412..33e4062 100644 --- a/XCAssetPacker/main.swift +++ b/XCAssetPacker/main.swift @@ -26,9 +26,9 @@ import Cocoa // Setup command line options let inputPathOption = StringOption(shortFlag: "i", longFlag: "input", helpMessage: "Path to the input folder.") -let configurationOption = StringOption(shortFlag: "c", longFlag: "config", required: false, helpMessage: "The location of a json configuration file. If none is specified then uses sensible defaults.") -let outputPathOption = StringOption(shortFlag: "o", longFlag: "output", helpMessage: "Path to the output file or folder. If a folder is given then an Assets.xcassets package will be created inside it.") -let swiftDestinationOption = StringOption(longFlag: "swift", helpMessage: "Path to the output swift file or folder. If a folder is given then an Images.swift package will be created inside it.") +let configurationOption = StringOption(shortFlag: "c", longFlag: "config", required: false, helpMessage: "The location of a json configuration file.\n If none is specified then uses sensible defaults.") +let outputPathOption = StringOption(shortFlag: "o", longFlag: "output", helpMessage: "Path to the output file or folder.\n If a folder is given then an Assets.xcassets package will be created inside it.") +let swiftDestinationOption = StringOption(longFlag: "swift", helpMessage: "Path to the output swift file or folder.\n If a folder is given then an Images.swift file will be created inside it.") let overwriteOption = BoolOption(shortFlag: "f", longFlag: "force", helpMessage: "Overwrite any existing .xcassets package or Swift file.") // Target @@ -146,7 +146,7 @@ do { print(description) exit(EX_IOERR) } -} catch { - print("Unexpected error") +} catch let error { + print("Unexpected error: \(String(describing: error))") exit(EX_IOERR) }