We could start from a NativeScript project or in Xcode to create a SwiftUI file. Because we have the choice at anytime, we'll use Xcode first to demonstrate how skills become interchangeable (and celebrated) with NativeScript.
The following works with any JavaScript flavor you prefer (Angular, React, Solid, Svelte, Vue, etc). We'll use just plain TypeScript (no flavor) here.
You can refer to this GitHub repo for a full example.
To create a checkbox view in Xcode, follow these steps:
For the Checkbox view, we need a data model that contains the state of the checkbox. The state is a boolean
value that indicates whether the checkbox is checked or not.
In the default .swift class Xcode created for us, we can create a data model class named CheckboxData
to hold the checkbox state and the button tap callback method. To notify the view of changes in the data model, we make the data model class conform to the ObservableObject protocol.
class CheckboxData: ObservableObject {
}
Add a checked property to contain the checkbox state. The checked
property is a boolean
value that indicates whether the checkbox is checked or not.
class CheckboxData: ObservableObject {
var checked: Bool = false
}
To publish the changes in the checked
property to the view, we need to wrap the checked
property in the @Published
property wrapper.
class CheckboxData: ObservableObject {
@Published var checked: Bool = false
}
We need to declare a property to hold the tap callback that toggles the checked
property when the button is tapped. We declare the property as a function type that accepts no parameters and returns no value.
class CheckboxData: ObservableObject {
@Published var checked: Bool = false
var toggleChecked: (() -> Void)?
}
We need to declare the property as an optional(?
) function type since we don't have a tap callback at the time of the declaration. We will define and assign the callback later when we create the checkbox view. We will use the tap callback to send the checked
property value to NativeScript.
To create the SwiftUI checkbox view, follow these steps:
struct
, before the body
property, declare a property(data
in this case) to hold an instance of the data model.struct Checkbox: View {
var data: CheckboxData
var body: some View {
Text("Hello, World!")
}
}
For the checkbox view to receive changes in the data model when they occur, we wrap the view data
property in the @ObservedObject
property wrapper.
struct Checkbox: View {
@ObservedObject var data: CheckboxData
var body: some View {
Text("Hello, World!")
}
}
Text
view with a Button view. Set the button's action
to the toggleChecked
method's call. For the button's label, we use the Label
view with the systemImage
property set to the checkmark icon. The systemImage
property accepts the name of the system icon to display. We set the icon to "checkmark.circle.fill"
, from Apple's built-in system font, when the checkbox is checked and to "circle"
when it's not checked.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
view is a container view that displays a text and an icon. For the checkbox, we only need the icon. To show just the icon, we apply the labelStyle
modifier with the value .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
modifier with the desired color: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)
})
}
}
To use the checkbox in NativeScript, follow these steps:
To use SwiftUI views in NativeScript, we can use @nativescript/swift-ui.
npm i @nativescript/swift-ui
App_Resources/iOS/Checkbox.swift
. We can even continue to use Xcode to edit the file as needed later after our project is run by opening platforms/ios/{project}.xcodeproj
to see the file in NSNativeSource
folder.App_Resources/iOS/CheckboxViewProvider.swift
file with the following structure declaring the property to hold the Checkbox instance:@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
as a property so it can be updated when the checked
property changes. We then provide it to the Checkbox
view in viewDidLoad
and call setupSwiftUIView
with it.@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
property value to 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.ts
or main.ts
), register the checkbox view with 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' })
This registers a new view element/component with swiftId
= checkbox
which you can now use anywhere in your layout.
To add the checkbox view to your NativeScript markup, add the SwiftUI
component and set its swiftId
attribute.
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:ui="nativescript-swiftui">
<ui:SwiftUI swiftId="checkbox" />
</Page>
Note: Each JavaScript flavor has it's own view markup syntax however you can use the same principle across any you prefer.
Let's use the checkbox view in a ListView to select the players who will start the match. Replace <ui:SwiftUI swiftId="checkbox" />
with the following code.
<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
property holds the JS data that gets sent to SwiftUI.export class HelloWorldModel extends Observable {
nativeCheckboxData: ICheckboxNativeData = {
checkboxOutlineType: "circle",
color: new Color("#77588C"),
buttonType: "checkmark",
checked: false
};
}
swiftUIEvent
is triggered when SwiftUI
sends data to 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
}
Android and iOS both optimize list controls with row recycling. Meaning to add more rows on the screen when the user scrolls, the list reuses rows already created to avoid creating extraneous views for long lists to keep memory efficient on mobile devices. This means the SwiftUI
component is reused when the user scrolls. To see this in action, select the first and second checkbox and scroll down. You will see there is a checkbox selected that you did not select. This is because the first SwiftUI
component is reused. Scroll back up and down again and you will see checkboxes selected that you did not select.
The issue occurs because the changes made by the checkbox to the ObservableObject instance are not communicated to the data source that drives the list view rows. Consequently, the list view items are recycled with the old data.
To solve the issue, we can take the following steps:
id
property to the CheckboxData class to identify the checkbox.public class CheckboxData: ObservableObject {
@Published var id: Int = 0
@Published var checked: Bool = false
...
}
id
to link the ListView data to the checkbox in our CheckboxProvider
:func updateData(data: NSDictionary) {
if let itemId = data.value(forKey: "id") as? Int {
checkbox.checkboxProps.id = itemId
}
}
checked
value:func updateData(data: NSDictionary) {
if let checkedValue = data.value(forKey: "checked") as? Bool {
checkbox.checkboxProps.checked = checkedValue
}
}
onEvent(evt: SwiftUIEventData<ICheckboxNativeData>) {
const viewId = evt.data.id;
const rowItem = this.items.find((item) => item.id === viewId);
rowItem.nativeCheckboxData.checked = evt.data.checked;
}
By now, only those checkboxes that you selected should be selected. Pretty neat.