├── .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 | 
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
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 |
--------------------------------------------------------------------------------