ryannn
ryannn

Reputation: 1557

SwiftUI UISearchController searchResultsController navigation stack issue

I have UISearchController that's been made UIViewControllerRepresentable for SwiftUI, as follows:

struct SearchViewController<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content
    let searchResultsView = SearchResultsView()

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> UINavigationController {

        let rootViewController = UIHostingController(rootView: content())
        let navigationController = UINavigationController(rootViewController: rootViewController)
        let searchResultsController = UIHostingController(rootView: searchResultsView)

        // Set nav properties
        navigationController.navigationBar.prefersLargeTitles = true
        navigationController.definesPresentationContext = true

        // Create search controller
        let searchController = UISearchController(searchResultsController: searchResultsController)
        searchController.searchBar.autocapitalizationType = .none
        searchController.delegate =  context.coordinator
        searchController.searchBar.delegate = context.coordinator // Monitor when the search button is tapped.

        // Create default view
        rootViewController.navigationItem.searchController = searchController
        rootViewController.title = "Search"

        return navigationController
    }

    func updateUIViewController(_ navigationController: UINavigationController, context: UIViewControllerRepresentableContext<SearchViewController>) {
        //
    }
}

This works, and displays the searchResultsController when the user is searching. However, the searchResultsController doesn't seem to know it's navigational context/stack, so I can't navigate from that list view in the searchResultsController.

Can this be structured to allow navigation from searchResultsController, or is this currently a SwiftUI limitation.

Any advice is much appreciated!

Upvotes: 3

Views: 1716

Answers (2)

thislooksfun
thislooksfun

Reputation: 1089

(EDIT) iOS 15+:

iOS 15 added the new property .searchable() (here is the official guide on how to use it). You should use it instead unless you still need to target iOS 14 or below.

Original:

I just made a package that would likely solve this issue. It's similar to @EthanMengoreo's answer, but has (in my opinion) a more SwiftUI-like syntax.

I'm also including the full relevant source code for v1.0.0 here for those who dislike links or just want to copy/paste (I am not keeping this in sync with any updates to the GitHub repo).

Extension:

// Copyright © 2020 thislooksfun
// 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 SwiftUI
import Combine

public extension View {
    public func navigationBarSearch(_ searchText: Binding<String>) -> some View {
        return overlay(SearchBar(text: searchText).frame(width: 0, height: 0))
    }
}

fileprivate struct SearchBar: UIViewControllerRepresentable {
    @Binding
    var text: String
    
    init(text: Binding<String>) {
        self._text = text
    }
    
    func makeUIViewController(context: Context) -> SearchBarWrapperController {
        return SearchBarWrapperController()
    }
    
    func updateUIViewController(_ controller: SearchBarWrapperController, context: Context) {
        controller.searchController = context.coordinator.searchController
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }
    
    class Coordinator: NSObject, UISearchResultsUpdating {
        @Binding
        var text: String
        let searchController: UISearchController
        
        private var subscription: AnyCancellable?
        
        init(text: Binding<String>) {
            self._text = text
            self.searchController = UISearchController(searchResultsController: nil)
            
            super.init()
            
            searchController.searchResultsUpdater = self
            searchController.hidesNavigationBarDuringPresentation = true
            searchController.obscuresBackgroundDuringPresentation = false
            
            self.searchController.searchBar.text = self.text
            self.subscription = self.text.publisher.sink { _ in
                self.searchController.searchBar.text = self.text
            }
        }
        
        deinit {
            self.subscription?.cancel()
        }
        
        func updateSearchResults(for searchController: UISearchController) {
            guard let text = searchController.searchBar.text else { return }
            self.text = text
        }
    }
    
    class SearchBarWrapperController: UIViewController {
        var searchController: UISearchController? {
            didSet {
                self.parent?.navigationItem.searchController = searchController
            }
        }
        
        override func viewWillAppear(_ animated: Bool) {
            self.parent?.navigationItem.searchController = searchController
        }
        override func viewDidAppear(_ animated: Bool) {
            self.parent?.navigationItem.searchController = searchController
        }
    }
}

Usage:

import SwiftlySearch

struct MRE: View {
  let items: [String]

  @State
  var searchText = ""

  var body: some View {
    NavigationView {
      List(items.filter { $0.localizedStandardContains(searchText) }) { item in
        Text(item)
      }.navigationBarSearch(self.$searchText)
    }
  }
}

Upvotes: 1

Cheungbo Mong
Cheungbo Mong

Reputation: 163

I recently also need to implement this feature(search bar like navigation bar) in my app. Looking you snippets really inspired m2! My Thanks First!

After learning from you, ula1990, Lorenzo Boaro and V8tr, I implemented my own.

I set the searchResultsController to nil (due to untappable navigation link in it, so i set it to nil).

Demo

Code:

struct SearchController<Result: View>: UIViewControllerRepresentable {
    @Binding var searchText: String
    private var content: (_ searchText:String)->Result
    
    private var searchBarPlaceholder: String
    

    init(_ searchBarPlaceholder: String = "", searchedText: Binding<String>,
         resultView: @escaping (_ searchText:String) -> Result) {
        self.content = resultView
        self._searchText = searchedText
        self.searchBarPlaceholder = searchBarPlaceholder
    }
    func makeUIViewController(context: Context) -> UINavigationController {
        let contentViewController = UIHostingController(rootView: SearchResultView(result: $searchText, content: content))
        let navigationController = UINavigationController(rootViewController: contentViewController)
        
        let searchController = UISearchController(searchResultsController: nil)
        searchController.searchResultsUpdater = context.coordinator
        searchController.obscuresBackgroundDuringPresentation = false // for results
        searchController.searchBar.placeholder = searchBarPlaceholder
        
        contentViewController.title = "\\(Title)" // for customization
        contentViewController.navigationItem.searchController = searchController
        contentViewController.navigationItem.hidesSearchBarWhenScrolling = true
        contentViewController.definesPresentationContext = true
        
        searchController.searchBar.delegate = context.coordinator
        
        return navigationController
    }
    
    func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<SearchController>) {
        // 
        
    }
}
extension SearchController {
    func makeCoordinator() -> SearchController<Result>.Coordinator {
        Coordinator(self)
    }
    class Coordinator: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
        var parent: SearchController
        init(_ parent: SearchController){self.parent = parent}
        
        // MARK: - UISearchResultsUpdating
        func updateSearchResults(for searchController: UISearchController) {
            self.parent.searchText = searchController.searchBar.text!
        }
        
        // MARK: - UISearchBarDelegate
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            self.parent.searchText = ""
        }

        func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
            self.parent.searchText = ""
            return true
        }
    }
}

// "nofity" the result content about the searchText
struct SearchResultView<Content: View>: View { 
    @Binding var searchText: String
    private var content: (_ searchText:String)->Content
    init(result searchText: Binding<String>, @ViewBuilder content: @escaping (_ searchText:String) -> Content) {
        self._searchText = searchText
        self.content = content
    }
    var body: some View {
        content(searchText)
    }
}

I don't if this is what you want, but thanks again!.

Upvotes: 2

Related Questions