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