2

访问 SwiftUI 内部的 UIKit 组件

 2 years ago
source link: https://mp.weixin.qq.com/s/xYKGs3FkrlI_9pq1cdnC5Q
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

访问 SwiftUI 内部的 UIKit 组件

Original 猫克杯 Swift花园 2020-04-19
收录于话题 #深入SwiftUI 17个内容

已经学习并使用过 SwiftUI 一段时间的同学,可能会有这样的需求:想要禁用一个列表的滚动,在 SwiftUI 中要怎么实现?而熟悉 UIKit 的同学都知道,这在 UIScrollView 中是很简单的事情。

抛开 SwiftUI 尚不完备的工具不说,SwiftUI 的确因其构建 UI 的便捷性给开发者带来了兴奋。有一个令人欣慰的事实是,许多 SwiftUI 组件实际上是基于 UIKit 构建的。除此之外,SwiftUI 和 UIKit 的互操作性使得我们可以充分利用 UIViewRepresentable 和 UIViewControllerRepresentable —— 这两者都是为了让你可以将 UIKit 组件移植到 SwiftUI 而存在的。

但这是我们大家已经知道的事情,那这篇文章的目的又是什么?

在接下来的几节,我将带你探索一个令人惊讶的 SwiftUI 库,它叫 Introspect ,来自 https://github.com/siteline/SwiftUI-Introspect 。利用它,我们能够访问 SwiftUI 组件底层的 UIKit 视图。

我们会涉及下列主题:

  • Introspect 库底层是如何工作的?

  • 如何禁用一个 SwiftUI 的列表?

  • 如何自定义 SwiftUI 里的 Segmented 风格的 Picker?

  • 如何修改 NavigationView 和 TabView 的颜色?

  • 如何让 SwiftUI 的 TextField 变成 first responder?

背后的原理

如果我的描述对你来说不是很好理解,让我们借助下面的步骤来进一步说明 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 视图。

SwiftUIUIKitAppKitIntrospectListUITableViewNSTableView.introspectTableView()ScrollViewUIScrollViewNSScrollView.introspectScrollView()NavigationViewUINavigationControllerN/A.introspectNavigationController()任何嵌入的视图UIViewControllerN/A.introspectViewController()TableViewUITabBarControllerN/A.introspectTabBarController()TextFieldUITextFieldNSTextField.introspectTextField()ToggleUISwitchN/A.introspectSwitch()SliderUISliderNSSlider.introspectSlider()StepperUIStepperNSStepper.introspectStepper()DatePickerUIDatePickerNSDatePicker.introspectDatePicker()Picker(SegmentedPickerStyle)UISegmentedControlNSSegmentedControl.introspectSegmentedControl()

接下来,让我们来看一些可以借助检视底层 UIKit 视图来构建 SwiftUI 里缺失的特性。

禁用 SwiftUI 列表滚动

SwiftUI 的 List 当前并没有一个isScrollEnabled属性可以让我们定制滚动行为,UITableView是有的。借助VStack + ForEach,我们也能实现无滚动的特性,但这样做有一个缺点:SwiftUI 列表或者UITableView的行可点击的效果缺失了。

相反,借助introspectTableView视图 modifier,我们在保留原生列表特性的同时,轻松禁用滚动,就像下面这样:

640?wx_fmt=gif

同样地,如果要隐藏 SwiftUI 列表元素间的分隔线,我们只需要简单地调用tableView.separatorColor = .none就可以了。

在 SwiftUI 中自定义Segmented 控件

SwiftUI 允许我们给Picker设置SegmentedPickerStyle,同时也有很多限制:自定义边框,半径,标题和背景都没法做到。

再一次,我们要借助底层的视图,来定制 SwiftUI 中 Segmented 控件的外观。在接下来的例子中,我们会移除 Segmented 控件里的圆角,并且设置一个边框颜色:

import SwiftUIimport Introspectstruct 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()

}
}}

预览效果如下:

640?wx_fmt=jpeg

自定义 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 SwiftUIimport Introspectstruct 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)!]
}
}
}}

预览效果如下:

640?wx_fmt=jpeg

让 TextField 成为 First Responder

SwiftUI 当前没有提供自动弹出键盘的方法。除非我们做点什么,否则用户就得手动获取 TextField 的焦点。同样的,我们通过访问底层的UITextField,调用becomeFirstResponder函数来优化这个体验,像下面这样:

import SwiftUIimport Introspectstruct 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 的定制能力。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK