本文中,我们将演示如何在 NativeScript 中使用 SwiftUI,共同探索构建精彩用户界面的新可能性。
前提条件
- macOS Catalina 或更高版本
- Xcode 11 或更高版本
- 运行 iOS 13 或更高版本的 iOS 设备/模拟器
SwiftUI 概念
现代 iOS 开发主要使用 Swift 编程语言。SwiftUI 采用了声明式的语法——你只需说明用户界面应当做什么。
我建议你先浏览一下官方的 SwiftUI 教程,以便熟悉基本概念。
创建 NativeScript 应用
我们可以使用标准的 TypeScript 模板来创建一个应用:
ns create swiftui --ts
cd swiftui
这会搭建一个通常被称为“原生风味”(vanilla)的 NativeScript 应用。换句话说,它提供了基本的数据绑定能力以及相当简单的设置。不过,我们在此介绍的内容适用于任何框架(Angular、React、Svelte、Vue 等)。
SwiftUI 插件
安装 SwiftUI 插件:
npm install @nativescript/swift-ui
注意: 你的最低 iOS 部署目标应至少为 13。
你可以在 App_Resources/iOS/build.xcconfig 文件中添加这一行:
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
使用 SwiftUI
A. 创建你的 SwiftUI 视图
创建 App_Resources/iOS/src/SampleView.swift:
import SwiftUI
struct SampleView: View {
var body: some View {
VStack {
Text("Hello World")
.padding()
}
}
}
B. 创建你的 SwiftUI 提供者 (Provider)
这将为你的 SwiftUI 准备好与 NativeScript 的双向数据绑定。
创建 App_Resources/iOS/src/SampleViewProvider.swift:
import SwiftUI
@objc
class SampleViewProvider: UIViewController, SwiftUIProvider {
// MARK: 初始化
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
required public init() {
super.init(nibName: nil, bundle: nil)
}
public override func viewDidLoad() {
super.viewDidLoad()
setupSwiftUIView(content: swiftUIView)
}
// MARK: 私有部分
private var swiftUIView = SampleView()
/// 从 NativeScript 接收数据
func updateData(data: NSDictionary) {
// 可以留空
}
/// 允许向 NativeScript 发送数据
var onEvent: ((NSDictionary) -> ())?
}
C. 插入到任意 NativeScript 布局中
app/main-page.xml
<Page
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:sw="@nativescript/swift-ui"
class="page"
>
<StackLayout>
<sw:SwiftUI swiftId="sampleView" height="100" />
</StackLayout>
</Page>
D. 通过 swiftId 注册你的 SwiftUI 视图
这一步可以在 NativeScript 应用的启动引导文件中完成(通常是 app.ts 或 main.ts)。
app.ts
import {
registerSwiftUI,
UIDataDriver
} from "@nativescript/swift-ui";
// A. 你可以使用 'ns typings ios' 为你自己的 Swift Provider 生成类型定义
// B. 否则,可以通过声明你知道已提供的类名来忽略此步骤
declare const SampleViewProvider: any;
registerSwiftUI("sampleView", (view) =>
new UIDataDriver(SampleViewProvider.alloc().init(), view)
);
现在你可以使用 ns debug ios 来运行应用了。
使用 Xcode 开发你的 SwiftUI
运行项目一次后,你就可以在 Xcode 中打开它,利用 Xcode 强大的智能感知辅助,进一步开发你的 SwiftUI。例如,在项目的根目录下执行:
open platforms/ios/swiftui.xcworkspace
你会发现你的 .swift 代码位于 TNSNativeSource 文件夹下,如下图所示
基础视图应用截图
高级 SwiftUI 集成
让我们深入一点,建立 SwiftUI 和 NativeScript 之间的数据绑定和事件交互。
创建 SwiftUI 组件
这可以是你想在 NativeScript 中使用的任何 SwiftUI 视图。
创建 App_Resources/iOS/src/SampleView.swift:
import SwiftUI
class ButtonProps: ObservableObject {
@Published var count: Int = 0
var incrementCount: (() -> Void)?
}
struct SampleView: View {
@ObservedObject var props = ButtonProps()
var body: some View {
VStack(alignment: .center, spacing: 0) {
HStack(alignment:.center) {
Text("Count $props.count)")
.padding()
.scaledToFill()
.minimumScaleFactor(0.5)
}
HStack(alignment: .center) {
Button(action: {
self.props.incrementCount?()
}) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.white)
.padding()
.background(LinearGradient(
gradient: Gradient(
colors: [Color.purple, Color.pink]), startPoint: .top, endPoint: .bottom
))
.clipShape(Circle())
}
}
}
.padding()
.clipShape(Circle())
}
}
创建 App_Resources/iOS/src/SampleViewProvider.swift:
import SwiftUI
@objc
class SampleViewProvider: UIViewController, SwiftUIProvider {
// MARK: 初始化
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
required public init() {
super.init(nibName: nil, bundle: nil)
}
public override func viewDidLoad() {
super.viewDidLoad()
setupSwiftUIView(content: swiftUIView)
registerObservers()
}
// MARK: 私有部分
private var swiftUIView = SampleView()
private func registerObservers() {
swiftUIView.props.incrementCount = {
let count = self.swiftUIView.props.count + 1
// 更新 swiftUI 视图
self.swiftUIView.props.count = count
// 通知 nativescript
self.onEvent?(["count": count])
}
}
// MARK: API
/// 从 NativeScript 接收数据
func updateData(data: NSDictionary) {
if let count = data.value(forKey: "count") as? Int {
// 更新 swiftUI 视图
swiftUIView.props.count = count
// 通知 nativescript
self.onEvent?(["count": count])
}
}
/// 向 NativeScript 发送数据
var onEvent: ((NSDictionary) -> Void)?
}
在 NativeScript 布局中使用你的 SwiftUI
app/main-page.xml:
<Page
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:sw="@nativescript/swift-ui"
navigatingTo="navigatingTo"
>
<StackLayout>
<sw:SwiftUI swiftId="sampleView" data="{{ nativeCount }}" swiftUIEvent="{{ onEvent }}" loaded="{{ loadedSwiftUI }}" />
<Label text="{{ 'NativeScript Label: ' + nativeCount.count }}" class="h2" />
<Button text="NativeScript 数据绑定: 减少" tap="{{ updateNativeScriptData }}" class="btn btn-primary" />
<Button text="SwiftUI 数据绑定: 减少" tap="{{ updateSwiftData }}" class="btn btn-primary" />
</StackLayout>
</Page>
app/main-page.ts:
import {
registerSwiftUI,
UIDataDriver,
SwiftUI,
SwiftUIEventData,
} from "@nativescript/swift-ui";
import {
EventData,
Observable,
Page
} from "@nativescript/core";
// A. 你可以使用 'ns typings ios' 为你自己的 Swift Provider 生成类型定义
// B. 否则,可以通过声明你知道已提供的类名来忽略此步骤
declare const SampleViewProvider: any;
registerSwiftUI("sampleView", (view) =>
new UIDataDriver(SampleViewProvider.alloc().init(), view)
);
interface CountData {
count: number;
}
export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new DemoModel();
}
export class DemoModel extends Observable {
swiftui: SwiftUI;
nativeCount = {
count: 0,
};
loadedSwiftUI(args) {
this.swiftui = args.object;
}
onEvent(evt: SwiftUIEventData<CountData>) {
this.set("nativeCount", { count: evt.data.count });
}
updateNativeScriptData() {
this.set('nativeCount', { count: this.nativeCount.count - 1 });
}
updateSwiftData() {
this.swiftui.updateData({ count: this.nativeCount.count - 1 });
}
}