更多内容,欢迎关注公众号 「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 库的工作方式是:通过添加一个自定义的 overlay 到视图层级里,以检视 UIKit 层级,找到相关的视图。
如果我的描述对你来说不是很好理解,让我们借助下面的步骤来进一步说明 introspec 库背后的原理。
基本上,我们是叠加了一个不可见的UIViewRepresentable
到 SwiftUI 视图的上层,然后借助这个视图向内挖掘视图链,最后找到托管 SwiftUI 视图的UIHostingView
。一旦我们拿到这个视图,就可以从中访问 UIKit 视图了。
不过,并非所有的 SwiftUI 视图都可以被检视。例如,SwiftUI 的Text
就不是基于UILabel
构建的。相似地,Image
和Button
也不是基于UIImageView
和UIButton
构建的。因此,我们无法访问它们底层的UIKit 视图 —— 因此它们根本就不存在。下面这个表格显示了可以被检视的 SwiftUI 视图。
接下来,让我们来看一些可以借助检视底层 UIKit 视图来构建 SwiftUI 里缺失的特性。
SwiftUI 的 List 当前并没有一个isScrollEnabled
属性可以让我们定制滚动行为,UITableView
是有的。借助VStack + ForEach
,我们也能实现无滚动的特性,但这样做有一个缺点: SwiftUI 列表或者UITableView
的行可点击的效果缺失了。
相反,借助introspectTableView
视图 modifier,我们在保留原生列表特性的同时,轻松禁用滚动,就像下面这样:
同样地,如果要隐藏 SwiftUI 列表元素间的分隔线,我们只需要简单地调用tableView.separatorColor = .none
就可以了。
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() } } }复制代码
预览效果如下:
修改 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)!] } } } }复制代码
预览效果如下:
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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~