Realm Objects implementing their own QuerySubscriptions Methods (MVI architecture)

Hi all,

I want to refactor an app according to some MVI principles whereby – as I understood from one of your video’s on this topic :raised_hands: – the business logic is moved away from the Views/ViewModels towards the Models themselves. To do so, I’ve been trying to extend my Realm models with some methods to handle their own subscriptions; I.e. instead of handling subscriptions in private functions of views like described in the docs / found in the RChat Example project, I want to extend my models with generic functions to handle subscription operations of their own Type.

However, I have not yet managed to successfully to do so. Does anyone have some suggestions on how to proceed?

Consider the following traditional example of handling the subscription logic within a private function of a view:

struct LoggedInView: View {
    @EnvironmentObject var state: AppState
    @Environment(\.realm) var realm
    @ObservedResults(User.self) var users
    
    var body: some View {
        // Grouped to allow for .task modifier below.
        Group {
            if let user = users.first {
                HomeView(user: user)
            } else {
                Text("waiting for User to be retrieved from Realm")
            }
        }
        .task {
            do {
                // Traditional Approach
                try await appendSubForUserFoo()
            } catch let error {
                state.error = error.localizedDescription
            }

        }
        
    }
    private func appendSubForUserFoo() async throws -> Void {
        let subscriptions = realm.subscriptions
        try await subscriptions.update {
            subscriptions.append(
                QuerySubscription<User>(name: "foo-user") {
                     $0._id == "foo"
            })
         
        }
    }

}

Instead, I am trying the following approach where I call a method of the User class to update the subscription for me:

extension User {
    static func appendSubscription<V>(to realm: Realm,
                                      where propertyGetter: (Self) -> V,
                                      equals value : V) async throws -> Void {

        let subscriptions = realm.subscriptions
        try await subscriptions.update {
            subscriptions.append(
                QuerySubscription<Self> { obj in
                     //  Err: Cannot convert value of type 'Query<Self>' to expected argument type 'Self'
                     propertyGetter(obj) == value
                  }
            )
        
        }
        
    }
}

struct LoggedInView: View {
    @EnvironmentObject var state: AppState
    @Environment(\.realm) var realm
    @ObservedResults(User.self) var users
    
    var body: some View {
        // Grouped to allow for .task modifier below.
        Group {
            if let user = users.first {
                HomeView(user: user)
            } else {
                Text("waiting for User to be retrieved from Realm")
            }
        }
        .task {
            do {
                // Append subscription to Realm named "foo-user", where _id of user should equal "Foo".
                try await User.appendSubscription(to: realm, where: \._id, equals: "Foo")
            } catch let error {
                state.error = error.localizedDescription
            }

        }
    }

}

In the line where I use the provided propertyGetter to check for equivalence in the query, I get the error:

Cannot convert value of type ‘Query’ to expected argument type ‘Self’

However, I cannot find a way to resolve this issue without effectively returning the the traditional approach of handling the subscription in the View instead of in the Model…

Any suggestions are much appreciated.
Thanks in advance!

  • Julius