Back to Blog Home
← all posts

Create a checkbox for iOS with NativeScript and SwiftUI

January 7, 2024 — by Nandee Tjihero

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.

Create a SwiftUI checkbox project in Xcode

To create a checkbox view in Xcode, follow these steps:

  1. Launch Xcode.
  2. On the welcome screen of Xcode, click Create a new Xcode project.
  3. Select App as the project type and click Next.
  4. Type Checkbox as the product name. Select SwiftUI as the user interface. Select Swift as the language. Click Next.
  5. Select a location to save the project and click Create.

Create the data model

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

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
}

Declare a property to hold the tap callback

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.

Add the checkbox view

To create the SwiftUI checkbox view, follow these steps:

  • In the Checkbox view block of the 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!")
    }
}
  • In the body of the checkbox struct, replace the default 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)
        })
    }
}
  • The 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)
        })
    }
}
  • To set the fill and stroke colors of the checkbox button, we apply the .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)
        })
    }
}

Use the SwiftUI checkbox in NativeScript

To use the checkbox in NativeScript, follow these steps:

Install the @nativescript/swift-ui plugin.

To use SwiftUI views in NativeScript, we can use @nativescript/swift-ui.

npm i @nativescript/swift-ui

Register our SwiftUI checkbox view with NativeScript.

  • We can put our SwiftUI created above into 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.
  • We can now create a 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) {

    } 
}
  • Instantiate the 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) {

    } 
}
  • We can now register a callback function for when the checkbox is tapped. This function will send the 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) {

    } 
}
  • In the bootstrap file (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.

  • Use your SwiftUI checkbox in a NativeScript view

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>
  • The 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
    }

ListView item recycling with SwiftUI?

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.

Solution to ListView item recycling

To solve the issue, we can take the following steps:

  • Add an id property to the CheckboxData class to identify the checkbox.
public class CheckboxData: ObservableObject {
    @Published var id: Int = 0
    @Published var checked: Bool = false
    ...
}
  • Then set the 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
    }
}
  • Enable checkbox state to be driven by ListView data by setting its checked value:
func updateData(data: NSDictionary) {
    if let checkedValue = data.value(forKey: "checked") as? Bool {
        checkbox.checkboxProps.checked = checkedValue
    }
}
  • Now when the checkbox is tapped, we can update the ListView data source with the new value from the ObservableObject instance:
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.

Credits