The use of @state and its limitations were introduced earlier: if the observed is an object, @State cannot update the UI after the data changes have been made. See this article for details. It is for this reason that swiftUI introduced @observedobject and @Published to solve this problem. Let’s see how it works.

The basic use

First, specify a class to implement ObservableObject Protocol

class TestViewModel : ObservableObject {}Copy the code

In this class, the element whose data changes need to update the UI is decorated with @published

class TestViewModel : ObservableObject {
    @Published var name:String
    var age:Int
    
    init(name:String.age:Int) {
        self.name = name
        self.age = age
    }
}
Copy the code

Finally, create this class in the View with the @obServedobject modifier

struct ContentView: View {
    
    @state var user = user (name: "jaychen")
    @ObservedObject var vm = TestViewModel(name: "jaychen", age: 12)
    
    var body: some View {
        
        VStack{
            TextField("input your name", text: $vm.name)
                .frame(width: 200)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            Text("your name is \(vm.name)")
        }.padding()
        
    }
}
Copy the code

The full code can be found in gist.

Since TestViewModel is defined as a class, the VM can share data between views in the form of arguments.

@published is a simple way to use it but how it works has always puzzled me. After browsing a few data, saw a paragraph to let a person suddenly enlightened realization finally. However, before explaining the @Published implementation, we need to take a look at the property wrapper.

Property Wrapper

Here is the detailed Chinese documentation for attribute wrappers: Attribute wrappers. It’s documented, but LET me explain this from my understanding.

What is the property wrapper

A propertyWrapper is also a struct, but the struct needs to be decorated with @propertywrapper to refer to the code in the document. TwelveOrLess is now a propertyWrapper even if it does nothing.

@propertyWrapper
struct TwelveOrLess {}Copy the code

What is the property wrapper used for

A property wrapper is used to wrap a class/struct property.

struct SmallRectangle{
    @TwelveOrLess var height: Int   TwelveOrLess is a TwelveOrLess wrapper that TwelveOrLess is TwelveOrLess.
}
Copy the code

In the code above, height is wrapped by @Twelveorless. After wrapping, all changes to height need to be reviewed by @Twelveorless. In the example of the document, @twelveorless checks to see if the new value is greater than 12 each time height is reassigned.

How to use it

I will try to explain this example in detail based on the documentation example. TwelveOrLess is used to ensure that height is less than 12.

  1. Define the property wrapper first
@propertyWrapper
struct TwelveOrLess {}Copy the code
  1. There needs to be one for each property wrapperwrappedValueProperty, which can be understood as a proxy for the wrapped property. In the case of documents,wrappedValueisheightA surrogate for this property.
@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    
    init(a) { self.number = 0 }
    
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12)}}}Copy the code

When s.eight = 23 is changed to height, the set method of wrappedValue is executed. Get in the same way.

The property wrapper has a project Value property in addition to the wrappedValue property. What does this property do?

An attribute wrapper is also essentially a struct that can have many attributes. We don’t want to write let t = TwelveOrLess(), how do we get other attributes of the attribute wrapper?

This is where projectedValue comes in: It exposes the properties of the property wrapper.

Let’s say I have code

@propertyWrapper
struct LogWrapper {

    var wrappedValue: Int {
        get { 1 }

        set {
            print("set new :\(newValue)")}}// projectedValue can be of any type
    var projectedValue : String {
        get {
            return  "Whatever it is."}}}class Student {
    @LogWrapper var age: Int
    init(age: Int) {
        self.age = age
    }
}


var s = Student(age: 234)
print(s.$age)   // Output: whatever
Copy the code

As in the code above, you can get the projectedValue of the property wrapper just by using the $modifier, and it can be of any type.

Here are some things about attribute wrappers, to sum up:

  1. Property wrappers are anotherstructUsed for packagingclass/structA property of.
  2. After wrapping a property,wrappedValueBecomes a “proxy for the wrapped property”, all pairs of “wrapped property”get/setWill be calledwrappedValueget/set.
  3. The property wrapper has oneprojectedValueProperty, which can expose properties of the property wrapper itself, using$Modify to get.

@Publishedimplementation

@published is a property wrapper itself. How does it decorate a property and then update the UI when the value of the property changes?

We said that wrappedValue is a proxy for the wrapped attribute, so we can notify the UI in the didSet function of wappedValue. Sounds very feasible, let’s try to simulate @published of swiftUI.

We want to be able to use it like swiftUI

class Student {
    @Published var age: Int
    init(age: Int){
        self.age = age
    }
}
var s = Student(age : 12)
s.age = 13  // This is the time to update the UI
Copy the code

So let’s start racing:

@propertyWrapper
struct Published<Value> {

    var wrappedValue: Value {
        didSet {
            mockUpdateUI()
        }
    }
}
Copy the code

We have a few lines of code above, where it actually implements some logic after the age change. (Here we use mockUpdateUI to simulate the swiftUI update).

Further, combine enables the following functions


class Student {
    @Published var age: Int
}
.
student.$age.sink{
    print("age is changed: \ [$0)")}Copy the code

So, how is @published going to do this? If you think about it, the sink execution time is the same as the didSet execution time in wappedValue, we just have to execute the closure that sink passed in the didSet.

So, our code can go one step further

@propertyWrapper
struct Published<Value> {

    var closure: ((Value) - >Void)?
    
    / / here
    var projectedValue: Published {
        get {
            return self}}var wrappedValue: Value {
        didSet {
            if closure ! = nil {
                closure!(self.wrappedValue)
            }
        }
    }

    init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
    }

    func observe(with closure: @escaping (Value) - >Void) {
        closure(wrappedValue)
    }
}


////// Test code
class Student {
    @Published var age: Int

    init(age: Int) {
        self.age = age
    }
}


var s = Student(age: 234)
s.age = 44
s.$age.observe { i in
    print("age is changed: \(i)")}Copy the code

Note that projectedValue returns the entire structure instance of self, so s.$age is the structure itself, and you can call observe directly. The observe argument is a closure that is stored and the closure logic is executed when didSet is called.

The above is the content of @published this time, more content of swiftUI&&Combine will try to keep updating ✊, if you are willing to give me some positive feedback to make me more motivated to update, that would be great