mozilla / uniffi-rs

a multi-language bindings generator for rust
https://mozilla.github.io/uniffi-rs/
Mozilla Public License 2.0
2.48k stars 211 forks source link

Question: Convention for using UniFFI with SwiftUI (Update a SwiftUI view when a UniFFI interface object changes). #2145

Closed GraniteLake closed 3 weeks ago

GraniteLake commented 3 weeks ago

SwiftUI uses property wrappers such as @StateObject and @ObservedObject on an ObservableObject (a type of object with a publisher that emits before the object has changed) parameter of a SwiftUI view to update the view whenever the parameter's published properties change (see https://developer.apple.com/documentation/Combine/ObservableObject).

UniFFi issue #2097 discussed that UniFFi does not currently support swift property wrappers but the creator of the issue indicated that there is a convention for using UniFFI with SwiftUI and I was wondering which convention is used to have a SwiftUI view update when a UniFFI interface object changes?

cadnza commented 3 weeks ago

Answered here, to be taken with a huge grain of salt:

@cadnza What is the convention for writing SwiftUI code with UniFFI with regard to having a view update when a field in a UniFFi Interface / rust struct changes?

@GraniteLake As far as I understand—first pointing out how wide and varied my lack of experience is with this—the way it's usually done is by writing an intermediary stuct/class in Swift that wraps UniFFI's class and adds SwiftUI's property wrappers and macro annotations. As far as how you get UniFFI to update the struct based on those wrappers and annotations, I'm not sure, but something along those lines is what I've been seeing going through other peoples' code.

mhammond commented 3 weeks ago

I think @Sajjon has some interest/knowledge here?

Sajjon commented 3 weeks ago

Hey! There are several dimension to using UniFFI and SwiftUI; let me start by trying to reduce the scope a bit...

What is your minimum deployment target? If you can target iOS 17, you can simply write one wrapping model per screen, lets call it ViewModel, which is a class and annotate it [with @Observable macro ](https://developer.apple.com/documentation/observation/observable()). Which does not require its stored properties to have been marked with anything IIRC. So you can use UniFFI exported classes (or enums / srructs) as stored properties in this ViewModel.

This "solves" your issue completely without any changes needed to UniFFI.

If you need to target lower than iOS 17, you can employ the same technique with PointFrees Perception framework.

It might be interesting to explore possibilities to in Rust with UniFFI mark a uniffi::Object to be marking the Swift class with @Observable, but it being iOS 17+ we would need to wrap it with conditional compilation. We would also need to mark the generated swift file with #canImport(Observation) because it is not available for Linux.... so there are several missing pieces and challenges in doing so.

EDIT [clarification]: Even if we (conditionally for iOS/macOS etc) could I don't think we should "blanket mark" all classes with @Observable, it generates quite a bit of code, and for large classes in performance critical code one might not want said classes to grow even more. So best if we could make it possible - a new syntax - for UniFFI devs to tell UniFFI to mark specific classes with it.

Foremost we don't have any UDL spelling nor proc-macro for binding language specific settings for certain types. It would be very nice to have though! E.g. being able to express mutable/immutable stored properies on certain Swift types, and not catch em all, like we have today. Maybe @mhammond or @bendk can share some thoughts about that specifically...

If you are not using/required to use "vanilla SwiftUI" - whatever that means, since there is no clear standard; why I'm a huge fan of opinionated TCA by PointFree too - finally a standard! But I'm digressing... I was gonna say that I've recently managed to POC with TCA and SharedState with UniFFI.. Big PR... TL;DR; my app is completely driven by UniFFI, an object (swift class) for which I have a singleton, lets call it App. Hosts (iOS TCA+SwiftUI apps / Android Kotlin app) implement "Drivers" which are UniFFI traits which App uses to make use of ""peripherals"" on the host: networking (URLSession), file IO, unsafe storage (UserDefaults), secure storage (keychain), and "EventBus" (AsyncPassthroughSubject) with which App can emit "notifications" Rust side, with which I've implemented a custom PersistenceKey being the building block for @SharedReader in TCAs state - which is @ObservableState - TCAs value semantics analogue counterpart to @Observable.

GraniteLake commented 3 weeks ago

Really appreciate all your responses! @Sajjon our minimum deployment is iOS 17 so that's a great solution. Excited to check out TCA and your app I wasn't familiar with TCA. I'll leave the issue open until at least tomorrow to allow members of the UniFFI team to share their thoughts on your suggestions.

mhammond commented 3 weeks ago

Foremost we don't have any UDL spelling nor proc-macro for binding language specific settings for certain types. It would be very nice to have though! E.g. being able to express mutable/immutable stored properies on certain Swift types, and not catch em all, like we have today. Maybe @mhammond or @bendk can share some thoughts about that specifically...

We'd probably prefer to keep things which are language-specific like that in uniffi.toml for as long as we can - eg, you would list those types there and it would influence the binding generator. Expressing this inside proc-macros would be both tricky (because we don't really have a way to carry arbitrary metadata from proc-macros all the way to the scaffolding generation) and it seems like it would become unwieldy if all languages ended up with their own set of custom things. That said though, we do try and be pragmatic.