├── .github
├── FUNDING.yml
└── workflows
│ ├── test.yml
│ ├── build.yml
│ ├── docc.yml
│ ├── version-bump.yml
│ └── xcframework.yml
├── Resources
├── Logo.png
├── Icon-Badge.png
└── Icon-Plain.png
├── .gitignore
├── Demo
├── Demo
│ ├── Resources
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ └── AppIcon.icon
│ │ │ ├── Assets
│ │ │ └── PickerKit.png
│ │ │ └── icon.json
│ ├── Demo.entitlements
│ ├── DemoApp.swift
│ ├── Screens
│ │ ├── ForEachPickerScreen.swift
│ │ ├── FontPickerScreen.swift
│ │ ├── MultiPickerScreen.swift
│ │ ├── CameraScreen.swift
│ │ ├── DocumentScannerScreen.swift
│ │ ├── ImagePickerScreen.swift
│ │ ├── ColorPickerBarScreen.swift
│ │ └── FilePickerScreen.swift
│ └── ContentView.swift
└── Demo.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── Sources
└── PickerKit
│ ├── PickerKit.docc
│ ├── Resources
│ │ ├── Icon.png
│ │ └── Logo.png
│ └── PickerKit.md
│ ├── Fonts
│ ├── CustomFont.swift
│ ├── FontPickerItem.swift
│ ├── FontPickerFont+OpenDyslexic.swift
│ ├── FontPickerFont.swift
│ └── FontPicker.swift
│ ├── General
│ ├── CancellableResult.swift
│ ├── OptionalBinding.swift
│ ├── PickerItem.swift
│ ├── ForEachPicker.swift
│ └── MultiPicker.swift
│ ├── Multiplatform
│ ├── FontRepresentable.swift
│ ├── ColorRepresentable.swift
│ └── ImageRepresentable.swift
│ ├── Images
│ ├── Camera.swift
│ ├── DocumentScanner.swift
│ └── ImagePicker.swift
│ ├── Colors
│ ├── ColorPickerBar+Style.swift
│ └── ColorPickerBar.swift
│ └── Files
│ └── FilePicker.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
├── .swiftlint.yml
├── Tests
└── PickerKitTests
│ └── PickerKitTests.swift
├── Package.resolved
├── Package.swift
├── LICENSE
├── RELEASE_NOTES.md
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [danielsaidi]
2 |
--------------------------------------------------------------------------------
/Resources/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/PickerKit/HEAD/Resources/Logo.png
--------------------------------------------------------------------------------
/Resources/Icon-Badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/PickerKit/HEAD/Resources/Icon-Badge.png
--------------------------------------------------------------------------------
/Resources/Icon-Plain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/PickerKit/HEAD/Resources/Icon-Plain.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # SPM defaults
2 | .DS_Store
3 | /.build
4 | /Packages
5 | .swiftpm/
6 | xcuserdata/
7 | DerivedData/
8 |
9 | **/*.xcuserstate
--------------------------------------------------------------------------------
/Demo/Demo/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Resources/AppIcon.icon/Assets/PickerKit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/PickerKit/HEAD/Demo/Demo/Resources/AppIcon.icon/Assets/PickerKit.png
--------------------------------------------------------------------------------
/Sources/PickerKit/PickerKit.docc/Resources/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/PickerKit/HEAD/Sources/PickerKit/PickerKit.docc/Resources/Icon.png
--------------------------------------------------------------------------------
/Sources/PickerKit/PickerKit.docc/Resources/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielsaidi/PickerKit/HEAD/Sources/PickerKit/PickerKit.docc/Resources/Logo.png
--------------------------------------------------------------------------------
/scripts/tools/StringCatalogKeyBuilder/Sources/StringCatalogKeyBuilder/main.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // Run the CLI tool
4 | StringCatalogParserCommand.main()
5 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - identifier_name
3 | - operator_whitespace
4 | - trailing_whitespace
5 | - vertical_whitespace
6 |
7 | included:
8 | - Sources
9 | - Tests
10 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Demo/Demo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tests/PickerKitTests/PickerKitTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import PickerKit
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/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/DemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoApp.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Fonts/CustomFont.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomFont.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2022-07-11.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import FontKit
10 |
11 | /// This is a package typealias for FontKit.CustomFont.
12 | public typealias CustomFont = FontKit.CustomFont
13 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "fd57f22c58881d8a1312fdb42713abf81c24c73d69a7d6eca80ea61c6edbedc8",
3 | "pins" : [
4 | {
5 | "identity" : "fontkit",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/danielsaidi/FontKit",
8 | "state" : {
9 | "revision" : "e4d2142bc4b73cba019ea7f3928267606fee1431",
10 | "version" : "0.1.4"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "ea5d13db0ba082036b7b295e9e55bbce5f20e2bc5ec02a75219b9ae7ec41623c",
3 | "pins" : [
4 | {
5 | "identity" : "fontkit",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/danielsaidi/FontKit",
8 | "state" : {
9 | "revision" : "1445142b6212a2d9959607132046211268ece90f",
10 | "version" : "0.2.0"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/General/CancellableResult.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CancellableResult.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2020-04-07.
6 | // Copyright © 2020-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// This enum represents a cancellable picker result value.
12 | public enum CancellableResult {
13 |
14 | /// The operation was cancelled.
15 | case cancelled
16 |
17 | /// The operation failed.
18 | case failure(Error)
19 |
20 | /// The operation succeeded.
21 | case success(Value)
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Multiplatform/FontRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontRepresentable.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2022-05-06.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(macOS)
10 | import class AppKit.NSFont
11 |
12 | /// This typealias bridges platform-specific font types.
13 | public typealias FontRepresentable = NSFont
14 | #endif
15 |
16 | #if canImport(UIKit)
17 | import class UIKit.UIFont
18 |
19 | /// This typealias bridges platform-specific font types.
20 | public typealias FontRepresentable = UIFont
21 | #endif
22 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Multiplatform/ColorRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorRepresentable.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2022-05-06.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(macOS)
10 | import class AppKit.NSColor
11 |
12 | /// This typealias bridges platform-specific color types.
13 | public typealias ColorRepresentable = NSColor
14 | #endif
15 |
16 | #if canImport(UIKit)
17 | import class UIKit.UIColor
18 |
19 | /// This typealias bridges platform-specific color types.
20 | public typealias ColorRepresentable = UIColor
21 | #endif
22 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.1
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "PickerKit",
7 | platforms: [
8 | .iOS(.v15),
9 | .tvOS(.v15),
10 | .watchOS(.v8),
11 | .macOS(.v12),
12 | .visionOS(.v1)
13 | ],
14 | products: [
15 | .library(
16 | name: "PickerKit",
17 | targets: ["PickerKit"]
18 | )
19 | ],
20 | dependencies: [
21 | .package(
22 | url: "https://github.com/danielsaidi/FontKit",
23 | .upToNextMajor(from: "0.1.4")
24 | )
25 | ],
26 | targets: [
27 | .target(
28 | name: "PickerKit",
29 | dependencies: ["FontKit"]
30 | ),
31 | .testTarget(
32 | name: "PickerKitTests",
33 | dependencies: ["PickerKit"]
34 | )
35 | ]
36 | )
37 |
--------------------------------------------------------------------------------
/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/Screens/ForEachPickerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEachPickerScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct ForEachPickerScreen: View {
13 |
14 | struct Model: Identifiable, Hashable {
15 | let id: Int
16 | var name: String { "Item \(id + 1)" }
17 | }
18 |
19 | @State private var items = (0..<10).map(Model.init)
20 | @State private var selection: Model?
21 |
22 | var body: some View {
23 | List {
24 | ForEachPicker(selection: $selection, values: items) { item in
25 | PickerItem(
26 | item.name,
27 | isSelected: item == selection
28 | )
29 | }
30 | }
31 | .navigationTitle("ForEach Picker")
32 | }
33 | }
34 |
35 | #Preview {
36 | ForEachPickerScreen()
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Multiplatform/ImageRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageRepresentable.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2022-05-06.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if canImport(UIKit)
10 | import class UIKit.UIImage
11 |
12 | /// This typealias bridges platform-specific image types.
13 | public typealias ImageRepresentable = UIImage
14 | #elseif canImport(AppKit)
15 | import class AppKit.NSImage
16 |
17 | /// This typealias bridges platform-specific image types.
18 | public typealias ImageRepresentable = NSImage
19 | #endif
20 |
21 | import SwiftUI
22 |
23 | public extension Image {
24 |
25 | /// Create an image from a certain ``ImageRepresentable``.
26 | init(image: ImageRepresentable) {
27 | #if canImport(UIKit)
28 | self.init(uiImage: image)
29 | #elseif canImport(AppKit)
30 | self.init(nsImage: image)
31 | #endif
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/PickerKit/General/OptionalBinding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OptionalBinding.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2023-06-15.
6 | // Copyright © 2023-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This lets you use optional bindings with native SwiftUI.
12 | ///
13 | /// To pass in optional bindings to a non-optional parameter, just define a fallback:
14 | ///
15 | /// ```swift
16 | /// @State
17 | /// var myValue: Double?
18 | ///
19 | /// func doSomething(with binding: Binding) { ... }
20 | ///
21 | /// doSomething(with: $myValue ?? 0)
22 | /// ```
23 | @MainActor
24 | public func OptionalBinding(
25 | _ binding: Binding,
26 | _ defaultValue: T
27 | ) -> Binding {
28 | Binding(get: {
29 | binding.wrappedValue ?? defaultValue
30 | }, set: {
31 | binding.wrappedValue = $0
32 | })
33 | }
34 |
35 | @MainActor
36 | public func ?? (left: Binding, right: T) -> Binding {
37 | OptionalBinding(left, right)
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Fonts/FontPickerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPickerItem.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2022-03-17.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This view can be used to pick a ``FontPickerFont``.
12 | ///
13 | /// This view will automatically apply the font and apply a `.tag` modifier for the
14 | /// font value as well, for when you want to use it in a regular picker.
15 | public struct FontPickerItem: View {
16 |
17 | public init(
18 | font: FontPickerFont,
19 | fontSize: CGFloat = 20,
20 | isSelected: Bool
21 | ) {
22 | self.font = font
23 | self.fontSize = fontSize
24 | self.isSelected = isSelected
25 | }
26 |
27 | private let font: FontPickerFont
28 | private let fontSize: CGFloat
29 | private let isSelected: Bool
30 |
31 | public var body: some View {
32 | PickerItem(
33 | font.displayName,
34 | isSelected: isSelected
35 | )
36 | .font(.dynamic(font, size: fontSize))
37 | .lineLimit(1)
38 | .tag(font)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/FontPickerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPickerScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct FontPickerScreen: View {
13 |
14 | @State private var selection: FontPickerFont?
15 |
16 | var body: some View {
17 | List {
18 | Section("Preview") {
19 | Text("The quick brown fox jumps over the lazy dog.")
20 | .font(font)
21 | }
22 |
23 | Section("Custom Fonts") {
24 | FontPicker(
25 | selection: $selection,
26 | fonts: .openDyslexicFonts
27 | )
28 | }
29 | Section("System Fonts") {
30 | FontPicker(selection: $selection)
31 | }
32 | }
33 | .navigationTitle("Font Picker")
34 | }
35 | }
36 |
37 | private extension FontPickerScreen {
38 |
39 | var font: Font {
40 | guard let selection else { return .body }
41 | return .relative(selection, size: 15, relativeTo: .body)
42 | }
43 | }
44 |
45 | #Preview {
46 | FontPickerScreen()
47 | }
48 |
--------------------------------------------------------------------------------
/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 | "blur-material" : null,
22 | "hidden" : false,
23 | "layers" : [
24 | {
25 | "image-name" : "PickerKit.png",
26 | "name" : "PickerKit",
27 | "position" : {
28 | "scale" : 0.8,
29 | "translation-in-points" : [
30 | 2,
31 | 0
32 | ]
33 | }
34 | }
35 | ],
36 | "name" : "Middle",
37 | "shadow" : {
38 | "kind" : "neutral",
39 | "opacity" : 0.5
40 | },
41 | "specular" : true,
42 | "translucency" : {
43 | "enabled" : true,
44 | "value" : 0.5
45 | }
46 | }
47 | ],
48 | "supported-platforms" : {
49 | "circles" : [
50 | "watchOS"
51 | ],
52 | "squares" : "shared"
53 | }
54 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/MultiPickerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiPickerScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct MultiPickerScreen: View {
13 |
14 | struct Model: Identifiable, Hashable {
15 | let id: Int
16 | var name: String { "Item \(id + 1)" }
17 | }
18 |
19 | @State private var items = (0..<5).map(Model.init)
20 | @State private var selection1: [Model] = []
21 | @State private var selection2: [Model] = []
22 | @State private var selection3: [Model] = []
23 |
24 | var body: some View {
25 | List {
26 | listSection(1, selection: $selection1)
27 | listSection(2, selection: $selection2)
28 | listSection(3, selection: $selection3)
29 | }
30 | .navigationTitle("Multi Picker")
31 | }
32 |
33 | func listSection(
34 | _ number: Int,
35 | selection: Binding<[Model]>
36 | ) -> some View {
37 | Section("Section \(number)") {
38 | MultiPicker(
39 | items: items,
40 | selection: selection
41 | ) { item, isSelected in
42 | PickerItem(isSelected: isSelected) {
43 | Text(item.name).tag(item)
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | #Preview {
51 | MultiPickerScreen()
52 | }
53 |
--------------------------------------------------------------------------------
/.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/PickerKit/General/PickerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PickerItem.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2025-07-02.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This view can be used to add a checkmark to a view, based on selection state.
11 | public struct PickerItem: View {
12 |
13 | /// Create a picker item with a non-localized title.
14 | public init(
15 | _ title: String,
16 | isSelected: Bool
17 | ) where Content == Text {
18 | self.init(isSelected: isSelected) {
19 | Text(title)
20 | }
21 | }
22 |
23 | /// Create a picker item with a localized title.
24 | public init(
25 | _ title: LocalizedStringKey,
26 | titleBundle: Bundle = .main,
27 | isSelected: Bool
28 | ) where Content == Text {
29 | self.init(isSelected: isSelected) {
30 | Text(title, bundle: titleBundle)
31 | }
32 | }
33 |
34 | /// Create a picker item with a custom content view.
35 | public init(
36 | isSelected: Bool,
37 | content: @escaping () -> Content
38 | ) {
39 | self.isSelected = isSelected
40 | self.content = content
41 | }
42 |
43 | private let isSelected: Bool
44 | private let content: () -> Content
45 |
46 | public var body: some View {
47 | HStack {
48 | content()
49 | .frame(
50 | maxWidth: .infinity,
51 | alignment: .leading
52 | )
53 | if isSelected {
54 | Image(systemName: "checkmark")
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Demo/Demo/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 | var body: some View {
12 | NavigationStack {
13 | List {
14 | Section("Colors") {
15 | NavigationLink("Color Picker Bar") {
16 | ColorPickerBarScreen()
17 | }
18 | }
19 | Section("Files") {
20 | NavigationLink("File Picker") {
21 | FilePickerScreen()
22 | }
23 | }
24 | Section("Fonts") {
25 | NavigationLink("Font Picker") {
26 | FontPickerScreen()
27 | }
28 | }
29 | Section("Images") {
30 | NavigationLink("Camera") {
31 | CameraScreen()
32 | }
33 | NavigationLink("Document Scanner") {
34 | DocumentScannerScreen()
35 | }
36 | NavigationLink("Image Picker") {
37 | ImagePickerScreen()
38 | }
39 | }
40 | Section("General") {
41 | NavigationLink("ForEach Picker") {
42 | ForEachPickerScreen()
43 | }
44 | NavigationLink("Multi Picker") {
45 | MultiPickerScreen()
46 | }
47 | }
48 | }
49 | .navigationTitle("Picker Kit")
50 | }
51 | }
52 | }
53 |
54 | #Preview {
55 | ContentView()
56 | }
57 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/CameraScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct CameraScreen: View {
13 |
14 | @State private var isPickerPresented = false
15 | @State private var images: [UIImage] = []
16 |
17 | var body: some View {
18 | List {
19 | Section("Images") {
20 | if images.isEmpty {
21 | Text("None")
22 | }
23 | ForEach(Array(images.enumerated()), id: \.offset) { item in
24 | Image(uiImage: item.element)
25 | .resizable()
26 | .aspectRatio(contentMode: .fit)
27 | .frame(maxWidth: 150)
28 | }
29 | .onDelete { index in
30 | images.remove(atOffsets: index)
31 | }
32 | }
33 | }
34 | .safeAreaInset(edge: .bottom) {
35 | Button("Take Photo") {
36 | isPickerPresented = true
37 | }
38 | .buttonStyle(.borderedProminent)
39 | }
40 | .fullScreenCover(isPresented: $isPickerPresented) {
41 | Camera(
42 | isPresented: $isPickerPresented,
43 | action: { result in
44 | switch result {
45 | case .cancelled: break
46 | case .failure: break
47 | case .success(let image): images.append(image)
48 | }
49 | }
50 | )
51 | .ignoresSafeArea()
52 | }
53 | .navigationTitle("Camera")
54 | }
55 | }
56 |
57 | #Preview {
58 | CameraScreen()
59 | }
60 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/DocumentScannerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentScannerScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct DocumentScannerScreen: View {
13 |
14 | @State private var isPickerPresented = false
15 | @State private var images: [UIImage] = []
16 |
17 | var body: some View {
18 | List {
19 | Section("Images") {
20 | if images.isEmpty {
21 | Text("None")
22 | }
23 | ForEach(Array(images.enumerated()), id: \.offset) { item in
24 | Image(uiImage: item.element)
25 | .resizable()
26 | .aspectRatio(contentMode: .fit)
27 | .frame(maxWidth: 150)
28 | }
29 | .onDelete { index in
30 | images.remove(atOffsets: index)
31 | }
32 | }
33 | }
34 | .safeAreaInset(edge: .bottom) {
35 | Button("Scan Documents") {
36 | isPickerPresented = true
37 | }
38 | .buttonStyle(.borderedProminent)
39 | }
40 | .fullScreenCover(isPresented: $isPickerPresented) {
41 | DocumentScanner(
42 | isPresented: $isPickerPresented,
43 | action: { result in
44 | switch result {
45 | case .cancelled: break
46 | case .failure: break
47 | case .success(let scan): images += scan.images
48 | }
49 | }
50 | )
51 | .ignoresSafeArea()
52 | }
53 | .navigationTitle("Document Scanner")
54 | }
55 | }
56 |
57 | #Preview {
58 | DocumentScannerScreen()
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Fonts/FontPickerFont+OpenDyslexic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPickerFont+OpenDyslexic.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2025-07-07.
6 | //
7 | // Courtesy by OpenDyslexic.org
8 | // Please consider donating at https://opendyslexic.org
9 | //
10 |
11 | import SwiftUI
12 |
13 | public extension FontPickerFont {
14 |
15 | /// A regular OpenDyslexic font variant.
16 | static let openDyslexic = FontPickerFont(
17 | from: .openDyslexic
18 | )
19 |
20 | /// A bold OpenDyslexic font variant.
21 | static let openDyslexicBold = FontPickerFont(
22 | from: .openDyslexicBold
23 | )
24 |
25 | /// A bold italic OpenDyslexic font variant.
26 | static let openDyslexicBoldItalic = FontPickerFont(
27 | from: .openDyslexicBoldItalic
28 | )
29 |
30 | /// An italic OpenDyslexic font variant.
31 | static let openDyslexicItalic = FontPickerFont(
32 | from: .openDyslexicItalic
33 | )
34 | }
35 |
36 | public extension FontPickerFont {
37 |
38 | /// A collection of all OpenDyslexic font variants.
39 | static var openDyslexicFonts: [FontPickerFont] { .openDyslexicFonts }
40 | }
41 |
42 | public extension Collection where Element == FontPickerFont {
43 |
44 | /// A collection of all OpenDyslexic font variants.
45 | static var openDyslexicFonts: [Element] {
46 | [
47 | .openDyslexic,
48 | .openDyslexicBold,
49 | .openDyslexicItalic,
50 | .openDyslexicBoldItalic
51 | ]
52 | }
53 | }
54 |
55 | #Preview {
56 |
57 | let size = 15.0
58 |
59 | List {
60 | Text("OpenDyslexic")
61 | .font(.system(size: size))
62 | ForEach(FontPickerFont.openDyslexicFonts) { font in
63 | Text(font.displayName)
64 | .font(.fixed(font, size: size))
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/ImagePickerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImagePickerScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct ImagePickerScreen: View {
13 |
14 | @State private var isPickerPresented = false
15 | @State private var images: [UIImage] = []
16 |
17 | var body: some View {
18 | List {
19 | Section("Images") {
20 | if images.isEmpty {
21 | Text("None")
22 | }
23 | ForEach(Array(images.enumerated()), id: \.offset) { item in
24 | Image(uiImage: item.element)
25 | .resizable()
26 | .aspectRatio(contentMode: .fit)
27 | .frame(maxWidth: 150)
28 | }
29 | .onDelete { index in
30 | images.remove(atOffsets: index)
31 | }
32 | }
33 | }
34 | .safeAreaInset(edge: .bottom) {
35 | Button("Pick Images") {
36 | isPickerPresented = true
37 | }
38 | .buttonStyle(.borderedProminent)
39 | }
40 | .fullScreenCover(isPresented: $isPickerPresented) {
41 | ImagePicker(
42 | sourceType: .photoLibrary,
43 | isPresented: $isPickerPresented,
44 | pickerConfig: { picker in },
45 | action: { result in
46 | switch result {
47 | case .cancelled: break
48 | case .failure: break
49 | case .success(let image): images.append(image)
50 | }
51 | }
52 | )
53 | .ignoresSafeArea()
54 | }
55 | .navigationTitle("Document Scanner")
56 | }
57 | }
58 |
59 | #Preview {
60 | ImagePickerScreen()
61 | }
62 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/ColorPickerBarScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorPickerBarScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import SwiftUI
10 |
11 | struct ColorPickerBarScreen: View {
12 |
13 | @State var axis: Axis = .horizontal
14 | @State var color: Color? = .yellow
15 |
16 | var body: some View {
17 | ZStack(alignment: .trailing) {
18 | color?.ignoresSafeArea()
19 | if axis == .vertical {
20 | colorPickerBar
21 | }
22 | }
23 | .safeAreaInset(edge: .bottom) {
24 | if axis == .horizontal {
25 | colorPickerBar
26 | }
27 | }
28 | .toolbar {
29 | ToolbarItem(placement: .principal) {
30 | Picker("Pick Axis", selection: $axis) {
31 | Text("Horizontal").tag(Axis.horizontal)
32 | Text("Vertical").tag(Axis.vertical)
33 | }
34 | .pickerStyle(.segmented)
35 | }
36 | }
37 | .navigationTitle("Color Picker Bar")
38 | }
39 | }
40 |
41 | private extension ColorPickerBarScreen {
42 |
43 | var colorPickerBar: some View {
44 | ColorPickerBar(
45 | selection: $color,
46 | axis: axis,
47 | colors: .standardColorPickerBarColors(withClearColor: true),
48 | resetValue: .pink,
49 | supportsOpacity: true
50 | )
51 | .padding(axis == .horizontal ? .leading : .horizontal, 10)
52 | .padding(.vertical)
53 | .background(.thinMaterial)
54 | .colorPickerBarStyle(.init(
55 | animation: .bouncy,
56 | spacing: 10,
57 | colorSize: 30,
58 | selectedColorSize: 45,
59 | resetButton: true
60 | ))
61 | }
62 | }
63 |
64 | #Preview {
65 | NavigationStack {
66 | ColorPickerBarScreen()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Demo/Demo/Screens/FilePickerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilePickerScreen.swift
3 | // Demo
4 | //
5 | // Created by Daniel Saidi on 2025-06-10.
6 | //
7 |
8 | import PickerKit
9 | import QuickLook
10 | import SwiftUI
11 |
12 | struct FilePickerScreen: View {
13 |
14 | @State private var quickLookUrl: URL?
15 | @State private var isPickerPresented = false
16 | @State private var urls: [URL] = []
17 |
18 | var body: some View {
19 | List {
20 | Section("Files") {
21 | if urls.isEmpty {
22 | Text("None")
23 | }
24 | ForEach(Array(urls.enumerated()), id: \.offset) { item in
25 | LabeledContent("File \(item.offset + 1)") {
26 | Button {
27 | quickLookUrl = item.element
28 | } label: {
29 | Label("Preview", systemImage: "eye")
30 | .labelStyle(.iconOnly)
31 | }
32 | }
33 | }
34 | }
35 | }
36 | .safeAreaInset(edge: .bottom) {
37 | Button("Pick Files") {
38 | isPickerPresented = true
39 | }
40 | .buttonStyle(.borderedProminent)
41 | }
42 | .fullScreenCover(isPresented: $isPickerPresented) {
43 | FilePicker(
44 | documentTypes: [.content],
45 | isPresented: $isPickerPresented,
46 | pickerConfig: { picker in
47 | picker.allowsMultipleSelection = true
48 | },
49 | action: { result in
50 | switch result {
51 | case .cancelled: break
52 | case .failure: break
53 | case .success(let urls): self.urls = urls
54 | }
55 | }
56 | )
57 | .ignoresSafeArea()
58 | }
59 | .quickLookPreview($quickLookUrl)
60 | .navigationTitle("File Picker")
61 | }
62 | }
63 |
64 | #Preview {
65 | FilePickerScreen()
66 | }
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 |
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 | [PickerKit](https://github.com/danielsaidi/PickerKit) will use semver after 1.0.
4 |
5 | Until then, breaking changes can happen in minor versions.
6 |
7 |
8 |
9 | ## 0.5
10 |
11 | This version makes the SDK use Swift 6.1 and bumps the demo to Xcode 26.
12 |
13 |
14 |
15 | ## 0.4.2
16 |
17 | ### 💡 Adjustments
18 |
19 | * `FontPickerFont` now conforms to `CustomFontRepresentable`.
20 |
21 |
22 |
23 | ## 0.4.1
24 |
25 | ### 💡 Adjustments
26 |
27 | * `FontPickerFont` now conforms to `Codable`.
28 |
29 |
30 |
31 | ## 0.4
32 |
33 | ### ✨ Features
34 |
35 | * `Font` has new `CustomFont`-based font builders.
36 | * `FontPickerFont` has new `.openDyslexic` fonts.
37 | * `FontPickerFont` has new `CustomFont`-based font builders.
38 |
39 | ### 📦 Package Changes
40 |
41 | * `CustomFont` has been moved to https://github.com/danielsaidi/FontKit
42 |
43 |
44 |
45 | ## 0.3
46 |
47 | ### ✨ Features
48 |
49 | * ``CustomFont`` can be used to handle custom fonts.
50 |
51 |
52 |
53 | ## 0.2
54 |
55 | ### ✨ Features
56 |
57 | * ``ColorPickerBar`` has new configuration properties.
58 | * ``ColorPickerBar.Style`` has a new `resetButton` property.
59 | * ``FontPicker`` is a new `ForEach`-based font picker.
60 | * ``FontPickerFont`` is a new multiplatform font representation.
61 | * ``ForEachPicker`` is a new `ForEach`-based value picker.
62 |
63 | ### ✨ Multiplatform Features
64 |
65 | * ``ColorRepresentable`` is a new multiplatform typealias.
66 | * ``ImageRepresentable`` is a new multiplatform typealias.
67 | * ``FontRepresentable`` is a new multiplatform typealias.
68 |
69 | ### 🗑️ Deprecations
70 |
71 | * ``ColorPickerBar.Configuration`` is deprecated.
72 |
73 |
74 |
75 | ## 0.1.1
76 |
77 | ### ✨ Features
78 |
79 | * ``ImagePicker`` has new source type properties.
80 |
81 |
82 |
83 | ## 0.1
84 |
85 | This is the first public release of PickerKit.
86 |
87 | ### ✨ Features
88 |
89 | * ``Camera`` can be used to take photos and handle them as images.
90 | * ``ColorPickerBar`` adds a color picker to a horizontal or vertical bar with additional colors.
91 | * ``DocumentScanner`` can be used to scan documents and handle them as images.
92 | * ``FilePicker`` can be used to pick files from the Files app.
93 | * ``ImagePicker`` can be used to pick images from the photo library.
94 | * ``MultiPicker`` can be used to pick multiple items in e.g. a list or form.
95 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Images/Camera.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Camera.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2020-11-27.
6 | // Copyright © 2020-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import Photos
11 | import SwiftUI
12 |
13 | /// This camera picker can take images and return the result.
14 | ///
15 | /// You can create this view with a result action and an optional `isPresented`:
16 | ///
17 | /// ```swift
18 | /// let picker = Camera(
19 | /// isPresented: $isCameraPresented,
20 | /// action: { result in ... })
21 | /// }
22 | /// ```
23 | ///
24 | /// If you pass in an `isPresented` binding, the view will automatically dismiss
25 | /// itself when it's done.
26 | ///
27 | /// > Important: This view needs the `NSCameraUsageDescription` permission.
28 | public struct Camera: View {
29 |
30 | /// Create a photo camera.
31 | ///
32 | /// - Parameters:
33 | /// - isPresented: An external presented state, if any.
34 | /// - action: The action to use to handle the camera result.
35 | public init(
36 | isPresented: Binding? = nil,
37 | action: @escaping ImagePicker.ResultAction
38 | ) {
39 | self.isPresented = isPresented
40 | self.action = action
41 | }
42 |
43 | private let isPresented: Binding?
44 | private let action: ImagePicker.ResultAction
45 |
46 | public var body: some View {
47 | ImagePicker(
48 | sourceType: .camera,
49 | isPresented: isPresented,
50 | action: action
51 | )
52 | }
53 | }
54 |
55 | #Preview {
56 | struct Preview: View {
57 |
58 | @State var image: Image?
59 | @State var isPresented = false
60 |
61 | var body: some View {
62 | ImagePickerPreview(
63 | image: image,
64 | buttonTitle: "Take Photo",
65 | isPresented: $isPresented
66 | )
67 | .fullScreenCover(isPresented: $isPresented) {
68 | Camera(isPresented: $isPresented) { result in
69 | switch result {
70 | case .cancelled: print("Cancelled")
71 | case .failure(let error): print(error)
72 | case .success(let uiImage): image = Image(uiImage: uiImage)
73 | }
74 | }
75 | .ignoresSafeArea()
76 | }
77 | }
78 | }
79 |
80 | return Preview()
81 | }
82 | #endif
83 |
--------------------------------------------------------------------------------
/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/PickerKit/PickerKit.docc/PickerKit.md:
--------------------------------------------------------------------------------
1 | # ``PickerKit``
2 |
3 | A SwiftUI library with various pickers, cameras, document scanners, etc.
4 |
5 |
6 | ## Overview
7 |
8 | 
9 |
10 | PickerKit is a SwiftUI library with various pickers, cameras, document scanners, etc.
11 |
12 |
13 |
14 | ## Installation
15 |
16 | PickerKit can be installed with the Swift Package Manager:
17 |
18 | ```
19 | https://github.com/danielsaidi/PickerKit.git
20 | ```
21 |
22 |
23 |
24 | ## Support My Work
25 |
26 | You can [become a sponsor][Sponsors] to help me dedicate more time on my various [open-source tools][OpenSource]. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed.
27 |
28 |
29 |
30 | ## Getting Started
31 |
32 | PickerKit has many additional picker utilities, like:
33 |
34 | * ``Camera`` can take photos and handle them as images.
35 | * ``ColorPickerBar`` adds additional colors to a color picker.
36 | * ``DocumentScanner`` can scan documents and handle them as images.
37 | * ``FilePicker`` can pick files from the Files app.
38 | * ``FontPicker`` can pick system and custom fonts.
39 | * ``ImagePicker`` can pick images from the photo library.
40 |
41 | There are also general utilities like:
42 |
43 | * ``ForEachPicker`` can be used to pick single items in a `ForEach`.
44 | * ``MultiPicker`` can be used to pick multiple items in a `ForEach`.
45 |
46 | Note that you must add the proper permissions to be able to access the camera, photo album, files, etc.
47 |
48 |
49 |
50 | ## Repository
51 |
52 | For more information, source code, etc., visit the [project repository](https://github.com/danielsaidi/PickerKit).
53 |
54 |
55 |
56 | ## License
57 |
58 | PickerKit is available under the MIT license.
59 |
60 |
61 |
62 | ## Topics
63 |
64 | ### Colors
65 |
66 | - ``ColorPickerBar``
67 |
68 | ### Files
69 |
70 | - ``FilePicker``
71 |
72 | ### Fonts
73 |
74 | - ``CustomFont``
75 | - ``FontPicker``
76 | - ``FontPickerFont``
77 | - ``FontPickerItem``
78 |
79 | ### General
80 |
81 | - ``CancellableResult``
82 | - ``ForEachPicker``
83 | - ``MultiPicker``
84 | - ``OptionalBinding(_:_:)``
85 | - ``PickerItem``
86 |
87 | ### Images
88 |
89 | - ``Camera``
90 | - ``DocumentScanner``
91 | - ``ImagePicker``
92 |
93 | ### Multiplatform
94 |
95 | - ``ColorRepresentable``
96 | - ``ImageRepresentable``
97 | - ``FontRepresentable``
98 |
99 |
100 |
101 | [Email]: mailto:daniel.saidi@gmail.com
102 | [Website]: https://danielsaidi.com
103 | [GitHub]: https://github.com/danielsaidi
104 | [OpenSource]: https://danielsaidi.com/opensource
105 | [Sponsors]: https://github.com/sponsors/danielsaidi
106 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Colors/ColorPickerBar+Style.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorPickerBar.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2023-06-13.
6 | // Copyright © 2023-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if os(iOS) || os(macOS) || os(visionOS)
12 | import SwiftUI
13 |
14 | public extension ColorPickerBar {
15 |
16 | /// This type can be used to style a ``ColorPickerBar``.
17 | ///
18 | /// You can apply this style with ``SwiftUICore/View/colorPickerBarStyle(_:)``.
19 | struct Style {
20 |
21 | /// Create a custom color picker bar style.
22 | ///
23 | /// - Parameters:
24 | /// - animation: The animation to apply when picking color, by default `.default`.
25 | /// - spacing: The bar item spacing, by default `10`.
26 | /// - colorSize: The size of non-selected colors, by default `20`.
27 | /// - selectedColorSize: The size of the selected color, by default `30`.
28 | /// - resetButton: Whether to add a reset button, by default `false`.
29 | /// - resetButtonImage: The image to use as reset button image, by default `circle.slash`.
30 | public init(
31 | animation: Animation = .default,
32 | spacing: Double = 10.0,
33 | colorSize: Double = 20.0,
34 | selectedColorSize: Double = 30.0,
35 | resetButton: Bool = false,
36 | resetButtonImage: Image = Image(systemName: "xmark.circle")
37 | ) {
38 | self.animation = animation
39 | self.spacing = spacing
40 | self.colorSize = colorSize
41 | self.selectedColorSize = selectedColorSize
42 | self.resetButton = resetButton
43 | self.resetButtonImage = resetButtonImage
44 | }
45 |
46 | /// The animation to apply when picking colors.
47 | public var animation: Animation
48 |
49 | /// The bar item spacing.
50 | public var spacing: Double
51 |
52 | /// The size of non-selected colors.
53 | public var colorSize: Double
54 |
55 | /// The size of the selected color.
56 | public var selectedColorSize: Double
57 |
58 | /// Whether to add a reset button to the picker bar.
59 | public var resetButton: Bool
60 |
61 | /// The image to use as reset button image
62 | public var resetButtonImage: Image
63 | }
64 | }
65 |
66 | public extension ColorPickerBar.Style {
67 |
68 | /// A standard color picker bar style.
69 | static var standard: Self { .init() }
70 | }
71 |
72 | public extension View {
73 |
74 | /// Apply a custom ``ColorPickerBar/Style``.
75 | func colorPickerBarStyle(
76 | _ style: ColorPickerBar.Style
77 | ) -> some View {
78 | self.environment(\.colorPickerBarStyle, style)
79 | }
80 | }
81 |
82 | public extension EnvironmentValues {
83 |
84 | @Entry var colorPickerBarStyle = ColorPickerBar.Style.standard
85 | }
86 | #endif
87 |
--------------------------------------------------------------------------------
/Sources/PickerKit/General/ForEachPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEachPicker.swift
3 | // SwiftUIKit
4 | //
5 | // Created by Daniel Saidi on 2022-03-17.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This picker lists `Identifiable` values in a `ForEach`.
12 | ///
13 | /// The picker supports both optional and non-optional value bindings, and wraps
14 | /// the `content` view in a button that applies the value when it's tapped.
15 | public struct ForEachPicker: View {
16 |
17 | /// Create a picker with an optional binding.
18 | ///
19 | /// - Parameters:
20 | /// - selection: The selected value.
21 | /// - values: The values to display.
22 | /// - content: The list item content view.
23 | public init(
24 | selection: Binding,
25 | values: [Value],
26 | content: @escaping (Value) -> Content
27 | ) {
28 | self.selection = selection
29 | self.values = values
30 | self.content = content
31 | }
32 |
33 | /// Create a picker with a non-optional binding.
34 | ///
35 | /// - Parameters:
36 | /// - selection: The selected value.
37 | /// - values: The values to display.
38 | /// - content: The list item content view.
39 | public init(
40 | selection: Binding,
41 | values: [Value],
42 | content: @escaping (Value) -> Content
43 | ) {
44 | let optionalBinding: Binding = .init(
45 | get: { selection.wrappedValue },
46 | set: {
47 | guard let new = $0 else { return }
48 | selection.wrappedValue = new
49 | }
50 | )
51 | self.init(
52 | selection: optionalBinding,
53 | values: values,
54 | content: content
55 | )
56 | }
57 |
58 | private var selection: Binding
59 | private let values: [Value]
60 | private let content: (Value) -> Content
61 |
62 | public var body: some View {
63 | ForEach(values) { value in
64 | Button {
65 | selection.wrappedValue = value
66 | } label: {
67 | content(value)
68 | }
69 | .tint(.primary)
70 | }
71 | }
72 | }
73 |
74 | #Preview {
75 |
76 | struct Person: Equatable, Identifiable {
77 |
78 | let name: String
79 |
80 | var id: String { name }
81 | }
82 |
83 | struct Preview: View {
84 |
85 | @State var selection: Person?
86 |
87 | let persons: [Person] = [
88 | .init(name: "Daniel"),
89 | .init(name: "Johanna")
90 | ]
91 |
92 | var body: some View {
93 | List {
94 | ForEachPicker(
95 | selection: $selection,
96 | values: persons
97 | ) {
98 | PickerItem($0.name, isSelected: $0 == selection)
99 | }
100 | }
101 | }
102 | }
103 |
104 | return Preview()
105 | }
106 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | # PickerKit
14 |
15 | PickerKit is a SwiftUI library with various pickers, cameras, document scanners, etc.
16 |
17 |
18 | ## Installation
19 |
20 | PickerKit can be installed with the Swift Package Manager:
21 |
22 | ```
23 | https://github.com/danielsaidi/PickerKit.git
24 | ```
25 |
26 |
27 | ## Getting Started
28 |
29 | PickerKit has many additional picker utilities, like:
30 |
31 | * ``Camera`` can take photos and handle them as images.
32 | * ``ColorPickerBar`` adds additional colors to a color picker.
33 | * ``DocumentScanner`` can scan documents and handle them as images.
34 | * ``FilePicker`` can pick files from the Files app.
35 | * ``FontPicker`` can pick system and custom fonts.
36 | * ``ImagePicker`` can pick images from the photo library.
37 |
38 | There are also general utilities like:
39 |
40 | * ``ForEachPicker`` can be used to pick single items in a `ForEach`.
41 | * ``MultiPicker`` can be used to pick multiple items in a `ForEach`.
42 |
43 | Note that you must add the proper permissions to be able to access the camera, photo album, files, etc.
44 |
45 |
46 | ## Documentation
47 |
48 | The online [documentation][Documentation] has more information, articles, code examples, etc.
49 |
50 |
51 | ## Demo Application
52 |
53 | The `Demo` folder has a demo app that lets you test the various pickers in the library.
54 |
55 |
56 | ## Support My Work
57 |
58 | You can [become a sponsor][Sponsors] to help me dedicate more time on my various [open-source tools][OpenSource]. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed.
59 |
60 |
61 | ## Contact
62 |
63 | Feel free to reach out if you have questions, or want to contribute in any way:
64 |
65 | * Website: [danielsaidi.com][Website]
66 | * E-mail: [daniel.saidi@gmail.com][Email]
67 | * Bluesky: [@danielsaidi@bsky.social][Bluesky]
68 | * Mastodon: [@danielsaidi@mastodon.social][Mastodon]
69 |
70 |
71 | ## License
72 |
73 | PickerKit is available under the MIT license. See the [LICENSE][License] file for more info.
74 |
75 |
76 | [Email]: mailto:daniel.saidi@gmail.com
77 | [Website]: https://danielsaidi.com
78 | [GitHub]: https://github.com/danielsaidi
79 | [OpenSource]: https://danielsaidi.com/opensource
80 | [Sponsors]: https://github.com/sponsors/danielsaidi
81 |
82 | [Bluesky]: https://bsky.app/profile/danielsaidi.bsky.social
83 | [Mastodon]: https://mastodon.social/@danielsaidi
84 | [Twitter]: https://twitter.com/danielsaidi
85 |
86 | [Documentation]: https://danielsaidi.github.io/PickerKit
87 | [Getting-Started]: https://danielsaidi.github.io/PickerKit/documentation/Pickerkit/getting-started
88 | [License]: https://github.com/danielsaidi/PickerKit/blob/master/LICENSE
89 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Fonts/FontPickerFont.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPickerFont.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2022-03-17.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import FontKit
10 | import Foundation
11 | import SwiftUI
12 |
13 | /// A platform-agnostic font picker font.
14 | ///
15 | /// You can use ``systemFonts`` to get all the system fonts that are provided
16 | /// by the operating system.
17 | ///
18 | /// You can use ``CustomFont`` fonts to use custom fonts with the font picker.
19 | public struct FontPickerFont: Codable, CustomFontRepresentable, Equatable, Hashable, Identifiable, Sendable {
20 |
21 | /// Create a system picker font based on a font name.
22 | ///
23 | /// - Parameters:
24 | /// - fontName: The font name.
25 | /// - displayName: A custom font display name, if any.
26 | public init(
27 | fontName: String,
28 | displayName: String? = nil
29 | ) {
30 | let fontName = fontName.capitalized
31 | self.name = fontName
32 | self.displayName = displayName ?? fontName
33 | self.systemFontScaleFactor = 1
34 | }
35 |
36 | /// Create a system picker font based on a custom font.
37 | ///
38 | /// - Parameters:
39 | /// - font: The custom font to use.
40 | public init(
41 | from font: CustomFont
42 | ) {
43 | self.name = font.name
44 | self.displayName = font.displayName
45 | self.systemFontScaleFactor = font.systemFontScaleFactor
46 | }
47 |
48 | /// The font's unique identifier.
49 | public var id: String { name }
50 |
51 | // The font name.
52 | public let name: String
53 |
54 | /// The font display name
55 | public let displayName: String
56 |
57 | /// The approximate system font scale factor.
58 | public let systemFontScaleFactor: Double
59 | }
60 |
61 | public extension FontPickerFont {
62 |
63 | /// Get all available font picker fonts.
64 | static var systemFonts: [FontPickerFont] {
65 | let all = FontRepresentable.systemFonts
66 | let sorted = all.sorted { $0.displayName < $1.displayName }
67 | return sorted
68 | }
69 | }
70 |
71 | public extension Collection where Element == FontPickerFont {
72 |
73 | /// Get all available font picker fonts.
74 | static var systemFonts: [FontPickerFont] {
75 | Element.systemFonts
76 | }
77 |
78 | /// Move a certain font topmost in the list.
79 | func moveTopmost(_ topmost: String) -> [FontPickerFont] {
80 | let topmost = topmost.trimmingCharacters(in: .whitespaces)
81 | let exists = contains { $0.name.lowercased() == topmost.lowercased() }
82 | guard exists else { return Array(self) }
83 | var filtered = filter { $0.name.lowercased() != topmost.lowercased() }
84 | let new = FontPickerFont(fontName: topmost)
85 | filtered.insert(new, at: 0)
86 | return filtered
87 | }
88 | }
89 |
90 | private extension FontRepresentable {
91 |
92 | /// Get all available system fonts.
93 | static var systemFonts: [FontPickerFont] {
94 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
95 | NSFontManager.shared
96 | .availableFontFamilies
97 | .map { FontPickerFont(fontName: $0) }
98 | #else
99 | UIFont.familyNames
100 | .map { FontPickerFont(fontName: $0) }
101 | #endif
102 | }
103 | }
104 |
105 | #Preview {
106 |
107 | List {
108 | ForEach(FontPickerFont.systemFonts) { font in
109 | Text(font.name)
110 | .font(.fixed(font, size: 15))
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/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/PickerKit/General/MultiPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiPicker.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2021-08-20.
6 | // Copyright © 2021-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This picker lists `Identifiable` items in a `ForEach`, and allows you to
12 | /// pick multiple items.
13 | ///
14 | /// You can use this view instead of the native `Picker`, to select multiple items
15 | /// in a list or form, to get more control over the list item views.
16 | public struct MultiPicker: View {
17 |
18 | /// Create a for-each multi-picker.
19 | ///
20 | /// - Parameters:
21 | /// - items: The items to list in the picker.
22 | /// - selection: The current selection.
23 | /// - itemView: A item view builder.
24 | public init(
25 | items: [Item],
26 | selection: Binding<[Item]>,
27 | itemView: @escaping ItemViewBuilder
28 | ) {
29 | self.items = items
30 | self.selection = selection
31 | self.itemView = itemView
32 | }
33 |
34 | private let items: [Item]
35 | private let selection: Binding<[Item]>
36 | private let itemView: ItemViewBuilder
37 |
38 | public typealias ItemViewBuilder = (_ item: Item, _ isSelected: Bool) -> ItemView
39 |
40 | @Environment(\.dismiss)
41 | public var dismiss
42 |
43 | public var body: some View {
44 | ForEach(items) { item in
45 | Button(action: { toggleSelection(for: item) }, label: {
46 | HStack {
47 | itemView(item, isSelected(item))
48 | }
49 | })
50 | }
51 | }
52 | }
53 |
54 | private extension MultiPicker {
55 |
56 | var selectedIds: [Item.ID] {
57 | selection.wrappedValue.map { $0.id }
58 | }
59 |
60 | func isSelected(_ item: Item) -> Bool {
61 | selectedIds.contains(item.id)
62 | }
63 |
64 | func toggleSelection(for item: Item) {
65 | if isSelected(item) {
66 | selection.wrappedValue = selection.wrappedValue.filter { $0.id != item.id }
67 | } else {
68 | selection.wrappedValue.append(item)
69 | }
70 | }
71 | }
72 |
73 | #Preview {
74 |
75 | struct PreviewModel: Identifiable {
76 | let id: Int
77 | }
78 |
79 | struct Preview: View {
80 |
81 | @State var selection1: [PreviewModel] = []
82 | @State var selection2: [PreviewModel] = []
83 |
84 | func createValues() -> [PreviewModel] {
85 | (0...10).map { .init(id: $0) }
86 | }
87 |
88 | var body: some View {
89 | NavigationView {
90 | List {
91 | MultiPicker(items: createValues(), selection: $selection1) { item, selected in
92 | PickerItem(isSelected: selected) {
93 | Text("\(item.id)")
94 | }
95 | }
96 | }
97 | .navigationTitle("Pick multiple items")
98 | }
99 | }
100 | }
101 |
102 | struct PreviewItem: Identifiable, Equatable {
103 |
104 | let name: String
105 |
106 | var id: String { name }
107 |
108 | static let all = [
109 | PreviewItem(name: "Item #1"),
110 | PreviewItem(name: "Item #2"),
111 | PreviewItem(name: "Item #3"),
112 | PreviewItem(name: "Item #4"),
113 | PreviewItem(name: "Item #5"),
114 | PreviewItem(name: "Item #6"),
115 | PreviewItem(name: "Item #7"),
116 | PreviewItem(name: "Item #8"),
117 | PreviewItem(name: "Item #9"),
118 | PreviewItem(name: "Item #10"),
119 | PreviewItem(name: "Item #11"),
120 | PreviewItem(name: "Item #12"),
121 | PreviewItem(name: "Item #13"),
122 | PreviewItem(name: "Item #14"),
123 | PreviewItem(name: "Item #15")
124 | ]
125 | }
126 |
127 | return Preview()
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Images/DocumentScanner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentScanner.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2020-01-22.
6 | // Copyright © 2020-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 | import VisionKit
12 |
13 | /// This document scanner can scan one or several pages of a physical document.
14 | ///
15 | /// You can create this view with a result action and an optional `isPresented`:
16 | ///
17 | /// ```swift
18 | /// let camera = DocumentScanner(
19 | /// isPresented: $isScannerPresented,
20 | /// action: { result in ... }
21 | /// )
22 | /// ```
23 | ///
24 | /// If you pass in an `isPresented` binding, the view will automatically dismiss
25 | /// itself when it's done.
26 | ///
27 | /// > Important: This view needs the `NSCameraUsageDescription` permission.
28 | public struct DocumentScanner: UIViewControllerRepresentable {
29 |
30 | /// Create a document scanner.
31 | ///
32 | /// - Parameters:
33 | /// - isPresented: An external presented state, if any.
34 | /// - action: The action to use to handle the scan result.
35 | public init(
36 | isPresented: Binding? = nil,
37 | action: @escaping ResultAction
38 | ) {
39 | self.isPresented = isPresented
40 | self.action = action
41 | }
42 |
43 | public typealias Result = CancellableResult
44 | public typealias ResultAction = (Result) -> Void
45 |
46 | private let isPresented: Binding?
47 | private let action: (Result) -> Void
48 |
49 | public func makeCoordinator() -> Coordinator {
50 | Coordinator(
51 | isPresented: isPresented,
52 | action: action
53 | )
54 | }
55 |
56 | public func makeUIViewController(
57 | context: Context
58 | ) -> VNDocumentCameraViewController {
59 | let controller = VNDocumentCameraViewController()
60 | controller.delegate = context.coordinator
61 | return controller
62 | }
63 |
64 | public func updateUIViewController(
65 | _ uiViewController: VNDocumentCameraViewController,
66 | context: Context
67 | ) {}
68 | }
69 |
70 | public extension DocumentScanner {
71 |
72 | class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
73 |
74 | public init(
75 | isPresented: Binding?,
76 | action: @escaping DocumentScanner.ResultAction
77 | ) {
78 | self.isPresented = isPresented
79 | self.action = action
80 | }
81 |
82 | private let isPresented: Binding?
83 | private let action: DocumentScanner.ResultAction
84 |
85 | public func documentCameraViewControllerDidCancel(
86 | _ controller: VNDocumentCameraViewController
87 | ) {
88 | action(.cancelled)
89 | tryDismiss()
90 | }
91 |
92 | public func documentCameraViewController(
93 | _ controller: VNDocumentCameraViewController,
94 | didFailWithError error: Error
95 | ) {
96 | action(.failure(error))
97 | tryDismiss()
98 | }
99 |
100 | public func documentCameraViewController(
101 | _ controller: VNDocumentCameraViewController,
102 | didFinishWith scan: VNDocumentCameraScan
103 | ) {
104 | action(.success(scan))
105 | tryDismiss()
106 | }
107 |
108 | public func tryDismiss() {
109 | isPresented?.wrappedValue = false
110 | }
111 | }
112 | }
113 |
114 | public extension VNDocumentCameraScan {
115 |
116 | /// Get all the images from the scan.
117 | var images: [ImageRepresentable] {
118 | (0..> $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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Fonts/FontPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPicker.swift
3 | // SwiftUIKit
4 | //
5 | // Created by Daniel Saidi on 2022-03-17.
6 | // Copyright © 2022-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// This picker lists ``FontPickerFont`` items in a `ForEach` that properly
12 | /// renders each font.
13 | ///
14 | /// The picker supports both optional and non-optional value bindings, and wraps
15 | /// the `content` view in a button that applies the value when it's tapped.
16 | ///
17 | /// Any ``CustomFont`` you resolve is automatically added to the picker, since
18 | /// the initializer registers the font. If multiple custom fonts share a font family, this
19 | /// picker will only show the base font.
20 | public struct FontPicker: View {
21 |
22 | /// Create a font picker with an optional binding.
23 | ///
24 | /// - Parameters:
25 | /// - selection: The selected font.
26 | /// - fonts: The fonts to display, by default `.systemFonts`.
27 | /// - content: The list item content view, by default a ``FontPickerItem``.
28 | public init(
29 | selection: Binding,
30 | fonts: [FontPickerFont]? = nil,
31 | content: @escaping (FontPickerFont) -> Content
32 | ) {
33 | self.selection = selection
34 | self.fonts = fonts ?? FontPickerFont.systemFonts
35 | self.content = content
36 | }
37 |
38 | /// Create a font picker with a non-optional binding.
39 | ///
40 | /// - Parameters:
41 | /// - selection: The selected font.
42 | /// - fonts: The fonts to display, by default `all`.
43 | /// - content: The list item content view, by default a ``FontPickerItem``.
44 | public init(
45 | selection: Binding,
46 | fonts: [FontPickerFont]? = nil,
47 | content: @escaping (FontPickerFont) -> Content
48 | ) {
49 | let optionalBinding: Binding = .init(
50 | get: { selection.wrappedValue },
51 | set: {
52 | guard let new = $0 else { return }
53 | selection.wrappedValue = new
54 | }
55 | )
56 | self.init(
57 | selection: optionalBinding,
58 | fonts: fonts,
59 | content: content
60 | )
61 | }
62 |
63 | private var selection: Binding
64 | private let fonts: [FontPickerFont]
65 | private let content: (FontPickerFont) -> Content
66 |
67 | public var body: some View {
68 | ForEachPicker(
69 | selection: selection,
70 | values: fonts,
71 | content: content
72 | )
73 | }
74 | }
75 |
76 | public extension FontPicker where Content == FontPickerItem {
77 |
78 | /// Create a font picker with an optional binding.
79 | ///
80 | /// This initializer applies a ``FontPickerItem`` as the item content view.
81 | ///
82 | /// - Parameters:
83 | /// - selection: The selected font.
84 | /// - fonts: The fonts to display, by default `all`.
85 | init(
86 | selection: Binding,
87 | fonts: [FontPickerFont]? = nil
88 | ) {
89 | self.selection = selection
90 | self.fonts = fonts ?? FontPickerFont.systemFonts
91 | self.content = { FontPickerItem(font: $0, isSelected: selection.wrappedValue == $0) }
92 | }
93 |
94 | /// Create a font picker with a non-optional binding.
95 | ///
96 | /// This initializer applies a ``FontPickerItem`` as the item content view.
97 | ///
98 | /// - Parameters:
99 | /// - selection: The selected font.
100 | /// - fonts: The fonts to display, by default `all`.
101 | init(
102 | selection: Binding,
103 | fonts: [FontPickerFont]? = nil
104 | ) {
105 | let optionalBinding: Binding = .init(
106 | get: { selection.wrappedValue },
107 | set: {
108 | guard let new = $0 else { return }
109 | selection.wrappedValue = new
110 | }
111 | )
112 | self.init(
113 | selection: optionalBinding,
114 | fonts: fonts
115 | )
116 | }
117 | }
118 |
119 | #Preview {
120 |
121 | struct Preview: View {
122 |
123 | @State var selection: FontPickerFont?
124 |
125 | var body: some View {
126 | List {
127 | Section {
128 | FontPicker(
129 | selection: $selection,
130 | fonts: .openDyslexicFonts
131 | )
132 | }
133 | Section {
134 | FontPicker(
135 | selection: $selection,
136 | fonts: .systemFonts
137 | )
138 | }
139 | }
140 | }
141 | }
142 |
143 | return Preview()
144 | }
145 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Files/FilePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilePicker.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2020-04-07.
6 | // Copyright © 2020-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 | import UIKit
12 | import UniformTypeIdentifiers
13 |
14 | /// This file picker can be used to pick files from Files.
15 | ///
16 | /// ```swift
17 | /// let picker = FilePicker(
18 | /// documentTypes: ["public.png"],
19 | /// isPresented: $isFilePickerPresented,
20 | /// action: { result in ... }
21 | /// )
22 | /// ```
23 | ///
24 | /// If you pass in an `isPresented` binding, the view will automatically dismiss
25 | /// itself when it's done.
26 | ///
27 | /// The result contains a list of file urls that you can use in any way that you want.
28 | public struct FilePicker: UIViewControllerRepresentable {
29 |
30 | /// Create a file picker.
31 | ///
32 | /// - Parameters:
33 | /// - documentTypes: The uniform types to pick.
34 | /// - isPresented: An external presented state, if any.
35 | /// - pickerConfig: A custom picker configuration, if any.
36 | /// - action: The action to use to handle the picker result.
37 | public init(
38 | documentTypes: [UTType],
39 | isPresented: Binding? = nil,
40 | pickerConfig: @escaping PickerConfig = { _ in },
41 | action: @escaping ResultAction
42 | ) {
43 | self.documentTypes = documentTypes
44 | self.isPresented = isPresented
45 | self.pickerConfig = pickerConfig
46 | self.action = action
47 | }
48 |
49 | public typealias PickerConfig = (UIDocumentPickerViewController) -> Void
50 | public typealias Result = CancellableResult<[URL]>
51 | public typealias ResultAction = (Result) -> Void
52 |
53 | private let documentTypes: [UTType]
54 | private let isPresented: Binding?
55 | private let pickerConfig: PickerConfig
56 | private let action: ResultAction
57 |
58 | public func makeCoordinator() -> Coordinator {
59 | .init(isPresented: isPresented, action: action)
60 | }
61 |
62 | public func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
63 | let controller = UIDocumentPickerViewController(forOpeningContentTypes: documentTypes)
64 | controller.delegate = context.coordinator
65 | pickerConfig(controller)
66 | return controller
67 | }
68 |
69 | public func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
70 | }
71 |
72 | public extension FilePicker {
73 |
74 | /// This enum defines ``FilePicker``-specific errors.
75 | enum PickerError: Error {
76 | case noAvailableUrl
77 | }
78 | }
79 |
80 | public extension FilePicker {
81 |
82 | class Coordinator: NSObject, UINavigationControllerDelegate, UIDocumentPickerDelegate {
83 |
84 | public init(
85 | isPresented: Binding?,
86 | action: @escaping FilePicker.ResultAction
87 | ) {
88 | self.isPresented = isPresented
89 | self.action = action
90 | }
91 |
92 | private let isPresented: Binding?
93 | private let action: FilePicker.ResultAction
94 |
95 | public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
96 | action(.cancelled)
97 | }
98 |
99 | public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
100 | action(.success(urls))
101 | }
102 | }
103 | }
104 |
105 | #Preview {
106 |
107 | struct Preview: View {
108 |
109 | @State var image: Image?
110 | @State var isPresented = false
111 |
112 | var body: some View {
113 | ImagePickerPreview(
114 | image: image,
115 | buttonTitle: "Pick Image File",
116 | isPresented: $isPresented
117 | )
118 | .fullScreenCover(isPresented: $isPresented) {
119 | FilePicker(
120 | documentTypes: [.image],
121 | isPresented: $isPresented
122 | ) { result in
123 | switch result {
124 | case .cancelled: print("Cancelled")
125 | case .failure(let error): print(error)
126 | case .success(let urls):
127 | guard
128 | let url = urls.first,
129 | let data = try? Data(contentsOf: url),
130 | let uiImage = UIImage(data: data)
131 | else { return }
132 | image = Image(uiImage: uiImage)
133 | }
134 | }
135 | .ignoresSafeArea()
136 | }
137 | }
138 | }
139 |
140 | return Preview()
141 | }
142 | #endif
143 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/PickerKit/Images/ImagePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImagePicker.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2020-04-07.
6 | // Copyright © 2020-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import Photos
11 | import SwiftUI
12 | import UIKit
13 |
14 | /// This picker can be used to pick photos from Photos, with the camera, etc.
15 | ///
16 | /// ```swift
17 | /// let picker = ImagePicker(
18 | /// sourceType: .photoAlbum,
19 | /// isPresented: $isPickerPresented,
20 | /// pickerConfig: { picker in ... },
21 | /// action: { result in ... }
22 | /// )
23 | /// ```
24 | ///
25 | /// If you pass in an `isPresented` binding, the view will automatically dismiss
26 | /// itself when it's done.
27 | public struct ImagePicker: UIViewControllerRepresentable {
28 |
29 | /// Create an image picker.
30 | ///
31 | /// - Parameters:
32 | /// - sourceType: The image source type to pick.
33 | /// - isPresented: An external presented state, if any.
34 | /// - pickerConfig: A custom picker configuration, if any.
35 | /// - action: The action to use to handle the picker result.
36 | public init(
37 | sourceType: UIImagePickerController.SourceType,
38 | isPresented: Binding? = nil,
39 | pickerConfig: @escaping PickerConfig = { _ in },
40 | action: @escaping ResultAction
41 | ) {
42 | self.sourceType = sourceType
43 | self.isPresented = isPresented
44 | self.pickerConfig = pickerConfig
45 | self.action = action
46 | }
47 |
48 | public typealias PickerConfig = (UIImagePickerController) -> Void
49 | public typealias Result = CancellableResult
50 | public typealias ResultAction = (Result) -> Void
51 | public typealias SourceType = UIImagePickerController.SourceType
52 |
53 | private let sourceType: UIImagePickerController.SourceType
54 | private let isPresented: Binding?
55 | private let pickerConfig: PickerConfig
56 | private let action: ResultAction
57 |
58 | public func makeCoordinator() -> Coordinator {
59 | .init(isPresented: isPresented, action: action)
60 | }
61 |
62 | public func makeUIViewController(context: Context) -> UIImagePickerController {
63 | let controller = UIImagePickerController()
64 | controller.sourceType = sourceType
65 | controller.delegate = context.coordinator
66 | pickerConfig(controller)
67 | return controller
68 | }
69 |
70 | public func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
71 | }
72 |
73 | public extension ImagePicker {
74 |
75 | /// This enum defines ``ImagePicker``-specific errors.
76 | enum PickerError: Error {
77 | case missingPhotoLibraryPermissions
78 | case missingPickedImage
79 | }
80 |
81 | /// All source types that are enabled for the picker.
82 | static var availableSourceTypes: [UIImagePickerController.SourceType] {
83 | supportedSourceTypes.filter(UIImagePickerController.isSourceTypeAvailable)
84 | }
85 |
86 | /// All source types that are supported by the picker.
87 | static var supportedSourceTypes: [UIImagePickerController.SourceType] {
88 | [.camera, .photoLibrary, .savedPhotosAlbum]
89 | }
90 | }
91 |
92 | public extension ImagePicker {
93 |
94 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
95 |
96 | public init(
97 | isPresented: Binding?,
98 | action: @escaping ImagePicker.ResultAction
99 | ) {
100 | self.isPresented = isPresented
101 | self.action = action
102 | }
103 |
104 | private let isPresented: Binding?
105 | private let action: ImagePicker.ResultAction
106 |
107 | public func imagePickerControllerDidCancel(
108 | _ picker: UIImagePickerController
109 | ) {
110 | action(.cancelled)
111 | tryDismiss()
112 | }
113 |
114 | public func imagePickerController(
115 | _ picker: UIImagePickerController,
116 | didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
117 | ) {
118 | if let image = info[.originalImage] as? UIImage {
119 | action(.success(image))
120 | } else {
121 | let error = PickerError.missingPickedImage
122 | action(.failure(error))
123 | }
124 | tryDismiss()
125 | }
126 |
127 | public func tryDismiss() {
128 | isPresented?.wrappedValue = false
129 | }
130 | }
131 | }
132 |
133 | struct ImagePickerPreview: View {
134 |
135 | let image: Image?
136 | let buttonTitle: String
137 | let isPresented: Binding
138 |
139 | var body: some View {
140 | ScrollView {
141 | image?
142 | .resizable()
143 | .aspectRatio(contentMode: .fit)
144 | .clipShape(.rect(cornerRadius: 10))
145 | .padding()
146 | }
147 | .safeAreaInset(edge: .bottom) {
148 | Button(buttonTitle) {
149 | isPresented.wrappedValue = true
150 | }
151 | .buttonStyle(.borderedProminent)
152 | }
153 | }
154 | }
155 |
156 | #Preview {
157 | struct Preview: View {
158 |
159 | @State var image: Image?
160 | @State var isPresented = false
161 |
162 | var body: some View {
163 | ImagePickerPreview(
164 | image: image,
165 | buttonTitle: "Pick Image",
166 | isPresented: $isPresented
167 | )
168 | .fullScreenCover(isPresented: $isPresented) {
169 | ImagePicker(
170 | sourceType: .photoLibrary,
171 | isPresented: $isPresented
172 | ) { result in
173 | switch result {
174 | case .cancelled: print("Cancelled")
175 | case .failure(let error): print(error)
176 | case .success(let uiImage): image = Image(uiImage: uiImage)
177 | }
178 | }
179 | .ignoresSafeArea()
180 | }
181 | }
182 | }
183 |
184 | return Preview()
185 | }
186 | #endif
187 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Sources/PickerKit/Colors/ColorPickerBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorPickerBar.swift
3 | // PickerKit
4 | //
5 | // Created by Daniel Saidi on 2023-06-13.
6 | // Copyright © 2023-2025 Daniel Saidi. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | #if os(iOS) || os(macOS) || os(visionOS)
12 | import SwiftUI
13 |
14 | /// This color picker adds a bar of extra colors to a SwiftUI `ColorPicker`.
15 | ///
16 | /// The picker supports both optional and non-optional colors. You can customize
17 | /// which colors to add to the bar, whether the picker supports opacity, etc.
18 | ///
19 | /// Available view modifiers:
20 | /// - ``SwiftUICore/View/colorPickerBarStyle(_:)``
21 | public struct ColorPickerBar: View {
22 |
23 | /// Create a color picker bar with an optional binding.
24 | ///
25 | /// - Parameters:
26 | /// - title: The picker title, by default "Pick Color".
27 | /// - titleBundle: The picker title localization bundle, by default `.main`.
28 | /// - selection: An optional color value binding.
29 | /// - axis: The picker axis, by default `.horizontal`.
30 | /// - colors: The colors to add to the bar, by default `.standardColorPickerBarColors`.
31 | /// - resetValue: An optional reset value, by default `nil`.
32 | /// - supportsOpacity: Whether to support opacity, by default `true`.
33 | public init(
34 | _ title: String.LocalizationValue = "Pick Color",
35 | titleBundle: Bundle? = nil,
36 | selection: Binding,
37 | axis: Axis? = nil,
38 | colors: [Color]? = nil,
39 | resetValue: Color? = nil,
40 | supportsOpacity: Bool? = nil
41 | ) {
42 | self.title = title
43 | self.titleBundle = titleBundle ?? .main
44 | self.selection = selection
45 | self.axis = axis ?? .horizontal
46 | self.colors = colors ?? .standardColorPickerBarColors
47 | self.resetValue = resetValue
48 | self.supportsOpacity = supportsOpacity ?? true
49 | }
50 |
51 | /// Create a color picker bar with a non-optional binding.
52 | ///
53 | /// - Parameters:
54 | /// - title: The picker title, by default "Pick Color".
55 | /// - titleBundle: The picker title localization bundle, by default `.main`.
56 | /// - axis: The picker axis, by default `.horizontal`.
57 | /// - selection: An non-optional color value binding.
58 | /// - colors: The colors to add to the bar, by default `.standardColorPickerBarColors`.
59 | /// - resetValue: An optional reset value, by default `nil`.
60 | /// - supportsOpacity: Whether to support opacity, by default `true`.
61 | public init(
62 | _ title: String.LocalizationValue,
63 | titleBundle: Bundle? = nil,
64 | selection: Binding,
65 | axis: Axis? = nil,
66 | colors: [Color]? = nil,
67 | resetValue: Color? = nil,
68 | supportsOpacity: Bool? = nil
69 | ) {
70 | let optionalBinding: Binding = .init(
71 | get: { selection.wrappedValue },
72 | set: { selection.wrappedValue = $0 ?? .clear }
73 | )
74 | self.init(
75 | title,
76 | titleBundle: titleBundle,
77 | selection: optionalBinding,
78 | axis: axis,
79 | colors: colors,
80 | resetValue: resetValue,
81 | supportsOpacity: supportsOpacity
82 | )
83 | }
84 |
85 | private let title: String.LocalizationValue
86 | private let titleBundle: Bundle
87 | private let selection: Binding
88 | private let axis: Axis
89 | private let colors: [Color]
90 | private var resetValue: Color?
91 | private var supportsOpacity: Bool
92 |
93 | @Environment(\.colorPickerBarStyle)
94 | private var style: Style
95 |
96 | @Environment(\.colorScheme)
97 | private var colorScheme
98 |
99 | public var body: some View {
100 | if axis == .vertical {
101 | VStack(spacing: 0) {
102 | bodyContent
103 | }
104 | .labelsHidden()
105 | .frame(maxWidth: style.selectedColorSize)
106 | } else {
107 | HStack(spacing: 0) {
108 | bodyContent
109 | }
110 | .labelsHidden()
111 | .frame(maxHeight: style.selectedColorSize)
112 | }
113 | }
114 |
115 | @ViewBuilder
116 | var bodyContent: some View {
117 | picker
118 | if !colors.isEmpty {
119 | divider
120 | scrollView
121 | }
122 | if shouldShowResetButton {
123 | divider
124 | resetButton
125 | }
126 | }
127 | }
128 |
129 | private extension ColorPickerBar {
130 |
131 | func colorButton(for color: Color) -> some View {
132 | Button {
133 | selection.wrappedValue = color
134 | } label: {
135 | let size = scrollViewCircleSize(for: color)
136 | colorCircle(for: color)
137 | .frame(width: size, height: size)
138 | .padding(colorButtonPaddingEdge, isSelected(color) ? 0 : 5)
139 | .animation(style.animation, value: selection.wrappedValue)
140 | }
141 | .buttonStyle(.plain)
142 | }
143 |
144 | var colorButtonPaddingEdge: Edge.Set {
145 | axis == .horizontal ? .vertical : .horizontal
146 | }
147 |
148 | @ViewBuilder
149 | func colorCircle(for color: Color) -> some View {
150 | Circle()
151 | .stroke(scrollViewCircleStroke(for: color), lineWidth: 1)
152 | .background(scrollViewCircleBackground(for: color))
153 | }
154 |
155 | var divider: some View {
156 | Divider()
157 | }
158 |
159 | var picker: some View {
160 | ColorPicker(
161 | String(localized: title, bundle: titleBundle),
162 | selection: selection ?? .clear,
163 | supportsOpacity: supportsOpacity
164 | )
165 | .fixedSize()
166 | .padding(pickerPaddingEdge, style.spacing)
167 | }
168 |
169 | var pickerPaddingEdge: Edge.Set {
170 | axis == .horizontal ? .trailing : .bottom
171 | }
172 |
173 | var resetButton: some View {
174 | Button {
175 | selection.wrappedValue = resetValue
176 | } label: {
177 | style.resetButtonImage
178 | .resizable()
179 | .frame(width: style.colorSize, height: style.colorSize)
180 | }
181 | .padding(resetButtonPaddingEdge, style.spacing)
182 | }
183 |
184 | var resetButtonPaddingEdge: Edge.Set {
185 | axis == .horizontal ? .horizontal : .vertical
186 | }
187 |
188 | var scrollView: some View {
189 | ScrollView(scrollViewAxis, showsIndicators: false) {
190 | scrollViewStack
191 | .padding(scrollViewStylePaddingEdge, style.spacing)
192 | .padding(scrollViewStaticPaddingEdge, 2)
193 | }
194 | .frame(maxWidth: .infinity)
195 | }
196 |
197 | @ViewBuilder
198 | var scrollViewStack: some View {
199 | if axis == .horizontal {
200 | HStack(spacing: style.spacing) {
201 | scrollViewStackContent
202 | }
203 | } else {
204 | VStack(spacing: style.spacing) {
205 | scrollViewStackContent
206 | }
207 | }
208 | }
209 |
210 | var scrollViewStackContent: some View {
211 | ForEach(Array(colors.enumerated()), id: \.offset) {
212 | colorButton(for: $0.element)
213 | }
214 | }
215 |
216 | var scrollViewAxis: Axis.Set {
217 | axis == .horizontal ? .horizontal : .vertical
218 | }
219 |
220 | var scrollViewStylePaddingEdge: Edge.Set {
221 | axis == .horizontal ? .horizontal : .vertical
222 | }
223 |
224 | var scrollViewStaticPaddingEdge: Edge.Set {
225 | axis == .horizontal ? .vertical : .horizontal
226 | }
227 |
228 | @ViewBuilder
229 | func scrollViewCircleBackground(for color: Color) -> some View {
230 | if color == .clear {
231 | Image(systemName: "circle.dotted")
232 | .resizable()
233 | } else {
234 | Circle()
235 | .fill(color)
236 | .shadow(radius: 1, y: 1)
237 | }
238 | }
239 |
240 | func scrollViewCircleSize(for color: Color) -> Double {
241 | isSelected(color) ? style.selectedColorSize : style.colorSize
242 | }
243 |
244 | func scrollViewCircleStroke(for color: Color) -> Color {
245 | if color == .black && colorScheme == .dark { return .white }
246 | return .clear
247 | }
248 | }
249 |
250 | private extension ColorPickerBar {
251 |
252 | var hasChanges: Bool {
253 | selection.wrappedValue != resetValue
254 | }
255 |
256 | var shouldShowResetButton: Bool {
257 | style.resetButton && hasChanges
258 | }
259 |
260 | func isSelected(_ color: Color) -> Bool {
261 | selection.wrappedValue == color
262 | }
263 |
264 | func select(color: Color) {
265 | selection.wrappedValue = color
266 | }
267 | }
268 |
269 | public extension Collection where Element == Color {
270 |
271 | /// Get a standard list of `ColorPickerBar` colors.
272 | static var standardColorPickerBarColors: [Color] {
273 | [
274 | .black, .gray, .white,
275 | .red, .pink, .orange, .yellow,
276 | .indigo, .purple, .blue, .cyan, .teal, .mint,
277 | .green, .brown
278 | ]
279 | }
280 |
281 | /// Get a standard list of `ColorPickerBar` colors, with
282 | /// an optional clear color.
283 | static func standardColorPickerBarColors(
284 | withClearColor: Bool
285 | ) -> [Color] {
286 | let standard = standardColorPickerBarColors
287 | guard withClearColor else { return standard }
288 | return [.clear] + standard
289 | }
290 | }
291 |
292 | private struct Preview: View {
293 |
294 | let axis: Axis
295 |
296 | @State var color1: Color = .red
297 | @State var color2: Color = .yellow
298 | @State var color3: Color = .purple
299 | @State var color4: Color?
300 |
301 | @ViewBuilder
302 | var pickerStack: some View {
303 | switch axis {
304 | case .horizontal: VStack { pickerStackContent }.padding([.leading, .vertical])
305 | case .vertical: HStack { pickerStackContent }.padding([.top, .horizontal])
306 | }
307 | }
308 |
309 | @ViewBuilder
310 | var pickerStackContent: some View {
311 | ColorPickerBar(
312 | "Pick Color",
313 | selection: $color1,
314 | axis: axis,
315 | colors: [.red, .green, .blue]
316 | )
317 | ColorPickerBar(
318 | "Pick Color",
319 | selection: $color2,
320 | axis: axis,
321 | )
322 | ColorPickerBar(
323 | "Pick Color",
324 | selection: $color3,
325 | axis: axis,
326 | )
327 | ColorPickerBar(
328 | "Pick Color",
329 | selection: $color4,
330 | axis: axis,
331 | colors: .standardColorPickerBarColors(withClearColor: true),
332 | resetValue: nil,
333 | supportsOpacity: false
334 | )
335 | }
336 |
337 | var body: some View {
338 | VStack(spacing: 0) {
339 | pickerStack
340 | pickerStack
341 | .background(Color.black)
342 | .colorScheme(.dark)
343 | }
344 | }
345 | }
346 |
347 | #Preview("Horizontal") {
348 | Preview(axis: .horizontal)
349 | }
350 |
351 | #Preview("Vertical") {
352 | Preview(axis: .vertical)
353 | }
354 | #endif
355 |
--------------------------------------------------------------------------------
/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 | A993A4512DF82DC40031E8B3 /* PickerKit in Frameworks */ = {isa = PBXBuildFile; productRef = A993A4502DF82DC40031E8B3 /* PickerKit */; };
11 | /* End PBXBuildFile section */
12 |
13 | /* Begin PBXFileReference section */
14 | A993A4402DF82DB10031E8B3 /* 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 | A993A4422DF82DB10031E8B3 /* Demo */ = {
19 | isa = PBXFileSystemSynchronizedRootGroup;
20 | path = Demo;
21 | sourceTree = "";
22 | };
23 | /* End PBXFileSystemSynchronizedRootGroup section */
24 |
25 | /* Begin PBXFrameworksBuildPhase section */
26 | A993A43D2DF82DB10031E8B3 /* Frameworks */ = {
27 | isa = PBXFrameworksBuildPhase;
28 | buildActionMask = 2147483647;
29 | files = (
30 | A993A4512DF82DC40031E8B3 /* PickerKit in Frameworks */,
31 | );
32 | runOnlyForDeploymentPostprocessing = 0;
33 | };
34 | /* End PBXFrameworksBuildPhase section */
35 |
36 | /* Begin PBXGroup section */
37 | A993A4372DF82DB10031E8B3 = {
38 | isa = PBXGroup;
39 | children = (
40 | A993A4422DF82DB10031E8B3 /* Demo */,
41 | A993A4412DF82DB10031E8B3 /* Products */,
42 | );
43 | sourceTree = "";
44 | };
45 | A993A4412DF82DB10031E8B3 /* Products */ = {
46 | isa = PBXGroup;
47 | children = (
48 | A993A4402DF82DB10031E8B3 /* Demo.app */,
49 | );
50 | name = Products;
51 | sourceTree = "";
52 | };
53 | /* End PBXGroup section */
54 |
55 | /* Begin PBXNativeTarget section */
56 | A993A43F2DF82DB10031E8B3 /* Demo */ = {
57 | isa = PBXNativeTarget;
58 | buildConfigurationList = A993A44C2DF82DB30031E8B3 /* Build configuration list for PBXNativeTarget "Demo" */;
59 | buildPhases = (
60 | A993A43C2DF82DB10031E8B3 /* Sources */,
61 | A993A43D2DF82DB10031E8B3 /* Frameworks */,
62 | A993A43E2DF82DB10031E8B3 /* Resources */,
63 | );
64 | buildRules = (
65 | );
66 | dependencies = (
67 | );
68 | fileSystemSynchronizedGroups = (
69 | A993A4422DF82DB10031E8B3 /* Demo */,
70 | );
71 | name = Demo;
72 | packageProductDependencies = (
73 | A993A4502DF82DC40031E8B3 /* PickerKit */,
74 | );
75 | productName = Demo;
76 | productReference = A993A4402DF82DB10031E8B3 /* Demo.app */;
77 | productType = "com.apple.product-type.application";
78 | };
79 | /* End PBXNativeTarget section */
80 |
81 | /* Begin PBXProject section */
82 | A993A4382DF82DB10031E8B3 /* Project object */ = {
83 | isa = PBXProject;
84 | attributes = {
85 | BuildIndependentTargetsInParallel = 1;
86 | LastSwiftUpdateCheck = 1640;
87 | LastUpgradeCheck = 2600;
88 | ORGANIZATIONNAME = "Daniel Saidi";
89 | TargetAttributes = {
90 | A993A43F2DF82DB10031E8B3 = {
91 | CreatedOnToolsVersion = 16.4;
92 | };
93 | };
94 | };
95 | buildConfigurationList = A993A43B2DF82DB10031E8B3 /* Build configuration list for PBXProject "Demo" */;
96 | developmentRegion = en;
97 | hasScannedForEncodings = 0;
98 | knownRegions = (
99 | en,
100 | Base,
101 | );
102 | mainGroup = A993A4372DF82DB10031E8B3;
103 | minimizedProjectReferenceProxies = 1;
104 | packageReferences = (
105 | A993A44F2DF82DC40031E8B3 /* XCLocalSwiftPackageReference "../../PickerKit" */,
106 | );
107 | preferredProjectObjectVersion = 77;
108 | productRefGroup = A993A4412DF82DB10031E8B3 /* Products */;
109 | projectDirPath = "";
110 | projectRoot = "";
111 | targets = (
112 | A993A43F2DF82DB10031E8B3 /* Demo */,
113 | );
114 | };
115 | /* End PBXProject section */
116 |
117 | /* Begin PBXResourcesBuildPhase section */
118 | A993A43E2DF82DB10031E8B3 /* Resources */ = {
119 | isa = PBXResourcesBuildPhase;
120 | buildActionMask = 2147483647;
121 | files = (
122 | );
123 | runOnlyForDeploymentPostprocessing = 0;
124 | };
125 | /* End PBXResourcesBuildPhase section */
126 |
127 | /* Begin PBXSourcesBuildPhase section */
128 | A993A43C2DF82DB10031E8B3 /* Sources */ = {
129 | isa = PBXSourcesBuildPhase;
130 | buildActionMask = 2147483647;
131 | files = (
132 | );
133 | runOnlyForDeploymentPostprocessing = 0;
134 | };
135 | /* End PBXSourcesBuildPhase section */
136 |
137 | /* Begin XCBuildConfiguration section */
138 | A993A44A2DF82DB30031E8B3 /* Debug */ = {
139 | isa = XCBuildConfiguration;
140 | buildSettings = {
141 | ALWAYS_SEARCH_USER_PATHS = NO;
142 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
143 | CLANG_ANALYZER_NONNULL = YES;
144 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
145 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
146 | CLANG_ENABLE_MODULES = YES;
147 | CLANG_ENABLE_OBJC_ARC = YES;
148 | CLANG_ENABLE_OBJC_WEAK = YES;
149 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
150 | CLANG_WARN_BOOL_CONVERSION = YES;
151 | CLANG_WARN_COMMA = YES;
152 | CLANG_WARN_CONSTANT_CONVERSION = YES;
153 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
154 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
155 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
156 | CLANG_WARN_EMPTY_BODY = YES;
157 | CLANG_WARN_ENUM_CONVERSION = YES;
158 | CLANG_WARN_INFINITE_RECURSION = YES;
159 | CLANG_WARN_INT_CONVERSION = YES;
160 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
161 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
162 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
163 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
164 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
165 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
166 | CLANG_WARN_STRICT_PROTOTYPES = YES;
167 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
168 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
169 | CLANG_WARN_UNREACHABLE_CODE = YES;
170 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
171 | COPY_PHASE_STRIP = NO;
172 | DEAD_CODE_STRIPPING = YES;
173 | DEBUG_INFORMATION_FORMAT = dwarf;
174 | DEVELOPMENT_TEAM = PMEDFW438U;
175 | ENABLE_STRICT_OBJC_MSGSEND = YES;
176 | ENABLE_TESTABILITY = YES;
177 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
178 | GCC_C_LANGUAGE_STANDARD = gnu17;
179 | GCC_DYNAMIC_NO_PIC = NO;
180 | GCC_NO_COMMON_BLOCKS = YES;
181 | GCC_OPTIMIZATION_LEVEL = 0;
182 | GCC_PREPROCESSOR_DEFINITIONS = (
183 | "DEBUG=1",
184 | "$(inherited)",
185 | );
186 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
187 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
188 | GCC_WARN_UNDECLARED_SELECTOR = YES;
189 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
190 | GCC_WARN_UNUSED_FUNCTION = YES;
191 | GCC_WARN_UNUSED_VARIABLE = YES;
192 | IPHONEOS_DEPLOYMENT_TARGET = 26.0;
193 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
194 | MACOSX_DEPLOYMENT_TARGET = 26.0;
195 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
196 | MTL_FAST_MATH = YES;
197 | ONLY_ACTIVE_ARCH = YES;
198 | STRING_CATALOG_GENERATE_SYMBOLS = YES;
199 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
200 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
201 | TVOS_DEPLOYMENT_TARGET = 26.0;
202 | WATCHOS_DEPLOYMENT_TARGET = 26.0;
203 | XROS_DEPLOYMENT_TARGET = 26.0;
204 | };
205 | name = Debug;
206 | };
207 | A993A44B2DF82DB30031E8B3 /* Release */ = {
208 | isa = XCBuildConfiguration;
209 | buildSettings = {
210 | ALWAYS_SEARCH_USER_PATHS = NO;
211 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
212 | CLANG_ANALYZER_NONNULL = YES;
213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
215 | CLANG_ENABLE_MODULES = YES;
216 | CLANG_ENABLE_OBJC_ARC = YES;
217 | CLANG_ENABLE_OBJC_WEAK = YES;
218 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
219 | CLANG_WARN_BOOL_CONVERSION = YES;
220 | CLANG_WARN_COMMA = YES;
221 | CLANG_WARN_CONSTANT_CONVERSION = YES;
222 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
224 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
225 | CLANG_WARN_EMPTY_BODY = YES;
226 | CLANG_WARN_ENUM_CONVERSION = YES;
227 | CLANG_WARN_INFINITE_RECURSION = YES;
228 | CLANG_WARN_INT_CONVERSION = YES;
229 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
230 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
231 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
233 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
235 | CLANG_WARN_STRICT_PROTOTYPES = YES;
236 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
238 | CLANG_WARN_UNREACHABLE_CODE = YES;
239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
240 | COPY_PHASE_STRIP = NO;
241 | DEAD_CODE_STRIPPING = YES;
242 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
243 | DEVELOPMENT_TEAM = PMEDFW438U;
244 | ENABLE_NS_ASSERTIONS = NO;
245 | ENABLE_STRICT_OBJC_MSGSEND = YES;
246 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
247 | GCC_C_LANGUAGE_STANDARD = gnu17;
248 | GCC_NO_COMMON_BLOCKS = YES;
249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
251 | GCC_WARN_UNDECLARED_SELECTOR = YES;
252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
253 | GCC_WARN_UNUSED_FUNCTION = YES;
254 | GCC_WARN_UNUSED_VARIABLE = YES;
255 | IPHONEOS_DEPLOYMENT_TARGET = 26.0;
256 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
257 | MACOSX_DEPLOYMENT_TARGET = 26.0;
258 | MTL_ENABLE_DEBUG_INFO = NO;
259 | MTL_FAST_MATH = YES;
260 | STRING_CATALOG_GENERATE_SYMBOLS = YES;
261 | SWIFT_COMPILATION_MODE = wholemodule;
262 | TVOS_DEPLOYMENT_TARGET = 26.0;
263 | WATCHOS_DEPLOYMENT_TARGET = 26.0;
264 | XROS_DEPLOYMENT_TARGET = 26.0;
265 | };
266 | name = Release;
267 | };
268 | A993A44D2DF82DB30031E8B3 /* Debug */ = {
269 | isa = XCBuildConfiguration;
270 | buildSettings = {
271 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
272 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
273 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements;
274 | CODE_SIGN_STYLE = Automatic;
275 | CURRENT_PROJECT_VERSION = 1;
276 | DEAD_CODE_STRIPPING = YES;
277 | DEVELOPMENT_TEAM = PMEDFW438U;
278 | ENABLE_APP_SANDBOX = YES;
279 | ENABLE_HARDENED_RUNTIME = YES;
280 | ENABLE_PREVIEWS = YES;
281 | ENABLE_USER_SELECTED_FILES = readonly;
282 | GENERATE_INFOPLIST_FILE = YES;
283 | INFOPLIST_KEY_CFBundleDisplayName = PickerKit;
284 | INFOPLIST_KEY_NSCameraUsageDescription = "This app uses the camera to take pictures.";
285 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
286 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
287 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
288 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
289 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
290 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
291 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
292 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
293 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
294 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
295 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
296 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
297 | MARKETING_VERSION = 1.0;
298 | PRODUCT_BUNDLE_IDENTIFIER = com.kankoda.Demo;
299 | PRODUCT_NAME = "$(TARGET_NAME)";
300 | REGISTER_APP_GROUPS = YES;
301 | SDKROOT = auto;
302 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
303 | SWIFT_EMIT_LOC_STRINGS = YES;
304 | SWIFT_VERSION = 5.0;
305 | TARGETED_DEVICE_FAMILY = "1,2,7";
306 | };
307 | name = Debug;
308 | };
309 | A993A44E2DF82DB30031E8B3 /* Release */ = {
310 | isa = XCBuildConfiguration;
311 | buildSettings = {
312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
313 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
314 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements;
315 | CODE_SIGN_STYLE = Automatic;
316 | CURRENT_PROJECT_VERSION = 1;
317 | DEAD_CODE_STRIPPING = YES;
318 | DEVELOPMENT_TEAM = PMEDFW438U;
319 | ENABLE_APP_SANDBOX = YES;
320 | ENABLE_HARDENED_RUNTIME = YES;
321 | ENABLE_PREVIEWS = YES;
322 | ENABLE_USER_SELECTED_FILES = readonly;
323 | GENERATE_INFOPLIST_FILE = YES;
324 | INFOPLIST_KEY_CFBundleDisplayName = PickerKit;
325 | INFOPLIST_KEY_NSCameraUsageDescription = "This app uses the camera to take pictures.";
326 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
327 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
328 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
329 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
330 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
331 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
332 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
333 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
334 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
335 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
336 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
337 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
338 | MARKETING_VERSION = 1.0;
339 | PRODUCT_BUNDLE_IDENTIFIER = com.kankoda.Demo;
340 | PRODUCT_NAME = "$(TARGET_NAME)";
341 | REGISTER_APP_GROUPS = YES;
342 | SDKROOT = auto;
343 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
344 | SWIFT_EMIT_LOC_STRINGS = YES;
345 | SWIFT_VERSION = 5.0;
346 | TARGETED_DEVICE_FAMILY = "1,2,7";
347 | };
348 | name = Release;
349 | };
350 | /* End XCBuildConfiguration section */
351 |
352 | /* Begin XCConfigurationList section */
353 | A993A43B2DF82DB10031E8B3 /* Build configuration list for PBXProject "Demo" */ = {
354 | isa = XCConfigurationList;
355 | buildConfigurations = (
356 | A993A44A2DF82DB30031E8B3 /* Debug */,
357 | A993A44B2DF82DB30031E8B3 /* Release */,
358 | );
359 | defaultConfigurationIsVisible = 0;
360 | defaultConfigurationName = Release;
361 | };
362 | A993A44C2DF82DB30031E8B3 /* Build configuration list for PBXNativeTarget "Demo" */ = {
363 | isa = XCConfigurationList;
364 | buildConfigurations = (
365 | A993A44D2DF82DB30031E8B3 /* Debug */,
366 | A993A44E2DF82DB30031E8B3 /* Release */,
367 | );
368 | defaultConfigurationIsVisible = 0;
369 | defaultConfigurationName = Release;
370 | };
371 | /* End XCConfigurationList section */
372 |
373 | /* Begin XCLocalSwiftPackageReference section */
374 | A993A44F2DF82DC40031E8B3 /* XCLocalSwiftPackageReference "../../PickerKit" */ = {
375 | isa = XCLocalSwiftPackageReference;
376 | relativePath = ../../PickerKit;
377 | };
378 | /* End XCLocalSwiftPackageReference section */
379 |
380 | /* Begin XCSwiftPackageProductDependency section */
381 | A993A4502DF82DC40031E8B3 /* PickerKit */ = {
382 | isa = XCSwiftPackageProductDependency;
383 | productName = PickerKit;
384 | };
385 | /* End XCSwiftPackageProductDependency section */
386 | };
387 | rootObject = A993A4382DF82DB10031E8B3 /* Project object */;
388 | }
389 |
--------------------------------------------------------------------------------