I have a toggle UI element in a List, and I need to execute code when the toggle state changes. As I understand it, that's accomplished by using a binding on the toggle, and then adding a ".onChange:of:" on the binding variable.
Done:
//
import Foundation
import SwiftUI
struct User : Hashable, Identifiable {
var isToggled = false
var id: String
var firstName: String?
var lastName: String?
var loggedIn: Bool = false
var loggedInSince: String?
var isActive: Bool?
var isAdmin: Bool?
}
struct ListRow: View {
@EnvironmentObject var userListModel: UserListModel
@Binding var user: User
var body: some View {
Toggle(isOn: $user.loggedIn) {
VStack {
Text("\(user.firstName!) \(user.lastName!)")
if user.loggedIn {
Text("Logged in since \(user.loggedInSince!)")
.font(Font.system(size: 10))
}
else {
Text("Logged out since \(user.loggedInSince!)")
.font(Font.system(size: 10))
}
}
}
.disabled(self.cantLogInOut())
.onChange(of: user.loggedIn) { value in // THIS ISN'T WORKING, IT'S NOT GETTING CALLED
// action...
print(value)
userListModel.changeLogStatus(user: user)
}
}
So, the code in the onChange ("print", "userListModel.changeLogStatus") is never called.
I'm getting this in the console:
2022-02-12 22:56:39.860044-0500 TimeCard[10104:4072116] invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.
I have no idea what that means, googling it isn't helpful, and when I put a Symbolic breakpoint on that error, nothing useful is shown in the debugger (it shows a stack trace that is two assembly code segments out of main.)
Here is some code that you can use to "...execute code when the toggle state changes."
Since you are using userListModel.changeLogStatus(user: user)
to record the change in loggedIn
,
there is no need to have a @Binding var user: User
use @State var user: User
instead.
class UserListModel: ObservableObject {
@Published var users = [User(isToggled: false, id: "1", firstName: "firstName-1", lastName: "lastName-1", loggedIn: false, loggedInSince: "yesterday", isActive: false, isAdmin: false),
User(isToggled: false, id: "2", firstName: "firstName-2", lastName: "lastName-2", loggedIn: false, loggedInSince: "yesterday", isActive: false, isAdmin: false),
User(isToggled: false, id: "3", firstName: "firstName-3", lastName: "lastName-3", loggedIn: false, loggedInSince: "yesterday", isActive: false, isAdmin: false)
]
func changeLogStatus(user: User) {
if let ndx = users.firstIndex(of: user) {
users[ndx].loggedIn = user.loggedIn
}
}
}
struct User: Hashable, Identifiable {
var isToggled = false
var id: String
var firstName: String?
var lastName: String?
var loggedIn: Bool = false
var loggedInSince: String?
var isActive: Bool?
var isAdmin: Bool?
}
struct ListRow: View {
@EnvironmentObject var userListModel: UserListModel
@State var user: User // <-- use @State
var body: some View {
Toggle(isOn: $user.loggedIn) {
VStack {
Text("\(user.firstName!) \(user.lastName!)")
if user.loggedIn {
Text("Logged in since \(user.loggedInSince!)").font(Font.system(size: 10))
}
else {
Text("Logged in since \(user.loggedInSince!)").font(Font.system(size: 10))
}
}
}
// .disabled(self.cantLogInOut())
.onChange(of: user.loggedIn) { value in // <-- THIS IS WORKING
print("in onChange value: \(value)")
userListModel.changeLogStatus(user: user)
}
}
}
struct ContentView: View {
@StateObject var model = UserListModel()
var body: some View {
List(model.users) { user in
ListRow(user: user)
}.environmentObject(model)
}
}
You can also do the opposite, using only a binding
, without the EnvironmentObject
UserListModel
, as in this code example:
struct ListRow: View {
@Binding var user: User // <-- use @Binding, no need for userListModel here
var body: some View {
Toggle(isOn: $user.loggedIn) {
VStack {
Text("\(user.firstName!) \(user.lastName!)")
if user.loggedIn {
Text("Logged in since \(user.loggedInSince!)").font(Font.system(size: 10))
}
else {
Text("Logged in since \(user.loggedInSince!)").font(Font.system(size: 10))
}
}
}
//.disabled(self.cantLogInOut())
.onChange(of: user.loggedIn) { value in // <-- THIS IS WORKING
print("in onChange value: \(value)")
}
}
}
struct ContentView: View {
@StateObject var model = UserListModel()
var body: some View {
List($model.users) { $user in // <-- note the $
ListRow(user: $user)
}
}
}