directory

  • @state, @Published, @observedobject, etc
  • Categorize by projectedValue
  • Schrodinger’s @State
  • Ghostly status updates

translation
Nalexn. Making. IO/stranger – th…


For more content, please follow the public account “Swift Garden”.


Like articles? How about a πŸ”ΊπŸ’›βž• triple? Follow the column, follow me πŸš€πŸš€πŸš€

Like many developers, I got to know SwiftUI from apple’s great official tutorials. However, this opening pose also gave me the false impression that SwiftUI is extremely easy to learn.

Since then, I’ve been fascinated by SwiftUI’s many fun and technical themes. It can be challenging to figure them out quickly. Even some experienced developers say the process of learning SwiftUI is like learning everything from scratch.

In this post, I’ve gathered some of the aspects of SwiftUI that I personally find most confusing related to status management. In a manner of speaking, I could have saved myself hours of painful problem-solving if I had this article in hand.

Let’s get started!

@state, @Published, @observedobject, etc

At first, my understanding of these @Something attributes was that they were a new set of language attributes, like Weak or lazy, that were introduced specifically for SwiftUI.

So I was quickly confused by the variety of variations these new “keywords” could produce based on prefixes:

Value, $value, and _value represent three completely different things!

I don’t know, these @Things are just structures in the SwiftUI framework, not part of the Swift language.

What is really part of the language is a new feature introduced in Swift 5.1: the attribute wrapper.

After reading the documentation on property wrappers, it dawns on me that there are no secrets behind @state or @Published. It is these wrappers that give “superpowers” to the original variables, such as the immutable and deformable nature of @state and the responsive nature of @Published.

Are you more confused after hearing this? Don’t worry — I’ll explain it to you right away.

The full picture is pretty clear: When we attribute a variable with @something in SwiftUI, such as @state var value: Int = 0, the Swift compiler generates three variables for us! (Two of these are computed attributes) :

Value – the raw value wrapped by our declaration type (wrappedValue), such as Int in this example.

$value – an “extra” projectedValue whose type is determined by the property wrapper we use. The projectedValue of @state is of type Binding, so in our example it is of type Binding.

_value — a reference to the property wrapper itself, which may be used during view initialization:

struct MyView: View {@Binding var flag: Bool

    init(flag: Binding<Bool>) {
        self._flag = flag
    }
}Copy the code

Categorize by projectedValue

Let’s take a look at the most common @things in SwiftUI and see what their project Values are:

  • @StateBinding<Value>
  • @BindingBinding<Value>
  • @ObservedObjectBinding<Value> (*)
  • @EnvironmentObjectBinding<Value> (*)
  • @PublishedPublisher<Value, Never>

Technically, (*) gives us the intermediate value of the Wrapper type, which becomes a Binding once we specify the keyPath for the actual value in the object.

As you can see, most of the property wrappers in SwiftUI are related to the state of a view and are projected as Binding for passing state between views.

The only one that differs from most wrappers is @Published, but please note:

  1. It is stated in the Combine framework rather than SwiftUI
  2. Its purpose is to make the value observable
  3. It’s not used for view variable declarations, it’s used forObservableObjectInternal.

Consider a fairly common scenario in SwiftUI: Declare an ObservableObject and use it in a view with the @observeDobject property:

class ViewModel: ObservableObject {@Published var value: Int = 0
}

struct MyView: View {@ObservedObject var viewModel = ViewModel(a)var body: some View{... }}Copy the code

MyView can refer to both $viewModel.value and viewModel.$value — both expressions are legal. A little confused, huh?

These two expressions actually represent two completely different types: Binding and Publisher.

Both have practical uses:

var body: some View {
    OtherView(binding: $viewModel.value)     // Binding
        .onReceive(viewModel.$value) { value // Publisher
            // Do something that does not require view updates}}Copy the code

Schrodinger’s @State

We all know that a struct inside an immutable struct is immutable.

In SwiftUI, most of the time we’re faced with an unmodifiable self, for example, in a Button callback. Based on this context, every instance variable, including the @state structure, is also immutable.

So, can you explain why the following code is perfectly legal?

struct MyView: View {@State var counter: Int = 0

    var body: some View {
        Button(action: {
            self.counter += 1 // Modify an immutable structure!
        }, label: { Text("Tap me!")}}}Copy the code

Even though it’s an immutable structure, we can still change its value. What’s the magic of @state?

There is a detailed explanation of how SwiftUI handles value changes in this scenario, but HERE I want to highlight the fact that SwiftUI uses hidden external storage for the actual value of the @state variable.

@state is actually a proxy: it has an internal variable, _location, for accessing external storage.

Let me start you off with an interview question: What does this example print out?

func test(a) {
    var view = MyView()
    view.counter = 10
    print("\(view.counter)")}Copy the code

The above code is fairly straightforward; Our intuition tells us that the printed value should be 10.

It’s not — the output is 0.

The catch is that the view is not always connected to the state store: SwiftUI closes the connection when the view needs to be redrawn or when the view receives a callback from SwiftUI, and then disconnects it later.

At the same time, changes to State in dispatchQueue.main.async will not guarantee success: they may work some of the time. But if you introduce a delay and the storage connection is already disconnected by the time the closure is executed, the state change will not take effect.

Traditional asynchronous distribution is not safe for SwiftUI views — don’t get burned.

Ghostly status updates

After years of using RxSwift and ReactiveSwift, I took it for granted that the data flow would be connected to the view’s properties through a responsive binding.

But when I tried to put SwiftUI and Combine together, I was shocked.

The two frameworks are quite heterogeneous: one cannot easily connect a Publisher to a Binding, or convert a CurrentValueSubject to an ObservableObject.

There are only a few ways the two frameworks can interoperate.

The first point of contact is ObservableObject — a protocol declared in Combine, but already widely used for SwiftUI views.

The second is the.onReceive() view modifier, which is the only API that lets you connect views to any data.

My next big question is related to this modifier. Take a look at this example:

struct MyView: View {

    let publisher: AnyPublisher<String.Never>

    @State var text: String = ""
    @State var didAppear: Bool = false

    var body: some View {
        Text(text)
            .onAppear { self.didAppear = true }
            .onReceive(publisher) {
                print("onReceive")
                self.text = $0}}}Copy the code

This is a view that just displays the string produced by Publisher, and sets the didAppear tag when the view appears on the screen, and that’s it.

Now, try answering me, how many times do you think print(“onReceive”) will be triggered in these two use cases?

struct TestView: View {

    let publisher = PassthroughSubject<String.Never> ()/ / 1
    let publisher = CurrentValueSubject<String.Never> ("") / / 2

    var body: some View {
        MyView(publisher: publisher.eraseToAnyPublisher())
    }
}Copy the code

Let’s consider PassthroughSubject first.

If your answer is zero, congratulations, you’re right. The PassthroughSubject never receives any values, so nothing is submitted to the onReceive closure.

The second use case is a bit deceptive. Please be careful and analyze it carefully.

When an attempt is created, the onReceive modifier will subscribe to Publisher, providing an unlimited number of values “required” (see the description in Combine).

Since CurrentValueSubject has an initial value “”, it immediately pushes the value to its new subscriber, triggering the onReceive callback.

Then, when the view is about to be displayed on the screen for the first time, SwiftUI calls its onAppear callback, which, in our case, changes the state of the view by setting didAppear to true.

So what happens next? You guessed it! The onReceive closure is called again! Why is that?

When MyView changes the state in onAppear, SwiftUI needs to create a new view to compare with the view before the state change! These are the steps required to properly patch the view hierarchy.

Since the view of the second creation process is also subscribed to Publisher, the latter is happy to push its values again.

The correct answer is 2.

Can you imagine my confusion as I tried to debug these ghostly update calls that were passed to onReceive? As I try to filter out these repeated update calls, my forehead is covered with question marks.

One final quiz: If we set self.text = “ABC” in onAppear, what text will be displayed at the end?

If you didn’t know the story above, the logical answer would be “ABC,” but as you’ve upgraded yourself with new knowledge: Whenever and wherever you assign a value to text, the onReceive callback follows, erasing the value you just assigned with the value of CurrentValueSubject.


My official account here is Swift and computer programming related articles, as well as excellent foreign article translation, welcome to follow ~