├── Icons ├── GitHubBanner.png ├── GitHubBanner.pxd ├── SettingsKitIcon.png └── SettingsKitIcon.pxd ├── Tests ├── TestApp │ ├── TestApp │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── TestApp.entitlements │ │ ├── View │ │ │ └── ContentView.swift │ │ ├── TestAppApp.swift │ │ └── ViewModel │ │ │ └── TestAppModel.swift │ ├── TestApp.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── project.pbxproj │ ├── xcodebuild.log │ └── .swiftlint.yml ├── Examples │ ├── Examples │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── Examples.entitlements │ │ ├── ContentView.swift │ │ ├── AccountView.swift │ │ ├── GeneralSettings.swift │ │ └── ExamplesApp.swift │ └── Examples.xcodeproj │ │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ │ └── project.pbxproj └── SettingsKitTests │ └── SettingsKitTests.swift ├── Sources └── SettingsKit │ ├── SettingsKit.docc │ ├── Resources │ │ ├── DefaultDesign.png │ │ └── SidebarDesign.png │ ├── SettingsKit.md │ ├── theme-settings.json │ ├── Usage │ │ ├── SidebarDesign.md │ │ ├── Actions.md │ │ ├── AddSettingsWindow.md │ │ └── TabsAndSubtabs.md │ └── GettingStarted.md │ ├── Model │ ├── Extensions │ │ ├── SwiftUI │ │ │ ├── KeyboardShortcut.swift │ │ │ ├── View.swift │ │ │ └── Scene.swift │ │ ├── Double.swift │ │ ├── String.swift │ │ ├── CGFloat.swift │ │ ├── Array.swift │ │ └── SettingsTab+.swift │ └── Data │ │ ├── SettingsWindowDesign.swift │ │ ├── ToolbarActionProtocol.swift │ │ ├── TabType.swift │ │ ├── CustomToolbarButton.swift │ │ ├── SettingsAction.swift │ │ ├── ToolbarAction.swift │ │ ├── SettingsSubtab.swift │ │ ├── ToolbarMenu.swift │ │ ├── ToolbarGroup.swift │ │ ├── ArrayBuilder.swift │ │ └── SettingsTab.swift │ ├── ViewModel │ └── SettingsModel.swift │ └── Components │ └── SettingsKitScene.swift ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── swiftlint.yml │ └── docs.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── Package.swift ├── LICENSE.md ├── README.md ├── CONTRIBUTING.md └── .swiftlint.yml /Icons/GitHubBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-swift/SettingsKit-macOS/HEAD/Icons/GitHubBanner.png -------------------------------------------------------------------------------- /Icons/GitHubBanner.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-swift/SettingsKit-macOS/HEAD/Icons/GitHubBanner.pxd -------------------------------------------------------------------------------- /Icons/SettingsKitIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-swift/SettingsKit-macOS/HEAD/Icons/SettingsKitIcon.png -------------------------------------------------------------------------------- /Icons/SettingsKitIcon.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-swift/SettingsKit-macOS/HEAD/Icons/SettingsKitIcon.pxd -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/Resources/DefaultDesign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-swift/SettingsKit-macOS/HEAD/Sources/SettingsKit/SettingsKit.docc/Resources/DefaultDesign.png -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/Resources/SidebarDesign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-swift/SettingsKit-macOS/HEAD/Sources/SettingsKit/SettingsKit.docc/Resources/SidebarDesign.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/ 8 | .netrc 9 | /Package.resolved 10 | .Ulysses-Group.plist 11 | /.docc-build -------------------------------------------------------------------------------- /Tests/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/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 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/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 | -------------------------------------------------------------------------------- /Tests/SettingsKitTests/SettingsKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsKitTests.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 19.01.23. 6 | // 7 | 8 | @testable import SettingsKit 9 | import XCTest 10 | 11 | /// Tests for the ``SettingsKit``. 12 | final class SettingsKitTests: XCTestCase { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/Examples.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/TestApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swiftlintplugin", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/lukepistrol/SwiftLintPlugin", 7 | "state" : { 8 | "revision" : "b1090ecd269dddd96bda0df24ca3f1aa78f33578", 9 | "version" : "0.52.4" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/SwiftUI/KeyboardShortcut.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardShortcut.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 21.01.23. 6 | // 7 | // swiftlint:disable string_literals 8 | 9 | import SwiftUI 10 | 11 | extension KeyboardShortcut { 12 | 13 | /// The keyboard shortcut for the settings button command. 14 | internal static var settings: Self { .init(",") } 15 | 16 | } 17 | 18 | // swiftlint:enable string_literals 19 | -------------------------------------------------------------------------------- /Tests/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "240535a599fed89b02f5807b6c07942dffcfdbb00d09be349fc00eb7bbac18f4", 3 | "pins" : [ 4 | { 5 | "identity" : "swiftlintplugin", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/lukepistrol/SwiftLintPlugin", 8 | "state" : { 9 | "revision" : "b1090ecd269dddd96bda0df24ca3f1aa78f33578", 10 | "version" : "0.52.4" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/SettingsWindowDesign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsWindowDesign.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 17.09.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The design of the settings window can be either the default macOS settings window design or a modern sidebar design. 11 | public enum SettingsWindowDesign { 12 | 13 | /// The default macOS settings design with a tab bar. 14 | case `default` 15 | /// A modern settings design with a sidebar. 16 | @available(macOS 13, *) 17 | case sidebar 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/SettingsKit.md: -------------------------------------------------------------------------------- 1 | # ``SettingsKit`` 2 | 3 | _SettingsKit_ is a library making it easier to add a settings window to your macOS app. 4 | 5 | ## Overview 6 | 7 | You can add a settings window to an existing scene using the ``SwiftUI/Scene/settings(design:symbolVariant:preferredColorScheme:selectedTab:_:)`` modifier. 8 | 9 | Find a tutorial covering all the steps under . 10 | 11 | ## Topics 12 | 13 | ### Articles 14 | 15 | - 16 | - 17 | - 18 | - 19 | - 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Steps 2 | - [ ] Add your name or username and a link to your GitHub profile into the [Contributors.md][1] file. 3 | - [ ] Build the project on your machine. If it does not compile, fix the errors. 4 | - [ ] Describe the purpose and approach of your pull request below. 5 | - [ ] Submit the pull request. Thank you very much for your contribution! 6 | 7 | ## Purpose 8 | _Describe the problem or feature._ 9 | _If there is a related issue, add the link._ 10 | 11 | ## Approach 12 | _Describe how this pull request solves the problem or adds the feature._ 13 | 14 | [1]: /Contributors.md -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/ToolbarActionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarActionProtocol.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.06.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A protocol for items in a custom or freeform toolbar's group. 11 | public protocol ToolbarActionProtocol { 12 | 13 | /// The toolbar item's identifier. 14 | var id: UUID { get } 15 | /// Whether the toolbar item is activated (background visible). 16 | var isOn: Bool { get } 17 | /// The body view. 18 | /// - Parameter padding: Padding. 19 | /// - Returns: The view. 20 | func body(padding: Edge.Set) -> AnyView 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/Double.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | // swiftlint:disable no_magic_numbers 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | 13 | /// The opacity of the white layer on a ``SettingsImageButtonStyle`` button when the button is not pressed. 14 | internal static var notPressedOpacity: Self { 0.7 } 15 | /// The opacity of the white layer on a ``SettingsImageButtonStyle`` button when the button is pressed. 16 | internal static var isPressedOpacity: Self { 0.4 } 17 | 18 | } 19 | 20 | // swiftlint:enable no_magic_numbers 21 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | // swiftlint:disable string_literals 8 | 9 | import SwiftUI 10 | 11 | extension String { 12 | 13 | /// The selector for the settings window. 14 | static var settingsWindowSelector: Self { "showSettingsWindow:" } 15 | /// The identifier for the settings window. 16 | public static var settingsWindowIdentifier: Self { "com_apple_SwiftUI_Settings_window" } 17 | /// The identifier for the selected subtabs in the user defaults. 18 | static var selectedSubtabs: Self { "selected_subtabs" } 19 | 20 | } 21 | 22 | // swiftlint:enable string_literals 23 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 17.09.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// The main window only displaying a "Hello, world!" screen. 11 | /// Navigate to "Examples" > "Settings..." to open the settings. 12 | struct ContentView: View { 13 | 14 | /// The view's body. 15 | var body: some View { 16 | VStack { 17 | Image(systemName: "globe") 18 | .imageScale(.large) 19 | .foregroundStyle(.tint) 20 | .accessibilityHidden(true) 21 | Text("Hello, world!") 22 | } 23 | .padding() 24 | } 25 | 26 | } 27 | 28 | #Preview { 29 | ContentView() 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | push: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | pull_request: 10 | paths: 11 | - '.github/workflows/swiftlint.yml' 12 | - '.swiftlint.yml' 13 | - '**/*.swift' 14 | workflow_dispatch: 15 | paths: 16 | - '.github/workflows/swiftlint.yml' 17 | - '.swiftlint.yml' 18 | - '**/*.swift' 19 | 20 | jobs: 21 | SwiftLint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: SwiftLint 26 | uses: norio-nomura/action-swiftlint@3.2.1 27 | with: 28 | args: --strict 29 | env: 30 | WORKING_DIRECTORY: Source 31 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/TabType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabType.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// The type of a settings tab or subtab. 11 | public enum TabType { 12 | 13 | /// A new settings tab or subtab with a label. 14 | case new(title: String, image: Image) 15 | /// An extension for an existing settings tab or subtab. 16 | case extend(id: String) 17 | /// A settings subtab that is selected if nothing else is selected, or a hidden settings tab. 18 | case noSelection 19 | 20 | /// Whether the tab type is ``noSelection``. 21 | var isNoSelection: Bool { 22 | switch self { 23 | case .noSelection: 24 | return true 25 | default: 26 | return false 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/CustomToolbarButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomToolbarButton.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.06.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// The button style of a custom toolbar action button. 11 | struct CustomToolbarButton: ButtonStyle { 12 | 13 | /// Create a button from a configuration. 14 | /// - Parameter configuration: The button configuration. 15 | /// - Returns: A view containing the button. 16 | func makeBody(configuration: Configuration) -> some View { 17 | let scale = 0.6 18 | return configuration.label 19 | .foregroundColor(configuration.isPressed ? .accentColor : .primary) 20 | .scaleEffect(configuration.isPressed ? scale : 1) 21 | .animation(.default, value: configuration.isPressed) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Tests/TestApp/xcodebuild.log: -------------------------------------------------------------------------------- 1 | Command line invocation: 2 | /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -scheme SettingsKit 3 | 4 | User defaults from command line: 5 | IDEPackageSupportUseBuiltinSCM = YES 6 | 7 | Resolve Package Graph 8 | 9 | Fetching from https://github.com/lukepistrol/SwiftLintPlugin (cached) 10 | 11 | Fetching from https://github.com/SFSafeSymbols/SFSafeSymbols (cached) 12 | 13 | Cloning local copy of package ‘SwiftLintPlugin’ 14 | 15 | Checking out main of package ‘SwiftLintPlugin’ 16 | 17 | Cloning local copy of package ‘SFSafeSymbols’ 18 | 19 | Checking out 4.1.0 of package ‘SFSafeSymbols’ 20 | 21 | 22 | Resolved source packages: 23 | SwiftLintPlugin: https://github.com/lukepistrol/SwiftLintPlugin @ main 24 | SettingsKit: /Users/david/Documents/Code/PigeonApp/SettingsKit 25 | SFSafeSymbols: https://github.com/SFSafeSymbols/SFSafeSymbols @ 4.1.0 26 | 27 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/View/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | 8 | import SettingsKit 9 | import SwiftUI 10 | 11 | /// The main view of the test app. 12 | struct ContentView: View { 13 | 14 | /// The body of the ``ContentView``. 15 | var body: some View { 16 | if #available(macOS 14, *) { 17 | SettingsLink() 18 | .labelStyle(.titleOnly) 19 | } else { 20 | // swiftlint:disable string_literals 21 | Button("Show Settings") { 22 | SettingsAction.showSettings() 23 | } 24 | // swiftlint:enable string_literals 25 | } 26 | } 27 | 28 | } 29 | 30 | /// Previews for the ``ContentView``. 31 | struct ContentView_Previews: PreviewProvider { 32 | 33 | /// Previews for the ``ContentView``. 34 | static var previews: some View { 35 | ContentView() 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SettingsKit/ViewModel/SettingsModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsModel.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 21.01.23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class containing all the information about the settings window state. 11 | final class SettingsModel: ObservableObject { 12 | 13 | /// A shared instance of the ``SettingsModel``. 14 | static var shared = SettingsModel() 15 | 16 | /// The identifier of the selected tab. 17 | @Published var selectedTab: String 18 | /// The identifiers of the selected subtabs. 19 | @Published var selectedSubtabs: [String: String] { 20 | didSet { 21 | UserDefaults.standard.set(selectedSubtabs, forKey: .selectedSubtabs) 22 | } 23 | } 24 | 25 | /// The initializer. It fetches the stored user defaults. 26 | init() { 27 | selectedTab = .init() 28 | selectedSubtabs = UserDefaults.standard.value(forKey: .selectedSubtabs) as? [String: String] ?? [:] 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/CGFloat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 19.01.23. 6 | // 7 | // swiftlint:disable no_magic_numbers 8 | 9 | import Foundation 10 | 11 | extension CGFloat { 12 | 13 | /// The corner radius of the images. 14 | internal static var imageCornerRadius: Self { 5 } 15 | /// The width of a settings window. 16 | internal static var settingsWidth: Self { 800 } 17 | /// The height of a settings window. 18 | internal static var settingsHeight: Self { 500 } 19 | /// The minimum width of a sidebar in a settings tab. 20 | internal static var settingsSidebarWidth: Self { 200 } 21 | /// The minimum width of the content in a settings tab. 22 | internal static var settingsContentWidth: Self { 500 } 23 | /// The height of a custom toolbar. 24 | internal static var customToolbarHeight: Self { 40 } 25 | /// The padding of the actions in the sidebar. 26 | internal static var actionsPadding: Self { 10 } 27 | 28 | } 29 | 30 | // swiftlint:enable no_magic_numbers 31 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // 3 | // Package.swift 4 | // SettingsKit 5 | // 6 | // Created by david-swift on 19.01.23. 7 | // 8 | 9 | import PackageDescription 10 | 11 | /// The SettingsKit package. 12 | let package = Package( 13 | name: "SettingsKit", 14 | platforms: [ 15 | .macOS(.v11) 16 | ], 17 | products: [ 18 | .library( 19 | name: "SettingsKit", 20 | targets: ["SettingsKit"] 21 | ) 22 | ], 23 | dependencies: [ 24 | .package(url: "https://github.com/lukepistrol/SwiftLintPlugin", from: "0.2.2") 25 | ], 26 | targets: [ 27 | .target( 28 | name: "SettingsKit", 29 | plugins: [ 30 | .plugin(name: "SwiftLint", package: "SwiftLintPlugin") 31 | ] 32 | ), 33 | .testTarget( 34 | name: "SettingsKitTests", 35 | dependencies: ["SettingsKit"], 36 | plugins: [ 37 | .plugin(name: "SwiftLint", package: "SwiftLintPlugin") 38 | ] 39 | ) 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Something is not working as expected. 3 | title: Description of the bug 4 | labels: bug 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe the bug 10 | description: >- 11 | A clear and concise description of what the bug is. 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: To Reproduce 18 | description: >- 19 | Steps to reproduce the behavior. 20 | placeholder: | 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Expected behavior 31 | description: >- 32 | A clear and concise description of what you expected to happen. 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Additional context 39 | description: >- 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 david-swift 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 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: Description of the feature request 4 | labels: enhancement 5 | 6 | body: 7 | - type: input 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | validations: 12 | required: false 13 | 14 | - type: textarea 15 | attributes: 16 | label: Describe the solution you'd like 17 | placeholder: >- 18 | A clear and concise description of what you want to happen. 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: Describe alternatives you've considered 25 | placeholder: >- 26 | A clear and concise description of any alternative solutions or features you've considered. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | attributes: 32 | label: Additional context 33 | placeholder: >- 34 | Add any other context or screenshots about the feature request here. 35 | validations: 36 | required: true 37 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "border-radius": "10px", 4 | "button": { 5 | "border-radius": "20px" 6 | }, 7 | "color": { 8 | "button-background": "#3490EC", 9 | "button-background-active": "#3490EC", 10 | "button-background-hover": "#5688FD", 11 | "button-text": "#ffffff", 12 | "header": "#323F80", 13 | "documentation-intro-accent": "var(--color-header)", 14 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)", 15 | "link": "#3490EC", 16 | "nav-link-color": "#3490EC", 17 | "nav-dark-link-color": "#3490EC", 18 | "tabnav-item-border-color": "#3490EC", 19 | "fill-light-blue-secondary": "#3490EC", 20 | "fill-blue": "#3490EC", 21 | "figure-blue": "#3490EC", 22 | "standard-blue-documentation-intro-fill": "#3490EC", 23 | "figure-blue": "#3490EC", 24 | "navigator-item-hover": { 25 | "light": "#3490EC15", 26 | "dark": "#2B466F" 27 | } 28 | }, 29 | "additionalProperties": "#3490EC" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/AccountView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountView.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 17.09.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// The settings test view for accounts. 11 | struct AccountView: View { 12 | 13 | /// The account number. 14 | var account: Int 15 | 16 | // swiftlint:disable no_magic_numbers 17 | /// The view's body. 18 | var body: some View { 19 | VStack(alignment: .center) { 20 | Image(systemName: "person.fill") 21 | .font(.system(size: 100)) 22 | .bold() 23 | .foregroundStyle(.white) 24 | .shadow(radius: 20) 25 | .padding() 26 | .background { 27 | RoundedRectangle(cornerRadius: 30) 28 | .foregroundStyle(.blue.gradient) 29 | .shadow(radius: 10) 30 | .aspectRatio(contentMode: .fit) 31 | } 32 | .accessibilityHidden(true) 33 | Text("Account \(account + 1)") 34 | .font(.title) 35 | .bold() 36 | .padding() 37 | } 38 | } 39 | // swiftlint:enable no_magic_numbers 40 | 41 | } 42 | 43 | #Preview { 44 | AccountView(account: 0) 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/Usage/SidebarDesign.md: -------------------------------------------------------------------------------- 1 | # The sidebar design 2 | 3 | It’s possible to activate a design with a sidebar for navigation using `settings(design: .sidebar) { }` instead of `.settings { }`. There are some modifiers that only have an effect when using the sidebar design. 4 | 5 | ## Add views above or below the list of subtabs 6 | When using the sidebar design, it's possible to add views above the list of subtabs using the ``SettingsTab/top(_:)`` or below the list using the ``SettingsTab/bottom(_:)`` modifier on the settings tab: 7 | ```swift 8 | SettingsTab(.new(title: "Accounts", icon: .at), id: "accounts") { 9 | // Subtabs 10 | } 11 | .top { 12 | // View 13 | } 14 | ``` 15 | 16 | ## Toolbar items 17 | Adding toolbar items to the content view of a subtab with the default macOS design results in buggy behavior. This isn't the case for the sidebar design. It is possible to use toolbars for e.g. modifying or deleting items (an example can be found in the sample app under the "Accounts" tab when activating the sidebar design). 18 | 19 | ## Disable the automatic selection of subtabs 20 | Disable the automatic selection of subtabs with the following syntax: 21 | ```swift 22 | SettingsTab(.new(title: "Accounts", icon: .at), id: "accounts") { 23 | // Subtabs 24 | } 25 | .automaticSubtabSelection(false) 26 | ``` 27 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/Usage/Actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | Actions are buttons, menus and other views below the list of subtabs. They “belong” to the subtabs which means that those actions should affect the subtabs. An important difference between the default and sidebar design is that in the default design, the actions are visible while a subtab is selected, which isn’t the case with the sidebar design. 4 | 5 | ## Create custom actions 6 | 1. Add the actions modifier to your settings tab: 7 | ```swift 8 | SettingsTab(.new(title: "Accounts", icon: .at), id: "accounts") { 9 | // Subtabs 10 | } 11 | .actions { 12 | // Actions 13 | } 14 | ``` 15 | 2. Actions are grouped into toolbar groups. Toolbar groups are visually separated from the other groups. They can contain toolbar elements and custom views: 16 | ```swift 17 | ToolbarGroup { 18 | // Toolbar Elements 19 | } body: { 20 | // Custom Views 21 | } 22 | ``` 23 | 3. You should use toolbar elements instead of custom views wherever possible. There are two types of toolbar elements: 24 | - ``ToolbarAction`` results in a button 25 | - ``ToolbarMenu`` is a menu 26 | 27 | ## Standard actions 28 | For the most common case with a “+” button or menu, “-“ button and optionally an ellipsis button, there are modifiers available (``SettingsTab/standardActions(add:remove:options:)-57w0k``) that directly add those to a settings tab. 29 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | concurrency: 13 | group: "pages" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | Deploy: 18 | environment: 19 | name: github-pages 20 | url: ${{ steps.deployment.outputs.page_url }} 21 | runs-on: macos-14 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Build Docs 25 | run: | 26 | xcrun xcodebuild docbuild \ 27 | -scheme SettingsKit \ 28 | -destination 'generic/platform=macOS' \ 29 | -derivedDataPath "$PWD/.derivedData" \ 30 | -skipPackagePluginValidation 31 | xcrun docc process-archive transform-for-static-hosting \ 32 | "$PWD/.derivedData/Build/Products/Debug/SettingsKit.doccarchive" \ 33 | --output-path "docs" \ 34 | --hosting-base-path "SettingsKit-macOS" 35 | - name: Modify Docs 36 | run: | 37 | echo "" > docs/index.html; 38 | sed -i '' 's/,2px/,10px/g' docs/css/index.038e887c.css 39 | - name: Upload Artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: 'docs' 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v4 46 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/Usage/AddSettingsWindow.md: -------------------------------------------------------------------------------- 1 | # Add a settings window 2 | 3 | Adding a settings window to your existing macOS app written in SwiftUI is very simple. 4 | 5 | ## Add a settings window 6 | 1. Add the following import to the file calling your app’s main scene (by default, it’s in the `[AppName]App.swift` file): 7 | ```swift 8 | import SettingsKit 9 | ``` 10 | 2. Add the ``SwiftUI/Scene/settings(design:symbolVariant:preferredColorScheme:selectedTab:_:)`` modifier to your scene, in this example `WindowGroup`: 11 | ```swift 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | .settings { 17 | SettingsTab(.new(title: "Test", icon: .app), id: "test") { 18 | SettingsSubtab(.noSelection, id: "no-selection") { 19 | Text("Test Tab") 20 | } 21 | } 22 | } 23 | } 24 | ``` 25 | The content of the modifier adds a settings tab called “Test” which contains a single subtab with a simple SwiftUI view as the content. 26 | 27 | ## Choose a design 28 | There are two designs available: the default macOS design and the sidebar design. You can switch to the sidebar design by replacing the line `settings {` with `settings(design: .sidebar) {`. 29 | 30 | ## Change the appearance of icons 31 | With the `symbolVariant` parameter, it’s possible to change the look of the icons. When using the sidebar design, the icons are always filled. 32 | 33 | ## Observe and change the selected tab 34 | Pass a binding to the `selectedTab` parameter that will sync with the selected tab. Changes you make to the binding will affect the selected tab, and changes the user makes via the UI will change your binding. 35 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | _SettingsKit_ is a SwiftUI library for adding a settings window to a macOS app. A settings window has got several _settings tabs_ which contain one or more _settings subtabs_. 4 | 5 | ## Design overview 6 | 7 | In the default macOS settings window design with a tab bar, the subtabs are displayed in a small sidebar. If there is only one subtab and there are no buttons in the sidebar, the sidebar is not visible. 8 | 9 | ![A settings window in the default design](DefaultDesign.png) 10 | 11 | In the settings window design with a sidebar, the subtabs are listed in the main view. With a click on the subtab in the list, one can view the details of the subtab. If there is only one subtab and no buttons in the sidebar, the detail view of that subtab is directly the child of the tab. 12 | 13 | ![A settings window in the sidebar design](SidebarDesign.png) 14 | 15 | ## Installation 16 | ### Swift package 17 | 1. Open your Swift package in Xcode. 18 | 2. Navigate to `File > Add Packages`. 19 | 3. Paste this URL into the search field: `https://github.com/david-swift/SettingsKit-macOS` 20 | 4. Click on `Copy Dependency`. 21 | 5. Navigate to the `Package.swift` file. 22 | 6. In the `Package` initializer, under `dependencies`, paste the dependency into the array. 23 | 24 | ###  Xcode project 25 | 1. Open your Xcode project in Xcode. 26 | 2. Navigate to `File > Add Packages`. 27 | 3. Paste this URL into the search field: `https://github.com/david-swift/SettingsKit-macOS` 28 | 4. Click on `Add Package`. 29 | 30 | ## Development 31 | SettingsKit is an open source project. Visit the [GitHub repository][1] for bug reports, feature requests, pull requests and more information. 32 | 33 | [1]: https://github.com/david-swift/SettingsKit-macOS 34 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/SettingsAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsAction.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 21.01.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Actions for interacting with SettingsKit. 11 | public enum SettingsAction { 12 | 13 | /// The shared instance of the settings model. 14 | static var settingsModel: SettingsModel { .shared } 15 | 16 | // swiftlint:disable string_literals 17 | /// Show the settings window. 18 | /// 19 | /// This function does no longer work in macOS 14 or higher. 20 | /// Please use the `SettingsLink` view for adding a button for opening the settings. 21 | /// 22 | /// - Parameters: 23 | /// - tab: The identifier of the new tab selection. 24 | /// - subtab: The identifier of the new subtab selection. 25 | @available( 26 | macOS, 27 | deprecated: 14, 28 | message: """ 29 | This function does no longer work in macOS 14 or higher. 30 | Please use the "SettingsLink" SwiftUI view for adding a button for opening the settings. 31 | """ 32 | ) 33 | public static func showSettings(tab: String? = nil, subtab: String? = nil) { 34 | if let tab { 35 | settingsModel.selectedTab = tab 36 | if let subtab { 37 | settingsModel.selectedSubtabs[tab] = subtab 38 | } 39 | } 40 | NSApplication.shared.sendAction(.init((.settingsWindowSelector)), to: nil, from: nil) 41 | } 42 | // swiftlint:enable string_literals 43 | 44 | /// Get the selected tab and subtab selection. 45 | /// - Returns: The selected tab and the selected subtabs of all the tabs. 46 | public static func getSelection() -> (String, [String: String]) { 47 | (settingsModel.selectedTab, settingsModel.selectedSubtabs) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/TestAppApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAppApp.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | 8 | import SettingsKit 9 | import SwiftUI 10 | 11 | /// A simple app for testing the `SettingsKit`. 12 | @main 13 | struct TestAppApp: App { 14 | 15 | /// The app model for testing the ``SettingsKit``. 16 | @StateObject private var appModel = TestAppModel.shared 17 | /// Whether the sidebar design should be used for the settings window. 18 | @AppStorage("sidebar-design") 19 | var sidebarDesign = false 20 | /// The currently selected tab. 21 | @AppStorage("tab") 22 | var selectedTab = "" 23 | /// The preferred color scheme. 24 | @AppStorage("scheme") 25 | var forceDark = false 26 | 27 | /// The main view of the test app. 28 | var body: some Scene { 29 | Window("Window", id: "Window") { 30 | ContentView() 31 | Button("Select Test") { 32 | selectedTab = "test" 33 | } 34 | } 35 | .settings( 36 | design: sidebarDesign ? .sidebar : .default, 37 | preferredColorScheme: forceDark ? .dark : nil, 38 | selectedTab: $selectedTab 39 | ) { 40 | for settingsTab in appModel.allSettings { 41 | settingsTab 42 | } 43 | SettingsTab(.new(title: "Test", image: .init(systemName: "arrow.left.circle")), id: "test") { 44 | SettingsSubtab(.noSelection, id: "subtab") { 45 | let width = 500.0 46 | let height = 100.0 47 | Form { 48 | Toggle("Sidebar Design", isOn: $sidebarDesign) 49 | Toggle("Force Dark Design", isOn: $forceDark) 50 | } 51 | .frame(width: width, height: height) 52 | } 53 | } 54 | .frame() 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/ToolbarAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarAction.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.06.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A button in a custom toolbar. 11 | public struct ToolbarAction: ToolbarActionProtocol, Identifiable { 12 | 13 | /// The identifier of the toolbar action. 14 | public let id: UUID 15 | /// The icon of the toolbar action. 16 | var icon: Image 17 | /// The action's title. 18 | var title: LocalizedStringKey 19 | /// The action. 20 | var action: () -> Void 21 | /// Whether the toggle is activated. 22 | public var isOn: Bool 23 | 24 | /// The initializer. 25 | /// - Parameters: 26 | /// - title: The action's title. 27 | /// - symbol: The SF symbol. 28 | /// - action: The action's description. 29 | public init(_ title: LocalizedStringKey, symbol: Image, action: @escaping () -> Void) { 30 | id = .init() 31 | self.icon = symbol 32 | self.title = title 33 | self.action = action 34 | isOn = false 35 | } 36 | 37 | /// The action's view. 38 | /// - Parameter padding: The horizontal padding around the button. 39 | /// - Returns: A view containing the action button. 40 | public func body(padding: Edge.Set) -> AnyView { 41 | .init( 42 | Button { 43 | action() 44 | } label: { 45 | Label { 46 | Text(title) 47 | } icon: { 48 | icon 49 | } 50 | .customToolbarItem(padding: padding) 51 | } 52 | .buttonStyle(CustomToolbarButton()) 53 | .help(title) 54 | ) 55 | } 56 | 57 | /// Add a binding to convert this action into a toggle. 58 | /// - Parameter isOn: Whether the toggle is on. 59 | /// - Returns: The toolbar action as a toggle. 60 | public func isOn(_ isOn: Bool) -> Self { 61 | var newSelf = self 62 | newSelf.isOn = isOn 63 | return newSelf 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/SettingsSubtab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsSubtab.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A tab in a settings tab. 11 | public struct SettingsSubtab: Identifiable, View { 12 | 13 | /// The tab's identifier. 14 | public let id: String 15 | /// The tab's type. 16 | public var type: TabType 17 | /// The tab's view. 18 | public var content: any View 19 | /// The tab's color. 20 | public var color: Color 21 | /// Whether the subtab is the standard tab. 22 | var standard = false 23 | 24 | /// The view's body. 25 | /// It is an AnyView wrapped around the ``content``. 26 | public var body: some View { 27 | AnyView( 28 | content 29 | ) 30 | } 31 | 32 | /// The label of a custom tab, or else nil. 33 | public var label: Label? { 34 | guard case let .new(title: title, image: image) = type else { 35 | return nil 36 | } 37 | return .init { 38 | Text(title) 39 | } icon: { 40 | image 41 | } 42 | } 43 | 44 | /// The label for the sidebar style. 45 | @ViewBuilder public var sidebarLabel: some View { 46 | if case let .new(title: title, image: image) = type { 47 | HStack { 48 | image 49 | .sidebarSettingsIcon(color: color) 50 | .accessibilityHidden(true) 51 | Text(title) 52 | } 53 | } 54 | } 55 | 56 | /// The initializer. 57 | /// - Parameters: 58 | /// - type: The tab type of the settings subtab. 59 | /// - id: The identifier. 60 | /// - color: The tab's color in the sidebar style. 61 | /// - content: The content of the settings subtab. 62 | public init(_ type: TabType, id: String, color: Color = .blue, @ViewBuilder content: () -> any View) { 63 | self.id = id 64 | self.type = type 65 | self.color = color 66 | self.content = content() 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/GeneralSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralSettings.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 17.09.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// The settings test view for general settings. 11 | struct GeneralSettings: View { 12 | 13 | /// A test storage value. 14 | @AppStorage("show-first-name") 15 | var firstNameBeforeLastName = true 16 | /// A test storage value. 17 | @AppStorage("sort-by") 18 | var sortByLastName = true 19 | /// A test storage value. 20 | @AppStorage("prefer-nicknames") 21 | var preferNicknames = true 22 | /// A test storage value. 23 | @AppStorage("show-siri-suggestions") 24 | var showSiriSuggestions = true 25 | /// A test storage value which switches between the default and sidebar settings design. 26 | @AppStorage("default-settings-design") 27 | var defaultSettingsDesign = true 28 | 29 | /// The view's body. 30 | var body: some View { 31 | let width = 500.0 32 | Form { 33 | Picker("Show First Name:", selection: $firstNameBeforeLastName) { 34 | Text("Before last name") 35 | .tag(true) 36 | Text("Following last name") 37 | .tag(false) 38 | } 39 | .pickerStyle(.radioGroup) 40 | Picker("Sort By:", selection: $sortByLastName) { 41 | Text("Last Name") 42 | .tag(true) 43 | Text("First Name") 44 | .tag(false) 45 | } 46 | Toggle("Prefer nicknames", isOn: $preferNicknames) 47 | Toggle("Show Siri Suggestions", isOn: $showSiriSuggestions) 48 | Section { 49 | Picker("Settings Window Design:", selection: $defaultSettingsDesign) { 50 | Text("Default macOS Design") 51 | .tag(true) 52 | Text("Sidebar Design") 53 | .tag(false) 54 | } 55 | .pickerStyle(.radioGroup) 56 | } 57 | } 58 | .frame(width: width) 59 | .fixedSize() 60 | .padding() 61 | } 62 | } 63 | 64 | #Preview { 65 | GeneralSettings() 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | SettingsKit Icon 3 |

SettingsKit

4 |

5 | 6 |

7 | 8 | Documentation 9 | 10 | · 11 | 12 | GitHub 13 | 14 | 15 |

16 | 17 | _SettingsKit_ makes it easier to add a settings window to a SwiftUI app for macOS. 18 | 19 | ![GitHub Banner][image-1] 20 | 21 | ## Table of Contents 22 | 23 | - [Installation][1] 24 | - [Usage][2] 25 | - [Thanks][3] 26 | 27 | ## Installation 28 | 29 | ### Swift Package 30 | 1. Open your Swift package in Xcode. 31 | 2. Navigate to `File > Add Packages`. 32 | 3. Paste this URL into the search field: `https://github.com/david-swift/SettingsKit-macOS` 33 | 4. Click on `Copy Dependency`. 34 | 5. Navigate to the `Package.swift` file. 35 | 6. In the `Package` initializer, under `dependencies`, paste the dependency into the array. 36 | 37 | ###  Xcode Project 38 | 1. Open your Xcode project in Xcode. 39 | 2. Navigate to `File > Add Packages`. 40 | 3. Paste this URL into the search field: `https://github.com/david-swift/SettingsKit-macOS` 41 | 4. Click on `Add Package`. 42 | 43 | ## Usage 44 | 45 | An example app project is available [here.][4] Browse the documentation [here.](https://david-swift.github.io/SettingsKit-macOS/) 46 | 47 | ## Thanks 48 | 49 | ### Dependencies 50 | - [SwiftLintPlugin][12] licensed under the [MIT license][13] 51 | 52 | ### Other Thanks 53 | - [SwiftLint][19] for checking whether code style conventions are violated 54 | - The programming language [Swift][20] 55 | 56 | [1]: #installation 57 | [2]: #usage 58 | [3]: #thanks 59 | [4]: /Tests/Examples/ 60 | [10]: https://github.com/SFSafeSymbols/SFSafeSymbols 61 | [11]: https://github.com/SFSafeSymbols/SFSafeSymbols/blob/stable/LICENSE 62 | [12]: https://github.com/lukepistrol/SwiftLintPlugin 63 | [13]: https://github.com/lukepistrol/SwiftLintPlugin/blob/main/LICENSE 64 | [14]: https://github.com/david-swift/ColibriComponents-macOS 65 | [15]: https://github.com/david-swift/ColibriComponents-macOS/blob/main/LICENSE.md 66 | [18]: Documentation/Reference/SettingsKit-macOS/README.md 67 | [19]: https://github.com/realm/SwiftLint 68 | [20]: https://github.com/apple/swift 69 | 70 | [image-1]: Icons/GitHubBanner.png 71 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/ToolbarMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarMenu.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 02.08.2023. 6 | // Edited by Daniel Nguyen on 05.08.2023. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A button in a custom toolbar. 12 | public struct ToolbarMenu: ToolbarActionProtocol where Content: View { 13 | 14 | /// The identifier of the toolbar menu. 15 | public let id: UUID 16 | /// The icon of the toolbar menu. 17 | var icon: Image 18 | /// The menu's title. 19 | var title: LocalizedStringKey 20 | /// The content. 21 | var content: Content 22 | /// A toolbar menu is never activated. 23 | public var isOn: Bool { false } 24 | 25 | /// The initializer. 26 | /// - Parameters: 27 | /// - title: The action's title. 28 | /// - symbol: The SF symbol. 29 | /// - content: The content. 30 | public init(_ title: LocalizedStringKey, symbol: Image, @ViewBuilder content: @escaping () -> Content) { 31 | id = .init() 32 | self.icon = symbol 33 | self.title = title 34 | self.content = content() 35 | } 36 | 37 | /// The action's view. 38 | /// - Parameter padding: The horizontal padding around the button. 39 | /// - Returns: A view containing the action button. 40 | public func body(padding: Edge.Set) -> AnyView { 41 | let width = 20.0 42 | let paddingValue = 5.0 43 | if #available(macOS 12, *) { 44 | return .init( 45 | Menu { 46 | content 47 | } label: { 48 | Label { 49 | Text(title) 50 | } icon: { 51 | icon 52 | } 53 | .customToolbarItem(padding: padding) 54 | .border(.red) 55 | } 56 | .menuStyle(.borderlessButton) 57 | .menuIndicator(.hidden) 58 | .frame(width: width) 59 | .padding(padding, paddingValue) 60 | .buttonStyle(CustomToolbarButton()) 61 | .help(title) 62 | ) 63 | } else { 64 | return .init( 65 | MenuButton(title) { 66 | content 67 | } 68 | .buttonStyle(.borderless) 69 | ) 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.06.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Array { 11 | 12 | /// Accesses the element at the specified position safely. 13 | /// - Parameters: 14 | /// - index: The position of the element to access. 15 | /// 16 | /// Access and set elements the safe way: 17 | /// ```swift 18 | /// var array = ["Hello", "World"] 19 | /// print(array[safe: 2] ?? "Out of range") 20 | /// ``` 21 | public subscript(safe index: Int?) -> Element? { 22 | get { 23 | if let index, checkIndex(index) { 24 | return self[index] 25 | } 26 | return nil 27 | } 28 | set { 29 | if let index, let value = newValue, checkIndex(index) { 30 | self[index] = value 31 | } 32 | } 33 | } 34 | 35 | /// Check if a given index is valid for the array. 36 | /// - Parameter index: The index to test. 37 | /// - Returns: Return whether the index is valid or not. 38 | private func checkIndex(_ index: Int) -> Bool { 39 | index < count && index >= 0 40 | } 41 | 42 | } 43 | 44 | extension Array where Element: Identifiable { 45 | 46 | /// Accesses the element with a specified identifier safely. 47 | /// - Parameters: 48 | /// - id: The identifier of the element to access. 49 | public subscript(id id: Element.ID?) -> Element? { 50 | get { 51 | let index = getIndex(id: id) 52 | return self[safe: index] 53 | } 54 | set { 55 | let index = getIndex(id: id) 56 | self[safe: index] = newValue 57 | } 58 | } 59 | 60 | /// Get the index of an element with a specified identifier. 61 | /// - Parameter id: The element's identifier. 62 | /// - Returns: The index of the element. 63 | private func getIndex(id: Element.ID?) -> Int? { 64 | firstIndex { $0.id == id } 65 | } 66 | 67 | } 68 | 69 | extension Array: View where Element == ToolbarGroup { 70 | 71 | /// The toolbar. 72 | public var body: some View { 73 | let padding = 5.0 74 | let height = 20.0 75 | let offset = 2.0 76 | HStack { 77 | ForEach(self) { $0 } 78 | .frame(alignment: .leading) 79 | } 80 | .padding(padding) 81 | .frame(height: height) 82 | .offset(y: offset) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/SwiftUI/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 17.09.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | 12 | /// Get the view as an icon for the sidebar settings. 13 | public func sidebarSettingsIcon(color: Color) -> some View { 14 | let cornerRadius = 5.0 15 | let shadowRadius = 0.5 16 | let sideLength = 23.0 17 | let iconShadowRadius = 4.0 18 | let iconPadding = 4.0 19 | let rect = RoundedRectangle(cornerRadius: cornerRadius) 20 | .shadow(radius: shadowRadius) 21 | .aspectRatio(contentMode: .fit) 22 | .frame(width: sideLength, height: sideLength) 23 | @ViewBuilder var view: some View { 24 | if #available(macOS 13, *) { 25 | rect.foregroundStyle(color.gradient) 26 | } else { 27 | rect.foregroundColor(color) 28 | } 29 | } 30 | return ZStack { 31 | view 32 | let font = font(.body.bold()) 33 | .foregroundColor(.white) 34 | .shadow(radius: iconShadowRadius) 35 | .padding(iconPadding) 36 | if #available(macOS 12, *) { 37 | font.symbolVariant(.fill) 38 | } else { 39 | font 40 | } 41 | } 42 | } 43 | 44 | /// Style a view to fit into the custom toolbar. 45 | /// - Parameter visible: The visibility of the background. 46 | /// - Returns: The view with the background if visible is true, otherwise the view. 47 | public func customToolbarBackground(visible: Bool) -> some View { 48 | let opacity = 0.05 49 | let cornerRadius = 10.0 50 | return VStack { 51 | self 52 | RoundedRectangle(cornerRadius: cornerRadius) 53 | .foregroundColor(.secondary.opacity(visible ? opacity : 0)) 54 | } 55 | } 56 | 57 | /// Modifies the view for a custom toolbar item. 58 | /// - Parameter padding: The horizontal padding of the item. 59 | /// - Returns: A view containing the item. 60 | internal func customToolbarItem(padding: Edge.Set) -> some View { 61 | let sideLength = 20.0 62 | let paddingValue = 5.0 63 | return frame(width: sideLength, height: sideLength) 64 | .labelStyle(.iconOnly) 65 | .padding([.vertical, padding], paddingValue) 66 | .buttonStyle(.plain) 67 | .contentShape(Rectangle()) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/ToolbarGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarGroup.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.06.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A group of buttons in a custom toolbar. 11 | public struct ToolbarGroup: Identifiable, View { 12 | 13 | /// The identifier of the toolbar action group. 14 | public let id: UUID 15 | /// Actions in the group. 16 | var actions: [ToolbarActionProtocol] 17 | /// The content view. 18 | var content: AnyView? 19 | /// Whether there is a spacer after the group. 20 | var addSpacer: Bool 21 | /// Whether the toolbar group is hovered. If true, the background color changes. 22 | @State private var hover = false 23 | 24 | /// The group's view. 25 | public var body: some View { 26 | HStack { 27 | ForEach(actions, id: \.id) { action in 28 | let index = actions.firstIndex { $0.id == action.id } ?? 0 29 | if index != 0 { 30 | Divider() 31 | } 32 | action.body(padding: padding(index: index)) 33 | } 34 | if let content { 35 | if !actions.isEmpty { 36 | Divider() 37 | } 38 | let height = 30.0 39 | content 40 | .padding(.vertical) 41 | .frame(height: height) 42 | } 43 | } 44 | .onHover { hover = $0 } 45 | if addSpacer { 46 | Spacer() 47 | } 48 | } 49 | 50 | /// The initializer of ``ToolbarGroup``. 51 | /// - Parameters: 52 | /// - actions: The actions. 53 | /// - body: The body. 54 | public init( 55 | @ArrayBuilder _ actions: () -> [ToolbarActionProtocol] = { [] }, 56 | @ViewBuilder body: @escaping () -> Content? = { nil as EmptyView? } 57 | ) where Content: View { 58 | id = .init() 59 | self.actions = actions() 60 | addSpacer = false 61 | if let body = body() { 62 | content = .init(body) 63 | } else { 64 | content = nil 65 | } 66 | } 67 | 68 | /// Create a spacer after this action. 69 | /// - Returns: The action with the spacer. 70 | public func spacer() -> Self { 71 | var newAction = self 72 | newAction.addSpacer = true 73 | return newAction 74 | } 75 | 76 | /// Get the padding of the action button at a certain position. 77 | /// - Parameter index: The position. 78 | /// - Returns: A set containing the edges with padding. 79 | private func padding(index: Int) -> Edge.Set { 80 | if actions.count <= 1 { 81 | return .all 82 | } else if index == 0 { 83 | return .leading 84 | } else if index == actions.count - 1 { 85 | return .trailing 86 | } else { 87 | return [] 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Sources/SettingsKit/SettingsKit.docc/Usage/TabsAndSubtabs.md: -------------------------------------------------------------------------------- 1 | # Tabs & subtabs 2 | 3 | The settings modifier expects a list of settings tabs as its content. Settings tabs enable a top-level navigation to different settings topics. If another layer of navigation is required, for example when having a list of multiple accounts, one can use subtabs. Later, we’ll see how it is possible to add buttons for e.g. adding a new account to the list of subtabs. 4 | 5 | ## Tabs 6 | The settings tab initializer accepts three to four parameters. 7 | - Tab type: Either ``TabType/new(title:image:)`` with the tab’s title and icon for creating a new tab (almost always used), or ``TabType/extend(id:)`` in rare cases where you want to add subtabs to an already existing tab. Tabs with the type `.noSelection` are not rendered. 8 | - ID: A string that differs from the IDs of all of the other tabs. 9 | - Color (optional): This parameter only has an effect when using the sidebar settings style. It sets the background color of the icon in the sidebar. 10 | - Content: The subtabs in that tab. 11 | As an example, the code for a “General” tab could look like this: 12 | ```swift 13 | SettingsTab(.new(title: "General", icon: .gearshape), id: "general", color: .gray) { 14 | SettingsSubtab(.noSelection, id: "no-selection") { 15 | Text("General Settings") 16 | } 17 | } 18 | ``` 19 | The closure of the settings body supports many DSL features, such as `if`/`else` closures, `for` loops, etc. 20 | 21 | ### Change the width or height of a settings tab 22 | Normally, _SettingsKit_ uses a fixed width and height. That is not always desired. Find more information [here][1] on how to change that. 23 | 24 | ## Subtabs 25 | The settings subtab initializer accepts the same parameters as the settings tab initializer, but there are some differences: 26 | - Tab type: ``TabType/new(title:image:)``, ``TabType/extend(id:)`` or ``TabType/noSelection``. ``TabType/noSelection`` means that this subtab is selected if no other subtab is available. It is not visible in the list of subtabs. 27 | - ID: A string that differs from the IDs of all of the other subtabs. 28 | - Color (optional): This parameter only has an effect when using the sidebar settings style. It sets the background color of the icon in the subtabs list. 29 | - Content: A SwiftUI view which is displayed when this subtab is selected. In many cases, it might make sense to use SwiftUI’s `Form` structure inside of subtabs. When using the sidebar style, the grouped form style is set if you do not overwrite it. 30 | The closure of the settings tab supports many DSL features, mainly `for` loops might be important in some cases, e.g. a list of accounts where the user can add or remove items. 31 | The example with the accounts might look similar to that: 32 | ```swift 33 | SettingsTab(.new(title: "Accounts", icon: .at), id: "accounts") { 34 | for account in accounts { 35 | SettingsSubtab(.new(title: account.title, icon: .person), id: account.id) { 36 | AccountView(account) 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | One important feature is missing in the example above: Adding and removing accounts. This is the topic of the next section. 43 | 44 | [1]: https://github.com/david-swift/SettingsKit-macOS/issues/2#issuecomment-1627618096 45 | -------------------------------------------------------------------------------- /Tests/Examples/Examples/ExamplesApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExamplesApp.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 17.09.2023. 6 | // 7 | 8 | import SettingsKit 9 | import SwiftUI 10 | 11 | /// An app for showcasing the usage of the SettingsKit package. 12 | @main 13 | struct ExamplesApp: App { 14 | 15 | /// A test storage value. 16 | @State private var accounts = [0] 17 | /// A test storage value. 18 | @AppStorage("show-first-name") 19 | var firstNameBeforeLastName = true 20 | /// A test storage value. 21 | @AppStorage("default-settings-design") 22 | var defaultSettingsDesign = true 23 | 24 | /// The app's body. 25 | var body: some Scene { 26 | WindowGroup { 27 | ContentView() 28 | } 29 | .settings(design: defaultSettingsDesign ? .default : .sidebar) { 30 | SettingsTab(.new(title: "General", image: .init(systemName: "gearshape")), id: "general", color: .gray) { 31 | SettingsSubtab(.noSelection, id: "general") { GeneralSettings() } 32 | } 33 | .frame() 34 | if defaultSettingsDesign { 35 | accountsTab 36 | .standardActions { 37 | self.accounts.append((self.accounts.last ?? -1) + 1) 38 | } remove: { _, index in 39 | if let index { 40 | self.accounts.remove(at: index) 41 | } 42 | } 43 | } else { 44 | accountsTab.automaticSubtabSelection(false) 45 | } 46 | SettingsTab(.new(title: "Advanced", image: .init(systemName: "gearshape.2")), id: "advanced") { 47 | SettingsSubtab(.noSelection, id: "advanced") { 48 | VStack { 49 | Spacer() 50 | HStack { 51 | Spacer() 52 | Text("Advanced Settings") 53 | Spacer() 54 | } 55 | Spacer() 56 | } 57 | } 58 | } 59 | .frame() 60 | } 61 | } 62 | 63 | /// The sample "Accounts" tab. 64 | private var accountsTab: SettingsTab { 65 | .init(.new(title: "Accounts", image: .init(systemName: "at")), id: "accounts") { 66 | for account in self.accounts { 67 | SettingsSubtab( 68 | .new(title: "Account \(account + 1)", image: .init(systemName: "person.fill")), 69 | id: "account-\(account)" 70 | ) { 71 | if defaultSettingsDesign { 72 | AccountView(account: account) 73 | } else { 74 | AccountView(account: account) 75 | .toolbar { 76 | Button("Delete Account") { 77 | self.accounts = self.accounts.filter { $0 != account } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | .top { 85 | Section { 86 | HStack { 87 | Text("Accounts") 88 | Spacer() 89 | Button("Add Account") { 90 | self.accounts.append((self.accounts.last ?? -1) + 1) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/ArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayBuilder.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.06.24. 6 | // 7 | // Thanks to Jaden Geller for the GitHub Gist: 8 | // "ArrayBuilder.swift" 9 | // https://gist.github.com/JadenGeller/c375fc15ad5900a0ddac4ed8ba8307a9 10 | // 11 | 12 | import Foundation 13 | 14 | /// The ``ArrayBuilder`` is a simple result builder that outputs an array of any type. 15 | /// 16 | /// You can define any array using Swift's DSL: 17 | /// ```swift 18 | /// @ArrayBuilder var string: [String] { 19 | /// "Hello, " 20 | /// if bool { 21 | /// "world!" 22 | /// } else { 23 | /// "colibri!" 24 | /// } 25 | /// for x in 0...10 { 26 | /// "\nIteration Number \(x)" 27 | /// } 28 | /// } 29 | /// ``` 30 | @resultBuilder 31 | public enum ArrayBuilder { 32 | 33 | /// A component used in the ``ArrayBuilder``. 34 | public enum Component { 35 | 36 | /// An element as a component. 37 | case element(_: Element) 38 | /// An array of components as a component. 39 | case components(_: [Self]) 40 | 41 | } 42 | 43 | /// Build combined results from statement blocks. 44 | /// - Parameter elements: The components. 45 | /// - Returns: The components in a component. 46 | public static func buildBlock(_ elements: Component...) -> Component { 47 | .components(elements) 48 | } 49 | 50 | /// Translate an element into an ``ArrayBuilder/Component``. 51 | /// - Parameter element: The element to translate. 52 | /// - Returns: A component created from the element. 53 | public static func buildExpression(_ element: Element) -> Component { 54 | .element(element) 55 | } 56 | 57 | /// Fetch a component. 58 | /// - Parameter component: A component. 59 | /// - Returns: The component. 60 | public static func buildExpression(_ component: Component) -> Component { 61 | component 62 | } 63 | 64 | /// Enables support for `if` statements without an `else`. 65 | /// - Parameter component: An optional component. 66 | /// - Returns: A nonoptional component. 67 | public static func buildOptional(_ component: Component?) -> Component { 68 | component ?? .components([]) 69 | } 70 | 71 | /// Enables support for `if`-`else` and `switch` statements. 72 | /// - Parameter component: A component. 73 | /// - Returns: The component. 74 | public static func buildEither(first component: Component) -> Component { 75 | component 76 | } 77 | 78 | /// Enables support for `if`-`else` and `switch` statements. 79 | /// - Parameter component: A component. 80 | /// - Returns: The component. 81 | public static func buildEither(second component: Component) -> Component { 82 | component 83 | } 84 | 85 | /// Enables support for `for..in` loops. 86 | /// - Parameter components: The components as a two dimensional array. 87 | /// - Returns: The components as a one dimensional array. 88 | public static func buildArray(_ components: [Component]) -> Component { 89 | .components(components) 90 | } 91 | 92 | /// Convert a component to an array of elements. 93 | /// - Parameter component: The component to convert. 94 | /// - Returns: The generated array of elements. 95 | public static func buildFinalResult(_ component: Component) -> [Element] { 96 | switch component { 97 | case let .element(element): 98 | return [element] 99 | case let .components(components): 100 | return components.flatMap { buildFinalResult($0) } 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp/ViewModel/TestAppModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAppModel.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 27.01.23. 6 | // 7 | 8 | import SettingsKit 9 | import SwiftUI 10 | 11 | /// The app model of the test app. 12 | class TestAppModel: ObservableObject { 13 | 14 | /// A shared instance of the ``TestAppModel``. 15 | static var shared = TestAppModel() 16 | 17 | /// The settings. 18 | @Published private var settings: [SettingsTab] 19 | 20 | /// All the settings: the "Settings" tab + the `settings`. 21 | @ArrayBuilder var allSettings: [SettingsTab] { 22 | SettingsTab( 23 | .new( 24 | title: "Settings", 25 | image: .init(systemName: "gearshape") 26 | ), 27 | id: "settings-tab" 28 | ) { 29 | for subtab in tabsGeneralSubtabs { 30 | subtab 31 | } 32 | noSelection 33 | } 34 | .standardActions { 35 | VStack { 36 | Button("Icon Tab") { 37 | self.settings.append(self.newTab) 38 | } 39 | Button("Color Tab") { 40 | self.settings.append(self.colorTab) 41 | } 42 | } 43 | } remove: { _, index in 44 | if let index { 45 | self.settings.remove(at: index) 46 | } 47 | } 48 | 49 | for settingsTab in settings { 50 | settingsTab 51 | } 52 | } 53 | 54 | /// The subtabs in the "Setttings" tab that represent the other tabs. 55 | @ArrayBuilder private var tabsGeneralSubtabs: [SettingsSubtab] { 56 | for settingsTab in settings { 57 | if case let .new(title: title, image: icon) = settingsTab.type { 58 | SettingsSubtab(.new(title: title, image: icon), id: settingsTab.id) { 59 | Label { 60 | Text(title) 61 | } icon: { 62 | icon 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /// The view when nothing is selected in the "Settings" tab. 70 | private var noSelection: SettingsSubtab { 71 | .init(.noSelection, id: "no-selection-settings") { 72 | Text( 73 | .init( 74 | "Add a new tab with the \"+\" button.", 75 | comment: "TestAppModel (Description of a view without a selection for testing purposes)" 76 | ) 77 | ) 78 | } 79 | } 80 | 81 | /// Generates a new settings tab. 82 | var newTab: SettingsTab { 83 | .init(.new(title: randomLabel.0, image: .init(systemName: randomLabel.1)), id: UUID().uuidString) { 84 | SettingsSubtab(.noSelection, id: "no-selection-subtab") { 85 | Label(randomLabel.0, systemImage: randomLabel.1) 86 | } 87 | } 88 | } 89 | 90 | /// Generates a new settings tab. 91 | var colorTab: SettingsTab { 92 | .init(.new(title: randomLabel.0, image: .init(systemName: randomLabel.1)), id: UUID().uuidString) { 93 | SettingsSubtab(.noSelection, id: "no-selection-subtab") { 94 | Color.accentColor 95 | } 96 | } 97 | } 98 | 99 | /// Generates a random label. 100 | private var randomLabel: (String, String) { 101 | ( 102 | "Test Tab", 103 | randomSymbol 104 | ) 105 | } 106 | 107 | /// Chooses a random SFSymbol. 108 | private var randomSymbol: String { 109 | Bool.random() ? "figure.walk" : "house" 110 | } 111 | 112 | /// The intializer. 113 | init() { 114 | settings = [] 115 | settings = [self.newTab] 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/SwiftUI/Scene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scene.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Scene { 11 | 12 | /// Adds the settings to a scene. 13 | /// - Parameters: 14 | /// - design: Whether the default or sidebar design is used. 15 | /// - symbolVariant: The way symbols should be displayed. 16 | /// - preferredColorScheme: Force either light or dark mode. 17 | /// - selectedTab: The currently selected tab. 18 | /// - settings: The settings tabs in the settings window. 19 | /// - Returns: The scene with the settings. 20 | /// 21 | /// Use it as a modifier for a scene: 22 | /// ```swift 23 | /// WindowGroup { 24 | /// ContentView() 25 | /// } 26 | /// .settings { 27 | /// SettingsTab(.init("General", systemSymbol: .gearshape), id: "general-tab") { 28 | /// GeneralTabContent() 29 | /// } 30 | /// } 31 | /// ``` 32 | @available(macOS, introduced: 12) 33 | public func settings( 34 | design: SettingsWindowDesign = .default, 35 | symbolVariant: SymbolVariants = .none, 36 | preferredColorScheme: ColorScheme? = nil, 37 | selectedTab: Binding? = nil, 38 | @ArrayBuilder _ settings: () -> [SettingsTab] 39 | ) -> some Scene { 40 | let (settings, standardID) = getSettings(settings()) 41 | return SettingsKitScene( 42 | content: self, 43 | settings: settings, 44 | standardID: standardID, 45 | symbolVariant: symbolVariant, 46 | design: design, 47 | colorScheme: preferredColorScheme, 48 | selectedTab: selectedTab 49 | ) 50 | } 51 | 52 | /// Adds the settings to a scene. 53 | /// - Parameters: 54 | /// - design: Whether the default or sidebar design is used. 55 | /// - preferredColorScheme: Force either light or dark mode. 56 | /// - selectedTab: The currently selected tab. 57 | /// - settings: The settings tabs in the settings window. 58 | /// - Returns: The scene with the settings. 59 | /// 60 | /// Use it as a modifier for a scene: 61 | /// ```swift 62 | /// WindowGroup { 63 | /// ContentView() 64 | /// } 65 | /// .settings { 66 | /// SettingsTab(.init("General", systemSymbol: .gearshape), id: "general-tab") { 67 | /// GeneralTabContent() 68 | /// } 69 | /// } 70 | /// ``` 71 | public func settings( 72 | design: SettingsWindowDesign = .default, 73 | preferredColorScheme: ColorScheme? = nil, 74 | selectedTab: Binding? = nil, 75 | @ArrayBuilder _ settings: () -> [SettingsTab] 76 | ) -> some Scene { 77 | let (settings, standardID) = getSettings(settings()) 78 | return SettingsKitScene( 79 | content: self, 80 | settings: settings, 81 | standardID: standardID, 82 | symbolVariant: 0, 83 | design: design, 84 | colorScheme: preferredColorScheme, 85 | selectedTab: selectedTab 86 | ) 87 | } 88 | 89 | /// Converts the settings to an array containing only the valid settings. 90 | /// - Parameter settings: The settings in the form defined by the coder. 91 | /// - Returns: The settings in the settings form and a string with the identifier of the standard tab. 92 | private func getSettings(_ settings: [SettingsTab]) -> ([SettingsTab], String) { 93 | var newTabs = settings.filter { tab in 94 | switch tab.type { 95 | case .new: 96 | return true 97 | default: 98 | return false 99 | } 100 | } 101 | let otherTabs = settings.filter { tab in 102 | !newTabs.contains { tab.id == $0.id } 103 | } 104 | for tab in otherTabs { 105 | if case let .extend(id: id) = tab.type { 106 | for index in newTabs.indices where newTabs[safe: index]?.id == id { 107 | for item in tab.content { 108 | switch item.type { 109 | case let .extend(id: id): 110 | let content = newTabs[safe: index]?.content[id: id] 111 | newTabs[safe: index]?.content[id: id]?.content = VStack { 112 | content 113 | item 114 | } 115 | default: 116 | newTabs[safe: index]?.content.append(item) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | var standardSettingsSubtab: String = .init() 123 | for tab in newTabs.reversed() { 124 | for subtab in tab.content.reversed() where subtab.standard { 125 | standardSettingsSubtab = subtab.id 126 | } 127 | } 128 | return (newTabs, standardSettingsSubtab) 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you very much for taking the time for contributing to this project. 4 | 5 | ## Report a Bug 6 | Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution! 7 | 8 | ## Suggest a New Feature 9 | Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution! 10 | 11 | ## Pull Requests 12 | I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution! 13 | 14 | ### 1. Fork & Clone this Project 15 | Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer. 16 | 17 | ### 2. Open the Project 18 | If there is a top-level `(PROJECT_NAME).xcodeproj` file in the directory, open this file in Xcode (or AppCode). Else, open the `Package.swift` file. 19 | 20 | ### 3. Understand the Project Structure 21 | You will find some top-level files and directories in both app (`(PROJECT_NAME).xcodeproj`) and package (`Package.swift`) projects: 22 | - The `README.md` file contains a description of the app or package. 23 | - The `Contributors.md` file contains the names or user names of all the contributors with a link to their GitHub profile. 24 | - The `LICENSE.md` contains an MIT license. 25 | - `CONTRIBUTING.md` is this file. 26 | - Directory `Icons` that contains PNG and PXD (Pixelmator Pro) files for the images used in the app and guides. 27 | - Directory `Documentation` that contains the documentation generated with [SourceDocs][1]. 28 | 29 | #### Package Project 30 | In a package project, you will find those directories that contain the Swift code: 31 | - `Sources` contains the source code of the project. 32 | - `(PROJECT_NAME)` contains the source code of the project. 33 | - `ViewModel` contains classes that store information for a part of the project and files related to them. 34 | - `View` contains structures that conform to the SwiftUI `View` protocol and whose main function it is to present content in a view. All of the files here have an additional structure add the bottom of the file that helps preview the views in Xcode. 35 | - `Components` contains structures whose main function is to present content but do not conform to the SwiftUI `View` protocol. 36 | - `Model` is the directory for other content. 37 | - `Data` contains mainly structures and enumerations that represent a part of the app data, for example, a structure that is used by a view model. 38 | - `Extensions` contains all the extensions of types that are not defined in this project. 39 | - `SwiftUI` contains extensions to SwiftUI types. 40 | - `Protocols` contains all the protocols. 41 | - `View` contains data that defines how the content is presented and nothing else. 42 | - `Tests` contains code written in Swift for testing the project. 43 | - `(PROJECT_NAME)Tests` contains single Swift files for testing the source code or sometimes even entire Xcode projects. 44 | 45 | #### App Project 46 | In an app project, you will find those directories and files: 47 | - `(PROJECT_NAME)` contains the source code. 48 | - `(PROJECT_NAME)App.swift` contains the structure that conforms to the `App` protocol and is the entry point of the app. 49 | - `ViewModel` contains classes that store information for a part of the project and files related to them. 50 | - `AppModel.swift` contains a class that stores information for the whole app. 51 | - `ViewModel.swift` contains a class that stores information for one window of the app. 52 | - `View` contains structures that conform to the SwiftUI `View` protocol and whose main function it is to present content in a view. All of the files here have an additional structure add the bottom of the file that helps preview the views in Xcode. 53 | - `ContentView.swift` contains the main view for the main window. 54 | - `Components` contains structures whose main function is to present content but do not conform to the SwiftUI `View` protocol. 55 | - `Model` is the directory for other content. 56 | - `Data` contains mainly structures and enumerations that represent a part of the app data, for example, a structure that is used by a view model. 57 | - `Extensions` contains all the extensions of types that are not defined in this project. 58 | - `SwiftUI` contains extensions to SwiftUI types. 59 | - `Protocols` contains all the protocols. 60 | - `View` contains data that defines how the content is presented and nothing else. 61 | - `Assets.xcassets` contains the assets for the app. 62 | - `SUMMARY.md` is the table of contents for the user documentation. 63 | - `user-manual` contains the user documentation. 64 | 65 | ### 4. Edit the Code or Docs 66 | Edit the code. Build the code at least once to get warnings for violating the code style. If you add a new type, add documentation in the code. 67 | You can also edit the docs’ text or add images. 68 | 69 | ### 5. Commit to the Fork 70 | Commit and push the fork. 71 | 72 | ### 6. Pull Request 73 | Open GitHub to submit a pull request. Thank you very much for your contribution! 74 | 75 | [1]: https://github.com/SourceDocs/SourceDocs 76 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Components/SettingsKitScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsKitScene.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 21.01.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A structure adding the settings to another scene. 11 | struct SettingsKitScene: Scene where Content: Scene { 12 | 13 | /// The shared instance of the ``SettingsModel``. 14 | @StateObject private var model = SettingsModel.shared 15 | /// The scene the settings are added to. 16 | var content: Content 17 | /// The settings tabs. 18 | var settings: [SettingsTab] 19 | /// The identifier of the settings tab with the keyboard shortcut. 20 | var standardID: String 21 | /// Modify the way symbols are displayed. 22 | var symbolVariant: Any? 23 | /// The design of the settings window. 24 | var design: SettingsWindowDesign 25 | /// The preferred color scheme. 26 | var colorScheme: ColorScheme? 27 | /// The filter in the sidebar design. 28 | @State private var search = "" 29 | /// The binding controlling the selection. 30 | var selectedTab: Binding? 31 | 32 | /// Get the symbol variants as symbol variants in macOS 12. 33 | @available(macOS 12, *) 34 | var unwrappedSymbolVariant: SymbolVariants? { 35 | symbolVariant as? SymbolVariants 36 | } 37 | 38 | /// The scene. 39 | var body: some Scene { 40 | Group { 41 | content 42 | Settings { 43 | let group = Group { 44 | if #available(macOS 13, *), design == .sidebar { 45 | navigationView 46 | } else { 47 | tabView 48 | } 49 | } 50 | .onChange(of: selectedTab?.wrappedValue) { newValue in 51 | if let newValue { 52 | model.selectedTab = newValue 53 | } 54 | } 55 | .onAppear { 56 | model.selectedTab = selectedTab?.wrappedValue ?? "" 57 | } 58 | .onChange(of: model.selectedTab) { newValue in 59 | selectedTab?.wrappedValue = newValue 60 | } 61 | .preferredColorScheme(colorScheme) 62 | group 63 | } 64 | } 65 | } 66 | 67 | /// The view with the sidebar design. 68 | @available(macOS 13, *) 69 | private var navigationView: some View { 70 | NavigationView { 71 | List(selection: .init { SettingsModel.shared.selectedTab } set: { tab in 72 | DispatchQueue.main.asyncAfter(deadline: .now()) { model.selectedTab = tab } 73 | }) { 74 | Section { 75 | ForEach(settings.filter { tab in 76 | if case let .new(title: title, image: _) = tab.type { 77 | let search = search.lowercased() 78 | let contentContains = tab.content.contains { subtab in 79 | if case let .new(title: title, image: _) = subtab.type { 80 | return title.lowercased().contains(search) 81 | } 82 | return false 83 | } 84 | return title.lowercased().contains(search) || search.isEmpty || contentContains 85 | } 86 | return false 87 | }) { $0.sidebarLabel } 88 | } 89 | } 90 | let tab = settings.first { $0.id == SettingsModel.shared.selectedTab } 91 | tab?.sidebarBody 92 | .navigationTitle({ () -> String in 93 | if case let .new(title: title, image: _) = tab?.type { 94 | return title 95 | } else { 96 | return "" 97 | } 98 | }()) 99 | .formStyle(.grouped) 100 | } 101 | .searchable(text: $search, placement: .sidebar) 102 | .toolbar { 103 | Text("") 104 | } 105 | .task { 106 | let window = NSApplication.shared.keyWindow 107 | window?.toolbarStyle = .unified 108 | window?.toolbar?.displayMode = .iconOnly 109 | } 110 | .onAppear { 111 | if !settings.contains(where: { $0.id == model.selectedTab }), let id = settings.first?.id { 112 | model.selectedTab = id 113 | } 114 | } 115 | } 116 | 117 | /// The view with the tab design. 118 | private var tabView: some View { 119 | TabView(selection: .init { SettingsModel.shared.selectedTab } set: { newValue in 120 | model.selectedTab = newValue 121 | }) { 122 | ForEach(settings) { tab in 123 | if case .new = tab.type { 124 | tab 125 | .tabViewStyle(.automatic) 126 | .tabItem { 127 | tab.label 128 | } 129 | } 130 | } 131 | } 132 | .onAppear { 133 | Task { 134 | let window = NSApplication.shared.keyWindow 135 | window?.toolbarStyle = .preference 136 | window?.toolbar?.displayMode = .iconAndLabel 137 | } 138 | } 139 | .onAppear { 140 | let selection = SettingsModel.shared.selectedTab 141 | model.selectedTab = "" 142 | model.selectedTab = selection 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Opt-In Rules 2 | opt_in_rules: 3 | - accessibility_label_for_image 4 | - accessibility_trait_for_button 5 | - anonymous_argument_in_multiline_closure 6 | - array_init 7 | - attributes 8 | - closure_body_length 9 | - closure_end_indentation 10 | - closure_spacing 11 | - collection_alignment 12 | - comma_inheritance 13 | - conditional_returns_on_newline 14 | - contains_over_filter_count 15 | - contains_over_filter_is_empty 16 | - contains_over_first_not_nil 17 | - contains_over_range_nil_comparison 18 | - convenience_type 19 | - discouraged_none_name 20 | - discouraged_object_literal 21 | - discouraged_optional_boolean 22 | - discouraged_optional_collection 23 | - empty_collection_literal 24 | - empty_count 25 | - empty_string 26 | - enum_case_associated_values_count 27 | - explicit_init 28 | - fallthrough 29 | - file_header 30 | - file_name 31 | - file_name_no_space 32 | - first_where 33 | - flatmap_over_map_reduce 34 | - force_unwrapping 35 | - function_default_parameter_at_end 36 | - identical_operands 37 | - implicit_return 38 | - implicitly_unwrapped_optional 39 | - joined_default_parameter 40 | - last_where 41 | - legacy_multiple 42 | - let_var_whitespace 43 | - literal_expression_end_indentation 44 | - local_doc_comment 45 | - lower_acl_than_parent 46 | - missing_docs 47 | - modifier_order 48 | - multiline_arguments 49 | - multiline_arguments_brackets 50 | - multiline_function_chains 51 | - multiline_literal_brackets 52 | - multiline_parameters 53 | - multiline_parameters_brackets 54 | - no_extension_access_modifier 55 | - no_grouping_extension 56 | - no_magic_numbers 57 | - number_separator 58 | - operator_usage_whitespace 59 | - optional_enum_case_matching 60 | - prefer_self_in_static_references 61 | - prefer_self_type_over_type_of_self 62 | - prefer_zero_over_explicit_init 63 | - prohibited_interface_builder 64 | - redundant_nil_coalescing 65 | - redundant_type_annotation 66 | - return_value_from_void_function 67 | - shorthand_optional_binding 68 | - sorted_first_last 69 | - sorted_imports 70 | - static_operator 71 | - strict_fileprivate 72 | - switch_case_on_newline 73 | - toggle_bool 74 | - trailing_closure 75 | - type_contents_order 76 | - unneeded_parentheses_in_closure_argument 77 | - yoda_condition 78 | 79 | # Disabled Rules 80 | disabled_rules: 81 | - block_based_kvo 82 | - class_delegate_protocol 83 | - dynamic_inline 84 | - is_disjoint 85 | - no_fallthrough_only 86 | - notification_center_detachment 87 | - ns_number_init_as_function_reference 88 | - nsobject_prefer_isequal 89 | - private_over_fileprivate 90 | - redundant_objc_attribute 91 | - self_in_property_initialization 92 | - todo 93 | - unavailable_condition 94 | - valid_ibinspectable 95 | - xctfail_message 96 | 97 | # Custom Rules 98 | custom_rules: 99 | github_issue: 100 | name: 'GitHub Issue' 101 | regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/david-swift/SettingsKit-macOS/issues/\d))' 102 | message: 'The related GitHub issue must be included in a TODO or FIXME.' 103 | severity: warning 104 | 105 | fatal_error: 106 | name: 'Fatal Error' 107 | regex: 'fatalError.*\(.*\)' 108 | message: 'Fatal error should not be used.' 109 | severity: error 110 | 111 | enum_case_parameter: 112 | name: 'Enum Case Parameter' 113 | regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)' 114 | message: 'The associated values of an enum case should have parameters.' 115 | severity: warning 116 | 117 | tab: 118 | name: 'Whitespaces Instead of Tab' 119 | regex: '\t' 120 | message: 'Spaces should be used instead of tabs.' 121 | severity: warning 122 | 123 | string_literals: 124 | name: 'String Literals' 125 | regex: '(".*")|("""(.|\n)*""")' 126 | message: 'String literals should not be used. Disable this rule in String and LocalizedStringResource extensions.' 127 | match_kinds: 128 | - string 129 | severity: warning 130 | 131 | # Thanks to David Furman for the pull request 132 | # "README: Add SwiftLint Suggestion" 133 | # https://github.com/SFSafeSymbols/SFSafeSymbols/pull/113 (07.11.22) 134 | # in the GitHub repository 135 | # "SFSafeSymbols" 136 | # https://github.com/SFSafeSymbols/SFSafeSymbols 137 | sf_safe_symbol: 138 | name: 'Safe SFSymbol' 139 | regex: '(Image\\(systemName:)|(NSImage\\(symbolName:)|(Label.*?systemImage:)|(UIApplicationShortcutIcon\\(systemImageName:)' 140 | message: 'Use `SFSafeSymbols` via `systemSymbol` parameters for type safety.' 141 | severity: warning 142 | 143 | # Thanks to the creator of the SwiftLint rule 144 | # "empty_first_line" 145 | # https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml 146 | # in the GitHub repository 147 | # "CotEditor" 148 | # https://github.com/coteditor/CotEditor 149 | empty_first_line: 150 | name: 'Empty First Line' 151 | regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)' 152 | message: 'There should be an empty line after a declaration' 153 | severity: error 154 | 155 | # Analyzer Rules 156 | analyzer_rules: 157 | - unused_declaration 158 | - unused_import 159 | 160 | # Options 161 | file_header: 162 | required_pattern: '(// swift-tools-version: .+)?//\n// .*.swift\n// SettingsKit\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n' 163 | missing_docs: 164 | warning: [internal, private] 165 | error: [open, public] 166 | excludes_extensions: false 167 | excludes_inherited_types: false 168 | prohibited_interface_builder: 169 | severity: error 170 | type_contents_order: 171 | order: 172 | - case 173 | - type_alias 174 | - associated_type 175 | - type_property 176 | - instance_property 177 | - ib_inspectable 178 | - ib_outlet 179 | - subscript 180 | - initializer 181 | - deinitializer 182 | - subtype 183 | - type_method 184 | - view_life_cycle_method 185 | - ib_action 186 | - other_method 187 | -------------------------------------------------------------------------------- /Tests/TestApp/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Opt-In Rules 2 | opt_in_rules: 3 | - accessibility_label_for_image 4 | - accessibility_trait_for_button 5 | - anonymous_argument_in_multiline_closure 6 | - array_init 7 | - attributes 8 | - closure_body_length 9 | - closure_end_indentation 10 | - closure_spacing 11 | - collection_alignment 12 | - comma_inheritance 13 | - conditional_returns_on_newline 14 | - contains_over_filter_count 15 | - contains_over_filter_is_empty 16 | - contains_over_first_not_nil 17 | - contains_over_range_nil_comparison 18 | - convenience_type 19 | - discouraged_none_name 20 | - discouraged_object_literal 21 | - discouraged_optional_boolean 22 | - discouraged_optional_collection 23 | - empty_collection_literal 24 | - empty_count 25 | - empty_string 26 | - enum_case_associated_values_count 27 | - explicit_init 28 | - fallthrough 29 | - file_header 30 | - file_name 31 | - file_name_no_space 32 | - first_where 33 | - flatmap_over_map_reduce 34 | - force_unwrapping 35 | - function_default_parameter_at_end 36 | - identical_operands 37 | - implicit_return 38 | - implicitly_unwrapped_optional 39 | - joined_default_parameter 40 | - last_where 41 | - legacy_multiple 42 | - let_var_whitespace 43 | - literal_expression_end_indentation 44 | - local_doc_comment 45 | - lower_acl_than_parent 46 | - missing_docs 47 | - modifier_order 48 | - multiline_arguments 49 | - multiline_arguments_brackets 50 | - multiline_function_chains 51 | - multiline_literal_brackets 52 | - multiline_parameters 53 | - multiline_parameters_brackets 54 | - no_extension_access_modifier 55 | - no_grouping_extension 56 | - no_magic_numbers 57 | - number_separator 58 | - operator_usage_whitespace 59 | - optional_enum_case_matching 60 | - prefer_self_in_static_references 61 | - prefer_self_type_over_type_of_self 62 | - prefer_zero_over_explicit_init 63 | - prohibited_interface_builder 64 | - redundant_nil_coalescing 65 | - redundant_type_annotation 66 | - return_value_from_void_function 67 | - shorthand_optional_binding 68 | - sorted_first_last 69 | - sorted_imports 70 | - static_operator 71 | - strict_fileprivate 72 | - switch_case_on_newline 73 | - toggle_bool 74 | - trailing_closure 75 | - type_contents_order 76 | - unneeded_parentheses_in_closure_argument 77 | - vertical_parameter_alignment_on_call 78 | - yoda_condition 79 | 80 | # Disabled Rules 81 | disabled_rules: 82 | - block_based_kvo 83 | - class_delegate_protocol 84 | - dynamic_inline 85 | - is_disjoint 86 | - no_fallthrough_only 87 | - notification_center_detachment 88 | - ns_number_init_as_function_reference 89 | - nsobject_prefer_isequal 90 | - private_over_fileprivate 91 | - redundant_objc_attribute 92 | - self_in_property_initialization 93 | - todo 94 | - unavailable_condition 95 | - valid_ibinspectable 96 | - xctfail_message 97 | 98 | # Custom Rules 99 | custom_rules: 100 | github_issue: 101 | name: 'GitHub Issue' 102 | regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/david-swift/SettingsKit/issues/\d))' 103 | message: 'The related GitHub issue must be included in a TODO or FIXME.' 104 | severity: warning 105 | 106 | fatal_error: 107 | name: 'Fatal Error' 108 | regex: 'fatalError.*\(.*\)' 109 | message: 'Fatal error should not be used.' 110 | severity: error 111 | 112 | enum_case_parameter: 113 | name: 'Enum Case Parameter' 114 | regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)' 115 | message: 'The associated values of an enum case should have parameters.' 116 | severity: warning 117 | 118 | tab: 119 | name: 'Whitespaces Instead of Tab' 120 | regex: '\t' 121 | message: 'Spaces should be used instead of tabs.' 122 | severity: warning 123 | 124 | string_literals: 125 | name: 'String Literals' 126 | regex: '(".*")|("""(.|\n)*""")' 127 | message: 'String literals should not be used. Disable this rule in String and LocalizedStringResource extensions.' 128 | match_kinds: 129 | - string 130 | severity: warning 131 | 132 | # Thanks to David Furman for the pull request 133 | # "README: Add SwiftLint Suggestion" 134 | # https://github.com/SFSafeSymbols/SFSafeSymbols/pull/113 (07.11.22) 135 | # in the GitHub repository 136 | # "SFSafeSymbols" 137 | # https://github.com/SFSafeSymbols/SFSafeSymbols 138 | sf_safe_symbol: 139 | name: 'Safe SFSymbol' 140 | regex: '(Image\\(systemName:)|(NSImage\\(symbolName:)|(Label.*?systemImage:)|(UIApplicationShortcutIcon\\(systemImageName:)' 141 | message: 'Use `SFSafeSymbols` via `systemSymbol` parameters for type safety.' 142 | severity: warning 143 | 144 | # Thanks to the creator of the SwiftLint rule 145 | # "empty_first_line" 146 | # https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml 147 | # in the GitHub repository 148 | # "CotEditor" 149 | # https://github.com/coteditor/CotEditor 150 | empty_first_line: 151 | name: 'Empty First Line' 152 | regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)' 153 | message: 'There should be an empty line after a declaration' 154 | severity: error 155 | 156 | # Analyzer Rules 157 | analyzer_rules: 158 | - unused_declaration 159 | - unused_import 160 | 161 | # Options 162 | file_header: 163 | required_pattern: '//\n// .*.swift\n// SettingsKit\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n' 164 | missing_docs: 165 | warning: [internal, private] 166 | error: [open, public] 167 | excludes_extensions: false 168 | excludes_inherited_types: false 169 | prohibited_interface_builder: 170 | severity: error 171 | type_contents_order: 172 | order: 173 | - case 174 | - type_alias 175 | - associated_type 176 | - type_property 177 | - instance_property 178 | - ib_inspectable 179 | - ib_outlet 180 | - subscript 181 | - initializer 182 | - deinitializer 183 | - subtype 184 | - type_method 185 | - view_life_cycle_method 186 | - ib_action 187 | - other_method 188 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Data/SettingsTab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsTab.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 20.01.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A tab in the settings window. 11 | public struct SettingsTab: Identifiable, View { 12 | 13 | /// The instance of the settings model. 14 | @ObservedObject var model = SettingsModel.shared 15 | /// The tab's identifier. 16 | public let id: String 17 | /// The tab's type. 18 | public var type: TabType 19 | /// The tab's color in the sidebar design. 20 | public var color: Color 21 | /// The tab's content. 22 | public var content: [SettingsSubtab] 23 | /// The view above the list of the subtabs in the sidebar style settings window. 24 | public var top: AnyView? 25 | /// The view below the list of the subtabs in the sidebar style settings window. 26 | public var bottom: AnyView? 27 | /// The sidebar actions view. 28 | public var sidebarActions: [ToolbarGroup] 29 | /// The settings window's width. 30 | public var windowWidth: CGFloat? = .settingsWidth 31 | /// The settings window's height. 32 | public var windowHeight: CGFloat? = .settingsHeight 33 | /// Whether a subtab is automatically selected after being created. 34 | public var autoSelect = true 35 | 36 | /// The tab's content, but without the subtabs with the ``TabType.noSelection`` type. 37 | var contentWithoutNoSelectionSubtabs: [SettingsSubtab] { 38 | content.filter { !$0.type.isNoSelection } 39 | } 40 | 41 | /// The view containing all the subtabs. 42 | public var body: some View { 43 | if content.count <= 1 && sidebarActions.isEmpty { 44 | content.first? 45 | .frame(width: windowWidth, height: windowHeight) 46 | } else { 47 | HSplitView { 48 | sidebar 49 | contentView 50 | } 51 | .frame(width: .settingsWidth, height: .settingsHeight) 52 | } 53 | } 54 | 55 | /// The tab's sidebar containing all the subtabs. 56 | var sidebar: some View { 57 | ZStack { 58 | Color(.textBackgroundColor) 59 | VStack { 60 | ZStack { 61 | sidebarList 62 | VStack { 63 | Spacer() 64 | Divider() 65 | } 66 | } 67 | if !sidebarActions.isEmpty { 68 | sidebarActions 69 | .padding(.bottom, .actionsPadding) 70 | } 71 | } 72 | } 73 | .frame(height: .settingsHeight) 74 | .frame(minWidth: .settingsSidebarWidth) 75 | } 76 | 77 | /// The list in the tab's sidebar. 78 | var sidebarList: some View { 79 | contentView { 80 | if #available(macOS 13, *) { 81 | let notOptional = model.selectedSubtabs[id] ?? "" 82 | List( 83 | contentWithoutNoSelectionSubtabs, 84 | selection: .init { 85 | notOptional 86 | } set: { newValue in 87 | model.selectedSubtabs[id] = newValue 88 | } 89 | ) { subtab in 90 | listContent(subtab: subtab) 91 | } 92 | } else { 93 | List( 94 | contentWithoutNoSelectionSubtabs, 95 | selection: .init { 96 | model.selectedSubtabs[id] 97 | } set: { newValue in 98 | model.selectedSubtabs[id] = newValue 99 | } 100 | ) { subtab in 101 | listContent(subtab: subtab) 102 | } 103 | } 104 | } 105 | } 106 | 107 | /// The body if the sidebar layout is active. 108 | @available(macOS 13, *) 109 | @ViewBuilder var sidebarBody: some View { 110 | contentView { 111 | if content.count <= 1 && top == nil && bottom == nil { body } else { 112 | NavigationStack(path: .init { () -> [String] in 113 | if content.contains(where: { $0.id == model.selectedSubtabs[id] }) { 114 | return [model.selectedSubtabs[id] ?? ""] 115 | } 116 | return [] 117 | } set: { newValue in 118 | guard let first = newValue.first else { 119 | return 120 | } 121 | model.selectedSubtabs[id] = first 122 | }) { 123 | Form { 124 | top 125 | ForEach(content) { content in 126 | if !content.type.isNoSelection { 127 | NavigationLink(value: content.id) { content.sidebarLabel } 128 | } 129 | } 130 | if !sidebarActions.isEmpty { 131 | let bottomPadding = 5.0 132 | sidebarActions.padding(.bottom, bottomPadding) 133 | } 134 | bottom 135 | } 136 | .formStyle(.grouped) 137 | .navigationDestination(for: String.self) { content[id: $0]?.body.navigationSubtitle("Hi") } 138 | } 139 | } 140 | } 141 | } 142 | 143 | /// The selected subtab's content. 144 | var contentView: some View { 145 | Form { 146 | if let first = contentWithoutNoSelectionSubtabs.first(where: { $0.id == model.selectedSubtabs[id] }) { 147 | first 148 | } else { 149 | content.first { $0.type.isNoSelection } 150 | } 151 | } 152 | .frame(minWidth: .settingsContentWidth, maxWidth: .infinity) 153 | } 154 | 155 | /// The label of a custom tab, or else nil. 156 | public var label: Label? { 157 | guard case let .new(title: title, image: icon) = type else { 158 | return nil 159 | } 160 | return .init { 161 | Text(title) 162 | } icon: { 163 | icon 164 | } 165 | } 166 | 167 | /// The label in the sidebar. 168 | @ViewBuilder public var sidebarLabel: some View { 169 | if case let .new(title: title, image: icon) = type { 170 | HStack { 171 | icon 172 | .sidebarSettingsIcon(color: color) 173 | .accessibilityHidden(true) 174 | Text(title) 175 | } 176 | } 177 | } 178 | 179 | /// The initializer. 180 | /// - Parameters: 181 | /// - type: The tab type of the settings tab. 182 | /// - id: The identifier. 183 | /// - color: The tab's color in the settings window with the sidebar design. 184 | /// - content: The content of the settings tab. 185 | public init( 186 | _ type: TabType, 187 | id: String, 188 | color: Color = .blue, 189 | @ArrayBuilder content: () -> [SettingsSubtab] 190 | ) { 191 | self.id = id 192 | self.type = type 193 | self.content = content() 194 | self.color = color 195 | sidebarActions = [] 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /Sources/SettingsKit/Model/Extensions/SettingsTab+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsTab+.swift 3 | // SettingsKit 4 | // 5 | // Created by david-swift on 09.10.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SettingsTab { 11 | 12 | /// Wrap the view with the required observers. 13 | /// - Parameter _: The view. 14 | /// - Returns: The view with observers. 15 | func contentView(@ViewBuilder _ content: () -> Content) -> some View where Content: View { 16 | content() 17 | .onChange(of: model.selectedSubtabs[id]) { newValue in 18 | if !contentWithoutNoSelectionSubtabs.contains(where: { $0.id == newValue }) { 19 | updateSubtabSelection(ids: contentWithoutNoSelectionSubtabs.map { $0.id }) 20 | } 21 | } 22 | .onChange(of: contentWithoutNoSelectionSubtabs.map { $0.id }) { updateSubtabSelection(ids: $0) } 23 | .onAppear { updateSubtabSelection(ids: contentWithoutNoSelectionSubtabs.map { $0.id }) } 24 | } 25 | 26 | /// A row in the sidebar list. 27 | /// - Parameter subtab: The subtab of the row. 28 | /// - Returns: The row. 29 | @ViewBuilder 30 | func listContent(subtab: SettingsSubtab) -> some View { 31 | if #available(macOS 13, *) { 32 | subtab.label 33 | .tag(subtab.id) 34 | .listRowSeparator(.hidden) 35 | } else { 36 | subtab.label 37 | .tag(subtab.id) 38 | } 39 | } 40 | 41 | /// Update the selection of the subtab. 42 | /// - Parameter ids: The identifiers of the subtabs. 43 | func updateSubtabSelection(ids: [String]) { 44 | if let first = ids.first(where: { id in 45 | !content.contains { $0.id == id } 46 | }), autoSelect { 47 | model.selectedSubtabs[id] = first 48 | } else if content.count > ids.count { 49 | let index = contentWithoutNoSelectionSubtabs.firstIndex { $0.id == model.selectedSubtabs[id] } 50 | if let after = ids[safe: index ?? ids.count] { 51 | model.selectedSubtabs[id] = after 52 | } else if let before = ids[safe: (index ?? 0) - 1] { 53 | model.selectedSubtabs[id] = before 54 | } else { 55 | model.selectedSubtabs[id] = ids.last ?? "" 56 | } 57 | } else if !ids.contains(model.selectedSubtabs[id] ?? ""), let last = ids.last { 58 | model.selectedSubtabs[id] = last 59 | } 60 | } 61 | 62 | /// Adds actions to the settings sidebar. 63 | /// - Parameter content: The actions. 64 | /// - Returns: The new tab with the actions. 65 | public func actions(@ArrayBuilder content: () -> [ToolbarGroup]) -> Self { 66 | actions(content: content()) 67 | } 68 | 69 | /// Add actions to the settings sidebar by providing an array. 70 | /// - Parameter content: The actions as an array.. 71 | /// - Returns: The new tab with the actions. 72 | public func actions(content: [ToolbarGroup]) -> Self { 73 | var newTab = self 74 | newTab.sidebarActions = content 75 | return newTab 76 | } 77 | 78 | /// The standard set of actions with an add button, a remove button and optionally an options button. 79 | /// - Parameters: 80 | /// - add: The action that is called when the add button is pressed. 81 | /// - remove: The action that is called when the remove button is pressed, 82 | /// giving the the selected subtab's id and index. 83 | /// - options: The action that is called when the options button is pressed. 84 | /// If it is nil, there is no options button. 85 | /// - Returns: The new tab with the actions. 86 | public func standardActions( 87 | add: @escaping () -> Void, 88 | remove: @escaping (String?, Int?) -> Void, 89 | options: (() -> Void)? = nil 90 | ) -> Self { 91 | actions { 92 | ToolbarGroup { 93 | ToolbarAction( 94 | "Add", 95 | symbol: .init(systemName: "plus"), 96 | action: add 97 | ) 98 | ToolbarAction( 99 | "Remove", 100 | symbol: .init(systemName: "minus") 101 | ) { 102 | let index = content.firstIndex { $0.id == SettingsModel.shared.selectedSubtabs[id] } 103 | remove(content[safe: index]?.id, index) 104 | } 105 | } 106 | .spacer() 107 | if let options { 108 | ToolbarGroup { 109 | ToolbarAction( 110 | "Options", 111 | symbol: .init(systemName: "ellipsis"), 112 | action: options 113 | ) 114 | } 115 | } 116 | } 117 | } 118 | 119 | /// The standard set of actions with an add menu, a remove button and optionally an options button. 120 | /// - Parameters: 121 | /// - add: The menu that is opened when the add button is pressed. 122 | /// - remove: The action that is called when the remove button is pressed, 123 | /// giving the the selected subtab's id and index. 124 | /// - options: The action that is called when the options button is pressed. 125 | /// If it is nil, there is no options button. 126 | /// - Returns: The new tab with the actions. 127 | public func standardActions( 128 | @ViewBuilder add: @escaping () -> ContentView, 129 | remove: @escaping (String?, Int?) -> Void, 130 | options: (() -> Void)? = nil 131 | ) -> Self where ContentView: View { 132 | actions { 133 | ToolbarGroup { 134 | ToolbarMenu( 135 | "Add", 136 | symbol: .init(systemName: "plus") 137 | ) { add() } 138 | ToolbarAction( 139 | "Remove", 140 | symbol: .init(systemName: "minus") 141 | ) { 142 | let index = content.firstIndex { $0.id == SettingsModel.shared.selectedSubtabs[id] } 143 | remove(content[safe: index]?.id, index) 144 | } 145 | } 146 | .spacer() 147 | if let options { 148 | ToolbarGroup { 149 | ToolbarAction( 150 | "Options", 151 | symbol: .init(systemName: "ellipsis"), 152 | action: options 153 | ) 154 | } 155 | } 156 | } 157 | } 158 | 159 | /// Set the window's width and height when this tab is open. 160 | /// This is being ignored if there is more than one subtab or if there are settings actions. 161 | /// - Parameters: 162 | /// - width: The width. If nil, the window uses the content's width. 163 | /// - height: The height. If nil, the window uses the content's height. 164 | /// - Returns: The settings tab with the new window size. 165 | public func frame(width: CGFloat? = nil, height: CGFloat? = nil) -> Self { 166 | var newSelf = self 167 | newSelf.windowWidth = width 168 | newSelf.windowHeight = height 169 | return newSelf 170 | } 171 | 172 | /// Set the window's width when this tab is open without affecting the height. 173 | /// This is being ignored if there is more than one subtab or if there are settings actions. 174 | /// - Parameter width: The width. If nil, the window uses the content's width. 175 | /// - Returns: The settings tab with the new window size. 176 | public func width(_ width: CGFloat? = nil) -> Self { 177 | var newSelf = self 178 | newSelf.windowWidth = width 179 | return newSelf 180 | } 181 | 182 | /// Set the window's height when this tab is open without affecting the width. 183 | /// This is being ignored if there is more than one subtab or if there are settings actions. 184 | /// - Parameter height: The height. If nil, the window uses the content's height. 185 | /// - Returns: The settings tab with the new window size. 186 | public func height(_ height: CGFloat? = nil) -> Self { 187 | var newSelf = self 188 | newSelf.windowHeight = height 189 | return newSelf 190 | } 191 | 192 | /// Set the content above the list of subtabs. 193 | /// - Parameter view: The content. 194 | /// - Returns: The settings tab. 195 | public func top(_ view: () -> Top) -> Self where Top: View { 196 | var newSelf = self 197 | newSelf.top = .init(view()) 198 | return newSelf 199 | } 200 | 201 | /// Set the content below the list of subtabs. 202 | /// - Parameter view: The content. 203 | /// - Returns: The settings tab. 204 | public func bottom(_ view: () -> Bottom) -> Self where Bottom: View { 205 | var newSelf = self 206 | newSelf.bottom = .init(view()) 207 | return newSelf 208 | } 209 | 210 | /// Enable or disable the automatic selection of settings tabs that are created. 211 | /// - Parameter enabled: Whether settings tabs that are created are automatically selected. 212 | /// - Returns: The settings tab. 213 | public func automaticSubtabSelection(_ enabled: Bool = true) -> Self { 214 | var newSelf = self 215 | newSelf.autoSelect = enabled 216 | return newSelf 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /Tests/Examples/Examples.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 60; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8DCE375B2AB6EC570099CC85 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCE375A2AB6EC570099CC85 /* ExamplesApp.swift */; }; 11 | 8DCE375D2AB6EC570099CC85 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCE375C2AB6EC570099CC85 /* ContentView.swift */; }; 12 | 8DCE375F2AB6EC580099CC85 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCE375E2AB6EC580099CC85 /* Assets.xcassets */; }; 13 | 8DCE37622AB6EC580099CC85 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCE37612AB6EC580099CC85 /* Preview Assets.xcassets */; }; 14 | 8DCE376B2AB6EC7B0099CC85 /* SettingsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8DCE376A2AB6EC7B0099CC85 /* SettingsKit */; }; 15 | 8DCE376D2AB6EE280099CC85 /* GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCE376C2AB6EE280099CC85 /* GeneralSettings.swift */; }; 16 | 8DCE376F2AB6FF410099CC85 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCE376E2AB6FF410099CC85 /* AccountView.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 8DCE37572AB6EC570099CC85 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 8DCE375A2AB6EC570099CC85 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; 22 | 8DCE375C2AB6EC570099CC85 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | 8DCE375E2AB6EC580099CC85 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 8DCE37612AB6EC580099CC85 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 25 | 8DCE37632AB6EC580099CC85 /* Examples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Examples.entitlements; sourceTree = ""; }; 26 | 8DCE376C2AB6EE280099CC85 /* GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettings.swift; sourceTree = ""; }; 27 | 8DCE376E2AB6FF410099CC85 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 8DCE37542AB6EC570099CC85 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | 8DCE376B2AB6EC7B0099CC85 /* SettingsKit in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 8DCE374E2AB6EC570099CC85 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 8DCE37592AB6EC570099CC85 /* Examples */, 46 | 8DCE37582AB6EC570099CC85 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 8DCE37582AB6EC570099CC85 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 8DCE37572AB6EC570099CC85 /* Examples.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 8DCE37592AB6EC570099CC85 /* Examples */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 8DCE375A2AB6EC570099CC85 /* ExamplesApp.swift */, 62 | 8DCE375C2AB6EC570099CC85 /* ContentView.swift */, 63 | 8DCE376C2AB6EE280099CC85 /* GeneralSettings.swift */, 64 | 8DCE376E2AB6FF410099CC85 /* AccountView.swift */, 65 | 8DCE375E2AB6EC580099CC85 /* Assets.xcassets */, 66 | 8DCE37632AB6EC580099CC85 /* Examples.entitlements */, 67 | 8DCE37602AB6EC580099CC85 /* Preview Content */, 68 | ); 69 | path = Examples; 70 | sourceTree = ""; 71 | }; 72 | 8DCE37602AB6EC580099CC85 /* Preview Content */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 8DCE37612AB6EC580099CC85 /* Preview Assets.xcassets */, 76 | ); 77 | path = "Preview Content"; 78 | sourceTree = ""; 79 | }; 80 | /* End PBXGroup section */ 81 | 82 | /* Begin PBXNativeTarget section */ 83 | 8DCE37562AB6EC570099CC85 /* Examples */ = { 84 | isa = PBXNativeTarget; 85 | buildConfigurationList = 8DCE37662AB6EC580099CC85 /* Build configuration list for PBXNativeTarget "Examples" */; 86 | buildPhases = ( 87 | 8DCE37532AB6EC570099CC85 /* Sources */, 88 | 8DCE37542AB6EC570099CC85 /* Frameworks */, 89 | 8DCE37552AB6EC570099CC85 /* Resources */, 90 | ); 91 | buildRules = ( 92 | ); 93 | dependencies = ( 94 | ); 95 | name = Examples; 96 | packageProductDependencies = ( 97 | 8DCE376A2AB6EC7B0099CC85 /* SettingsKit */, 98 | ); 99 | productName = Examples; 100 | productReference = 8DCE37572AB6EC570099CC85 /* Examples.app */; 101 | productType = "com.apple.product-type.application"; 102 | }; 103 | /* End PBXNativeTarget section */ 104 | 105 | /* Begin PBXProject section */ 106 | 8DCE374F2AB6EC570099CC85 /* Project object */ = { 107 | isa = PBXProject; 108 | attributes = { 109 | BuildIndependentTargetsInParallel = 1; 110 | LastSwiftUpdateCheck = 1500; 111 | LastUpgradeCheck = 1500; 112 | TargetAttributes = { 113 | 8DCE37562AB6EC570099CC85 = { 114 | CreatedOnToolsVersion = 15.0; 115 | }; 116 | }; 117 | }; 118 | buildConfigurationList = 8DCE37522AB6EC570099CC85 /* Build configuration list for PBXProject "Examples" */; 119 | compatibilityVersion = "Xcode 14.0"; 120 | developmentRegion = en; 121 | hasScannedForEncodings = 0; 122 | knownRegions = ( 123 | en, 124 | Base, 125 | ); 126 | mainGroup = 8DCE374E2AB6EC570099CC85; 127 | packageReferences = ( 128 | 8DCE37692AB6EC7B0099CC85 /* XCLocalSwiftPackageReference "../.." */, 129 | ); 130 | productRefGroup = 8DCE37582AB6EC570099CC85 /* Products */; 131 | projectDirPath = ""; 132 | projectRoot = ""; 133 | targets = ( 134 | 8DCE37562AB6EC570099CC85 /* Examples */, 135 | ); 136 | }; 137 | /* End PBXProject section */ 138 | 139 | /* Begin PBXResourcesBuildPhase section */ 140 | 8DCE37552AB6EC570099CC85 /* Resources */ = { 141 | isa = PBXResourcesBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 8DCE37622AB6EC580099CC85 /* Preview Assets.xcassets in Resources */, 145 | 8DCE375F2AB6EC580099CC85 /* Assets.xcassets in Resources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXResourcesBuildPhase section */ 150 | 151 | /* Begin PBXSourcesBuildPhase section */ 152 | 8DCE37532AB6EC570099CC85 /* Sources */ = { 153 | isa = PBXSourcesBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | 8DCE376F2AB6FF410099CC85 /* AccountView.swift in Sources */, 157 | 8DCE375D2AB6EC570099CC85 /* ContentView.swift in Sources */, 158 | 8DCE376D2AB6EE280099CC85 /* GeneralSettings.swift in Sources */, 159 | 8DCE375B2AB6EC570099CC85 /* ExamplesApp.swift in Sources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXSourcesBuildPhase section */ 164 | 165 | /* Begin XCBuildConfiguration section */ 166 | 8DCE37642AB6EC580099CC85 /* Debug */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 171 | CLANG_ANALYZER_NONNULL = YES; 172 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 173 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 174 | CLANG_ENABLE_MODULES = YES; 175 | CLANG_ENABLE_OBJC_ARC = YES; 176 | CLANG_ENABLE_OBJC_WEAK = YES; 177 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 178 | CLANG_WARN_BOOL_CONVERSION = YES; 179 | CLANG_WARN_COMMA = YES; 180 | CLANG_WARN_CONSTANT_CONVERSION = YES; 181 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 182 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 183 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 184 | CLANG_WARN_EMPTY_BODY = YES; 185 | CLANG_WARN_ENUM_CONVERSION = YES; 186 | CLANG_WARN_INFINITE_RECURSION = YES; 187 | CLANG_WARN_INT_CONVERSION = YES; 188 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 189 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 190 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 192 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 193 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 194 | CLANG_WARN_STRICT_PROTOTYPES = YES; 195 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 196 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | COPY_PHASE_STRIP = NO; 200 | DEBUG_INFORMATION_FORMAT = dwarf; 201 | ENABLE_STRICT_OBJC_MSGSEND = YES; 202 | ENABLE_TESTABILITY = YES; 203 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu17; 205 | GCC_DYNAMIC_NO_PIC = NO; 206 | GCC_NO_COMMON_BLOCKS = YES; 207 | GCC_OPTIMIZATION_LEVEL = 0; 208 | GCC_PREPROCESSOR_DEFINITIONS = ( 209 | "DEBUG=1", 210 | "$(inherited)", 211 | ); 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 219 | MACOSX_DEPLOYMENT_TARGET = 13.0; 220 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 221 | MTL_FAST_MATH = YES; 222 | ONLY_ACTIVE_ARCH = YES; 223 | SDKROOT = macosx; 224 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 225 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 226 | }; 227 | name = Debug; 228 | }; 229 | 8DCE37652AB6EC580099CC85 /* Release */ = { 230 | isa = XCBuildConfiguration; 231 | buildSettings = { 232 | ALWAYS_SEARCH_USER_PATHS = NO; 233 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 237 | CLANG_ENABLE_MODULES = YES; 238 | CLANG_ENABLE_OBJC_ARC = YES; 239 | CLANG_ENABLE_OBJC_WEAK = YES; 240 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 241 | CLANG_WARN_BOOL_CONVERSION = YES; 242 | CLANG_WARN_COMMA = YES; 243 | CLANG_WARN_CONSTANT_CONVERSION = YES; 244 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 245 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 246 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 247 | CLANG_WARN_EMPTY_BODY = YES; 248 | CLANG_WARN_ENUM_CONVERSION = YES; 249 | CLANG_WARN_INFINITE_RECURSION = YES; 250 | CLANG_WARN_INT_CONVERSION = YES; 251 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 252 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 253 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 255 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 257 | CLANG_WARN_STRICT_PROTOTYPES = YES; 258 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | COPY_PHASE_STRIP = NO; 263 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 264 | ENABLE_NS_ASSERTIONS = NO; 265 | ENABLE_STRICT_OBJC_MSGSEND = YES; 266 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu17; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 276 | MACOSX_DEPLOYMENT_TARGET = 13.0; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = macosx; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | }; 282 | name = Release; 283 | }; 284 | 8DCE37672AB6EC580099CC85 /* Debug */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 289 | CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; 290 | CODE_SIGN_STYLE = Automatic; 291 | COMBINE_HIDPI_IMAGES = YES; 292 | CURRENT_PROJECT_VERSION = 1; 293 | DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\""; 294 | DEVELOPMENT_TEAM = KW3F3CY4T9; 295 | ENABLE_HARDENED_RUNTIME = YES; 296 | ENABLE_PREVIEWS = YES; 297 | GENERATE_INFOPLIST_FILE = YES; 298 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 299 | LD_RUNPATH_SEARCH_PATHS = ( 300 | "$(inherited)", 301 | "@executable_path/../Frameworks", 302 | ); 303 | MACOSX_DEPLOYMENT_TARGET = 13.0; 304 | MARKETING_VERSION = 1.0; 305 | PRODUCT_BUNDLE_IDENTIFIER = "ch.david-swift.Examples"; 306 | PRODUCT_NAME = "$(TARGET_NAME)"; 307 | SWIFT_EMIT_LOC_STRINGS = YES; 308 | SWIFT_VERSION = 5.0; 309 | }; 310 | name = Debug; 311 | }; 312 | 8DCE37682AB6EC580099CC85 /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 317 | CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; 318 | CODE_SIGN_STYLE = Automatic; 319 | COMBINE_HIDPI_IMAGES = YES; 320 | CURRENT_PROJECT_VERSION = 1; 321 | DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\""; 322 | DEVELOPMENT_TEAM = KW3F3CY4T9; 323 | ENABLE_HARDENED_RUNTIME = YES; 324 | ENABLE_PREVIEWS = YES; 325 | GENERATE_INFOPLIST_FILE = YES; 326 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 327 | LD_RUNPATH_SEARCH_PATHS = ( 328 | "$(inherited)", 329 | "@executable_path/../Frameworks", 330 | ); 331 | MACOSX_DEPLOYMENT_TARGET = 13.0; 332 | MARKETING_VERSION = 1.0; 333 | PRODUCT_BUNDLE_IDENTIFIER = "ch.david-swift.Examples"; 334 | PRODUCT_NAME = "$(TARGET_NAME)"; 335 | SWIFT_EMIT_LOC_STRINGS = YES; 336 | SWIFT_VERSION = 5.0; 337 | }; 338 | name = Release; 339 | }; 340 | /* End XCBuildConfiguration section */ 341 | 342 | /* Begin XCConfigurationList section */ 343 | 8DCE37522AB6EC570099CC85 /* Build configuration list for PBXProject "Examples" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 8DCE37642AB6EC580099CC85 /* Debug */, 347 | 8DCE37652AB6EC580099CC85 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | 8DCE37662AB6EC580099CC85 /* Build configuration list for PBXNativeTarget "Examples" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | 8DCE37672AB6EC580099CC85 /* Debug */, 356 | 8DCE37682AB6EC580099CC85 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | /* End XCConfigurationList section */ 362 | 363 | /* Begin XCLocalSwiftPackageReference section */ 364 | 8DCE37692AB6EC7B0099CC85 /* XCLocalSwiftPackageReference "../.." */ = { 365 | isa = XCLocalSwiftPackageReference; 366 | relativePath = ../..; 367 | }; 368 | /* End XCLocalSwiftPackageReference section */ 369 | 370 | /* Begin XCSwiftPackageProductDependency section */ 371 | 8DCE376A2AB6EC7B0099CC85 /* SettingsKit */ = { 372 | isa = XCSwiftPackageProductDependency; 373 | productName = SettingsKit; 374 | }; 375 | /* End XCSwiftPackageProductDependency section */ 376 | }; 377 | rootObject = 8DCE374F2AB6EC570099CC85 /* Project object */; 378 | } 379 | -------------------------------------------------------------------------------- /Tests/TestApp/TestApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 630E92DB297B1CF800847A3A /* TestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630E92DA297B1CF800847A3A /* TestAppApp.swift */; }; 11 | 630E92DD297B1CF800847A3A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630E92DC297B1CF800847A3A /* ContentView.swift */; }; 12 | 630E92DF297B1CFA00847A3A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 630E92DE297B1CFA00847A3A /* Assets.xcassets */; }; 13 | 630E92E3297B1CFA00847A3A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 630E92E2297B1CFA00847A3A /* Preview Assets.xcassets */; }; 14 | 638EEADA298397960099321D /* SettingsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 638EEAD9298397960099321D /* SettingsKit */; }; 15 | 638EEADC29845B440099321D /* TestAppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638EEADB29845B440099321D /* TestAppModel.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXBuildRule section */ 19 | 638EEADE298460300099321D /* PBXBuildRule */ = { 20 | isa = PBXBuildRule; 21 | compilerSpec = com.apple.compilers.proxy.script; 22 | fileType = pattern.proxy; 23 | inputFiles = ( 24 | ); 25 | isEditable = 1; 26 | outputFiles = ( 27 | ); 28 | script = "# Type a script or drag a script file from your workspace to insert its path.\n"; 29 | }; 30 | /* End PBXBuildRule section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 630E92D7297B1CF800847A3A /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 630E92DA297B1CF800847A3A /* TestAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppApp.swift; sourceTree = ""; }; 35 | 630E92DC297B1CF800847A3A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 36 | 630E92DE297B1CFA00847A3A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 630E92E0297B1CFA00847A3A /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; 38 | 630E92E2297B1CFA00847A3A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | 638EEAD8298397890099321D /* SettingsKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SettingsKit; path = ../..; sourceTree = ""; }; 40 | 638EEADB29845B440099321D /* TestAppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppModel.swift; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 630E92D4297B1CF800847A3A /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | 638EEADA298397960099321D /* SettingsKit in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 630E92CE297B1CF800847A3A = { 56 | isa = PBXGroup; 57 | children = ( 58 | 630E92E9297B1D3700847A3A /* Packages */, 59 | 630E92D9297B1CF800847A3A /* TestApp */, 60 | 630E92D8297B1CF800847A3A /* Products */, 61 | 630E92F3297B326400847A3A /* Frameworks */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 630E92D8297B1CF800847A3A /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 630E92D7297B1CF800847A3A /* TestApp.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 630E92D9297B1CF800847A3A /* TestApp */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 630E92DA297B1CF800847A3A /* TestAppApp.swift */, 77 | 630E92EC297B1DBF00847A3A /* ViewModel */, 78 | 630E92EB297B1DA800847A3A /* View */, 79 | 630E92DE297B1CFA00847A3A /* Assets.xcassets */, 80 | 630E92E0297B1CFA00847A3A /* TestApp.entitlements */, 81 | 630E92E1297B1CFA00847A3A /* Preview Content */, 82 | ); 83 | path = TestApp; 84 | sourceTree = ""; 85 | }; 86 | 630E92E1297B1CFA00847A3A /* Preview Content */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 630E92E2297B1CFA00847A3A /* Preview Assets.xcassets */, 90 | ); 91 | path = "Preview Content"; 92 | sourceTree = ""; 93 | }; 94 | 630E92E9297B1D3700847A3A /* Packages */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 638EEAD8298397890099321D /* SettingsKit */, 98 | ); 99 | name = Packages; 100 | sourceTree = ""; 101 | }; 102 | 630E92EB297B1DA800847A3A /* View */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 630E92DC297B1CF800847A3A /* ContentView.swift */, 106 | ); 107 | path = View; 108 | sourceTree = ""; 109 | }; 110 | 630E92EC297B1DBF00847A3A /* ViewModel */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 638EEADB29845B440099321D /* TestAppModel.swift */, 114 | ); 115 | path = ViewModel; 116 | sourceTree = ""; 117 | }; 118 | 630E92F3297B326400847A3A /* Frameworks */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | ); 122 | name = Frameworks; 123 | sourceTree = ""; 124 | }; 125 | /* End PBXGroup section */ 126 | 127 | /* Begin PBXNativeTarget section */ 128 | 630E92D6297B1CF800847A3A /* TestApp */ = { 129 | isa = PBXNativeTarget; 130 | buildConfigurationList = 630E92E6297B1CFA00847A3A /* Build configuration list for PBXNativeTarget "TestApp" */; 131 | buildPhases = ( 132 | 630E92D3297B1CF800847A3A /* Sources */, 133 | 630E92D4297B1CF800847A3A /* Frameworks */, 134 | 630E92D5297B1CF800847A3A /* Resources */, 135 | ); 136 | buildRules = ( 137 | 638EEADE298460300099321D /* PBXBuildRule */, 138 | ); 139 | dependencies = ( 140 | 638EEAE0298460400099321D /* PBXTargetDependency */, 141 | ); 142 | name = TestApp; 143 | packageProductDependencies = ( 144 | 638EEAD9298397960099321D /* SettingsKit */, 145 | ); 146 | productName = TestApp; 147 | productReference = 630E92D7297B1CF800847A3A /* TestApp.app */; 148 | productType = "com.apple.product-type.application"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | 630E92CF297B1CF800847A3A /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | BuildIndependentTargetsInParallel = 1; 157 | LastSwiftUpdateCheck = 1420; 158 | LastUpgradeCheck = 1420; 159 | TargetAttributes = { 160 | 630E92D6297B1CF800847A3A = { 161 | CreatedOnToolsVersion = 14.2; 162 | }; 163 | }; 164 | }; 165 | buildConfigurationList = 630E92D2297B1CF800847A3A /* Build configuration list for PBXProject "TestApp" */; 166 | compatibilityVersion = "Xcode 14.0"; 167 | developmentRegion = en; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | Base, 172 | ); 173 | mainGroup = 630E92CE297B1CF800847A3A; 174 | packageReferences = ( 175 | 638EEADD298460260099321D /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */, 176 | ); 177 | productRefGroup = 630E92D8297B1CF800847A3A /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 630E92D6297B1CF800847A3A /* TestApp */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 630E92D5297B1CF800847A3A /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 630E92E3297B1CFA00847A3A /* Preview Assets.xcassets in Resources */, 192 | 630E92DF297B1CFA00847A3A /* Assets.xcassets in Resources */, 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | /* End PBXResourcesBuildPhase section */ 197 | 198 | /* Begin PBXSourcesBuildPhase section */ 199 | 630E92D3297B1CF800847A3A /* Sources */ = { 200 | isa = PBXSourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 630E92DD297B1CF800847A3A /* ContentView.swift in Sources */, 204 | 638EEADC29845B440099321D /* TestAppModel.swift in Sources */, 205 | 630E92DB297B1CF800847A3A /* TestAppApp.swift in Sources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXSourcesBuildPhase section */ 210 | 211 | /* Begin PBXTargetDependency section */ 212 | 638EEAE0298460400099321D /* PBXTargetDependency */ = { 213 | isa = PBXTargetDependency; 214 | productRef = 638EEADF298460400099321D /* SwiftLint */; 215 | }; 216 | /* End PBXTargetDependency section */ 217 | 218 | /* Begin XCBuildConfiguration section */ 219 | 630E92E4297B1CFA00847A3A /* Debug */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | ALWAYS_SEARCH_USER_PATHS = NO; 223 | CLANG_ANALYZER_NONNULL = YES; 224 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 226 | CLANG_ENABLE_MODULES = YES; 227 | CLANG_ENABLE_OBJC_ARC = YES; 228 | CLANG_ENABLE_OBJC_WEAK = YES; 229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 230 | CLANG_WARN_BOOL_CONVERSION = YES; 231 | CLANG_WARN_COMMA = YES; 232 | CLANG_WARN_CONSTANT_CONVERSION = YES; 233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 236 | CLANG_WARN_EMPTY_BODY = YES; 237 | CLANG_WARN_ENUM_CONVERSION = YES; 238 | CLANG_WARN_INFINITE_RECURSION = YES; 239 | CLANG_WARN_INT_CONVERSION = YES; 240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 246 | CLANG_WARN_STRICT_PROTOTYPES = YES; 247 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 249 | CLANG_WARN_UNREACHABLE_CODE = YES; 250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 251 | COPY_PHASE_STRIP = NO; 252 | DEBUG_INFORMATION_FORMAT = dwarf; 253 | ENABLE_STRICT_OBJC_MSGSEND = YES; 254 | ENABLE_TESTABILITY = YES; 255 | GCC_C_LANGUAGE_STANDARD = gnu11; 256 | GCC_DYNAMIC_NO_PIC = NO; 257 | GCC_NO_COMMON_BLOCKS = YES; 258 | GCC_OPTIMIZATION_LEVEL = 0; 259 | GCC_PREPROCESSOR_DEFINITIONS = ( 260 | "DEBUG=1", 261 | "$(inherited)", 262 | ); 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 270 | MTL_FAST_MATH = YES; 271 | ONLY_ACTIVE_ARCH = YES; 272 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 273 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 274 | }; 275 | name = Debug; 276 | }; 277 | 630E92E5297B1CFA00847A3A /* Release */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_ANALYZER_NONNULL = YES; 282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 284 | CLANG_ENABLE_MODULES = YES; 285 | CLANG_ENABLE_OBJC_ARC = YES; 286 | CLANG_ENABLE_OBJC_WEAK = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 303 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 304 | CLANG_WARN_STRICT_PROTOTYPES = YES; 305 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 306 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 307 | CLANG_WARN_UNREACHABLE_CODE = YES; 308 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 309 | COPY_PHASE_STRIP = NO; 310 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 311 | ENABLE_NS_ASSERTIONS = NO; 312 | ENABLE_STRICT_OBJC_MSGSEND = YES; 313 | GCC_C_LANGUAGE_STANDARD = gnu11; 314 | GCC_NO_COMMON_BLOCKS = YES; 315 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 316 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 317 | GCC_WARN_UNDECLARED_SELECTOR = YES; 318 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 319 | GCC_WARN_UNUSED_FUNCTION = YES; 320 | GCC_WARN_UNUSED_VARIABLE = YES; 321 | MTL_ENABLE_DEBUG_INFO = NO; 322 | MTL_FAST_MATH = YES; 323 | SWIFT_COMPILATION_MODE = wholemodule; 324 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 325 | }; 326 | name = Release; 327 | }; 328 | 630E92E7297B1CFA00847A3A /* Debug */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 332 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 333 | CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; 334 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 335 | CODE_SIGN_STYLE = Automatic; 336 | CURRENT_PROJECT_VERSION = 1; 337 | DEVELOPMENT_ASSET_PATHS = "\"TestApp/Preview Content\""; 338 | DEVELOPMENT_TEAM = KW3F3CY4T9; 339 | ENABLE_HARDENED_RUNTIME = YES; 340 | ENABLE_PREVIEWS = YES; 341 | GENERATE_INFOPLIST_FILE = YES; 342 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 343 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 344 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 345 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 346 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 347 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 348 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 349 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 350 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 351 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 352 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 353 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 354 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 355 | MACOSX_DEPLOYMENT_TARGET = 13.1; 356 | MARKETING_VERSION = 1.0; 357 | PRODUCT_BUNDLE_IDENTIFIER = "ch.david-swift.TestApp"; 358 | PRODUCT_NAME = "$(TARGET_NAME)"; 359 | SDKROOT = auto; 360 | SUPPORTED_PLATFORMS = macosx; 361 | SUPPORTS_MACCATALYST = NO; 362 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 363 | SWIFT_EMIT_LOC_STRINGS = YES; 364 | SWIFT_VERSION = 5.0; 365 | }; 366 | name = Debug; 367 | }; 368 | 630E92E8297B1CFA00847A3A /* Release */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 372 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 373 | CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; 374 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 375 | CODE_SIGN_STYLE = Automatic; 376 | CURRENT_PROJECT_VERSION = 1; 377 | DEVELOPMENT_ASSET_PATHS = "\"TestApp/Preview Content\""; 378 | DEVELOPMENT_TEAM = KW3F3CY4T9; 379 | ENABLE_HARDENED_RUNTIME = YES; 380 | ENABLE_PREVIEWS = YES; 381 | GENERATE_INFOPLIST_FILE = YES; 382 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 383 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 384 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 385 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 386 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 387 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 388 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 389 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 390 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 391 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 392 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 393 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 394 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 395 | MACOSX_DEPLOYMENT_TARGET = 13.1; 396 | MARKETING_VERSION = 1.0; 397 | PRODUCT_BUNDLE_IDENTIFIER = "ch.david-swift.TestApp"; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | SDKROOT = auto; 400 | SUPPORTED_PLATFORMS = macosx; 401 | SUPPORTS_MACCATALYST = NO; 402 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 403 | SWIFT_EMIT_LOC_STRINGS = YES; 404 | SWIFT_VERSION = 5.0; 405 | }; 406 | name = Release; 407 | }; 408 | /* End XCBuildConfiguration section */ 409 | 410 | /* Begin XCConfigurationList section */ 411 | 630E92D2297B1CF800847A3A /* Build configuration list for PBXProject "TestApp" */ = { 412 | isa = XCConfigurationList; 413 | buildConfigurations = ( 414 | 630E92E4297B1CFA00847A3A /* Debug */, 415 | 630E92E5297B1CFA00847A3A /* Release */, 416 | ); 417 | defaultConfigurationIsVisible = 0; 418 | defaultConfigurationName = Release; 419 | }; 420 | 630E92E6297B1CFA00847A3A /* Build configuration list for PBXNativeTarget "TestApp" */ = { 421 | isa = XCConfigurationList; 422 | buildConfigurations = ( 423 | 630E92E7297B1CFA00847A3A /* Debug */, 424 | 630E92E8297B1CFA00847A3A /* Release */, 425 | ); 426 | defaultConfigurationIsVisible = 0; 427 | defaultConfigurationName = Release; 428 | }; 429 | /* End XCConfigurationList section */ 430 | 431 | /* Begin XCRemoteSwiftPackageReference section */ 432 | 638EEADD298460260099321D /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */ = { 433 | isa = XCRemoteSwiftPackageReference; 434 | repositoryURL = "https://github.com/lukepistrol/SwiftLintPlugin"; 435 | requirement = { 436 | kind = upToNextMajorVersion; 437 | minimumVersion = 0.2.2; 438 | }; 439 | }; 440 | /* End XCRemoteSwiftPackageReference section */ 441 | 442 | /* Begin XCSwiftPackageProductDependency section */ 443 | 638EEAD9298397960099321D /* SettingsKit */ = { 444 | isa = XCSwiftPackageProductDependency; 445 | productName = SettingsKit; 446 | }; 447 | 638EEADF298460400099321D /* SwiftLint */ = { 448 | isa = XCSwiftPackageProductDependency; 449 | package = 638EEADD298460260099321D /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */; 450 | productName = "plugin:SwiftLint"; 451 | }; 452 | /* End XCSwiftPackageProductDependency section */ 453 | }; 454 | rootObject = 630E92CF297B1CF800847A3A /* Project object */; 455 | } 456 | --------------------------------------------------------------------------------