如何将 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
(通过 children
和 parent
引用)。拥有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
时有效。当您使用 ScrollView
或 VStack
之类的其他东西时,它会变得有问题。我已经在 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 与 UINavigationController 一起使用
如何让 UISearchController 与 UISearchBar Interface Builder 控件一起使用?
将 unwind segue 与 UISearchController 一起使用时出错
如何将 segmentedControl 和 UISearchController 作为 UITableViewController 标头? (以编程方式)