├── 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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------