├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Example
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── contents.xcworkspacedata
├── Package.swift
├── README.md
└── Sources
│ ├── ExampleAppIcon
│ └── ExampleAppIconView.swift
│ └── Export
│ └── main.swift
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── AppIconCreator
│ ├── IconConfig.swift
│ ├── IconImage.swift
│ ├── IconPreview.swift
│ ├── IconPreviews.swift
│ ├── IconView.swift
│ └── View+If.swift
├── screenshot_dark.png
└── screenshot_light.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Example/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "example-app-icon",
7 | platforms: [
8 | .macOS(.v11)
9 | ],
10 | products: [
11 | .library(
12 | name: "ExampleAppIcon",
13 | targets: [
14 | "ExampleAppIcon"
15 | ]
16 | ),
17 | .executable(
18 | name: "export",
19 | targets: [
20 | "Export"
21 | ]
22 | )
23 | ],
24 | dependencies: [
25 | .package(path: "../")
26 | ],
27 | targets: [
28 | .target(
29 | name: "ExampleAppIcon",
30 | dependencies: [
31 | .product(
32 | name: "AppIconCreator",
33 | package: "swiftui-app-icon-creator"
34 | )
35 | ]
36 | ),
37 | .target(
38 | name: "Export",
39 | dependencies: [
40 | "ExampleAppIcon"
41 | ]
42 | )
43 | ]
44 | )
45 |
--------------------------------------------------------------------------------
/Example/README.md:
--------------------------------------------------------------------------------
1 | # Example App Icon
2 |
3 | SwiftUI App Icon Creator Usage Example
4 |
--------------------------------------------------------------------------------
/Example/Sources/ExampleAppIcon/ExampleAppIconView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import AppIconCreator
3 |
4 | public struct ExampleAppIconView: View {
5 | public enum Platform {
6 | case iOS
7 | case macOS
8 | }
9 |
10 | public init(_ platform: Platform) {
11 | self.platform = platform
12 | }
13 |
14 | var platform: Platform
15 |
16 | public var body: some View {
17 | GeometryReader { geometry in
18 | Image(systemName: "leaf.fill")
19 | .resizable()
20 | .scaledToFit()
21 | .font(Font.body.bold())
22 | .overlayMask(LinearGradient(
23 | gradient: Gradient(colors: [.green, .yellow]),
24 | startPoint: .top,
25 | endPoint: .bottom
26 | ))
27 | .padding(geometry.size.width * 0.18)
28 | .frame(maxWidth: .infinity, maxHeight: .infinity)
29 | .background(
30 | LinearGradient(
31 | gradient: Gradient(colors: [.blue, .purple]),
32 | startPoint: .top,
33 | endPoint: .bottom
34 | )
35 | )
36 | .if(platform == .macOS) {
37 | $0.overlay(
38 | RoundedRectangle(
39 | cornerRadius: geometry.size.width * 0.4,
40 | style: .continuous
41 | )
42 | .strokeBorder(
43 | lineWidth: geometry.size.width * 0.06
44 | )
45 | .overlayMask(
46 | LinearGradient(
47 | gradient: Gradient(colors: [.purple, .blue]),
48 | startPoint: .top,
49 | endPoint: .bottom
50 | )
51 | )
52 | .shadow(
53 | color: Color.black.opacity(0.5),
54 | radius: geometry.size.width * 0.015
55 | )
56 | )
57 | .clipShape(
58 | RoundedRectangle(
59 | cornerRadius: geometry.size.width * 0.4,
60 | style: .continuous
61 | )
62 | )
63 | }
64 | }
65 | }
66 | }
67 |
68 | extension View {
69 | func overlayMask(_ overlay: Overlay) -> some View {
70 | self.overlay(overlay).mask(self)
71 | }
72 | }
73 |
74 | struct ExampleAppIconView_Preivews: PreviewProvider {
75 | static var previews: some View {
76 | IconPreviews(
77 | icon: ExampleAppIconView(.iOS),
78 | configs: .iOS
79 | )
80 | IconPreviews(
81 | icon: ExampleAppIconView(.macOS),
82 | configs: .macOS,
83 | clip: false
84 | )
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Example/Sources/Export/main.swift:
--------------------------------------------------------------------------------
1 | import AppIconCreator
2 | import ExampleAppIcon
3 | import Foundation
4 |
5 | let exportURL = FileManager.default
6 | .homeDirectoryForCurrentUser
7 | .appendingPathComponent("Desktop")
8 | .appendingPathComponent("ExampleAppIcon")
9 |
10 | [IconImage]
11 | .images(for: ExampleAppIconView(.iOS), with: .iOS)
12 | .forEach { $0.save(to: exportURL) }
13 |
14 | [IconImage]
15 | .images(for: ExampleAppIconView(.macOS), with: .macOS)
16 | .forEach { $0.save(to: exportURL) }
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Dariusz Rybicki Darrarski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "swiftui-app-icon-creator",
7 | platforms: [
8 | .macOS(.v11),
9 | .iOS(.v13),
10 | ],
11 | products: [
12 | .library(
13 | name: "AppIconCreator",
14 | targets: [
15 | "AppIconCreator"
16 | ]
17 | )
18 | ],
19 | targets: [
20 | .target(
21 | name: "AppIconCreator"
22 | )
23 | ]
24 | )
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI App Icon Creator
2 |
3 | 
4 | 
5 |
6 | Create iOS and macOS application icon in Xcode with SwiftUI
7 |
8 | *Xcode 12 and macOS 11 required*
9 |
10 | 
11 |
12 |
13 | Dark mode supported
14 |
15 | 
16 |
17 |
18 |
19 | ## 📝 How to
20 |
21 | **TL;DR:** check out [example project](Example) in this repository.
22 |
23 | #### 1️⃣ Create a new Swift Package
24 |
25 | Define two products in your `Package.swift`:
26 |
27 | - Library that will contain your icon source code
28 | - Executable that you will use to export icon images
29 |
30 | Add `swiftui-app-icon-creator` package as a dependency.
31 |
32 | Your `Package.swift` should like this:
33 |
34 | ```swift
35 | // swift-tools-version:5.3
36 | import PackageDescription
37 |
38 | let package = Package(
39 | name: "my-app-icon",
40 | platforms: [.macOS(.v11)],
41 | products: [
42 | .library(name: "MyAppIcon", targets: ["MyAppIcon"]),
43 | .executable(name: "export", targets: ["Export"])
44 | ],
45 | dependencies: [
46 | .package(url: "https://github.com/darrarski/swiftui-app-icon-creator.git", from: "1.0.0")
47 | ],
48 | targets: [
49 | .target(name: "MyAppIcon", dependencies: [
50 | .product(name: "AppIconCreator", package: "swiftui-app-icon-creator")
51 | ]),
52 | .target(name: "Export", dependencies: ["MyAppIcon"])
53 | ]
54 | )
55 | ```
56 |
57 | #### 2️⃣ Create an icon view in the library target, using SwiftUI
58 |
59 | Just create a new SwiftUI view in the library target:
60 |
61 | ```swift
62 | import SwiftUI
63 |
64 | public struct MyAppIconView: View {
65 | public init() {}
66 |
67 | public var body: some View {
68 | // ...
69 | }
70 | }
71 | ```
72 |
73 | #### 3️⃣ Use `IconPreviews` to live-preview your icon in Xcode
74 |
75 | Add this code to the file which contains your icon view:
76 |
77 | ```swift
78 | import AppIconCreator
79 |
80 | struct MyAppIconView_Preivews: PreviewProvider {
81 | static var previews: some View {
82 | IconPreviews(
83 | icon: MyAppIconView(),
84 | configs: .iOS
85 | )
86 | }
87 | }
88 | ```
89 |
90 | Make sure you have selected the build scheme connected with your library target (`MyAppIcon` in this example).
91 |
92 | You should be able to live-preview the icon in Xcode previews.
93 |
94 | You can adjust the `configs` parameter to specify which types of icons you want to preview. Check out [`IconConfig.swift`](Sources/AppIconCreator/IconConfig.swift) for possible options.
95 |
96 | #### 4️⃣ Add exporting code to the executable target
97 |
98 | Add this code to `main.swift` file in your executable target:
99 |
100 | ```swift
101 | import AppIconCreator
102 | import MyAppIcon
103 | import Foundation
104 |
105 | let icon = MyAppIconView()
106 | let configs = [IconConfig].iOS
107 | let images = [IconImage].images(for: icon, with: configs)
108 | let exportURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Desktop").appendingPathComponent("MyAppIcon")
109 | images.forEach { $0.save(to: exportURL) }
110 | ```
111 |
112 | You can adjust the `configs` variable to specify which types of icons you want to export. Check out [`IconConfig.swift`](Sources/AppIconCreator/IconConfig.swift) for possible options.
113 |
114 | In the above example, the images will be exported to `MyAppIcon` directory on the current user's desktop. Feel free to adjust the `exportURL` variable to your needs.
115 |
116 | #### 5️⃣ Run the executable from Xcode to export your icon images
117 |
118 | Make sure you have selected the build scheme connected with your executable target (`export` in this example).
119 |
120 | Images of your icon should be exported into a directory specified in `exportURL` variable.
121 |
122 | ## ☕️ Do you like the project?
123 |
124 |
125 |
126 | ## 📄 License
127 |
128 | Copyright © 2020 Dariusz Rybicki Darrarski
129 |
130 | License: [MIT](LICENSE)
131 |
--------------------------------------------------------------------------------
/Sources/AppIconCreator/IconConfig.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct IconConfig: Equatable {
4 | public var name: String
5 | public var size: Float
6 | public var scales: [Int]
7 |
8 | public init(name: String, size: Float, scales: [Int]) {
9 | self.name = name
10 | self.size = size
11 | self.scales = scales
12 | }
13 | }
14 |
15 | extension IconConfig: Identifiable {
16 | public var id: String { "\(name) \(sizeString)" }
17 | }
18 |
19 | extension IconConfig {
20 | var sizeString: String {
21 | "\(Self.pointSizeFormatter.string(from: NSNumber(value: size))!)pt"
22 | }
23 |
24 | private static let pointSizeFormatter: NumberFormatter = {
25 | let formatter = NumberFormatter()
26 | formatter.decimalSeparator = "."
27 | formatter.minimumFractionDigits = 0
28 | formatter.maximumFractionDigits = 1
29 | return formatter
30 | }()
31 | }
32 |
33 | public extension Sequence where Element == IconConfig {
34 | static var iOS: [Element] {[
35 | .iPhone_Notification,
36 | .iPhone_Settings,
37 | .iPhone_Spotlight,
38 | .iPhone_App,
39 | .iPad_Notification,
40 | .iPad_Settings,
41 | .iPad_Spotlight,
42 | .iPad_App,
43 | .iPad_Pro_12_9_App,
44 | .iOS_App_Store
45 | ]}
46 | }
47 |
48 | public extension IconConfig {
49 | static let iPhone_Notification = IconConfig(name: "iPhone Notification", size: 20, scales: [2, 3])
50 | static let iPhone_Settings = IconConfig(name: "iPhone Settings", size: 29, scales: [2, 3])
51 | static let iPhone_Spotlight = IconConfig(name: "iPhone Spotlight", size: 40, scales: [2, 3])
52 | static let iPhone_App = IconConfig(name: "iPhone App", size: 60, scales: [2, 3])
53 | static let iPad_Notification = IconConfig(name: "iPad Notification", size: 20, scales: [1, 2])
54 | static let iPad_Settings = IconConfig(name: "iPad Settings", size: 29, scales: [1, 2])
55 | static let iPad_Spotlight = IconConfig(name: "iPad Spotlight", size: 40, scales: [1, 2])
56 | static let iPad_App = IconConfig(name: "iPad App", size: 76, scales: [1, 2])
57 | static let iPad_Pro_12_9_App = IconConfig(name: "iPad Pro (12.9-inch) App", size: 83.5, scales: [2])
58 | static let iOS_App_Store = IconConfig(name: "iOS App Store", size: 1024, scales: [1])
59 | }
60 |
61 | public extension Sequence where Element == IconConfig {
62 | static var macOS: [IconConfig] {[
63 | .macOS_16pt,
64 | .macOS_32pt,
65 | .macOS_128pt,
66 | .macOS_256pt,
67 | .macOS_512pt
68 | ]}
69 | }
70 |
71 | public extension IconConfig {
72 | static let macOS_16pt = IconConfig(name: "Mac", size: 16, scales: [1, 2])
73 | static let macOS_32pt = IconConfig(name: "Mac", size: 32, scales: [1, 2])
74 | static let macOS_128pt = IconConfig(name: "Mac", size: 128, scales: [1, 2])
75 | static let macOS_256pt = IconConfig(name: "Mac", size: 256, scales: [1, 2])
76 | static let macOS_512pt = IconConfig(name: "Mac", size: 512, scales: [1, 2])
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/AppIconCreator/IconImage.swift:
--------------------------------------------------------------------------------
1 | #if os(macOS)
2 | import SwiftUI
3 |
4 | public struct IconImage {
5 | public var name: String
6 | public var image: NSImage
7 | }
8 |
9 | public extension Sequence where Element == IconImage {
10 | static func images(
11 | for icon: Icon,
12 | with configs: [IconConfig]
13 | ) -> [Element] {
14 | configs.flatMap { config in
15 | images(for: icon, with: config)
16 | }
17 | }
18 |
19 | static func images(
20 | for icon: Icon,
21 | with config: IconConfig
22 | ) -> [Element] {
23 | config.scales.map { scale in
24 | IconImage(
25 | name: "\(config.id) \(scale)x",
26 | image: NSView.icon(icon, size: config.size * Float(scale))
27 | .toImage()
28 | .unscaled()
29 | )
30 | }
31 | }
32 | }
33 |
34 | public extension IconImage {
35 | func save(to directoryUrl: URL) {
36 | let fileManager = FileManager.default
37 | if fileManager.fileExists(atPath: directoryUrl.path) == false {
38 | try! fileManager.createDirectory(at: directoryUrl, withIntermediateDirectories: true)
39 | }
40 | let data = image.pngData()
41 | let url = directoryUrl
42 | .appendingPathComponent(name)
43 | .appendingPathExtension("png")
44 | try! data.write(to: url)
45 | }
46 | }
47 |
48 | extension NSView {
49 | static func icon(_ icon: Icon, size: Float) -> NSView {
50 | let view = NSHostingView(rootView: icon)
51 | let viewSize = CGFloat(size)
52 | view.frame = NSRect(
53 | origin: .zero,
54 | size: NSSize(width: viewSize, height: viewSize)
55 | )
56 | return view
57 | }
58 |
59 | func toImage() -> NSImage {
60 | let cacheRep = bitmapImageRepForCachingDisplay(in: bounds)!
61 | cacheDisplay(in: bounds, to: cacheRep)
62 | let image = NSImage(size: bounds.size)
63 | image.addRepresentation(cacheRep)
64 | return image
65 | }
66 | }
67 |
68 | extension NSImage {
69 | func unscaled() -> NSImage {
70 | let image = NSImage(size: size)
71 | image.addRepresentation(unscaledBitmapImageRep())
72 | return image
73 | }
74 |
75 | func unscaledBitmapImageRep() -> NSBitmapImageRep {
76 | let imageRep = NSBitmapImageRep(
77 | bitmapDataPlanes: nil,
78 | pixelsWide: Int(size.width),
79 | pixelsHigh: Int(size.height),
80 | bitsPerSample: 8,
81 | samplesPerPixel: 4,
82 | hasAlpha: true,
83 | isPlanar: false,
84 | colorSpaceName: .deviceRGB,
85 | bytesPerRow: 0,
86 | bitsPerPixel: 0
87 | )!
88 | imageRep.size = size
89 | NSGraphicsContext.saveGraphicsState()
90 | NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: imageRep)
91 | draw(at: .zero, from: .zero, operation: .sourceOver, fraction: 1)
92 | NSGraphicsContext.restoreGraphicsState()
93 | return imageRep
94 | }
95 |
96 | func pngData() -> Data {
97 | let tiffData = tiffRepresentation!
98 | let imageRep = NSBitmapImageRep(data: tiffData)!
99 | return imageRep.representation(using: .png, properties: [.compressionFactor: 1])!
100 | }
101 | }
102 | #endif
103 |
--------------------------------------------------------------------------------
/Sources/AppIconCreator/IconPreview.swift:
--------------------------------------------------------------------------------
1 | #if os(macOS)
2 | import SwiftUI
3 |
4 | public struct IconPreview: View {
5 | public var icon: Icon
6 | public var config: IconConfig
7 | public var clip: Bool
8 |
9 | public init(icon: Icon, config: IconConfig, clip: Bool = true) {
10 | self.icon = icon
11 | self.config = config
12 | self.clip = clip
13 | }
14 |
15 | public var body: some View {
16 | VStack(alignment: .leading, spacing: 16) {
17 | Text(config.id).font(.title2)
18 | HStack(alignment: .top, spacing: 16) {
19 | ForEach(config.scales.sorted(by: >), id: \.self) { scale in
20 | VStack(alignment: .center, spacing: 8) {
21 | Text("\(scale)x").font(.title3)
22 | iconView(size: config.size, scale: scale)
23 | }
24 | }
25 | }
26 | }
27 | .padding()
28 | .fixedSize()
29 | }
30 |
31 | private func iconView(size: Float, scale: Int) -> some View {
32 | let screenScale = NSScreen.main!.backingScaleFactor
33 | let viewSize = CGFloat(config.size) * CGFloat(scale) / screenScale
34 | return icon
35 | .environment(\.colorScheme, .light)
36 | .frame(width: viewSize, height: viewSize, alignment: .center)
37 | .if(clip) {
38 | $0.clipShape(RoundedRectangle(
39 | cornerRadius: viewSize * 0.2,
40 | style: .continuous
41 | ))
42 | }
43 | }
44 | }
45 | #endif
46 |
--------------------------------------------------------------------------------
/Sources/AppIconCreator/IconPreviews.swift:
--------------------------------------------------------------------------------
1 | #if os(macOS)
2 | import SwiftUI
3 |
4 | public struct IconPreviews: View {
5 | public var icon: Icon
6 | public var configs: [IconConfig]
7 | public var clip: Bool
8 |
9 | public init(icon: Icon, configs: [IconConfig], clip: Bool = true) {
10 | self.icon = icon
11 | self.configs = configs
12 | self.clip = clip
13 | }
14 |
15 | public var body: some View {
16 | VStack(alignment: .leading, spacing: 16) {
17 | ForEach(configs) { config in
18 | IconPreview(icon: icon, config: config, clip: clip)
19 | }
20 | }
21 | .fixedSize()
22 | }
23 | }
24 | #endif
25 |
--------------------------------------------------------------------------------
/Sources/AppIconCreator/IconView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct IconView: View, Equatable {
4 | public init() {}
5 |
6 | public var body: some View {
7 | HStack(spacing: 0) {
8 | Color.red
9 | Color.green
10 | Color.blue
11 | }
12 | }
13 | }
14 |
15 | struct IconView_Preivews: PreviewProvider {
16 | static var previews: some View {
17 | #if os(macOS)
18 | IconPreviews(
19 | icon: IconView(),
20 | configs: .iOS
21 | )
22 | #endif
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/AppIconCreator/View+If.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension View {
4 | @ViewBuilder
5 | func `if`(_ condition: Bool, content: (Self) -> Content) -> some View {
6 | if condition {
7 | content(self)
8 | } else {
9 | self
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/screenshot_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swiftui-app-icon-creator/f0c7ba4e66d3dc8135ccf9146afc05f9dff3c4ff/screenshot_dark.png
--------------------------------------------------------------------------------
/screenshot_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swiftui-app-icon-creator/f0c7ba4e66d3dc8135ccf9146afc05f9dff3c4ff/screenshot_light.png
--------------------------------------------------------------------------------