├── .gitignore ├── Example ├── IconHarness.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── IconHarness.xcscheme └── IconHarness │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── AppIcon-ios-marketing-1024x1024.png │ │ ├── AppIcon-ipad-20x20.png │ │ ├── AppIcon-ipad-20x20@2x.png │ │ ├── AppIcon-ipad-29x29.png │ │ ├── AppIcon-ipad-29x29@2x.png │ │ ├── AppIcon-ipad-40x40.png │ │ ├── AppIcon-ipad-40x40@2x.png │ │ ├── AppIcon-ipad-76x76.png │ │ ├── AppIcon-ipad-76x76@2x.png │ │ ├── AppIcon-ipad-83.5x83.5@2x.png │ │ ├── AppIcon-iphone-20x20@2x.png │ │ ├── AppIcon-iphone-20x20@3x.png │ │ ├── AppIcon-iphone-29x29@2x.png │ │ ├── AppIcon-iphone-29x29@3x.png │ │ ├── AppIcon-iphone-40x40@2x.png │ │ ├── AppIcon-iphone-40x40@3x.png │ │ ├── AppIcon-iphone-60x60@2x.png │ │ ├── AppIcon-iphone-60x60@3x.png │ │ ├── AppIcon-mac-128x128.png │ │ ├── AppIcon-mac-128x128@2x.png │ │ ├── AppIcon-mac-16x16.png │ │ ├── AppIcon-mac-16x16@2x.png │ │ ├── AppIcon-mac-256x256.png │ │ ├── AppIcon-mac-256x256@2x.png │ │ ├── AppIcon-mac-32x32.png │ │ ├── AppIcon-mac-32x32@2x.png │ │ ├── AppIcon-mac-512x512.png │ │ ├── AppIcon-mac-512x512@2x.png │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── Icon.swift │ ├── IconHarness.entitlements │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── SceneDelegate.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftUIcon │ ├── Icon+PreviewHelpers.swift │ ├── IconGenerator.swift │ └── main.swift └── build-script.sh /.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | *.DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Thumbnails 7 | ._* 8 | 9 | # Files that might appear in the root of a volume 10 | .DocumentRevisions-V100 11 | .fseventsd 12 | .Spotlight-V100 13 | .TemporaryItems 14 | .Trashes 15 | .VolumeIcon.icns 16 | .com.apple.timemachine.donotpresent 17 | 18 | # Directories potentially created on remote AFP share 19 | .AppleDB 20 | .AppleDesktop 21 | Network Trash Folder 22 | Temporary Items 23 | .apdisk 24 | 25 | ### Swift ### 26 | 27 | # Xcode 28 | ## Build generated 29 | build/ 30 | DerivedData/ 31 | 32 | ## Various settings 33 | *.pbxuser 34 | !default.pbxuser 35 | *.mode1v3 36 | !default.mode1v3 37 | *.mode2v3 38 | !default.mode2v3 39 | *.perspectivev3 40 | !default.perspectivev3 41 | xcuserdata/ 42 | 43 | ## Other 44 | *.moved-aside 45 | *.xccheckout 46 | *.xcscmblueprint 47 | 48 | ## Obj-C/Swift specific 49 | *.hmap 50 | *.ipa 51 | *.dSYM.zip 52 | *.dSYM 53 | *.mobileprovision 54 | -------------------------------------------------------------------------------- /Example/IconHarness.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 110076CB264A04EF0067CE56 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110076CA264A04EF0067CE56 /* Icon.swift */; }; 11 | 110076CE264A05030067CE56 /* SwiftUIcon in Frameworks */ = {isa = PBXBuildFile; productRef = 110076CD264A05030067CE56 /* SwiftUIcon */; }; 12 | 11DC1FFB2345959E00848840 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DC1FFA2345959E00848840 /* AppDelegate.swift */; }; 13 | 11DC1FFD2345959E00848840 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DC1FFC2345959E00848840 /* SceneDelegate.swift */; }; 14 | 11DC1FFF2345959E00848840 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DC1FFE2345959E00848840 /* ContentView.swift */; }; 15 | 11DC20012345959F00848840 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 11DC20002345959F00848840 /* Assets.xcassets */; }; 16 | 11DC20042345959F00848840 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 11DC20032345959F00848840 /* Preview Assets.xcassets */; }; 17 | 11DC20072345959F00848840 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11DC20052345959F00848840 /* LaunchScreen.storyboard */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 110076C9264A04A10067CE56 /* SwiftUIcon */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftUIcon; path = ../..; sourceTree = ""; }; 22 | 110076CA264A04EF0067CE56 /* Icon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; 23 | 11DC1FF72345959E00848840 /* IconHarness.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IconHarness.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | 11DC1FFA2345959E00848840 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | 11DC1FFC2345959E00848840 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 26 | 11DC1FFE2345959E00848840 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 27 | 11DC20002345959F00848840 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 11DC20032345959F00848840 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 29 | 11DC20062345959F00848840 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 11DC20082345959F00848840 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 77079A0B26448A650094512F /* IconHarness.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IconHarness.entitlements; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | 11DC1FF42345959E00848840 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | 110076CE264A05030067CE56 /* SwiftUIcon in Frameworks */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 110076CC264A05030067CE56 /* Frameworks */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | ); 50 | name = Frameworks; 51 | sourceTree = ""; 52 | }; 53 | 11DC1FEE2345959E00848840 = { 54 | isa = PBXGroup; 55 | children = ( 56 | 11DC1FF92345959E00848840 /* IconHarness */, 57 | 11DC1FF82345959E00848840 /* Products */, 58 | 110076CC264A05030067CE56 /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 11DC1FF82345959E00848840 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 11DC1FF72345959E00848840 /* IconHarness.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 11DC1FF92345959E00848840 /* IconHarness */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 77079A0B26448A650094512F /* IconHarness.entitlements */, 74 | 11DC1FFA2345959E00848840 /* AppDelegate.swift */, 75 | 11DC1FFC2345959E00848840 /* SceneDelegate.swift */, 76 | 11DC1FFE2345959E00848840 /* ContentView.swift */, 77 | 110076CA264A04EF0067CE56 /* Icon.swift */, 78 | 11DC20002345959F00848840 /* Assets.xcassets */, 79 | 11DC20052345959F00848840 /* LaunchScreen.storyboard */, 80 | 11DC20082345959F00848840 /* Info.plist */, 81 | 110076C9264A04A10067CE56 /* SwiftUIcon */, 82 | 11DC20022345959F00848840 /* Preview Content */, 83 | ); 84 | path = IconHarness; 85 | sourceTree = ""; 86 | }; 87 | 11DC20022345959F00848840 /* Preview Content */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 11DC20032345959F00848840 /* Preview Assets.xcassets */, 91 | ); 92 | path = "Preview Content"; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 11DC1FF62345959E00848840 /* IconHarness */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 11DC200B2345959F00848840 /* Build configuration list for PBXNativeTarget "IconHarness" */; 101 | buildPhases = ( 102 | 11DC1FF32345959E00848840 /* Sources */, 103 | 11DC1FF42345959E00848840 /* Frameworks */, 104 | 11DC201D23459AF100848840 /* Generate Icon */, 105 | 11DC1FF52345959E00848840 /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = IconHarness; 112 | packageProductDependencies = ( 113 | 110076CD264A05030067CE56 /* SwiftUIcon */, 114 | ); 115 | productName = IconHarness; 116 | productReference = 11DC1FF72345959E00848840 /* IconHarness.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | 11DC1FEF2345959E00848840 /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | LastSwiftUpdateCheck = 1110; 126 | LastUpgradeCheck = 1250; 127 | ORGANIZATIONNAME = "Velos Mobile LLC"; 128 | TargetAttributes = { 129 | 11DC1FF62345959E00848840 = { 130 | CreatedOnToolsVersion = 11.1; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 11DC1FF22345959E00848840 /* Build configuration list for PBXProject "IconHarness" */; 135 | compatibilityVersion = "Xcode 9.3"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = 11DC1FEE2345959E00848840; 143 | productRefGroup = 11DC1FF82345959E00848840 /* Products */; 144 | projectDirPath = ""; 145 | projectRoot = ""; 146 | targets = ( 147 | 11DC1FF62345959E00848840 /* IconHarness */, 148 | ); 149 | }; 150 | /* End PBXProject section */ 151 | 152 | /* Begin PBXResourcesBuildPhase section */ 153 | 11DC1FF52345959E00848840 /* Resources */ = { 154 | isa = PBXResourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 11DC20072345959F00848840 /* LaunchScreen.storyboard in Resources */, 158 | 11DC20042345959F00848840 /* Preview Assets.xcassets in Resources */, 159 | 11DC20012345959F00848840 /* Assets.xcassets in Resources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXResourcesBuildPhase section */ 164 | 165 | /* Begin PBXShellScriptBuildPhase section */ 166 | 11DC201D23459AF100848840 /* Generate Icon */ = { 167 | isa = PBXShellScriptBuildPhase; 168 | buildActionMask = 12; 169 | files = ( 170 | ); 171 | inputFileListPaths = ( 172 | ); 173 | inputPaths = ( 174 | "$(PROJECT_DIR)/$(PRODUCT_NAME)/Icon.swift", 175 | ); 176 | name = "Generate Icon"; 177 | outputFileListPaths = ( 178 | ); 179 | outputPaths = ( 180 | "$(PROJECT_DIR)/$(PRODUCT_NAME)/Assets.xcassets", 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "# For example purposes, this references a local file. But in your app, you'd do something like:\n# \"${BUILD_ROOT%Build/*}SourcePackages/checkouts/SwiftUIcon/build-script.sh\"\n\n\"${PROJECT_DIR}/../build-script.sh\"\n"; 185 | }; 186 | /* End PBXShellScriptBuildPhase section */ 187 | 188 | /* Begin PBXSourcesBuildPhase section */ 189 | 11DC1FF32345959E00848840 /* Sources */ = { 190 | isa = PBXSourcesBuildPhase; 191 | buildActionMask = 2147483647; 192 | files = ( 193 | 11DC1FFB2345959E00848840 /* AppDelegate.swift in Sources */, 194 | 11DC1FFD2345959E00848840 /* SceneDelegate.swift in Sources */, 195 | 110076CB264A04EF0067CE56 /* Icon.swift in Sources */, 196 | 11DC1FFF2345959E00848840 /* ContentView.swift in Sources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXSourcesBuildPhase section */ 201 | 202 | /* Begin PBXVariantGroup section */ 203 | 11DC20052345959F00848840 /* LaunchScreen.storyboard */ = { 204 | isa = PBXVariantGroup; 205 | children = ( 206 | 11DC20062345959F00848840 /* Base */, 207 | ); 208 | name = LaunchScreen.storyboard; 209 | sourceTree = ""; 210 | }; 211 | /* End PBXVariantGroup section */ 212 | 213 | /* Begin XCBuildConfiguration section */ 214 | 11DC20092345959F00848840 /* Debug */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | ALWAYS_SEARCH_USER_PATHS = NO; 218 | CLANG_ANALYZER_NONNULL = YES; 219 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 220 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 221 | CLANG_CXX_LIBRARY = "libc++"; 222 | CLANG_ENABLE_MODULES = YES; 223 | CLANG_ENABLE_OBJC_ARC = YES; 224 | CLANG_ENABLE_OBJC_WEAK = YES; 225 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 226 | CLANG_WARN_BOOL_CONVERSION = YES; 227 | CLANG_WARN_COMMA = YES; 228 | CLANG_WARN_CONSTANT_CONVERSION = YES; 229 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 230 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 231 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 232 | CLANG_WARN_EMPTY_BODY = YES; 233 | CLANG_WARN_ENUM_CONVERSION = YES; 234 | CLANG_WARN_INFINITE_RECURSION = YES; 235 | CLANG_WARN_INT_CONVERSION = YES; 236 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 238 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 240 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 241 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 242 | CLANG_WARN_STRICT_PROTOTYPES = YES; 243 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 244 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 245 | CLANG_WARN_UNREACHABLE_CODE = YES; 246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 247 | COPY_PHASE_STRIP = NO; 248 | DEBUG_INFORMATION_FORMAT = dwarf; 249 | ENABLE_STRICT_OBJC_MSGSEND = YES; 250 | ENABLE_TESTABILITY = YES; 251 | GCC_C_LANGUAGE_STANDARD = gnu11; 252 | GCC_DYNAMIC_NO_PIC = NO; 253 | GCC_NO_COMMON_BLOCKS = YES; 254 | GCC_OPTIMIZATION_LEVEL = 0; 255 | GCC_PREPROCESSOR_DEFINITIONS = ( 256 | "DEBUG=1", 257 | "$(inherited)", 258 | ); 259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 261 | GCC_WARN_UNDECLARED_SELECTOR = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 263 | GCC_WARN_UNUSED_FUNCTION = YES; 264 | GCC_WARN_UNUSED_VARIABLE = YES; 265 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 266 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 267 | MTL_FAST_MATH = YES; 268 | ONLY_ACTIVE_ARCH = YES; 269 | SDKROOT = iphoneos; 270 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 271 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 272 | }; 273 | name = Debug; 274 | }; 275 | 11DC200A2345959F00848840 /* Release */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ALWAYS_SEARCH_USER_PATHS = NO; 279 | CLANG_ANALYZER_NONNULL = YES; 280 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 281 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 282 | CLANG_CXX_LIBRARY = "libc++"; 283 | CLANG_ENABLE_MODULES = YES; 284 | CLANG_ENABLE_OBJC_ARC = YES; 285 | CLANG_ENABLE_OBJC_WEAK = YES; 286 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 287 | CLANG_WARN_BOOL_CONVERSION = YES; 288 | CLANG_WARN_COMMA = YES; 289 | CLANG_WARN_CONSTANT_CONVERSION = YES; 290 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 291 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 292 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 293 | CLANG_WARN_EMPTY_BODY = YES; 294 | CLANG_WARN_ENUM_CONVERSION = YES; 295 | CLANG_WARN_INFINITE_RECURSION = YES; 296 | CLANG_WARN_INT_CONVERSION = YES; 297 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 299 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 300 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 301 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 302 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 303 | CLANG_WARN_STRICT_PROTOTYPES = YES; 304 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 305 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 306 | CLANG_WARN_UNREACHABLE_CODE = YES; 307 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 308 | COPY_PHASE_STRIP = NO; 309 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 310 | ENABLE_NS_ASSERTIONS = NO; 311 | ENABLE_STRICT_OBJC_MSGSEND = YES; 312 | GCC_C_LANGUAGE_STANDARD = gnu11; 313 | GCC_NO_COMMON_BLOCKS = YES; 314 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 315 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 316 | GCC_WARN_UNDECLARED_SELECTOR = YES; 317 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 318 | GCC_WARN_UNUSED_FUNCTION = YES; 319 | GCC_WARN_UNUSED_VARIABLE = YES; 320 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 321 | MTL_ENABLE_DEBUG_INFO = NO; 322 | MTL_FAST_MATH = YES; 323 | SDKROOT = iphoneos; 324 | SWIFT_COMPILATION_MODE = wholemodule; 325 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 326 | VALIDATE_PRODUCT = YES; 327 | }; 328 | name = Release; 329 | }; 330 | 11DC200C2345959F00848840 /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 334 | CODE_SIGN_ENTITLEMENTS = IconHarness/IconHarness.entitlements; 335 | CODE_SIGN_STYLE = Automatic; 336 | DEVELOPMENT_ASSET_PATHS = "\"IconHarness/Preview Content\""; 337 | DEVELOPMENT_TEAM = 32R66SNDGS; 338 | ENABLE_PREVIEWS = YES; 339 | INFOPLIST_FILE = IconHarness/Info.plist; 340 | LD_RUNPATH_SEARCH_PATHS = ( 341 | "$(inherited)", 342 | "@executable_path/Frameworks", 343 | ); 344 | PRODUCT_BUNDLE_IDENTIFIER = com.velosmobile.IconHarness; 345 | PRODUCT_NAME = "$(TARGET_NAME)"; 346 | SUPPORTS_MACCATALYST = YES; 347 | SWIFT_VERSION = 5.0; 348 | TARGETED_DEVICE_FAMILY = "1,2"; 349 | }; 350 | name = Debug; 351 | }; 352 | 11DC200D2345959F00848840 /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 356 | CODE_SIGN_ENTITLEMENTS = IconHarness/IconHarness.entitlements; 357 | CODE_SIGN_STYLE = Automatic; 358 | DEVELOPMENT_ASSET_PATHS = "\"IconHarness/Preview Content\""; 359 | DEVELOPMENT_TEAM = 32R66SNDGS; 360 | ENABLE_PREVIEWS = YES; 361 | INFOPLIST_FILE = IconHarness/Info.plist; 362 | LD_RUNPATH_SEARCH_PATHS = ( 363 | "$(inherited)", 364 | "@executable_path/Frameworks", 365 | ); 366 | PRODUCT_BUNDLE_IDENTIFIER = com.velosmobile.IconHarness; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | SUPPORTS_MACCATALYST = YES; 369 | SWIFT_VERSION = 5.0; 370 | TARGETED_DEVICE_FAMILY = "1,2"; 371 | }; 372 | name = Release; 373 | }; 374 | /* End XCBuildConfiguration section */ 375 | 376 | /* Begin XCConfigurationList section */ 377 | 11DC1FF22345959E00848840 /* Build configuration list for PBXProject "IconHarness" */ = { 378 | isa = XCConfigurationList; 379 | buildConfigurations = ( 380 | 11DC20092345959F00848840 /* Debug */, 381 | 11DC200A2345959F00848840 /* Release */, 382 | ); 383 | defaultConfigurationIsVisible = 0; 384 | defaultConfigurationName = Release; 385 | }; 386 | 11DC200B2345959F00848840 /* Build configuration list for PBXNativeTarget "IconHarness" */ = { 387 | isa = XCConfigurationList; 388 | buildConfigurations = ( 389 | 11DC200C2345959F00848840 /* Debug */, 390 | 11DC200D2345959F00848840 /* Release */, 391 | ); 392 | defaultConfigurationIsVisible = 0; 393 | defaultConfigurationName = Release; 394 | }; 395 | /* End XCConfigurationList section */ 396 | 397 | /* Begin XCSwiftPackageProductDependency section */ 398 | 110076CD264A05030067CE56 /* SwiftUIcon */ = { 399 | isa = XCSwiftPackageProductDependency; 400 | productName = SwiftUIcon; 401 | }; 402 | /* End XCSwiftPackageProductDependency section */ 403 | }; 404 | rootObject = 11DC1FEF2345959E00848840 /* Project object */; 405 | } 406 | -------------------------------------------------------------------------------- /Example/IconHarness.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/IconHarness.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/IconHarness.xcodeproj/xcshareddata/xcschemes/IconHarness.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/IconHarness/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // IconHarness 4 | // 5 | // Created by Zac White. 6 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | return true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ios-marketing-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ios-marketing-1024x1024.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-20x20.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-20x20@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-29x29.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-29x29@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-40x40.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-40x40@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-76x76.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-76x76@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-ipad-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-20x20@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-20x20@3x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-29x29@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-29x29@3x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-40x40@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-40x40@3x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-60x60@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-iphone-60x60@3x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-128x128.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-128x128@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-16x16.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-16x16@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-256x256.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-256x256@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-32x32.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-32x32@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-512x512.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velos/SwiftUIcon/f530d4dd8ef80303675db5db12349cb53f59ae35/Example/IconHarness/Assets.xcassets/AppIcon.appiconset/AppIcon-mac-512x512@2x.png -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"20x20","idiom":"ipad","filename":"AppIcon-ipad-20x20.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-ipad-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-ipad-29x29.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-ipad-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-ipad-40x40.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-ipad-40x40@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-ipad-76x76.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-ipad-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-ipad-83.5x83.5@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-iphone-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-iphone-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-iphone-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-iphone-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-iphone-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-iphone-40x40@3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-iphone-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-iphone-60x60@3x.png","scale":"3x"},{"size":"16x16","idiom":"mac","filename":"AppIcon-mac-16x16.png","scale":"1x"},{"size":"16x16","idiom":"mac","filename":"AppIcon-mac-16x16@2x.png","scale":"2x"},{"size":"32x32","idiom":"mac","filename":"AppIcon-mac-32x32.png","scale":"1x"},{"size":"32x32","idiom":"mac","filename":"AppIcon-mac-32x32@2x.png","scale":"2x"},{"size":"128x128","idiom":"mac","filename":"AppIcon-mac-128x128.png","scale":"1x"},{"size":"128x128","idiom":"mac","filename":"AppIcon-mac-128x128@2x.png","scale":"2x"},{"size":"256x256","idiom":"mac","filename":"AppIcon-mac-256x256.png","scale":"1x"},{"size":"256x256","idiom":"mac","filename":"AppIcon-mac-256x256@2x.png","scale":"2x"},{"size":"512x512","idiom":"mac","filename":"AppIcon-mac-512x512.png","scale":"1x"},{"size":"512x512","idiom":"mac","filename":"AppIcon-mac-512x512@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-ios-marketing-1024x1024.png","scale":"1x"}]} -------------------------------------------------------------------------------- /Example/IconHarness/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/IconHarness/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /Example/IconHarness/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // IconHarness 4 | // 5 | // Created by Zac White. 6 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | Icon() 14 | .frameIcon() 15 | } 16 | } 17 | 18 | #if DEBUG 19 | struct ContentView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | ContentView() 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Example/IconHarness/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon.swift 3 | // 4 | // Created by Zac White. 5 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIcon 10 | 11 | struct Icon: View { 12 | 13 | var body: some View { 14 | /// Note: All of these assume a canvas size of 1024. 15 | let spacing: CGFloat = 80 16 | let radius: CGFloat = 135 17 | let pillLength: CGFloat = 350 18 | let pillRotation: Angle = .degrees(30) 19 | let circleOffsetX: CGFloat = 50 20 | let circleOffsetY: CGFloat = 20 21 | 22 | let velosBackground = Color(red: 0/256, green: 180/256, blue: 185/256) 23 | let velosPrimary = Color.white 24 | let velosSecondary = Color(red: 248/256, green: 208/256, blue: 55/256) 25 | 26 | return IconStack { canvas in 27 | velosBackground 28 | .edgesIgnoringSafeArea(.all) 29 | 30 | HStack(alignment: .center, spacing: canvas[spacing]) { 31 | HStack(alignment: .top, spacing: canvas[spacing]) { 32 | Circle() 33 | .fill(velosPrimary) 34 | .frame(width: canvas[radius], height: canvas[radius]) 35 | .offset(x: canvas[circleOffsetX], y: canvas[circleOffsetY]) 36 | RoundedRectangle(cornerRadius: canvas[radius]) 37 | .fill(velosPrimary) 38 | .frame(width: canvas[radius], height: canvas[pillLength]) 39 | .rotationEffect(pillRotation) 40 | } 41 | HStack(alignment: .bottom, spacing: canvas[spacing]) { 42 | RoundedRectangle(cornerRadius: canvas[radius]) 43 | .fill(velosSecondary) 44 | .frame(width: canvas[radius], height: canvas[pillLength]) 45 | .rotationEffect(pillRotation) 46 | RoundedRectangle(cornerRadius: canvas[radius]) 47 | .fill(velosSecondary) 48 | .frame(width: canvas[radius], height: canvas[pillLength]) 49 | .rotationEffect(pillRotation) 50 | Circle() 51 | .fill(velosSecondary) 52 | .frame(width: canvas[radius], height: canvas[radius]) 53 | .offset(x: -canvas[circleOffsetX], y: -canvas[circleOffsetY]) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | #if DEBUG 61 | struct Icon_Previews : PreviewProvider { 62 | static var previews: some View { 63 | Group { 64 | Icon() 65 | .previewIcon() 66 | .previewLayout(.sizeThatFits) 67 | 68 | Icon() 69 | .previewHomescreen() 70 | .background( 71 | LinearGradient( 72 | gradient: Gradient(colors: [.purple, .orange]), 73 | startPoint: .bottom, 74 | endPoint: .top 75 | ) 76 | ) 77 | .previewLayout(.fixed(width: 500, height: 500)) 78 | } 79 | } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /Example/IconHarness/IconHarness.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/IconHarness/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Example/IconHarness/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/IconHarness/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // IconHarness 4 | // 5 | // Created by Zac White. 6 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | let contentView = ContentView() 18 | 19 | if let windowScene = scene as? UIWindowScene { 20 | let window = UIWindow(windowScene: windowScene) 21 | window.rootViewController = UIHostingController(rootView: contentView) 22 | self.window = window 23 | window.makeKeyAndVisible() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Velos Mobile LLC 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: "SwiftUIcon", 7 | products: [ 8 | .library(name: "SwiftUIcon", targets: ["SwiftUIcon"]), 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target( 13 | name: "SwiftUIcon", 14 | exclude: [ 15 | "main.swift", 16 | "IconGenerator.swift" 17 | ], 18 | sources: ["Icon+PreviewHelpers.swift"] 19 | ), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUIcon 2 | 3 | **SwiftUIIcon** is a library that provides scripts and preview helpers for drawing and generating your iOS, iPad OS, and macOS app icons in SwiftUI using shape and path drawing primitives. 4 | 5 | By defining your app's icon in code, SwiftUIcon can generate all image sizes you need, show you real-time previews of your icon changes and can even allow you to integrate your icon right into your app's view hierarchy. 6 | 7 |

8 | 9 |

10 | 11 | ## Getting Started 12 | 13 | 1. Add SwiftUIcon to your project using Swift Package Manager. Add the library to the app target. 14 | 2. Create an `Icon.swift` file in your project. It must have a `View` called `Icon`. Check out [the example](Example/IconHarness/Icon.swift) for a more complex icon, but this could help get you started: 15 | 16 | ```Swift 17 | struct Icon: View { 18 | var body: some View { 19 | IconStack { canvas in 20 | Color.blue 21 | } 22 | } 23 | } 24 | 25 | #if DEBUG 26 | struct Icon_Previews : PreviewProvider { 27 | static var previews: some View { 28 | IconPreviews(icon: Icon()) 29 | } 30 | } 31 | #endif 32 | ``` 33 | 34 | 3. Add a Run Script build phase before your Copy Resources phase calling the `build-script.sh` included in the package. You'll need to specify the path to your `Icon.swift` as the Input File (probably `$(PROJECT_DIR)/$(PRODUCT_NAME)/Icon.swift`) and the `Assets.xcassets` as the output file (probably `$(PROJECT_DIR)/$(PRODUCT_NAME)/Assets.xcassets`): 35 | 36 | ```bash 37 | "${BUILD_ROOT%Build/*}SourcePackages/checkouts/SwiftUIcon/build-script.sh" 38 | ``` 39 |

40 | 41 |

42 | 43 | ## Adding Your Icon 44 | 45 | You can now edit the contents of the `IconStack` wrapper helper view inside of `Icon.swift` and add any shapes, paths or colors you want to build your icon. See [Apple's SwiftUI Drawing Tutorial](https://developer.apple.com/tutorials/swiftui/drawing-paths-and-shapes) for more info on drawing using SwiftUI. 46 | 47 | The `IconStack` is essentially a `ZStack` with a `.center` alignment that provides a `CanvasProxy` value to relatively position elements based on an assumed 1024x1024 canvas size. This is helpful because the `Icon` is rendered individually at all the required icon sizes, so anything that has a fixed size will not scale properly. Any place where you would normally have a hard-coded number like `42`, you should instead use `canvas[42]`. You can also scale fonts and other elements manually using the `CanvasProxy.scale` property. As an example, `Text("Testing").font(Font.system(size: 200 * canvas.scale))` should get you a properly scaled `Text` element. 48 | 49 | ## Limitations 50 | 51 | * It's a bit of a hack, but [Velos](https://velosmobile.com/) is currently using it in a project 👍 52 | * Since the run script essentially concatenates all the Swift files and runs it as a macOS script, any elements you use in your Icon will be rendered using your Mac's version of SwiftUI. Because of this, there might be some differences or changes between macOS versions or between macOS and iOS that could manifest in your Icon. You should probably also stay away from putting UI elements like `Slider` in your Icon too 😉 53 | 54 | ## License 55 | MIT 56 | 57 | ## Contact 58 | * Email - zac@velosmobile.com 59 | * Github - [@zac](https://github.com/zac) / [@velos](https://github.com/velos) 60 | * Twitter - [@zacwhite](https://twitter.com/zacwhite) / [@velosmobile](https://twitter.com/velosmobile) 61 | -------------------------------------------------------------------------------- /Sources/SwiftUIcon/Icon+PreviewHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon+PreviewHelpers.swift 3 | // 4 | // Created by Zac White. 5 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 11 | public struct IconPreviews: View { 12 | var icon: () -> Icon 13 | 14 | public init(icon: @autoclosure @escaping () -> Icon) { 15 | self.icon = icon 16 | } 17 | 18 | public var body: some View { 19 | Group { 20 | icon() 21 | .previewIcon() 22 | .previewLayout(.sizeThatFits) 23 | 24 | icon() 25 | .previewHomescreen() 26 | .background( 27 | LinearGradient( 28 | gradient: Gradient(colors: [.purple, .orange]), 29 | startPoint: .bottom, 30 | endPoint: .top 31 | ) 32 | ) 33 | .previewLayout(.fixed(width: 500, height: 500)) 34 | } 35 | } 36 | } 37 | 38 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 39 | private enum Constants { 40 | static let radiusFactor: CGFloat = 4.3 41 | static let appNameFont: Font = .system(size: 18, weight: .medium) 42 | static let iconDimension: CGFloat = 100 43 | static let stackDimension: CGFloat = 1024 44 | } 45 | 46 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 47 | public struct IconStack: View where IconContent: View { 48 | 49 | public var body: some View { 50 | return GeometryReader { proxy in 51 | ZStack(alignment: .center) { 52 | self.content(CanvasProxy(proxy: proxy, expected: CGSize(width: Constants.stackDimension, height: Constants.stackDimension))) 53 | } 54 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top) 55 | } 56 | .edgesIgnoringSafeArea(.all) 57 | .aspectRatio(1, contentMode: .fit) 58 | .clipped() 59 | } 60 | 61 | public var content: (CanvasProxy) -> IconContent 62 | 63 | public init(@ViewBuilder content: @escaping (CanvasProxy) -> IconContent) { 64 | self.content = content 65 | } 66 | } 67 | 68 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 69 | public struct CanvasProxy { 70 | private let proxy: GeometryProxy 71 | public let expected: CGSize 72 | 73 | init(proxy: GeometryProxy, expected: CGSize) { 74 | self.proxy = proxy 75 | self.expected = expected 76 | } 77 | 78 | /// The size of the container view. 79 | public var actual: CGSize { 80 | return proxy.size 81 | } 82 | 83 | public var scale: CGFloat { 84 | return self.actual.width / self.expected.width 85 | } 86 | 87 | public subscript(_ points: CGFloat) -> CGFloat { 88 | return points * scale 89 | } 90 | } 91 | 92 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 93 | extension View { 94 | 95 | /// Frames the icon and adds a continuous rounded rectangle to approximate the real icon shape. 96 | /// - Parameter dimension: <#dimension description#> 97 | /// - Returns: <#description#> 98 | public func frameIcon(dimension: CGFloat = 200) -> some View { 99 | self 100 | .frame(width: dimension, height: dimension) 101 | .clipShape(RoundedRectangle(cornerRadius: dimension / Constants.radiusFactor, style: .continuous)) 102 | } 103 | } 104 | 105 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 106 | extension View { 107 | 108 | /// Previews the icon using `.previewLayout()` with a `.fixed(width:height:)` layout, clipping to an approximation of the Apple icon shape 109 | /// - Parameter dimension: The width and height to use when laying out the icon. Defaults to 500 110 | /// - Returns: A View suitible for previewing in SwiftUI previews 111 | public func previewIcon(dimension: CGFloat = 500) -> some View { 112 | self 113 | .previewLayout(.fixed(width: dimension, height: dimension)) 114 | .clipShape(RoundedRectangle(cornerRadius: dimension / Constants.radiusFactor, style: .continuous)) 115 | } 116 | 117 | /// A View representing the app's name 118 | /// - Parameter name: The name to use 119 | /// - Returns: A View with text for the passed in name 120 | private func app(name: String) -> some View { 121 | Text(name) 122 | .foregroundColor(.white) 123 | .font(Constants.appNameFont) 124 | .shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 0) 125 | } 126 | 127 | /// A fake icon for use in the `previewHomescreenIcon(dimension:)` function 128 | /// - Parameters: 129 | /// - color: The color to use as the background 130 | /// - dimension: The width and height to use when laying out the icon. Defaults to 100 131 | /// - name: The name of the fake app 132 | /// - Returns: A View which fakes the icon of another app 133 | private func fakeIcon(_ color: Color, dimension: CGFloat = 100, name: String) -> some View { 134 | VStack(spacing: 10) { 135 | RoundedRectangle(cornerRadius: Constants.iconDimension / Constants.radiusFactor, style: .continuous) 136 | .fill(color) 137 | .frame(width: dimension, height: dimension) 138 | .padding([.leading, .trailing], 20) 139 | app(name: name) 140 | } 141 | } 142 | 143 | /// Previews the icon as a 'homescreen' icon with the icon and app name with approximately correct padding 144 | /// - Parameter dimension: The dimension to make the icon in the preview 145 | /// - Returns: A View suitable for preview of just your app's icon on the homescreen 146 | public func previewHomescreenIcon(dimension: CGFloat = 100) -> some View { 147 | VStack(spacing: 10) { 148 | self.previewIcon(dimension: Constants.iconDimension) 149 | .frame(width: Constants.iconDimension, height: Constants.iconDimension) 150 | .padding([.leading, .trailing], 20) 151 | app(name: (Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "Your App") 152 | } 153 | } 154 | 155 | /// Previews an entire Homescreen with 9 apps where your app's icon is the middle icon 156 | /// - Returns: A View suitable for preview of your app plus several other fake app icons 157 | public func previewHomescreen() -> some View { 158 | let spacingX: CGFloat = 10 159 | let spacingY: CGFloat = 30 160 | return VStack(spacing: spacingY) { 161 | HStack(spacing: spacingX) { 162 | fakeIcon(Color.white, dimension: Constants.iconDimension, name: "Other App") 163 | fakeIcon(Color.purple, dimension: Constants.iconDimension, name: "Other App") 164 | fakeIcon(Color.red, dimension: Constants.iconDimension, name: "Other App") 165 | } 166 | HStack(spacing: spacingX) { 167 | fakeIcon(Color.gray, dimension: Constants.iconDimension, name: "Other App") 168 | previewHomescreenIcon(dimension: Constants.iconDimension) 169 | fakeIcon(Color.green, dimension: Constants.iconDimension, name: "Other App") 170 | } 171 | HStack(spacing: spacingX) { 172 | fakeIcon(Color.orange, dimension: Constants.iconDimension, name: "Other App") 173 | fakeIcon(Color.pink, dimension: Constants.iconDimension, name: "Other App") 174 | fakeIcon(Color.black, dimension: Constants.iconDimension, name: "Other App") 175 | } 176 | } 177 | .padding(50) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/SwiftUIcon/IconGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconGenerator.swift 3 | // 4 | // Created by Zac White. 5 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | #if os(macOS) 12 | 13 | enum Idiom: String { 14 | case iPad = "ipad" 15 | case iPhone = "iphone" 16 | case mac = "mac" 17 | case marketing = "ios-marketing" 18 | } 19 | 20 | /// An IconSet based around a View for a given set of idioms 21 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 22 | struct IconSet: Encodable { 23 | 24 | /// The View to use when generating the IconSet 25 | let content: Content 26 | 27 | /// The Icon instances to use when generating metadata 28 | let images: [Icon] 29 | 30 | /// Creates an IconSet for the given idioms based on the passed in View 31 | /// - Parameter idioms: A Set of idioms to use when generating the IconSet 32 | /// - Parameter view: The View to base the generated icon off of 33 | init(idioms: Set, view: Content) { 34 | images = [ 35 | .init(idiom: .iPad, size: CGSize(width: 20, height: 20), scale: .oneX, placeholder: !idioms.contains(.iPad)), 36 | .init(idiom: .iPad, size: CGSize(width: 20, height: 20), scale: .twoX, placeholder: !idioms.contains(.iPad)), 37 | 38 | .init(idiom: .iPad, size: CGSize(width: 29, height: 29), scale: .oneX, placeholder: !idioms.contains(.iPad)), 39 | .init(idiom: .iPad, size: CGSize(width: 29, height: 29), scale: .twoX, placeholder: !idioms.contains(.iPad)), 40 | 41 | .init(idiom: .iPad, size: CGSize(width: 40, height: 40), scale: .oneX, placeholder: !idioms.contains(.iPad)), 42 | .init(idiom: .iPad, size: CGSize(width: 40, height: 40), scale: .twoX, placeholder: !idioms.contains(.iPad)), 43 | 44 | .init(idiom: .iPad, size: CGSize(width: 76, height: 76), scale: .oneX, placeholder: !idioms.contains(.iPad)), 45 | .init(idiom: .iPad, size: CGSize(width: 76, height: 76), scale: .twoX, placeholder: !idioms.contains(.iPad)), 46 | 47 | .init(idiom: .iPad, size: CGSize(width: 83.5, height: 83.5), scale: .twoX, placeholder: !idioms.contains(.iPad)), 48 | 49 | .init(idiom: .iPhone, size: CGSize(width: 20, height: 20), scale: .twoX, placeholder: !idioms.contains(.iPhone)), 50 | .init(idiom: .iPhone, size: CGSize(width: 20, height: 20), scale: .threeX, placeholder: !idioms.contains(.iPhone)), 51 | 52 | .init(idiom: .iPhone, size: CGSize(width: 29, height: 29), scale: .twoX, placeholder: !idioms.contains(.iPhone)), 53 | .init(idiom: .iPhone, size: CGSize(width: 29, height: 29), scale: .threeX, placeholder: !idioms.contains(.iPhone)), 54 | 55 | .init(idiom: .iPhone, size: CGSize(width: 40, height: 40), scale: .twoX, placeholder: !idioms.contains(.iPhone)), 56 | .init(idiom: .iPhone, size: CGSize(width: 40, height: 40), scale: .threeX, placeholder: !idioms.contains(.iPhone)), 57 | 58 | .init(idiom: .iPhone, size: CGSize(width: 60, height: 60), scale: .twoX, placeholder: !idioms.contains(.iPhone)), 59 | .init(idiom: .iPhone, size: CGSize(width: 60, height: 60), scale: .threeX, placeholder: !idioms.contains(.iPhone)), 60 | 61 | .init(idiom: .mac, size: CGSize(width: 16, height: 16), scale: .oneX, placeholder: !idioms.contains(.mac)), 62 | .init(idiom: .mac, size: CGSize(width: 16, height: 16), scale: .twoX, placeholder: !idioms.contains(.mac)), 63 | 64 | .init(idiom: .mac, size: CGSize(width: 32, height: 32), scale: .oneX, placeholder: !idioms.contains(.mac)), 65 | .init(idiom: .mac, size: CGSize(width: 32, height: 32), scale: .twoX, placeholder: !idioms.contains(.mac)), 66 | 67 | .init(idiom: .mac, size: CGSize(width: 128, height: 128), scale: .oneX, placeholder: !idioms.contains(.mac)), 68 | .init(idiom: .mac, size: CGSize(width: 128, height: 128), scale: .twoX, placeholder: !idioms.contains(.mac)), 69 | 70 | .init(idiom: .mac, size: CGSize(width: 256, height: 256), scale: .oneX, placeholder: !idioms.contains(.mac)), 71 | .init(idiom: .mac, size: CGSize(width: 256, height: 256), scale: .twoX, placeholder: !idioms.contains(.mac)), 72 | 73 | .init(idiom: .mac, size: CGSize(width: 512, height: 512), scale: .oneX, placeholder: !idioms.contains(.mac)), 74 | .init(idiom: .mac, size: CGSize(width: 512, height: 512), scale: .twoX, placeholder: !idioms.contains(.mac)), 75 | 76 | .init(idiom: .marketing, size: CGSize(width: 1024, height: 1024), scale: .oneX, placeholder: !idioms.contains(.marketing)) 77 | ] 78 | 79 | content = view 80 | } 81 | 82 | enum Scale: String { 83 | case oneX = "1x" 84 | case twoX = "2x" 85 | case threeX = "3x" 86 | 87 | var multiplier: CGFloat { 88 | switch self { 89 | case .oneX: return 1 90 | case .twoX: return 2 91 | case .threeX: return 3 92 | } 93 | } 94 | } 95 | 96 | struct Icon: Hashable, Encodable { 97 | let idiom: Idiom 98 | let size: CGSize 99 | let scale: Scale 100 | let placeholder: Bool 101 | 102 | fileprivate var filename: String? { 103 | guard !placeholder else { return nil } 104 | 105 | var fullName = "AppIcon" 106 | fullName.append("-\(idiom.rawValue)") 107 | fullName.append("-\(size.sizeString)") 108 | 109 | if scale != .oneX { 110 | fullName.append("@\(scale.rawValue)") 111 | } 112 | 113 | fullName.append(".png") 114 | 115 | return fullName 116 | } 117 | 118 | func hash(into hasher: inout Hasher) { 119 | hasher.combine(idiom) 120 | hasher.combine(scale) 121 | hasher.combine(size.width) 122 | hasher.combine(size.height) 123 | } 124 | 125 | enum CodingKeys: CodingKey { 126 | case idiom, size, scale, filename 127 | } 128 | 129 | func encode(to encoder: Encoder) throws { 130 | var container = encoder.container(keyedBy: CodingKeys.self) 131 | try container.encode(idiom.rawValue, forKey: .idiom) 132 | try container.encode(scale.rawValue, forKey: .scale) 133 | try container.encode(size.sizeString, forKey: .size) 134 | try container.encodeIfPresent(filename, forKey: .filename) 135 | } 136 | } 137 | 138 | private enum CodingKeys: CodingKey { 139 | case images 140 | } 141 | 142 | /// Writes the AppIcon.appiconset to the given URL for the xcassets folder. This will overwrite any existing AppIcon.appiconset that exists and will fail if any 143 | /// of the icons can't be generated or written to the proper locations. 144 | /// - Parameter url: The file URL that points to the `Assets.xcassets` folder in the project directory 145 | func write(to url: URL) throws { 146 | 147 | // create the folder "AppIcon.appiconset" 148 | let iconSetUrl = url.appendingPathComponent("AppIcon.appiconset", isDirectory: true) 149 | 150 | // remove any existing icon set 151 | try? FileManager.default.removeItem(at: iconSetUrl) 152 | 153 | // create all the directories needed to start writing the image files 154 | try FileManager.default.createDirectory(at: iconSetUrl, withIntermediateDirectories: true, attributes: nil) 155 | 156 | for image in images { 157 | if !image.placeholder, let filename = image.filename { 158 | try content.writeImage( 159 | to: iconSetUrl.appendingPathComponent(filename), 160 | size: image.size * image.scale.multiplier, 161 | roundedFrame: image.idiom == .mac 162 | ) 163 | } 164 | } 165 | 166 | // encode the manifest 167 | let manifest = try JSONEncoder().encode(self) 168 | 169 | // write the manifest to the `Contents.json` files 170 | try manifest.write(to: iconSetUrl.appendingPathComponent("Contents.json")) 171 | } 172 | } 173 | 174 | extension CGSize { 175 | 176 | /// A size string suitible for putting into the size property of the Contents.json file. Ex. "83.5x83.5" or "76x76" 177 | var sizeString: String { 178 | var format: String = "" 179 | if width.distance(to: round(width)) < 0.001 { 180 | format.append("%.0f") 181 | } else { 182 | format.append("%.1f") 183 | } 184 | 185 | format.append("x") 186 | 187 | if height.distance(to: round(height)) < 0.001 { 188 | format.append("%.0f") 189 | } else { 190 | format.append("%.1f") 191 | } 192 | 193 | return String(format: format, width, height) 194 | } 195 | 196 | /// Scales a size by the right-hand-side value 197 | /// - Parameter lhs: The size to scale 198 | /// - Parameter rhs: The factor to use when scaling the size 199 | static func * (lhs: CGSize, rhs: CGFloat) -> CGSize { 200 | return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) 201 | } 202 | } 203 | 204 | enum GenerationError: Error { 205 | case couldNotGetImageRep 206 | case couldNotGeneratePNG 207 | case couldNotWriteImage 208 | } 209 | 210 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 211 | extension View { 212 | 213 | /// Generates an image from the current View 214 | /// - Parameter url: The file url to write the image 215 | /// - Parameter size: The size of the image to generate 216 | /// - Parameter roundedFrame: Whether the image should be clipped to macOS like rounded frame or not 217 | public func writeImage(to url: URL, size: CGSize, roundedFrame: Bool = false) throws { 218 | 219 | // works around FB9488576 by scaling up the view, and then scaling down the rasterized image 220 | let upscaledSize = CGSize(width: size.width * 4.0, height: size.height * 4.0) 221 | 222 | let wrapper = roundedFrame 223 | ? NSHostingView(rootView: self.frameIcon(dimension: min(upscaledSize.width, upscaledSize.height) * 0.8)) 224 | : NSHostingView(rootView: self) 225 | wrapper.frame = CGRect(origin: .zero, size: upscaledSize) 226 | 227 | guard let bitmapRepresentation = wrapper.bitmapImageRepForCachingDisplay(in: wrapper.bounds) else { 228 | throw GenerationError.couldNotGetImageRep 229 | } 230 | 231 | bitmapRepresentation.size = wrapper.bounds.size 232 | wrapper.cacheDisplay(in: wrapper.bounds, to: bitmapRepresentation) 233 | 234 | guard let image = bitmapRepresentation.cgImage else { 235 | throw GenerationError.couldNotGeneratePNG 236 | } 237 | 238 | // generate a CGContext and draw the image into it at the desired size 239 | let context = CGContext( 240 | data: nil, 241 | width: Int(size.width), 242 | height: Int(size.height), 243 | bitsPerComponent: image.bitsPerComponent, 244 | bytesPerRow: 0, 245 | space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!, 246 | bitmapInfo: image.bitmapInfo.rawValue 247 | ) 248 | 249 | context?.interpolationQuality = .high 250 | context?.draw(image, in: CGRect(origin: .zero, size: size)) 251 | 252 | guard let scaledImage = context?.makeImage() else { 253 | throw GenerationError.couldNotGeneratePNG 254 | } 255 | 256 | // write the image to disk 257 | guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else { 258 | throw GenerationError.couldNotWriteImage 259 | } 260 | 261 | CGImageDestinationAddImage(destination, scaledImage, nil) 262 | if !CGImageDestinationFinalize(destination) { 263 | throw GenerationError.couldNotWriteImage 264 | } 265 | } 266 | } 267 | 268 | #endif 269 | -------------------------------------------------------------------------------- /Sources/SwiftUIcon/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // Created by Zac White. 5 | // Copyright © 2020 Velos Mobile LLC / https://velosmobile.com / All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | let env = ProcessInfo.processInfo.environment 11 | 12 | guard let assets = env["SCRIPT_OUTPUT_FILE_0"], 13 | let deviceFamily = env["TARGETED_DEVICE_FAMILY"], 14 | let projectPath = env["PROJECT_DIR"], 15 | let project = env["PROJECT"] else { 16 | print("error: Missing environment variables, this should have been caught by the build-script.sh") 17 | exit(1) 18 | } 19 | 20 | let macCatalyst = env["SUPPORTS_MACCATALYST"] 21 | 22 | var idioms: Set = [.marketing] 23 | 24 | if deviceFamily.contains("1") { 25 | idioms.insert(.iPhone) 26 | } 27 | 28 | if deviceFamily.contains("2") { 29 | idioms.insert(.iPad) 30 | } 31 | 32 | if macCatalyst == "YES" { 33 | idioms.insert(.mac) 34 | } 35 | 36 | if #available(OSX 10.15, *) { 37 | let set = IconSet(idioms: idioms, view: Icon()) 38 | 39 | try set.write( 40 | to: URL(fileURLWithPath: assets) 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /build-script.sh: -------------------------------------------------------------------------------- 1 | [[ "$ENABLE_PREVIEWS" = "NO" ]] || exit 0 2 | [[ "$MAC_OS_X_VERSION_MAJOR" -ge "101500" ]] || exit 0 3 | 4 | TMPFILE=`mktemp /tmp/SwiftUIcon.swift.XXXXXX` || exit 1 5 | trap "rm -f $TMPFILE" EXIT 6 | 7 | 8 | [[ -s "$SCRIPT_INPUT_FILE_0" ]] && [ "${SCRIPT_INPUT_FILE_0: -5}" == "swift" ] || { 9 | echo "error: You must specify your Icon.swift as the first Input File in the Build Phase." 10 | exit 1 11 | } 12 | 13 | [[ -s "$SCRIPT_OUTPUT_FILE_0" ]] && [ "${SCRIPT_OUTPUT_FILE_0: -8}" == "xcassets" ] || { 14 | echo "error: You must specify your Assets file as the first Output File in the Build Phase." 15 | exit 1 16 | } 17 | 18 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 19 | 20 | HELPER="$SCRIPT_DIR/Sources/SwiftUIcon/Icon+PreviewHelpers.swift" 21 | GENERATOR="$SCRIPT_DIR/Sources/SwiftUIcon/IconGenerator.swift" 22 | MAIN="$SCRIPT_DIR/Sources/SwiftUIcon/main.swift" 23 | 24 | # Concatenate all files and remove import that is most likely in the input file 25 | cat $SCRIPT_INPUT_FILE_0 $HELPER $GENERATOR $MAIN | grep -v "import\sSwiftUIcon" > $TMPFILE 26 | 27 | xcrun -sdk macosx swift $TMPFILE || echo "error: Failed to generate icons from $SCRIPT_INPUT_FILE_0 into $SCRIPT_OUTPUT_FILE_0" 28 | --------------------------------------------------------------------------------