2015 年,React Native 为跨平台移动开发带来了一种声明式组件方法。不久,这种面向组件的理念扩展到了类似的框架。现在,借助 SwiftUI 和 Jetpack Compose,这种声明式方法也可以在 native 平台上实现了。
SwiftUI 是一个 UI 框架,它将声明式组件方法引入到包括 iOS,macOS 和 watchOS 在内的 Apple 平台。
它可以与其先前的 UIKit 互换使用,这意味着开发人员可以将新框架无缝集成到其现有应用程序中。不过混合 UIKit 和 SwiftUI 需要中间件来桥接它们。
今天,我们将编写一个代理,允许开发人员在其 React Native 应用程序中使用 SwiftUI。
请记住,SwiftUI 仅在 iOS 13.0 及更高版本上可用(在撰写本文时,iOS 13 安装率达到了 94%)。如果您的应用程序必须支持以前的 iOS 版本,则不能使用 SwiftUI。
让我们首先创建一个带有 native UI 组件的 React Native 应用程序(目前,一个简单的 UIView 就可以了)。
如果您不熟悉如何将 native 组件桥接到 React Native,强烈建议您先阅读文档中的相应文章,然后再继续。初始设置的代码也可以在 GitHub[1] 上找到。
当我们有了一个初始设置的应用程序后,我们可以创建一个 Swift 类(SwiftUIButtonProxy),该类将充当 SwiftUI 视图和 RCTViewManager 之间的代理。
在 Objective-C 项目中创建第一个 Swift 类之后,Xcode 会询问您是否要它创建桥接头文件。它要求将任意的 Objective-C 代码公开给我们以后使用的 Swift,因此请务必选择 Yes。
新视图类似于以下:
// SwiftUIButtonProxy.swift
@objc class SwiftUIButtonProxy : UIView {}
现在,让我们来修改 RCTViewManager 以便渲染它:
// SwiftUIButtonManager.m
-(UIView *)view {
return [[SwiftUIButtonProxy alloc] init];
}
在这一步,您可能会注意到 Xcode 开始抱怨没有找到 SwiftUIButtonProxy。
发生这种情况的原因是,没有导入可访问 Swift 结构信息的头文件,Objective-C 无法识别任何 Swift 类:
#import "<ProjectName>-Swift.h"
请注意,<ProjectName>
只是您的项目名称的占位符。例如,如果您的项目名为 “FooBar”,则导入应如下所示:
#import "FooBar-Swift.h"
你可能会困惑并没有这个头文件。*.xcworkspace
或 *.xcodeproj
中没有相应的文件,但是它是存在的。Xcode 将在构建步骤中根据您的 Swift 代码库的可见性来构建它。
导入头文件后,错误消失了,并且应用程序能够正常构建。如果您在执行此步骤后遇到任何问题,可以在我的教程库
2中检出 “proxy-class” tag。
尽管渲染 Swift 的视图应该非常简单,但桥接 SwiftUI 却并非如此。由于设计限制,从 UIKit 运行 SwiftUI 的唯一方法是使用UIHostingController。
到目前为止,我还没有提到在应用程序中使用 UIViewController 或任何其他控制器。实际上,这通常会使学习 React Native 的人感到困惑。
您会看到,在 native 开发中,ViewController 在管理应用程序的视图和数据中起着重要作用。在 React Native 中,native 组件是由 JavaScript 来接管。
但是,如果我们构建一个复杂的 native 组件,或者桥接一个 SwiftUI 视图,那么学习如何使用它们可能会派上用场。
如前所述,可以使用 UIHostingController 将 SwiftUI 视图层次结构集成到现有的应用程序中。它充当 UIKit 和 SwiftUI 框架之间的中介者,因此可以将 SwiftUI 视图呈现为 UIKit 视图层次结构的一部分。
让我们创建一个新的 SwiftUI 视图,并将其命名为 SwiftUIButton。
// SwiftUIButton.swift
struct SwiftUIButton : View {
var body: some View {
Text("Hello, world!")
}
}
然后,我们调整 SwiftUIButtonProxy 类以使用新视图创建 UIHostingController。由于我们将创建一个视图控制器并公开其视图,因此无需让代理类继承自 UIView。
但是,由于我们无法将不继承自 NSObject 的类公开给 Objective-C,因此我们将 UIView 替换为 NSObject 以使其可访问。
// SwiftUIButtonProxy
@objcMembers class SwiftUIButtonProxy: NSObject {
private var vc = UIHostingController(rootView: SwiftUIButton())
var view: UIView {
return vc.view
}
}
相应的管理类也需要更改:
// SwiftUIButtonManager.m
- (UIView *)view {
SwiftUIButtonProxy *proxy = [[SwiftUIButtonProxy alloc] init];
return [proxy view];
}
如您所见,我们没有返回 SwiftUIButtonProxy 本身,而是将其替换为通过 UIHostingController 代理的 SwiftUI 视图。
恭喜你!您刚刚桥接了第一个 SwiftUI 视图。
大多数 native 组件都实现配置属性和回调。在本部分的教程中,我们将桥接两个简单的属性:count: Int
和 onCountChange: RCTBubblingEventBlock
。
这可能是本教程中最复杂的部分。数据转发并非易事。
首先,我们无法使用标准的 RCT_EXPORT_VIEW_PROPERTY 宏,因为它会生成尝试访问视图实例的设置器。不幸的是,SwiftUI 视图无法直接暴露给Objective-C,因为 Swift 的 struct 没有相应的类型。
这就把我们推到这样一个点,即数据传递的唯一方式是通过自定义可以访问两个运行时的 Swift 代理:Objective-C 从 JavaScript 接收视图属性,而 Swift 将这些属性设置给相应的 SwiftUI 视图。
听起来很复杂,但请不要担心!下面是它的工作原理:
native 组件的管理器通过特殊的设置器接收新属性,这些设置器由宏(如 RCT_EXPORT_VIEW_PROPERTY)在预编译例程期间生成。
每次使用上述宏时,都会为包裹的属性生成一个自定义设置器,因此,当您从 JavaScript 发送一组新的属性时,React Native 会检查在管理器中是否有与新属性对应的设置器。
类似于 RCT_EXPORT_VIEW_PROPERTY 的宏会生成属性设置器,当用户将新属性分配给 native 组件的 JavaScript 表达式时,将调用这些属性设置器。
由于我们无法使用 RCT_EXPORT_VIEW_PROPERTY,因此我建议创建自己的宏。按照 RCT_EXPORT_VIEW_PROPERTY 命名约定,我将其称为 RCT_EXPORT_SWIFTUI_PROPERTY 和 RCT_EXPORT_SWIFTUI_CALLBACK。
这些宏均利用 RCT_CUSTOM_VIEW_PROPERTY 的功能,该功能允许开发人员创建自定义设置器。
如果您决定使用它们,请记住它们仅在以下条件下起作用:
• 您有一个代理类,该类提供了称为 storage 的静态 NSMutableDictionary。
• RCT_EXPORT_SWIFTUI_PROPERTY 仅支持基本属性,例如 int,string,bool 等。复杂类型需要正确的转换,并且超出了本文的讨论范围
如前所述,每个自定义宏都依赖于 React Native 的 RCT_CUSTOM_VIEW_PROPERTY 宏,该宏允许开发人员为属性创建自定义设置器。该宏需要一个用于定义设置器的块。
在该块的范围内,React Native 提供了三个参数:
• UIView *view -— 属性的接收者。
• UIView *defaultView -— 默认视图。
• id json —- 指向接收到的数据的指针。
我们主要对 json 和 view 感兴趣。你可能会注意到 view 的类型是不同的。UIHostingController 暴露给桥接的是一个普通的 UIView,而不是 SwiftUI 视图。不幸的是,这还不够。
要设置新的属性,我们需要获得对实际 SwiftUI 视图的引用。此时唯一的方法是通过 UIHostingController,但控制器本身却无法做到……
为了解决这个问题,我创建了一个 key:value 存储(NSMutableDicrionary)并将其作为静态属性分配给 SwiftUIButtonProxy。
现在,如果在创建视图时存储了一个 view:proxy 对,我将可以访问 setter 中的所需的 proxy。由于我们的代理是 UIHostingController 的容器,因此无需费劲就能获得对 SwiftUI 视图的正确引用。
但是,请不要忘记我们是在 Objective-C 进行操作的,这使得无法直接操作 SwiftUI 视图属性。
如果我们在 SwiftUIButtonProxy 类中定义了自定义 count 和 onCountChange 属性,则可以解决此限制,以便它们使用控制器的 rootView 属性传播新值:
// SwiftUIButtonProxy.swift
var count: Int {
set { vc.rootView.props.count = newValue }
get { return vc.rootView.props.count }
}
var onCountChange: RCTBubblingEventBlock {
set { vc.rootView.props.onCountChange = newValue }
get { return vc.rootView.props.onCountChange }
}
手动为每个属性编写 getter 和 setter 有点麻烦(特别是如果您有几个属性的话),但这是我发现的最简单的方法。
另外,请注意此处的 props:SwiftUI 组件有多种状态,包括本地状态 @State,对外暴露状态 @ObservedObject (props) 和共享状态@EnvironmentObject (context)。
在此示例中,我使用 @ObservedObject,因为我们需要从外部设置值。在 SwiftUI 视图中,它看起来像这样:
// SwiftUIButton.swift
@ObservedObject var props = ButtonProps()
现在,让我们向视图添加一个按钮。每次用户单击该按钮时,我们要使用新的递增计数器值调用 props.onCountChange。经过一些样式上的更改,SwiftUIButton 代码可能类似于以下:
struct SwiftUIButton : View {
@ObservedObject var props = ButtonProps()
var body: some View {
VStack {
Text("Count \(props.count)")
.padding()
Button(action: {
self.props.onCountChange(["count": self.props.count + 1])
}) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.white)
.padding()
.background(Color.red)
.clipShape(Circle())
}
}
.padding()
.clipShape(Circle())
}
}
现在,为了结束本示例,我们只需要补充两个缺少的部分:ButtonProps 和 JavaScript 部分:
class ButtonProps : ObservableObject {
@Published var count: Int = 0
@Published var onCountChange: RCTDirectEventBlock = { _ in }
}
ButtonProps 是一个 ObservableObject,它保存我们从 JavaScript 传递过来的值。如果您对 @Published 感到困惑,那很好。该指令创建一个绑定,该绑定告诉 SwiftUI 如果已发布的属性之一已更改,则重新渲染。
JavaScript 部分的代码如下:
import React, {useState} from 'react';
import {requireNativeComponent} from 'react-native';
const SwiftUIButton = requireNativeComponent('SwiftUIButton');
const App = () => {
const [count, updateCount] = useState(0);
return (
<SwiftUIButton
style={styles.container}
count={count}
onCountChange={e => updateCount(e.nativeEvent.count)}
/>
);
};
就是这些!这是一条漫长坎坷的路,但您已成功完成。
通过一些小的调整,例如在 SwiftUI 视图中添加一个按钮并使用回调函数连接相应的数据,您就会拥有一个从 0 到 正无穷大计数的应用程序。
最终的代码(带有一些本机UI调整)可以在教程库[3]中的 work-example 标记下找到。
我们只是将一个 SwiftUI 组件桥接到一个 React Native 应用程序中 -- 尽管这开辟了巨大的可能性,但这个主题依然具有挑战性。
如果您需要时间考虑一下,那就很好。我花了几个不眠之夜想出一种方法,而且坦率地说,我不确定这是否是最好的方法。
解决方案的整体复杂性使我寄希望于将来在发布新的 React Native 架构时这些事情会变得更加容易。
[1]https://github.com/Kureev/ReactNativeWithSwiftUITutorial/releases/tag/initial-setup [2]https://github.com/Kureev/ReactNativeWithSwiftUITutorial [3]https://github.com/Kureev/ReactNativeWithSwiftUITutorial
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8