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.
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
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.
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
Togglewill need the value ofisEnabledproperty of theshellScriptRow, thegetmethod will be used. - To update the SwiftData model
ShellScriptRow, whenever users tap on the button, thesetmethod 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
2️⃣ Adding an ability to toggle visibility for actions in the Action Panel pic.twitter.com/i4k3y49wIt
— Quick Drop App (@UseQuickDrop) May 21, 2024
