如何将 UISearchController 与 SwiftUI 集成

Posted

技术标签:

【中文标题】如何将 UISearchController 与 SwiftUI 集成【英文标题】:How to integrate UISearchController with SwiftUI 【发布时间】:2019-12-09 06:26:25 【问题描述】:

我有一个符合 UIViewControllerRepresentable 的 SearchController,并且我已经实现了所需的协议方法。但是当我在 SwiftUI 结构中创建 SearchController 的实例时,一旦加载,SearchController 就不会出现在屏幕上。有人对我有任何建议吗?谢谢!

下面是符合 UIViewControllerRepresentable 的 SearchController 结构体:

struct SearchController: UIViewControllerRepresentable 

    let placeholder: String
    @Binding var text: String

    func makeCoordinator() -> Coordinator 
        return Coordinator(self)
    

    func makeUIViewController(context: Context) -> UISearchController 
        let controller = UISearchController(searchResultsController: nil)
        controller.searchResultsUpdater = context.coordinator
        controller.obscuresBackgroundDuringPresentation = false
        controller.hidesNavigationBarDuringPresentation = false
        controller.searchBar.delegate = context.coordinator
        controller.searchBar.placeholder = placeholder

        return controller
    

    func updateUIViewController(_ uiViewController: UISearchController, context: Context) 
        uiViewController.searchBar.text = text
    

    class Coordinator: NSObject, UISearchResultsUpdating, UISearchBarDelegate 

        var controller: SearchController

        init(_ controller: SearchController) 
            self.controller = controller
        

        func updateSearchResults(for searchController: UISearchController) 
            let searchBar = searchController.searchBar
            controller.text = searchBar.text!
        
    

这是我的 SwiftUIView 的代码,它应该显示 SearchController:

struct SearchCarsView: View 

    let cars = cardsData

    // MARK: @State Properties

    @State private var searchCarsText: String = ""

    // MARK: Views

    var body: some View 
        SearchController(placeholder: "Name, Model, Year", text: $searchCarsText)
           .background(Color.blue)
    

这是 SearchController 没有出现在屏幕上的图像:

【问题讨论】:

【参考方案1】:

(编辑)ios 15:

iOS 15 添加了新属性.searchable()。您可能应该改用它。

原文:

如果有人还在寻找,我发了a package 来处理这个问题。

我还在这里为那些不喜欢链接或只想复制/粘贴的人提供完整的相关源代码。

扩展名:

// 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
        
    

用法:

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)
    
  

【讨论】:

感谢您制作这个,这正是我想要的 :) 这太棒了!我只是有一个小改动来修复“在视图更新期间修改状态”警告你会得到:只需将函数 updateSearchResults 中的行“self.text = text”包装在这样的异步调用中: DispatchQueue.main.async self .text = 文本 @G.Marc 以及其他一些改进已在 github 版本 (github.com/thislooksfun/SwiftlySearch) 中实现。【参考方案2】:

您可以在解析对实际 SwiftUI View 的底层 UIKit.UINavigationItem 的引用后直接使用 UISearchController。我在SwiftUI Search Bar in the Navigation Bar 用示例项目写了一个关于整个过程的文章,但让我在下面给你一个快速的概述。

您实际上可以利用 SwiftUI 保留视图控制器的层次结构这一事实来解析任何 SwiftUI View 的底层 UIViewController(通过 childrenparent 引用)。拥有UIViewController 后,您可以将UISearchController 设置为navigationItem.searchController。如果您将搜索控制器实例提取到不同的 ObservableObject,您可以将 updateSearchResults(for:) 委托方法连接到 @Published String 属性,因此您可以使用 @ObservedObject 在 SwiftUI 端进行绑定。

【讨论】:

感谢您提供的非常好的示例!我很好奇:你能指出一个地方,它记录了 SwiftUI 中的父级始终是 UIViewController 吗? 对于单个视图,总是有一个UIHostingController,它继承自UIViewController 但是,对于 NavigationView 层次结构中的子视图,我认为没有这样的文档。但是,如果您嵌入了UIViewControllerRepresentable,那么您肯定会拥有一个视图控制器。仅当该控制器将移动到父视图控制器时,此代码才会运行(请参阅 ViewControllerResolver.swift 中的 didMove(toParent:))。所以如果没有父级,那么这段代码就不会运行。 如果您设置UIKit 导航层次结构,并在UIHostingController 实例中推送/弹出SwiftUI 视图,您可以选择(流行的)混合方法。然后,您将直接引用视图控制器(文章中也提到)。 感谢您的澄清!老实说,当我从 GitHub 来到这里时,我没有阅读完整的文章,当时我搜索了您的实现细节(并看到这是您的解决方案)。是的,对于生产性用途,目前最好选择一种混合方法。现在很难依赖实现细节,因为 Apple 可能会远离底层的 UIKit 架构,因此最好通过这种方式确保可以访问 NavigationController。无论如何,对于其他灵活的项目,这个解析器很棒!【参考方案3】:

本例中的实际视觉元素是UISearchBar,所以最简单的起点应该如下

import SwiftUI
import UIKit

struct SearchView: UIViewRepresentable 
    let controller = UISearchController()
    func makeUIView(context: UIViewRepresentableContext<SearchView>) -> UISearchBar 
        self.controller.searchBar
    

    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchView>) 

    

    typealias UIViewType = UISearchBar




struct TestSearchController: View 
    var body: some View 
        SearchView()
    


struct TestSearchController_Previews: PreviewProvider 
    static var previews: some View 
        TestSearchController()
    

接下来的一切都可以在 init 中进行配置,并且像往常一样将 coordinator 设置为委托。

【讨论】:

【参考方案4】:

我尝试让 UISearchController 与 NavigationView 和 UIViewRepresentable 一起工作以注入搜索控制器,但结果确实有问题。最好的方法可能是使用常规的 UINavigationController 而不是 NavigationView,然后还使用容器 UIViewController 将 navigationItem.searchController 设置为 UISearchController。

【讨论】:

【参考方案5】:

UISearchController 在附加到 List 时有效。当您使用 ScrollViewVStack 之类的其他东西时,它会变得有问题。我已经在 Apple 的反馈应用程序中报告了这一点,我希望其他人也这样做。

如果您想在您的应用中包含UISearchController,请不要少。我在 Github 上创建了一个名为 NavigationSearchBar 的 Swift 包。

代码

// Copyright © 2020 Mark van Wijnen
// 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

public extension View 
    func navigationSearchBar(text: Binding<String>, scopeSelection: Binding<Int> = Binding.constant(0), options: [NavigationSearchBarOptionKey : Any] = [NavigationSearchBarOptionKey : Any](), actions: [NavigationSearchBarActionKey : NavigationSearchBarActionTask] = [NavigationSearchBarActionKey : NavigationSearchBarActionTask]()) -> some View 
        overlay(NavigationSearchBar<AnyView>(text: text, scopeSelection: scopeSelection, options: options, actions: actions).frame(width: 0, height: 0))
    

    func navigationSearchBar<SearchResultsContent>(text: Binding<String>, scopeSelection: Binding<Int> = Binding.constant(0), options: [NavigationSearchBarOptionKey : Any] = [NavigationSearchBarOptionKey : Any](), actions: [NavigationSearchBarActionKey : NavigationSearchBarActionTask] = [NavigationSearchBarActionKey : NavigationSearchBarActionTask](), @ViewBuilder searchResultsContent: @escaping () -> SearchResultsContent) -> some View where SearchResultsContent : View 
        overlay(NavigationSearchBar<SearchResultsContent>(text: text, scopeSelection: scopeSelection, options: options, actions: actions, searchResultsContent: searchResultsContent).frame(width: 0, height: 0))
    


public struct NavigationSearchBarOptionKey: Hashable, Equatable, RawRepresentable 
    public static let automaticallyShowsSearchBar = NavigationSearchBarOptionKey("automaticallyShowsSearchBar")
    public static let obscuresBackgroundDuringPresentation = NavigationSearchBarOptionKey("obscuresBackgroundDuringPresentation")
    public static let hidesNavigationBarDuringPresentation = NavigationSearchBarOptionKey("hidesNavigationBarDuringPresentation")
    public static let hidesSearchBarWhenScrolling = NavigationSearchBarOptionKey("hidesSearchBarWhenScrolling")
    public static let placeholder = NavigationSearchBarOptionKey("Placeholder")
    public static let showsBookmarkButton = NavigationSearchBarOptionKey("showsBookmarkButton")
    public static let scopeButtonTitles = NavigationSearchBarOptionKey("scopeButtonTitles")
    
    public static func == (lhs: NavigationSearchBarOptionKey, rhs: NavigationSearchBarOptionKey) -> Bool 
        return lhs.rawValue == rhs.rawValue
    
    
    public let rawValue: String
    
    public init(rawValue: String) 
        self.rawValue = rawValue
    
    
    public init(_ rawValue: String) 
        self.init(rawValue: rawValue)
    


public struct NavigationSearchBarActionKey: Hashable, Equatable, RawRepresentable 
    public static let onCancelButtonClicked = NavigationSearchBarActionKey("onCancelButtonClicked")
    public static let onSearchButtonClicked = NavigationSearchBarActionKey("onSearchButtonClicked")
    public static let onBookmarkButtonClicked = NavigationSearchBarActionKey("onBookmarkButtonClicked")

    public static func == (lhs: NavigationSearchBarActionKey, rhs: NavigationSearchBarActionKey) -> Bool 
        return lhs.rawValue == rhs.rawValue
    
    
    public let rawValue: String
    
    public init(rawValue: String) 
        self.rawValue = rawValue
    
    
    public init(_ rawValue: String) 
        self.init(rawValue: rawValue)
    


public typealias NavigationSearchBarActionTask = () -> Void

fileprivate struct NavigationSearchBar<SearchResultsContent>: UIViewControllerRepresentable where SearchResultsContent : View 
    typealias UIViewControllerType = Wrapper
    typealias OptionKey = NavigationSearchBarOptionKey
    typealias ActionKey = NavigationSearchBarActionKey
    typealias ActionTask = NavigationSearchBarActionTask

    @Binding var text: String
    @Binding var scopeSelection: Int
    
    let options: [OptionKey : Any]
    let actions: [ActionKey : ActionTask]
    let searchResultsContent: () -> SearchResultsContent?
    
    init(text: Binding<String>, scopeSelection: Binding<Int> = Binding.constant(0), options: [OptionKey : Any] = [OptionKey : Any](), actions: [ActionKey : ActionTask] = [ActionKey : ActionTask](), @ViewBuilder searchResultsContent: @escaping () -> SearchResultsContent? =  nil ) 
        self._text = text
        self._scopeSelection = scopeSelection
        self.options = options
        self.actions = actions
        self.searchResultsContent = searchResultsContent
    
    
    func makeCoordinator() -> Coordinator 
        Coordinator(representable: self)
    
    
    func makeUIViewController(context: Context) -> Wrapper 
        Wrapper()
    
    
    func updateUIViewController(_ wrapper: Wrapper, context: Context) 
        if wrapper.searchController != context.coordinator.searchController 
            wrapper.searchController = context.coordinator.searchController
        
        
        if let hidesSearchBarWhenScrolling = options[.hidesSearchBarWhenScrolling] as? Bool 
            wrapper.hidesSearchBarWhenScrolling = hidesSearchBarWhenScrolling
        
        
        if options[.automaticallyShowsSearchBar] as? Bool == nil || options[.automaticallyShowsSearchBar] as! Bool  
            wrapper.navigationBarSizeToFit()
        

        if let searchController = wrapper.searchController 
            searchController.automaticallyShowsScopeBar = true
            
            if let obscuresBackgroundDuringPresentation = options[.obscuresBackgroundDuringPresentation] as? Bool 
                searchController.obscuresBackgroundDuringPresentation = obscuresBackgroundDuringPresentation
             else 
                searchController.obscuresBackgroundDuringPresentation = false
            
            
            if let hidesNavigationBarDuringPresentation = options[.hidesNavigationBarDuringPresentation] as? Bool 
                searchController.hidesNavigationBarDuringPresentation = hidesNavigationBarDuringPresentation
            

            if let searchResultsContent = searchResultsContent() 
                (searchController.searchResultsController as? UIHostingController<SearchResultsContent>)?.rootView = searchResultsContent
            
        
        
        if let searchBar = wrapper.searchController?.searchBar 
            searchBar.text = text
            
            if let placeholder = options[.placeholder] as? String 
                searchBar.placeholder = placeholder
            
            
            if let showsBookmarkButton = options[.showsBookmarkButton] as? Bool 
                searchBar.showsBookmarkButton = showsBookmarkButton
            
            
            if let scopeButtonTitles = options[.scopeButtonTitles] as? [String] 
                searchBar.scopeButtonTitles = scopeButtonTitles
            
            
            searchBar.selectedScopeButtonIndex = scopeSelection
        
    
    
    class Coordinator: NSObject, UISearchControllerDelegate, UISearchResultsUpdating, UISearchBarDelegate 
        let representable: NavigationSearchBar
        
        let searchController: UISearchController
        
        init(representable: NavigationSearchBar) 
            self.representable = representable
            
            var searchResultsController: UIViewController? = nil
            if let searchResultsContent = representable.searchResultsContent() 
                searchResultsController = UIHostingController<SearchResultsContent>(rootView: searchResultsContent)
            
            
            self.searchController = UISearchController(searchResultsController: searchResultsController)
            
            super.init()
            
            self.searchController.searchResultsUpdater = self
            self.searchController.searchBar.delegate = self
        
        
        // MARK: - UISearchResultsUpdating
        func updateSearchResults(for searchController: UISearchController) 
            guard let text = searchController.searchBar.text else  return 
            DispatchQueue.main.async  [weak self] in self?.representable.text = text 
        
        
        // MARK: - UISearchBarDelegate
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) 
            guard let action = self.representable.actions[.onCancelButtonClicked] else  return 
            DispatchQueue.main.async  action() 
        
        
        func searchBarSearchButtonClicked(_ searchBar: UISearchBar) 
            guard let action = self.representable.actions[.onSearchButtonClicked] else  return 
            DispatchQueue.main.async  action() 
        
        
        func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) 
            guard let action = self.representable.actions[.onBookmarkButtonClicked] else  return 
            DispatchQueue.main.async  action() 
        
        
        func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) 
            DispatchQueue.main.async  [weak self] in self?.representable.scopeSelection = selectedScope 
        
    
    
    class Wrapper: UIViewController 
        var searchController: UISearchController? 
            get 
                self.parent?.navigationItem.searchController
            
            set 
                self.parent?.navigationItem.searchController = newValue
            
        
        
        var hidesSearchBarWhenScrolling: Bool 
            get 
                self.parent?.navigationItem.hidesSearchBarWhenScrolling ?? true
            
            set 
                self.parent?.navigationItem.hidesSearchBarWhenScrolling = newValue
            
        
        
        func navigationBarSizeToFit() 
            self.parent?.navigationController?.navigationBar.sizeToFit()
        
    

用法

import SwiftUI
import NavigationSearchBar

struct ContentView: View 
    @State var text: String = ""
    @State var scopeSelection: Int = 0
    
    var body: some View 
        NavigationView 
            List 
                ForEach(1..<5)  index in
                    Text("Sample Text")
                
            
            .navigationTitle("Navigation")
            .navigationSearchBar(text: $text,
                                 scopeSelection: $scopeSelection,
                                 options: [
                                    .automaticallyShowsSearchBar: true,
                                    .obscuresBackgroundDuringPresentation: true,
                                    .hidesNavigationBarDuringPresentation: true,
                                    .hidesSearchBarWhenScrolling: false,
                                    .placeholder: "Search",
                                    .showsBookmarkButton: true,
                                    .scopeButtonTitles: ["All", "Missed", "Other"]
                                 ],
                                 actions: [
                                    .onCancelButtonClicked: 
                                        print("Cancel")
                                    ,
                                    .onSearchButtonClicked: 
                                        print("Search")
                                    ,
                                    .onBookmarkButtonClicked: 
                                        print("Present Bookmarks")
                                    
                                 ], searchResultsContent: 
                                    Text("Search Results for \(text) in \(String(scopeSelection))")
                                 )
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

【讨论】:

以上是关于如何将 UISearchController 与 SwiftUI 集成的主要内容,如果未能解决你的问题,请参考以下文章

如何下移 UISearchController?

将 UISearchController 与 UINavigationController 一起使用

如何让 UISearchController 与 UISearchBar Interface Builder 控件一起使用?

将 unwind segue 与 UISearchController 一起使用时出错

如何将 segmentedControl 和 UISearchController 作为 UITableViewController 标头? (以编程方式)

UISearchController 不能与非半透明 UINavigationBar 一起正常工作