Public should not be used in an app since it declares public to additional modules. However, an app is one module. Some structs/ classes need to be left public to conform to CoreData's generation. Signed-off-by: kingbri <bdashore3@proton.me>
224 lines
7.8 KiB
Swift
224 lines
7.8 KiB
Swift
//
|
|
// ExpandedSearchable.swift
|
|
// Ferrite
|
|
//
|
|
// Created by Brian Dashore on 4/8/23.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
extension View {
|
|
// A dismissAction must be added in the parent view struct due to lifecycle issues
|
|
func expandedSearchable(text: Binding<String>,
|
|
isSearching: Binding<Bool>? = nil,
|
|
isEditingSearch: Binding<Bool>? = nil,
|
|
prompt: String? = nil,
|
|
dismiss: Binding<() -> Void>? = nil,
|
|
scopeBarContent: @escaping () -> some View = {
|
|
EmptyView()
|
|
},
|
|
onSubmit: (() -> Void)? = nil,
|
|
onCancel: (() -> Void)? = nil) -> some View
|
|
{
|
|
overlay(
|
|
SearchBar(
|
|
searchText: text,
|
|
isSearching: isSearching ?? Binding(get: { true }, set: { _, _ in }),
|
|
isEditingSearch: isEditingSearch ?? 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 = .init(action: {})
|
|
|
|
let action: () -> Void
|
|
|
|
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 = .init(searchResultsController: nil)
|
|
|
|
@Environment(\.autocorrectionDisabled) var autocorrectionDisabled
|
|
@Environment(\.esAutocapitalizationType) var autocapitalization
|
|
|
|
// Passed in vars
|
|
@Binding var searchText: String
|
|
@Binding var isSearching: Bool
|
|
@Binding var isEditingSearch: Bool
|
|
var prompt: String
|
|
@Binding var dismiss: () -> Void
|
|
let scopeBarContent: () -> ScopeContent
|
|
let onSubmit: (() -> Void)?
|
|
let onCancel: (() -> Void)?
|
|
|
|
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 searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
|
parent.isEditingSearch = true
|
|
}
|
|
|
|
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
|
parent.isEditingSearch = false
|
|
}
|
|
|
|
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 {
|
|
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
|
|
|
|
guard
|
|
let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView,
|
|
!containerView.subviews.contains(where: { String(describing: $0.classForCoder).contains("UIHostingView") })
|
|
else {
|
|
return
|
|
}
|
|
|
|
let hostingController = UIHostingController(rootView: content)
|
|
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
hostingController.view.backgroundColor = .clear
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|