在 SwiftUI 中使用 UIKit 视图

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

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

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

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

健康笔记是我开发的一个iOS app,主要服务于有长期健康管理需求的人士。可为全家不同成员创建各自的笔记,允许使用者自定义数据类型以记录各种健康数据。

推广

基础

在具体演示包装代码之前,我们先介绍一些与在 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的生命周期中只会调用一次。

  • updateUIView

SwiftUI 会在应用程序的状态(State)发生变化时更新受这些变化影响的界面部分。当UIViewRepresentable视图中的注入依赖发生变化时,SwiftUI 会调用updateUIView。其调用时机同标准 SwiftUI 视图的body一致,最大的不同为,调用body为计算值,而调用updateview仅为通知UIViewRepresentable视图依赖有变化,至于是否需要根据这些变化来做反应,则由开发者来自行处理。

该方法在UIViewRepresentable的生命周期中会多次调用,直到视图被移出视图树(更准确地描述是切换到另一个不包含该视图的视图树分支)。

在 makeUIVIew 执行后,updateUIVew 必然会执行一次

  • dismantleUIView

UIViewRepresentable视图被移出视图树之前,SwiftUI 会调用dismantleUIView,通常在此方法中可以执行 u 删除观察器等善后操作。dismantleUIView为类型方法。

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

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等也都是所谓的原始类型)。

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

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

TextField("name:",text:$name)
image-20210822184949860

查看 源代码

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

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

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

        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变量。

创建协调器:

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中添加如下代码:

    func makeCoordinator() -> Coordinator {
        .init(text: $text)
    }

最后在makeUIView中添加:

    textfield.delegate = context.coordinator

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

image-20210822191834883

查看 源代码

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

你确定?

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

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中添加如下代码即可:

    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

首先修改协调器:

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进行修改:

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视图:

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

健康笔记是我开发的一个iOS app,主要服务于有长期健康管理需求的人士。可为全家不同成员创建各自的笔记,允许使用者自定义数据类型以记录各种健康数据。

推广

SwiftUI 风格化

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

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

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

        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来设置前景色,比如下面的代码:

            VStack{
                Text("hello world")
                    .foregroundColor(.red)
            }
            .foregroundColor(.blue)

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

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添加一个变量

private var color:UIColor = .label

updateUIView中增加

uiView.textColor = color

设置配置方法:

extension TextFieldWrapper {
    func foregroundColor(_ color:UIColor) -> Self{
        var view = self
        view.color = color
        return view
    }
}

查看 源代码

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

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

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

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

    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中添加:

@Environment(\.isEnabled) var isEnabled

updateUIView中添加:

uiView.isEnabled = isEnabled

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

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

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

uiView.isEnabled = context.environment.isEnabled

查看 源代码

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

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

font

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

创建环境值myFont

struct MyFontKey:EnvironmentKey{
    static var defaultValue: UIFont?
}

extension EnvironmentValues{
    var myFont:UIFont?{
        get{self[MyFontKey.self]}
        set{self[MyFontKey.self] = newValue}
    }
}

updateUIVIew中添加:

uiView.font = context.environment.myFont

font方法可以有多种写法:

  • forgroundColor一样的对TextFieldWrapper进行扩展
    func font(_ font:UIFont) -> some View{
        environment(\.myFont, font)
    }
  • View进行扩展
extension View {
    func font(_ font:UIFont?) -> some View{
        environment(\.myFont, font)
    }
}

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

查看 源代码

onCommit

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

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

context.coordinator.onCommit = onCommit
context.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设置

        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 提供一点帮助。

本博客所有文章除特别声明外,均采用CC 4.0许可协议。转载请注明出处和作者。

关注微信公共号肘子的Swift记事本或在推特上关注@fatbobman,永远不会错过新内容! 您的支持和鼓励将为我的博客写作增添更多的动力! 如果您或身边的朋友有健康数据管理的需求,请使用我开发的app【健康笔记】,正是因为它我才创建了这个博客。

关注