diff --git a/core/Sources/FileawayCore/Model/DirectoryModel.swift b/core/Sources/FileawayCore/Model/DirectoryModel.swift index cef34ed1..50987086 100644 --- a/core/Sources/FileawayCore/Model/DirectoryModel.swift +++ b/core/Sources/FileawayCore/Model/DirectoryModel.swift @@ -71,7 +71,7 @@ public class DirectoryModel: ObservableObject, Identifiable, Hashable { directoryMonitor .$files .compactMap { $0 } // nil is a marker that the data is loading - .combineLatest(settings.$types) + .combineLatest(settings.$fileTypes) .receive(on: syncQueue) .map { files, types in let files = files diff --git a/core/Sources/FileawayCore/Model/FileTypePickerModel.swift b/core/Sources/FileawayCore/Model/FileTypesViewModel.swift similarity index 68% rename from core/Sources/FileawayCore/Model/FileTypePickerModel.swift rename to core/Sources/FileawayCore/Model/FileTypesViewModel.swift index 3b5464fa..b415bdc3 100644 --- a/core/Sources/FileawayCore/Model/FileTypePickerModel.swift +++ b/core/Sources/FileawayCore/Model/FileTypesViewModel.swift @@ -24,17 +24,18 @@ import UniformTypeIdentifiers import Interact -public class FileTypePickerModel: ObservableObject, Runnable { +public class FileTypesViewModel: ObservableObject, Runnable { - @Published public var types: [UTType] = [] + @Published public var fileTypes: [UTType] = [] @Published public var selection: Set = [] - @Published public var input: String = "" + @Published public var newFilenameExtension: String = "" + @Published public var proposedFileType: UTType? = nil private var settings: Settings private var cancellables: Set = [] @MainActor public var canSubmit: Bool { - return !self.input.isEmpty + return !self.newFilenameExtension.isEmpty } public init(settings: Settings) { @@ -44,7 +45,7 @@ public class FileTypePickerModel: ObservableObject, Runnable { @MainActor public func start() { settings - .$types + .$fileTypes .map { types in return Array(types) .sorted { lhs, rhs in @@ -53,10 +54,16 @@ public class FileTypePickerModel: ObservableObject, Runnable { } .receive(on: DispatchQueue.main) .sink { types in - self.types = types + self.fileTypes = types } .store(in: &cancellables) + $newFilenameExtension + .receive(on: DispatchQueue.main) + .sink { newFilenameExtension in + self.proposedFileType = UTType(filenameExtension: newFilenameExtension) + } + .store(in: &cancellables) } @MainActor public func stop() { @@ -64,20 +71,25 @@ public class FileTypePickerModel: ObservableObject, Runnable { } @MainActor public func submit() { - guard let type = UTType(filenameExtension: input) else { - // TODO: Print unknown type?? - print("Unknown type '\(input)'.") + guard let proposedFileType = proposedFileType else { return } - self.settings.types.insert(type) - input = "" + self.settings.fileTypes.insert(proposedFileType) + newFilenameExtension = "" } @MainActor public func remove(_ ids: Set) { - self.settings.types = self.settings.types.filter { !ids.contains($0.id) } + self.settings.fileTypes = self.settings.fileTypes.filter { !ids.contains($0.id) } for id in ids { selection.remove(id) } } + @MainActor public func remove(_ indexSet: IndexSet) { + let ids = indexSet.map { index in + return self.fileTypes[index].id + } + self.remove(Set(ids)) + } + } diff --git a/core/Sources/FileawayCore/Model/Settings.swift b/core/Sources/FileawayCore/Model/Settings.swift index 9e774fba..f0b46583 100644 --- a/core/Sources/FileawayCore/Model/Settings.swift +++ b/core/Sources/FileawayCore/Model/Settings.swift @@ -45,9 +45,9 @@ public class Settings: ObservableObject { @Published public var inboxUrls: [URL] = [] @Published public var archiveUrls: [URL] = [] - @Published public var types: Set = [] { + @Published public var fileTypes: Set = [] { didSet { - try? defaults.setCodable(types, for: .fileTypes) + try? defaults.setCodable(fileTypes, for: .fileTypes) } } @@ -56,7 +56,7 @@ public class Settings: ObservableObject { public init() { inboxUrls = (try? defaults.securityScopeURLs(for: .inboxUrls)) ?? [] archiveUrls = (try? defaults.securityScopeURLs(for: .archiveUrls)) ?? [] - types = (try? defaults.codable(Set.self, for: .fileTypes)) ?? Self.defaultFileTypes + fileTypes = (try? defaults.codable(Set.self, for: .fileTypes)) ?? Self.defaultFileTypes } public func setInboxUrls(_ urls: [URL]) throws { diff --git a/interact b/interact index 3e290cba..ccaf51e5 160000 --- a/interact +++ b/interact @@ -1 +1 @@ -Subproject commit 3e290cba9ef5f9f97d116be0dbb2124d0a41eccf +Subproject commit ccaf51e59c64faf8eba18f6944417f1456b85464 diff --git a/ios/Fileaway-iOS.xcodeproj/project.pbxproj b/ios/Fileaway-iOS.xcodeproj/project.pbxproj index 2955f5b3..83b90b1a 100644 --- a/ios/Fileaway-iOS.xcodeproj/project.pbxproj +++ b/ios/Fileaway-iOS.xcodeproj/project.pbxproj @@ -9,7 +9,9 @@ /* Begin PBXBuildFile section */ D81C5D93249BE3710083BD6F /* RulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81C5D92249BE3710083BD6F /* RulesView.swift */; }; D81C5D95249BE6F40083BD6F /* RuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81C5D94249BE6F40083BD6F /* RuleView.swift */; }; + D85067FA292B8DE80017865A /* FileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85067F9292B8DE80017865A /* FileTypesView.swift */; }; D85532A0291E5FB400C21E12 /* WizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D855329F291E5FB400C21E12 /* WizardView.swift */; }; + D864870D292CB99C007CD688 /* AddFileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D864870C292CB99C007CD688 /* AddFileTypesView.swift */; }; D86AA556291DB7E00078B2DA /* FilePicker in Frameworks */ = {isa = PBXBuildFile; productRef = D86AA555291DB7E00078B2DA /* FilePicker */; }; D88B353D28B61247000C8910 /* FileawayCore in Frameworks */ = {isa = PBXBuildFile; productRef = D88B353C28B61247000C8910 /* FileawayCore */; }; D8A871D22916D56400B2932A /* ComponentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A871D12916D56400B2932A /* ComponentItem.swift */; }; @@ -55,7 +57,9 @@ /* Begin PBXFileReference section */ D81C5D92249BE3710083BD6F /* RulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesView.swift; sourceTree = ""; }; D81C5D94249BE6F40083BD6F /* RuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleView.swift; sourceTree = ""; }; + D85067F9292B8DE80017865A /* FileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypesView.swift; sourceTree = ""; }; D855329F291E5FB400C21E12 /* WizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardView.swift; sourceTree = ""; }; + D864870C292CB99C007CD688 /* AddFileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFileTypesView.swift; sourceTree = ""; }; D866D66326197AC80025329E /* FileawayCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FileawayCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D86AA554291DB7BF0078B2DA /* FilePicker */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FilePicker; path = ../FilePicker; sourceTree = ""; }; D8A871D12916D56400B2932A /* ComponentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentItem.swift; sourceTree = ""; }; @@ -232,6 +236,8 @@ D8F763D6292255A30051D740 /* Settings */ = { isa = PBXGroup; children = ( + D864870C292CB99C007CD688 /* AddFileTypesView.swift */, + D85067F9292B8DE80017865A /* FileTypesView.swift */, D8CCD292246EDC6A007EF2BB /* SettingsView.swift */, ); path = Settings; @@ -411,9 +417,11 @@ D8CCD293246EDC6A007EF2BB /* SettingsView.swift in Sources */, D8E4CA57249CF76500F5BC8E /* EditSafeButton.swift in Sources */, D8F763DA292259960051D740 /* RulePicker.swift in Sources */, + D85067FA292B8DE80017865A /* FileTypesView.swift in Sources */, D8CAE3EE249D3AD70047CA36 /* ErrorText.swift in Sources */, D81C5D93249BE3710083BD6F /* RulesView.swift in Sources */, D8E4CA5D249D061D00F5BC8E /* EditableText.swift in Sources */, + D864870D292CB99C007CD688 /* AddFileTypesView.swift in Sources */, D8D81EB9291DD23800870E6E /* VariableRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Fileaway/Views/Settings/AddFileTypesView.swift b/ios/Fileaway/Views/Settings/AddFileTypesView.swift new file mode 100644 index 00000000..d2b612fe --- /dev/null +++ b/ios/Fileaway/Views/Settings/AddFileTypesView.swift @@ -0,0 +1,77 @@ +// Copyright (c) 2018-2022 InSeven Limited +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import MobileCoreServices +import SwiftUI +import UniformTypeIdentifiers + +import Interact + +import FileawayCore + +struct AddFileTypeView: View { + + enum Focus: Hashable { + case add + } + + @Environment(\.dismiss) var dismiss + + @ObservedObject var model: FileTypesViewModel + @FocusState private var focus: Focus? + + var body: some View { + NavigationView { + Form { + Section { + TextField("File Extension", text: $model.newFilenameExtension) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .focused($focus, equals: .add) + } + Section { + if let proposedFileType = model.proposedFileType { + Button { + model.submit() + dismiss() + } label: { + LabeledContent(proposedFileType.localizedDisplayName, + value: proposedFileType.preferredFilenameExtension ?? "") + } + .tint(.primary) + } + } + } + .defaultFocus($focus, .add) + .navigationTitle("Add File Type") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + + } + } + } + +} diff --git a/ios/Fileaway/Views/Settings/FileTypesView.swift b/ios/Fileaway/Views/Settings/FileTypesView.swift new file mode 100644 index 00000000..e9ad01e8 --- /dev/null +++ b/ios/Fileaway/Views/Settings/FileTypesView.swift @@ -0,0 +1,73 @@ +// Copyright (c) 2018-2022 InSeven Limited +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import MobileCoreServices +import SwiftUI +import UniformTypeIdentifiers + +import FileawayCore + +struct FileTypesView: View { + + enum SheetType: Identifiable { + + var id: Self { self } + + case add + } + + @StateObject var model: FileTypesViewModel + @State var sheet: SheetType? + + init(settings: Settings) { + _model = StateObject(wrappedValue: FileTypesViewModel(settings: settings)) + } + + var body: some View { + List { + ForEach(model.fileTypes) { fileType in + LabeledContent(fileType.localizedDisplayName, value: fileType.preferredFilenameExtension ?? "") + } + .onDelete { indexSet in + model.remove(indexSet) + } + } + .navigationTitle("File Types") + .toolbar { + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + sheet = .add + } label: { + Label("Add File Type", systemImage: "plus") + } + } + + } + .sheet(item: $sheet) { sheet in + switch sheet { + case .add: + AddFileTypeView(model: model) + } + } + .runs(model) + } + +} diff --git a/ios/Fileaway/Views/Settings/SettingsView.swift b/ios/Fileaway/Views/Settings/SettingsView.swift index 9fd08a2a..06793f0d 100644 --- a/ios/Fileaway/Views/Settings/SettingsView.swift +++ b/ios/Fileaway/Views/Settings/SettingsView.swift @@ -33,6 +33,7 @@ struct SettingsView: View { } @ObservedObject var applicationModel: ApplicationModel + @ObservedObject var settings: Settings @Environment(\.dismiss) private var dismiss @State private var sheet: SheetType? = nil @@ -40,12 +41,25 @@ struct SettingsView: View { var body: some View { NavigationStack { Form { + Section("General") { + NavigationLink { + FileTypesView(settings: settings) + } label: { + Text("File Types") + .badge(settings.fileTypes.count) + } + } Section("Rules") { - ForEach(applicationModel.directories(type: .archive)) { directory in - NavigationLink { - RulesView(rulesModel: directory.ruleSet) - } label: { - Text(directory.name) + if applicationModel.directories(type: .archive).isEmpty { + Text("None") + .foregroundColor(.secondary) + } else { + ForEach(applicationModel.directories(type: .archive)) { directory in + NavigationLink { + RulesView(rulesModel: directory.ruleSet) + } label: { + Text(directory.name) + } } } } diff --git a/ios/Fileaway/Views/Viewer/ContentView.swift b/ios/Fileaway/Views/Viewer/ContentView.swift index a4730c95..b04659d6 100644 --- a/ios/Fileaway/Views/Viewer/ContentView.swift +++ b/ios/Fileaway/Views/Viewer/ContentView.swift @@ -62,7 +62,7 @@ struct ContentView: View { .sheet(item: $sceneModel.sheet) { sheet in switch sheet { case .settings: - SettingsView(applicationModel: applicationModel) + SettingsView(applicationModel: applicationModel, settings: applicationModel.settings) case .addLocation(let type): FilePickerUIRepresentable(types: [.folder], allowMultiple: false, asCopy: false) { urls in guard let url = urls.first else { diff --git a/macos/Fileaway-macOS.xcodeproj/project.pbxproj b/macos/Fileaway-macOS.xcodeproj/project.pbxproj index 003d2813..0da955b0 100644 --- a/macos/Fileaway-macOS.xcodeproj/project.pbxproj +++ b/macos/Fileaway-macOS.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ D8037B122617F8CD00F41971 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D8037B112617F8CD00F41971 /* Introspect */; }; D82D73D728B4C659006F49F1 /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82D73D628B4C659006F49F1 /* Wizard.swift */; }; D8384906292AE7EB00EA2595 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8384905292AE7EB00EA2595 /* GeneralSettingsView.swift */; }; - D8384908292AE96800EA2595 /* FileTypePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8384907292AE96800EA2595 /* FileTypePickerView.swift */; }; + D8384908292AE96800EA2595 /* FileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8384907292AE96800EA2595 /* FileTypesView.swift */; }; D83CDCEA2620FF210063C91C /* RuleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83CDCE92620FF210063C91C /* RuleSheet.swift */; }; D85C95812579CED30007AE1E /* SelectionToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85C95802579CED30007AE1E /* SelectionToolbar.swift */; }; D87DEF7F25774F6C006CFE85 /* FileawayApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87DEF7E25774F6C006CFE85 /* FileawayApp.swift */; }; @@ -68,7 +68,7 @@ /* Begin PBXFileReference section */ D82D73D628B4C659006F49F1 /* Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wizard.swift; sourceTree = ""; }; D8384905292AE7EB00EA2595 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; - D8384907292AE96800EA2595 /* FileTypePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypePickerView.swift; sourceTree = ""; }; + D8384907292AE96800EA2595 /* FileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypesView.swift; sourceTree = ""; }; D83CDCE92620FF210063C91C /* RuleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleSheet.swift; sourceTree = ""; }; D85C95802579CED30007AE1E /* SelectionToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionToolbar.swift; sourceTree = ""; }; D876A3B32654873E006D19FB /* FileawayCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FileawayCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -278,7 +278,7 @@ D8F2524E2923A7DE00CC30AF /* ComponentValueTextField.swift */, D8F2523F2922E5A600CC30AF /* ComponentView.swift */, D8E7F9E22599B00A00C7409B /* DestinationTable.swift */, - D8384907292AE96800EA2595 /* FileTypePickerView.swift */, + D8384907292AE96800EA2595 /* FileTypesView.swift */, D8384905292AE7EB00EA2595 /* GeneralSettingsView.swift */, D8B657ED2642D40F009DE837 /* LocationsEditor.swift */, D8FC5349257BB7A4008FB609 /* LocationsSettingsView.swift */, @@ -480,7 +480,7 @@ D8F252522923BF5B00CC30AF /* NSItemProvider.swift in Sources */, D8F252402922E5A600CC30AF /* ComponentView.swift in Sources */, D8F8BE3725881C1A0010432F /* RulePicker.swift in Sources */, - D8384908292AE96800EA2595 /* FileTypePickerView.swift in Sources */, + D8384908292AE96800EA2595 /* FileTypesView.swift in Sources */, D8F252442922E62400CC30AF /* VariableNameTextField.swift in Sources */, D85C95812579CED30007AE1E /* SelectionToolbar.swift in Sources */, D8C4CC4D292101680038B06E /* StackNavigationBar.swift in Sources */, diff --git a/macos/Fileaway/Views/Settings/FileTypePickerView.swift b/macos/Fileaway/Views/Settings/FileTypesView.swift similarity index 76% rename from macos/Fileaway/Views/Settings/FileTypePickerView.swift rename to macos/Fileaway/Views/Settings/FileTypesView.swift index 4349b357..33b5b10b 100644 --- a/macos/Fileaway/Views/Settings/FileTypePickerView.swift +++ b/macos/Fileaway/Views/Settings/FileTypesView.swift @@ -26,19 +26,19 @@ import Interact import FileawayCore -struct FileTypePickerView: View { +struct FileTypesView: View { - @StateObject var fileTypePickerModel: FileTypePickerModel + @StateObject var model: FileTypesViewModel init(settings: FileawayCore.Settings) { - _fileTypePickerModel = StateObject(wrappedValue: FileTypePickerModel(settings: settings)) + _model = StateObject(wrappedValue: FileTypesViewModel(settings: settings)) } var body: some View { GroupBox("File Types") { VStack { - List(selection: $fileTypePickerModel.selection) { - ForEach(fileTypePickerModel.types) { type in + List(selection: $model.selection) { + ForEach(model.fileTypes) { type in HStack { Text(type.localizedDisplayName) Spacer() @@ -55,25 +55,25 @@ struct FileTypePickerView: View { } .contextMenu(forSelectionType: UTType.ID.self) { selection in Button("Remove") { - fileTypePickerModel.remove(selection) + model.remove(selection) } } .onDeleteCommand { - fileTypePickerModel.remove(fileTypePickerModel.selection) + model.remove(model.selection) } HStack { - TextField("File Extension", text: $fileTypePickerModel.input) + TextField("File Extension", text: $model.newFilenameExtension) .onSubmit { - fileTypePickerModel.submit() + model.submit() } Button("Add") { - fileTypePickerModel.submit() + model.submit() } - .disabled(!fileTypePickerModel.canSubmit) + .disabled(!model.canSubmit) } } } - .runs(fileTypePickerModel) + .runs(model) } } diff --git a/macos/Fileaway/Views/Settings/GeneralSettingsView.swift b/macos/Fileaway/Views/Settings/GeneralSettingsView.swift index 92e73547..b90677fd 100644 --- a/macos/Fileaway/Views/Settings/GeneralSettingsView.swift +++ b/macos/Fileaway/Views/Settings/GeneralSettingsView.swift @@ -27,7 +27,7 @@ struct GeneralSettingsView: View { @Environment(\.applicationModel) var applicationModel: ApplicationModel var body: some View { - FileTypePickerView(settings: applicationModel.settings) + FileTypesView(settings: applicationModel.settings) } } diff --git a/macos/Fileaway/Views/Wizard/RulePicker.swift b/macos/Fileaway/Views/Wizard/RulePicker.swift index 91fab75c..e38f9d48 100644 --- a/macos/Fileaway/Views/Wizard/RulePicker.swift +++ b/macos/Fileaway/Views/Wizard/RulePicker.swift @@ -111,11 +111,7 @@ struct RulePicker: View { } } .showsStackNavigationBar("Select Rule") - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - focus = .search - } - } + .defaultFocus($focus, .search) .runs(rulePickerModel) }