Swift Combine Pipeline to compare draft model - swift

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.

Related

What is the purpose of objectWillChangeSequence compared to the onChange modifier in SwiftUI?

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.

When the #Published publishes the value it wraps?

I've got the publisher
#Published var feedData = Feed()
And this piece of code, which listens to it
// some View
.onReceive(feed.$feedData) { feedData in
if feedData.personalTasks.count > 0 {
withAnimation(.easeOut(duration: 0.3)) {
showCards = true
}
}
}
The question is when .onRecieve will be executed? Every time feedData is accessed? Or every time any property of feedData is changed? How does this property wrapper know when something changes in feedData?
.onReceive will be executed every time feedData is changed, which is when the Published publisher will emit a value.
If Feed is a value-type, like a struct, then anytime any of its properties change, the value-type semantics of Swift ensure that the entire object is being changed.
If Feed is a reference-type - a class, then only when setting feedData to a different instance would emit a value.

RxSwift - change specific object in PublishSubject on event

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.

Realm Notification is not triggered when NSPredicate changes

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.

Save model state between viewControllers with Realm - it breaks relation

I use Realm in my swift project.
Here are my objects, Meal and Reaction - with a mapping of one to many:
enum DishType: String{
case Breakfast
case Second_breakfast
case Brunch
case Elevenses
case Lunch
case Tea
case Dinner
case Supper
case Snack
}
class Meal : Object{
dynamic var date: NSDate = NSDate()
dynamic var dishType = DishType.Breakfast.rawValue
var dishTypeEnum: DishType{
get{
return DishType(rawValue: dishType)!
}
set{
dishType = newValue.rawValue
}
}
dynamic var foodItems: String = ""
var reactions = List<Reaction>()
}
enum Category: String{
case Apetit
case Energy
case Emotion
}
enum Type: String{
case Positive
case Negative
}
class Reaction: Object{
dynamic var category = Category.Apetit.rawValue
dynamic var text: String?
dynamic var selected: Bool = false
dynamic var type = Type.Positive.rawValue
.....
}
I need to save state from one viewController to the other.
I select some reactions, which I have to see selected when returning to the reactions screen.
I was thinking saving into Realm, but I also have to clear all reactions state when entering a new set of data.
When deleting from Reaction, it seems that it also breaks the relation. I have Meal objects saved (which also have a relation with Reaction).
I do not know how to distinguish between Reactions that indicate state and the ones that are in relation, from a saved Meal.
I was thinking saving Reactions for state, just in NSUserDefaults. But like this I will combine both frameworks.
What to you think? How do you save state?
I was thinking saving into Realm, but I also have to clear all reactions state when entering a new set of data. When deleting from Reaction, it seems that it also breaks the relation. I have Meal objects saved (which also have a relation with Reaction).
When you make the modifications to the Reaction objects, these modifications will appear where ever the reactions are linked. So you likely don't want to make modifications to them unless it's a rewording. (e.g. fixing a typo "You don't want desert" => "You don't want dessert" 😉) Also if you delete them, they will be also deleted from Meal objects where they were linked.
I was thinking saving Reactions for state, just in NSUserDefaults. But like this I will combine both frameworks.
That's not necessary. You can solve your use-case entirely by using Realm.
The solution is that you don't include an attribute selected in Reaction. Instead you just add the selected reactions to the Meal. In your view controller that means, that you render a cell for each Reaction by querying for all of them with let reactions = realm.objects(Reaction).
You can then figure out whether you need to add the checkmark, by checking whether they are included in the Meal's reactions like this:
meal.reactions.contains(reaction)
If a reaction for a meal is selected, you add it to the meal:
meal.reactions.add(reactions[indexPath.row])
If a reaction is deselected, you remove it from the meal:
meal.reactions.removeAtIndex(meal.reactions.indexOf(reactions[indexPath.row]))