diff --git a/Documentation/Reference/SettingsKit/structs/SettingsTab.md b/Documentation/Reference/SettingsKit/structs/SettingsTab.md index 055053e..e841249 100644 --- a/Documentation/Reference/SettingsKit/structs/SettingsTab.md +++ b/Documentation/Reference/SettingsKit/structs/SettingsTab.md @@ -12,7 +12,7 @@ A tab in the settings window. ### `model` ```swift -@StateObject private var model = SettingsModel.shared +@StateObject var model = SettingsModel.shared ``` The instance of the settings model. @@ -49,6 +49,22 @@ public var content: [SettingsSubtab] The tab's content. +### `top` + +```swift +public var top: AnyView? +``` + +The view above the list of the subtabs in the sidebar style settings window. + +### `bottom` + +```swift +public var bottom: AnyView? +``` + +The view below the list of the subtabs in the sidebar style settings window. + ### `sidebarActions` ```swift @@ -76,7 +92,7 @@ The settings window's height. ### `contentWithoutNoSelectionSubtabs` ```swift -private var contentWithoutNoSelectionSubtabs: [SettingsSubtab] +var contentWithoutNoSelectionSubtabs: [SettingsSubtab] ``` The tab's content, but without the subtabs with the ``TabType.noSelection`` type. @@ -92,7 +108,7 @@ The view containing all the subtabs. ### `sidebar` ```swift -private var sidebar: some View +var sidebar: some View ``` The tab's sidebar containing all the subtabs. @@ -100,7 +116,7 @@ The tab's sidebar containing all the subtabs. ### `sidebarList` ```swift -private var sidebarList: some View +var sidebarList: some View ``` The list in the tab's sidebar. @@ -116,7 +132,7 @@ The body if the sidebar layout is active. ### `contentView` ```swift -private var contentView: some View +var contentView: some View ``` The selected subtab's content. @@ -163,175 +179,4 @@ The initializer. | type | The tab type of the settings tab. | | id | The identifier. | | color | The tab’s color in the settings window with the sidebar design. | -| content | The content of the settings tab. | - -### `listContent(subtab:)` - -```swift -private func listContent(subtab: SettingsSubtab) -> some View -``` - -A row in the sidebar list. -- Parameter subtab: The subtab of the row. -- Returns: The row. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| subtab | The subtab of the row. | - -### `updateSubtabSelection(ids:)` - -```swift -private func updateSubtabSelection(ids: [String]) -``` - -Update the selection of the subtab. -- Parameter ids: The identifiers of the subtabs. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| ids | The identifiers of the subtabs. | - -### `actions(content:)` - -```swift -public func actions(@ArrayBuilder content: () -> [ToolbarGroup]) -> Self -``` - -Adds actions to the settings sidebar. -- Parameter content: The actions. -- Returns: The new tab with the actions. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| content | The actions. | - -### `actions(content:)` - -```swift -public func actions(content: [ToolbarGroup]) -> Self -``` - -Add actions to the settings sidebar by providing an array. -- Parameter content: The actions as an array.. -- Returns: The new tab with the actions. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| content | The actions as an array.. | - -### `standardActions(add:remove:options:)` - -```swift -public func standardActions( - add: @escaping () -> Void, - remove: @escaping (String?, Int?) -> Void, - options: (() -> Void)? = nil -) -> Self -``` - -The standard set of actions with an add button, a remove button and optionally an options button. -- Parameters: - - add: The action that is called when the add button is pressed. - - remove: The action that is called when the remove button is pressed, - giving the the selected subtab's id and index. - - options: The action that is called when the options button is pressed. - If it is nil, there is no options button. -- Returns: The new tab with the actions. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| add | The action that is called when the add button is pressed. | -| remove | The action that is called when the remove button is pressed, giving the the selected subtab’s id and index. | -| options | The action that is called when the options button is pressed. If it is nil, there is no options button. | - -### `standardActions(add:remove:options:)` - -```swift -public func standardActions( - @ViewBuilder add: @escaping () -> ContentView, - remove: @escaping (String?, Int?) -> Void, - options: (() -> Void)? = nil -) -> Self where ContentView: View -``` - -The standard set of actions with an add menu, a remove button and optionally an options button. -- Parameters: - - add: The menu that is opened when the add button is pressed. - - remove: The action that is called when the remove button is pressed, - giving the the selected subtab's id and index. - - options: The action that is called when the options button is pressed. - If it is nil, there is no options button. -- Returns: The new tab with the actions. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| add | The menu that is opened when the add button is pressed. | -| remove | The action that is called when the remove button is pressed, giving the the selected subtab’s id and index. | -| options | The action that is called when the options button is pressed. If it is nil, there is no options button. | - -### `frame(width:height:)` - -```swift -public func frame(width: CGFloat? = nil, height: CGFloat? = nil) -> Self -``` - -Set the window's width and height when this tab is open. -This is being ignored if there is more than one subtab or if there are settings actions. -- Parameters: - - width: The width. If nil, the window uses the content's width. - - height: The height. If nil, the window uses the content's height. -- Returns: The settings tab with the new window size. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| width | The width. If nil, the window uses the content’s width. | -| height | The height. If nil, the window uses the content’s height. | - -### `width(_:)` - -```swift -public func width(_ width: CGFloat? = nil) -> Self -``` - -Set the window's width when this tab is open without affecting the height. -This is being ignored if there is more than one subtab or if there are settings actions. -- Parameter width: The width. If nil, the window uses the content's width. -- Returns: The settings tab with the new window size. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| width | The width. If nil, the window uses the content’s width. | - -### `height(_:)` - -```swift -public func height(_ height: CGFloat? = nil) -> Self -``` - -Set the window's height when this tab is open without affecting the width. -This is being ignored if there is more than one subtab or if there are settings actions. -- Parameter height: The height. If nil, the window uses the content's height. -- Returns: The settings tab with the new window size. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| height | The height. If nil, the window uses the content’s height. | \ No newline at end of file +| content | The content of the settings tab. | \ No newline at end of file diff --git a/Icons/SidebarDesign.png b/Icons/SidebarDesign.png index 1f4e568..c982455 100644 Binary files a/Icons/SidebarDesign.png and b/Icons/SidebarDesign.png differ diff --git a/README.md b/README.md index 27b29b1..cb3e01d 100644 --- a/README.md +++ b/README.md @@ -54,19 +54,20 @@ An example app project is available [here.][4] * [Add a Settings Window][6] * [Tabs & Subtabs][7] * [Actions][8] +* [The Sidebar Design][9] ## Thanks ### Dependencies -- [SFSafeSymbols][9] licensed under the [MIT license][10] -- [SwiftLintPlugin][11] licensed under the [MIT license][12] -- [ColibriComponents][13] licensed under the [MIT license][14] +- [SFSafeSymbols][10] licensed under the [MIT license][11] +- [SwiftLintPlugin][12] licensed under the [MIT license][13] +- [ColibriComponents][14] licensed under the [MIT license][15] ### Other Thanks -- The [contributors][15] -- [SourceDocs][16] used for generating the [docs][17] -- [SwiftLint][18] for checking whether code style conventions are violated -- The programming language [Swift][19] +- The [contributors][16] +- [SourceDocs][17] used for generating the [docs][18] +- [SwiftLint][19] for checking whether code style conventions are violated +- The programming language [Swift][20] [1]: #installation [2]: #usage @@ -76,16 +77,17 @@ An example app project is available [here.][4] [6]: user-manual/Usage/AddSettingsWindow.md [7]: user-manual/Usage/TabsAndSubtabs.md [8]: user-manual/Usage/Actions.md -[9]: https://github.com/SFSafeSymbols/SFSafeSymbols -[10]: https://github.com/SFSafeSymbols/SFSafeSymbols/blob/stable/LICENSE -[11]: https://github.com/lukepistrol/SwiftLintPlugin -[12]: https://github.com/lukepistrol/SwiftLintPlugin/blob/main/LICENSE -[13]: https://github.com/david-swift/ColibriComponents-macOS -[14]: https://github.com/david-swift/ColibriComponents-macOS/blob/main/LICENSE.md -[15]: Contributors.md -[16]: https://github.com/SourceDocs/SourceDocs -[17]: Documentation/Reference/SettingsKit-macOS/README.md -[18]: https://github.com/realm/SwiftLint -[19]: https://github.com/apple/swift +[9]: user-manual/Usage/SidebarDesign.md +[10]: https://github.com/SFSafeSymbols/SFSafeSymbols +[11]: https://github.com/SFSafeSymbols/SFSafeSymbols/blob/stable/LICENSE +[12]: https://github.com/lukepistrol/SwiftLintPlugin +[13]: https://github.com/lukepistrol/SwiftLintPlugin/blob/main/LICENSE +[14]: https://github.com/david-swift/ColibriComponents-macOS +[15]: https://github.com/david-swift/ColibriComponents-macOS/blob/main/LICENSE.md +[16]: Contributors.md +[17]: https://github.com/SourceDocs/SourceDocs +[18]: Documentation/Reference/SettingsKit-macOS/README.md +[19]: https://github.com/realm/SwiftLint +[20]: https://github.com/apple/swift [image-1]: Icons/GitHubBanner.png \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md index 2a16562..c03f3ee 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -8,9 +8,11 @@ * [Add a Settings Window][3] * [Tabs & Subtabs][4] * [Actions][5] +* [The Sidebar Design][6] [1]: README.md [2]: user-manual/GettingStarted.md [3]: user-manual/Usage/AddSettingsWindow.md [4]: user-manual/Usage/TabsAndSubtabs.md -[5]: user-manual/Usage/Actions.md \ No newline at end of file +[5]: user-manual/Usage/Actions.md +[6]: user-manual/Usage/SidebarDesign.md diff --git a/Sources/SettingsKit/Components/SettingsKitScene.swift b/Sources/SettingsKit/Components/SettingsKitScene.swift index aa797c6..4e9eea0 100755 --- a/Sources/SettingsKit/Components/SettingsKitScene.swift +++ b/Sources/SettingsKit/Components/SettingsKitScene.swift @@ -52,13 +52,17 @@ struct SettingsKitScene: Scene where Content: Scene { Section { ForEach(settings.filter { tab in if case let .new(title: title, icon: _) = tab.type { - return title.lowercased().contains(search.lowercased()) || search.isEmpty - } else { - return false + let search = search.lowercased() + let contentContains = tab.content.contains { subtab in + if case let .new(title: title, icon: _) = subtab.type { + return title.lowercased().contains(search) + } + return false + } + return title.lowercased().contains(search) || search.isEmpty || contentContains } - }) { tab in - tab.sidebarLabel - } + return false + }) { $0.sidebarLabel } } } let tab = settings.first { $0.id == SettingsModel.shared.selectedTab } @@ -81,6 +85,11 @@ struct SettingsKitScene: Scene where Content: Scene { window?.toolbarStyle = .unified window?.toolbar?.displayMode = .iconOnly } + .onAppear { + if !settings.contains(where: { $0.id == model.selectedTab }), let id = settings.first?.id { + model.selectedTab = id + } + } } /// The view with the tab design. diff --git a/Sources/SettingsKit/Model/Data/SettingsTab.swift b/Sources/SettingsKit/Model/Data/SettingsTab.swift index 76839a7..5db28f8 100755 --- a/Sources/SettingsKit/Model/Data/SettingsTab.swift +++ b/Sources/SettingsKit/Model/Data/SettingsTab.swift @@ -12,7 +12,7 @@ import SwiftUI public struct SettingsTab: Identifiable, View { /// The instance of the settings model. - @StateObject private var model = SettingsModel.shared + @StateObject var model = SettingsModel.shared /// The tab's identifier. public let id: String /// The tab's type. @@ -21,6 +21,10 @@ public struct SettingsTab: Identifiable, View { public var color: Color /// The tab's content. public var content: [SettingsSubtab] + /// The view above the list of the subtabs in the sidebar style settings window. + public var top: AnyView? + /// The view below the list of the subtabs in the sidebar style settings window. + public var bottom: AnyView? /// The sidebar actions view. public var sidebarActions: [ToolbarGroup] /// The settings window's width. @@ -29,14 +33,14 @@ public struct SettingsTab: Identifiable, View { public var windowHeight: CGFloat? = .settingsHeight /// The tab's content, but without the subtabs with the ``TabType.noSelection`` type. - private var contentWithoutNoSelectionSubtabs: [SettingsSubtab] { + var contentWithoutNoSelectionSubtabs: [SettingsSubtab] { content.filter { !$0.type.isNoSelection } } /// The view containing all the subtabs. public var body: some View { if content.count <= 1 && sidebarActions.isEmpty { - content.first + content.first? .frame(width: windowWidth, height: windowHeight) } else { HSplitView { @@ -48,7 +52,7 @@ public struct SettingsTab: Identifiable, View { } /// The tab's sidebar containing all the subtabs. - private var sidebar: some View { + var sidebar: some View { VStack { sidebarList .overlay(alignment: .bottom) { Divider() } @@ -62,8 +66,8 @@ public struct SettingsTab: Identifiable, View { } /// The list in the tab's sidebar. - private var sidebarList: some View { - Group { + var sidebarList: some View { + contentView { if #available(macOS 13, *) { let notOptional = model.selectedSubtabs[id] ?? "" List( @@ -85,40 +89,46 @@ public struct SettingsTab: Identifiable, View { } } } - .onChange(of: model.selectedSubtabs[id]) { newValue in - if !contentWithoutNoSelectionSubtabs.contains(where: { $0.id == newValue }) { - updateSubtabSelection(ids: contentWithoutNoSelectionSubtabs.map { $0.id }) - } - } - .onChange(of: contentWithoutNoSelectionSubtabs.map { $0.id }) { updateSubtabSelection(ids: $0) } - .onAppear { updateSubtabSelection(ids: contentWithoutNoSelectionSubtabs.map { $0.id }) } } /// The body if the sidebar layout is active. @available(macOS 13, *) @ViewBuilder var sidebarBody: some View { - if content.count <= 1 { body } else { - NavigationStack { - Form { - ForEach(content) { content in - if !content.type.isNoSelection { - NavigationLink(value: content.id) { content.sidebarLabel } - } + contentView { + if content.count <= 1 && top == nil && bottom == nil { body } else { + NavigationStack(path: .init { () -> [String] in + if content.contains(where: { $0.id == model.selectedSubtabs[id] }) { + return [model.selectedSubtabs[id] ?? ""] } - if !sidebarActions.isEmpty { - let bottomPadding = 5.0 - sidebarActions - .padding(.bottom, bottomPadding) + return [] + } set: { newValue in + guard let first = newValue.first else { + return + } + model.selectedSubtabs[id] = first + }) { + Form { + top + ForEach(content) { content in + if !content.type.isNoSelection { + NavigationLink(value: content.id) { content.sidebarLabel } + } + } + if !sidebarActions.isEmpty { + let bottomPadding = 5.0 + sidebarActions.padding(.bottom, bottomPadding) + } + bottom } + .formStyle(.grouped) + .navigationDestination(for: String.self) { content[id: $0]?.body.navigationSubtitle("Hi") } } - .formStyle(.grouped) - .navigationDestination(for: String.self) { content[id: $0]?.body } } } } /// The selected subtab's content. - private var contentView: some View { + var contentView: some View { Form { if let first = contentWithoutNoSelectionSubtabs.first(where: { $0.id == model.selectedSubtabs[id] }) { first @@ -168,179 +178,4 @@ public struct SettingsTab: Identifiable, View { sidebarActions = [] } - /// A row in the sidebar list. - /// - Parameter subtab: The subtab of the row. - /// - Returns: The row. - @ViewBuilder - private func listContent(subtab: SettingsSubtab) -> some View { - if #available(macOS 13, *) { - subtab.label - .tag(subtab.id) - .listRowSeparator(.hidden) - } else { - subtab.label - .tag(subtab.id) - } - } - - /// Update the selection of the subtab. - /// - Parameter ids: The identifiers of the subtabs. - private func updateSubtabSelection(ids: [String]) { - if let first = ids.first(where: { id in - !content.contains { $0.id == id } - }) { - model.selectedSubtabs[id] = first - } else if content.count > ids.count { - let index = contentWithoutNoSelectionSubtabs.firstIndex { $0.id == model.selectedSubtabs[id] } - if let after = ids[safe: index ?? ids.count] { - model.selectedSubtabs[id] = after - } else if let before = ids[safe: (index ?? 0) - 1] { - model.selectedSubtabs[id] = before - } else { - model.selectedSubtabs[id] = ids.last ?? "" - } - } else if !ids.contains(model.selectedSubtabs[id] ?? ""), let last = ids.last { - model.selectedSubtabs[id] = last - } - } - - /// Adds actions to the settings sidebar. - /// - Parameter content: The actions. - /// - Returns: The new tab with the actions. - public func actions(@ArrayBuilder content: () -> [ToolbarGroup]) -> Self { - actions(content: content()) - } - - /// Add actions to the settings sidebar by providing an array. - /// - Parameter content: The actions as an array.. - /// - Returns: The new tab with the actions. - public func actions(content: [ToolbarGroup]) -> Self { - var newTab = self - newTab.sidebarActions = content - return newTab - } - - /// The standard set of actions with an add button, a remove button and optionally an options button. - /// - Parameters: - /// - add: The action that is called when the add button is pressed. - /// - remove: The action that is called when the remove button is pressed, - /// giving the the selected subtab's id and index. - /// - options: The action that is called when the options button is pressed. - /// If it is nil, there is no options button. - /// - Returns: The new tab with the actions. - public func standardActions( - add: @escaping () -> Void, - remove: @escaping (String?, Int?) -> Void, - options: (() -> Void)? = nil - ) -> Self { - actions { - ToolbarGroup { - ToolbarAction( - .init(localized: "Add", comment: "SettingsTab (Label of the standard \"Add\" action)"), - systemSymbol: .plus, - action: add - ) - ToolbarAction( - .init(localized: "Remove", comment: "SettingsTab (Label of the standard \"Remove\" action)"), - systemSymbol: .minus - ) { - let index = content.firstIndex { $0.id == SettingsModel.shared.selectedSubtabs[id] } - remove(content[safe: index]?.id, index) - } - } - .spacer() - if let options { - ToolbarGroup { - ToolbarAction( - .init( - localized: "Options", - comment: "SettingsTab (Label of the standard \"Options\" action)" - ), - systemSymbol: .ellipsis, - action: options - ) - } - } - } - } - - /// The standard set of actions with an add menu, a remove button and optionally an options button. - /// - Parameters: - /// - add: The menu that is opened when the add button is pressed. - /// - remove: The action that is called when the remove button is pressed, - /// giving the the selected subtab's id and index. - /// - options: The action that is called when the options button is pressed. - /// If it is nil, there is no options button. - /// - Returns: The new tab with the actions. - public func standardActions( - @ViewBuilder add: @escaping () -> ContentView, - remove: @escaping (String?, Int?) -> Void, - options: (() -> Void)? = nil - ) -> Self where ContentView: View { - actions { - ToolbarGroup { - ToolbarMenu( - .init( - localized: "Add", - comment: "SettingsTab (Label of the standard \"Add\" action)" - ), - systemSymbol: .plus - ) { add() } - ToolbarAction( - .init(localized: "Remove", comment: "SettingsTab (Label of the standard \"Remove\" action)"), - systemSymbol: .minus - ) { - let index = content.firstIndex { $0.id == SettingsModel.shared.selectedSubtabs[id] } - remove(content[safe: index]?.id, index) - } - } - .spacer() - if let options { - ToolbarGroup { - ToolbarAction( - .init( - localized: "Options", - comment: "SettingsTab (Label of the standard \"Options\" action)" - ), - systemSymbol: .ellipsis, - action: options - ) - } - } - } - } - - /// Set the window's width and height when this tab is open. - /// This is being ignored if there is more than one subtab or if there are settings actions. - /// - Parameters: - /// - width: The width. If nil, the window uses the content's width. - /// - height: The height. If nil, the window uses the content's height. - /// - Returns: The settings tab with the new window size. - public func frame(width: CGFloat? = nil, height: CGFloat? = nil) -> Self { - var newSelf = self - newSelf.windowWidth = width - newSelf.windowHeight = height - return newSelf - } - - /// Set the window's width when this tab is open without affecting the height. - /// This is being ignored if there is more than one subtab or if there are settings actions. - /// - Parameter width: The width. If nil, the window uses the content's width. - /// - Returns: The settings tab with the new window size. - public func width(_ width: CGFloat? = nil) -> Self { - var newSelf = self - newSelf.windowWidth = width - return newSelf - } - - /// Set the window's height when this tab is open without affecting the width. - /// This is being ignored if there is more than one subtab or if there are settings actions. - /// - Parameter height: The height. If nil, the window uses the content's height. - /// - Returns: The settings tab with the new window size. - public func height(_ height: CGFloat? = nil) -> Self { - var newSelf = self - newSelf.windowHeight = height - return newSelf - } - } diff --git a/Sources/SettingsKit/Model/Extensions/SettingsTab+.swift b/Sources/SettingsKit/Model/Extensions/SettingsTab+.swift new file mode 100644 index 0000000..cfb541e --- /dev/null +++ b/Sources/SettingsKit/Model/Extensions/SettingsTab+.swift @@ -0,0 +1,220 @@ +// +// SettingsTab+.swift +// SettingsKit +// +// Created by david-swift on 09.10.2023. +// + +import ColibriComponents +import SwiftUI + +extension SettingsTab { + + /// Wrap the view with the required observers. + /// - Parameter _: The view. + /// - Returns: The view with observers. + func contentView(@ViewBuilder _ content: () -> Content) -> some View where Content: View { + content() + .onChange(of: model.selectedSubtabs[id]) { newValue in + if !contentWithoutNoSelectionSubtabs.contains(where: { $0.id == newValue }) { + updateSubtabSelection(ids: contentWithoutNoSelectionSubtabs.map { $0.id }) + } + } + .onChange(of: contentWithoutNoSelectionSubtabs.map { $0.id }) { updateSubtabSelection(ids: $0) } + .onAppear { updateSubtabSelection(ids: contentWithoutNoSelectionSubtabs.map { $0.id }) } + } + + /// A row in the sidebar list. + /// - Parameter subtab: The subtab of the row. + /// - Returns: The row. + @ViewBuilder + func listContent(subtab: SettingsSubtab) -> some View { + if #available(macOS 13, *) { + subtab.label + .tag(subtab.id) + .listRowSeparator(.hidden) + } else { + subtab.label + .tag(subtab.id) + } + } + + /// Update the selection of the subtab. + /// - Parameter ids: The identifiers of the subtabs. + func updateSubtabSelection(ids: [String]) { + if let first = ids.first(where: { id in + !content.contains { $0.id == id } + }) { + model.selectedSubtabs[id] = first + } else if content.count > ids.count { + let index = contentWithoutNoSelectionSubtabs.firstIndex { $0.id == model.selectedSubtabs[id] } + if let after = ids[safe: index ?? ids.count] { + model.selectedSubtabs[id] = after + } else if let before = ids[safe: (index ?? 0) - 1] { + model.selectedSubtabs[id] = before + } else { + model.selectedSubtabs[id] = ids.last ?? "" + } + } else if !ids.contains(model.selectedSubtabs[id] ?? ""), let last = ids.last { + model.selectedSubtabs[id] = last + } + } + + /// Adds actions to the settings sidebar. + /// - Parameter content: The actions. + /// - Returns: The new tab with the actions. + public func actions(@ArrayBuilder content: () -> [ToolbarGroup]) -> Self { + actions(content: content()) + } + + /// Add actions to the settings sidebar by providing an array. + /// - Parameter content: The actions as an array.. + /// - Returns: The new tab with the actions. + public func actions(content: [ToolbarGroup]) -> Self { + var newTab = self + newTab.sidebarActions = content + return newTab + } + + /// The standard set of actions with an add button, a remove button and optionally an options button. + /// - Parameters: + /// - add: The action that is called when the add button is pressed. + /// - remove: The action that is called when the remove button is pressed, + /// giving the the selected subtab's id and index. + /// - options: The action that is called when the options button is pressed. + /// If it is nil, there is no options button. + /// - Returns: The new tab with the actions. + public func standardActions( + add: @escaping () -> Void, + remove: @escaping (String?, Int?) -> Void, + options: (() -> Void)? = nil + ) -> Self { + actions { + ToolbarGroup { + ToolbarAction( + .init(localized: "Add", comment: "SettingsTab (Label of the standard \"Add\" action)"), + systemSymbol: .plus, + action: add + ) + ToolbarAction( + .init(localized: "Remove", comment: "SettingsTab (Label of the standard \"Remove\" action)"), + systemSymbol: .minus + ) { + let index = content.firstIndex { $0.id == SettingsModel.shared.selectedSubtabs[id] } + remove(content[safe: index]?.id, index) + } + } + .spacer() + if let options { + ToolbarGroup { + ToolbarAction( + .init( + localized: "Options", + comment: "SettingsTab (Label of the standard \"Options\" action)" + ), + systemSymbol: .ellipsis, + action: options + ) + } + } + } + } + + /// The standard set of actions with an add menu, a remove button and optionally an options button. + /// - Parameters: + /// - add: The menu that is opened when the add button is pressed. + /// - remove: The action that is called when the remove button is pressed, + /// giving the the selected subtab's id and index. + /// - options: The action that is called when the options button is pressed. + /// If it is nil, there is no options button. + /// - Returns: The new tab with the actions. + public func standardActions( + @ViewBuilder add: @escaping () -> ContentView, + remove: @escaping (String?, Int?) -> Void, + options: (() -> Void)? = nil + ) -> Self where ContentView: View { + actions { + ToolbarGroup { + ToolbarMenu( + .init( + localized: "Add", + comment: "SettingsTab (Label of the standard \"Add\" action)" + ), + systemSymbol: .plus + ) { add() } + ToolbarAction( + .init(localized: "Remove", comment: "SettingsTab (Label of the standard \"Remove\" action)"), + systemSymbol: .minus + ) { + let index = content.firstIndex { $0.id == SettingsModel.shared.selectedSubtabs[id] } + remove(content[safe: index]?.id, index) + } + } + .spacer() + if let options { + ToolbarGroup { + ToolbarAction( + .init( + localized: "Options", + comment: "SettingsTab (Label of the standard \"Options\" action)" + ), + systemSymbol: .ellipsis, + action: options + ) + } + } + } + } + + /// Set the window's width and height when this tab is open. + /// This is being ignored if there is more than one subtab or if there are settings actions. + /// - Parameters: + /// - width: The width. If nil, the window uses the content's width. + /// - height: The height. If nil, the window uses the content's height. + /// - Returns: The settings tab with the new window size. + public func frame(width: CGFloat? = nil, height: CGFloat? = nil) -> Self { + var newSelf = self + newSelf.windowWidth = width + newSelf.windowHeight = height + return newSelf + } + + /// Set the window's width when this tab is open without affecting the height. + /// This is being ignored if there is more than one subtab or if there are settings actions. + /// - Parameter width: The width. If nil, the window uses the content's width. + /// - Returns: The settings tab with the new window size. + public func width(_ width: CGFloat? = nil) -> Self { + var newSelf = self + newSelf.windowWidth = width + return newSelf + } + + /// Set the window's height when this tab is open without affecting the width. + /// This is being ignored if there is more than one subtab or if there are settings actions. + /// - Parameter height: The height. If nil, the window uses the content's height. + /// - Returns: The settings tab with the new window size. + public func height(_ height: CGFloat? = nil) -> Self { + var newSelf = self + newSelf.windowHeight = height + return newSelf + } + + /// Set the content above the list of subtabs. + /// - Parameter view: The content. + /// - Returns: The settings tab. + public func top(_ view: () -> Top) -> Self where Top: View { + var newSelf = self + newSelf.top = .init(view()) + return newSelf + } + + /// Set the content below the list of subtabs. + /// - Parameter view: The content. + /// - Returns: The settings tab. + public func bottom(_ view: () -> Bottom) -> Self where Bottom: View { + var newSelf = self + newSelf.bottom = .init(view()) + return newSelf + } + +} diff --git a/Tests/Examples/Examples/ContentView.swift b/Tests/Examples/Examples/ContentView.swift index b98a827..9b08b0d 100644 --- a/Tests/Examples/Examples/ContentView.swift +++ b/Tests/Examples/Examples/ContentView.swift @@ -17,6 +17,7 @@ struct ContentView: View { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) + .accessibilityHidden(true) Text("Hello, world!") } .padding() diff --git a/Tests/Examples/Examples/ExamplesApp.swift b/Tests/Examples/Examples/ExamplesApp.swift index 3c3a741..2a9a4c3 100644 --- a/Tests/Examples/Examples/ExamplesApp.swift +++ b/Tests/Examples/Examples/ExamplesApp.swift @@ -13,8 +13,7 @@ import SwiftUI struct ExamplesApp: App { /// A test storage value. - @AppStorage("accounts-count") - var accountsCount = 0 + @State private var accounts = [0] /// A test storage value. @AppStorage("show-first-name") var firstNameBeforeLastName = true @@ -29,29 +28,64 @@ struct ExamplesApp: App { } .settings(design: defaultSettingsDesign ? .default : .sidebar) { SettingsTab(.new(title: "General", icon: .gearshape), id: "general", color: .gray) { - SettingsSubtab(.noSelection, id: "general") { - GeneralSettings() - .padding() - } + SettingsSubtab(.noSelection, id: "general") { GeneralSettings() } } .frame() - SettingsTab(.new(title: "Accounts", icon: .at), id: "accounts") { - for account in 0.. 0 { - accountsCount -= 1 + .frame() + } + } + + /// The sample "Accounts" tab. + private var accountsTab: SettingsTab { + .init(.new(title: "Accounts", icon: .at), id: "accounts") { + for account in self.accounts { + SettingsSubtab(.new(title: "Account \(account + 1)", icon: .personFill), id: "account-\(account)") { + if defaultSettingsDesign { + AccountView(account: account) + } else { + AccountView(account: account) + .toolbar { + Button("Delete Account") { + self.accounts = self.accounts.filter { $0 != account } + } + } + } } } - SettingsTab(.new(title: "Advanced", icon: .gearshape2), id: "advanced") { - SettingsSubtab(.noSelection, id: "advanced") { - Text("Advanced Settings") + } + .top { + Section { + HStack { + Text("Accounts") + Spacer() + Button("Add Account") { + self.accounts.append((self.accounts.last ?? -1) + 1) + } } } } diff --git a/Tests/Examples/Examples/GeneralSettings.swift b/Tests/Examples/Examples/GeneralSettings.swift index bafcc67..3c48d28 100644 --- a/Tests/Examples/Examples/GeneralSettings.swift +++ b/Tests/Examples/Examples/GeneralSettings.swift @@ -57,6 +57,7 @@ struct GeneralSettings: View { } .frame(width: width) .fixedSize() + .padding() } } diff --git a/user-manual/Usage/SidebarDesign.md b/user-manual/Usage/SidebarDesign.md new file mode 100644 index 0000000..5437bc5 --- /dev/null +++ b/user-manual/Usage/SidebarDesign.md @@ -0,0 +1,17 @@ +# The Sidebar Design + +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. + +## Add Views Above or Below the List of Subtabs +When using the sidebar design, it's possible to add views above the list of subtabs using the `top(_:)` or below the list using the `bottom(_:)` modifier on the settings tab: +```swift +SettingsTab(.new(title: "Accounts", icon: .at), id: "accounts") { + // Subtabs +} +.top { + // View +} +``` + +## Toolbar Items +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).