I recently worked on SwiftUI and starting writing code in a declarative way. But here comes a confusion. Like what is shown below, I want to (1)load the song data and (2)show the view by setting isInfoViewShown, after song is assigned to any value.
I assume didSet{} and Combine #Published .sink{} are doing things interchangeably. So I want to ask what are the differences between them? And in my own opinion, didSet{} can do most of jobs Combine do. So why should Apple announce Combine framework?
Any help is appreciated.
class InfoViewModel: ObservableObject {
#Published var song: Song? {
didSet { // Here's the didSet{}: [1] load song data
if let song = song {
load(song: song)
}
}
}
private var songSelectedSubscription: AnyCancellable?
#Published var isInfoViewShown: Bool = false
init() { // Here's the Combine #Published .sink{}: [2] show the view
songSelectedSubscription = $song.sink{ self.isInfoViewShown = ($0 == nil ? false : true) }
}
}
Sure, there are lots of ways to observe changes to data, KVO, Notification Center, didSet, combine etc, so in one sense these things are indeed similar. Differences though are:
a property can only have one didSet, which makes it hard for any number of observers to register an interest in the property.
But the big win with Combine is: a Combine pipeline allows you to create streams easily where you can say for example, transform the stream of changes to a stream that: observes changes to the user's input string, debounces it (rate limiting changes so we don't spam the server), filter those changes for any value that is at least 3 characters long, and that produces a new stream which you can observe. Map/FlatMap is also a really important combine operator, transform a stream of a's into a stream of b's. Also merging two streams together with combineLatest and so on, so you can take a stream of a's and stream of b's and make a stream of (a, b)'s and then map that to a stream of validated c's, for example.
ObservableObject is designed to hold the model structs. For view data like isInfoViewShown that should be #State in the View struct. In SwiftUI the View struct is comparable to the view model object in UIKit it looks like you are used to. Since it is a struct (value) type is faster and less error-prone. You can extract related #State var into their own struct using mutating func for testable logic which you probably did in your view model objects. A View struct will automatically call body when a let or a #State var value changes, this dependency tracking is built-in to SwiftUI, you no-longer need to use Combine for this like you might in UIKit.
FYI when we use Combine inside the ObservableObject we assign the end of the pipeline to the #Published we don't tend to use sink because then we would need to perform manual cancellation in deinit whereas assign does that automatically. Inside the #Published auto-genenerated willSet, it calls objectWillChange.send() which SwiftUI coalesces multiple of into a single recalculation of body in any View struct that use #StateObject, #ObservedObject or #EnvironmentObject (regardless of the object's properties accessed or not - let and #State var are more optimal in that regard).
Related
I'm new to SwiftUI and was wondering if there is a concept similar to React.useEffect in SwiftUI.
Below is my code for listening keyboard events on macos.
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
var hello: String
#State var monitor: Any?
#State var text = ""
init(hello: String) {
self.hello = hello
print("ContentView init")
}
var body: some View {
VStack{
Text(hello)
.padding()
TextField("input", text: $text)
}
.onAppear {
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
print(hello)
return nil
}
}
}
}
struct MainView: View {
#State var hello: String = "h"
var body: some View {
ContentView(hello: hello)
Button(action: {
hello += "_h"
}) {
Text("tap me")
}
}
}
PlaygroundPage.current.setLiveView(MainView())
The playground output is as follows
ContentView init
h
h
ContentView init
h
h
h
Since onAppear trigger only once, even ContentView init multiple times. So the event callback here always prints the first captured value ("h").
So where should I add event listener and where should I remove it?
In React, you use useEffect from within a Component in order to declare a task or operation which causes side effects outside the rendering phase.
Well, SwiftUI is not exactly React, and there are problems and use cases which you would solve in a complete different approach. But, when trying to find something similar:
In SwiftUI you could call any function which is called from any "action" closure, for example from a SwiftUI Button. This function can modify #State variables, without disrupting the rendering phase.
Or you can use the Task Modifier, i.e. calling .task { ... } for a SwiftUI view, which comes probably closest.
Personally, I would not declare to use any task or operation which causes side effects to the AppState or Model within a SwiftUI View's body function. Rather, I prefer to send actions (aka "Intent", "Event") from the user to a Model or a ViewModel, or a Finite State Automaton. These events then get processed in a pure function, call it "update()", which performs the "logic", and this pure function may declare "Effects". These effects will then be called outside this pure update function, cause there side effects wherever they need to, and return a result which is materialised as an event, which itself gets fed into the pure update function again. That update function produces a "view state", which the view needs to render.
Now, I want to clarify some potential misconceptions:
"Since onAppear trigger only once, even ContentView init multiple times"
onAppear
This can be actually called several times for a view which you identify on the screen as a "view".
Usually, it is not always without issues to utilise onAppear for performing some sort of initialisation or setup. There are approaches to avoid these problem altogether, though.
"ContentView init"
You are better off viewing a SwiftUI View as a "function" (what?)
With that "function" you achieve two things:
Create an underlying view whose responsibility is to render pixels and also create (private) data for this view which it needs to render accordingly.
Modify this data or attributes of this underlying view.
For either action, you have to call the SwiftUI View's initialiser.
When either action is done, the SwiftUI View (a struct!) will diminish again. Usually, the struct's value, the SwiftUI View resides on the stack only temporarily.
Variables declared as #State and friends, are associated to the underlying view which is responsible to render the pixels. Their lifetime is bound to this renderable view which you can perceive on the screen.
Now, looking at your code, it should work as expected. You created a private #State variable for the event handler object. This seems to be the right approach. However, #State is meant as a private variable where a change would cause the view to render differently. Your event handler object is actually an "Any", i.e. a reference. This reference never changes: it will be setup at onAppear then it never changes anymore, except onAppear will be called again for the same underlying renderable view. There is probably a better solution than using #State and onAppear for your event handler object (see below later).
Now, when you want to render the event's value (aka mask as NSEvent.EventTypeMask) then you need another #State variable in your SwiftUI View of this type, which you set/update in the notification handler. The variable should be a struct or enum, not a reference!
SwiftUI then notifies the changes to this variable and in turn will call the body function where you explicitly render this value. Note, that you can update a #State variable from any thread.
Problems
According the documentation "You must call removeMonitor(_:) to stop the monitor."
Unfortunately, your #State variable which holds the reference to the event handler object will not call removeMonitor(_:) when the underlying renderable view gets deallocated.
Bummer!
What you have to do is, changing your design. What you need to do is to introduce a "Model" which is an ObservableObject. It should publish a value (a representation of what you receive in the notification handler) which will be rendered in the SwiftUI view accordingly.
This Model should also receive an event (say a function will be called for the Model from the SwiftUI view) when the view appears, where the Model then creates the event handler object, unless it has been created already (which completely solves your onAppear issues). Alternatively, just create the event handler once and only once in the Model's initialiser - which is arguable the better solution.
When the event handler's notification handler will be called, you update the published value of your Model accordingly.
Integrating the Model - an ObservableObject - properly into a SwiftUI view is a standard pattern in SwiftUI. Please look for help on SO, if you are uncertain how to accomplish this.
Now, since the Model is a class value, you can ensure to call removeMonitor(_:) in its deinit function.
Headstart
import SwiftUI
final class EventHandlerModel: ObservableObject {
private var monitor: Any!
#Published private(set) var viewState: String = ""
init() {
monitor = NSEvent.addLocalMonitorForEvents(
matching: .keyDown
) { event in
assert(Thread.isMainThread)
self.viewState = "\(event)"
return event
}
}
deinit {
guard let monitor = self.monitor else {
return
}
NSEvent.removeMonitor(monitor)
}
}
struct ContentView: View {
#StateObject private var model = EventHandlerModel()
var body: some View {
Text(verbatim: model.viewState)
}
}
I would like to use a struct instead of a class as a state for my View, and as you may know, ObservableObject is a protocol only classes can conform to.
Do I have to wrap my struct in a ViewModel or some other similar type of object ? What happens if I don't ?
A sample on what that looks like now :
import Foundation
import SwiftUI
struct Object3D {
var x : Int
var y : Int
var z : Int
var visible : Bool
}
struct NumberView<Number : Strideable> : View {
var label : String
#State var number : Number
var body : some View {
HStack {
TextField(
self.label,
value: self.$number,
formatter: NumberFormatter()
)
Stepper("", value: self.$number)
.labelsHidden()
}
}
}
struct ObjectInspector : View {
#State var object : Object3D
var body : some View {
VStack {
Form {
Toggle("Visible", isOn: $object.visible)
}
Divider()
Form {
HStack {
NumberView(label: "X:", number: object.x)
NumberView(label: "Y:", number: object.y)
NumberView(label: "Z:", number: object.z)
}
}
}
.padding()
}
}
struct ObjectInspector_Previews: PreviewProvider {
static var previews: some View {
ObjectInspector(object: Object3D(x: 0, y: 0, z: 0, visible: true))
}
}
You don't have to use #ObservedObject to ensure that updates to your model object are updating your view.
If you want to use a struct as your model object, you can use #State and your view will be updated correctly whenever your #State struct is updated.
There are lots of different property wrappers that you can use to update your SwiftUI views whenever your model object is updated. You can use both value and reference types as your model objects, however, depending on your choice, you have to use different property wrappers.
#State can only be used on value types and #State properties can only be updated from inside the view itself (hence they must be private).
#ObservedObject (and all other ...Object property wrappers, such as #EnvironmentObject and #StateObject) can only be used with classes that conform to ObservableObject. If you want to be able to update your model objects from both inside and outside your view, you must use an ObservableObject conformant type with the appropriate property wrapper, you cannot use #State in this case.
So think about what sources your model objects can be updated from (only from user input captured directly inside your View or from outside the view as well - such as from other views or from the network), whether you need value or reference semantics and make the appropriate choice for your data model accordingly.
For more information on the differences between #ObservedObject and #StateObject, see What is the difference between ObservedObject and StateObject in SwiftUI.
I would like to use a struct instead of a class as a state for my View, and as you may know, ObservableObject is a protocol only classes can conform to.
A model is usually shared among whichever parts of the app need it, so that they're all looking at the same data all the time. For that, you want a reference type (i.e. a class), so that everybody shares a single instance of the model. If you use a value type (i.e. a struct), your model will be copied each time you assign it to something. To make that work, you'd need to copy the updated info back to wherever it belongs whenever you finish updating it, and then arrange for every other part of the app that might use it to get an updated copy. It's usually a whole lot easier and safer to share one instance than to manage that sort of updating.
Do I have to wrap my struct in a ViewModel or some other similar type of object ? What happens if I don't ?
It's your code -- you can do whatever you like. ObservableObject provides a nice mechanism for communicating the fact that your model has changed to other parts of your program. It's not the only possible way to do that, but it's the way that SwiftUI does it, so if you go another route you're going to lose out on a lot of support that's built into SwiftUI.
The View is a struct already, it cannot be a class. It holds the data that SwiftUI diffs to update the actual UIViews and NSViews on screen. It uses the #State and #Binding property wrappers to make it behave like a class, i.e. so when the hierarchy of View structs is recreated they are given back their property values from last time. You can refactor groups of vars into their own testable struct and include mutating funcs.
You usually only need an ObservableObject if you are using Combine, it's part of the Combine framework.
I recommend watching Data Flow through SwiftUI WWDC 2019 for more detail.
Do all #Published variables need to have an initial value in a view model (I am conforming to MVVM) for SwiftUI?
Why can't I just say that I want the #Published variable of type string with no initial value?
So does that mean that I need to have:
If not how can I get around this?
I was thinking about making an init() for the class, but I would still need to input default values when I initialize the class.
Unlike SwiftUI views, which are Structs, Observable Objects are always classes, and in Swift, all classes must have initializers.
A) Consider making your #Published variable an optional
#Published var title: String?
B) Add an init method
init() { self.title = "" }
Else, there's way to not have an initial value for a class' property.
You may find that force unwrapping with "!" will "solve" your problem, but that's a bad practice, don't do it; if you don't have an initial value for your variable, then it must be optional in your case.
But why are you designing a Model, as an observable object, for SwiftUI, consider using simple Structs if you are not intending on persisting (saving to disk), your data, else use Core Data and it's NSManagedObject class, that is already conforming to ObservableObject.
All stored properties should be initialised somehow. You can delay initialization till construction, like
final class ViewModel: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
and later, somewhere in SwiftUI View, create it with value needed in context
ViewModel(title: "some value")
I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.
I want to create MyViewModel which gets data from network and then updates the arrray of results. MyView should subscribe to the $model.results and show List filled with the results.
Unfortunately I get an error about "Type of expression is ambiguous without more context".
How to properly use ForEach for this case?
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var results: [String] = []
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.results = ["Hello", "World", "!!!"]
}
}
}
struct MyView: View {
#ObservedObject var model: MyViewModel
var body: some View {
VStack {
List {
ForEach($model.results) { text in
Text(text)
// ^--- Type of expression is ambiguous without more context
}
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(model: MyViewModel())
}
}
P.S. If I replace the model with #State var results: [String] all works fine, but I need have separate class MyViewModel: ObservableObject for my purposes
The fix
Change your ForEach block to
ForEach(model.results, id: \.self) { text in
Text(text)
}
Explanation
SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text) to Text(text as String) and remove the $ before model.results), is "Generic parameter 'ID' could not be inferred".
In other words, to use ForEach, the elements that you are iterating over need to be uniquely identified in one of two ways.
If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property var id: Hashable. You don't need the id parameter in this case.
The other option is to specifically tell ForEach what to use as a unique identifier using the id parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.
In this case, we chose option 2 and told ForEach to use the String element itself as the identifier (\.self). We can do this since String conforms to the Hashable protocol.
What about the $?
Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:
A Toggle needs to update a Bool value in response to a switch
A Slider needs to update a Double value in response to a slide
A TextField needs to update a String value in response to typing
The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>. So a Toggle requires you to pass it a Binding<Bool>, a Slider requires a Binding<Double>, and a TextField requires a Binding<String>.
This is where the #State property wrapper (or #Published inside of an #ObservedObject) come in. That property wrapper "wraps" the value it contains in a Binding (along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable, but if we need the binding, we can use the shorthand $myVariable.
So, in this case, your original code contained ForEach($model.results). In other words, you were telling the compiler, "Iterate over this Binding<[String]>", but Binding is not a collection you can iterate over. Removing the $ says, "Iterate over this [String]," and Array is a collection you can iterate over.