SwiftUI/Combine Are publishers bi-directional? - swiftui

The execution of this code is confusing me:
class RecipeListViewModel: ObservableObject {
#Published var recipeList = [RecipeViewModel]()
init(){
RecipeRepository.$allRecipeVMs.map { recipes in
recipes.map { recipe in
recipe
}
}
.assign(to: &$recipeList)
}
}
My understanding was that SwiftUI publishers are uni-directional, meaning that when RecipeRepository.allRecipeVMs is modified recipeList is too. However, the behaviour I'm seeing is that When recipeList is updated, RecipeRepository.allRecipeVMs is also updated. I've tried to find documentation on this but can't seem to find anything.
Here is the RecipeViewModel class, not sure if it's useful:
class RecipeViewModel : Identifiable, ObservableObject, Hashable{
var id = UUID().uuidString
#Published var recipe: Recipe
init(recipe: Recipe){
self.recipe = recipe
}
}

It's not clear what "modifications" you are calling. #Published and SwiftUI views diff value types, whereas you've created an array of reference types. It will diff only when the instance of that object changes, not when the object itself fires its ObjectWillChange publisher.
Frankly, that's a pretty useful setup because you can have containers that distribute and manage dependencies in your app without causing the root view hierarchy to diff and rebuild constantly.
If you want some sort of two-way Delegate relationship, you can simply set a weak variable in the VMs to self.

Related

SwiftUI architecture - value vs reference types

Putting aside the reasons why one might choose a value type over a reference type, does SwiftUI itself prefer use of #State vs #ObservedObject in terms of architecture.
Here for example are two implementations of a basic list app - cats implemented using a struct, dogs using a class:
import SwiftUI
struct Cat: Identifiable {
let id = UUID()
var name: String = "New cat"
}
final class Dog: Identifiable, ObservableObject {
let id = UUID()
#Published var name: String = "New dog"
}
final class Dogs: ObservableObject {
#Published var dogs: [Dog] = []
}
struct ContentView: View {
#State private var cats: [Cat] = []
#ObservedObject private var dogs = Dogs()
var body: some View {
VStack {
Text("Cats").bold()
ForEach($cats, content: CatView.init)
Button("New cat") {
cats.append(Cat())
}
Divider().padding()
Text("Dogs").bold()
ForEach(dogs.dogs, content: DogView.init)
Button("New dog") {
dogs.dogs.append(Dog())
}
}
}
}
struct CatView: View {
#Binding var cat: Cat
var body: some View {
TextField("Name", text: $cat.name)
}
}
struct DogView: View {
#ObservedObject var dog: Dog
var body: some View {
TextField("Name", text: $dog.name)
}
}
Very little difference in terms of coding...so:
Is there a reason why one might prefer one over the other? Apple's own examples often use structs + #State + #Bindings.
Is there any performance implication for either choice, say when either Cat or Dog object has many fields (ObservedObjects let you precisely specify which attribute should generate an objectWillChange call), or if the arrays hold many items, i.e. 100k's?
When would one go for a reference type, when Bindings give pseudo-reference attributes to a value type?
structs are cheaper to create than classes since there is no cost of references or ARC. Using structs also reduces the risk of shared-mutation bugs. See Choosing Between Structures and Classes (Apple developer article) on why to prefer structs.
Your code also has a bug in it which you may not have initially noticed. Since Dog is a class, and Dogs contains an array of Dog, the #Published here won't help in certain scenarios. This is because #Published detects a value type changing, e.g. the array having different elements. However, this doesn't account for mutating a Dog's name property. The Dog class reference in the array doesn't change so the #Published in Dogs doesn't emit an update. This is another bug caused by using a class instead of a struct. There is a way around this, but it introduces unnecessary complexity.
Is there a reason why one might prefer one over the other? Apple's own examples often use structs + #State + #Bindings.
Prefer structs because they are faster and less prone to bugs (see more detail above). In my apps I built / am building, I very rarely used ObservableObjects. They were the source of many bugs, unnecessary view updates, and complexity. The performance also greatly suffers when you have an ObservableObject with multiple #Published properties - changing one property updates every view which subscribes to the object updates.
Is there any performance implication for either choice, say when either Cat or Dog object has many fields (ObservedObjects let you precisely specify which attribute should generate an objectWillChange call), or if the arrays hold many items, i.e. 100k's?
The cost here is mutating the array of Cat or Dog, so I doubt there would be a performance difference to prefer class over struct. As mentioned earlier, structs are still more performant in general. Since in neither scenario the array is being copied (and if it was copied, it would again be the same) there is no difference.
When would one go for a reference type, when Bindings give pseudo-reference attributes to a value type?
Use ObservableObjects when you rely on sharing data across a section of your app and potentially need some logic to get/set the #Published properties within. I (personally) only ever use ObservableObjects when I need to pass the object through the environment at some point with #EnvironmentObject.
To finish off, choose what you think is best for your architecture. Prefer structs everywhere possible, and switch to class if it solves a problem that is too complex to solve just with structs.

Can you change the realm configuration of #ObservedResults to get new results on the fly?

Context
I have two different realms that a user can switch between at any point they decide; in-memory or on-disk.
I store this Realm configuration on an observable object which publishes the change when required:
class RealmServiceConfig: ObservableObject {
static var config = CurrentValueSubject<Realm.Configuration?, Never>(nil)
}
I have a class that stores realm filters/sorts as #Published variables that a user can change at any point.
class Store: ObservableObject
{
static var shared = Store()
#Published var realmFilter: NSPredicate? = nil
#Published var realmSortKeyPath: String = "fieldOnMyRealmModel"
#Published var realmSortKeyPathAscending: Bool = false
}
I have two views:
ListView - This shows the live results of Objects in the current Realm.
As the filter/sort changes, the filteredObjects is recalculated thanks to the #Published values, but, it still points to the same config.
//import SwiftUI
//import Combine
//import RealmSwift
struct ListView: View
{
#ObservedObject var store = Store.shared
#ObservedResults(MyRealmModel.self, configuration: RealmServiceConfig.config.value) var unfilteredObjects
var body: some View {
let filteredObjects = unfilteredObjects
.filter(store.realmFilter ?? NSPredicate(value: true))
.sorted(by: [SortDescriptor(keyPath: "\(store.realmSortKeyPath)", ascending: store.realmSortKeyPathAscending)])
return VStack(spacing: 0) {
ScrollView {
VStack {
ForEach(filteredObjects) { object in
Text(object.name)
}
}
}
}
}
}
SettingsView - The user can change their Realm configuration between in-memory and on-disk here which updates the aforementioned RealmServiceConfig.config value
When navigating to SettingsView, the ListView is moved to the background, it is not removed from the view hierarchy.
Question
When a user changes the realm and therefore updates the RealmServiceConfig.config value, is it possible to force reload #ObservedResults with the latest configuration?
Based on the current setup, even though my configuration parameter points to the published value, this is never applied.
Workaround
I have implemented this on a model class that stores the Results<Object> in a #Published variable which allows me to react and update the query based on the config changes and pass the results through to the views.
It would just be cleaner to have this all contained in the SwiftUI view where it's needed.
Maybe this is not available yet/works against how #ObservedResults is meant to be used?

Observe Collection Results in Realm with Combine and SwiftUI

I am trying out this quick start for SwiftUI and Combine in order to try and understand how to connect my Realm database to Combine.
The example observes a RealmSwift.List and keeps a table populated with its data. This is is a linked list to a child class. I'm wondering how to observe a Results collection so I can keep track of any changes to an entire Realm class.
For example, let's say I have a Workspace class:
class Workspace: Object, ObjectKeyIdentifiable{
#objc dynamic var id = UUID().uuidString
#objc dynamic var name = ""
#objc dynamic var archived = false
}
In the state object, I can set up a Results<Workspace> variable like this:
class AppState: ObservableObject {
#Published var workspaces: Results<Workspace>?
var cancellables = Set<AnyCancellable>()
init(){
let realmPublisher = PassthroughSubject<Realm, Error>()
realmPublisher
.sink(receiveCompletion: { _ in }, receiveValue: { realm in
//Get the Results
self.workspaces = realm.objects(Workspace.self)
})
.store(in: &cancellables)
realmPublisher.send(try! Realm())
return
}
}
But when it comes time to observe the object, I can't because Results isn't an object (I assume).
struct ContentView: App {
#ObservedObject var state = AppState()
var view: some View {
ItemsView(workspaces: state.workspaces!)
}
var body: some Scene {
WindowGroup {
view.environmentObject(state)
}
}
}
struct ItemsView: View {
#ObservedObject var workspaces: Results<Workspace> //<!-- Error
var body: some View {
//...
}
}
Xcode gives a syntax error on the workspaces property:
Property type 'Results' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
Is it possible to observe a set of Results just like we can have a notification listener on a collection of Results?
Technically, you could hook up a sink to state.workspaces (state.$workspaces.sink()), but in this case, I think you're overcomplicating the problem.
You already have an #ObservableObject in your ContentView (AppState) that is managing the results for you. So, change ItemsView to just take this as a parameter:
var workspaces: Results<Workspace>?
It doesn't need to be an #ObservedObject -- either way, whether it's getting observed in that view or it's parent view, it's going to get re-rendered. It does have to be optional here, since it's an optional value on your AppState, unless you want to keep passing it with the force unwrap (!), but that's generally a bad idea, since it'll crash if it ever is in fact nil.
Also, above, in your Realm code, make sure it's matching the tutorial that you were following. For example, you have Publisher.sink which should really be realmPublisher.sink
You are correct, Results is a struct, and therefore cannot be covered by #StateObject or #ObservedObject. Your workaround is suitable for now.
Once https://github.com/realm/realm-cocoa/pull/7045 is released, you will be able to use one of the new Realm property wrappers to embed your Results into the view directly. At the time of this posting, that would be #FetchRealmResults, but that is subject to change.

SwiftUI View not updating on async change to published properties of Observed Object

I have the following SwiftUI View:
struct ProductView: View {
#ObservedObject var productViewModel: ProductViewModel
var body: some View {
VStack {
ZStack(alignment: .top) {
if(self.productViewModel.product != nil) {
URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
}
else {
Image("loading")
}
}
}
}
that observes a ProductViewModel
class ProductViewModel: ObservableObject {
#Published var selectedColor: UIColor = .white
#Published var product: Product?
private var cancellable: AnyCancellable!
init(productFuture: Future<Product, Never>) {
self.cancellable = productFuture.sink(receiveCompletion: { comp in
print(comp)
}, receiveValue: { product in
self.product = product
print(self.product) // this prints the expected product. The network call works just fine
})
}
The Product is a Swift struct that contains several string properties:
struct Product {
let id: String
let imageurl: String
let price: String
}
It is fetched from a remote API. The service that does the fetching returns a Combine future and passes it to the view model like so:
let productFuture = retrieveProduct(productID: "1")
let productVM = ProductViewModel(productFuture: productFuture)
let productView = ProductView(productViewModel: productViewModel)
func retrieveProduct(productID: String) -> Future<Product, Never>{
let future = Future<Product, Never> { promise in
// networking logic that fetches the remote product, once it finishes the success callback is invoked
promise(.success(product))
}
return future
}
For the sake of brevity, I've excluded the networking and error handling logic since it is irrelevant for the case at hand. To reproduce this as quickly as possible, just initialize a mock product with some dummy values and pass it to the success callback with a delay like this:
let mockproduct = Product(id: "1", imageurl: "https://exampleurl.com", price: "$10")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
promise(.success(mockproduct))
})
Once the product arrives over the network, it is assigned to the published product property.
The fetching works and the correct value is assigned to the published property. Obviously this happens after the view has been created since the network call takes some time. However, the View never updates even though the published object is changed.
When I pass the product directly through the View Model initializer rather than the future, it works as expected and the view displays the correct product.
Any suggestions on why the view does not react to changes in the state of the view model when it is updated asynchronously through the combine future?
EDIT: When I asked this question I had the ProductViewModel + ProductView nested inside another view. So basically the productview was only a part of a larger CategoryView. The CategoryViewmodel initialized both the ProductViewModel and the ProductView in a dedicated method:
func createProductView() -> AnyView {
let productVM = productViewModels[productIndex]
return AnyView(ProductView(productViewModel: productVM))
}
which was then called by the CategoryView on every update. I guess this got the Published variables in the nested ProductViewModel to not update correctly because the view hierarchy from CategoryView downwards got rebuilt on every update. Accordingly, the method createProductView got invoked on every new update, resulting in a completely new initialization of the ProductView + ProductViewModel.
Maybe someone with more experience with SwiftUI can comment on this.
Is it generally a bad idea to have nested observable objects in nested views or is there a way to make this work that is not an antipattern?
If not, how do you usually solve this problem when you have nested views that each have their own states?
I have been iterating on patterns like this to find what I think works best. Not sure what the problem is exactly. My intuition suggests that SwiftUI is having trouble making updates on the != nil part.
Here is the pattern that I have been using which has been working.
Define an enum for states in your networking logic
public enum NetworkingModelViewState {
case loading
case hasData
case noResults
case error
}
Add the enumeration as a variable on your "View Model"
class ProductViewModel: ObservableObject {
#Published public var state: NetworkingModelViewState = .loading
}
Update the state as you progress through your networking
self.cancellable = productFuture.sink(receiveCompletion: { comp in
print(comp)
}, receiveValue: { product in
self.product = product
self.state = NetworkingModelViewState.hasData
print(self.product) // this prints the expected product. The network call works just fine
})
Now make a decision in your SwiftUI based on the Enum value
if(self.productViewModel.state == NetworkingModelViewState.hasData) {
URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
}
else {
Image("loading")
}
Musings ~ It's hard to debug declarative frameworks. They are powerful and we should keep learning them but be aware of getting stuck. Moving too SwiftUI has forced me to really think about MVVM. My takeaway is that you really need to separate out every possible variable that controls your UI. You should not rely on checks outside of reading a variable. The Combine future pattern has a memory leak that Apple will fix next release. Also, you will be able to switch inside SwiftUI next release.

Does not conform to protocol BindableObject - Xcode 11 Beta 4

Playing around with examples out there. Found a project that had a class that was a bindableobject and it didn't give any errors. Now that Xcode 11 beta 4 is out, I'm getting the error:
Type 'UserSettings' does not conform to protocol 'BindableObject'
It has a fix button on the error which when you click on that, it adds
typealias PublisherType = <#type#>
It expects you to fill in the type.
What would the type be?
class UserSettings: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var score: Int = 0 {
didSet {
didChange.send()
}
}
}
Beta 4 Release notes say:
The BindableObject protocol’s requirement is now willChange instead of
didChange, and should now be sent before the object changes rather
than after it changes. This change allows for improved coalescing of
change notifications. (51580731)
You need to change your code to:
class UserSettings: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var score: Int = 0 {
willSet {
willChange.send()
}
}
}
In Beta 5 they change it again. This time they deprecated BindableObject all together!
BindableObject is replaced by the ObservableObject protocol from the
Combine framework. (50800624)
You can manually conform to ObservableObject by defining an
objectWillChange publisher that emits before the object changes.
However, by default, ObservableObject automatically synthesizes
objectWillChange and emits before any #Published properties change.
#ObjectBinding is replaced by #ObservedObject.
class UserSettings: ObservableObject {
#Published var score: Int = 0
}
struct MyView: View {
#ObservedObject var settings: UserSettings
}
in Xcode 11.X, I verify is fine in Xcode 11.2.1, 11.3.
BindableObject is changed to ObservableObject.
ObjectBinding is now ObservedObject.
didChange should be changed to objectWillChange.
List(dataSource.pictures, id: .self) { }
You can also now get rid of the did/willChange publisher and the .send code and just make pictures #Published
The rest will be autogenerated for you.
for example:
import SwiftUI
import Combine
import Foundation
class RoomStore: ObservableObject {
#Published var rooms: [Room]
init(rooms: [Room]) {
self.rooms = rooms
}
}
struct ContentView: View {
#ObservedObject var store = RoomStore(rooms: [])
}
ref: https://www.reddit.com/r/swift/comments/cu8cqk/getting_the_errors_pictured_below_when_try_to/
SwiftUI and Combine are two new frameworks that were announced at WWDC 2019. These two frameworks received a lot of attention at WWDC 2019, as evidenced by the number of sessions in which these technologies were featured.
SwiftUI was introduced as
a revolutionary, new way to build better apps, faster.
Combine is described as
a unified declarative framework for processing values over time
Between the initial release and now (May, 2020, Swift 5.2), there have been some changes. Anyone new to SwiftUI and Combine, who may have watched the WWDC videos, may be left with a few questions as to how the two frameworks work together.
Combine defines two interfaces: Publisher and Subscriber. A publisher sends events to subscribers. See sequence diagram below.
If you start an application in SwiftUI, and then add combine, there will be no mention of a Publisher or a Subscriber, the two main players required to use Combine. Consider this very simple sample application below.
import SwiftUI
import Combine
import SwiftUI
final class ActorViewModel: ObservableObject {
var name : String
private var imageUrl : URL?
//#Published
private (set) var image : Image = Image(systemName: "photo") {
willSet {
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
init(name: String, imageUrl: URL?) {
self.name = name
self.imageUrl = imageUrl
self.fetchImage()
}
private func fetchImage() {
guard nil != self.imageUrl,
String() != self.imageUrl!.absoluteString else { return }
let task = URLSession.shared.dataTask(with: self.imageUrl!) { (data, response, error) in
guard nil == error , nil != response, nil != data,
let uiImage = UIImage(data: data!) else { return }
self.image = Image(uiImage: uiImage)
}
task.resume()
}
}
struct ContentView: View {
#ObservedObject var actor : ActorViewModel
var body: some View {
HStack {
actor.image
.resizable()
.aspectRatio(contentMode: ContentMode.fit)
.frame(width: 60, height: 60)
Text(actor.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let actor = ActorViewModel(name: "Mark Hammill",
imageUrl: URL(string: "https://m.media-amazon.com/images/M/MV5BOGY2MjI5MDQtOThmMC00ZGIwLWFmYjgtYWU4MzcxOGEwMGVkXkEyXkFqcGdeQXVyMzM4MjM0Nzg#._V1_.jpg"))
return ContentView(actor: actor)
}
}
The app preview via the canvas will look like this:
The app uses a list view to display names and images of actors. There are just two classes to consider:
ContentView -- the SwiftUI View subclass
ActorViewModel -- the source of the data for the ContentView (called a ViewModel as it performs the role of VM in MVVM)
The view has a reference to the actor object, as per the class diagram below.
Although this example is using Combine, it is not immediately apparent. There is no mention of a Publisher or a Subscriber. What is going on?
Answer: Looking at the class hierarchy fills in the missing gaps. The below class diagram explains the full picture (click on the image to see it in greater detail).
Consulting Apple's documentation provides definitions for these types:
ObservedObject: A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
ObservableObject: A type of object with a publisher that emits before the object has changed. By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its #Published properties changes.
objectWillChange: A publisher that emits before the object has changed.
PassthroughSubject: A subject that broadcasts elements to downstream subscribers. As a concrete implementation of Subject, the PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
First, consider what the #ObservedObject means. This is a property wrapper. A property wrapper reduces code duplication, and allows for a succinct syntax when declaring properties that hides how the property is stored and defined. In this case, the "Observed Object" is a property which observes another object.
In other words, the property is a Subscriber (from the Combine Framework). The actor is (through the use of a property wrapper) is a Subscriber, which subscribes to a Publisher, but what is the Publisher in this scenario?
The "Observable Object" is not itself the publisher, but rather has a publisher. The ActorViewModel conforms to the ObservableObject protocol. By doing so, it is provided with a publisher property called objectWillChange by an extension (which the framework provides on the ObservableObject protocol). This objectWillChange property is of type PassthroughSubject, which is a concrete type of the Publisher protocol. The passthrough subject has a property called send, which is a publisher method used to send data to any subscribers. So the property called "objectWillChange" is the Publisher.
To recap, the Subscriber is the property called actor from the ContentView class, and the Publisher is the property objectWillChange from the ActorViewModel class. What about the need for the Subscriber to Subscribe to the Publisher? The "#ObservedObject" property wrapper is itself a Subscriber, so it must subscribe to the Publisher. But how does the View find out about changes sent to the Subscriber? That is handled by the SwiftUI framework, which we never see.
Take-away: we don't need to worry about subscribing the view to the Publisher. On the other hand, we do need to worry about making sure the publisher tell the subscriber when something is about to change. When the image has been fetched from a remote server, and the data has been transformed into an image object, we call objectWillChange.send() to inform the View. Once the subscriber receives notification from the publisher that something is about to / has changed, it invalidates the view (which results in the view redrawing itself).
Summary
The way in which SwiftUI uses a ObservedObject PropertyWrapper does not on the surface give away the fact that Combine even exists in the equation. But by inspecting ObservedObject and ObservableObject, the underlying Combine framework is revealed, along with the design pattern:
subscriber --> subscribing to a publisher --> which then publishes changes --> that are received by the subscriber
References:
Blog Article
WWDC 2019 Session 204
WWDC 2019 Session 226