5

在 SwiftUI 中使用 UIKit 视图

 1 year ago
source link: https://www.fatbobman.com/posts/uikitInSwiftUI/
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.
neoserver,ios ssh client

在 SwiftUI 中使用 UIKit 视图

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

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

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

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

uikitInSwiftUI
健康笔记 - 全家人的健康助手

健康笔记适用于任何有健康管理需求的人士。提供了强大的自定义数据类型功能,可以记录生活中绝大多数的健康项目数据。你可以为每个家庭成员创建各自的记录笔记,或者针对某个特定项目、特定时期创建对应的笔记。

在具体演示包装代码之前,我们先介绍一些与在 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
uikitInSwiftUI
健康笔记 - 全家人的健康助手

健康笔记适用于任何有健康管理需求的人士。提供了强大的自定义数据类型功能,可以记录生活中绝大多数的健康项目数据。你可以为每个家庭成员创建各自的记录笔记,或者针对某个特定项目、特定时期创建对应的笔记。

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就只能放置在这个节点之前。

我们也可以自己创建环境值来实现对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 提供一点帮助。同时也欢迎你通过 TwitterDiscord 频道或下方的留言板与我进行交流。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK