swiftUI中已经提供了很多基础的原生功能,但是还是有很多无法直接通过原生swiftUI来完成的,工程中开发者仍需要在swiftUI中依赖UIKit或AppKit代码。而swiftUI也为开发者提供了便捷的方式将UIKit(AppKit)视图或控制器包装成swiftUI视图。
SwiftUI 和 UIKit(AppKit)视图中的异同
swiftUI和UIKit或AppKit中的主要区别之一就是
- SwiftUI中视图是值类型,并不负责对视图进行绘制,只是视图的描述,实际上并不渲染它们。
- UIKit或AppKit中视图为引用类型,并对视图进行绘制和渲染
- UIKit或AppKit中有明确的生命周期节点,
ViewDidLoad
,viewWillAppear
等,SwiftUI中的视图没有明确的生命周期,它提供了几个Modifier来实现UIKit或AppKit中钩子方法的行为,onAppear
和onDisappear
等。
将UIKit视图包装成SwiftUI视图非常简单,只需要遵守一个UIViewRepresentable
协议即可,需要实现对应的协议方法,其中 makeUIView
和updateUIView
是必须要实现的。且UIViewRepresentable
协议本身是遵守View协议,因此SwiftUI会将任何符合该协议的视图都当做一般的SwiftUI视图来处理。
1.SwiftUI中使用UIKit
定义结构体遵循UIViewRepresentable
,对UIViewRepresentable
中方法执行顺序进行测试,
struct ActivityIndicator: UIViewRepresentable {
typealias UIViewType = UIActivityIndicatorView
var style: UIActivityIndicatorView.Style = .medium
init(_ style: UIActivityIndicatorView.Style = .medium) {
self.style = style
print("init 111")
}
func makeUIView(context: Context) -> UIActivityIndicatorView {
print("makeUIView 222")
let view = UIActivityIndicatorView()
view.color = .black
view.style = self.style
return view
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
print("updateUIView 333")
uiView.startAnimating()
}
func makeCoordinator() -> () {
print("makeCoordinator 444")
}
static func dismantleUIView(_ uiView: UIActivityIndicatorView, coordinator: ()) {
print("dismantleUIView 555")
}
}
//函数执行顺序
init 111
makeCoordinator 444
makeUIView 222
updateUIView 333
updateUIView 333
dismantleUIView 555
- Init
视图初始化声明,在视图生命周期中只会调用一次
- makeCoordinator
如果我们声明了Coordinator,UIViewRepresentable
视图会在初始化后首先创建它的实例,以便在其他的方法中调用,Coordinator
默认为Void,该方法在UIViewRepresentable
的生命周期内只会调用一次,也只有一个协调器实例。
- makeUIView
创建一个包装UIView的容器,该方法在UIViewRepresentable
生命周期中只会调用一次
- updateUIView
SwiftUI会在视图状态发生变化的时候更新受这些视图状态的视图,当UIViewRepresentable
视图中的注入依赖发生变化时,swiftUI会调用updateUIView
,调用时机同swiftUI视图的body一致,最大的不同为调用body为计算值,而调用updateView仅为通知UIViewRepresentable
视图依赖有变化,在这里决定是否要处理这些依赖变化。
该方法在UIViewRepresentable
生命周期中会多次调用,直到视图被移出视图树。在makeUIView方法执行后 updateUIView
方法必然会执行一次
- dismantleUIView
在UIViewRepresentable
视图被移出视图树之前,swiftUI会调用该方法,通常在该方法中执行删除观察器等善后操作
- sizeThatFits
返回视图需要的尺寸,该方法 iOS16 以后才可用
上面实现的视图是对UIKit的UIActivityIndicatorView
进行包装,展示一个选装的菊花
同样的将AppKit包装为SwiftUI对应的视图 NSViewRepresentable 对应 NSView,NSViewContrllerRepresentable对应NSViewController,其内部的实现结构和逻辑都是一致。
makeCoordinator(协调器) 作用
-
实现UIKit视图的代理
-
同swiftUI框架进行沟通
-
处理UIKit视图中的复杂逻辑
接下来 对UIKit中的TextField进行包装,使用makeCoordinator对TextField的代理方法和键盘按钮点击事件进行相应,直接设置如下,展示一个TextField,并对当前输入的文字,实时同步到Text中
struct ZJTextFieldTest: View {
@State private var inputText:String = ""
var body: some View {
VStack{
Text("测试展示数据")
TextFieldWrapper("请输入", text: $inputText)
.border(.blue)
.padding()
Text("修改同步展示")
Text(inputText)
}
}
}
struct TextFieldWrapper: UIViewRepresentable {
/// 对UITextField进行包装
typealias UIViewType = UITextField
let placeholder: String
@Binding var text: String
init(_ placeholder: String,text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.placeholder = placeholder
textField.text = text
/// UItextField在swiftUI中不进行约束的情况下回默认占据全部可用空间,需要尽心以下设置就可以达到预期的效果
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
func makeCoordinator() -> () {
}
static func dismantleUIView(_ uiView: UITextField, coordinator: ()) {
}
}
此时设置的View会充满整个屏幕,是因为TextField在不进行约束的情况下会占据全部的可用空间,进行如下设置会达到预期效果。
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
进行设置后在UI上满足了我们,但是进行输入之后,并不会在下方同步展示输入文字,那是因为在包装的视图中并没有对绑定的text进行修改,外部也不会进行展示。UITextField在每次录入文字的时候都会自动调用func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
代理方法,需要在该方法中将对应的值回调回初时设置中。
1.创建Coordinator(协调器)
extension TextFieldWrapper {
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
init(text: Binding<String>){
self._text = text
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as? NSString {
let finalText = text.replacingCharacters(in: range, with: string)
self.text = finalText as String
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
//点击发送 分行等 return按钮的回调
return true
}
}
}
2.初始化Coordinator 设置TextField代理
func makeUIView(context: Context) -> UITextField {
………………
textField.delegate = context.coordinator
………………
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
func makeCoordinator() -> Coordinator {
//初始化 协调器
.init(text: $text)
}
此时输入文字,下方也同步展示输入文字
3.SwiftUI 更新TextField展示数据
在展示TextField视图中修改inputText的值,预期是 TextField和下方同步展示的Text都会修改,但是点击按钮的时候,只有同步的Text变化,TextField中并不会相应。
由于在makeUIView
中设置了textField.text = text
该方法在生命周期内只会执行一次,swiftUI层中的inputText进行修改,TextField并不会同时修改。我们需要在updateUIView
方法中更新text
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
这样就实现了 UItextField输入和swiftUI同步修改状态。上面这些操作基本上实现了TextField的输入和文字对外同步功能,但是TextField对外处理中和一些UIKit中常用的功能还需要对Coordinator进行修改,支持文字修改和事件回调.接下来添加如下属性设置和回调
let placeholder: String
@Binding var text: String
let color: UIColor
let font: UIFont
let returnKeyType: UIReturnKeyType
let clearButtonMode: UITextField.ViewMode
var onCommit: () -> Void
var onEditingChanged: (Bool) -> Void
/// 更新对应的展示视图
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.font = font
uiView.textColor = color
uiView.returnKeyType = returnKeyType
uiView.clearButtonMode = clearButtonMode
uiView.text = text
}
func makeCoordinator() -> Coordinator {
.init(text: $text, onCommit:onCommit, onEditingChanged: onEditingChanged)
}
在协调器中设置对应的回调事件和TextField相应的代理方法
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
//点击发送 分行等 return按钮的回调
onCommit()
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
onEditingChanged(true)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
onEditingChanged(false)
}
这样我们在初始化方法中进行对应属性的设置就可以监听TextField 输入和点击return 按钮
TextFieldWrapper("请输入",
text: $inputText,
color:.red,
font: .systemFont(ofSize: 18),
onCommit: {
print("点击提交")
},
onEditingChanged: { value in
print("编辑状态变化")
})
.border(.blue)
.padding()
4.swiftUI 风格化处理
上面在TextFieldWrapper初始化时需要传入很多的参数,对比swiftUI设置属性的链式语法显的很异类,而且随着功能的逐渐增加,初始化越来越冗余,接下来将上面的属性设置包装swiftUI化,
/// swiftUI 支持的风格
TextFieldWrapper(text: $inputText)
.font(.systemFont(ofSize: 18))
.color(.red)
.returnKeyType(.done)
.onCommit {
print("点击提交")
}
.onEditingChanged { value in
print("编辑状态变化(value)")
}
.border(.blue)
.padding()
/// 传值设置
TextFieldWrapper("请输入",
text: $inputText,
color:.red,
font: .systemFont(ofSize: 18),
onCommit: {
print("点击提交")
},
onEditingChanged: { value in
print("编辑状态变化")
})
.border(.blue)
.padding()
支持swiftUI风格 变量设置为private,通过对应的方法对变量进行修改
extension TextFieldWrapper {
//swiftUI风格写法
func color(_ color: UIColor) -> Self {
then { $0.color = color }
}
func font(_ font: UIFont) -> Self {
then{ $0.font = font }
}
func placeholder(_ placeholder: String) -> Self {
then{ $0.placeholder = placeholder }
}
func returnKeyType(_ returnKeyType: UIReturnKeyType) -> Self {
then { $0.returnKeyType = returnKeyType }
}
func clearButtonMode(_ clearButtonMode: UITextField.ViewMode) -> Self {
then { $0.clearButtonMode = clearButtonMode }
}
func onCommit(_ task: @escaping () -> ()) -> Self {
then { $0.onCommit = task }
}
func onEditingChanged(_ task: @escaping (Bool) -> ()) -> Self {
then { $0.onEditingChanged = task }
}
}
/// 对View的拓展 支持链式语法
extension View {
func then(_ body: (inout Self) -> Void) -> Self {
var result = self
body(&result)
return result
}
}
对于onCommit和onEditingChanged 可以通过在内部修改协调器内的变量的方法进行,这是一种非常有效的方法。
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.font = font
uiView.textColor = color
uiView.returnKeyType = returnKeyType
uiView.clearButtonMode = clearButtonMode
uiView.text = text
//修改协调器内部变量
context.coordinator.onCommit = onCommit
context.coordinator.onEditingChanged = onEditingChanged
}
回过头来看UIViewRepresentable
中的协议方法和对UITextField的包装过程
同理对应UIViewControllerRepresentable协议也是如此,将UIViewController进行包装可以在SwiftUI中直接使用。
2.UIKit或AppKit中使用SwiftUI
UIKit要使用SwiftUI中视图,需要借助UIHostingController
AppKit中使用SwiftUI中视图需要借助NSHostingView或NSHostingController
接下来我们以UIKit中UIHostingController
为例
1.创建SwiftUI视图
struct SwiftUITestPage: View {
var title: String = "我就是我不一样的烟火"
var textColor: Color = .black
/// 点击回调方法
var onSubmit: (() -> ())?
var body: some View {
VStack{
Text(title)
.font(.system(size: 14))
.foregroundStyle(textColor)
.padding()
Button {
print("按钮开始点击了")
onSubmit?()
} label: {
Text("我是一个按钮")
.padding()
.font(.system(size: 14))
.foregroundStyle(textColor)
.frame(width: 150,height: 40)
.background(.teal)
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay {
RoundedRectangle(cornerRadius:20).strokeBorder(.blue, style: StrokeStyle(lineWidth:1))
}
}
}
}
}
swiftUI 视图中 定义了一个title,color和按钮点击事件回调,
2.UIKit中包装SwiftUI
func addSwiftUI(){
//展示SwiftUi 视图
let hostController = UIHostingController(rootView: SwiftUITestPage())
addChild(hostController)
self.view.addSubview(hostController.view)
hostController.view.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 200, height: 200))
make.center.equalToSuperview()
}
}
展示如下
3.UIKit和SwiftUI视图交互
在UIKit中我们定义一个可以改变titile和 color的点击事件,SwiftUI视图中同样定义了一个回调方法,在UIKit监听SwiftUI中 按钮的点。修改titile和color 是通过对UIHostingController
中的rootView的修改,其实就是重新创建SwiftUI视图,在swiftUI中创建视图是很轻量的,SwiftUI中视图是值类型的且只是负责对UI样式的声明,占用内存少,创建几乎没有开销。
var hostVC: UIHostingController<SwiftUITestPage>!
// 事件接收
let swiftUICallClosure: () -> () = {
print("按钮事件点击===1111")
}
@objc func buttonAction(sender: UIButton){
if sender.tag == 1 {
hostVC.rootView = recreateSwiftUIPage(title: "我就是我------")
}else{
hostVC.rootView = recreateSwiftUIPage(color: .red)
}
}
/// 每次都是创建新的SwiftUI视图,回调执行保持不变
func recreateSwiftUIPage(title: String? = nil, color: Color? = nil) -> SwiftUITestPage {
SwiftUITestPage(title: title, textColor: color, onSubmit: swiftUICallClosure)
}
这样修改下来会有一个弊端,每次修改title和color,之前修改的样式又会回到初始状态,这就需要我们在创建swiftUI视图的时候,同步保存修改的数据。对其进行修改如下(简单使用)
let titles = ["123我来了","大哥火箭走一波","哈哈哈哈","测试是不是好了","我自横刀向天笑,去留肝胆两昆仑","感时花溅泪,恨别鸟惊心"]
let colors: [Color] = [.red,.green,.pink,.purple,.accentColor,.brown,.cyan]
var testModel: SwiftTestModel = SwiftTestModel(title: "我是初始值", textColor: .black)
///每次修改属性值 都重新创建SwiftUI
@objc func buttonAction(sender: UIButton){
if sender.tag == 1 {
testModel.title = titles[Int.random(in: 0..<titles.count)]
}else{
testModel.textColor = colors[Int.random(in: 0..<colors.count)]
}
hostVC.rootView = recreateSwiftUIPage()
}
func recreateSwiftUIPage() -> SwiftUITestPage {
SwiftUITestPage(testModel: testModel, onSubmit: swiftUICallClosure)
}
这样每次修改都会保存最新的值,创建SwiftUI视图就不会回到初始状态
由于系统只提供了UIHostingController这个容器,需要先把对应的swiftUI视图包装成ViewController才能使用,之后通过rootView获取到当前的swiftUI视图设置约束 当做UIView 来使用。
同理 AppKit中 提供了NSHostingView
和NSHostingController
来分别包装NSView
和NSViewController