diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 694903e..d51e8b3 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -43,7 +43,6 @@ 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; }; 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; }; 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; }; - 0C45E6A529D4B2FE00F047D2 /* SearchListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */; }; 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; }; 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; }; 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; }; @@ -135,13 +134,13 @@ 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; + 0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2CA7329E24F63000A8585 /* ExpandedSearchable.swift */; }; 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */; }; 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; }; 0CD4030A29DA01B6008D9F03 /* PluginInfoMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */; }; 0CD4030C29DA0222008D9F03 /* PluginInfoAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */; }; 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; }; 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; }; - 0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */; }; 0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */; }; 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; }; 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; @@ -189,7 +188,6 @@ 0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = ""; }; 0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = ""; }; - 0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListener.swift; sourceTree = ""; }; 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = ""; }; 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = ""; }; @@ -276,6 +274,7 @@ 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; + 0CC2CA7329E24F63000A8585 /* ExpandedSearchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedSearchable.swift; sourceTree = ""; }; 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = ""; }; 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = ""; }; 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -283,7 +282,6 @@ 0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoAboutView.swift; sourceTree = ""; }; 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = ""; }; 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = ""; }; - 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScopeBar.swift; sourceTree = ""; }; 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginPickerView.swift; sourceTree = ""; }; 0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; @@ -452,8 +450,6 @@ 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, - 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */, - 0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */, ); path = Modifiers; sourceTree = ""; @@ -614,6 +610,7 @@ children = ( 0CA148CE288903F000DE2211 /* WebView.swift */, 0C7075E529D3845D0093DB2D /* ShareSheet.swift */, + 0CC2CA7329E24F63000A8585 /* ExpandedSearchable.swift */, ); path = RepresentableViews; sourceTree = ""; @@ -838,7 +835,6 @@ 0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */, 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */, - 0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */, 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, @@ -891,11 +887,11 @@ 0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */, 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */, 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */, - 0C45E6A529D4B2FE00F047D2 /* SearchListener.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */, 0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */, 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */, + 0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */, 0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */, 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 181536a..d5f4880 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -32,12 +32,4 @@ extension View { func inlinedList(inset: CGFloat) -> some View { modifier(InlinedListModifier(inset: inset)) } - - func customScopeBar(_ content: @escaping () -> some View) -> some View { - modifier(CustomScopeBarModifier(scopeBarContent: content())) - } - - func searchListener(isSearching: Binding) -> some View { - modifier(SearchListenerModifier(isSearching: isSearching)) - } } diff --git a/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift deleted file mode 100644 index 12569bf..0000000 --- a/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// SearchAppearance.swift -// Ferrite -// -// Created by Brian Dashore on 2/14/23. -// - -import Introspect -import SwiftUI - -struct CustomScopeBarModifier: ViewModifier { - let scopeBarContent: V - @State private var hostingController: UIHostingController? - - func body(content: Content) -> some View { - content - .introspectSearchController { searchController in - - // MARK: One-time setup - - guard hostingController == nil else { return } - - searchController.hidesNavigationBarDuringPresentation = true - searchController.searchBar.showsScopeBar = true - searchController.searchBar.scopeButtonTitles = [""] - (searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true - - let hostingController = UIHostingController(rootView: scopeBarContent) - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - hostingController.view.backgroundColor = .clear - - guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else { - return - } - containerView.addSubview(hostingController.view) - - NSLayoutConstraint.activate([ - hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor), - hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor), - hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor) - ]) - - self.hostingController = hostingController - } - .introspectNavigationController { navigationController in - if #available(iOS 16, *) { - navigationController.viewControllers.first?.navigationItem.preferredSearchBarPlacement = .stacked - } - - navigationController.navigationBar.prefersLargeTitles = true - navigationController.navigationBar.sizeToFit() - } - } -} diff --git a/Ferrite/Views/CommonViews/Modifiers/SearchListener.swift b/Ferrite/Views/CommonViews/Modifiers/SearchListener.swift deleted file mode 100644 index ab3374d..0000000 --- a/Ferrite/Views/CommonViews/Modifiers/SearchListener.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// SearchListener.swift -// Ferrite -// -// Created by Brian Dashore on 3/29/23. -// -// Communicate isSearching back to the parent view -// - -import SwiftUI - -struct SearchListenerModifier: ViewModifier { - @Environment(\.isSearching) var isSearchingEnvironment - - @Binding var isSearching: Bool - - func body(content: Content) -> some View { - content - .background { - EmptyView() - .onChange(of: isSearchingEnvironment) { newValue in - isSearching = newValue - } - } - } -} diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift index 637e023..45ba1d9 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift @@ -8,8 +8,8 @@ import SwiftUI struct SearchResultsView: View { - @Environment(\.isSearching) var isSearching - @Environment(\.dismissSearch) var dismissSearch + @Environment(\.esIsSearching) var isSearching + @Environment(\.esDismissSearch) var dismissSearch @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 14729c4..20d5e67 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -16,6 +16,9 @@ struct ContentView: View { @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false + @State private var isSearching = false + @State private var dismissAction: () -> () = {} + var body: some View { NavView { List { @@ -24,33 +27,34 @@ struct ContentView: View { .listStyle(.insetGrouped) .inlinedList(inset: 20) .navigationTitle("Search") - .searchable( + .expandedSearchable( text: $scrapingModel.searchText, - placement: .navigationBarDrawer(displayMode: .always), - prompt: Text(navModel.searchPrompt) + isSearching: $isSearching, + prompt: navModel.searchPrompt, + dismiss: $dismissAction, + scopeBarContent: { + SearchFilterHeaderView() + }, + onSubmit: { + if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled { + scrapingModel.runningSearchTask = nil + return + } + + scrapingModel.runningSearchTask = Task { + let sources = pluginManager.fetchInstalledSources() + await scrapingModel.scanSources( + sources: sources, + debridManager: debridManager + ) + + logManager.hideIndeterminateToast() + scrapingModel.runningSearchTask = nil + } + } ) .autocorrectionDisabled(!autocorrectSearch) - .textInputAutocapitalization(autocorrectSearch ? .sentences : .never) - .onSubmit(of: .search) { - if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled { - scrapingModel.runningSearchTask = nil - return - } - - scrapingModel.runningSearchTask = Task { - let sources = pluginManager.fetchInstalledSources() - await scrapingModel.scanSources( - sources: sources, - debridManager: debridManager - ) - - logManager.hideIndeterminateToast() - scrapingModel.runningSearchTask = nil - } - } - .customScopeBar { - SearchFilterHeaderView() - } + .esAutocapitalization(autocorrectSearch ? .sentences : .none) .onAppear { navModel.getSearchPrompt() } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index b52700c..f2b2644 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -41,7 +41,6 @@ struct LibraryView: View { DebridCloudView(searchText: $searchText) } } - .searchListener(isSearching: $isSearching) .overlay { if !isSearching { switch navModel.libraryPickerSelection { @@ -81,12 +80,14 @@ struct LibraryView: View { } } } - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .expandedSearchable( + text: $searchText, + scopeBarContent: { + LibraryPickerView() + } + ) .autocorrectionDisabled(!autocorrectSearch) - .textInputAutocapitalization(autocorrectSearch ? .sentences : .never) - .customScopeBar { - LibraryPickerView() - } + .esAutocapitalization(autocorrectSearch ? .sentences : .none) .environment(\.editMode, $editMode) } .onChange(of: navModel.libraryPickerSelection) { _ in diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift index abadd08..dcd87b3 100644 --- a/Ferrite/Views/PluginsView.swift +++ b/Ferrite/Views/PluginsView.swift @@ -47,7 +47,6 @@ struct PluginsView: View { } } } - .searchListener(isSearching: $isSearching) .overlay { if !isSearching { if checkedForPlugins { @@ -74,12 +73,14 @@ struct PluginsView: View { checkedForPlugins = false } .navigationTitle("Plugins") - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .expandedSearchable( + text: $searchText, + scopeBarContent: { + PluginPickerView() + } + ) .autocorrectionDisabled(!autocorrectSearch) - .textInputAutocapitalization(autocorrectSearch ? .sentences : .never) - .customScopeBar { - PluginPickerView() - } + .esAutocapitalization(autocorrectSearch ? .sentences : .none) } } } diff --git a/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift b/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift new file mode 100644 index 0000000..95f53c7 --- /dev/null +++ b/Ferrite/Views/RepresentableViews/ExpandedSearchable.swift @@ -0,0 +1,211 @@ +// +// ExpandedSearchable.swift +// Ferrite +// +// Created by Brian Dashore on 4/8/23. +// + +import SwiftUI + +public extension View { + // A dismissAction must be added in the parent view struct due to lifecycle issues + func expandedSearchable( + text: Binding, + isSearching: Binding? = nil, + prompt: String? = nil, + dismiss: Binding<(() -> ())>? = nil, + scopeBarContent: @escaping () -> Content = { + EmptyView() + }, + onSubmit: (() -> ())? = nil, + onCancel: (() -> ())? = nil + ) -> some View { + self + .overlay( + SearchBar( + searchText: text, + isSearching: isSearching ?? Binding(get: { true }, set: { _, _ in }), + prompt: prompt ?? "Search", + dismiss: dismiss ?? Binding(get: { {} }, set: { _, _ in }), + scopeBarContent: scopeBarContent, + onSubmit: onSubmit, + onCancel: onCancel + ) + .frame(width: 0, height: 0) + ) + .environment(\.esIsSearching, isSearching?.wrappedValue ?? false) + .environment(\.esDismissSearch, ESDismissSearchAction(action: dismiss?.wrappedValue ?? { })) + } + + func esAutocapitalization(_ autocapitalizationType: UITextAutocapitalizationType) -> some View { + environment(\.esAutocapitalizationType, autocapitalizationType) + } +} + +struct ESIsSearching: EnvironmentKey { + static var defaultValue: Bool = false +} + +struct ESDismissSearchAction: EnvironmentKey { + static var defaultValue: ESDismissSearchAction = ESDismissSearchAction(action: {}) + + let action: () -> () + + func callAsFunction() { + action() + } +} + +struct ESAutocapitalization: EnvironmentKey { + static var defaultValue: UITextAutocapitalizationType = .none +} + +extension EnvironmentValues { + var esIsSearching: Bool { + get { self[ESIsSearching.self] } + set { self[ESIsSearching.self] = newValue } + } + + var esDismissSearch: ESDismissSearchAction { + get { self[ESDismissSearchAction.self] } + set { self[ESDismissSearchAction.self] = newValue } + } + + var esAutocapitalizationType: UITextAutocapitalizationType { + get { self[ESAutocapitalization.self] } + set { self[ESAutocapitalization.self] = newValue } + } +} + +struct SearchBar: UIViewControllerRepresentable { + var searchController: UISearchController = UISearchController(searchResultsController: nil) + + @Environment(\.autocorrectionDisabled) var autocorrectionDisabled + @Environment(\.esAutocapitalizationType) var autocapitalization + + // Passed in vars + @Binding var searchText: String + @Binding var isSearching: Bool + var prompt: String + @Binding var dismiss: (() -> ()) + let scopeBarContent: () -> ScopeContent + let onSubmit: (() -> ())? + let onCancel: (() -> ())? + + class Coordinator: NSObject, UISearchBarDelegate, UISearchResultsUpdating { + let parent: SearchBar + + init(_ parent: SearchBar) { + self.parent = parent + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + parent.searchText = searchText + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + if let onSubmit = parent.onSubmit { + onSubmit() + } + } + + // Not necessary since you can listen to isSearching + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + parent.searchText = "" + if let onCancel = parent.onCancel { + onCancel() + } + } + + func updateSearchResults(for searchController: UISearchController) { + parent.isSearching = searchController.isActive + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + func makeUIViewController(context: Context) -> NavSearchBarWrapper { + searchController.hidesNavigationBarDuringPresentation = true + searchController.searchBar.delegate = context.coordinator + searchController.searchResultsUpdater = context.coordinator + searchController.searchBar.autocorrectionType = autocorrectionDisabled ? .no : .yes + searchController.searchBar.autocapitalizationType = autocapitalization + + dismiss = { + searchText = "" + searchController.isActive = false + } + + if ScopeContent.self != EmptyView.self { + setupScopeBar(scopeBarContent()) + } + + return NavSearchBarWrapper(searchController: searchController) + } + + // TODO: Split into a separate ViewController class for root search controller modification + // Or put this in the coordinator + func updateUIViewController(_ controller: NavSearchBarWrapper, context: Context) { + controller.searchController.searchBar.placeholder = prompt + controller.searchController.searchBar.autocorrectionType = autocorrectionDisabled ? .no : .yes + controller.searchController.searchBar.autocapitalizationType = autocapitalization + } + + func setupScopeBar(_ content: ScopeContent) { + searchController.searchBar.showsScopeBar = true + searchController.searchBar.scopeButtonTitles = [""] + (searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true + + let hostingController = UIHostingController(rootView: content) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = .clear + + guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else { + return + } + containerView.addSubview(hostingController.view) + + NSLayoutConstraint.activate([ + hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor), + hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor), + hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor) + ]) + } + + // Appends search controller to the nearest NavigationView + class NavSearchBarWrapper: UIViewController { + var searchController: UISearchController + init(searchController: UISearchController) { + self.searchController = searchController + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + setup() + } + + override func viewDidAppear(_ animated: Bool) { + setup() + } + + // Acts on the parent of this VC which is the representable view + private func setup() { + parent?.navigationItem.searchController = searchController + parent?.navigationItem.hidesSearchBarWhenScrolling = false + + if #available(iOS 16, *) { + parent?.navigationItem.preferredSearchBarPlacement = .stacked + } + + // Makes search bar appear when application starts + parent?.navigationController?.navigationBar.sizeToFit() + } + } +}