Skip to content

Commit ed5d076

Browse files
committed
Update with Xcode's new template, support universal
1 parent 3d00a3b commit ed5d076

File tree

7 files changed

+295
-219
lines changed

7 files changed

+295
-219
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.DS_Store
22
.swiftpm
33
Package.resolved
4+
.build

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,31 @@ A command line tool and Swift package for generating image assets for Apple plat
1414

1515
## Installation
1616

17+
### Prebuilt
18+
1719
Please download prebuilt binaries from [Releases](https://github.com/xnth97/AssetKit/releases).
1820

21+
### Build
22+
23+
1. Clone this repo, `cd` into the base directory.
24+
2. Run `swift build -c release`.
25+
3. Binaries are located in `.build/release`.
26+
1927
## Usage
2028

2129
The CLI `assetool` supports subcommands for generating both `.appiconset` and `.imageset`. Default subcommand is `icon`.
2230

2331
### Icon
2432

2533
```
26-
assetool <input> [-o <output>] [-p <platforms>]
34+
assetool <input> [-o <output>] [-p <platforms>] [--universal]
2735
```
2836

2937
`-o, --output <output>`: Path of the output folder. If empty, will use current path of terminal.
3038

31-
`-p, --platforms <platforms>`: Valid values are: `ios`, `iphone`, `ipad`, `mac`, `car`. You can also generate an icon set with multiple platform idioms by sending a string of multiple values separated by comma, e.g. `ios,mac,watch`.
39+
`-p, --platforms <platforms>`: Valid values are: `ios`, `mac`, `macos`, `watch`, `watchos`. You can also generate an icon set with multiple platform idioms by sending a string of multiple values separated by comma, e.g. `ios,mac,watch`.
40+
41+
`-u, --universal` Generates single size for iOS and watchOS, suited for newer Xcode.
3242

3343
### Image
3444

Sources/AssetKit/AssetGenerator.swift

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,79 +24,65 @@ public class AssetGenerator {
2424
let filename: String
2525
}
2626

27-
private static let platformIdiomMap: [AssetKit.Platform: Set<String>] = [
28-
.iphone: Set(["iphone", "ios-marketing"]),
29-
.ipad: Set(["ipad", "ios-marketing"]),
30-
.ios: Set(["iphone", "ipad", "ios-marketing"]),
31-
.watch: Set(["watch", "watch-marketing"]),
32-
.car: Set(["car", "car"]),
33-
.mac: Set(["mac"]),
34-
]
35-
3627
public func generateIconSet(input: NSImage,
3728
outputPath: String,
38-
platforms: [AssetKit.Platform] = [.ios]) throws {
39-
guard let imageSource = AssetUtils.createCGImageSource(from: input) else {
40-
throw AssetGeneratorError.dataSourceError
41-
}
42-
43-
try generateIconSet(imageSource: imageSource, outputPath: outputPath, platforms: platforms)
29+
platforms: Set<AssetKit.Platform> = [.ios],
30+
prefersUniversal: Bool = true) throws {
31+
let imageSource = try AssetUtils.createCGImageSource(from: input)
32+
try generateIconSet(
33+
imageSource: imageSource,
34+
outputPath: outputPath,
35+
platforms: platforms,
36+
prefersUniversal: prefersUniversal)
4437
}
4538

4639
public func generateIconSet(inputPath: String,
4740
outputPath: String,
48-
platforms: [AssetKit.Platform] = [.ios]) throws {
49-
guard let imageSource = AssetUtils.createCGImageSource(from: inputPath) else {
50-
throw AssetGeneratorError.dataSourceError
51-
}
52-
53-
try generateIconSet(imageSource: imageSource, outputPath: outputPath, platforms: platforms)
41+
platforms: Set<AssetKit.Platform> = [.ios],
42+
prefersUniversal: Bool = true) throws {
43+
let imageSource = try AssetUtils.createCGImageSource(from: inputPath)
44+
try generateIconSet(
45+
imageSource: imageSource,
46+
outputPath: outputPath,
47+
platforms: platforms,
48+
prefersUniversal: prefersUniversal)
5449
}
5550

5651
private func generateIconSet(imageSource: CGImageSource,
5752
outputPath: String,
58-
platforms: [AssetKit.Platform] = [.ios]) throws {
59-
guard var config = AssetUtils.loadResourceJson(filename: "icon_contents") else {
60-
throw AssetGeneratorError.resourceError
61-
}
53+
platforms: Set<AssetKit.Platform> = [.ios],
54+
prefersUniversal: Bool) throws {
55+
var config = try AssetUtils.loadResourceJson(filename: "icon_contents")
6256

63-
let outputFolder = URL(fileURLWithPath: outputPath).appendingPathComponent("AppIcon.appiconset")
57+
let iconSetFileName = "AppIcon.appiconset"
58+
let outputFolder = URL(fileURLWithPath: outputPath).appendingPathComponent(iconSetFileName)
6459
try AssetUtils.createDirectoryIfNeeded(url: outputFolder)
6560

6661
var resizeConfigs: [ResizeConfiguration] = []
6762
var filteredImageConfigs: [[String: Any]] = []
6863
guard let imageConfigs = config["images"] as? [[String: Any]] else {
6964
throw AssetGeneratorError.configError
7065
}
71-
for imageConfig in imageConfigs {
72-
guard let idiom = imageConfig["idiom"] as? String else {
73-
throw AssetGeneratorError.configError
74-
}
7566

76-
func shouldUseThisConfig() -> Bool {
77-
for platform in platforms {
78-
guard let platformIdiomStringSet = Self.platformIdiomMap[platform] else {
79-
continue
80-
}
81-
if platformIdiomStringSet.contains(idiom) {
82-
return true
83-
}
84-
}
85-
return false
67+
for imageConfig in imageConfigs {
68+
guard let configPlatform = getPlatform(from: imageConfig),
69+
platforms.contains(configPlatform) else {
70+
continue
8671
}
8772

88-
guard shouldUseThisConfig() else {
73+
/// If prefers universal, skip image config parsing.
74+
if prefersUniversal, configPlatform == .ios || configPlatform == .watchos {
8975
continue
9076
}
9177

92-
guard let scaleStr = (imageConfig["scale"] as? String)?.prefix(1),
93-
let scale = Float(scaleStr),
94-
let sizeStr = (imageConfig["size"] as? String)?.split(separator: "x")[0],
78+
let scaleStr = (imageConfig["scale"] as? String)?.prefix(1) ?? "1"
79+
guard let scale = Float(scaleStr),
80+
let sizeStr = (imageConfig["size"] as? String)?.split(separator: "x").first,
9581
let size = Float(sizeStr) else {
96-
continue
97-
}
82+
continue
83+
}
9884

99-
let newFilename = "AppIcon-\(sizeStr)@\(scaleStr)x.png"
85+
let newFilename = "AppIcon_\(sizeStr)@\(scaleStr)x.png"
10086
let resizeConfig = ResizeConfiguration(width: Int(size * scale), height: Int(size * scale), filename: newFilename)
10187
resizeConfigs.append(resizeConfig)
10288

@@ -105,22 +91,54 @@ public class AssetGenerator {
10591
filteredImageConfigs.append(mutableImageConfig)
10692
}
10793

94+
/// If prefers universal, generate 1024x1024 images for `ios` and `watchos`.
95+
if prefersUniversal {
96+
for platform in platforms {
97+
guard platform == .ios || platform == .watchos else {
98+
continue
99+
}
100+
101+
let filename = "AppIcon_\(platform.rawValue).png"
102+
resizeConfigs.append(ResizeConfiguration(width: 1024, height: 1024, filename: filename))
103+
filteredImageConfigs.append([
104+
"filename" : filename,
105+
"idiom" : "universal",
106+
"platform" : platform.rawValue,
107+
"size" : "1024x1024",
108+
])
109+
}
110+
}
111+
108112
resizeConfigs.forEach { config in
109113
try? resizeImage(imageSource: imageSource, resizeConfiguration: config, outputUrl: outputFolder)
110114
}
111115

112116
config["images"] = filteredImageConfigs
113117
try AssetUtils.writeDictionaryToJson(config, filename: "Contents.json", url: outputFolder)
118+
119+
print("[assetool] IconSet generated at \(outputFolder)")
120+
}
121+
122+
private func getPlatform(from config: [String: Any]) -> AssetKit.Platform? {
123+
if (config["idiom"] as? String) == "mac" {
124+
return .macos
125+
}
126+
if let platformString = config["platform"] as? String {
127+
if platformString == "ios" {
128+
return .ios
129+
} else if platformString == "watchos" {
130+
return .watchos
131+
}
132+
}
133+
return nil
114134
}
115135

116136
public func generateImageSet(input: NSImage,
117137
filename: String,
118138
outputPath: String,
119139
width: CGFloat? = nil,
120140
height: CGFloat? = nil) throws {
121-
guard let imageSource = AssetUtils.createCGImageSource(from: input) else {
122-
throw AssetGeneratorError.dataSourceError
123-
}
141+
let imageSource = try AssetUtils.createCGImageSource(from: input)
124142
try generateImageSet(
125143
imageSource: imageSource,
126144
filename: filename,
@@ -133,9 +151,7 @@ public class AssetGenerator {
133151
outputPath: String,
134152
width: CGFloat? = nil,
135153
height: CGFloat? = nil) throws {
136-
guard let imageSource = AssetUtils.createCGImageSource(from: inputPath) else {
137-
throw AssetGeneratorError.dataSourceError
138-
}
154+
let imageSource = try AssetUtils.createCGImageSource(from: inputPath)
139155
try generateImageSet(
140156
imageSource: imageSource,
141157
filename: AssetUtils.extractFilename(inputPath: inputPath),
@@ -149,10 +165,11 @@ public class AssetGenerator {
149165
outputPath: String,
150166
width: CGFloat? = nil,
151167
height: CGFloat? = nil) throws {
152-
guard let originalSize = AssetUtils.sizeOfImageSource(imageSource),
153-
var config = AssetUtils.loadResourceJson(filename: "image_contents") else {
154-
throw AssetGeneratorError.resourceError
155-
}
168+
guard let originalSize = AssetUtils.sizeOfImageSource(imageSource) else {
169+
throw AssetGeneratorError.resourceError
170+
}
171+
172+
var config = try AssetUtils.loadResourceJson(filename: "image_contents")
156173

157174
// @1x size
158175
var oneXWidth: CGFloat = 0
@@ -213,7 +230,7 @@ public class AssetGenerator {
213230
kCGImageSourceCreateThumbnailWithTransform: true,
214231
kCGImageSourceShouldCacheImmediately: true,
215232
kCGImageSourceThumbnailMaxPixelSize: max(resizeConfiguration.width, resizeConfiguration.height),
216-
] as CFDictionary
233+
] as [CFString : Any] as CFDictionary
217234

218235
guard let resized = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) else {
219236
throw AssetGeneratorError.dataSourceError

Sources/AssetKit/AssetKit.swift

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ import AppKit
1010

1111
public struct AssetKit {
1212

13-
public enum Platform: String {
14-
case iphone
15-
case ipad
13+
public enum Platform: String, CaseIterable {
1614
case ios
17-
case car
18-
case watch
19-
case mac
15+
case macos
16+
case watchos
2017
}
2118

2219
private static let generator = AssetGenerator()
@@ -31,7 +28,11 @@ public struct AssetKit {
3128
outputPath: String,
3229
width: CGFloat? = nil,
3330
height: CGFloat? = nil) {
34-
try? generator.generateImageSet(inputPath: inputPath, outputPath: outputPath, width: width, height: height)
31+
try? generator.generateImageSet(
32+
inputPath: inputPath,
33+
outputPath: outputPath,
34+
width: width,
35+
height: height)
3536
}
3637

3738
/// Generates `.imageset` with a given input image.
@@ -46,29 +47,46 @@ public struct AssetKit {
4647
outputPath: String,
4748
width: CGFloat? = nil,
4849
height: CGFloat? = nil) {
49-
try? generator.generateImageSet(input: input, filename: filename, outputPath: outputPath, width: width, height: height)
50+
try? generator.generateImageSet(
51+
input: input,
52+
filename: filename,
53+
outputPath: outputPath,
54+
width: width,
55+
height: height)
5056
}
5157

5258
/// Generates `.appiconset` with a given input image.
5359
/// - Parameters:
5460
/// - inputPath: Path to input image.
5561
/// - outputPath: Path to output folder.
5662
/// - platforms: Platform idioms that need to be included in the generated icon set.
63+
/// - prefersUniversal: Generates single size for iOS and watchOS, suited for newer Xcode.
5764
public static func generateIconSet(inputPath: String,
5865
outputPath: String,
59-
platforms: [Platform] = [.ios]) {
60-
try? generator.generateIconSet(inputPath: inputPath, outputPath: outputPath, platforms: platforms)
66+
platforms: Set<Platform> = [.ios],
67+
prefersUniversal: Bool = false) {
68+
try? generator.generateIconSet(
69+
inputPath: inputPath,
70+
outputPath: outputPath,
71+
platforms: platforms,
72+
prefersUniversal: prefersUniversal)
6173
}
6274

6375
/// Generates `.appiconset` with a given input image.
6476
/// - Parameters:
6577
/// - input: Input `NSImage` image.
6678
/// - outputPath: Path to output folder.
6779
/// - platforms: Platform idioms that need to be included in the generated icon set.
80+
/// - prefersUniversal: Generates single size for iOS and watchOS, suited for newer Xcode.
6881
public static func generateIconSet(input: NSImage,
6982
outputPath: String,
70-
platforms: [AssetKit.Platform] = [.ios]) {
71-
try? generator.generateIconSet(input: input, outputPath: outputPath, platforms: platforms)
83+
platforms: Set<Platform> = [.ios],
84+
prefersUniversal: Bool = false) {
85+
try? generator.generateIconSet(
86+
input: input,
87+
outputPath: outputPath,
88+
platforms: platforms,
89+
prefersUniversal: prefersUniversal)
7290
}
7391

7492
}

Sources/AssetKit/AssetUtils.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,40 @@ import ImageIO
1111

1212
struct AssetUtils {
1313

14-
static func loadResourceJson(filename: String) -> [String: Any]? {
14+
static func loadResourceJson(filename: String) throws -> [String: Any] {
1515
guard let jsonUrl = Bundle.module.url(forResource: filename, withExtension: "json"),
1616
let jsonData = try? Data(contentsOf: jsonUrl),
1717
let jsonDict = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else {
18-
return nil
19-
}
18+
throw AssetGenerator.AssetGeneratorError.configError
19+
}
2020
return jsonDict
2121
}
2222

23-
static func createCGImageSource(from path: String) -> CGImageSource? {
23+
static func createCGImageSource(from path: String) throws -> CGImageSource {
2424
let fileUrl = URL(fileURLWithPath: path) as CFURL
25-
return CGImageSourceCreateWithURL(fileUrl, nil)
25+
guard let imageSource = CGImageSourceCreateWithURL(fileUrl, nil) else {
26+
throw AssetGenerator.AssetGeneratorError.dataSourceError
27+
}
28+
return imageSource
2629
}
2730

28-
static func createCGImageSource(from image: NSImage) -> CGImageSource? {
29-
guard let data = image.tiffRepresentation else {
30-
return nil
31+
static func createCGImageSource(from image: NSImage) throws -> CGImageSource {
32+
guard let data = image.tiffRepresentation,
33+
let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
34+
throw AssetGenerator.AssetGeneratorError.dataSourceError
3135
}
32-
return CGImageSourceCreateWithData(data as CFData, nil)
36+
return imageSource
3337
}
3438

3539
static func sizeOfImage(for image: NSImage) -> CGSize? {
36-
guard let source = createCGImageSource(from: image) else {
40+
guard let source = try? createCGImageSource(from: image) else {
3741
return nil
3842
}
3943
return sizeOfImageSource(source)
4044
}
4145

4246
static func sizeOfImage(at path: String) -> CGSize? {
43-
guard let source = createCGImageSource(from: path) else {
47+
guard let source = try? createCGImageSource(from: path) else {
4448
return nil
4549
}
4650
return sizeOfImageSource(source)
@@ -54,8 +58,8 @@ struct AssetUtils {
5458

5559
guard let width = properties[kCGImagePropertyPixelWidth] as? CGFloat,
5660
let height = properties[kCGImagePropertyPixelHeight] as? CGFloat else {
57-
return nil
58-
}
61+
return nil
62+
}
5963

6064
return CGSize(width: width, height: height)
6165
}

0 commit comments

Comments
 (0)