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.
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 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.
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.
I'm using a custom getter like so
var currentShowLiked: Bool {
get {
return [some condition met] ? true : false
}
set {
self.currentShowLiked = newValue
}
}
and it works fine. However, I would expect to be able to set true or false value back to my variable, but Swift forced my to implement a setter, which does not produce any warnings, but at runtime if I'm to change value of my variable, app crashes with no apparent reason or sane explanation pointing to setter line with EXC_BAD_ACCESS (code=2...) and a message in console warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
Why do I do?
You are in recursive context.
You can't use currentShowLiked in the currentShowLiked property body as below:
// Recursive, Infinite loop
self.currentShowLiked = newValue
To fix this:
private var _currentShowLiked : Bool
var currentShowLiked: Bool {
get {
return [ some condition met on _currentShowLiked ] ? true : false
}
set {
self._currentShowLiked = newValue
}
}
A Swift property that defines a custom getter and / or setter operation is a computed property which does not actually stores a value. The custom setter can be used to modify the internal state of other properties.
See also: "The Swift Programming Language" page 180
You can not set value in its setter because you are creating a recursion -method calling itself because:
self.currentShowLiked = newValue
will call set method you defined which will go and go. If you are overriding setter and getter you probably want to have some backing store property which will be private.
Moreover you defined a getter which base on some condition so anyway you are not using the value you set on this property. I think you do not need this set
The error here is caused by a misunderstanding.
When you declare a
var a:Type
A member value a is allocated inside the object, and accessors and mutators are automatically created to allow you to read and write that variable.
However, when you declare
var a:Type { get { ... } set { ... } }
No member value is allocated - you have indicated that when the value is accessed (read) or mutated (written) from a user of the object, your code will do all necessary action to respond to it. If you ultimately want the value to be stored, you will need to store it to an actual value, (which will need a different name).
Since you are invoking the mutator of the object inside the mutator of the object, you have set up an infinite loop, which causes your program to crash due to stack overflow (all function call memory is used to store the record of the function calling itself over and over again).
The code you have above will crash because it causes an infinite loop - your setter for currentShowLiked sets currentShowLiked to the new value, so then that calls the setter again, and so on.
You don't have to implement a setter, but you then don't use get - the syntax is like this:
var currentShowLiked: Bool {
return [some condition met]
}
I'm trying to make a connection between my view and my viewModel using RAC 3.0. (Been reading Colin Eberhardt great tutorials on this) I want to subscribe to a Signal that fires every time func mouseUp(theEvent: NSEvent) is called. I.e.
func mouseUp(theEvent:NSEvent){
//create some signal or pass a variable here to my viewModel
}
and then in my viewModel I would like to subscribe to that stream.
let signalFromview = MutableProperty<String>("")
signalFromView.observe(next: { println($0) })
But I can't get this to work. The only way I've managed to get this to work is to subscribe to a SignalProducer like so:
View:
func mouseUp(theEvent:NSEvent){
viewModel.signalFromView.put(someValue)
}
ViewModel:
signalFromView.producer
|>start { println($0) }
But this isn't what I want since using the producer 'pulls' the value, i.e. upon first running this the code in the viewModel will be run on initialization with an empty value since it's trying to pull something that isn't there...
Hope this makes sense.
MutablePropertys should be used more like properties, and not as a proxy to a signal. So it should be initialized with a sensible initial value so that anyone that observes it via the producer will get values that make sense.
If you want a Signal, you can set up the signal with something like this:
let (signal, sink) = Signal<String, NoError>.pipe()
In your mouseUp function, you'd send events using something like:
sendNext(sink, someValue)
And you'd observe using:
signal.observe(next: { println($0) })