在SwiftUI中使用UIKit视图

2022-07-28 12:50:56 浏览数 (1)

在SwiftUI中使用UIKit视图

如想获得更好的阅读体验可以访问我的博客www.fatbobman.com,或点击下方的阅读原文

已迈入第三个年头的SwiftUI相较诞生初始已经提供了更多的原生功能,但仍有大量的事情是无法直接通过原生SwiftUI代码来完成的。在相当长的时间中开发者仍需在SwiftUI中依赖UIKit(AppKit)代码。好在,SwiftUI为开发者提供了便捷的方式将UIKit(AppKit)视图(或控制器)包装成SwiftUI视图。

本文将通过对UITextField的包装来讲解以下几点:

•如何在SwiftUI中使用UIKit视图•如何让你的UIKit包装视图具有SwiftUI风格•在SwiftUI使用UIKit视图需要注意的地方

如果你已经对如何使用UIViewRepresentable有所掌握,可以直接从SwiftUI风格化部分阅读

基础

在具体演示包装代码之前,我们先介绍一些与在SwiftUI中使用UIKit视图有关的基础知识。

无需担心是否能立即理解下述内容,在后续的演示中会有更多的内容帮助你掌握相关知识。

生命周期

SwiftUI同UIKit和AppKit的主要区别之一是,SwiftUI的视图(View)是值类型,并不是对屏幕上绘制内容的具体引用。在SwiftUI中,开发者为视图创建描述,而并不实际渲染它们。

在UIKit(或AppKit)中,视图(或视图控制器)有明确的生命周期节点,比如vidwDidloadloadViewviewWillAppeardidAddSubViewdidMoveToSuperview等方法,它们本质上充当了钩子的角色,让开发者能够通过执行一段逻辑来响应系统给定的事件。

SwiftUI的视图,本身没有清晰(可适当描述)的生命周期,它们是值、是声明。SwiftUI提供了几个修改器(modifier)来实现类似UIKit中钩子方法的行为。比如onAppearviewWillAppear的表现很类似。同UIKit的钩子方法的位置有很大的不同, onAppearonDisappear是在当前视图的父视图上声明的。

将UIKit视图包装成SwiftUI的视图时,我们需要了解两者生命周期之间的不同,不要强行试图找到完全对应的方法,要从SwiftUI的角度来思考如何调用UIKit视图。

UIViewRepresentable协议

在SwiftUI中包装UIView非常简单,只需要创建一个遵守UIViewRepresentable协议的结构体就行了。

UIViewControllerRepresentable对应UIViewControllerNSViewRepresentable对应NSViewNSViewControllerRepresentable对应NSViewController。内部的结构和实现逻辑都一致。

UIViewrepresentable的协议并不复杂,只包含:makeUIViewupdateUIViewdismantleUIViewmakeCoordinator四个方法。makeUIViewupdateUIView为必须提供实现的方法。

UIViewRepresentable本身遵守View协议,因此SwiftUI会将任何符合该协议的结构体都当作一般的SwiftUI视图来对待。不过由于UIViewRepresentable的特殊的用途,其内部的生命周期又同标准的SwiftUI视图有所不同。

UIViewRepresentableLifeCycle

•makeCoordinator如果我们声明了Coordinator(协调器),UIViewRepresentable视图会在初始化后首先创建它的实例,以便在其他的方法中调用。Coordinator默认为Void,该方法在UIViewRepresentable的生命周期中只会调用一次,因此只会创建一个协调器实例。•makeUIView创建一个用来包装的UIKit视图实例。该方法在UIViewRepresentable的生命周期中只会调用一次。•updateUIViewSwiftUI会在应用程序的状态(State)发生变化时更新受这些变化影响的界面部分。当UIViewRepresentable视图中的注入依赖发生变化时,SwiftUI会调用updateUIView。其调用时机同标准SwiftUI视图的body一致,最大的不同为,调用body为计算值,而调用updateview仅为通知UIViewRepresentable视图依赖有变化,至于是否需要根据这些变化来做反应,则由开发者来自行处理。该方法在UIViewRepresentable的生命周期中会多次调用,直到视图被移出视图树(更准确地描述是切换到另一个不包含该视图的视图树分支)。在makeUIVIew执行后,updateUIVew必然会执行一次•dismantleUIView在UIViewRepresentable视图被移出视图树之前,SwiftUI会调用dismantleUIView,通常在此方法中可以执行u删除观察器等善后操作。dismantleUIView为类型方法。

下面的代码将创建一个同ProgressView一样的转圈菊花:

代码语言:javascript复制
struct MyProgrssView: UIViewRepresentable {    func makeUIView(context: Context) -> UIActivityIndicatorView {        let view = UIActivityIndicatorView()        view.startAnimating()        return view    }    func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {}}struct Demo: View {    var body: some View {            MyProgrssView()    }}

黑匣子

SwiftUI在绘制屏幕时,会从视图树的顶端开始对视图的body求值,如果其中还包含子视图则将递归求值,直到获得最终的结果。但SwiftUI无法真正进行无限量的调用来绘制视图,因此它必须以某种方式缩短递归。为了结束递归,SwiftUI包含了很多的原始类型(primitive types)。当SwiftUI递归到这些原始类型时,将结束递归,它将不再关心原始类型的body,而让原始类型自行对其管理的区域进行处理。

SwiftUI框架通过将body定义为Never来标记该View为原始类型。UIViewRepresentable恰巧也为其中之一(TextZStackColorList等也都是所谓的原始类型)。

代码语言:javascript复制
public protocol UIViewRepresentable : View where Self.Body == Never

事实上几乎所有的原始类型都是对UIKit或AppKit的底层包装。

UIViewRepresentable作为原始类型,SwiftUI对其内部所知甚少(因为无需关心)。通常需要开发者在UIViewRepresentable视图的Coordinator(协调器)中做一些的工作,从而保证两个框架(SwiftUI同UIKit)代码之间的沟通和联系。

协调器

苹果框架很喜欢使用协调器(Coordinator)这个名词,UIKit开发中有协调器设计模式、Core Data中有持久化存储协调器。在UIViewRepresentable中协调器同它们的概念完全不同,主要起到以下几个方面的作用:

•实现UIKit视图的代理UIKit组件通常依赖代理(delegate)来实现一些功能,“代理”是响应其他地方发生的事件的对象。例如,UIKit中我们将一个代理对象附加到Text field视图上,当用户输入时,当用户按下return键时,该代理对象中对应的方法将被调用。通过将协调器声明为UIKit视图对应的代理对象,我们就可以在其中实现所需的代理方法。•同SwiftUI框架保持沟通上文中,我们提到UIViewRepresentable作为原始类型,需要主动承担更多的同SwiftUI框架或其他视图之间的沟通工作。在协调器中,我们可以通过双向绑定(Binding),通知中心(notificationCenter)或其他例如Redux模式的单项数据流等方式,将UIKit视图内部的状态报告给SwiftUI框架或其他需要的模块。同样也可以通过注册观察器、订阅Publisher等方式获取所需的信息。•处理UIKit视图中的复杂逻辑在UIKit开发中,通常会将业务逻辑放置在UIViewController中,SwiftUI没有Controller这个概念,视图仅是状态的呈现。对于一些实现复杂功能的UIKit模组,如果完全按照SwiftUI的模式将其业务逻辑彻底剥离是非常困难的。因此将无法剥离的业务逻辑的实现代码放入协调器中,靠近代理方法,便于相互之间的协调和管理。

包装UITextField

本节中我们将利用上面的知识实现一个具有简单功能的UITextField包装视图——TextFieldWrapper

版本1.0

在第一个版本中,我们要实现一个类似如下原生代码的功能:

代码语言:javascript复制
TextField("name:",text:$name)

image-20210822184949860

查看源代码

我们在makeUIView中创建了UITextField的实例,并对其placeholder和text进行了设定。在右侧的预览中,我们可以看到placeholder可以正常显示,如果你在其中输入文字,表现的状态也同TextField完全一致。

通过.border,我们看到TextFieldWrapper的视图尺寸没有符合预期,这是由于UITextField在不进行约束的情况下会默认占据全部可用空间。上文关于UIActivityIndicatorView的演示代码并没有出现这个情况。因此对于不同的UIKit组件,我们需要了解其默认设置,酌情对其进行约束设定。

makeUIView中添加如下语句,此时文本输入框的尺寸就和预期一致了:

代码语言:javascript复制
        textfield.setContentHuggingPriority(.defaultHigh, for: .vertical)        textfield.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

稍微调整一下Demo视图,在.padding()下添加Text("name:(name)")。如果按照TextField的正常行为,当我们在其中输入任何文本时,下方的Text中应该显示出对应的内容,不过在我们当前的代码版本中,并没有表现出预期的行为。

image-20210822190605447

让我们再次来分析一下代码。

尽管我们声明了一个Binding<String>类型的text,并且在makeUIView中将其赋值给了textfield,不过UITextField并不会将我们录入的内容自动回传给Binding<String>text,这导致Demo视图中的name并不会因为文字录入而发生改变。

UITextfield在每次录入文字时,都会自动调用func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool的代理方法。因此我们需要创建协调器,并在协调器中实现该方法,将录入的内容传递给Demo视图中的name变量。

创建协调器:

代码语言:javascript复制
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        }    }}

我们需要在textField方法中回传数据,因此在Coordinator中同样需要使用到Binding<String>,如此对text的操作即为对Demo视图中name的操作。

如果UIViewRepresentable视图中的Coordinator不为Void,则必须通过makeCoordinator来创建它的实例。在TextFieldWrapper中添加如下代码:

代码语言:javascript复制
    func makeCoordinator() -> Coordinator {        .init(text: $text)    }

最后在makeUIView中添加:

代码语言:javascript复制
    textfield.delegate = context.coordinator

UITextField在发生特定事件后将在协调器中查找并调用对应的代理方法。

image-20210822191834883

查看源代码

至此,我们创建的UITextField包装已经同原生的TextField的表现行为一致了。

你确定?

再度修改一下Demo视图,将其修改为:

代码语言:javascript复制
struct Demo: View {    @State var name: String = ""    var body: some View {        VStack {            TextFieldWrapper("name:", text: $name)                .border(.blue)                .padding()            Text("name:(name)")            Button("Random Name"){                name = String(Int.random(in: 0...100))            }        }    }}

按照对原生TextField的表现预期,当我们按下Random Name按钮时,TextTextFieldWrapper中的文字都应该变成由String(Int.random(in: 0...100))产生的随机数字,但是如果你使用上述代码进行测试,TextFieldWrapper中的文字并没有变化。

makeUIView中,我们使用textfield.text = text获取了Demo视图中name的值,但makeUIView只会执行一次。当点击Random Name引起name变化时,SwiftUI将会调用updateUIView,而我们并没有在其中做任何的处理。只需要在updateUIVIew中添加如下代码即可:

代码语言:javascript复制
    func updateUIView(_ uiView: UIViewType, context: Context) {        DispatchQueue.main.async {            uiView.text = text        }    }

makeUIView方法的参数中有一个context: Context,通过这个上下文,我们可以访问到Coordinator(自定义协调器)、transaction(如何处理状态更新,动画模式)以及environment(当前视图的环境值集合)。我们之后将通过实例演示其用法。该context同样可以在updateUIVIewdismantleUIView中访问。updataUIView的参数_ uiView:UIViewType为我们在makeUIVIew中创建的UIKit视图实例。

查看源代码

现在,我们的TextFieldWrapper的表现已经确实同TextField一致了。

textFieldWrappertest

版本2.0——添加设定

在第一个版本的基础上,我们将为TextFieldWrapper添加colorfontclearButtonModeonCommit以及onEditingChanged的配置设定。

考虑到尽量不将例程复杂化,我们使用UIColorUIFont作为配置类型。将SwiftUI的ColorFont转换成UIKit版本将增加不小的代码量。

colorfont以及我们新增加的clearButtonMode并不需要双向数据流,因此无需采用Binding方式,仅需在updateView中及时响应它们的变化既可。

onCommitonEditingChanged分别对应着UITextField代理的textFieldShouldReturntextFieldDidBeginEditing以及textFieldDidEndEditing方法,我们需要在协调器中分别实现这些方法,并调用对应的Block

首先修改协调器:

代码语言:javascript复制
extension TextFieldWrapper {    class Coordinator: NSObject, UITextFieldDelegate {        @Binding var text: String        var onCommit: () -> Void        var onEditingChanged: (Bool) -> Void        init(text: Binding<String>,             onCommit: @escaping () -> Void,             onEditingChanged: @escaping (Bool) -> Void) {            self._text = text            self.onCommit = onCommit            self.onEditingChanged = onEditingChanged        }        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 {            onCommit()            return true        }        func textFieldDidBeginEditing(_ textField: UITextField) {            onEditingChanged(true)        }        func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {            onEditingChanged(false)        }    }}

TextFieldWrapper进行修改:

代码语言:javascript复制
struct TextFieldWrapper: UIViewRepresentable {    init(_ placeholder: String,         text: Binding<String>,         color: UIColor = .label,         font: UIFont = .preferredFont(forTextStyle: .body),         clearButtonMode:UITextField.ViewMode = .whileEditing,         onCommit: @escaping () -> Void = {},         onEditingChanged: @escaping (Bool) -> Void = { _ in }    )    {        self.placeholder = placeholder        self._text = text        self.color = color        self.font = font        self.clearButtonMode = clearButtonMode        self.onCommit = onCommit        self.onEditingChanged = onEditingChanged    }    let placeholder: String    @Binding var text: String    let color: UIColor    let font: UIFont    let clearButtonMode: UITextField.ViewMode    var onCommit: () -> Void    var onEditingChanged: (Bool) -> Void    typealias UIViewType = UITextField    func makeUIView(context: Context) -> UIViewType {        let textfield = UITextField()        textfield.setContentHuggingPriority(.defaultHigh, for: .vertical)        textfield.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)        textfield.placeholder = placeholder        textfield.delegate = context.coordinator        return textfield    }    func updateUIView(_ uiView: UIViewType, context: Context) {        DispatchQueue.main.async {            uiView.text = text            uiView.textColor = color            uiView.font = font            uiView.clearButtonMode = clearButtonMode        }    }    func makeCoordinator() -> Coordinator {        .init(text: $text,onCommit: onCommit,onEditingChanged: onEditingChanged)    }}

修改Demo视图:

代码语言:javascript复制
struct Demo: View {    @State var name: String = ""    @State var color: UIColor = .red    var body: some View {        VStack {            TextFieldWrapper("name:",                             text: $name,                             color: color,                             font: .preferredFont(forTextStyle: .title1),                             clearButtonMode: .whileEditing,                             onCommit: { print("return") },                             onEditingChanged: { editing in print("isEditing (editing)") })                .border(.blue)                .padding()            Text("name:(name)")            Button("Random Name") {                name = String(Int.random(in: 0...100))            }            Button("Change Color") {                color = color == .red ? .label : .red            }        }    }}struct TextFieldWrapperPreview: PreviewProvider {    static var previews: some View {        Demo()    }}

查看源代码

textfieldwrapperdemo2

SwiftUI 风格化

我们不仅实现了对字体、色彩的设定,而且增加了原生TextField没有的clearButtonMode设置。按照上述的方法,可以逐步为其添加更多的设置,让TextFieldWrapper获得更多的功能。

代码好像有点不太对劲?!

随着功能配置的增加,上面代码在使用中会愈发的不方便。如何实现类似原生TextFiled的链式调用呢?譬如:

代码语言:javascript复制
               TextFieldWrapper("name:",text:$name)                .clearMode(.whileEditing)                                .onCommit{print("commit")}                .foregroundColor(.red)                .font(.title)                .disabled(allowEdit)

本节中,我们将重写配置代码,实现UIKit包装风格SwiftUI化。

本节以版本1.0结束时的代码为基础。

所谓的SwfitUI风格化,更确切地说应该是函数式编程的链式调用。将多个操作通过点号(.)链接在一起,增加可读性。作为将函数视为一等公民的Swift,实现上述的链式调用非常方便。不过有以下几点需要注意:

•如何改变View内的的值(View是结构)•如何处理返回的类型(保证调用链继续有效)•如何利用SwiftUI框架现有的数据并与之交互逻辑

为了更全面的演示,下面的例子,采用了不同的处理方式。在实际使用中,可根据实际需求选择适当的方案。

foregroundColor

我们在SwiftUI中经常会用到foregroundColor来设置前景色,比如下面的代码:

代码语言:javascript复制
            VStack{                Text("hello world")                    .foregroundColor(.red)            }            .foregroundColor(.blue)

不知道大家是否知道上面的两个foregroundColor有什么不同。

代码语言:javascript复制
extension Text{      public func foregroundColor(_ color: Color?) -> Text}extension View{      public func foregroundColor(_ color: Color?) -> some View}

方法名一样,但作用的对象不同。Text只有在针对本身的foregroundColor没有设置的时候,才会尝试从当前环境中获取foregroundColor(针对View)的设定。原生的TextFiled没有针对本身的foregroundColor,不过我们目前也没有办法获取到SwiftUI针对View的foregroundColor设定的环境值(估计是),因此我们可以使用Text的方式,为TextFieldWrapper创建一个专属的foregroundColor

TextFieldWrapper添加一个变量

代码语言:javascript复制
private var color:UIColor = .label

updateUIView中增加

代码语言:javascript复制
uiView.textColor = color

设置配置方法:

代码语言:javascript复制
extension TextFieldWrapper {    func foregroundColor(_ color:UIColor) -> Self{        var view = self        view.color = color        return view    }}

查看源代码

就这么简单。现在我们就可以使用.foreground(.red)来设置TextFieldWrapper的文字颜色了。

这种写法是为特定视图类型添加扩展的常用写法。有以下两个优点:

•使用private,无需暴露配置变量•仍返回特定类型的视图,有利于维持链式稳定

我们几乎可以使用这种方式完成全部的链式扩展。如果扩展较多时,可以采用下面的方式,进一步清晰、简化代码:

代码语言:javascript复制
    extension View {        func then(_ body: (inout Self) -> Void) -> Self {            var result = self            body(&result)            return result        }    }        func foregroundColor(_ color:UIColor) -> Self{        then{            $0.color = color        }    }

disabled

SwiftUI针对View预设了非常多的扩展,其中有相当的部分都是通过环境值EnvironmentValue来逐级传递的。通过直接响应该环境值的变化,我们可以在不编写特定TextFieldWrapper扩展的情况下,即可为其增加配置功能。

例如,View有一个扩展.disabled,通常我们会用它来控制交互控件的可操作性(.disable对应的EnviromentValueisEnabled)。

TextFieldWrapper中添加:

代码语言:javascript复制
@Environment(.isEnabled) var isEnabled

updateUIView中添加:

代码语言:javascript复制
uiView.isEnabled = isEnabled

只需要两条语句,TextFieldWrapper便可以直接使用Viewdisable扩展来控制其是否可以录入数据。

还记得上文中介绍的context吗?我们可以直接通过context获取上下文中的环境值。因此支持原生的View扩展将一步简化。

无需添加@Environemnt,只需要在updateUIView中添加一条语句既可:

代码语言:javascript复制
uiView.isEnabled = context.environment.isEnabled

查看源代码

在写本文时,在iOS15 beta下运行该代码,会出现AttributeGraph: cycle detected through attribute的警告,这个应该是iOS15的Bug,请自行忽略。

通过环境值来设置是一种十分便捷的方式,唯一需要注意的是,它会改变链式结构的返回值。因此,在该节点后的链式方法只能是针对View设置的,像之前我们创建的foregroundColor就只能放置在这个节点之前。

font

我们也可以自己创建环境值来实现对TextFieldWrapper的配置。比如,SwiftUI提供的font环境值的类型为Font,本例中我们将创建一个针对UIFont的环境值设定。

创建环境值myFont

代码语言:javascript复制
struct MyFontKey:EnvironmentKey{    static var defaultValue: UIFont?}extension EnvironmentValues{    var myFont:UIFont?{        get{self[MyFontKey.self]}        set{self[MyFontKey.self] = newValue}    }}

updateUIVIew中添加:

代码语言:javascript复制
uiView.font = context.environment.myFont

font方法可以有多种写法:

•同forgroundColor一样的对TextFieldWrapper进行扩展

代码语言:javascript复制
    func font(_ font:UIFont) -> some View{        environment(.myFont, font)    }

•对View进行扩展

代码语言:javascript复制
extension View {    func font(_ font:UIFont?) -> some View{        environment(.myFont, font)    }}

两种方式的链式节点的返回值都不再是TextFieldWrapper,后面应该接针对View的扩展。

查看源代码

onCommit

在版本2的代码中,我们为TextFieldWrapper添加了onCommit设置,在用户输入return时会触发该段代码。本例中,我们将为onCommit添加一个可修改版本,且不需要通过协调器构造函数传递。

本例中的技巧在之前都出现过,唯一需要提醒的是在updateUIView中,可以通过

代码语言:javascript复制
context.coordinator.onCommit = onCommitcontext.coordinator.onEditingChanged = onEditingChanged

改变协调器内的变量。这是一种非常有效的在SwiftUI和协调器之间进行沟通的手段。

image-20210823091321562

查看源代码

避免滥用UIKit包装

尽管在SwiftUI中使用UIKit或AppKit并不麻烦,但是当你打算包装一个UIKit控件时(尤其是已有SwiftUI官方原生解决方案),请务必三思。

苹果对SwiftUI的野心非常大,不仅为开发者带来了声明 响应式的编程体验,同时苹果对SwiftUI在跨设备、跨平台上(苹果生态)也做出了巨大的投入了。

苹果为每一个原生控件(比如TextField),针对不同的平台(iOS、macOS、tvOS、watchOS)做了大量的优化。这是其他任何人都很难自己完成的。因此,在你打算为了某个特定功能重新包装一个系统控件时,请先考虑以下几点。

官方的原生方案

SwiftUI这几年发展的很快,每个版本都增加了不少新功能,或许你需要的功能已经被添加。苹果最近两年对SwiftUI的文档支持提高了不少,但还没到令人满意的地步。作为SwiftUI的开发者,我推荐大家最好购买一份javier开发的A Companion for SwiftUI。该app提供了远比官方丰富、清晰的SwiftUI API指南。使用该app你会发现原来SwiftUI提供了如此多的功能。

用原生方法组合解决

在SwiftUI 3.0版本之前,SwiftUI并不提供searchbar,此时会出现两种路线,一种是自己包装一个UIKit的UISearchbar,另外就是通过使用SwiftUI的原生方法来组合一个searchbar。在多数情况下,两种方式都能取得满意的效果。不过用原生方法创建的searchbar在构图上更灵活,同时支持使用LocalizedString作为placeholder。我个人会更倾向于使用组合的方案。

SwiftUI中很多数据类型官方并不提供转换到其他框架类型的方案。比如ColorFont。不过这两个多写点代码还是可以转换的。LocalizedString目前只能通过非正常的手段来转换(使用Mirror),很难保证可以长久使用该转换方式。

Introspect for SwiftUI

在版本2代码中,我们为TextFieldWrapper添加了clearButtonMode的设置,也是我们唯一增加的目前TextField尚不支持的设定。不过,如果我们仅仅是为了添加这个功能就自己包装UITextField那就大错特错了。

Introspect通过自省的方法来尝试查找原生控件背后包装的UIKit(或AppKit)组件。目前官方尚未在SwiftUI中开放的功能多数可以通过此扩展库提供的方法来解决。

比如:下面的代码将为原生的TextField添加clearButtonMode设置

代码语言:javascript复制
        import Introspect        extension TextField {            func clearButtonMode(_ mode:UITextField.ViewMode) -> some View{                introspectTextField{ tf in                    tf.clearButtonMode = mode                }            }        }        TextField("name:",text:$name)           .clearButtonMode(.whileEditing)

总结

SwiftUI与UIKit和AppKit之间的互操作性为开发者提供了强大的灵活性。学会使用很容易,但想用好确实有一定的难度。在UIKit视图和SwiftUI视图之间共享可变状态和复杂的交互通常相当复杂,需要我们在这两种框架之间构建各种桥接层。

本文并没有涉及包装具有复杂逻辑代码的协调器同SwiftUI或Redux模式沟通交互的话题,里面包含的内容过多,或许需要通过另一篇文章来探讨。

希望本文能对你学习和了解如何将UIKit组件导入SwiftUI提供一点帮助。

0 人点赞