├── .github ├── FUNDING.yml └── workflows │ ├── test.yml │ ├── build.yml │ ├── docc.yml │ ├── version-bump.yml │ └── xcframework.yml ├── .gitignore ├── Resources ├── Preview.png ├── Icon-Badge.png └── Icon-Plain.png ├── Demo ├── Demo │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AlternateIcons │ │ │ │ ├── Contents.json │ │ │ │ ├── AppIcon-Red.imageset │ │ │ │ │ ├── AppIcon-Red.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon-Blue.imageset │ │ │ │ │ ├── AppIcon-Blue.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon-Green.imageset │ │ │ │ │ ├── AppIcon-Green.png │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon-Yellow.imageset │ │ │ │ │ ├── AppIcon-Yellow.png │ │ │ │ │ └── Contents.json │ │ │ ├── AlternateAppIcons │ │ │ │ ├── Contents.json │ │ │ │ ├── AppIcon-Blue.appiconset │ │ │ │ │ ├── Icon.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon-Red.appiconset │ │ │ │ │ ├── AppIcon-Red.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon-Green.appiconset │ │ │ │ │ ├── AppIcon-Green.png │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon-Yellow.appiconset │ │ │ │ │ ├── AppIcon-Yellow.png │ │ │ │ │ └── Contents.json │ │ │ └── AccentColor.colorset │ │ │ │ └── Contents.json │ │ └── AppIcon.icon │ │ │ ├── Assets │ │ │ └── Icon 2.png │ │ │ └── icon.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── DemoApp.swift │ ├── AlternateAppIcon+Demo.swift │ └── ContentView.swift └── Demo.xcodeproj │ ├── project.xcworkspace │ └── contents.xcworkspacedata │ └── project.pbxproj ├── Sources └── AppIconKit │ ├── Images.xcassets │ ├── Contents.json │ └── AppIcon.imageset │ │ ├── Icon-CandyShop-CuppyCake.png │ │ └── Contents.json │ ├── AppIconKit.docc │ ├── Resources │ │ ├── Logo.png │ │ └── Preview.png │ └── AppIconKit.md │ ├── AlternateAppIcon.swift │ ├── AlternateAppIconContext.swift │ ├── AlternateAppIconCollection.swift │ ├── AlternateAppIconShelf.swift │ ├── ItemShelf.swift │ └── AlternateAppIconListItem.swift ├── scripts ├── tools │ └── StringCatalogKeyBuilder │ │ ├── Sources │ │ └── StringCatalogKeyBuilder │ │ │ ├── main.swift │ │ │ └── StringCatalogParserCommand.swift │ │ ├── Package.resolved │ │ ├── Package.swift │ │ └── README.md ├── git-default-branch.sh ├── package-name.sh ├── version-number.sh ├── chmod-all.sh ├── sync-from.sh ├── sync-to.sh ├── release-validate-git.sh ├── build.sh ├── release-validate-package.sh ├── test.sh ├── release.sh ├── version-bump.sh ├── l10n-gen.sh ├── docc.sh └── xcframework.sh ├── Tests └── AppIconKitTests │ └── AppIconKitTests.swift ├── .swiftlint.yml ├── Package.swift ├── LICENSE ├── RELEASE_NOTES.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [danielsaidi] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | .swiftpm/ 5 | xcuserdata/ 6 | DerivedData/ -------------------------------------------------------------------------------- /Resources/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Resources/Preview.png -------------------------------------------------------------------------------- /Resources/Icon-Badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Resources/Icon-Badge.png -------------------------------------------------------------------------------- /Resources/Icon-Plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Resources/Icon-Plain.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/AppIconKit/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/AppIcon.icon/Assets/Icon 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/AppIcon.icon/Assets/Icon 2.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AppIconKit.docc/Resources/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Sources/AppIconKit/AppIconKit.docc/Resources/Logo.png -------------------------------------------------------------------------------- /Sources/AppIconKit/AppIconKit.docc/Resources/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Sources/AppIconKit/AppIconKit.docc/Resources/Preview.png -------------------------------------------------------------------------------- /scripts/tools/StringCatalogKeyBuilder/Sources/StringCatalogKeyBuilder/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Run the CLI tool 4 | StringCatalogParserCommand.main() 5 | -------------------------------------------------------------------------------- /Sources/AppIconKit/Images.xcassets/AppIcon.imageset/Icon-CandyShop-CuppyCake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Sources/AppIconKit/Images.xcassets/AppIcon.imageset/Icon-CandyShop-CuppyCake.png -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Blue.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Blue.appiconset/Icon.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Red.imageset/AppIcon-Red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Red.imageset/AppIcon-Red.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Blue.imageset/AppIcon-Blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Blue.imageset/AppIcon-Blue.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Red.appiconset/AppIcon-Red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Red.appiconset/AppIcon-Red.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Green.imageset/AppIcon-Green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Green.imageset/AppIcon-Green.png -------------------------------------------------------------------------------- /Tests/AppIconKitTests/AppIconKitTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import AppIconKit 3 | 4 | @Test func example() async throws { 5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Yellow.imageset/AppIcon-Yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Yellow.imageset/AppIcon-Yellow.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Green.appiconset/AppIcon-Green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Green.appiconset/AppIcon-Green.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Yellow.appiconset/AppIcon-Yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/AppIconKit/HEAD/Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Yellow.appiconset/AppIcon-Yellow.png -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - identifier_name 3 | - large_tuple 4 | - line_length 5 | - nesting 6 | - todo 7 | - trailing_whitespace 8 | - type_name 9 | - vertical_whitespace 10 | 11 | included: 12 | - Sources 13 | - Tests 14 | -------------------------------------------------------------------------------- /Sources/AppIconKit/Images.xcassets/AppIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-CandyShop-CuppyCake.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Red.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Red.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Blue.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Blue.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Green.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Green.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateIcons/AppIcon-Yellow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Yellow.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // Demo 4 | // 5 | // Created by Daniel Saidi on 2024-11-23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct DemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Blue.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Red.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Red.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Green.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Green.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AlternateAppIcons/AppIcon-Yellow.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-Yellow.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/tools/StringCatalogKeyBuilder/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "66815037dbf8ff7c30c7035965ef4f4e4b236aebc6b8a202f470391b382966af", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser.git", 8 | "state" : { 9 | "revision" : "cdd0ef3755280949551dc26dee5de9ddeda89f54", 10 | "version" : "1.6.2" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppIconKit", 7 | platforms: [ 8 | .iOS(.v17), 9 | .macOS(.v14), 10 | .tvOS(.v17), 11 | .watchOS(.v10), 12 | .visionOS(.v1) 13 | ], 14 | products: [ 15 | .library( 16 | name: "AppIconKit", 17 | targets: ["AppIconKit"] 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "AppIconKit" 23 | ), 24 | .testTarget( 25 | name: "AppIconKitTests", 26 | dependencies: ["AppIconKit"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow tests the project on all platforms. 2 | 3 | # For more information see: 4 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 5 | 6 | name: Test Runner 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | runs-on: macos-latest # macos-15 21 | 22 | steps: 23 | - name: Checkout Code 24 | uses: actions/checkout@v5 25 | 26 | - name: Setup Xcode 27 | uses: maxim-lobanov/setup-xcode@v1 28 | with: 29 | xcode-version: latest-stable # 16.4 30 | 31 | - name: Tests All Platforms 32 | run: bash scripts/test.sh -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds the project for all platforms. 2 | 3 | # For more information see: 4 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 5 | 6 | name: Build Runner 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | runs-on: macos-latest # macos-15 21 | 22 | steps: 23 | - name: Checkout Code 24 | uses: actions/checkout@v5 25 | 26 | - name: Setup Xcode 27 | uses: maxim-lobanov/setup-xcode@v1 28 | with: 29 | xcode-version: latest-stable # 16.4 30 | 31 | - name: Build All Platforms 32 | run: bash scripts/build.sh -------------------------------------------------------------------------------- /scripts/tools/StringCatalogKeyBuilder/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.1.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "StringCatalogKeyBuilder", 6 | platforms: [ 7 | .macOS(.v13), 8 | ], 9 | products: [ 10 | .executable( 11 | name: "l10n-gen", 12 | targets: ["StringCatalogKeyBuilder"] 13 | ), 14 | ], 15 | dependencies: [ 16 | .package( 17 | name: "SwiftPackageScripts", 18 | path: "../../../" 19 | ), 20 | .package( 21 | url: "https://github.com/apple/swift-argument-parser.git", 22 | .upToNextMajor(from: "1.5.0") 23 | ), 24 | ], 25 | targets: [ 26 | .executableTarget( 27 | name: "StringCatalogKeyBuilder", 28 | dependencies: [ 29 | "SwiftPackageScripts", 30 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 31 | ] 32 | ) 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /Demo/Demo/AlternateAppIcon+Demo.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // ContentView.swift 4 | // Demo 5 | // 6 | // Created by Daniel Saidi on 2024-11-23. 7 | // 8 | 9 | import AppIconKit 10 | import SwiftUI 11 | 12 | extension AlternateAppIcon { 13 | 14 | static var blue: Self { 15 | .init( 16 | name: "Blue", 17 | icon: .init(.appIconBlue), 18 | appIconName: "AppIcon-Blue" 19 | ) 20 | } 21 | 22 | static var green: Self { 23 | .init( 24 | name: "Green", 25 | icon: .init(.appIconGreen), 26 | appIconName: "AppIcon-Green" 27 | ) 28 | } 29 | 30 | static var red: Self { 31 | .init( 32 | name: "Red", 33 | icon: .init(.appIconRed), 34 | appIconName: "AppIcon-Red" 35 | ) 36 | } 37 | 38 | static var yellow: Self { 39 | .init( 40 | name: "Yellow", 41 | icon: .init(.appIconYellow), 42 | appIconName: "AppIcon-Yellow" 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Daniel Saidi 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 | -------------------------------------------------------------------------------- /scripts/git-default-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script outputs the default git branch name." 10 | 11 | echo 12 | echo "Usage: $0 [OPTIONS]" 13 | echo " -h, --help Show this help message" 14 | 15 | echo 16 | echo "Examples:" 17 | echo " $0" 18 | echo 19 | } 20 | 21 | # Function to display error message, show usage, and exit 22 | show_error_and_exit() { 23 | echo 24 | local error_message="$1" 25 | echo "Error: $error_message" 26 | show_usage 27 | exit 1 28 | } 29 | 30 | # Parse command line arguments 31 | while [[ $# -gt 0 ]]; do 32 | case $1 in 33 | -h|--help) 34 | show_usage; exit 0 ;; 35 | -*) 36 | show_error_and_exit "Unknown option $1" ;; 37 | *) 38 | show_error_and_exit "Unexpected argument '$1'" ;; 39 | esac 40 | shift 41 | done 42 | 43 | # Get the default git branch name 44 | if ! BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'); then 45 | echo "Failed to get default git branch" 46 | exit 1 47 | fi 48 | 49 | # Output the branch name 50 | echo $BRANCH 51 | -------------------------------------------------------------------------------- /.github/workflows/docc.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds and publishes DocC docs to GitHub Pages. 2 | 3 | # Source: https://maxxfrazer.medium.com/deploying-docc-with-github-actions-218c5ca6cad5 4 | # Sample: https://github.com/AgoraIO-Community/VideoUIKit-iOS/blob/main/.github/workflows/deploy_docs.yml 5 | 6 | name: DocC Runner 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | deploy: 25 | runs-on: macos-latest # macos-15 26 | 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | 31 | steps: 32 | - name: Checkout Code 33 | uses: actions/checkout@v5 34 | 35 | - name: Configure Pages 36 | uses: actions/configure-pages@v4 37 | 38 | - name: Setup Xcode 39 | uses: maxim-lobanov/setup-xcode@v1 40 | with: 41 | xcode-version: latest-stable # 16.4 42 | 43 | - name: Build DocC 44 | run: bash scripts/docc.sh 45 | 46 | - name: Upload DocC Artifact 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: '.build/docs-iOS' 50 | 51 | - name: Deploy to GitHub Pages 52 | uses: actions/deploy-pages@v4 53 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AlternateAppIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlternateAppIcon.swift 3 | // AppIconKit 4 | // 5 | // Created by Daniel Saidi on 2024-11-22. 6 | // Copyright © 2024-2025 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This model represents an alternate app icon. 12 | /// 13 | /// This model can be used to both list and set alternate app icons. 14 | /// 15 | /// The ``icon`` parameter is the `.imageset` to show when displaying the 16 | /// icon, while the ``appIconName`` refers to the `.appiconset` asset that 17 | /// should be set when the icon is selected. A `nil` value should reset the icon. 18 | public struct AlternateAppIcon: Identifiable { 19 | 20 | /// Create an alternate app icon value. 21 | /// 22 | /// - Parameters: 23 | /// - name: The icon name. 24 | /// - icon: The icon asset to display. 25 | /// - appIconName: The name of the `.appiconset` asset, if any. 26 | public init( 27 | name: String, 28 | icon: Image, 29 | appIconName: String? 30 | ) { 31 | self.name = name 32 | self.icon = icon 33 | self.appIconName = appIconName 34 | } 35 | 36 | /// The unique icon ID. 37 | public var id: String { appIconName ?? "app" } 38 | 39 | /// The icon name. 40 | public let name: String 41 | 42 | /// The icon asset to display. 43 | public let icon: Image 44 | 45 | /// The name of the `.appiconset` asset, if any. 46 | public let appIconName: String? 47 | } 48 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo 4 | // 5 | // Created by Daniel Saidi on 2024-11-23. 6 | // 7 | 8 | import AppIconKit 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | 13 | @StateObject 14 | var context = AlternateAppIconContext() 15 | 16 | var collections: [AlternateAppIconCollection] { 17 | [ 18 | .init(name: "Standard", icons: [.init(name: "AppIcon", icon: Image(.appIconBlue), appIconName: nil)]), 19 | .init(name: "Collection 1", icons: [.blue, .green, .red, .yellow]), 20 | .init(name: "Collection 2", icons: [.green, .red, .yellow]), 21 | .init(name: "Collection 3", icons: [.red, .yellow]), 22 | .init(name: "Collection 4", icons: [.yellow]) 23 | ] 24 | } 25 | 26 | var body: some View { 27 | NavigationStack { 28 | AlternateAppIconShelf( 29 | collections: collections, 30 | context: context, 31 | onIconSelected: { print("Selected \($0.name)") } 32 | ) 33 | .scrollContentBackground(.hidden) 34 | .background(Color.primary.opacity(0.05)) 35 | .navigationTitle("AppIconKit") 36 | .toolbar { 37 | ToolbarItem(placement: .confirmationAction) { 38 | Button("Reset") { 39 | context.resetAlternateAppIcon() 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | #Preview { 48 | ContentView() 49 | } 50 | -------------------------------------------------------------------------------- /scripts/package-name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script finds the main package name." 10 | 11 | echo 12 | echo "Usage: $0 [OPTIONS]" 13 | echo " -h, --help Show this help message" 14 | 15 | echo 16 | echo "Examples:" 17 | echo " $0" 18 | echo 19 | } 20 | 21 | # Function to display error message, show usage, and exit 22 | show_error_and_exit() { 23 | echo 24 | local error_message="$1" 25 | echo "Error: $error_message" 26 | show_usage 27 | exit 1 28 | } 29 | 30 | # Parse command line arguments 31 | while [[ $# -gt 0 ]]; do 32 | case $1 in 33 | -h|--help) 34 | show_usage; exit 0 ;; 35 | -*) 36 | show_error_and_exit "Unknown option $1" ;; 37 | *) 38 | show_error_and_exit "Unexpected argument '$1'" ;; 39 | esac 40 | shift 41 | done 42 | 43 | # Check that a Package.swift file exists 44 | if [ ! -f "Package.swift" ]; then 45 | show_error_and_exit "Package.swift not found in current directory" 46 | fi 47 | 48 | # Using grep and sed to extract the package name 49 | # 1. grep finds the line containing "name:" 50 | # 2. sed extracts the text between quotes 51 | if ! package_name=$(grep -m 1 'name:.*"' Package.swift | sed -n 's/.*name:[[:space:]]*"\([^"]*\)".*/\1/p'); then 52 | show_error_and_exit "Could not find package name in Package.swift" 53 | fi 54 | 55 | if [ -z "$package_name" ]; then 56 | show_error_and_exit "Could not find package name in Package.swift" 57 | fi 58 | 59 | # Output the package name 60 | echo "$package_name" 61 | -------------------------------------------------------------------------------- /scripts/tools/StringCatalogKeyBuilder/README.md: -------------------------------------------------------------------------------- 1 | # String Catalog Public Key Builder 2 | 3 | This command-line tool can generate public key wrappers for 4 | a string catalog's internal auto-generated keys. 5 | 6 | 7 | ## Why is this needed? 8 | 9 | When you manually add localized strings to a string catalog, 10 | Xcode will automatically generate keys that you can use to 11 | translate these keys. These keys are however internal, and 12 | can not be accessed from other targets. 13 | 14 | This script will auto-generate public key wrappers for all 15 | internal keys. These public keys can be used from any other 16 | target, and will use the `.module` bundle to ensure that a 17 | string is properly localised from anywhere. 18 | 19 | This script will also parse string namespaces into a nested 20 | string hierarchy, to allow you to group strings together. 21 | 22 | 23 | ## Usage 24 | 25 | Run `swift run l10n-gen --help` for help and usage examples. 26 | 27 | This command can parse a `from` catalog and write keys to a 28 | `to` target file path, or parse any `package` module string 29 | catalog at the package-relative `catalogPath` and write it 30 | to a package-relative `targetPath`. 31 | 32 | 33 | ## Terminal command 34 | 35 | The `/scripts/l10n-gen.script` can be used as a convenience. 36 | 37 | 38 | ## Namespaces 39 | 40 | You can add dots to a key to apply namespaces. For instance, 41 | `Experiments.DebugScreen.Title` would generate: 42 | 43 | ``` 44 | .l10n.experiments.debugScreen.title 45 | ``` 46 | 47 | You can customize the `l10n` root namespace name, to wrap a 48 | key collection in a specific root namespace. This is needed 49 | if you parse many different string catalogs, to avoid that 50 | the generated keys collide. 51 | 52 | Using namespaces also reduces the risk of merge conflicts. 53 | -------------------------------------------------------------------------------- /scripts/version-number.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script returns the latest project version." 10 | 11 | echo 12 | echo "Usage: $0 [OPTIONS]" 13 | echo " -h, --help Show this help message" 14 | 15 | echo 16 | echo "Examples:" 17 | echo " $0" 18 | echo 19 | } 20 | 21 | # Function to display error message, show usage, and exit 22 | show_error_and_exit() { 23 | echo 24 | local error_message="$1" 25 | echo "Error: $error_message" 26 | show_usage 27 | exit 1 28 | } 29 | 30 | # Parse command line arguments 31 | while [[ $# -gt 0 ]]; do 32 | case $1 in 33 | -h|--help) 34 | show_usage; exit 0 ;; 35 | -*) 36 | show_error_and_exit "Unknown option $1" ;; 37 | *) 38 | show_error_and_exit "Unexpected argument '$1'" ;; 39 | esac 40 | shift 41 | done 42 | 43 | # Check if the current directory is a Git repository 44 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 45 | show_error_and_exit "Not a Git repository" 46 | fi 47 | 48 | # Fetch all tags 49 | if ! git fetch --tags > /dev/null 2>&1; then 50 | show_error_and_exit "Failed to fetch tags from remote" 51 | fi 52 | 53 | # Get the latest semver tag 54 | if ! latest_version=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1); then 55 | show_error_and_exit "Failed to retrieve version tags" 56 | fi 57 | 58 | # Check if we found a version tag 59 | if [ -z "$latest_version" ]; then 60 | show_error_and_exit "No semver tags found in this repository" 61 | fi 62 | 63 | # Print the latest version 64 | echo "$latest_version" 65 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AlternateAppIconContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlternateAppIconContext.swift 3 | // AppIconKit 4 | // 5 | // Created by Daniel Saidi on 2024-11-22. 6 | // Copyright © 2024-2025 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | /// This observable context class can be used to managed the alternate app icon. 13 | @MainActor 14 | public class AlternateAppIconContext: ObservableObject { 15 | 16 | /// Create an alternate app icon context instance. 17 | public init() { 18 | #if os(macOS) 19 | guard let alternateAppIconName else { return } 20 | setAlternateAppIconName(alternateAppIconName) 21 | #endif 22 | } 23 | 24 | /// The currently set alternate app icon name. 25 | @AppStorage("com.danielsaidi.appiconkit.alternateappiconname") 26 | public private(set) var alternateAppIconName: String? 27 | } 28 | 29 | public extension AlternateAppIconContext { 30 | 31 | /// Reset the alternate app icon. 32 | func resetAlternateAppIcon() { 33 | guard alternateAppIconName != nil else { return } 34 | setAlternateAppIconName(nil) 35 | } 36 | 37 | /// Set a certain alternate app icon. 38 | func setAlternateAppIcon( 39 | _ icon: AlternateAppIcon 40 | ) { 41 | setAlternateAppIconName(icon.appIconName) 42 | } 43 | 44 | /// Set an alternate app icon with a certain name. 45 | func setAlternateAppIconName( 46 | _ name: String? 47 | ) { 48 | alternateAppIconName = name 49 | #if os(iOS) || os(tvOS) 50 | UIApplication.shared.setAlternateIconName(name) 51 | #elseif os(macOS) 52 | if let name { 53 | NSApplication.shared.applicationIconImage = Bundle.main.image(forResource: name) 54 | } else { 55 | NSApplication.shared.applicationIconImage = nil 56 | } 57 | #endif 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/version-bump.yml: -------------------------------------------------------------------------------- 1 | # This workflow bumps the repository version. 2 | # You can bump it a major, minor, or patch step, or use a custom version. 3 | 4 | # For this to work, you must define these repository secrets: 5 | # - BUILD_CERTIFICATE_BASE64 6 | # - P12_PASSWORD 7 | # - KEYCHAIN_PASSWORD 8 | 9 | # OBS! You must also change FRAMEWORK_NAME below to use your 10 | # framework's name. 11 | 12 | # For more information see: 13 | # https://danielsaidi.com/blog/2025/11/09/building-closed-source-binaries-with-github-actions 14 | 15 | name: Bump Version 16 | 17 | on: 18 | workflow_dispatch: 19 | inputs: 20 | bump_type: 21 | description: 'Version bump' 22 | required: true 23 | type: choice 24 | options: 25 | - patch 26 | - minor 27 | - major 28 | - custom 29 | custom_version: 30 | description: 'Custom version (for "custom")' 31 | required: false 32 | type: string 33 | 34 | permissions: 35 | contents: write 36 | 37 | concurrency: 38 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 39 | cancel-in-progress: true 40 | 41 | jobs: 42 | bump: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | fetch-depth: 0 # Fetch all history and tags 50 | 51 | - name: Configure Git 52 | run: | 53 | git config user.name "github-actions[bot]" 54 | git config user.email "github-actions[bot]@users.noreply.github.com" 55 | 56 | - name: Bump Version 57 | run: | 58 | if [ "${{ inputs.bump_type }}" = "custom" ]; then 59 | if [ -z "${{ inputs.custom_version }}" ]; then 60 | echo "Error: Custom version not provided" 61 | exit 1 62 | fi 63 | ./scripts/version-bump.sh --version "${{ inputs.custom_version }}" 64 | else 65 | ./scripts/version-bump.sh --type "${{ inputs.bump_type }}" 66 | fi 67 | -------------------------------------------------------------------------------- /scripts/chmod-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script runs chmod +x on all .sh files in the current directory." 10 | 11 | echo 12 | echo "Usage: $0 [OPTIONS]" 13 | echo " -h, --help Show this help message" 14 | 15 | echo 16 | echo "Examples:" 17 | echo " $0" 18 | echo 19 | } 20 | 21 | # Function to display error message, show usage, and exit 22 | show_error_and_exit() { 23 | echo 24 | local error_message="$1" 25 | echo "Error: $error_message" 26 | show_usage 27 | exit 1 28 | } 29 | 30 | # Parse command line arguments 31 | while [[ $# -gt 0 ]]; do 32 | case $1 in 33 | -h|--help) 34 | show_usage; exit 0 ;; 35 | -*) 36 | show_error_and_exit "Unknown option $1" ;; 37 | *) 38 | show_error_and_exit "Unexpected argument '$1'" ;; 39 | esac 40 | done 41 | 42 | # Use the script folder to refer to other scripts 43 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 44 | 45 | # Function to make scripts executable 46 | make_executable() { 47 | local script="$1" 48 | local filename=$(basename "$script") 49 | 50 | echo "Making $filename executable..." 51 | if ! chmod +x "$script"; then 52 | echo "Failed to make $filename executable" ; return 1 53 | fi 54 | 55 | echo "Successfully made $filename executable" 56 | } 57 | 58 | # Start script 59 | echo 60 | echo "Making all .sh files in $(basename "$FOLDER") executable..." 61 | 62 | # Find all .sh files in the FOLDER and make them executable 63 | SCRIPT_COUNT=0 64 | while read -r script; do 65 | if ! make_executable "$script"; then 66 | exit 1 67 | fi 68 | ((SCRIPT_COUNT++)) 69 | done < <(find "$FOLDER" -name "*.sh" ! -name "chmod-all.sh" -type f) 70 | 71 | # Complete successfully 72 | if [ $SCRIPT_COUNT -eq 0 ]; then 73 | echo 74 | echo "No .sh files found to make executable" 75 | else 76 | echo 77 | echo "Successfully made $SCRIPT_COUNT script(s) executable!" 78 | fi 79 | 80 | echo 81 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/AppIcon.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "color-space-for-untagged-svg-colors" : "display-p3", 3 | "fill" : { 4 | "linear-gradient" : [ 5 | "display-p3:0.04819,0.15344,0.36938,1.00000", 6 | "display-p3:0.03528,0.10236,0.24500,1.00000" 7 | ], 8 | "orientation" : { 9 | "start" : { 10 | "x" : 0.5, 11 | "y" : 0 12 | }, 13 | "stop" : { 14 | "x" : 0.5, 15 | "y" : 0.7 16 | } 17 | } 18 | }, 19 | "groups" : [ 20 | { 21 | "hidden" : true, 22 | "layers" : [ 23 | 24 | ], 25 | "name" : "Front", 26 | "shadow" : { 27 | "kind" : "neutral", 28 | "opacity" : 0.5 29 | }, 30 | "translucency" : { 31 | "enabled" : true, 32 | "value" : 0.5 33 | } 34 | }, 35 | { 36 | "blur-material" : null, 37 | "hidden" : true, 38 | "layers" : [ 39 | 40 | ], 41 | "name" : "Accent", 42 | "shadow" : { 43 | "kind" : "layer-color", 44 | "opacity" : 0.5 45 | }, 46 | "specular" : true, 47 | "translucency" : { 48 | "enabled" : false, 49 | "value" : 0.5 50 | } 51 | }, 52 | { 53 | "blur-material" : null, 54 | "hidden" : false, 55 | "layers" : [ 56 | { 57 | "hidden" : false, 58 | "image-name" : "Icon 2.png", 59 | "name" : "AppIconKit", 60 | "position" : { 61 | "scale" : 0.8, 62 | "translation-in-points" : [ 63 | 0, 64 | 0 65 | ] 66 | } 67 | } 68 | ], 69 | "name" : "Middle", 70 | "shadow" : { 71 | "kind" : "neutral", 72 | "opacity" : 0.5 73 | }, 74 | "specular" : true, 75 | "translucency" : { 76 | "enabled" : true, 77 | "value" : 0.5 78 | } 79 | }, 80 | { 81 | "layers" : [ 82 | 83 | ], 84 | "name" : "Back", 85 | "shadow" : { 86 | "kind" : "neutral", 87 | "opacity" : 0.5 88 | }, 89 | "translucency" : { 90 | "enabled" : true, 91 | "value" : 0.5 92 | } 93 | } 94 | ], 95 | "supported-platforms" : { 96 | "circles" : [ 97 | "watchOS" 98 | ], 99 | "squares" : "shared" 100 | } 101 | } -------------------------------------------------------------------------------- /scripts/sync-from.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script syncs Swift Package Scripts from a ." 10 | 11 | echo 12 | echo "Usage: $0 " 13 | echo " Required. The full path to a Swift Package Scripts root" 14 | 15 | echo 16 | echo "The script will replace the existing 'scripts' folder." 17 | 18 | echo 19 | echo "Examples:" 20 | echo " $0 ../SwiftPackageScripts" 21 | echo " $0 /path/to/SwiftPackageScripts" 22 | echo 23 | } 24 | 25 | # Function to display error message, show usage, and exit 26 | show_error_and_exit() { 27 | echo 28 | local error_message="$1" 29 | echo "Error: $error_message" 30 | show_usage 31 | exit 1 32 | } 33 | 34 | # Define argument variables 35 | SOURCE="" 36 | 37 | # Parse command line arguments 38 | while [[ $# -gt 0 ]]; do 39 | case $1 in 40 | -h|--help) 41 | show_usage; exit 0 ;; 42 | -*) 43 | show_error_and_exit "Unknown option $1" ;; 44 | *) 45 | if [ -z "$SOURCE" ]; then 46 | SOURCE="$1" 47 | else 48 | show_error_and_exit "Unexpected argument '$1'" 49 | fi 50 | shift 51 | ;; 52 | esac 53 | done 54 | 55 | # Verify SOURCE was provided 56 | if [ -z "$SOURCE" ]; then 57 | echo "" 58 | read -p "Please enter the source folder path: " SOURCE 59 | if [ -z "$SOURCE" ]; then 60 | show_error_and_exit "SOURCE_FOLDER is required" 61 | fi 62 | fi 63 | 64 | # Define variables 65 | FOLDER="scripts/" 66 | SOURCE_FOLDER="$SOURCE/$FOLDER" 67 | 68 | # Verify source folder exists 69 | if [ ! -d "$SOURCE_FOLDER" ]; then 70 | show_error_and_exit "Source folder '$SOURCE_FOLDER' does not exist" 71 | fi 72 | 73 | # Start script 74 | echo 75 | echo "Syncing scripts from $SOURCE_FOLDER..." 76 | 77 | # Remove existing folder 78 | echo "Removing existing scripts folder..." 79 | rm -rf $FOLDER 80 | 81 | # Copy folder 82 | echo "Copying scripts from source..." 83 | if ! cp -r "$SOURCE_FOLDER/" "$FOLDER/"; then 84 | echo "Failed to copy scripts from $SOURCE_FOLDER" 85 | exit 1 86 | fi 87 | 88 | # Complete successfully 89 | echo 90 | echo "Script syncing from $SOURCE_FOLDER completed successfully!" 91 | echo 92 | -------------------------------------------------------------------------------- /scripts/sync-to.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script syncs Swift Package Scripts to a ." 10 | 11 | echo 12 | echo "Usage: $0 " 13 | echo " Required. The full path to a Swift Package root" 14 | 15 | echo 16 | echo "The script will replace any existing 'scripts' folder." 17 | 18 | echo 19 | echo "Examples:" 20 | echo " $0 ../MyPackage" 21 | echo " $0 /path/to/MyPackage" 22 | echo 23 | } 24 | 25 | # Function to display error message, show usage, and exit 26 | show_error_and_exit() { 27 | echo 28 | local error_message="$1" 29 | echo "Error: $error_message" 30 | show_usage 31 | exit 1 32 | } 33 | 34 | # Define argument variables 35 | TARGET="" 36 | 37 | # Parse command line arguments 38 | while [[ $# -gt 0 ]]; do 39 | case $1 in 40 | -h|--help) 41 | show_usage; exit 0 ;; 42 | -*) 43 | show_error_and_exit "Unknown option $1" ;; 44 | *) 45 | if [ -z "$TARGET" ]; then 46 | TARGET="$1" 47 | else 48 | show_error_and_exit "Unexpected argument '$1'" 49 | fi 50 | shift 51 | ;; 52 | esac 53 | done 54 | 55 | # Verify TARGET was provided 56 | if [ -z "$TARGET" ]; then 57 | echo "" 58 | read -p "Please enter the target folder path: " TARGET 59 | if [ -z "$TARGET" ]; then 60 | show_error_and_exit "TARGET_FOLDER is required" 61 | fi 62 | fi 63 | 64 | # Define variables 65 | FOLDER="scripts/" 66 | TARGET_FOLDER="$TARGET/$FOLDER" 67 | 68 | # Verify source folder exists 69 | if [ ! -d "$FOLDER" ]; then 70 | show_error_and_exit "Source folder '$FOLDER' does not exist" 71 | fi 72 | 73 | # Verify target directory exists 74 | if [ ! -d "$TARGET" ]; then 75 | show_error_and_exit "Target directory '$TARGET' does not exist" 76 | fi 77 | 78 | # Start script 79 | echo 80 | echo "Syncing scripts to $TARGET_FOLDER..." 81 | 82 | # Remove existing folder if it exists 83 | if [ -d "$TARGET_FOLDER" ]; then 84 | echo "Removing existing scripts folder in target..." 85 | rm -rf "$TARGET_FOLDER" 86 | fi 87 | 88 | # Copy folder 89 | echo "Copying scripts to target..." 90 | if ! cp -r "$FOLDER" "$TARGET_FOLDER"; then 91 | show_error_and_exit "Failed to copy scripts to $TARGET_FOLDER" 92 | fi 93 | 94 | # Complete successfully 95 | echo 96 | echo "Script syncing to $TARGET_FOLDER completed successfully!" 97 | echo 98 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AppIconKit.docc/AppIconKit.md: -------------------------------------------------------------------------------- 1 | # ``AppIconKit`` 2 | 3 | AppIconKit is a Swift SDK that helps you manage alternate app icons on macOS and iOS. 4 | 5 | 6 | 7 | ## Overview 8 | 9 | ![AppIconKit logotype](Logo.png) 10 | 11 | AppIconKit is a Swift SDK that helps you manage alternate app icons on macOS and iOS. 12 | 13 | 14 | 15 | ## Installation 16 | 17 | AppIconKit can be installed with the Swift Package Manager: 18 | 19 | ``` 20 | https://github.com/danielsaidi/AppIconKit.git 21 | ``` 22 | 23 | 24 | ## Support My Work 25 | 26 | Maintaining my various [open-source tools][OpenSource] takes significant time and effort. You can [become a sponsor][Sponsors] to help me dedicate more time to creating, maintaining, and improving these projects. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed. Thank you for considering! 27 | 28 | 29 | 30 | ## Getting Started 31 | 32 | AppIconKit helps you manage alternate app icons on both macOS and iOS. 33 | 34 | The SDK has a couple of central types: 35 | 36 | * Use ``AlternateAppIcon`` to create alternate icon values for your app. 37 | * Use ``AlternateAppIconContext`` to set and keep track of the current icon. 38 | * Use ``AlternateAppIconCollection`` to group icons into related collections. 39 | * Use ``AlternateAppIconListItem`` when listing an app icon in lits and grids. 40 | * Use ``AlternateAppIconShelf`` to list app icons in a vertical list of horizontal shelves. 41 | 42 | The context will automatically restore the icon on macOS, when a context instance is created. 43 | 44 | > [!IMPORTANT] 45 | > Make sure to enable `Include All App Icon Assets` in the app Info.plist for the app to be able to pick icons. You must add an `.imageset` and an `.appiconset` for each icon, since SwiftUI can't render `.appiconset`s and the OS can't use `.imageset`s as app icon. 46 | 47 | 48 | 49 | ## Repository 50 | 51 | For more information, source code, etc., visit the [project repository](https://github.com/danielsaidi/AppIconKit). 52 | 53 | 54 | 55 | ## License 56 | 57 | AppIconKit is available under the MIT license. 58 | 59 | 60 | 61 | ## Topics 62 | 63 | ### Essentials 64 | 65 | - ``AlternateAppIcon`` 66 | - ``AlternateAppIconCollection`` 67 | - ``AlternateAppIconContext`` 68 | - ``AlternateAppIconListItem`` 69 | - ``AlternateAppIconShelf`` 70 | 71 | 72 | ### Item Shelf 73 | 74 | - ``ItemShelf`` 75 | - ``ItemShelfSection`` 76 | - ``ItemShelfStyle`` 77 | 78 | 79 | 80 | [Email]: mailto:daniel.saidi@gmail.com 81 | [Website]: https://danielsaidi.com 82 | [GitHub]: https://github.com/danielsaidi 83 | [OpenSource]: https://danielsaidi.com/opensource 84 | [Sponsors]: https://github.com/sponsors/danielsaidi 85 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AlternateAppIconCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlternateAppIconCollection.swift 3 | // AppIconKit 4 | // 5 | // Created by Daniel Saidi on 2024-11-22. 6 | // Copyright © 2024-2025 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This collection can be used to group alternate app icons into related groups. 12 | public struct AlternateAppIconCollection { 13 | 14 | /// Create an alternate app icon collection. 15 | /// 16 | /// - Parameters: 17 | /// - name: The name of the section. 18 | /// - icons: The icons in the section. 19 | public init( 20 | name: LocalizedStringKey, 21 | icons: [AlternateAppIcon] 22 | ) { 23 | self.name = name 24 | self.icons = icons 25 | } 26 | 27 | /// Create an icon collection by mapping a set of icon names to icon values. 28 | /// 29 | /// The ``appIconName`` should be the default app icon's asset name. 30 | /// 31 | /// The `iconNames` list should be a list of plain icon names, where each 32 | /// icon name will be prefixed by the `iconNamePrefix` when resolving 33 | /// the name of the `.imageset` asset, and the `appIconNamePrefix` 34 | /// when resolving the name of the `.appiconset` asset. 35 | /// 36 | /// If the resulting icon name equals the default `appIconName`, the icon 37 | /// will reset the alternate app icon when it's selected. 38 | /// 39 | /// - Parameters: 40 | /// - name: The name of the section. 41 | /// - appIconName: The default app icon name. 42 | /// - iconNames: A list of icon names. 43 | /// - imageNamePrefix: A prefix to add to the `.imageset` asset name. 44 | /// - appIconNamePrefix: A prefix to add to the `.appiconset` asset name. 45 | public init( 46 | name: LocalizedStringKey, 47 | appIconName: String, 48 | iconNames: [String], 49 | imageNamePrefix: String = "", 50 | appIconNamePrefix: String = "" 51 | ) { 52 | self.init( 53 | name: name, 54 | icons: iconNames.map { 55 | let name = $0.replacingOccurrences(of: " ", with: "") 56 | let imageName = "\(imageNamePrefix)\(name)" 57 | let isDefaultIcon = imageName == appIconName 58 | let appIconName = isDefaultIcon ? nil : "\(appIconNamePrefix)\(name)" 59 | return .init( 60 | name: imageName, 61 | icon: Image(imageName), 62 | appIconName: appIconName 63 | ) 64 | } 65 | ) 66 | } 67 | 68 | /// The name of the section. 69 | public let name: LocalizedStringKey 70 | 71 | /// The icons in the section. 72 | public let icons: [AlternateAppIcon] 73 | } 74 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | 4 | ## 0.8 5 | 6 | ### 💡 Adjustments 7 | 8 | * The package now uses Swift 6.1. 9 | * The demo app now targets iOS 26. 10 | 11 | 12 | 13 | ## 0.7 14 | 15 | This version removes previously deprecated code and adds DocC documentation to the package. 16 | 17 | 18 | 19 | ## 0.6 20 | 21 | ### ✨ Features 22 | 23 | * The new `ItemShelf` and its related types let you design other shelves like the `AlternateAppIconShelf`. 24 | 25 | ### 💡 Adjustments 26 | 27 | * The `AlternateAppIconCollection` view now uses `ItemShelf` under the hood. 28 | 29 | ### 🚨 Breaking Changes 30 | 31 | * Since `ItemShelfStyle` has no icon style, you must now apply a `.alternateAppIconListItemStyle` to style icons. 32 | 33 | 34 | 35 | ## 0.5 36 | 37 | ### 💡 Adjustments 38 | 39 | * The `AlternateAppIconCollection` can now define a separate app icon prefix, which is needed for macOS. 40 | 41 | ### 🚨 Breaking Changes 42 | 43 | * Due to the required alternate app icon adjustments, this version has some breaking changes. 44 | 45 | 46 | 47 | ## 0.4 48 | 49 | ### 💡 Adjustments 50 | 51 | * All `AlternateAppIcon` child types have been promoted to top-level types, for clarity. 52 | * All `AlternateAppIcon.Collection` is renamed to `AlternateAppIconCollection`. 53 | * All `AlternateAppIcon.Item` is renamed to `AlternateAppIconListItem`. 54 | * All `AlternateAppIconItem.Style` is renamed to `AlternateAppIconListItem.Style`. 55 | * All `AlternateAppIcon.Shelf` is renamed to `AlternateAppIconShelf`. 56 | * All `AlternateAppIcon.ShelfStyle` is renamed to `AlternateAppIconShelf.Style`. 57 | * The `AlternateAppIconShelf` view no longer requires you to specify a selection action. 58 | 59 | 60 | 61 | ## 0.3.1 62 | 63 | ### 💡 Adjustments 64 | 65 | * The `AlternateAppIconContext` now aborts resetting when no icon is set. 66 | 67 | 68 | 69 | ## 0.3 70 | 71 | ### ✨ Features 72 | 73 | * The `AlternateAppIcon.Collection` has a new initializer that lets you set up collections in a template-driven way. 74 | 75 | 76 | 77 | ## 0.2 78 | 79 | ### ✨ Features 80 | 81 | * The `AlternateAppIcon` and its `Item` can now use a `nil` icon name. 82 | 83 | ### 💡 Adjustments 84 | 85 | * The `AlternateAppIconContext`'s `resetAlternateAppIcon()` no longer takes an icon. 86 | 87 | 88 | 89 | ## 0.1.1 90 | 91 | ### ✨ Features 92 | 93 | * The `AlternateAppIconContext` has a new reset function. 94 | * The `AlternateAppIcon.ShelfStyle` has been slightly adjusted. 95 | 96 | 97 | 98 | ## 0.1 99 | 100 | This is the first beta version of AppIconKit. 101 | 102 | ### ✨ Features 103 | 104 | * The `AlternateAppIcon` contains app information. 105 | * The `AlternateAppIcon.Collection` can be used to group icons. 106 | * The `AlternateAppIcon.Item` can render an icon in a list or grid. 107 | * The `AlternateAppIcon.Shelf` is a vertically scrolling picker with horizontal shelves. 108 | * The `AlternateAppIconContext` can be used to select icons and manage selection state. 109 | -------------------------------------------------------------------------------- /scripts/tools/StringCatalogKeyBuilder/Sources/StringCatalogKeyBuilder/StringCatalogParserCommand.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | 4 | import SwiftPackageScripts 5 | 6 | /// This command can be used to parse a string catalog and generate 7 | /// Swift code that allows other targets to access its internal keys. 8 | struct StringCatalogParserCommand: ParsableCommand { 9 | static let configuration = CommandConfiguration( 10 | commandName: "l10n-gen", 11 | abstract: "Generate Swift code for a string catalog in any package.", 12 | usage: """ 13 | swift run l10n-gen --from /path/to/catalog.json --to /path/to/output.swift [--root ] 14 | swift run l10n-gen --package /path/to/package/ --catalog package/relative/catalog/path --target package/relative/file/path [--root ] 15 | """ 16 | ) 17 | 18 | @OptionGroup var packageOptions: PackageOptions 19 | @OptionGroup var pathOptions: PathOptions 20 | 21 | @Option var root: String = "l10n" 22 | 23 | func run() throws { 24 | print("\nGenerating code...\n") 25 | if try packageOptions.tryExecute(withRootNamespace: root) { return } 26 | if try pathOptions.tryExecute(withRootNamespace: root) { return } 27 | fatalError("No matching operation. Aborting.") 28 | } 29 | } 30 | 31 | /// These options are used when using the `--package` argument. 32 | struct PackageOptions: ParsableArguments { 33 | @Option(name: .long, help: "A command-relative path to a Swift Package.") 34 | var package: String? 35 | 36 | @Option(name: .long, help: "A package-relative path to the string catalog.") 37 | var catalog: String? 38 | 39 | @Option(name: .long, help: "A package-relative path to the target output file.") 40 | var target: String? 41 | 42 | func tryExecute( 43 | withRootNamespace root: String 44 | ) throws -> Bool { 45 | guard let package, let catalog, let target else { return false } 46 | let catalogPath = (package + catalog).cleanPath() 47 | let filePath = (package + target).cleanPath() 48 | try generateCode(from: catalogPath, to: filePath, withRootNamespace: root) 49 | return true 50 | } 51 | } 52 | 53 | /// These options are used when using the `--from` and `--to` arguments. 54 | struct PathOptions: ParsableArguments { 55 | @Option(name: .long, help: "A command-relative path to a source string catalog.") 56 | var from: String? 57 | 58 | @Option(name: .long, help: "A command-relative path to a target output file.") 59 | var to: String? 60 | 61 | func tryExecute( 62 | withRootNamespace root: String 63 | ) throws -> Bool { 64 | guard let from, let to else { return false } 65 | try generateCode(from: from, to: to, withRootNamespace: root) 66 | return true 67 | } 68 | } 69 | 70 | extension ParsableArguments { 71 | func generateCode( 72 | from catalogPath: String, 73 | to filePath: String, 74 | withRootNamespace root: String 75 | ) throws { 76 | print("Generating code from \"\(catalogPath)\" to \"\(filePath)\"...\n") 77 | let stringCatalog = try StringCatalog(path: catalogPath) 78 | let code = stringCatalog.generatePublicKeyWrappers(withRootNamespace: root) 79 | try code.write(toFile: filePath, atomically: true, encoding: .utf8) 80 | } 81 | } 82 | 83 | private extension String { 84 | func cleanPath() -> String { 85 | replacingOccurrences(of: "//", with: "/") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /scripts/release-validate-git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script validates the git repository for release." 10 | 11 | echo 12 | echo "Usage: $0 [-b|--branch ]" 13 | echo " -b, --branch Optional. The branch to validate (auto-detects default branch if not specified)" 14 | 15 | echo 16 | echo "This script will:" 17 | echo " * Validate that the script is run within a git repository" 18 | echo " * Validate that the current git branch matches the specified one" 19 | echo " * Validate that the git repository doesn't have any uncommitted changes" 20 | 21 | echo 22 | echo "Examples:" 23 | echo " $0" 24 | echo " $0 -b main" 25 | echo " $0 --branch develop" 26 | echo 27 | } 28 | 29 | # Function to display error message, show usage, and exit 30 | show_usage_error_and_exit() { 31 | echo 32 | local error_message="$1" 33 | echo "Error: $error_message" 34 | show_usage 35 | exit 1 36 | } 37 | 38 | # Function to display error message, and exit 39 | show_error_and_exit() { 40 | echo 41 | local error_message="$1" 42 | echo "Error: $error_message" 43 | echo 44 | exit 1 45 | } 46 | 47 | # Define argument variables 48 | BRANCH="" # Will be set to default after parsing 49 | 50 | # Parse command line arguments 51 | while [[ $# -gt 0 ]]; do 52 | case $1 in 53 | -b|--branch) 54 | shift 55 | if [[ $# -eq 0 || "$1" =~ ^- ]]; then 56 | show_usage_error_and_exit "--branch requires a branch name" 57 | fi 58 | BRANCH="$1" 59 | shift 60 | ;; 61 | -h|--help) 62 | show_usage; exit 0 ;; 63 | *) 64 | show_usage_error_and_exit "Unknown option or argument: $1" ;; 65 | esac 66 | done 67 | 68 | # If no BRANCH was provided, try to get the default branch name 69 | if [ -z "$BRANCH" ]; then 70 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 71 | SCRIPT_DEFAULT_BRANCH="$FOLDER/git-default-branch.sh" 72 | 73 | if [ ! -f "$SCRIPT_DEFAULT_BRANCH" ]; then 74 | show_error_and_exit "Script not found: $SCRIPT_DEFAULT_BRANCH" 75 | fi 76 | 77 | if ! BRANCH=$("$SCRIPT_DEFAULT_BRANCH"); then 78 | show_error_and_exit "Failed to get default branch" 79 | fi 80 | fi 81 | 82 | # Start script 83 | echo 84 | echo "Validating git repository for branch '$BRANCH'..." 85 | 86 | # Check if the current directory is a Git repository 87 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 88 | show_error_and_exit "Not a Git repository" 89 | fi 90 | 91 | # Verify that we're on the correct branch 92 | if ! current_branch=$(git rev-parse --abbrev-ref HEAD); then 93 | show_error_and_exit "Failed to get current branch name" 94 | fi 95 | 96 | if [ "$current_branch" != "$BRANCH" ]; then 97 | show_error_and_exit "Not on the specified branch. Current branch is '$current_branch', expected '$BRANCH'" 98 | fi 99 | 100 | # Check for uncommitted changes 101 | if [ -n "$(git status --porcelain)" ]; then 102 | show_error_and_exit "Git repository is dirty. There are uncommitted changes" 103 | fi 104 | 105 | # Complete successfully 106 | echo 107 | echo "Git repository validated successfully!" 108 | echo 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Project Icon 3 |

4 | 5 |

6 | Version 7 | Swift 6.1 8 | Documentation 9 | MIT License 10 |

11 | 12 | 13 | # AppIconKit 14 | 15 | AppIconKit is a Swift SDK that helps you manage alternate app icons on macOS and iOS. 16 | 17 |

18 | Screenshot 19 |

20 | 21 | 22 | ## Installation 23 | 24 | AppIconKit can be installed with the Swift Package Manager: 25 | 26 | ``` 27 | https://github.com/danielsaidi/AppIconKit.git 28 | ``` 29 | 30 | 31 | ## Getting Started 32 | 33 | AppIconKit helps you manage alternate app icons on both macOS and iOS. 34 | 35 | The SDK has a couple of central types: 36 | 37 | * Use `AlternateAppIcon` to create alternate icon values for your app. 38 | * Use `AlternateAppIconContext` to set and keep track of the current icon. 39 | * Use `AlternateAppIconCollection` to group icons into related collections. 40 | * Use `AlternateAppIconListItem` when listing an app icon in lits and grids. 41 | * Use `AlternateAppIconShelf` to list app icons in a vertical list of horizontal shelves. 42 | 43 | The context will automatically restore the icon on macOS, when a context instance is created. 44 | 45 | > [!IMPORTANT] 46 | > Make sure to enable `Include All App Icon Assets` in the app Info.plist for the app to be able to pick icons. You must add an `.imageset` and an `.appiconset` for each icon, since SwiftUI can't render `.appiconset`s and the OS can't use `.imageset`s as app icon. 47 | 48 | 49 | ## Documentation 50 | 51 | The online [documentation][Documentation] has more information, articles, code examples, etc. 52 | 53 | 54 | ## Demo Application 55 | 56 | The `Demo` folder has a demo app that lets you explore the library and try changing the app icon. 57 | 58 | 59 | ## Support My Work 60 | 61 | Maintaining my various [open-source tools][OpenSource] takes significant time and effort. You can [become a sponsor][Sponsors] to help me dedicate more time to creating, maintaining, and improving these projects. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed. Thank you for considering! 62 | 63 | 64 | ## Contact 65 | 66 | Feel free to reach out if you have questions or want to contribute in any way: 67 | 68 | * Website: [danielsaidi.com][Website] 69 | * E-mail: [daniel.saidi@gmail.com][Email] 70 | * Bluesky: [@danielsaidi@bsky.social][Bluesky] 71 | * Mastodon: [@danielsaidi@mastodon.social][Mastodon] 72 | 73 | 74 | ## License 75 | 76 | AppIconKit is available under the MIT license. See the [LICENSE][License] file for more info. 77 | 78 | 79 | [Email]: mailto:daniel.saidi@gmail.com 80 | [Website]: https://danielsaidi.com 81 | [GitHub]: https://github.com/danielsaidi 82 | [OpenSource]: https://danielsaidi.com/opensource 83 | [Sponsors]: https://github.com/sponsors/danielsaidi 84 | 85 | [Bluesky]: https://bsky.app/profile/danielsaidi.bsky.social 86 | [Mastodon]: https://mastodon.social/@danielsaidi 87 | [Twitter]: https://twitter.com/danielsaidi 88 | 89 | [Documentation]: https://danielsaidi.github.io/AppIconKit 90 | [Getting-Started]: https://danielsaidi.github.io/AppIconKit/documentation/appiconkit/getting-started 91 | [License]: https://github.com/danielsaidi/AppIconKit/blob/master/LICENSE 92 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script builds a for all provided ." 10 | 11 | echo 12 | echo "Usage: $0 [TARGET] [-p|--platforms ...]" 13 | echo " [TARGET] Optional. The target to build (defaults to package name)" 14 | echo " -p, --platforms Optional. List of platforms (default: iOS macOS tvOS watchOS xrOS)" 15 | 16 | echo 17 | echo "Examples:" 18 | echo " $0" 19 | echo " $0 MyTarget" 20 | echo " $0 -p iOS macOS" 21 | echo " $0 MyTarget -p iOS macOS" 22 | echo " $0 MyTarget --platforms iOS macOS tvOS watchOS xrOS" 23 | echo 24 | } 25 | 26 | # Function to display error message, show usage, and exit 27 | show_error_and_exit() { 28 | echo 29 | local error_message="$1" 30 | echo "Error: $error_message" 31 | show_usage 32 | exit 1 33 | } 34 | 35 | # Define argument variables 36 | TARGET="" 37 | PLATFORMS="iOS macOS tvOS watchOS xrOS" # Default platforms 38 | 39 | # Parse command line arguments 40 | while [[ $# -gt 0 ]]; do 41 | case $1 in 42 | -p|--platforms) 43 | shift # Remove --platforms from arguments 44 | PLATFORMS="" # Clear default platforms 45 | 46 | # Collect all platform arguments until we hit another flag or run out of args 47 | while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do 48 | PLATFORMS="$PLATFORMS $1" 49 | shift 50 | done 51 | 52 | # Remove leading space and check if we got any platforms 53 | PLATFORMS=$(echo "$PLATFORMS" | sed 's/^ *//') 54 | if [ -z "$PLATFORMS" ]; then 55 | show_error_and_exit "--platforms requires at least one platform" 56 | fi 57 | ;; 58 | -h|--help) 59 | show_usage; exit 0 ;; 60 | -*) 61 | show_error_and_exit "Unknown option $1" ;; 62 | *) 63 | if [ -z "$TARGET" ]; then 64 | TARGET="$1" 65 | else 66 | show_error_and_exit "Unexpected argument '$1'" 67 | fi 68 | shift 69 | ;; 70 | esac 71 | done 72 | 73 | # If no TARGET was provided, try to get the package name 74 | if [ -z "$TARGET" ]; then 75 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 76 | SCRIPT_PACKAGE_NAME="$FOLDER/package-name.sh" 77 | 78 | if [ ! -f "$SCRIPT_PACKAGE_NAME" ]; then 79 | show_error_and_exit "Script not found: $SCRIPT_PACKAGE_NAME" 80 | fi 81 | 82 | if ! TARGET=$("$SCRIPT_PACKAGE_NAME"); then 83 | show_error_and_exit "Failed to get package name" 84 | fi 85 | fi 86 | 87 | # A function that builds $TARGET for a specific platform 88 | build_platform() { 89 | 90 | # Define a local $PLATFORM variable 91 | local PLATFORM=$1 92 | 93 | # Build $TARGET for the $PLATFORM 94 | echo "Building $TARGET for $PLATFORM..." 95 | if ! xcodebuild -scheme $TARGET -derivedDataPath .build -destination generic/platform=$PLATFORM; then 96 | echo "Failed to build $TARGET for $PLATFORM" ; return 1 97 | fi 98 | 99 | # Complete successfully 100 | echo "Successfully built $TARGET for $PLATFORM" 101 | } 102 | 103 | # Start script 104 | echo 105 | echo "Building $TARGET for [$PLATFORMS]..." 106 | 107 | # Loop through all platforms and call the build function 108 | for PLATFORM in $PLATFORMS; do 109 | if ! build_platform "$PLATFORM"; then 110 | exit 1 111 | fi 112 | done 113 | 114 | # Complete successfully 115 | echo 116 | echo "Building $TARGET completed successfully!" 117 | echo 118 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AlternateAppIconShelf.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlternateAppIconShelves.swift 3 | // AppIconKit 4 | // 5 | // Created by Daniel Saidi on 2024-11-22. 6 | // Copyright © 2024-2025 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This shelf lists icons in horizontally scrolling shelves. 12 | /// 13 | /// You can style the view with ``alternateAppIconShelfStyle(_:)`` and 14 | /// apply a `.buttonStyle` to style the various item buttons. 15 | public struct AlternateAppIconShelf: View { 16 | 17 | /// Create an alternate app icon shelves view. 18 | /// 19 | /// - Parameters: 20 | /// - collections: The collections to display as shelves. 21 | /// - context: The icon context to affect. 22 | /// - onIconSelected: An extra action to trigger when an icon is selected, if any. 23 | public init( 24 | collections: [AlternateAppIconCollection], 25 | context: AlternateAppIconContext, 26 | onIconSelected: @escaping (AlternateAppIcon) -> Void = { _ in } 27 | ) { 28 | self.sections = collections.map { .init(title: $0.name, items: $0.icons) } 29 | self.context = context 30 | self.onIconSelected = onIconSelected 31 | } 32 | 33 | public typealias Section = ItemShelfSection 34 | 35 | private let sections: [Section] 36 | private let onIconSelected: (AlternateAppIcon) -> Void 37 | 38 | @Environment(\.alternateAppIconShelfStyle) 39 | private var style 40 | 41 | @ObservedObject 42 | private var context: AlternateAppIconContext 43 | 44 | public var body: some View { 45 | ItemShelf(sections: sections, itemView: itemView) 46 | } 47 | } 48 | 49 | private extension AlternateAppIconShelf { 50 | 51 | func itemView(for icon: AlternateAppIcon) -> some View { 52 | Button { 53 | selectIcon(icon) 54 | } label: { 55 | AlternateAppIconListItem( 56 | icon: icon, 57 | isSelected: context.alternateAppIconName == icon.appIconName 58 | ) 59 | } 60 | .buttonStyle(.plain) 61 | } 62 | } 63 | 64 | private extension AlternateAppIconShelf { 65 | 66 | func selectIcon(_ icon: AlternateAppIcon) { 67 | withAnimation { 68 | onIconSelected(icon) 69 | context.setAlternateAppIcon(icon) 70 | } 71 | } 72 | } 73 | 74 | public extension AlternateAppIconShelf { 75 | 76 | /// This style can style a ``AlternateAppIconShelf``. 77 | typealias Style = ItemShelfStyle 78 | } 79 | 80 | public extension View { 81 | 82 | /// Apply a ``AlternateAppIconShelf/Style``. 83 | func alternateAppIconShelfStyle( 84 | _ style: AlternateAppIconShelf.Style 85 | ) -> some View { 86 | self.environment(\.alternateAppIconShelfStyle, style) 87 | } 88 | } 89 | 90 | public extension EnvironmentValues { 91 | 92 | /// Apply a ``AlternateAppIconShelf/Style``. 93 | @Entry var alternateAppIconShelfStyle = AlternateAppIconShelf.Style.standard 94 | } 95 | 96 | #Preview { 97 | 98 | struct Preview: View { 99 | 100 | init() { 101 | let icon1 = AlternateAppIcon( 102 | name: "icon1", 103 | icon: .init(.appIcon), 104 | appIconName: nil 105 | ) 106 | let icon2 = AlternateAppIcon( 107 | name: "icon2", 108 | icon: .init(.appIcon), 109 | appIconName: "AppIcon2" 110 | ) 111 | let icon3 = AlternateAppIcon( 112 | name: "icon3", 113 | icon: .init(.appIcon), 114 | appIconName: "AppIcon3" 115 | ) 116 | 117 | self.collections = [ 118 | .init(name: "Section 1", icons: [icon1, icon2, icon3]), 119 | .init(name: "Section 2", icons: [icon1, icon2]), 120 | .init(name: "Section 3", icons: [icon1]), 121 | .init(name: "Section 4", icons: [icon1, icon2]), 122 | .init(name: "Section 5", icons: [icon1, icon2, icon3]) 123 | ] 124 | } 125 | 126 | @StateObject var context = AlternateAppIconContext() 127 | 128 | let collections: [AlternateAppIconCollection] 129 | 130 | var body: some View { 131 | AlternateAppIconShelf( 132 | collections: collections, 133 | context: context 134 | ) { icon in 135 | print(icon.appIconName ?? "Default") 136 | } 137 | } 138 | } 139 | 140 | return Preview() 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/xcframework.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds binaries for a closed-source package. 2 | # The output is an XCFramework container(!) zip a dSYMs zip. 3 | 4 | # For this to work you must define these repository secrets: 5 | # - BUILD_CERTIFICATE_BASE64 6 | # - P12_PASSWORD 7 | # - KEYCHAIN_PASSWORD 8 | 9 | # VERY IMPORTANT! 10 | # The XCFramework zip is a CONTAINER with the actual zip inside! 11 | # You must download and unzip it, then upload the nested zip to a release. 12 | # Uploading the container zip file will make Xcode fail to use the release. 13 | 14 | # For more information see: 15 | # https://danielsaidi.com/blog/2025/11/09/building-closed-source-binaries-with-github-actions 16 | 17 | name: Create Binary Artifacts 18 | 19 | on: 20 | workflow_dispatch: 21 | inputs: 22 | bump_type: 23 | description: 'Version bump' 24 | required: false 25 | type: choice 26 | options: 27 | - none 28 | - patch 29 | - minor 30 | - major 31 | - custom 32 | default: none 33 | custom_version: 34 | description: 'Custom version (for "custom")' 35 | required: false 36 | type: string 37 | 38 | permissions: 39 | contents: write 40 | 41 | concurrency: 42 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 43 | cancel-in-progress: true 44 | 45 | jobs: 46 | build: 47 | runs-on: macos-latest # macos-15 48 | 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 0 # Fetch all history and tags (needed for version bumping) 54 | 55 | - name: Get Package Name 56 | run: | 57 | PACKAGE_NAME=$(./scripts/package-name.sh) 58 | echo "PACKAGE_NAME=$PACKAGE_NAME" >> $GITHUB_ENV 59 | 60 | - name: Install build certificate 61 | env: 62 | BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} 63 | P12_PASSWORD: ${{ secrets.P12_PASSWORD }} 64 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 65 | run: | 66 | # create variables 67 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 68 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 69 | 70 | # import certificate from secrets 71 | echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH 72 | 73 | # create temporary keychain 74 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 75 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 76 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 77 | 78 | # import certificate to keychain 79 | security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 80 | security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 81 | security list-keychain -d user -s $KEYCHAIN_PATH 82 | 83 | - name: Set up Xcode 84 | uses: maxim-lobanov/setup-xcode@v1 85 | with: 86 | xcode-version: latest-stable # 16.4 87 | 88 | - name: Validate git 89 | run: ./scripts/release-validate-git.sh 90 | 91 | - name: Validate project 92 | run: ./scripts/release-validate-package.sh -p iOS --swiftlint 0 93 | 94 | - name: Run framwork script 95 | run: ./scripts/framework.sh -p iOS --dsyms 1 --zip 1 96 | 97 | - name: Upload XCFramework Container 98 | uses: actions/upload-artifact@v4 99 | with: 100 | name: ${{ env.PACKAGE_NAME }}-Container 101 | path: .build/${{ env.PACKAGE_NAME }}.zip 102 | if-no-files-found: error 103 | 104 | - name: Upload dSYMs 105 | uses: actions/upload-artifact@v4 106 | with: 107 | name: ${{ env.PACKAGE_NAME }}-dSYMs 108 | path: .build/dSYMs 109 | if-no-files-found: error 110 | 111 | - name: Configure Git 112 | if: ${{ inputs.bump_type != 'none' }} 113 | run: | 114 | git config user.name "github-actions[bot]" 115 | git config user.email "github-actions[bot]@users.noreply.github.com" 116 | 117 | - name: Bump Version 118 | if: ${{ inputs.bump_type != 'none' }} 119 | run: | 120 | if [ "${{ inputs.bump_type }}" = "custom" ]; then 121 | if [ -z "${{ inputs.custom_version }}" ]; then 122 | echo "Error: Custom version not provided" 123 | exit 1 124 | fi 125 | ./scripts/version-bump.sh --version "${{ inputs.custom_version }}" 126 | else 127 | ./scripts/version-bump.sh --type "${{ inputs.bump_type }}" 128 | fi 129 | -------------------------------------------------------------------------------- /scripts/release-validate-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script validates a for release by running lint and unit tests for all platforms." 10 | 11 | echo 12 | echo "Usage: $0 [TARGET] [-p|--platforms ...]" 13 | echo " [TARGET] Optional. The target to validate (defaults to package name)" 14 | echo " -p, --platforms Optional. List of platforms (default: iOS macOS tvOS watchOS xrOS)" 15 | echo " --swiftlint Optional. Run SwiftLint (1) or skip it (0) (default: 1)" 16 | 17 | echo 18 | echo "This script will:" 19 | echo " * Validate that swiftlint passes" 20 | echo " * Validate that all unit tests pass for all platforms" 21 | 22 | echo 23 | echo "Examples:" 24 | echo " $0" 25 | echo " $0 MyTarget" 26 | echo " $0 -p iOS macOS" 27 | echo " $0 MyTarget -p iOS macOS --swiftlint 0" 28 | echo " $0 MyTarget --platforms iOS macOS tvOS watchOS xrOS" 29 | echo 30 | } 31 | 32 | # Function to display error message, show usage, and exit 33 | show_usage_error_and_exit() { 34 | echo 35 | local error_message="$1" 36 | echo "Error: $error_message" 37 | show_usage 38 | exit 1 39 | } 40 | 41 | # Function to display error message, and exit 42 | show_error_and_exit() { 43 | echo 44 | local error_message="$1" 45 | echo "Error: $error_message" 46 | echo 47 | exit 1 48 | } 49 | 50 | # Define argument variables 51 | TARGET="" 52 | PLATFORMS="iOS macOS tvOS watchOS xrOS" # Default platforms 53 | SWIFTLINT=1 # Default to running SwiftLint 54 | 55 | # Parse command line arguments 56 | while [[ $# -gt 0 ]]; do 57 | case $1 in 58 | -p|--platforms) 59 | shift # Remove --platforms from arguments 60 | PLATFORMS="" # Clear default platforms 61 | 62 | # Collect all platform arguments until we hit another flag or run out of args 63 | while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do 64 | PLATFORMS="$PLATFORMS $1" 65 | shift 66 | done 67 | 68 | # Remove leading space and check if we got any platforms 69 | PLATFORMS=$(echo "$PLATFORMS" | sed 's/^ *//') 70 | if [ -z "$PLATFORMS" ]; then 71 | show_usage_error_and_exit "--platforms requires at least one platform" 72 | fi 73 | ;; 74 | --swiftlint) 75 | shift 76 | if [[ "$1" != "0" && "$1" != "1" ]]; then 77 | show_usage_error_and_exit "--swiftlint requires 0 or 1" 78 | fi 79 | SWIFTLINT="$1" 80 | shift 81 | ;; 82 | -h|--help) 83 | show_usage; exit 0 ;; 84 | -*) 85 | show_usage_error_and_exit "Unknown option $1" ;; 86 | *) 87 | if [ -z "$TARGET" ]; then 88 | TARGET="$1" 89 | else 90 | show_usage_error_and_exit "Unexpected argument '$1'" 91 | fi 92 | shift 93 | ;; 94 | esac 95 | done 96 | 97 | # If no TARGET was provided, try to get package name 98 | if [ -z "$TARGET" ]; then 99 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 100 | SCRIPT_PACKAGE_NAME="$FOLDER/package-name.sh" 101 | 102 | if [ ! -f "$SCRIPT_PACKAGE_NAME" ]; then 103 | show_error_and_exit "Script not found: $SCRIPT_PACKAGE_NAME" 104 | fi 105 | 106 | if ! TARGET=$("$SCRIPT_PACKAGE_NAME"); then 107 | show_error_and_exit "Failed to get package name" 108 | fi 109 | fi 110 | 111 | # Use the script folder to refer to other scripts 112 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 113 | SCRIPT_TEST="$FOLDER/test.sh" 114 | 115 | # A function that runs a certain script and checks for errors 116 | run_script() { 117 | local script="$1" 118 | shift # Remove the first argument (script path) from the argument list 119 | 120 | if [ ! -f "$script" ]; then 121 | show_error_and_exit "Script not found: $script" 122 | fi 123 | 124 | chmod +x "$script" 125 | if ! "$script" "$@"; then 126 | exit 1 127 | fi 128 | } 129 | 130 | # Start script 131 | echo 132 | echo "Validating project for target '$TARGET' with platforms [$PLATFORMS]..." 133 | 134 | # Run SwiftLint 135 | if [ "$SWIFTLINT" = "1" ]; then 136 | echo "Running SwiftLint..." 137 | if ! swiftlint --strict; then 138 | show_error_and_exit "SwiftLint failed" 139 | fi 140 | echo "SwiftLint passed" 141 | else 142 | echo "Skipping SwiftLint (disabled)" 143 | fi 144 | 145 | # Run unit tests 146 | echo "Running unit tests..." 147 | run_script $SCRIPT_TEST $TARGET -p $PLATFORMS 148 | 149 | # Complete successfully 150 | echo 151 | echo "Project successfully validated!" 152 | echo 153 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script tests a for all provided ." 10 | 11 | echo 12 | echo "Usage: $0 [TARGET] [-p|--platforms ...]" 13 | echo " [TARGET] Optional. The target to test (defaults to package name)" 14 | echo " -p, --platforms Optional. List of platforms (default: iOS macOS tvOS watchOS xrOS)" 15 | 16 | echo 17 | echo "Examples:" 18 | echo " $0" 19 | echo " $0 MyTarget" 20 | echo " $0 -p iOS macOS" 21 | echo " $0 MyTarget -p iOS macOS" 22 | echo " $0 MyTarget --platforms iOS macOS tvOS watchOS xrOS" 23 | echo 24 | } 25 | 26 | # Function to display error message, show usage, and exit 27 | show_error_and_exit() { 28 | echo 29 | local error_message="$1" 30 | echo "Error: $error_message" 31 | show_usage 32 | exit 1 33 | } 34 | 35 | # Define argument variables 36 | TARGET="" 37 | PLATFORMS="iOS macOS tvOS watchOS xrOS" # Default platforms 38 | 39 | # Parse command line arguments 40 | while [[ $# -gt 0 ]]; do 41 | case $1 in 42 | -p|--platforms) 43 | shift # Remove --platforms from arguments 44 | PLATFORMS="" # Clear default platforms 45 | 46 | # Collect all platform arguments until we hit another flag or run out of args 47 | while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do 48 | PLATFORMS="$PLATFORMS $1" 49 | shift 50 | done 51 | 52 | # Remove leading space and check if we got any platforms 53 | PLATFORMS=$(echo "$PLATFORMS" | sed 's/^ *//') 54 | if [ -z "$PLATFORMS" ]; then 55 | show_error_and_exit "--platforms requires at least one platform" 56 | fi 57 | ;; 58 | -h|--help) 59 | show_usage; exit 0 ;; 60 | -*) 61 | show_error_and_exit "Unknown option $1" ;; 62 | *) 63 | if [ -z "$TARGET" ]; then 64 | TARGET="$1" 65 | else 66 | show_error_and_exit "Unexpected argument '$1'" 67 | fi 68 | shift 69 | ;; 70 | esac 71 | done 72 | 73 | # If no TARGET was provided, try to get package name 74 | if [ -z "$TARGET" ]; then 75 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 76 | SCRIPT_PACKAGE_NAME="$FOLDER/package-name.sh" 77 | 78 | if [ ! -f "$SCRIPT_PACKAGE_NAME" ]; then 79 | show_error_and_exit "Script not found: $SCRIPT_PACKAGE_NAME" 80 | fi 81 | 82 | if ! TARGET=$("$SCRIPT_PACKAGE_NAME"); then 83 | show_error_and_exit "Failed to get package name" 84 | fi 85 | fi 86 | 87 | # A function that gets the latest simulator for a certain OS 88 | get_latest_simulator() { 89 | local PLATFORM=$1 90 | local SIMULATOR_TYPE 91 | 92 | case $PLATFORM in 93 | "iOS") 94 | SIMULATOR_TYPE="iPhone" 95 | ;; 96 | "tvOS") 97 | SIMULATOR_TYPE="Apple TV" 98 | ;; 99 | "watchOS") 100 | SIMULATOR_TYPE="Apple Watch" 101 | ;; 102 | "xrOS") 103 | SIMULATOR_TYPE="Apple Vision" 104 | ;; 105 | *) 106 | echo "Error: Unsupported platform for simulator '$PLATFORM'" 107 | return 1 108 | ;; 109 | esac 110 | 111 | # Get the latest simulator for the platform 112 | xcrun simctl list devices available | grep "$SIMULATOR_TYPE" | tail -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' 113 | } 114 | 115 | # A function that tests $TARGET for a specific platform 116 | test_platform() { 117 | 118 | # Define a local $PLATFORM variable 119 | local PLATFORM="${1//_/ }" 120 | 121 | # Define the destination, based on the $PLATFORM 122 | case $PLATFORM in 123 | "iOS"|"tvOS"|"watchOS"|"xrOS") 124 | local SIMULATOR_UDID=$(get_latest_simulator "$PLATFORM") 125 | if [ -z "$SIMULATOR_UDID" ]; then 126 | echo "Error: No simulator found for $PLATFORM" 127 | return 1 128 | fi 129 | DESTINATION="id=$SIMULATOR_UDID" 130 | ;; 131 | "macOS") 132 | DESTINATION="platform=macOS" 133 | ;; 134 | *) 135 | echo "Error: Unsupported platform '$PLATFORM'" 136 | return 1 137 | ;; 138 | esac 139 | 140 | # Test $TARGET for the $DESTINATION 141 | echo "Testing $TARGET for $PLATFORM..." 142 | if ! xcodebuild test -scheme $TARGET -derivedDataPath .build -destination "$DESTINATION" -enableCodeCoverage YES; then 143 | echo "Failed to test $TARGET for $PLATFORM" 144 | return 1 145 | fi 146 | 147 | # Complete successfully 148 | echo "Successfully tested $TARGET for $PLATFORM" 149 | } 150 | 151 | # Start script 152 | echo 153 | echo "Testing $TARGET for [$PLATFORMS]..." 154 | 155 | # Loop through all platforms and call the test function 156 | for PLATFORM in $PLATFORMS; do 157 | if ! test_platform "$PLATFORM"; then 158 | exit 1 159 | fi 160 | done 161 | 162 | # Complete successfully 163 | echo 164 | echo "Testing $TARGET completed successfully!" 165 | echo 166 | -------------------------------------------------------------------------------- /Sources/AppIconKit/ItemShelf.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemShelf.swift 3 | // AppIconKit 4 | // 5 | // Created by Daniel Saidi on 2025-01-20. 6 | // Copyright © 2025 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This general shelf can be used to list items in horizontally scrolling shelves. 12 | /// 13 | /// This component is used by ``AlternateAppIconShelf`` and is exposed 14 | /// so you can reuse it if an apps have more shelves that should look the same. 15 | public struct ItemShelf: View { 16 | 17 | public init( 18 | sections: [Section], 19 | itemView: @escaping (ItemType) -> ItemView 20 | ) { 21 | self.sections = sections 22 | self.itemView = itemView 23 | } 24 | 25 | public typealias Section = ItemShelfSection 26 | 27 | private let sections: [Section] 28 | private let itemView: (ItemType) -> ItemView 29 | 30 | @Environment(\.itemShelfStyle) 31 | private var style 32 | 33 | public var body: some View { 34 | ScrollView(.vertical) { 35 | LazyVStack(spacing: style.sectionSpacing) { 36 | ForEach(Array(sections.enumerated()), id: \.offset) { section in 37 | VStack(alignment: .leading, spacing: style.sectionTitleSpacing) { 38 | shelfTitle(for: section.element) 39 | shelf(for: section.element) 40 | } 41 | } 42 | } 43 | .padding(.vertical, style.scrollPadding) 44 | } 45 | .withHiddenScrollContent() 46 | .background(style.backgroundColor) 47 | } 48 | } 49 | 50 | private extension ItemShelf { 51 | 52 | func shelf(for section: Section) -> some View { 53 | ScrollView(.horizontal, showsIndicators: false) { 54 | LazyHStack(spacing: style.itemSpacing) { 55 | ForEach(Array(section.items.enumerated()), id: \.offset) { 56 | itemView($0.element) 57 | } 58 | } 59 | .padding(.horizontal, style.sectionPadding) 60 | } 61 | .scrollClipDisabled() 62 | } 63 | 64 | func shelfTitle(for collection: Section) -> some View { 65 | Text(collection.title) 66 | .font(.footnote) 67 | .textCase(.uppercase) 68 | .foregroundColor(.secondary) 69 | .padding(.horizontal, style.sectionPadding) 70 | .padding(.horizontal, 10) 71 | } 72 | } 73 | 74 | private extension View { 75 | 76 | func withHiddenScrollContent() -> some View { 77 | #if os(tvOS) 78 | self 79 | #else 80 | self.scrollContentBackground(.hidden) 81 | #endif 82 | } 83 | } 84 | 85 | /// This type can be used to define an item shelf section. 86 | public struct ItemShelfSection { 87 | 88 | public init( 89 | title: LocalizedStringKey, 90 | items: [ItemType] 91 | ) { 92 | self.title = title 93 | self.items = items 94 | } 95 | 96 | public let title: LocalizedStringKey 97 | public let items: [ItemType] 98 | } 99 | 100 | /// This style can be used to style ``ItemShelf`` views. 101 | /// 102 | /// You can apply custom style values with the view modifier 103 | /// ``SwiftUICore/View/itemShelfStyle(_:)``. 104 | public struct ItemShelfStyle: Sendable { 105 | 106 | /// Create a custom style. 107 | /// 108 | /// - Parameters: 109 | /// - scrollPadding: The main component padding, by default `20`. 110 | /// - sectionSpacing: The spacing between sections, by default `40`. 111 | /// - sectionTitleSpacing: The spacing between section title and items, by default `10`. 112 | /// - sectionPadding: The horizontal padding of each section, by default `nil`. 113 | /// - itemSpacing: The spacing between items, by default `16`. 114 | /// - backgroundColor: The shelf background color. 115 | public init( 116 | scrollPadding: Double = 20, 117 | sectionSpacing: Double = 40, 118 | sectionTitleSpacing: Double = 10, 119 | sectionPadding: CGFloat? = nil, 120 | itemSpacing: Double = 16, 121 | backgroundColor: Color = .primary.opacity(0.05) 122 | ) { 123 | self.scrollPadding = scrollPadding 124 | self.sectionSpacing = sectionSpacing 125 | self.sectionTitleSpacing = sectionTitleSpacing 126 | self.sectionPadding = sectionPadding 127 | self.itemSpacing = itemSpacing 128 | self.backgroundColor = backgroundColor 129 | } 130 | 131 | public var scrollPadding: Double 132 | public var sectionSpacing: Double 133 | public var sectionTitleSpacing: Double 134 | public var sectionPadding: CGFloat? 135 | public var itemSpacing: Double 136 | public var backgroundColor: Color 137 | } 138 | 139 | public extension ItemShelfStyle { 140 | 141 | /// The standard item shelf style. 142 | static var standard: Self { .init() } 143 | } 144 | 145 | public extension View { 146 | 147 | /// Apply a ``ItemShelfStyle`` to style an ``ItemShelf``. 148 | func itemShelfStyle( 149 | _ style: ItemShelfStyle 150 | ) -> some View { 151 | self.environment(\.itemShelfStyle, style) 152 | } 153 | } 154 | 155 | public extension EnvironmentValues { 156 | 157 | /// Apply a ``ItemShelfStyle``. 158 | @Entry var itemShelfStyle = ItemShelfStyle.standard 159 | } 160 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script creates a new release for the provided ." 10 | 11 | echo 12 | echo "Usage: $0 [TARGET] [-b|--branch ] [-p|--platforms ...]" 13 | echo " [TARGET] Optional. The target to release (defaults to package name)" 14 | echo " -b, --branch Optional. The branch to validate (auto-detects default branch if not specified)" 15 | echo " -p, --platforms Optional. List of platforms (default: iOS macOS tvOS watchOS xrOS)" 16 | 17 | echo 18 | echo "This script will:" 19 | echo " * Call release-validate-git.sh to validate the git repository for release." 20 | echo " * Call release-validate-package.sh to run unit tests, swiftlint, etc." 21 | echo " * Call version-bump.sh if all validation steps above passed" 22 | 23 | echo 24 | echo "Examples:" 25 | echo " $0" 26 | echo " $0 MyTarget" 27 | echo " $0 -b main" 28 | echo " $0 MyTarget -b develop" 29 | echo " $0 MyTarget -b main -p iOS macOS" 30 | echo " $0 MyTarget --branch main --platforms iOS macOS tvOS watchOS xrOS" 31 | echo 32 | } 33 | 34 | echo 35 | 36 | # Function to display error message, show usage, and exit 37 | show_usage_error_and_exit() { 38 | echo 39 | local error_message="$1" 40 | echo "Error: $error_message" 41 | show_usage 42 | exit 1 43 | } 44 | 45 | # Function to display error message, and exit 46 | show_error_and_exit() { 47 | echo 48 | local error_message="$1" 49 | echo "Error: $error_message" 50 | echo 51 | exit 1 52 | } 53 | 54 | # Define argument variables 55 | TARGET="" 56 | BRANCH="" 57 | PLATFORMS="iOS macOS tvOS watchOS xrOS" # Default platforms 58 | 59 | # Parse command line arguments 60 | while [[ $# -gt 0 ]]; do 61 | case $1 in 62 | -b|--branch) 63 | shift 64 | if [[ $# -eq 0 || "$1" =~ ^- ]]; then 65 | show_usage_error_and_exit "--branch requires a branch name" 66 | fi 67 | BRANCH="$1" 68 | shift 69 | ;; 70 | -p|--platforms) 71 | shift # Remove --platforms from arguments 72 | PLATFORMS="" # Clear default platforms 73 | 74 | # Collect all platform arguments until we hit another flag or run out of args 75 | while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do 76 | PLATFORMS="$PLATFORMS $1" 77 | shift 78 | done 79 | 80 | # Remove leading space and check if we got any platforms 81 | PLATFORMS=$(echo "$PLATFORMS" | sed 's/^ *//') 82 | if [ -z "$PLATFORMS" ]; then 83 | show_usage_error_and_exit "--platforms requires at least one platform" 84 | fi 85 | ;; 86 | -h|--help) 87 | show_usage; exit 0 ;; 88 | -*) 89 | show_usage_error_and_exit "Unknown option $1" ;; 90 | *) 91 | if [ -z "$TARGET" ]; then 92 | TARGET="$1" 93 | else 94 | show_usage_error_and_exit "Unexpected argument '$1'" 95 | fi 96 | shift 97 | ;; 98 | esac 99 | done 100 | 101 | # If no TARGET was provided, try to get package name 102 | if [ -z "$TARGET" ]; then 103 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 104 | SCRIPT_PACKAGE_NAME="$FOLDER/package-name.sh" 105 | 106 | if [ ! -f "$SCRIPT_PACKAGE_NAME" ]; then 107 | show_error_and_exit "Script not found: $SCRIPT_PACKAGE_NAME" 108 | fi 109 | 110 | if ! TARGET=$("$SCRIPT_PACKAGE_NAME"); then 111 | show_error_and_exit "Failed to get package name" 112 | fi 113 | fi 114 | 115 | # If no BRANCH was provided, try to get the default branch name 116 | if [ -z "$BRANCH" ]; then 117 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 118 | SCRIPT_DEFAULT_BRANCH="$FOLDER/git-default-branch.sh" 119 | 120 | if [ ! -f "$SCRIPT_DEFAULT_BRANCH" ]; then 121 | show_error_and_exit "Script not found: $SCRIPT_DEFAULT_BRANCH" 122 | fi 123 | 124 | if ! BRANCH=$("$SCRIPT_DEFAULT_BRANCH"); then 125 | show_error_and_exit "Failed to get default branch" 126 | fi 127 | fi 128 | 129 | # Use the script folder to refer to other scripts 130 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 131 | SCRIPT_VALIDATE_GIT="$FOLDER/release-validate-git.sh" 132 | SCRIPT_VALIDATE_PACKAGE="$FOLDER/release-validate-package.sh" 133 | SCRIPT_VERSION_BUMP="$FOLDER/version-bump.sh" 134 | 135 | # A function that runs a certain script and checks for errors 136 | run_script() { 137 | local script="$1" 138 | shift # Remove the first argument (script path) from the argument list 139 | 140 | if [ ! -f "$script" ]; then 141 | show_error_and_exit "Script not found: $script" 142 | fi 143 | 144 | chmod +x "$script" 145 | if ! "$script" "$@"; then 146 | exit 1 147 | fi 148 | } 149 | 150 | # Start script 151 | echo 152 | echo "Creating a new release for $TARGET on the $BRANCH branch with platforms [$PLATFORMS]..." 153 | 154 | # Validate git 155 | run_script "$SCRIPT_VALIDATE_GIT" -b "$BRANCH" 156 | 157 | # Validate project 158 | echo "Validating package..." 159 | run_script "$SCRIPT_VALIDATE_PACKAGE" "$TARGET" -p $PLATFORMS 160 | 161 | # Bump version 162 | echo "Bumping version..." 163 | run_script "$SCRIPT_VERSION_BUMP" 164 | 165 | # Complete successfully 166 | echo 167 | echo "Release created successfully!" 168 | echo 169 | -------------------------------------------------------------------------------- /scripts/version-bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script bumps the current version number and pushes a new tag." 10 | 11 | echo 12 | echo "Usage: $0 [OPTIONS]" 13 | echo " -t, --type TYPE Bump type: patch, minor, or major" 14 | echo " -v, --version VER Set explicit version number" 15 | echo " --no-semver Disable semantic version validation" 16 | echo " -h, --help Show this help message" 17 | 18 | echo 19 | echo "Examples:" 20 | echo " $0 -t patch" 21 | echo " $0 --type minor" 22 | echo " $0 --type major" 23 | echo " $0 -v 2.5.1" 24 | echo " $0 --version 3.0.0" 25 | echo " $0 (prompts for version)" 26 | echo " $0 --no-semver" 27 | echo 28 | } 29 | 30 | # Function to display error message, show usage, and exit 31 | show_error_and_exit() { 32 | echo 33 | local error_message="$1" 34 | echo "Error: $error_message" 35 | show_usage 36 | exit 1 37 | } 38 | 39 | # Function to parse version components 40 | parse_version() { 41 | local version=$1 42 | # Remove 'v' prefix if present 43 | version=${version#v} 44 | 45 | IFS='.' read -r -a parts <<< "$version" 46 | MAJOR="${parts[0]}" 47 | MINOR="${parts[1]}" 48 | PATCH="${parts[2]}" 49 | } 50 | 51 | # Function to bump version 52 | bump_version() { 53 | local bump_type=$1 54 | local current_version=$2 55 | 56 | parse_version "$current_version" 57 | 58 | case $bump_type in 59 | patch) 60 | PATCH=$((PATCH + 1)) 61 | echo "$MAJOR.$MINOR.$PATCH" 62 | ;; 63 | minor) 64 | MINOR=$((MINOR + 1)) 65 | echo "$MAJOR.$MINOR.0" 66 | ;; 67 | major) 68 | MAJOR=$((MAJOR + 1)) 69 | echo "$MAJOR.0.0" 70 | ;; 71 | *) 72 | show_error_and_exit "Invalid bump type: $bump_type. Use patch, minor, or major." 73 | ;; 74 | esac 75 | } 76 | 77 | # Define argument variables 78 | VALIDATE_SEMVER=true 79 | BUMP_TYPE="" 80 | EXPLICIT_VERSION="" 81 | 82 | # Parse command line arguments 83 | while [[ $# -gt 0 ]]; do 84 | case $1 in 85 | -t|--type) 86 | BUMP_TYPE="$2" 87 | shift 2 88 | ;; 89 | -v|--version) 90 | EXPLICIT_VERSION="$2" 91 | shift 2 92 | ;; 93 | --no-semver) 94 | VALIDATE_SEMVER=false 95 | shift 96 | ;; 97 | -h|--help) 98 | show_usage; exit 0 ;; 99 | -*) 100 | show_error_and_exit "Unknown option $1" ;; 101 | *) 102 | show_error_and_exit "Unexpected argument '$1'" ;; 103 | esac 104 | done 105 | 106 | # Check for conflicting options 107 | if [ -n "$BUMP_TYPE" ] && [ -n "$EXPLICIT_VERSION" ]; then 108 | show_error_and_exit "Cannot specify both --type and --version" 109 | fi 110 | 111 | # Use the script folder to refer to other scripts 112 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 113 | SCRIPT_VERSION_NUMBER="$FOLDER/version-number.sh" 114 | 115 | # Check if version script exists 116 | if [ ! -f "$SCRIPT_VERSION_NUMBER" ]; then 117 | show_error_and_exit "version-number.sh script not found at $SCRIPT_VERSION_NUMBER" 118 | fi 119 | 120 | # Function to validate semver format, including optional -rc. suffix 121 | validate_semver() { 122 | if [ "$VALIDATE_SEMVER" = false ]; then 123 | return 0 124 | fi 125 | 126 | if [[ $1 =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then 127 | return 0 128 | else 129 | return 1 130 | fi 131 | } 132 | 133 | # Start script 134 | echo 135 | echo "Bumping version number..." 136 | 137 | # Get the latest version 138 | echo "Getting current version..." 139 | if ! VERSION=$($SCRIPT_VERSION_NUMBER); then 140 | echo "Failed to get the latest version" 141 | exit 1 142 | fi 143 | 144 | # Print the current version 145 | echo "The current version is: $VERSION" 146 | 147 | # Determine new version 148 | if [ -n "$EXPLICIT_VERSION" ]; then 149 | # Explicit version provided 150 | NEW_VERSION="$EXPLICIT_VERSION" 151 | if ! validate_semver "$NEW_VERSION"; then 152 | if [ "$VALIDATE_SEMVER" = true ]; then 153 | show_error_and_exit "Invalid version format: $NEW_VERSION. Use semver format or --no-semver to disable validation." 154 | fi 155 | fi 156 | echo "New version will be: $NEW_VERSION" 157 | elif [ -n "$BUMP_TYPE" ]; then 158 | # Bump type provided, calculate new version 159 | NEW_VERSION=$(bump_version "$BUMP_TYPE" "$VERSION") 160 | echo "New version will be: $NEW_VERSION" 161 | else 162 | # Prompt user for new version 163 | while true; do 164 | echo "" 165 | read -p "Enter the new version number: " NEW_VERSION 166 | 167 | # Validate the version number to ensure that it's a semver version 168 | if validate_semver "$NEW_VERSION"; then 169 | break 170 | else 171 | if [ "$VALIDATE_SEMVER" = true ]; then 172 | echo "Invalid version format. Please use semver format (e.g., 1.2.3, v1.2.3, 1.2.3-rc.1, etc.)." 173 | echo "Use --no-semver to disable validation." 174 | else 175 | break 176 | fi 177 | fi 178 | done 179 | fi 180 | 181 | # Push the current branch and create tag 182 | echo "Pushing current branch..." 183 | if ! git push -u origin HEAD; then 184 | echo "Failed to push current branch" 185 | exit 1 186 | fi 187 | 188 | echo "Creating and pushing tag $NEW_VERSION..." 189 | if ! git tag $NEW_VERSION; then 190 | echo "Failed to create tag $NEW_VERSION" 191 | exit 1 192 | fi 193 | 194 | if ! git push --tags; then 195 | echo "Failed to push tags" 196 | exit 1 197 | fi 198 | 199 | # Complete successfully 200 | echo 201 | echo "Version tag $NEW_VERSION pushed successfully!" 202 | echo 203 | -------------------------------------------------------------------------------- /scripts/l10n-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script generates Swift code from a string catalog file." 10 | 11 | echo 12 | echo "Usage:" 13 | echo " $0 --from --to [--root ]" 14 | echo " $0 --package --catalog --target [--root ]" 15 | 16 | echo 17 | echo "Options:" 18 | echo " --from Command-relative path to a source string catalog" 19 | echo " --to Command-relative path to a target output file" 20 | echo " --package Command-relative path to a Swift Package" 21 | echo " --catalog Package-relative path to the string catalog" 22 | echo " --target Package-relative path to the target output file" 23 | echo " --root The root namespace of the key hierarchy, by default l10n." 24 | echo " -h, --help Show this help message" 25 | 26 | echo 27 | echo "Examples:" 28 | echo " $0 --from Resources/Localizable.xcstrings --to Sources/Generated/L10n.swift" 29 | echo " $0 --package Sources/MyPackage/ --catalog Resources/Localizable.xcstrings --target Generated/L10n.swift --root myPackageName" 30 | 31 | echo 32 | echo "Important:" 33 | echo " This script calls out to the Swift-based CLI tools/StringCatalogKeyBuilder." 34 | echo 35 | } 36 | 37 | # Function to display error message, show usage, and exit 38 | show_error_and_exit() { 39 | echo 40 | local error_message="$1" 41 | echo "Error: $error_message" 42 | show_usage 43 | exit 1 44 | } 45 | 46 | # Function to get absolute path 47 | get_absolute_path() { 48 | local path="$1" 49 | if [[ "$path" = /* ]]; then 50 | # Already absolute 51 | echo "$path" 52 | else 53 | # Make it absolute relative to current directory 54 | echo "$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" 55 | fi 56 | } 57 | 58 | # Define argument variables 59 | FROM="" 60 | TO="" 61 | PACKAGE="" 62 | CATALOG="" 63 | TARGET="" 64 | ROOT="" 65 | 66 | # Parse command line arguments 67 | while [[ $# -gt 0 ]]; do 68 | case $1 in 69 | -h|--help) 70 | show_usage; exit 0 ;; 71 | --from) 72 | FROM="$2"; shift 2 ;; 73 | --to) 74 | TO="$2"; shift 2 ;; 75 | --package) 76 | PACKAGE="$2"; shift 2 ;; 77 | --catalog) 78 | CATALOG="$2"; shift 2 ;; 79 | --target) 80 | TARGET="$2"; shift 2 ;; 81 | --root) 82 | ROOT="$2"; shift 2 ;; 83 | -*) 84 | show_error_and_exit "Unknown option $1" ;; 85 | *) 86 | show_error_and_exit "Unexpected argument '$1'" ;; 87 | esac 88 | done 89 | 90 | # Validate arguments 91 | if [ -n "$FROM" ] || [ -n "$TO" ]; then 92 | 93 | # Using --from/--to mode 94 | if [ -z "$FROM" ]; then 95 | show_error_and_exit "--from is required when using --from/--to mode" 96 | fi 97 | if [ -z "$TO" ]; then 98 | show_error_and_exit "--to is required when using --from/--to mode" 99 | fi 100 | if [ -n "$PACKAGE" ] || [ -n "$CATALOG" ] || [ -n "$TARGET" ]; then 101 | show_error_and_exit "Cannot mix --from/--to with --package/--catalog/--target" 102 | fi 103 | 104 | # Verify source file exists 105 | if [ ! -f "$FROM" ]; then 106 | show_error_and_exit "Source catalog '$FROM' does not exist" 107 | fi 108 | 109 | # Remove target file 110 | if [ -f "$TO" ]; then 111 | rm "$TO" 112 | fi 113 | 114 | # Convert to absolute paths 115 | FROM_ABS=$(get_absolute_path "$FROM") 116 | TO_ABS=$(get_absolute_path "$TO") 117 | 118 | # Build arguments 119 | ARGS="--from \"$FROM_ABS\" --to \"$TO_ABS\"" 120 | 121 | # Add root namespace if specified 122 | if [ -n "$ROOT" ]; then 123 | ARGS="$ARGS --root \"$ROOT\"" 124 | fi 125 | 126 | elif [ -n "$PACKAGE" ] || [ -n "$CATALOG" ] || [ -n "$TARGET" ]; then 127 | # Using --package/--catalog/--target mode 128 | if [ -z "$PACKAGE" ]; then 129 | show_error_and_exit "--package is required when using --package/--catalog/--target mode" 130 | fi 131 | if [ -z "$CATALOG" ]; then 132 | show_error_and_exit "--catalog is required when using --package/--catalog/--target mode" 133 | fi 134 | if [ -z "$TARGET" ]; then 135 | show_error_and_exit "--target is required when using --package/--catalog/--target mode" 136 | fi 137 | 138 | # Verify package directory exists 139 | if [ ! -d "$PACKAGE" ]; then 140 | show_error_and_exit "Package directory '$PACKAGE' does not exist" 141 | fi 142 | 143 | # Remove target file 144 | if [ -f "$PACKAGE/$TARGET" ]; then 145 | rm "$PACKAGE/$TARGET" 146 | fi 147 | 148 | # Convert package to absolute path (catalog and target remain relative to package) 149 | PACKAGE_ABS=$(get_absolute_path "$PACKAGE") 150 | 151 | # Build arguments 152 | ARGS="--package \"$PACKAGE_ABS/\" --catalog \"$CATALOG\" --target \"$TARGET\"" 153 | 154 | # Add root namespace if specified 155 | if [ -n "$ROOT" ]; then 156 | ARGS="$ARGS --root \"$ROOT\"" 157 | fi 158 | 159 | else 160 | show_error_and_exit "Either --from/--to or --package/--catalog/--target must be provided" 161 | fi 162 | 163 | # Define the tool directory 164 | TOOL_DIR="scripts/tools/StringCatalogKeyBuilder" 165 | 166 | # Verify tool directory exists 167 | if [ ! -d "$TOOL_DIR" ]; then 168 | show_error_and_exit "Tool directory '$TOOL_DIR' does not exist" 169 | fi 170 | 171 | # Start script 172 | echo 173 | echo "Generating localization code..." 174 | 175 | # Clean build cache and execute command 176 | echo "Cleaning build cache..." 177 | (cd "$TOOL_DIR" && swift package clean) 178 | 179 | echo "Running: swift run l10n-gen $ARGS" 180 | (cd "$TOOL_DIR" && eval "swift run l10n-gen $ARGS") 181 | 182 | # Complete successfully 183 | echo "Code generation completed successfully!" 184 | echo 185 | -------------------------------------------------------------------------------- /Sources/AppIconKit/AlternateAppIconListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlternateAppIconListItem.swift 3 | // AppIconKit 4 | // 5 | // Created by Daniel Saidi on 2024-11-22. 6 | // Copyright © 2024-2025 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This view can be used to list alternate app icons in a list or grid. 12 | /// 13 | /// You can style the view with ``alternateAppIconListItemStyle(_:)``. 14 | public struct AlternateAppIconListItem: View { 15 | 16 | /// Create an alternate icon list item. 17 | /// 18 | /// - Parameters: 19 | /// - icon: The alternate app icon to display. 20 | /// - isSelected: Whether or not the icon is selected. 21 | public init( 22 | icon: AlternateAppIcon, 23 | isSelected: Bool = false 24 | ) { 25 | self.init( 26 | icon: icon.icon, 27 | iconName: icon.appIconName, 28 | isSelected: isSelected 29 | ) 30 | } 31 | 32 | /// Create an alternate icon list item. 33 | /// 34 | /// - Parameters: 35 | /// - icon: The icon asset. 36 | /// - iconName: The name of the icon, if any. 37 | /// - isSelected: Whether or not the icon is selected. 38 | public init( 39 | icon: Image, 40 | iconName: String?, 41 | isSelected: Bool = false 42 | ) { 43 | self.icon = icon 44 | self.iconName = iconName 45 | self.isSelected = isSelected 46 | } 47 | 48 | private let icon: Image 49 | private let iconName: String? 50 | private let isSelected: Bool 51 | 52 | @Environment(\.alternateAppIconListItemStyle) 53 | private var style 54 | 55 | public var body: some View { 56 | GeometryReader { geo in 57 | icon.resizable() 58 | .aspectRatio(contentMode: .fit) 59 | .cornerRadius(0.225 * geo.size.width) 60 | .shadow( 61 | color: style.iconShadowColor, 62 | radius: 0, 63 | x: 0, 64 | y: style.iconShadowSize) 65 | .overlay(alignment: style.checkmarkAlignment) { 66 | checkmark(for: geo) 67 | } 68 | } 69 | .frame(width: style.iconSize, height: style.iconSize) 70 | } 71 | } 72 | 73 | private extension AlternateAppIconListItem { 74 | 75 | func checkmark(for geo: GeometryProxy) -> some View { 76 | Image(systemName: "checkmark") 77 | .resizable() 78 | .aspectRatio(1, contentMode: .fit) 79 | .frame(width: 0.15 * geo.size.width) 80 | .padding(7) 81 | .foregroundStyle(style.checkmarkForegroundColor) 82 | .background(style.checkmarkBackgroundColor) 83 | .clipShape(.circle) 84 | .font(.body.bold()) 85 | .shadow( 86 | color: style.checkmarkShadowColor, 87 | radius: 0, 88 | x: 0, 89 | y: style.checkmarkShadowSize) 90 | .padding(0.1 * geo.size.width) 91 | .opacity(isSelected ? 1 : 0) 92 | } 93 | } 94 | 95 | public extension AlternateAppIconListItem { 96 | 97 | /// This style can style an ``AlternateAppIconListItem``. 98 | struct Style: Sendable { 99 | 100 | /// Create a custom style. 101 | /// 102 | /// - Parameters: 103 | /// - iconSize: The icon size, by default `125` points. 104 | /// - iconShadowColor: The icon shadow color, by default transparent `.black`. 105 | /// - iconShadowSize: The icon shadow size, by default `1` point. 106 | /// - checkmarkAlignment: The checkmark alignment, by default `.topTrailing`. 107 | /// - checkmarkForegroundColor: The checkmark foreground color, by default `.white`. 108 | /// - checkmarkBackgroundColor: The checkmark background color, by default `.green`. 109 | /// - checkmarkShadowColor: The checkmark shadow color, by default transparent `.black`. 110 | /// - checkmarkShadowSize: The checkmark shadow size, by default `1` point. 111 | public init( 112 | iconSize: Double = 125, 113 | iconShadowColor: Color = .black.opacity(0.4), 114 | iconShadowSize: Double = 1, 115 | checkmarkAlignment: Alignment = .topTrailing, 116 | checkmarkForegroundColor: Color = .white, 117 | checkmarkBackgroundColor: Color = .green, 118 | checkmarkShadowColor: Color = .black.opacity(0.4), 119 | checkmarkShadowSize: Double = 1 120 | ) { 121 | self.iconSize = iconSize 122 | self.iconShadowColor = iconShadowColor 123 | self.iconShadowSize = iconShadowSize 124 | self.checkmarkAlignment = checkmarkAlignment 125 | self.checkmarkForegroundColor = checkmarkForegroundColor 126 | self.checkmarkBackgroundColor = checkmarkBackgroundColor 127 | self.checkmarkShadowColor = checkmarkShadowColor 128 | self.checkmarkShadowSize = checkmarkShadowSize 129 | } 130 | 131 | public var iconSize: Double 132 | public var iconShadowColor: Color 133 | public var iconShadowSize: Double 134 | public var checkmarkAlignment: Alignment 135 | public var checkmarkForegroundColor: Color 136 | public var checkmarkBackgroundColor: Color 137 | public var checkmarkShadowColor: Color 138 | public var checkmarkShadowSize: Double 139 | } 140 | } 141 | 142 | public extension AlternateAppIconListItem.Style { 143 | 144 | static var standard: Self { .init() } 145 | } 146 | 147 | public extension View { 148 | 149 | /// Apply a ``AlternateAppIconListItem/Style``. 150 | func alternateAppIconListItemStyle( 151 | _ style: AlternateAppIconListItem.Style 152 | ) -> some View { 153 | self.environment(\.alternateAppIconListItemStyle, style) 154 | } 155 | } 156 | 157 | public extension EnvironmentValues { 158 | 159 | @Entry var alternateAppIconListItemStyle = AlternateAppIconListItem.Style.standard 160 | } 161 | 162 | #Preview { 163 | 164 | HStack { 165 | 166 | AlternateAppIconListItem( 167 | icon: .init(.appIcon), 168 | iconName: "AppIcon", 169 | isSelected: true 170 | ) 171 | 172 | AlternateAppIconListItem( 173 | icon: .init(.appIcon), 174 | iconName: "AppIcon", 175 | isSelected: false 176 | ) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /scripts/docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Fail if any command in a pipeline fails 7 | set -o pipefail 8 | 9 | # Function to display usage information 10 | show_usage() { 11 | echo 12 | echo "This script builds DocC for a and certain ." 13 | 14 | echo 15 | echo "Usage: $0 [TARGET] [-p|--platforms ...] [--hosting-base-path ]" 16 | echo " [TARGET] Optional. The target to build documentation for (defaults to package name)" 17 | echo " -p, --platforms Optional. List of platforms (default: iOS macOS tvOS watchOS xrOS)" 18 | echo " --hosting-base-path Optional. Base path for static hosting (default: TARGET name, use empty string \"\" for root)" 19 | 20 | echo 21 | echo "The web transformed documentation ends up in .build/docs-." 22 | 23 | echo 24 | echo "Examples:" 25 | echo " $0" 26 | echo " $0 MyTarget" 27 | echo " $0 -p iOS macOS" 28 | echo " $0 MyTarget -p iOS macOS" 29 | echo " $0 MyTarget --platforms iOS macOS tvOS watchOS xrOS" 30 | echo " $0 MyTarget --hosting-base-path \"\"" 31 | echo " $0 MyTarget --hosting-base-path \"custom/path\"" 32 | echo 33 | } 34 | 35 | # Function to display error message, show usage, and exit 36 | show_error_and_exit() { 37 | echo 38 | local error_message="$1" 39 | echo "Error: $error_message" 40 | show_usage 41 | exit 1 42 | } 43 | 44 | # Define argument variables 45 | TARGET="" 46 | PLATFORMS="iOS macOS tvOS watchOS xrOS" # Default platforms 47 | HOSTING_BASE_PATH="" # Will be set to TARGET if not specified 48 | 49 | # Parse command line arguments 50 | while [[ $# -gt 0 ]]; do 51 | case $1 in 52 | -p|--platforms) 53 | shift # Remove --platforms from arguments 54 | PLATFORMS="" # Clear default platforms 55 | 56 | # Collect all platform arguments until we hit another flag or run out of args 57 | while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do 58 | PLATFORMS="$PLATFORMS $1" 59 | shift 60 | done 61 | 62 | # Remove leading space and check if we got any platforms 63 | PLATFORMS=$(echo "$PLATFORMS" | sed 's/^ *//') 64 | if [ -z "$PLATFORMS" ]; then 65 | show_error_and_exit "--platforms requires at least one platform" 66 | fi 67 | ;; 68 | --hosting-base-path) 69 | shift # Remove --hosting-base-path from arguments 70 | if [[ $# -eq 0 ]]; then 71 | show_error_and_exit "--hosting-base-path requires a value (use \"\" for empty path)" 72 | fi 73 | HOSTING_BASE_PATH="$1" 74 | shift 75 | ;; 76 | -h|--help) 77 | show_usage; exit 0 ;; 78 | -*) 79 | show_error_and_exit "Unknown option $1" ;; 80 | *) 81 | if [ -z "$TARGET" ]; then 82 | TARGET="$1" 83 | else 84 | show_error_and_exit "Unexpected argument '$1'" 85 | fi 86 | shift 87 | ;; 88 | esac 89 | done 90 | 91 | # If no TARGET was provided, try to get package name 92 | if [ -z "$TARGET" ]; then 93 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 94 | SCRIPT_PACKAGE_NAME="$FOLDER/package-name.sh" 95 | 96 | if [ ! -f "$SCRIPT_PACKAGE_NAME" ]; then 97 | show_error_and_exit "Script not found: $SCRIPT_PACKAGE_NAME" 98 | fi 99 | 100 | if ! TARGET=$("$SCRIPT_PACKAGE_NAME"); then 101 | show_error_and_exit "Failed to get package name" 102 | fi 103 | fi 104 | 105 | # Set default hosting base path if not specified 106 | if [ -z "$HOSTING_BASE_PATH" ]; then 107 | HOSTING_BASE_PATH="$TARGET" 108 | fi 109 | 110 | # Define target lowercase for redirect script 111 | TARGET_LOWERCASED=$(echo "$TARGET" | tr '[:upper:]' '[:lower:]') 112 | 113 | # Prepare the package for DocC 114 | swift package resolve; 115 | 116 | # A function that builds $TARGET documentation for a specific platform 117 | build_platform() { 118 | 119 | # Define a local $PLATFORM variable 120 | local PLATFORM=$1 121 | 122 | # Define the build folder name, based on the $PLATFORM 123 | case $PLATFORM in 124 | "iOS") 125 | DEBUG_PATH="Debug-iphoneos" 126 | ;; 127 | "macOS") 128 | DEBUG_PATH="Debug" 129 | ;; 130 | "tvOS") 131 | DEBUG_PATH="Debug-appletvos" 132 | ;; 133 | "watchOS") 134 | DEBUG_PATH="Debug-watchos" 135 | ;; 136 | "xrOS") 137 | DEBUG_PATH="Debug-xros" 138 | ;; 139 | *) 140 | echo "Error: Unsupported platform '$PLATFORM'" 141 | return 1 142 | ;; 143 | esac 144 | 145 | # Build $TARGET docs for the $PLATFORM 146 | echo "Building $TARGET docs for $PLATFORM..." 147 | if ! xcodebuild docbuild -scheme $TARGET -derivedDataPath .build/docbuild -destination "generic/platform=$PLATFORM"; then 148 | echo "Failed to build documentation for $PLATFORM" 149 | return 1 150 | fi 151 | 152 | # Transform docs for static hosting with configurable base path 153 | local DOCC_COMMAND="$(xcrun --find docc) process-archive transform-for-static-hosting .build/docbuild/Build/Products/$DEBUG_PATH/$TARGET.doccarchive --output-path .build/docs-$PLATFORM" 154 | 155 | # Add hosting-base-path only if it's not empty 156 | if [ -n "$HOSTING_BASE_PATH" ]; then 157 | DOCC_COMMAND="$DOCC_COMMAND --hosting-base-path \"$HOSTING_BASE_PATH\"" 158 | echo "Using hosting base path: '$HOSTING_BASE_PATH'" 159 | else 160 | echo "Using empty hosting base path (root level)" 161 | fi 162 | 163 | if ! eval "$DOCC_COMMAND"; then 164 | echo "Failed to transform documentation for $PLATFORM" 165 | return 1 166 | fi 167 | 168 | # Inject a root redirect script on the root page 169 | echo "" > .build/docs-$PLATFORM/index.html; 170 | 171 | # Complete successfully 172 | echo "Successfully built $TARGET docs for $PLATFORM" 173 | } 174 | 175 | # Start script 176 | echo 177 | echo "Building $TARGET docs for [$PLATFORMS]..." 178 | if [ -n "$HOSTING_BASE_PATH" ]; then 179 | echo "Hosting base path: '$HOSTING_BASE_PATH'" 180 | else 181 | echo "Hosting base path: (empty - root level)" 182 | fi 183 | 184 | # Loop through all platforms and call the build function 185 | for PLATFORM in $PLATFORMS; do 186 | if ! build_platform "$PLATFORM"; then 187 | exit 1 188 | fi 189 | done 190 | 191 | # Complete successfully 192 | echo 193 | echo "Building $TARGET docs completed successfully!" 194 | echo 195 | -------------------------------------------------------------------------------- /scripts/xcframework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if a command exits with a non-zero status 4 | set -e 5 | 6 | # Function to display usage information 7 | show_usage() { 8 | echo 9 | echo "This script builds an XCFramework for a and certain ." 10 | 11 | echo 12 | echo "Usage: $0 [TARGET] [-p|--platforms ...] [-d|--dSyms <0|1>]" 13 | echo " [TARGET] Optional. The target to build framework for (defaults to package name)" 14 | echo " -p, --platforms Optional. List of platforms (default: iOS macOS tvOS watchOS xrOS)" 15 | echo " -d, --dsyms Optional. Include dSYMs (0 or 1, default: 0)" 16 | echo " -z, --zip Optional. Generate zip files (0 or 1, default: 0)" 17 | 18 | echo 19 | echo "Important: This script doesn't work on packages, only on .xcproj projects that generate a framework." 20 | 21 | echo 22 | echo "Examples:" 23 | echo " $0" 24 | echo " $0 MyTarget" 25 | echo " $0 -p iOS macOS" 26 | echo " $0 MyTarget -p iOS macOS" 27 | echo " $0 MyTarget --platforms iOS macOS tvOS watchOS xrOS" 28 | echo " $0 MyTarget --dsyms 1 --zip 1" 29 | echo " $0 MyTarget -p iOS macOS -d 1 -z 1" 30 | echo 31 | } 32 | 33 | # Function to display error message, show usage, and exit 34 | show_error_and_exit() { 35 | echo 36 | local error_message="$1" 37 | echo "Error: $error_message" 38 | show_usage 39 | exit 1 40 | } 41 | 42 | # Define argument variables 43 | TARGET="" 44 | PLATFORMS="iOS macOS tvOS watchOS xrOS" # Default platforms 45 | INCLUDE_DSYMS=0 # Default to not including dSYMs 46 | GENERATE_ZIPS=0 # Default to not generating zip files 47 | 48 | # Parse command line arguments 49 | while [[ $# -gt 0 ]]; do 50 | case $1 in 51 | -p|--platforms) 52 | shift # Remove --platforms from arguments 53 | PLATFORMS="" # Clear default platforms 54 | 55 | # Collect all platform arguments until we hit another flag or run out of args 56 | while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do 57 | PLATFORMS="$PLATFORMS $1" 58 | shift 59 | done 60 | 61 | # Remove leading space and check if we got any platforms 62 | PLATFORMS=$(echo "$PLATFORMS" | sed 's/^ *//') 63 | if [ -z "$PLATFORMS" ]; then 64 | show_error_and_exit "--platforms requires at least one platform" 65 | fi 66 | ;; 67 | -d|--dsyms) 68 | shift 69 | if [[ "$1" == "0" || "$1" == "1" ]]; then 70 | INCLUDE_DSYMS="$1" 71 | shift 72 | else 73 | show_error_and_exit "--dsyms requires 0 or 1" 74 | fi 75 | ;; 76 | -z|--zip) 77 | shift 78 | if [[ "$1" == "0" || "$1" == "1" ]]; then 79 | GENERATE_ZIPS="$1" 80 | shift 81 | else 82 | show_error_and_exit "--zip requires 0 or 1" 83 | fi 84 | ;; 85 | -h|--help) 86 | show_usage; exit 0 ;; 87 | -*) 88 | show_error_and_exit "Unknown option $1" ;; 89 | *) 90 | if [ -z "$TARGET" ]; then 91 | TARGET="$1" 92 | else 93 | show_error_and_exit "Unexpected argument '$1'" 94 | fi 95 | shift 96 | ;; 97 | esac 98 | done 99 | 100 | # If no TARGET was provided, try to get package name 101 | if [ -z "$TARGET" ]; then 102 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 103 | SCRIPT_PACKAGE_NAME="$FOLDER/package-name.sh" 104 | 105 | if [ ! -f "$SCRIPT_PACKAGE_NAME" ]; then 106 | show_error_and_exit "Script not found: $SCRIPT_PACKAGE_NAME" 107 | fi 108 | 109 | if ! TARGET=$("$SCRIPT_PACKAGE_NAME"); then 110 | show_error_and_exit "Failed to get package name" 111 | fi 112 | fi 113 | 114 | # Define local variables 115 | BUILD_FOLDER=.build 116 | BUILD_FOLDER_ARCHIVES=.build/framework_archives 117 | BUILD_FILE=$BUILD_FOLDER/$TARGET.xcframework 118 | BUILD_ZIP=$BUILD_FOLDER/$TARGET.zip 119 | DSYM_FOLDER=$BUILD_FOLDER/dSYMs 120 | DSYM_ZIP=$BUILD_FOLDER/$TARGET-dSYMs.zip 121 | 122 | # Get absolute path for reliable dSYM referencing 123 | ABSOLUTE_BUILD_PATH="$(pwd)/$BUILD_FOLDER_ARCHIVES" 124 | 125 | # Start script 126 | echo 127 | echo "Building $TARGET XCFramework for [$PLATFORMS]..." 128 | if [ "$INCLUDE_DSYMS" == "1" ]; then 129 | echo "dSYMs will be packaged separately (note: dSYMs cannot be embedded in XCFramework with simulator variants)" 130 | fi 131 | 132 | # Delete old builds 133 | echo "Cleaning old builds..." 134 | rm -rf $BUILD_ZIP 135 | rm -rf $BUILD_FILE 136 | rm -rf $BUILD_FOLDER_ARCHIVES 137 | rm -rf $DSYM_FOLDER 138 | rm -rf $DSYM_ZIP 139 | 140 | # Generate XCArchive files for all platforms 141 | echo "Generating XCArchives..." 142 | 143 | # Initialize the xcframework command 144 | XCFRAMEWORK_CMD="xcodebuild -create-xcframework" 145 | 146 | # Build iOS archives and append to the xcframework command 147 | if [[ " ${PLATFORMS} " =~ " iOS " ]]; then 148 | echo "Building iOS archives..." 149 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=iOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-iOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 150 | echo "Failed to build iOS archive" 151 | exit 1 152 | fi 153 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=iOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-iOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 154 | echo "Failed to build iOS Simulator archive" 155 | exit 1 156 | fi 157 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-iOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 158 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-iOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 159 | fi 160 | 161 | # Build macOS archive and append to the xcframework command 162 | if [[ " ${PLATFORMS} " =~ " macOS " ]]; then 163 | echo "Building macOS archive..." 164 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=macOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-macOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 165 | echo "Failed to build macOS archive" 166 | exit 1 167 | fi 168 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-macOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 169 | fi 170 | 171 | # Build tvOS archives and append to the xcframework command 172 | if [[ " ${PLATFORMS} " =~ " tvOS " ]]; then 173 | echo "Building tvOS archives..." 174 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=tvOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 175 | echo "Failed to build tvOS archive" 176 | exit 1 177 | fi 178 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=tvOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 179 | echo "Failed to build tvOS Simulator archive" 180 | exit 1 181 | fi 182 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 183 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 184 | fi 185 | 186 | # Build watchOS archives and append to the xcframework command 187 | if [[ " ${PLATFORMS} " =~ " watchOS " ]]; then 188 | echo "Building watchOS archives..." 189 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=watchOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 190 | echo "Failed to build watchOS archive" 191 | exit 1 192 | fi 193 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=watchOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 194 | echo "Failed to build watchOS Simulator archive" 195 | exit 1 196 | fi 197 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 198 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 199 | fi 200 | 201 | # Build xrOS archives and append to the xcframework command 202 | if [[ " ${PLATFORMS} " =~ " xrOS " ]]; then 203 | echo "Building xrOS archives..." 204 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=xrOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 205 | echo "Failed to build xrOS archive" 206 | exit 1 207 | fi 208 | if ! xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=xrOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES DEBUG_INFORMATION_FORMAT=dwarf-with-dsym; then 209 | echo "Failed to build xrOS Simulator archive" 210 | exit 1 211 | fi 212 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 213 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 214 | fi 215 | 216 | # Generate XCFramework 217 | echo "Generating XCFramework..." 218 | XCFRAMEWORK_CMD+=" -output $BUILD_FILE" 219 | if ! eval "$XCFRAMEWORK_CMD"; then 220 | echo "Failed to generate XCFramework" 221 | exit 1 222 | fi 223 | 224 | # Generate XCFramework zip and checksum if requested 225 | if [ "$GENERATE_ZIPS" == "1" ]; then 226 | echo "Generating XCFramework zip..." 227 | if ! (cd $BUILD_FOLDER && zip -r $(basename $BUILD_ZIP) $(basename $BUILD_FILE)); then 228 | echo "Failed to generate XCFramework zip" 229 | exit 1 230 | fi 231 | 232 | echo 233 | echo "***** FRAMEWORK CHECKSUM *****" 234 | swift package compute-checksum $BUILD_ZIP 235 | echo "******************************" 236 | fi 237 | 238 | # Package dSYMs separately if requested 239 | if [ "$INCLUDE_DSYMS" == "1" ]; then 240 | echo 241 | echo "Packaging dSYMs separately..." 242 | mkdir -p $DSYM_FOLDER 243 | 244 | # Copy all dSYMs from archives with unique naming 245 | for archive in $BUILD_FOLDER_ARCHIVES/*.xcarchive; do 246 | if [ -d "$archive/dSYMs" ]; then 247 | archive_name=$(basename "$archive" .xcarchive) 248 | for dsym in "$archive/dSYMs"/*.dSYM; do 249 | if [ -e "$dsym" ]; then 250 | dsym_name=$(basename "$dsym") 251 | # Create platform-specific folder to avoid name conflicts 252 | platform_folder="$DSYM_FOLDER/$archive_name" 253 | mkdir -p "$platform_folder" 254 | cp -R "$dsym" "$platform_folder/" 2>/dev/null || true 255 | fi 256 | done 257 | fi 258 | done 259 | 260 | # Create dSYMs zip only if zip generation is enabled 261 | if [ "$GENERATE_ZIPS" == "1" ] && [ -d "$DSYM_FOLDER" ] && [ "$(ls -A $DSYM_FOLDER)" ]; then 262 | echo "Generating dSYMs zip..." 263 | if ! (cd $BUILD_FOLDER && zip -r $(basename $DSYM_ZIP) $(basename $DSYM_FOLDER)); then 264 | echo "Failed to generate dSYMs zip" 265 | exit 1 266 | fi 267 | elif [ "$GENERATE_ZIPS" == "0" ] && [ -d "$DSYM_FOLDER" ] && [ "$(ls -A $DSYM_FOLDER)" ]; then 268 | echo "dSYMs collected but not zipped (--zip not enabled)" 269 | else 270 | echo "Warning: No dSYMs found to package" 271 | fi 272 | fi 273 | 274 | # Complete successfully 275 | echo 276 | echo "$TARGET XCFramework created successfully!" 277 | if [ "$INCLUDE_DSYMS" == "1" ]; then 278 | echo "dSYMs package created successfully!" 279 | fi 280 | echo 281 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A9DBBFC12CF14E73007B48F4 /* AppIconKit in Frameworks */ = {isa = PBXBuildFile; productRef = A9DBBFC02CF14E73007B48F4 /* AppIconKit */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | A9DBBFAE2CF14C79007B48F4 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | /* End PBXFileReference section */ 16 | 17 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 18 | A9DBBFB02CF14C79007B48F4 /* Demo */ = { 19 | isa = PBXFileSystemSynchronizedRootGroup; 20 | path = Demo; 21 | sourceTree = ""; 22 | }; 23 | /* End PBXFileSystemSynchronizedRootGroup section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | A9DBBFAB2CF14C79007B48F4 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | A9DBBFC12CF14E73007B48F4 /* AppIconKit in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | A9DBBFA52CF14C79007B48F4 = { 38 | isa = PBXGroup; 39 | children = ( 40 | A9DBBFB02CF14C79007B48F4 /* Demo */, 41 | A9DBBFAF2CF14C79007B48F4 /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | A9DBBFAF2CF14C79007B48F4 /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | A9DBBFAE2CF14C79007B48F4 /* Demo.app */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | /* End PBXGroup section */ 54 | 55 | /* Begin PBXNativeTarget section */ 56 | A9DBBFAD2CF14C79007B48F4 /* Demo */ = { 57 | isa = PBXNativeTarget; 58 | buildConfigurationList = A9DBBFBC2CF14C7A007B48F4 /* Build configuration list for PBXNativeTarget "Demo" */; 59 | buildPhases = ( 60 | A9DBBFAA2CF14C79007B48F4 /* Sources */, 61 | A9DBBFAB2CF14C79007B48F4 /* Frameworks */, 62 | A9DBBFAC2CF14C79007B48F4 /* Resources */, 63 | ); 64 | buildRules = ( 65 | ); 66 | dependencies = ( 67 | ); 68 | fileSystemSynchronizedGroups = ( 69 | A9DBBFB02CF14C79007B48F4 /* Demo */, 70 | ); 71 | name = Demo; 72 | packageProductDependencies = ( 73 | A9DBBFC02CF14E73007B48F4 /* AppIconKit */, 74 | ); 75 | productName = Demo; 76 | productReference = A9DBBFAE2CF14C79007B48F4 /* Demo.app */; 77 | productType = "com.apple.product-type.application"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | A9DBBFA62CF14C79007B48F4 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | BuildIndependentTargetsInParallel = 1; 86 | LastSwiftUpdateCheck = 1610; 87 | LastUpgradeCheck = 1610; 88 | TargetAttributes = { 89 | A9DBBFAD2CF14C79007B48F4 = { 90 | CreatedOnToolsVersion = 16.1; 91 | }; 92 | }; 93 | }; 94 | buildConfigurationList = A9DBBFA92CF14C79007B48F4 /* Build configuration list for PBXProject "Demo" */; 95 | developmentRegion = en; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | Base, 100 | ); 101 | mainGroup = A9DBBFA52CF14C79007B48F4; 102 | minimizedProjectReferenceProxies = 1; 103 | packageReferences = ( 104 | A9DBBFBF2CF14E73007B48F4 /* XCLocalSwiftPackageReference "../../appiconkit" */, 105 | ); 106 | preferredProjectObjectVersion = 77; 107 | productRefGroup = A9DBBFAF2CF14C79007B48F4 /* Products */; 108 | projectDirPath = ""; 109 | projectRoot = ""; 110 | targets = ( 111 | A9DBBFAD2CF14C79007B48F4 /* Demo */, 112 | ); 113 | }; 114 | /* End PBXProject section */ 115 | 116 | /* Begin PBXResourcesBuildPhase section */ 117 | A9DBBFAC2CF14C79007B48F4 /* Resources */ = { 118 | isa = PBXResourcesBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | /* End PBXResourcesBuildPhase section */ 125 | 126 | /* Begin PBXSourcesBuildPhase section */ 127 | A9DBBFAA2CF14C79007B48F4 /* Sources */ = { 128 | isa = PBXSourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXSourcesBuildPhase section */ 135 | 136 | /* Begin XCBuildConfiguration section */ 137 | A9DBBFBA2CF14C7A007B48F4 /* Debug */ = { 138 | isa = XCBuildConfiguration; 139 | buildSettings = { 140 | ALWAYS_SEARCH_USER_PATHS = NO; 141 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 142 | CLANG_ANALYZER_NONNULL = YES; 143 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 145 | CLANG_ENABLE_MODULES = YES; 146 | CLANG_ENABLE_OBJC_ARC = YES; 147 | CLANG_ENABLE_OBJC_WEAK = YES; 148 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 149 | CLANG_WARN_BOOL_CONVERSION = YES; 150 | CLANG_WARN_COMMA = YES; 151 | CLANG_WARN_CONSTANT_CONVERSION = YES; 152 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 153 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 154 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INFINITE_RECURSION = YES; 158 | CLANG_WARN_INT_CONVERSION = YES; 159 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 161 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 168 | CLANG_WARN_UNREACHABLE_CODE = YES; 169 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 170 | COPY_PHASE_STRIP = NO; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 175 | GCC_C_LANGUAGE_STANDARD = gnu17; 176 | GCC_DYNAMIC_NO_PIC = NO; 177 | GCC_NO_COMMON_BLOCKS = YES; 178 | GCC_OPTIMIZATION_LEVEL = 0; 179 | GCC_PREPROCESSOR_DEFINITIONS = ( 180 | "DEBUG=1", 181 | "$(inherited)", 182 | ); 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; 190 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 191 | MACOSX_DEPLOYMENT_TARGET = 14.6; 192 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 193 | MTL_FAST_MATH = YES; 194 | ONLY_ACTIVE_ARCH = YES; 195 | SDKROOT = iphoneos; 196 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 197 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 198 | }; 199 | name = Debug; 200 | }; 201 | A9DBBFBB2CF14C7A007B48F4 /* Release */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 208 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 228 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 229 | CLANG_WARN_STRICT_PROTOTYPES = YES; 230 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 231 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 232 | CLANG_WARN_UNREACHABLE_CODE = YES; 233 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 234 | COPY_PHASE_STRIP = NO; 235 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 236 | ENABLE_NS_ASSERTIONS = NO; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu17; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 242 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 243 | GCC_WARN_UNDECLARED_SELECTOR = YES; 244 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 245 | GCC_WARN_UNUSED_FUNCTION = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; 248 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 249 | MACOSX_DEPLOYMENT_TARGET = 14.6; 250 | MTL_ENABLE_DEBUG_INFO = NO; 251 | MTL_FAST_MATH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_COMPILATION_MODE = wholemodule; 254 | VALIDATE_PRODUCT = YES; 255 | }; 256 | name = Release; 257 | }; 258 | A9DBBFBD2CF14C7A007B48F4 /* Debug */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 262 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 263 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 264 | CODE_SIGN_STYLE = Automatic; 265 | CURRENT_PROJECT_VERSION = 1; 266 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 267 | DEVELOPMENT_TEAM = PMEDFW438U; 268 | ENABLE_PREVIEWS = YES; 269 | GENERATE_INFOPLIST_FILE = YES; 270 | INFOPLIST_KEY_CFBundleDisplayName = AppIconKit; 271 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 272 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 273 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 274 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 275 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 276 | IPHONEOS_DEPLOYMENT_TARGET = 26.0; 277 | LD_RUNPATH_SEARCH_PATHS = ( 278 | "$(inherited)", 279 | "@executable_path/Frameworks", 280 | ); 281 | MACOSX_DEPLOYMENT_TARGET = 26.0; 282 | MARKETING_VERSION = 1.0; 283 | PRODUCT_BUNDLE_IDENTIFIER = com.danielsaidi.appiconkit.Demo; 284 | PRODUCT_NAME = "$(TARGET_NAME)"; 285 | REGISTER_APP_GROUPS = NO; 286 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 287 | SUPPORTS_MACCATALYST = NO; 288 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 289 | SWIFT_EMIT_LOC_STRINGS = YES; 290 | SWIFT_VERSION = 5.0; 291 | TARGETED_DEVICE_FAMILY = "1,2"; 292 | }; 293 | name = Debug; 294 | }; 295 | A9DBBFBE2CF14C7A007B48F4 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 301 | CODE_SIGN_STYLE = Automatic; 302 | CURRENT_PROJECT_VERSION = 1; 303 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 304 | DEVELOPMENT_TEAM = PMEDFW438U; 305 | ENABLE_PREVIEWS = YES; 306 | GENERATE_INFOPLIST_FILE = YES; 307 | INFOPLIST_KEY_CFBundleDisplayName = AppIconKit; 308 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 309 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 310 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 311 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 312 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 313 | IPHONEOS_DEPLOYMENT_TARGET = 26.0; 314 | LD_RUNPATH_SEARCH_PATHS = ( 315 | "$(inherited)", 316 | "@executable_path/Frameworks", 317 | ); 318 | MACOSX_DEPLOYMENT_TARGET = 26.0; 319 | MARKETING_VERSION = 1.0; 320 | PRODUCT_BUNDLE_IDENTIFIER = com.danielsaidi.appiconkit.Demo; 321 | PRODUCT_NAME = "$(TARGET_NAME)"; 322 | REGISTER_APP_GROUPS = NO; 323 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 324 | SUPPORTS_MACCATALYST = NO; 325 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 326 | SWIFT_EMIT_LOC_STRINGS = YES; 327 | SWIFT_VERSION = 5.0; 328 | TARGETED_DEVICE_FAMILY = "1,2"; 329 | }; 330 | name = Release; 331 | }; 332 | /* End XCBuildConfiguration section */ 333 | 334 | /* Begin XCConfigurationList section */ 335 | A9DBBFA92CF14C79007B48F4 /* Build configuration list for PBXProject "Demo" */ = { 336 | isa = XCConfigurationList; 337 | buildConfigurations = ( 338 | A9DBBFBA2CF14C7A007B48F4 /* Debug */, 339 | A9DBBFBB2CF14C7A007B48F4 /* Release */, 340 | ); 341 | defaultConfigurationIsVisible = 0; 342 | defaultConfigurationName = Release; 343 | }; 344 | A9DBBFBC2CF14C7A007B48F4 /* Build configuration list for PBXNativeTarget "Demo" */ = { 345 | isa = XCConfigurationList; 346 | buildConfigurations = ( 347 | A9DBBFBD2CF14C7A007B48F4 /* Debug */, 348 | A9DBBFBE2CF14C7A007B48F4 /* Release */, 349 | ); 350 | defaultConfigurationIsVisible = 0; 351 | defaultConfigurationName = Release; 352 | }; 353 | /* End XCConfigurationList section */ 354 | 355 | /* Begin XCLocalSwiftPackageReference section */ 356 | A9DBBFBF2CF14E73007B48F4 /* XCLocalSwiftPackageReference "../../appiconkit" */ = { 357 | isa = XCLocalSwiftPackageReference; 358 | relativePath = ../../appiconkit; 359 | }; 360 | /* End XCLocalSwiftPackageReference section */ 361 | 362 | /* Begin XCSwiftPackageProductDependency section */ 363 | A9DBBFC02CF14E73007B48F4 /* AppIconKit */ = { 364 | isa = XCSwiftPackageProductDependency; 365 | productName = AppIconKit; 366 | }; 367 | /* End XCSwiftPackageProductDependency section */ 368 | }; 369 | rootObject = A9DBBFA62CF14C79007B48F4 /* Project object */; 370 | } 371 | --------------------------------------------------------------------------------