Update Additional Field on Realm Object with ObservedRealmObject in SwiftUI - swiftui

Xcode 12.5, iOS 14.5, macOS 11.3
Let's say I have a simple Realm class like this in a SwiftUI app:
class Item: Object, ObjectKeyIdentifiable{
#objc dynamic var id = ""
#objc dynamic var name = ""
#objc dynamic var updated = Date()
}
I pass an object into a view using #ObservedRealmObject where I can edit its name property by binding it to a TextField view like this:
struct DetailView: View {
#ObservedRealmObject var item: Item
var body: some View{
TextField("Name...", $item.name)
}
}
When I edit the field, the name property updates as I type and the realm is updated in real-time.
But I also want to update the updated property with a new Date() timestamp whenever the object is modified. Since the write transaction is managed automatically by #ObservedRealmObject, I'm unclear on how to pull this off.
How can I change updated every time the name (or any other additional property) is changed?

There are a lot of ways this can be accomplished; from simply updating the updated property when the 'save' button is pressed to using KVO to observe changes to the objects name property to adding a Swift 'front end' to the objects objc properties that can be handled through Computed Properties.
Let's use the third option using Swift Computed Properties and modify the Item in the question.
class Item: Object, ObjectKeyIdentifiable{
#objc dynamic var _id = UUID().uuidString
#objc private dynamic var _name = ""
#objc dynamic var updated = Date()
var name: String {
get {
return _name
}
set {
_name = newValue
updated = Date()
}
}
convenience init(name: String) {
self.init()
self.name = name
}
override static func primaryKey() -> String? {
return "_id"
}
}
A few things to note:
The realm name property was made private with a name change to _name. I did that so you don't accidentally set it and go around the Swift implementation.
We then added a new Swift name property that acts as the 'front end' property to a Realm property backed by _name.
When an item is created using the convenience function let i = Item(name: "Item 0") or set within a write block someItem.name = "Updated name" the name computed property sets the Realm property and also populates the updated property
set {
_name = newValue
updated = Date()
}
This should work seamlessly with #ObservedRealmObject

Related

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?

Binding<String> action tried to update multiple times per frame in SwiftUI

I have a VM that is implemented as follows:
LoginViewModel
class LoginViewModel: ObservableObject {
var username: String = ""
var password: String = ""
}
In my ContentView, I use the VM as shown below:
#StateObject private var loginVM = LoginViewModel()
var body: some View {
NavigationView {
Form {
TextField("User name", text: $loginVM.username)
TextField("Password", text: $loginVM.password)
Every time I type something in the TextField it shows the following message in the output window:
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
It is a message and not an error.
If I decorate my username and password properties with #Published then the message goes away but the body is rendered each time I type in the TextField.
Any ideas what is going on and whether I should use #Published or not. I don't think I will gain anything from putting the #Published attribute since this is a one-way binding and I don't want to display anything on the view once the username changes.
If I decorate my username and password properties with #Published then the message goes away
This is the correct solution. You need to use #Published on those properties because that is how SwiftUI gets notified when the properties change.
the body is rendered each time I type in the TextField
That is fine. Your body method is not expensive to compute.
I don't think I will gain anything from putting the #Published attribute since this is a one-way binding
You cannot be sure SwiftUI will work correctly (now or in future releases) if you don't use #Published. SwiftUI expects to be notified when the value of a Binding changes, even when a built-in SwiftUI component like TextField causes the change.
For the simple case - the state is kept in the same view or in a ModelSupport class, consists of strings or other primitive types, and there's only one of each, #Published will work fine.
I got this error with a model class containing an array of structs and using a List, and every time you type inside a TextField inside a list (or every time you select an item in a list), the view gets refreshed, and the error gets triggered.
I am thus using a DelayedTextField:
struct DelayedTextField: View {
var title: String = ""
#Binding var text: String
#State private var tempText: String = ""
var body: some View {
TextField(title, text: $tempText, onEditingChanged: { editing in
if !editing {
$text.wrappedValue = tempText
}
})
.onAppear {
tempText = text
}
}
}
and the binding update error is no more.

SwiftUI view updates even if it does not depend on a dynamic property

I created an ObservableObject that gathers data and a view that depends on this object. More specifically some parts of the UI depend on one property of it and other parts depend on other properties.
The way ObservableObject works is that if any of its Published property gets updated (event if it does not change) it sends an objectWillChange notification that triggers updates for subscribed views.
However, I did expect that only the parts of the views that depends on an ObservableObject property that actually changed would be updated, not the entire view because body computations are expensive.
Unfortunately this it not the behavior I observed during my experiments. For instance in the following code the List view depends only on the color dynamic property of the ObservedObject "preferences" and the TextField updates only the text property of the observed object. I observed with the debugPrint's that when I type text the List and its rows gets updated each time even if it does not depend on text.
struct ContentView: View {
#State private var list = ["foo", "bar", "baz"]
#StateObject private var preferences = Preferences()
var body: some View {
VStack {
List(list, id: \.self) { element in
Text(element)
.foregroundColor(preferences.color)
.debugPrint("Row view updated")
}
Spacer()
HStack {
TextField("Text", text: $preferences.text)
.debugPrint("Text field view updated")
Button("Toogle Color", action: toggleColor)
.debugPrint("Button view updated")
}
}
.padding()
.debugPrint("Content view updated")
}
func toggleColor() {
preferences.color = [.blue, .green, .orange, .red].randomElement()!
}
}
final class Preferences: ObservableObject {
#Published var color: Color = .blue
#Published var text: String = ""
}
extension View {
func debugPrint(_ elements: Any...) -> Self {
#if DEBUG
print(elements)
return self
#else
return self
#endif
}
}
Is this the correct and intended behavior? How the optimal behavior I described above could be obtained, i.e. not calling body when the ObservedObject changes but only the components that depends on specific properties?
I observed in bigger SwiftUI projects that this behavior can greatly slow down the application and is not easily debuggable because unique state is often enforced through heavy ObservedObjects (or StateObject) injected at the root view and used in many places. I Observed this behavior even with small lists of components with heavy UI layout.
Note 1: I was bugged by the StateObject definition which suggests that only the views that depend on those properties are updated (and not when the StateObject changes).
SwiftUI creates a new instance of the object only once for each
instance of the structure that declares the object. When published
properties of the observable object change, SwiftUI updates the parts
of any view that depend on those properties [...]
Note 2: The issue totally disappear if I define the List in its own component, the rendering is fast:
struct ListView: View {
let list: [String] // Also works with a #Binding
let color: Color // Also works if it would be `preferences`
var body: some View {
List {
ForEach(list, id: \.self) { element in
Row(text: element, color: color)
}
}
}
}
Why in this case the view updates are fast?

Does Combine have publishers that only publish when a value actually changes?

Is there a way to make a #Published variable that only publishes its value when the new value is different from the old one?
Right now if we have
#Published var test: Bool = false
and we do
test = false
test = false
test = false
the publisher is called 3 times. This is quite annoying as it sometimes causes my SwiftUI Views to be recreated because somewhere higher up in the hierarchy a publisher was set to the value it previously was set, trough another publisher that was triggered (and that destroys text field inputs because view models through the hierarchy are recreated when that happens).
is there a way of only publishing when it goes from false to true or vice versa?
An example scenario:
We create a user object in our app, and we want to add a car to the user. The app should immediately show the "add car" view if no car is found, otherwise show the main application view. For that we have a listener somewhere. On our top level view we have:
#ObservedObject var viewModel = UserViewModel()
var body: some View {
if !viewModel.hasVehicles {
return AnyView(AddVehicleView(viewModel:AddVehicleViewModel())
} else {
return AnyView(UserMainView(user: user))
}
}
}
in our UserViewModel we have
class UserViewModel: ObservableObject {
#Published var hasVehicles: Bool = false
some code that updates that boolean when certain listeners trigger.
Inside AddVehicleView we have a form that allows the user to fill out some text fields and save the vehicle.
Now imagine that for some reason the code that updates the hasVehicles property is triggered, but there still are no vehicles. What happens:
hasVehicles = false
and the top level view is re-evaluated, resulting in return AnyView(AddVehicleView(viewModel:AddVehicleViewModel()) being executed, and my form with text fields is emptied.
I suppose in this case I could solve it by putting AddVehicleViewModel() as a property inside the View struct, but that wouldn't solve it in the case when we want this to be executed multiple times, as that would mean next time the view gets built it will show the data of the last time we created that view, as we reuse the view model.
Use Combine's removeDuplicates() operator in the code that updates the property. For example, the view model could create a publisher that updates a boolean property based on the value of two other boolean properties:
#Published var hasTrucks: Bool
#Published var hasCars: Bool
#Published var hasVehicles: Bool
func createVehiclePublisher() {
Publishers.CombineLatest($hasTrucks, $hasCars)
.map { $0 || $1 }
.removeDuplicates()
.assign(to: &$hasVehicles)
}
Using removeDuplicates() causes hasVehicles to only be updated when there would be a change in its value.
Try to use regular property with manual publisher activation, like
class UserViewModel: ObservableObject {
var hasVehicles: Bool = false {
willSet {
if hasVehicles != newValue {
objectWillChange.send()
}
}
}
// ... other code
}

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.