[SwiftUI] Custom bindings in ForEach/List loops

May 22, 20244 min read#swift, #macos, #quickdrop

I have recently got an interesting challenge when developing my latest app Quick Drop. I’m writing this short blog post to explain how to use a custom binding in SwiftUI to invoke extra code to manipulate a domain model inside a ForEach/List loops

Problem

Quick Drop app is a shell script manager where users can create custom list of scripts to process files and folders. Those scripts will be displayed in the action panel where users can drop any file or folder onto the action button to trigger the scripts.

panel

The scripts are managed by SwiftData, representing by the type ShellScriptRow:

@Model
class ShellScriptRow: Identifiable, Hashable {
    @Attribute(.unique) var id: UUID = UUID()
    var shellScript: ShellScript
    var isEnabled: Bool = true

    init(shellScript: ShellScript) {
        self.shellScript = shellScript
    }
}

Currently, I have the following UI to add/edit/delete the scripts

before

This is the script to display the list of scripts on the UI:

List(shellScripts, id: \.self) { shellScript in
    HStack {
        Text(shellScript.shellScript.name)
        Spacer()
        Button("Edit", systemImage: "pencil") {
            editingShellScriptRow = shellScript
            name = shellScript.shellScript.name
            script = shellScript.shellScript.command
            showModal.toggle()
        }
        Button("Delete", systemImage: "xmark") {
            deletingShellScriptRow = shellScript
            showDeletingConfirmation.toggle()
        }
    }
}

Now, I need to add a toggle button to show or hide the action button corresponding to a script in the action panel. The new UI should have the Enable/Disable button additionally to the current Edit and Delete button in each row of the list of scripts.

after

Technically, whenever users tap on the Enable/Disable buttons, I will need to update isEnabled property of the corresponding ShellScriptRow.

The challenge here is that, I need to know which Toggle corresponding to which ShellScriptRow is being invoked, and also need to call modelContext.saveChanges() from SwiftData to persist the changes.

Solution

The solution for this challenge is to create a custon Binding for the isOn parameter of the Toggle inside the HStack

List(shellScripts, id: \.self) { shellScript in
    HStack {
        // old code ...
        Toggle(shellScript.isEnabled ? "Disable" : "Enable", isOn: bindingForShellScriptRow(shellScript))
            .toggleStyle(.button)
    }
}

and the bindingForShellScriptRow is defined as follow

private func bindingForShellScriptRow(_ shellScriptRow: ShellScriptRow) -> Binding<Bool> {
    Binding<Bool>(
        get: { shellScriptRow.isEnabled },
        set: { newValue in
            shellScriptRow.isEnabled = newValue
            try? modelContext.saveChanges() // Save changes or trigger a model update
        }
    )
}

What is happening here is that, we are using a custom Binding, with a custom initialiser to define the get and set methods for the Binding:

  • To display the UI, the Toggle will need the value of isEnabled property of the shellScriptRow, the get method will be used.
  • To update the SwiftData model ShellScriptRow, whenever users tap on the button, the set method will be used.

With this custom binding, I can now toggle the visibility of the script in the settings. And the changes will be persisted in SwiftData. In the code for the action panel, I can filter the scripts with isEnabled == true to display

Quick Drop logo

Profile picture

Personal blog by An Tran. I'm focusing on creating useful apps.
#Swift #Kotlin #Mobile #MachineLearning #Minimalist


© An Tran - 2024