Search: Add ExpandedSearchable replacement
ExpandedSearchable opens up the capabilities of the SwiftUI searchable modifier and allows for additions of more properties such as custom scope bars. Since this is a reimplementation of UISearchController, changes to SwiftUI should not affect search bars that rely on the scope bar to always be present. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
fbd99752e4
commit
eacccf36ff
9 changed files with 259 additions and 134 deletions
|
|
@ -43,7 +43,6 @@
|
||||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
||||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
||||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.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 */; };
|
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
|
||||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
|
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 */; };
|
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 */; };
|
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.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 */; };
|
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 */; };
|
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; };
|
||||||
0CD4030A29DA01B6008D9F03 /* PluginInfoMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */; };
|
0CD4030A29DA01B6008D9F03 /* PluginInfoMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */; };
|
||||||
0CD4030C29DA0222008D9F03 /* PluginInfoAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */; };
|
0CD4030C29DA0222008D9F03 /* PluginInfoAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */; };
|
||||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.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 */; };
|
0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */; };
|
||||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
|
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
|
||||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.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 = "<group>"; };
|
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
|
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
|
||||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
|
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
|
||||||
0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListener.swift; sourceTree = "<group>"; };
|
|
||||||
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
|
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = "<group>"; };
|
0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -276,6 +274,7 @@
|
||||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
|
0CC2CA7329E24F63000A8585 /* ExpandedSearchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedSearchable.swift; sourceTree = "<group>"; };
|
||||||
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = "<group>"; };
|
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
|
@ -283,7 +282,6 @@
|
||||||
0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoAboutView.swift; sourceTree = "<group>"; };
|
0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoAboutView.swift; sourceTree = "<group>"; };
|
||||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
||||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
|
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
|
||||||
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScopeBar.swift; sourceTree = "<group>"; };
|
|
||||||
0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginPickerView.swift; sourceTree = "<group>"; };
|
0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginPickerView.swift; sourceTree = "<group>"; };
|
||||||
0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
|
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -452,8 +450,6 @@
|
||||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
|
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
|
||||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
||||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
||||||
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */,
|
|
||||||
0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */,
|
|
||||||
);
|
);
|
||||||
path = Modifiers;
|
path = Modifiers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -614,6 +610,7 @@
|
||||||
children = (
|
children = (
|
||||||
0CA148CE288903F000DE2211 /* WebView.swift */,
|
0CA148CE288903F000DE2211 /* WebView.swift */,
|
||||||
0C7075E529D3845D0093DB2D /* ShareSheet.swift */,
|
0C7075E529D3845D0093DB2D /* ShareSheet.swift */,
|
||||||
|
0CC2CA7329E24F63000A8585 /* ExpandedSearchable.swift */,
|
||||||
);
|
);
|
||||||
path = RepresentableViews;
|
path = RepresentableViews;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -838,7 +835,6 @@
|
||||||
0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */,
|
0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */,
|
||||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
||||||
0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */,
|
|
||||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
|
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
|
||||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||||
|
|
@ -891,11 +887,11 @@
|
||||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
|
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
|
||||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
||||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||||
0C45E6A529D4B2FE00F047D2 /* SearchListener.swift in Sources */,
|
|
||||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
||||||
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */,
|
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */,
|
||||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
||||||
|
0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */,
|
||||||
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
||||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,4 @@ extension View {
|
||||||
func inlinedList(inset: CGFloat) -> some View {
|
func inlinedList(inset: CGFloat) -> some View {
|
||||||
modifier(InlinedListModifier(inset: inset))
|
modifier(InlinedListModifier(inset: inset))
|
||||||
}
|
}
|
||||||
|
|
||||||
func customScopeBar(_ content: @escaping () -> some View) -> some View {
|
|
||||||
modifier(CustomScopeBarModifier(scopeBarContent: content()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchListener(isSearching: Binding<Bool>) -> some View {
|
|
||||||
modifier(SearchListenerModifier(isSearching: isSearching))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
//
|
|
||||||
// SearchAppearance.swift
|
|
||||||
// Ferrite
|
|
||||||
//
|
|
||||||
// Created by Brian Dashore on 2/14/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Introspect
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct CustomScopeBarModifier<V: View>: ViewModifier {
|
|
||||||
let scopeBarContent: V
|
|
||||||
@State private var hostingController: UIHostingController<V>?
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SearchResultsView: View {
|
struct SearchResultsView: View {
|
||||||
@Environment(\.isSearching) var isSearching
|
@Environment(\.esIsSearching) var isSearching
|
||||||
@Environment(\.dismissSearch) var dismissSearch
|
@Environment(\.esDismissSearch) var dismissSearch
|
||||||
|
|
||||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ struct ContentView: View {
|
||||||
|
|
||||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false
|
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false
|
||||||
|
|
||||||
|
@State private var isSearching = false
|
||||||
|
@State private var dismissAction: () -> () = {}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
List {
|
List {
|
||||||
|
|
@ -24,33 +27,34 @@ struct ContentView: View {
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.inlinedList(inset: 20)
|
.inlinedList(inset: 20)
|
||||||
.navigationTitle("Search")
|
.navigationTitle("Search")
|
||||||
.searchable(
|
.expandedSearchable(
|
||||||
text: $scrapingModel.searchText,
|
text: $scrapingModel.searchText,
|
||||||
placement: .navigationBarDrawer(displayMode: .always),
|
isSearching: $isSearching,
|
||||||
prompt: Text(navModel.searchPrompt)
|
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)
|
.autocorrectionDisabled(!autocorrectSearch)
|
||||||
.textInputAutocapitalization(autocorrectSearch ? .sentences : .never)
|
.esAutocapitalization(autocorrectSearch ? .sentences : .none)
|
||||||
.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()
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
navModel.getSearchPrompt()
|
navModel.getSearchPrompt()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ struct LibraryView: View {
|
||||||
DebridCloudView(searchText: $searchText)
|
DebridCloudView(searchText: $searchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchListener(isSearching: $isSearching)
|
|
||||||
.overlay {
|
.overlay {
|
||||||
if !isSearching {
|
if !isSearching {
|
||||||
switch navModel.libraryPickerSelection {
|
switch navModel.libraryPickerSelection {
|
||||||
|
|
@ -81,12 +80,14 @@ struct LibraryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
.expandedSearchable(
|
||||||
|
text: $searchText,
|
||||||
|
scopeBarContent: {
|
||||||
|
LibraryPickerView()
|
||||||
|
}
|
||||||
|
)
|
||||||
.autocorrectionDisabled(!autocorrectSearch)
|
.autocorrectionDisabled(!autocorrectSearch)
|
||||||
.textInputAutocapitalization(autocorrectSearch ? .sentences : .never)
|
.esAutocapitalization(autocorrectSearch ? .sentences : .none)
|
||||||
.customScopeBar {
|
|
||||||
LibraryPickerView()
|
|
||||||
}
|
|
||||||
.environment(\.editMode, $editMode)
|
.environment(\.editMode, $editMode)
|
||||||
}
|
}
|
||||||
.onChange(of: navModel.libraryPickerSelection) { _ in
|
.onChange(of: navModel.libraryPickerSelection) { _ in
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ struct PluginsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchListener(isSearching: $isSearching)
|
|
||||||
.overlay {
|
.overlay {
|
||||||
if !isSearching {
|
if !isSearching {
|
||||||
if checkedForPlugins {
|
if checkedForPlugins {
|
||||||
|
|
@ -74,12 +73,14 @@ struct PluginsView: View {
|
||||||
checkedForPlugins = false
|
checkedForPlugins = false
|
||||||
}
|
}
|
||||||
.navigationTitle("Plugins")
|
.navigationTitle("Plugins")
|
||||||
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
.expandedSearchable(
|
||||||
|
text: $searchText,
|
||||||
|
scopeBarContent: {
|
||||||
|
PluginPickerView()
|
||||||
|
}
|
||||||
|
)
|
||||||
.autocorrectionDisabled(!autocorrectSearch)
|
.autocorrectionDisabled(!autocorrectSearch)
|
||||||
.textInputAutocapitalization(autocorrectSearch ? .sentences : .never)
|
.esAutocapitalization(autocorrectSearch ? .sentences : .none)
|
||||||
.customScopeBar {
|
|
||||||
PluginPickerView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
211
Ferrite/Views/RepresentableViews/ExpandedSearchable.swift
Normal file
211
Ferrite/Views/RepresentableViews/ExpandedSearchable.swift
Normal file
|
|
@ -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<Content: View>(
|
||||||
|
text: Binding<String>,
|
||||||
|
isSearching: Binding<Bool>? = 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<ScopeContent: View>: 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue