返回博客首页
← 所有文章

使用 NativeScript 和 SwiftUI 创建 iOS 复选框

2024年1月7日 — 作者:Nandee Tjihero

我们可以从一个 NativeScript 项目开始,或者在 Xcode 中创建一个 SwiftUI 文件。因为我们随时可以选择,所以我们首先使用 Xcode 来演示 NativeScript 如何使技能变得可互换(以及受到赞赏)。

以下方法适用于您喜欢的任何 JavaScript 框架(Angular、React、Solid、Svelte、Vue 等)。这里我们将只使用纯 TypeScript(无框架)。

您可以参考这个 GitHub 仓库获取完整示例

在 Xcode 中创建一个 SwiftUI 复选框项目

要在 Xcode 中创建复选框视图,请按照以下步骤操作

  1. 启动 Xcode。
  2. 在 Xcode 的欢迎界面上,点击**“Create a new Xcode project”**。
  3. 选择**“App”**作为项目类型,然后点击**“Next”**。
  4. 输入**“Checkbox”**作为产品名称。选择**“SwiftUI”**作为用户界面。选择**“Swift”**作为语言。点击**“Next”**。
  5. 选择一个位置保存项目,然后点击**“Create”**。

创建数据模型

对于复选框视图,我们需要一个数据模型来包含复选框的状态。状态是一个boolean值,指示复选框是否被选中。

在 Xcode 为我们创建的默认 .swift 类中,我们可以创建一个名为CheckboxData的数据模型类来保存复选框状态和按钮点击回调方法。为了通知视图数据模型中的更改,我们使数据模型类符合ObservableObject协议。

class CheckboxData: ObservableObject {
}

添加一个 checked 属性来保存复选框的状态

添加一个 checked 属性来保存复选框的状态。checked属性是一个boolean值,指示复选框是否被选中。

class CheckboxData: ObservableObject {
    var checked: Bool = false
}

为了将checked属性中的更改发布到视图,我们需要将checked属性包装在@Published属性包装器中。

class CheckboxData: ObservableObject {
    @Published var checked: Bool = false
}

声明一个属性来保存点击回调

我们需要声明一个属性来保存点击回调,当按钮被点击时,该回调会切换checked属性。我们将属性声明为一个函数类型,该函数不接受参数也不返回值。

class CheckboxData: ObservableObject {
    @Published var checked: Bool = false
    var toggleChecked: (() -> Void)?
}

我们需要将该属性声明为可选(?)函数类型,因为在声明时我们没有点击回调。稍后,当我们创建复选框视图时,我们将定义并分配回调。我们将使用点击回调将checked属性值发送到 NativeScript。

添加复选框视图

要创建 SwiftUI 复选框视图,请按照以下步骤操作

  • struct的复选框视图块中,在body属性之前,声明一个属性(在本例中为data)来保存数据模型的实例。
struct Checkbox: View {
    var data: CheckboxData

    var body: some View {
        Text("Hello, World!")
    }
}

为了使复选框视图在数据模型发生更改时接收更改,我们将视图data属性包装在@ObservedObject属性包装器中。

struct Checkbox: View {
    @ObservedObject var data: CheckboxData

    var body: some View {
        Text("Hello, World!")
    }
}
  • 在复选框结构体的正文中,用Button视图替换默认的Text视图。将按钮的action设置为toggleChecked方法的调用。对于按钮的标签,我们使用Label视图,并将systemImage属性设置为复选标记图标。systemImage属性接受要显示的系统图标的名称。当复选框被选中时,我们将图标设置为"checkmark.circle.fill"来自 Apple 的内置系统字体,未选中时设置为"circle"
struct Checkbox: View {
    @ObservedObject var data: CheckboxData

    var body: some View {
        Button(action: {
            self.data.toggleChecked?()
        } label: {
            Label("",systemImage: self.data.checked ? "checkmark.circle.fill" : "circle")
                .labelStyle(.iconOnly)
                .foregroundColor(self.data.checked ? .yellow : .yellow)
        })
    }
}
  • Label视图是一个容器视图,显示文本和图标。对于复选框,我们只需要图标。要仅显示图标,我们应用labelStyle修饰符,其值为.iconOnly
struct Checkbox: View {
    @ObservedObject var data: CheckboxData

    var body: some View {
        Button(action: {
            self.data.toggleChecked?()
        } label: {
            Label("",systemImage: self.data.checked ? "checkmark.circle.fill" : "circle")
                .labelStyle(.iconOnly)
        })
    }
}
  • 要设置复选框按钮的填充和描边颜色,我们应用.foregroundColor修饰符并使用所需的颜色
struct Checkbox: View {
    @ObservedObject var data: CheckboxData

    var body: some View {
        Button(action: {
            self.data.toggleChecked?()
        } label: {
            Label("",systemImage: self.data.checked ? "checkmark.circle.fill" : "circle")
                .labelStyle(.iconOnly)
                .foregroundColor(self.data.checked ? .yellow : .yellow)
        })
    }
}

在 NativeScript 中使用 SwiftUI 复选框

要在 NativeScript 中使用复选框,请按照以下步骤操作

安装 @nativescript/swift-ui 插件。

要在 NativeScript 中使用 SwiftUI 视图,我们可以使用@nativescript/swift-ui

npm i @nativescript/swift-ui

在 NativeScript 中注册我们的 SwiftUI 复选框视图。

  • 我们可以将上面创建的 SwiftUI 放入App_Resources/iOS/Checkbox.swift中。我们甚至可以在项目运行后根据需要继续使用 Xcode 编辑该文件,方法是打开platforms/ios/{project}.xcodeproj,在NSNativeSource文件夹中查看该文件。
  • 我们现在可以创建一个App_Resources/iOS/CheckboxViewProvider.swift文件,该文件具有以下结构,声明用于保存 Checkbox 实例的属性
@objc
class CheckboxProvider: UIViewController, SwiftUIProvider {
    var checkbox: Checkbox!
    // Allow sending data to NativeScript
    var onEvent: ((NSDictionary) -> ())?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    required public init() {
        super.init(nibName: nil, bundle: nil)
    }

    public override func viewDidLoad() {
        super.viewDidLoad()  
    }

    // (Optionally) Receive data from NativeScript
    func updateData(data: NSDictionary) {

    } 
}
  • CheckboxData实例化为一个属性,以便在checked属性更改时可以更新它。然后,我们在viewDidLoad中将其提供给Checkbox视图,并使用它调用setupSwiftUIView
@objc
class CheckboxProvider: UIViewController, SwiftUIProvider {
    var checkboxData = CheckboxData()
    var checkbox: Checkbox!

    // Allow sending data to NativeScript
    var onEvent: ((NSDictionary) -> ())?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    required public init() {
        super.init(nibName: nil, bundle: nil)
    }

    public override func viewDidLoad() {
        super.viewDidLoad()  
        checkbox = Checkbox(checkboxProps: checkboxData)
        setupSwiftUIView(content: checkbox) 
    }

    // (Optionally) Receive data from NativeScript
    func updateData(data: NSDictionary) {

    } 
}
  • 我们现在可以注册一个回调函数,用于在点击复选框时触发。此函数会将checked属性值发送到 NativeScript。
@objc
class CheckboxProvider: UIViewController, SwiftUIProvider {
    var checkboxData = CheckboxData()
    var checkbox: Checkbox!

    // Allow sending data to NativeScript
    var onEvent: ((NSDictionary) -> ())?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    required public init() {
        super.init(nibName: nil, bundle: nil)
    }

    public override func viewDidLoad() {
        super.viewDidLoad()  
        checkbox = Checkbox(checkboxProps: checkboxData)
        setupSwiftUIView(content: checkbox) 
        registerObservers()
    }

    private func registerObservers() {
        self.checkbox.checkboxProps.changeChecked = { 
            // notify NativeScript
            self.onEvent?(["checked": self.checkbox.checkboxProps.checked])
        }
    }

    // (Optionally) Receive data from NativeScript
    func updateData(data: NSDictionary) {

    } 
}
  • 在引导文件(app.tsmain.ts)中,使用 NativeScript 注册复选框视图
// you can optionally run `ns typings ios` to include types if desired here
declare const CheckboxProvider: any;

import { 
    registerSwiftUI, 
    UIDataDriver
} from "@nativescript/swift-ui";

registerSwiftUI("checkbox", (view) => new UIDataDriver(CheckboxProvider.alloc().init(), view)
);

Application.run({ moduleName: 'app-root' })

这将使用swiftId = checkbox注册一个新的视图元素/组件,您现在可以在布局中的任何位置使用它。

  • 在 NativeScript 视图中使用您的 SwiftUI 复选框

要将复选框视图添加到 NativeScript 标记中,请添加SwiftUI组件并设置其swiftId属性。

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:ui="nativescript-swiftui">

    <ui:SwiftUI swiftId="checkbox" />
</Page>

注意:每个 JavaScript 框架都有自己的视图标记语法,但是您可以在任何您喜欢的框架中使用相同的原理。

让我们在 ListView 中使用复选框视图来选择将开始比赛的球员。用以下代码替换<ui:SwiftUI swiftId="checkbox" />

<ListView items="{{ items }}">
    <ListView.itemTemplate>
        <GridLayout columns="auto,*">
            <ui:SwiftUI swiftId="checkbox"
                id="{{ id }}"
                data="{{ nativeCheckboxData }}"
                swiftUIEvent="{{ onEvent }}" 
                width="50" height="50" 
                loaded="loadedSwiftUI"/>
            <Label col="1" text="{{ player }}" class="t-18 m-l-10" />
        </GridLayout>
    </ListView.itemTemplate>
</ListView>
  • data属性保存发送到 SwiftUI 的 JS 数据。
export class HelloWorldModel extends Observable {
    nativeCheckboxData: ICheckboxNativeData = {
        checkboxOutlineType: "circle",
        color: new Color("#77588C"),
        buttonType: "checkmark",
        checked: false
    };
}
  • swiftUIEventSwiftUI向 NativeScript 发送数据时触发。
import { Observable } from '@nativescript/core';

export class HelloWorldModel extends Observable {

    onEvent(evt: SwiftUIEventData<CheckboxData>) {
        const view = evt.object as View;
        const viewId = view.id;
        // handle our data
    }

ListView 项目循环使用 SwiftUI?

Android 和 iOS 都通过行循环优化列表控件。这意味着当用户滚动时,要在屏幕上添加更多行,列表会重复使用已创建的行,以避免为长列表创建多余的视图,从而保持移动设备上的内存效率。这意味着当用户滚动时,SwiftUI组件会被重复使用。要查看其工作原理,请选择第一个和第二个复选框并向下滚动。您将看到有一个您未选择的复选框被选中。这是因为第一个SwiftUI组件被重复使用。再次向上和向下滚动,您将看到您未选择的复选框被选中。

问题发生的原因是,复选框对 ObservableObject 实例所做的更改未传达给驱动列表视图行的的数据源。因此,列表视图项会使用旧数据进行循环使用。

ListView 项目循环使用的解决方案

要解决此问题,我们可以采取以下步骤

  • 在 CheckboxData 类中添加一个id属性来标识复选框。
public class CheckboxData: ObservableObject {
    @Published var id: Int = 0
    @Published var checked: Bool = false
    ...
}
  • 然后设置id以在我们的CheckboxProvider中将 ListView 数据链接到复选框
func updateData(data: NSDictionary) {
    if let itemId = data.value(forKey: "id") as? Int {
        checkbox.checkboxProps.id = itemId
    }
}
  • 通过设置其checked值,启用由 ListView 数据驱动的复选框状态
func updateData(data: NSDictionary) {
    if let checkedValue = data.value(forKey: "checked") as? Bool {
        checkbox.checkboxProps.checked = checkedValue
    }
}
  • 现在,当点击复选框时,我们可以使用 ObservableObject 实例中的新值更新 ListView 数据源
onEvent(evt: SwiftUIEventData<ICheckboxNativeData>) {
    const viewId = evt.data.id;
    const rowItem = this.items.find((item) => item.id === viewId);

    rowItem.nativeCheckboxData.checked = evt.data.checked;
  }

到目前为止,只有您选择的复选框才会被选中。非常棒。

鸣谢