diff --git a/App/Sources/App/ContentView.swift b/App/Sources/App/ContentView.swift index 69b1924..b7c6d80 100644 --- a/App/Sources/App/ContentView.swift +++ b/App/Sources/App/ContentView.swift @@ -26,7 +26,14 @@ struct ContentView: View { } } .sheet(isPresented: $isPresentingConfigEditor) { - ConfigVariableEditor(reader: viewModel.configVariableReader) { variables in + ConfigVariableEditor( + reader: viewModel.configVariableReader, + customSectionTitle: "Actions" + ) { + Button("Do something", role: .destructive) { + print("Did something!") + } + } onSave: { variables in print(variables) } } diff --git a/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift b/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift index cb2990b..47fe671 100644 --- a/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift +++ b/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift @@ -17,15 +17,42 @@ import SwiftUI /// variable's detail view. /// /// The toolbar provides Cancel, Save, and an overflow menu with Undo, Redo, and Clear Editor Overrides actions. -struct ConfigVariableListView: View { +struct ConfigVariableListView: View { @State var viewModel: ViewModel + /// The title for the custom section at the top of the list. + private let customSectionTitle: Text + + /// The custom section content to display at the top of the list. + private let customSection: CustomSection + @Environment(\.dismiss) private var dismiss + /// Creates a new list view. + /// + /// - Parameters: + /// - viewModel: The view model for the list. + /// - customSectionTitle: The title for the custom section. + /// - customSection: A view builder that produces custom content to display in a section at the top of the list. + init(viewModel: ViewModel, customSectionTitle: Text, @ViewBuilder customSection: () -> CustomSection) { + self._viewModel = State(initialValue: viewModel) + self.customSectionTitle = customSectionTitle + self.customSection = customSection() + } + + var body: some View { NavigationStack { List { + if CustomSection.self != EmptyView.self { + Section { + customSection + } header: { + customSectionTitle + } + } + Section(localizedStringResource("editorView.variablesSection.header")) { ForEach(viewModel.variables, id: \.key) { item in NavigationLink(value: item.key) { diff --git a/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift b/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift index add75ee..608f4f6 100644 --- a/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift +++ b/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift @@ -21,45 +21,121 @@ import SwiftUI /// // Handle changed variables /// } /// } -public struct ConfigVariableEditor: View { +/// +/// To add a custom section at the top of the list, provide a section title and content: +/// +/// ConfigVariableEditor( +/// reader: reader, +/// customSectionTitle: "Actions" +/// ) { +/// Button("Reset All") { … } +/// } onSave: { changedVariables in +/// // Handle changed variables +/// } +public struct ConfigVariableEditor: View { /// The list view model created from the reader. @State private var viewModel: ConfigVariableListViewModel? + /// The title for the custom section. + private let customSectionTitle: Text - /// Creates a new configuration variable editor. + /// The custom section content. + private let customSection: CustomSection + + + /// Creates a new configuration variable editor with a custom section at the top of the list. /// /// - Parameters: /// - reader: The configuration variable reader. If the reader was not created with `isEditorEnabled` set to /// `true`, the view is empty. + /// - customSectionTitle: The title for the custom section. + /// - customSection: A view builder that produces custom content to display in a section at the top of the list. /// - onSave: A closure called with the registered variables whose overrides changed when the user saves. public init( reader: ConfigVariableReader, + customSectionTitle: LocalizedStringKey, + @ViewBuilder customSection: () -> CustomSection, onSave: @escaping ([RegisteredConfigVariable]) -> Void ) { - if let editorOverrideProvider = reader.editorOverrideProvider { - // Exclude the editor override provider from the named providers passed to the document, - // since it is always the first entry in the reader's provider list - let namedProviders = Array(reader.namedProviders.dropFirst()) - - let document = EditorDocument( - editorOverrideProvider: editorOverrideProvider, - workingCopyDisplayName: localizedString("editorOverrideProvider.name"), - namedProviders: namedProviders, - registeredVariables: Array(reader.registeredVariables.values), - undoManager: UndoManager() - ) + self.init( + reader: reader, + customSectionTitle: Text(customSectionTitle), + customSection: customSection, + onSave: onSave + ) + } - self._viewModel = State( - initialValue: ConfigVariableListViewModel(document: document, onSave: onSave) - ) - } + + /// Creates a new configuration variable editor with a custom section at the top of the list. + /// + /// - Parameters: + /// - reader: The configuration variable reader. If the reader was not created with `isEditorEnabled` set to + /// `true`, the view is empty. + /// - customSectionTitle: A `Text` view to use as the title for the custom section. + /// - customSection: A view builder that produces custom content to display in a section at the top of the list. + /// - onSave: A closure called with the registered variables whose overrides changed when the user saves. + public init( + reader: ConfigVariableReader, + customSectionTitle: Text, + @ViewBuilder customSection: () -> CustomSection, + onSave: @escaping ([RegisteredConfigVariable]) -> Void + ) { + self.customSectionTitle = customSectionTitle + self.customSection = customSection() + self._viewModel = Self.makeViewModel(reader: reader, onSave: onSave) } public var body: some View { if let viewModel { - ConfigVariableListView(viewModel: viewModel) + ConfigVariableListView( + viewModel: viewModel, + customSectionTitle: customSectionTitle, + customSection: { customSection } + ) + } + } + + + private static func makeViewModel( + reader: ConfigVariableReader, + onSave: @escaping ([RegisteredConfigVariable]) -> Void + ) -> State { + guard let editorOverrideProvider = reader.editorOverrideProvider else { + return State(initialValue: nil) } + + // Exclude the editor override provider from the named providers passed to the document, + // since it is always the first entry in the reader's provider list + let namedProviders = Array(reader.namedProviders.dropFirst()) + + let document = EditorDocument( + editorOverrideProvider: editorOverrideProvider, + workingCopyDisplayName: localizedString("editorOverrideProvider.name"), + namedProviders: namedProviders, + registeredVariables: Array(reader.registeredVariables.values), + undoManager: UndoManager() + ) + + return State(initialValue: ConfigVariableListViewModel(document: document, onSave: onSave)) + } +} + + +extension ConfigVariableEditor where CustomSection == EmptyView { + /// Creates a new configuration variable editor. + /// + /// - Parameters: + /// - reader: The configuration variable reader. If the reader was not created with `isEditorEnabled` set to + /// `true`, the view is empty. + /// - onSave: A closure called with the registered variables whose overrides changed when the user saves. + public init( + reader: ConfigVariableReader, + onSave: @escaping ([RegisteredConfigVariable]) -> Void + ) { + self.customSectionTitle = Text(verbatim: "") + self.customSection = EmptyView() + self._viewModel = Self.makeViewModel(reader: reader, onSave: onSave) } }