

访问 SwiftUI 内部的 UIKit 组件
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 组件
已经学习并使用过 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,我们在保留原生列表特性的同时,轻松禁用滚动,就像下面这样:
同样地,如果要隐藏 SwiftUI 列表元素间的分隔线,我们只需要简单地调用tableView.separatorColor = .none就可以了。
在 SwiftUI 中自定义Segmented 控件
SwiftUI 允许我们给Picker设置SegmentedPickerStyle,同时也有很多限制:自定义边框,半径,标题和背景都没法做到。
再一次,我们要借助底层的视图,来定制 SwiftUI 中 Segmented 控件的外观。在接下来的例子中,我们会移除 Segmented 控件里的圆角,并且设置一个边框颜色:
@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方法里修改外观 —— 就像下面这样 —— 然后这并不是一个好的解决方案:
UINavigationBar.appearance().titleTextAttributes =
[.foregroundColor:UIColor.red]
UINavigationBar.appearance().backgroundColor = .green
UITabBar.appearance().backgroundColor = UIColor.blue
}
这种实现方案实际上并不是定制了 NavigationView 或者 TabView。相反,它是全局覆盖了它们的外观。
对于这个需求,我们有更好的解决方案。比如,下面的代码片段就以一种更简明的方式修改 NavigationBar 的标题和背景色。
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 视图:
@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函数来优化这个体验,像下面这样:
@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 的定制能力。
Recommend
-
44
At the current state of SwiftUI not all views are implemented yet. UIKit views can be added in the Swift hierarchy. In this tutorial a url will be displayed in a web view.SwiftUI requires Xcode 11 and MacOS Catalina, for...
-
28
A few weeks ago, we talked about building views like PagerView and BottomSheetView from scratch in SwiftUI . SwiftUI is pretty young and misses some components that we expect to hav...
-
20
咱们最有意思的第四篇 SwiftUI 教程来啦!为什么说是“最有意思”的呢?因为按照约定,在这篇文章里我们会一起来看看用 SwiftUI 开发界面的快捷便利体现在什么地方。相信这会让许多苹果开发者们耳目一新。 信了苹果教之后,每次有什么更新,...
-
11
在上一篇文章中,我们了解了 SwiftUI 的 Text 组件,并通过 Stack 系列的组件对内容进行了一些简单的布局。在这篇文章里,我们会认识一个全新的图片组件...
-
9
UIKit or SwiftUI: what to use in production? 2020, Sep 29 Appl...
-
10
Integrating UIKit & SwiftUIGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupShapeGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGroupGrou...
-
16
Introspect for SwiftUI Introspect allows you to get the underlying UIKit or AppKit element of a SwiftUI view. For instance, with Introspect you can access UITableView to modify separator...
-
12
Replicate 12 UIKit's contentMode options in SwiftUI 12 Apr 2021 ⋅ 4 min read ⋅ SwiftUI
-
8
How to use SwiftUI in UIKit Using SwiftUI as UIView and UIViewController Table of ContentsIn the previous post (How to use UIKit in SwiftUI)...
-
10
Send Events from SwiftUI to UIKit and Vice Versa // Written by Jordan Morgan /...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK