struct Contact : Codable, Hashable {
var id : String
...
}
I use PublishSubject to feed the data to the UITableView
let contacts : PublishSubject<[Contact]> = PublishSubject()
And when the value is changed on the other view controller, I want to change the specific value in the array.
I want to change the Contact object with the specific id.
contacts.filter {$0.id == contactId}[0].someKey = someValue
How can I do this with RxSwift?
Understand that a PublishSubject doesn't contain any state so there is nothing in it that you can change. Instead, you emit a new array from the publish subject with a new contact that has the new value.
Somewhere in your code, you are calling onNext(_:) on the subject (or connecting it to an Observable that is doing that. We would need to see that code to help you solve your problem.
Related
The sample comes from a WWDC22 video called The SwiftUI cookbook for navigation.
A NavigationModel is created to store the navigation path.
When the view appears, the NavigationModel is loaded with SceneStorage's data if any exists.
Whenever the NavigationModel changes, its data representation is saved in SceneStorage, by watching a custom objectWillChangeSequence computed property.
This last point intrigues me: why not just use the .onChange modifier? Like this:
.onChange(of: navModel.path) { _ in
data = navModel.jsonData
}
NB: the objectWillChangeSequence property is defined like this:
var objectWillChangeSequence: AsyncPublisher<Publishers.Buffer<ObservableObjectPublisher>> {
objectWillChange.buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest).values
}
There are lots of mistakes in the sample shown in the video.
One mistake is the Recipe struct has let id = UUID() which means even if the navigation path is restored, the recipe that was previously viewed would never be found because it has a different id from when it was persisted.
We can quickly fix it with this:
struct Recipe: Hashable, Identifiable {
var id: String {
return name
}
Now the #SceneStorage will start working and we can test replacing it with .onChange.
We quickly find out that NavigationModel has more than recipePath, there is also selectedCategory and columnVisibility. If we were to use onChange with jsonData then we would be needlessly doing an encode just to check if there has been a change.
I need to update all the objects in the database, when I try it, it says:
Thread 1: "Attempting to modify a frozen object - call thaw on the Object instance first."
I have the following:
#ObservedRealmObject var meal: MealTracking
#ObservedResults(MealTracking.self) var mealTracking
mealTracking is the one that contains everything and meal is the single object in the current view.
So once I update the name on that single object (meal), I want to update the name on all other objects. So I'm doing:
for (index, meal) in self.mealTracking.enumerated() {
$mealTracking.wrappedValue[index].mealName = ""
}
and I get the error of not being able to update the objects.
I also tried it like $mealTracking[index].mealName.wrappedValue = "" and gives another error: Referencing subscript 'subscript(_:)' requires wrapped value of type 'Results<MealTracking>'
For a the single object on the current view I can update it with:
$meal.mealName.wrappedValue = "some stuff", the problem is when attempting to update All objects
How can I update all the objects?
#ObservedRealmObject is a frozen object. If you want to modify the properties of an #ObservedRealmObject directly in a write transaction, you must .thaw() it first.
In your case something like :
mealName.thaw()?.name = ""
More information :
https://www.mongodb.com/docs/realm/sdk/swift/swiftui/
https://www.mongodb.com/community/forums/t/freeze-frozen-objects-and-thawing/15706/5
I have a VielModel in SwiftUI handling my person model. To be able to store draft persons in the editor in the View(s), I have two objects:
#Published var person: Person
#Published var draftPerson: Person
In the UI, I am only changing the draftPersons until the user clicks on "Save", which stores the draftPerson as the person. In the onAppear method of the editor, I reset the draftPerson to the person.
Now I want to disable the "Save" button of the Editor and therefor introduced a bool "modified" in the VM. Using a pipeline, I want to set the modified to true, if and as long as the draftPerson is not equal to person, by doing the following:
$draftPerson.map { draftPerson in
return draftPerson != self.person
}
.assign(to: \.modified, on: self)
.store(in: &cancellables)
It looks like it is working on first glance, but if I change something in a textField, the value of modified is only set to true after the second change in the field. Vice versa, if I delete the typed values, it is only set back to false after I delete one more character as were originally there.
Question 1:
Is there another "best practice" to handle changes in draft objects and deactivating the "Save" button in SwiftUI?
Question 2:
Why is the pipeline "one change behind"?
Thanks a lot for your input.
Edit: I created a separate part of the App focusing only on the pipeline and realized that it is indeed working as intended if I remove my other pipelines. I have to check now in detail. Nevertheless, I will stick with my first question:
Is there anything I can do better?
Please find the code here on Github
You could declare another #Published property and combine the two person and draftPerson publishers and publish whether they are the same, like this:
#Published var person: Person
#Published var draftPerson: Person
#Published var saveDisabled: Bool = true
public init() {
// all init code
Publishers.CombineLatest($person, $draftPerson)
.map { $0 == $1 }
.assign(to: &$saveDisabled)
}
But essentially it is not needed and a computed property will do the same job:
var saveDisabled: Bool {
person == draftPerson
}
Because both person and draftPerson are marked #Published each time one of them changes the View will be notified of the change so it will also pick new value of saveDisabled.
I have a simple ticked off list in Realm, where I am using NotificationToken to check for updates in the datasource and hereafter updates the tableView with the following items:
class Item : Object {
dynamic var name = ""
dynamic var isTickedOff = false
dynamic var timeStamp : Date?
}
The model:
var items: Results<Item> {
get {
let results = self.realm.objects(Item.self)
let alphabetic = SortDescriptor(property: "name", ascending: true)
let tickedOff = SortDescriptor(property: "isTickedOff", ascending: true)
let timestamp = SortDescriptor(property: "timeStamp", ascending: false)
return results.sorted(by: [tickedOff, timestamp, alphabetic]).filter("isTickedOff = %# || isTickedOff = %#", false, self.includeAll)
}
}
I have a switch in my tableview, where the user can change the self.includeAll property.
When inserting, deleting items or selecting them (resulting in setting them to isTickedOff or !isTickedOff) triggers the notification and updates the tableView. However, changing the self.includeAll property does not trigger the notification even though the items property is modified. I could include self.tableView.reloadData() when the user triggers the switch, but I would like the more smooth animations through the notification.
Is it me, who understands notifications wrong or is it a bug?
Thanks in advance!
Realm doesn't observe external properties, so it can't know when a property that is being used in a predicate query has changed, and to then subsequently generate a change notification off that.
When you access items the next time, that will give it a sufficient event to recalculate the contents, but by that point, it won't trigger a notification.
Obviously, like you said, the easiest solution would be to simply call tableView.reloadData() since that will force a refetch on items but there'll be no animation. Or conversely, like this SO question says, you can call tableView.reloadSections to actually enable a 're-shuffling' animation.
Finally, it might be a rather dirty hack, but if you still want it to trigger a Realm-based change notification, you could potentially just open a Realm write notification, change the includeAll property, and then close the write transaction to try and 'trick' Realm into performing an update.
One final point of note. I might not have enough information on this, so this might be incorrect, but if you're registering a Realm notification block off the items property, be aware that the way you've implemented that getter means that a new object is generated each time you call it. It might be more appropriate to mark that property as lazy so it's saved after the first call.
Using the (modified) examples in the Realm Swift documentation:
class Dog: Object {
dynamic var name = ""
dynamic var age = 0
let puppies = List<Puppies>()
}
class Person: Object {
dynamic var name = ""
dynamic var picture: NSData? = nil // optionals supported
let dogs = List<Dog>()
}
class Puppies: Object {
dynamic var name = ""
}
Let's assume that the Person.name = Bob, and that Bob has several dogs added to his dogs List. I have added another model class called Puppies, which would represent puppies that belong to Bob's dogs. (Apparently Bob owns a kennel.)
How would I get the values to display the names of Bob's dogs and the number of puppies belonging to each dog in a UITableview?
More specifically, what is the code to extract the property values of the List of dogs that belong to Bob. I assume that once I get those values it won't be difficult to list them in the tableview cells.
I decide to use the slightly modified example from the documentation instead of my own code so that those who read this won't have to try and interpret my code, and be able to focus on the solution.
I have been able to save my data and believe I have made the relationships between the objects link properly, but don't know how to get the values of the List objects, based on the primary key I have in my top level model. The problem I have is that (using the example above): the puppies know what dog they belong to, and the dog knows the person it belongs to, but the inverse relationships don't seem to work.
(By the way; I used the LinkingObject examples in the documentation in a playground and it throws and error. I'm not sure if the examples are incomplete in some way.)
In the Realm Browser (displaying the Person object) I can see the data as entered but the link that shows [Dog] has a 0 next to it and when I click on the link, the table that shows is blank. Maybe solving that issues will be the answer to make everything else work.
Please excuse my ignorance. I'm still learning.
Thanks to Ahmad F. for pointing me in the right direction.
Here is the answer:
I did not know how to append to the list property in each of the object classes. Following the example above, it is done by creating a variable that holds the Person object. Then the realm.write function would look something like this.
newDog = Dog()
newDog.name = "Phydeaux"
.....
try! realm.write {
currentPerson?.dogs.append(newDog)