Small knowledge, big challenge! This article is participating in the “Essentials for Programmers” creative activity. This article has participated in the “Digitalstar Project” to win a creative gift package and challenge the creative incentive money

How do we manage our Model in the AGE of UIKit? It relies on UIViewController, which acts as the glue between the data source and the view and acts as a bridge between the data and view. SwiftUI changes this model, using data (state) to drive UI and behavior.

The core idea of SwiftUI

  1. Data access = dependency
  2. Single source of truth

How do you understand that? So let’s look at the definition of View

public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}
Copy the code

The protocol only needs to be a body. We can think of a View as a function, and its return value is a View. The data that determines the contents of the view comes from the parameters of the function, as well as some local variables of its own.

If you know React, you must know that the input parameter is just like the props passed in externally, and the local variables are state and useStateCopy the code

The most intuitive manifestation in SwiftUI is @State and @binding.

@State

The official recommendation is to set the @state modifier value to private, which should belong only to the current view. We find that the View we usually write is also a struct (this does not mean that we cannot use a class; to use a class, the class must be marked as final), which means that the View created is also a value type. A value identified with @state gives SwiftUI control of that value, and assigning this property will trigger a View refresh, its body will be called again, and the underlying rendering engine will find and refresh the changed parts of the interface associated with this value.

But such types are not suitable for passing between objects. Remember that we passed a color value to the SlideView in the previous demo. If we used a pure value type, then we copied the value in the pass, and changes to the value in the SlideView will not apply to the external view.

@Binding

@binding is used to solve this problem. Like @state, @Binding is a modification to an attribute, and what it does is “convert” the attribute of the value semantics to the reference semantics. Assigning to a property declared as @binding changes not the property itself, but its reference, and the change is passed outward. Why add a $before the variable name?

It is called a projection property when $is added to a variable decorated with the @ sign. Some @ attributes, such as @state and @binding here, provide a way to get this projection attribute, and their projection attribute is their own Binding type. The next step is to convert State to reference semantics. Not all variables decorated with @ can do this, such as @observedobject, @environmentobject.

@state Further understanding

What kind of property do we normally declare @State? Basic type, String, Bool, etc., can it be a more complex type? Struct, struct, struct, struct, struct, struct, struct, struct, struct

struct Profile {
    var isRegistered: Bool = false
    var name: String = ""
    
    mutating func register() {
        self.name = "Ray"
        self.isRegistered = true
    }
}

struct StarterView: View {
    @State var profile: Profile
    var body: some View {
        VStack {
            Text("Hi (profile.name)")
            Button(action: {
                self.profile.register()
            }) {
                Text("Register")
            }
        }
    }
}
Copy the code

We have a button on the page, and if we click on it, we’ll see that there’s nothing wrong, and we’ll get the result we want, but if we change the Profile from a struct to a class, we’ll see that when we click on the button, the page doesn’t change at all, so @State should only be used for the token value type. If register is an asynchronous operation, for example:

struct Profile { var isRegistered: Bool = false var name: String = "" mutating func register () {DispatchQueue. Main. AsyncAfter (deadline: now () + 2.0) {self. Name =" Ray "}}}Copy the code

The solution is @observedobject when the compiler reports an error and the struct doesn’t support it.

@ObservedObject

Here, let’s take a simple example, a timer, whose clock needs to be synchronized on this View. For the full code, go to Github.

struct ContentView: View { ... @ObservedObject var counter: TimeCounter ... var body: some View { VStack { HStack { ... MatchingView(rGuess: $rGuess, gGuess: $gGuess, bGuess: $bGuess, counter: $counter.counter) } ... }}}Copy the code

@observedobject identifies an external state, and the code for TimeCounter looks like this:

class TimeCounter: ObservableObject { var timer: Timer? @Published var counter = 0 init() { timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.counter += 1 } } func killTimer() { timer? .invalidate() timer = nil } }Copy the code

TimeCounter needs to implement the ObservableObject protocol. After its internally defined counter is marked @published, users can use it just like @state internally. This pattern reminds us of MVVM. The TimeCounter can also be shared by multiple views as a data source for multiple views.

@EnvironmentObject

EnvironmentObject is a state shared by the entire App. EnvironmentObject is a state shared by all views in the App. First, we define a model that needs to be shared globally:

class UserSettings: ObservableObject {
  @Published var score = 0
}
Copy the code

Second, the globally shared Model is instantiated and injected when the root view is initialized.

var settings = UserSettings() 
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(settings))
Copy the code

Finally, we can reference any View with @environmentobject Var Settings.

struct ContentView: View {
  @EnvironmentObject var settings: UserSettings
  var body: some View {
    VStack {
      Button(action: { self.settings.score += 1 }) {
        Text("Current Score: (self.settings.score)")
      }
    }
  }
}
Copy the code

UIHostingController(rootView: ContentView().environmentobject (Settings).environmentobject (profile)) system also provides us with a number of environment variables

Available (iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension EnvironmentValues { /// The default font of this environment. public var font: Font? /// The display scale of this environment. public var displayScale: CGFloat /// The image scale for this environment. @available(OSX, unavailable) public var imageScale: Image.Scale /// The size of a pixel on the screen. Equal to 1 / displayScale. public var pixelLength: CGFloat { get } /// The accessibility bold text setting. public var legibilityWeight: LegibilityWeight? /// The current locale that views should use. public var locale: Locale /// The current calendar that views should use when handling dates. public var calendar: Calendar /// The current time zone that views should use when handling dates. public var timeZone: TimeZone /// The color scheme of this environment. /// /// If you're writing custom drawing code that depends on the current color /// scheme, you should also consider the `colorSchemeContrast` property. /// You can specify images and colors in asset catalogs ///  according to either the `light` or `dark` color scheme, as well as /// standard or increased contrast. The correct image or color displays /// automatically for the current environment. /// /// You only need to check `colorScheme` and `colorSchemeContrast` for /// custom drawing if the differences go beyond images and colors. public var colorScheme: ColorScheme /// The contrast associated with the color scheme of this environment. public var colorSchemeContrast: ColorSchemeContrast { get } }Copy the code

It just needs to be used

struct StarterView {
    @EnvironmentObject var profile: Profile
    @Environment(.locale) var locale: Locale
    ...
}
Copy the code

SwiftUI provides us with a dependency injection framework that is powerful when used properly.