├── .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 | ![PickerKit logo](Logo.png) 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 | Project Icon 3 |

4 | 5 |

6 | Version 7 | Swift 6.1 8 | Documentation 9 | MIT License 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 | --------------------------------------------------------------------------------