Home > Software engineering >  @Published Realm object not triggering view redrawing in SwiftUI
@Published Realm object not triggering view redrawing in SwiftUI

Time:01-23

In our app we use a UserService that is a ObservableObject and passed as environment. A synced realm is opened and the app user (a RealmObject) is obtained using flexible sync.

When updating the users properties, such as his username, the view does not get redrawn. This is against my expectations since UserService contains a @Published property where the user (that is being edited) is stored. On the database it clearly shows the property being edited, however the view does not get redrawn, only when restarting the app the new properties are shown.

What would be the best way to have a UserService objects taking care of all user related logic (storing a user object (reference to it), containing functions to update, ...) and use this to display the active data of this user throughout the views?

Here is a MRE (the login logic is left out to reduce complexity):

import SwiftUI
import RealmSwift

class UserService2: ObservableObject {
    
    var realm: Realm
    @Published var ownUser: User

    var userNotificationToken: NotificationToken?
    
    init(realm: Realm, ownUser: User) {
        self.realm = realm
        self.ownUser = ownUser

        userNotificationToken = ownUser.observe { change in
            print(change) // just to see that the user object is actually live and being updated...
        }
    }
    
    func changeName(newName: String) {
        do {
            try self.realm.write {
                self.ownUser.userName = newName
            }
        } catch {
            print("error")
        }
    }
}
struct TestView: View {
    
    @EnvironmentObject var userService: UserService2
    
    var body: some View {
        VStack {
            Text(userService.ownUser.userName ?? "no name")
            Button {
                userService.changeName(newName: Date().description)
            } label: {
                Text("change name")
            }
        }
    }
}

struct ContentView: View {
    
    var realm: Realm? = nil
    
    init() {
        let flexSyncConfig = app.currentUser!.flexibleSyncConfiguration(initialSubscriptions: { subs in
            subs.append(
                QuerySubscription<User>(name: "ownUserQuery") {
                    $0._id == "123"
                })
        })
        do {
            let realm = try Realm(configuration: flexSyncConfig)
            self.realm = realm
        } catch {
            print("sth went wrong")
        }
    }
    
    var body: some View {
        if let realm = realm, let ownUser = realm.objects(User.self).where( { $0._id == "123" } ).first {
            TestView()
                .environmentObject(UserService2(realm: realm, ownUser: ownUser))
        } else {
            ProgressView()
        }
    }
}

The User Object looks like this

import Foundation
import RealmSwift

class User: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id = UUID().uuidString
    @Persisted var userName: String?
    
    convenience init(_id: String? = nil, userName: String? = nil) {
        self.init()
        if let _id = _id {
            self._id = _id
        }
        self.userName = userName
    }
}

P.S. I assume I could observe changes on the object using realm and somehow force a view refresh, but I would find it much more clean using the already existing way to watch for changes and redraw views when needed using @Published...

P.P.S. This user object is created on the server using a trigger when someone authenticates. However, I assume this is not really relevant to this problem.

CodePudding user response:

The issue here is the usage of a reference type as "Source of truth".

ObservableObject and SwiftUI Views use Combine Publishers to know when to refresh.

The @Published value sends the .objectWillChange publisher of the ObservableObject only when its wrapped value "changes". "changes" in this context means it gets replaced. So value types are preferred here, because if you change one of the properties the whole object will be replaced. This does not happen for reference types.

Multiple possible solutions here:

  • change the User class to a struct (Probably not wanted here, because this object implements Realm)
  • use the .objectWillChange.send() method yourself before altering the user
  • instead of altering the ownUservar replace it with a new one that contains the new information.

func changeName(newName: String) {
    do {
        self.objectWillChange.send() //add this
        try self.realm.write {
            self.ownUser.userName = newName
        }
    } catch {
        print("error")
    }
}
  • Related