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

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

下面是符合 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 没有出现在屏幕上的图像:

【问题讨论】:

    标签: swift xcode swiftui uisearchcontroller


    【解决方案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()
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-07
          • 2020-05-14
          • 2016-01-26
          • 2010-09-23
          相关资源
          最近更新 更多