大师学SwiftUI第16章 - UIKit框架集成
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记
SwiftUI是一套新框架,因此并没有包含我们构建专业应用所需的所有工具。这意味着我们会需要求助于UIKit(移动设备)和AppKit(Mac电脑)等原生框架所提供的工具。
我们已介绍过UIKit。它是一套SwiftUI在后台实现用于创建大部分视图和控件的框架。有些UIKit类用于运行应用(UIApplication
)、加载图片(UIImage
)、管理设备(UIDevice
)及窗口(UIWindow
),而另一些定义用于设置应用和场景(UiApplicationDelegate
和UIWindowSceneDelegate
)的代理。当然,该框架提供了我们创建界面所需的所有工具,包括两个创建和管理视图的基本类,称为UIView
和UIViewController
。
最后面这两个类,UIView
和UIViewController
,是在我们需要对SwiftUI界面添加UIKit特性时需要实现的类。UIView的子类用于在屏幕上显示信息,比如标签和图像,以及创建控件,比如按钮、滑块和开关。而UIViewController
类的子类用于展示视图及包含处理它们的值和与用户交互的必要功能。为了将这些工具集成到SwiftUI界面中,SwiftUI框架定义了两个协议:UIViewRepresentable
和UIViewControllerRepresentable
。
Representable视图
UIViewRepresentable
协议定义了一结构体,作为UIView
类或其子类所创建对象的一层封装(wrapper)。实现这一协议的结构体可表示SwiftUI界面中的一个UIKit视图。创建及管理UIKit视图,结构体必须要实现如下的方法。
-
makeUIView(context: Context)
:该方法创建一个UIKit视图并返回。context
参数是对提供视图状态信息的UIViewRepresentableContext
类型结构体的引用。 -
updateUIView(UIViewType, context: Context)
:该方法通过一个绑定属性使用SwiftUI界面所提供的信息更新UIKit视图。第一个参数是对UIKit视图的引用,context
参数是对提供视图状态信息的UIViewRepresentableContext
类型结构体的引用。 -
dismantleUIView(UIViewType, coordinator: Coordinator)
:该类型方法为释放视图做准备工作。第一个参数是UIKit视图的指针,coordinator
参数是将值发回SwiftUI界面的对象。 -
makeCoordinator()
:该方法创建的对将UIKit视图的信息传回给SwiftUI界面。
要在SwiftUI接口中包含一个UIView
对象(或其子类所创建对象),我们需要定义一个实现UIViewRepresentable
协议的结构体,并实现上述的方法。makeUIView()
和updateUIView()
方法是必选的。在makeUIView()
方法中,我们必须创建并返回一个UIKit视图实例,updateUIView()
法用于使用SwiftUI界面中所获取的值来更新视图。
下例中创建了一个蓝色背景的UIKit视图,并在SwiftUI视图中包含它。只需要用makeUIView()
方法创建UIKit视图,但同时也要实现updateUIView()
方法,因为协议有相应要求。
示例16-1:准备一个供SwiftUI使用的UIKit视图
struct MyCustomView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
let view = UIView()
view.backgroundColor = UIColor(.blue)
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
}
在每次创建新的MyCustomView
结构体实例时调用makeUIView()
方法。在该方法中,我们创建一个UIView
视图,将背景色设置为蓝色并返回。因此,每次我们创建MyCustomView
结构体实例的时候,会创建一个UIView
并添加到SwiftUI实例之中,如下例所示。
示例16-2:在SwiftUI视图中显示UIKit视图
struct ContentView: View {
var body: some View {
VStack {
MyCustomView()
.frame(width: 200, height: 150)
.padding()
Spacer()
}
}
}
representable
视图默认为弹性大小,但可以通过SwiftUI修饰符进行修改。本例中,我们使用frame()修饰符赋了固定的宽和高。
图16-1:SwiftUI界面中的UIKit视图
UIView
对象创建一个空视图,但我们也可以实现接收其它用户输入的视图,如文本框、开关等。我们使用updateUIView()
方法将SwiftUI视图中的值传递给UIKit视图。但如果想将UIKit视图中的值传递给SwiftUI界面,则需要实现makeCoordinator()
方法。在这个方法中,我们必须创建一个coordinator
对象实例并返回。coordinator
是一个可将UIKit视图中的信息回发给Swift界面的对象,通常通过修改绑定属性实现。如何处理这些值取决于我们操作的UIkit视图的类型。例如,UITexView
视图创建一个用户可输入多行文本的输入框,和SwiftUI中的TextEditor
视图一样。这个视图通过调用代理方法上报改变。因此,要获取用户在UITexView
中插入的文本并在SwiftUI中进行处理,我们需要创建一个实现UITextViewDelegate
协议的coordinator
类,并实现其方法。下例演示了如何创建一个UIViewRepresentable
结构体来操作这个类。
示例16-3:对SwiftUI视图发送和接收值
import SwiftUI
struct TextView: UIViewRepresentable {
@Binding var input: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.backgroundColor = UIColor.yellow
view.font = UIFont.systemFont(ofSize: 17)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = input
}
func makeCoordinator() -> CoordinatorTextView {
return CoordinatorTextView(input: $input)
}
}
class CoordinatorTextView: NSObject, UITextViewDelegate {
@Binding var inputCoordinator: String
init(input: Binding<String>) {
self._inputCoordinator = input
}
func textViewDidChange(_ textView: UITextView) {
inputCoordinator = textView.text
}
}
本示例创建了一个名为TexView
的UIViewRepresentable
结构体。结构体中有一个名为input
的@Binding
属性,接收值并传递给SwiftUI视图。在TexView
结构体定义的下面,我们定义了一类名为CoordinatorTextView
的类。这是我们视图的coordinator
,任务是将值发回给SwiftUI视图。为此,我们通过一个关联TexView
结构体中定义的input属性的@Binding
属性对其初始化,然后实现一个代理方法,在用户插入或删了字符时执行。该方法获取输入框中的当前文本,因此我们将其赋值给那个@Binding
属性,发回到SwiftUI视图。UIViewRepresentable
结构体初始化时。CoordinatorTextView
对象通过makeCoordinator()
方法创建,因而该结构体在一开始就可接收和发送值了。
SwiftUI界面中的视图实现和之前相同。只需对存储添加一个@State
属性,将值传递给input
属性。
示例16-4:对UIKit视图接收和发送值
struct ContentView: View {
@State private var inputText: String = "Initial text"
var body: some View {
VStack {
HStack {
Text(inputText)
Spacer()
Button("Clear") {
inputText = ""
}
}
TextView(input: $inputText)
}.padding()
}
}
以上视图定义了一个名为inputText
的@State
属性、一个显示其值的Text
视图、一个用空字符串替换当前值的按钮以及自定义TextInput
视图的一个实例。该视图获取到inputText
属性的指针,与representable
视图中的输入属性相关联,这样便可以来回传递值 。
用户在编辑器中输入或删除字符时,representable
视图调用coordinator
中的textViewDidChange()
方法,该方法将文本视图中的当前值赋给inputCoordinator
属性,也就赋给了input
属性。这意味着值在@State
属性中可用 ,并且Text
视图可将其显示到屏幕上。而在用户点击Clear
按钮时,我们将空字符串赋值给@State
属性,系统执行representable
视图中的updateUIView()
方法,关联@State
属性的@Binding
属性的值被赋给了视图,文本视图被清除。
图16-2:SwiftUI界面中的UITexView
视图
✍️跟我一起做:创建一个多平台项目。根据示例16-3中的代码创建一个名为TextView.swift
的文件。根据示例16-4中的代码更新ContextView
视图。插入一段文本。应该会看到顶部的文本发生了变化。点击Clear
按钮。会看到文本视图及顶部的视图中的文本被删除。
注意:UIViewRepresentable
结构体可表示任意UIKit视图。实现会取决于想使用的视图。我们会在后续的章节中看到更多的示例。要学习UIKit及UiKit视图,可参阅UIKit for MasterMinds等书。
Representable视图控制器
UIViewControllerRepresentable
协议定义一个结构体,作为对UIViewController
类及子类所创建对象的封装。该类展示一个可包含其它视图的视图,为窗口或整个屏幕定义界面,类似SwiftUI文件中所定义的视图。实现UIViewControllerRepresentable
协议的结构体可在一个SwiftUI界面中展示这些视图控制器。创建及管理这个视图控制器,结构体必须实现如下方法。
-
makeUIViewController(context: Context)
:该方法创建一个UIKit视图控制器并返回。context
参数是UIViewControllerRepresentableContext
类型结构体的一个指针,提供视图控制器状态的相关信息。 -
updateUIViewController(UIViewControllerType, context: Context)
:该方法使用SwiftUI界面所提供的信息更新UIKit视图控制器。第一个参数是UIKit视图控制器的指针,context
参数是UIViewControllerRepresentableContext
类型结构体的一个指针,提供视图控制器状态的相关信息。 -
dismantleUiViewController(UIViewControllerType, coordinator: Coordinator)
:该类型方法为释放视图控制器做准备。第一个参数是UIKit视图控制器的指针,coordinator
参数是是将值发回SwiftUI界面的对象。 -
makeCoordinator()
:该方法创建将UIKit视图控制器信息传回SwiftUI界面的对象。
UIKit视图控制器由UIViewController
的子类创建。文件通过File菜单创建 ,但选取UIKit子类的选项称为Cocoa Touch Class。选择该选项后,Xcode会显示一个可输入文件名称的窗口,并可选择所创建子类相应的父类。本例中我们创建了一个继承UIViewController
类的DetailViewController
类。
示例16-5:创建一个UIKit视图控制器
import UIKit
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.frame = CGRect(x: 20, y: 16, width: 250, height: 30)
label.font = UIFont.systemFont(ofSize: 30)
label.text = "Hello World!"
view.addSubview(label)
}
}
在初始化视图控制器类时,它创建一个表示界面的视图,将其赋值给view
属性,并调用viewDidLoad()
方法来告诉我们的代码视图已就绪。在这个方法中,我们可以执行所有必要的任务来初始化视图。在本例中,我们创建一个UILabel
对象来在屏幕上显示文本。该对象类似SwiftUI的Text
视图,但需要做一些配置。在我们的例子中,通过将CGRect
值赋给frame
属性来为其设置位置和大小,字体大小定义为30像素,赋值给要显示的文本,通过addSubview()
方法将其添加给视图控制器的视图。
现在有了视图控制器,我们需要创建一个representable
视图将其转为SwiftUI视图,如下例所示:
示例16-6:为UIKit视图控制器创建representable
视图
import SwiftUI
struct MyViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> DetailViewController {
let controller = DetailViewController()
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
UIViewControllerRepresentable
协议类似UIViewRepresentable
。我们必须定义一个实现此协议的结构体,然后添加创建和更新视图所需的方法。在本例中,我们仅定义了makeUIViewController()
,因为只需要创建视图控制器的实例,来在屏幕上进行显示。以下的SwiftUI视图在用户点击按钮时在NavigationStack
视图中加载该视图控制器。
图16-7:通过SwiftUI视图加载一个UIKit视图控制器
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
NavigationLink("Open UIKit View", destination: {
MyViewController()
}).buttonStyle(.borderedProminent)
Spacer()
}.padding()
}
}
}
以上视图包含一个加载MyViewController
结构体实例的NavigationLink
视图,因而可以从初始视图导航至结构体所创建的视图控制器,和常规SwiftUI视图的操作一样。
图16-3:SwiftUI界面中的UIKit视图控制器
✍️跟我一起做:选择屏幕顶部菜单中的File选项,新建一个文件。点击iOS版块中的Cocoa Touch Class图标,创建一个UIKit文件。在SubClass选项中选择UIViewController
。插入名称DetailViewController
并点击Next进行保存。使用示例16-5中的代码更新DetailViewController
。用示例16-6中的代码创建一个名为MyViewController.swift
的文件。使用示例16-7中的代码更新ContentView
视图。运行应用,点击按钮打开UIKit视图控制器。
代码请见:GitHub仓库