访问 SwiftUI 内部的 UIKit 组件
2020/5/9 23:26:37
本文主要是介绍访问 SwiftUI 内部的 UIKit 组件,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
已经学习并使用过 SwiftUI 一段时间的同学,可能会有这样的需求:想要禁用一个列表的滚动,在 SwiftUI 中要怎么实现?而熟悉 UIKit 的同学都知道,这在 UIScrollView 中是很简单的事情。
抛开 SwiftUI 尚不完备的工具不说,SwiftUI 的确因其构建 UI 的便捷性给开发者带来了兴奋。有一个令人欣慰的事实是,许多 SwiftUI 组件实际上是基于 UIKit 构建的。除此之外,SwiftUI 和 UIKit 的互操作性使得我们可以充分利用 UIViewRepresentable 和UIViewControllerRepresentable —— 这两者都是为了让你可以将 UIKit 组件移植到 SwiftUI 而存在的。
但这是我们大家已经知道的事情,那这篇文章的目的又是什么?
在接下来的几节,我将带你探索一个令人惊讶的 SwiftUI 库,它叫 Introspect (github.com/siteline/Sw…)。利用它,我们能够访问 SwiftUI 组件底层的 UIKit 视图。
我们会涉及下列主题:
- Introspect 库底层是如何工作的?
- 如何禁用一个 SwiftUI 的列表?
- 如何自定义 SwiftUI 里的 Segmented 风格的 Picker?
- 如何修改 NavigationView 和 TabView 的颜色?
- 如何让 SwiftUI 的 TextField 变成 first responder?
背后的原理
Introspect 库的工作方式是:通过添加一个自定义的 overlay 到视图层级里,以检视 UIKit 层级,找到相关的视图。
如果我的描述对你来说不是很好理解,让我们借助下面的步骤来进一步说明 introspec 库背后的原理。
- 第 1 步:给一个 SwiftUI List 添加一个 UIViewRepresentable overlay
- 第 2 步:找到它的 ViewHost (SwiftUI 会把每个 UIView 包裹进一个 ViewHost,然后放进一个 HostingView)
- 第 3 步:找到它在视图层级里的兄弟视图(就这样,我们从 SwiftUI.List 里拿到了 UITableView,接下来就可以利用这个 UITableView 的属性来定制 SwiftUI 的 List。)
基本上,我们是叠加了一个不可见的UIViewRepresentable
到 SwiftUI 视图的上层,然后借助这个视图向内挖掘视图链,最后找到托管 SwiftUI 视图的UIHostingView
。一旦我们拿到这个视图,就可以从中访问 UIKit 视图了。
不过,并非所有的 SwiftUI 视图都可以被检视。例如,SwiftUI 的Text
就不是基于UILabel
构建的。相似地,Image
和Button
也不是基于UIImageView
和UIButton
构建的。因此,我们无法访问它们底层的UIKit 视图 —— 因此它们根本就不存在。下面这个表格显示了可以被检视的 SwiftUI 视图。
接下来,让我们来看一些可以借助检视底层 UIKit 视图来构建 SwiftUI 里缺失的特性。
禁用 SwiftUI 列表滚动
SwiftUI 的 List 当前并没有一个isScrollEnabled
属性可以让我们定制滚动行为,UITableView
是有的。借助VStack + ForEach
,我们也能实现无滚动的特性,但这样做有一个缺点: SwiftUI 列表或者UITableView
的行可点击的效果缺失了。
相反,借助introspectTableView
视图 modifier,我们在保留原生列表特性的同时,轻松禁用滚动,就像下面这样:
同样地,如果要隐藏 SwiftUI 列表元素间的分隔线,我们只需要简单地调用tableView.separatorColor = .none
就可以了。
在 SwiftUI 中自定义Segmented 控件
SwiftUI 允许我们给Picker
设置SegmentedPickerStyle
,同时也有很多限制:自定义边框,半径,标题和背景都没法做到。
再一次,我们要借助底层的视图,来定制 SwiftUI 中 Segmented 控件的外观。在接下来的例子中,我们会移除 Segmented 控件里的圆角,并且设置一个边框颜色:
import SwiftUI import Introspect struct ContentView: View { @State private var selectedIndex = 0 @State private var numbers = ["One", "Two", "Three"] var body: some View { VStack { Picker("Numbers", selection: $selectedIndex) { ForEach(0..<numbers.count) { index in Text(self.numbers[index]).tag(index) } } .pickerStyle(SegmentedPickerStyle()) .introspectSegmentedControl { segmentedControl in segmentedControl.layer.cornerRadius = 0 segmentedControl.layer.borderColor = UIColor.label.cgColor segmentedControl.layer.borderWidth = 1.0 segmentedControl.selectedSegmentTintColor = .red segmentedControl.setTitleTextAttributes([.foregroundColor:UIColor.white], for: .selected) segmentedControl.setTitleTextAttributes([.foregroundColor:UIColor.red], for: .normal) } Text("选中的值:\(numbers[selectedIndex])").padding() } } }复制代码
预览效果如下:
自定义 NavigationView 和 TabView 的样式
修改 NavigationBar 中标题文本的颜色不是很直观,对于 TabView 也一样。有人可能建议在init
方法里修改外观 —— 就像下面这样 —— 然后这并不是一个好的解决方案:
init() { UINavigationBar.appearance().titleTextAttributes = [.foregroundColor:UIColor.red] UINavigationBar.appearance().backgroundColor = .green UITabBar.appearance().backgroundColor = UIColor.blue }复制代码
这种实现方案实际上并不是定制了 NavigationView 或者 TabView。相反,它是全局覆盖了它们的外观。
对于这个需求,我们有更好的解决方案。比如,下面的代码片段就以一种更简明的方式修改 NavigationBar 的标题和背景色。
import SwiftUI import Introspect struct ContentView : View { var body: some View { NavigationView { VStack { Text("不使用 .appearance()") } .navigationBarTitle("标题", displayMode: .inline) .introspectNavigationController{ navController in navController.navigationBar.barTintColor = .blue navController.navigationBar.titleTextAttributes = [ .foregroundColor: UIColor.white, .font : UIFont(name:"Helvetica Neue", size: 20)!] } } } }复制代码
通过检视TabView
和NavigationView
,我们能够修改它们对应的 UIKit 视图:
struct ContentView : View { @State private var selection = 1 var body: some View { NavigationView { VStack { Text("不使用 .appearance()") TabView(selection: $selection) { Text("第一屏") .tabItem { Image(systemName: "1.square.fill") Text("第一屏") }.tag(1) Text("第二屏") .tabItem { Image(systemName: "2.square.fill") Text("第二屏") }.tag(2) } .accentColor(.white) .introspectTabBarController { tabController in tabController.tabBar.barTintColor = .blue tabController.tabBar.isTranslucent = false } } .navigationBarTitle("标题", displayMode: .inline) .introspectNavigationController{ navController in navController.navigationBar.barTintColor = .blue navController.navigationBar.titleTextAttributes = [ .foregroundColor: UIColor.white, .font : UIFont(name:"Helvetica Neue", size: 20)!] } } } }复制代码
预览效果如下:
让 TextField 成为 First Responder
SwiftUI 当前没有提供自动弹出键盘的方法。除非我们做点什么,否则用户就得手动获取 TextField 的焦点。同样的,我们通过访问底层的UITextField
,调用becomeFirstResponder
函数来优化这个体验,像下面这样:
import SwiftUI import Introspect struct ContentView : View { @State var text = "" var body: some View { VStack { TextField("Enter some text", text: $text) .textFieldStyle(RoundedBorderTextFieldStyle()) .introspectTextField{ textField in textField.becomeFirstResponder() } } } }复制代码
总结
我们可以看到,检视 SwiftUI 底层的 UIKit 视图可以让我们突破某些 SwiftUI 组件的限制。比如,我们在文章中介绍了列表,segmented 风格的 Picker,还有 NavigationView,TabView 和 TextField。
进一步的,你还可以采用一样的方法定制 Stepper
,Slider
和 DatePicker
。
当然,我相信 Apple 会在未来的版本给 SwiftUI 赋予更强大的功能和更灵活的 API。在这之前,你可以借助这种思路,释放原来的 UIKit API 的定制能力。
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
这篇关于访问 SwiftUI 内部的 UIKit 组件的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值