preface

SwiftUI

Write less code, build a better app.

SwiftUI is an innovative, concise way to program and build user interfaces across all Apple platforms using the power of Swift. With it, you can create a user interface for any Apple device with a single set of tools and apis. SwiftUI uses the easy-to-understand, naturally written declarative Swift syntax and seamlessly supports the new Xcode design tools to keep your code highly in sync with your design. SwiftUI has native support for ‘dynamic fonts’,’ Dark mode ‘, localization and accessibility — the first line of SwiftUI code you write is already the most powerful UI code you’ll ever write. Developer.apple.com/xcode/swift…

Creating and Combining Views

aboutsome View

Create a new SwiftUI project and the following code will appear: A Text is displayed in the body.

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}
Copy the code

You might be unfamiliar with some, so let’s start with View.

It can be seen from the document that View is a SwiftUI protocol, which contains an associatedType:

Available (iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 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 /// The content and behavior of the view. @ViewBuilder var body: Self.Body { get } }Copy the code

A protocol with this modifier cannot be used as a type, only as a constraint. Some View embellishments assure the compiler that each closure must return a certain, view-compliant type, regardless of which type it is.

// Error
func createView() -> View {
}

// Correct
func createView<T: View>() -> T {
}
Copy the code

This notation uses the Opaque return Types feature of Swift 5.1. This design provides developers with a flexible development model, eliminating specific types, eliminating the need to modify the public API to determine the return type of each closure, and making it easier to write code.

Preview SwiftUI

SwiftUI includes Apple’s Hot Reloading tool for React Native or Flutter. Xcode will statically analyze the code to find all types that comply with the PreviewProvider protocol for preview rendering. After trial, Preview SwiftUI does not need to run the app to view the real-time Preview, unlike other Hot Reloading, which may need to restart the app or enter the corresponding interface to adjust data for debugging.

Shortcut keys: Option + Command + P

aboutViewBuilder

Let’s start with the simplest UI:

We created a landscape layout componentHstack, which contains two text componentsText. The problem is these twoTextNot as an arraycontentSo why does this notation work? Let’s take a look atHstackInitialization method:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct HStack<Content> : View where Content : View {

    /// Creates a horizontal stack with the given spacing and vertical alignment.
    ///
    /// - Parameters:
    ///   - alignment: The guide for aligning the subviews in this stack. This
    ///     guide has the same vertical screen coordinate for every child view.
    ///   - spacing: The distance between adjacent subviews, or `nil` if you
    ///     want the stack to choose a default distance for each pair of
    ///     subviews.
    ///   - content: A view builder that creates the content of this stack.
    @inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

    /// 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.
    public typealias Body = Never
}
Copy the code

Looking at the last parameter content, only one () -> content type is returned, but we only listed two Text when we created it and did not return a usable content.

Here we use another Swift 5.1 feature, Function Builder, and notice the @ViewBuilder tag in front of the Content.

Available (iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @_functionBuilder public struct ViewBuilder { /// Builds an empty view from a block containing no statements. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view through unmodified. /// /// An example of a single view written as a child view is /// `{ Text("Hello") }`. public static func buildBlock<Content>(_ content: Content) -> Content where Content : View }Copy the code

Types tagged by @_functionBuilder can be used to tag other types, so in this structure, @ViewBuilder can tag content, So tagged content is built according to the appropriate buildBlock in ViewBuilder.

It’s interesting to see how many buildblocks we find in the documentation:

@available(iOS 13.0, macOS 13.0, tvOS 13.0, watchOS 6.0, *) Extension ViewBuilder {public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0 : View, C1 : View, C2 : View, C3 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View} @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View }Copy the code

In fact, the sample code I just wrote looks like this:

And based onfunction builderThere are certain limitations to the construction of the10A parameter.

Building Lists and Navigation

List

The simplest way to do this is to create a static List like this:

Here,ListandHStackorVStackSimilarly, take a View Builder and adopt itView DSLSeveral ways are listedRow. In this way, the corresponding sigma is constructedUITableViewThe staticcellIs organized.

Available (iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension List where SelectionValue == Never { /// Creates a list with the given content. /// /// - Parameter content: The content of the list. public init(@ViewBuilder content: () -> Content) }Copy the code

Let’s also look at the List initializer, as well as some other dynamic initializers:

    public init<RowContent>(_ data: Range<Int>, @ViewBuilder rowContent: @escaping (Int) -> RowContent) where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent : View
Copy the code

NavigationView

NavigationView can be used to jump to the page:

Handling User Input

@State

It is important to refresh data in a timely manner on the front-end interface. Generally, the data source is updated, and the interface and UI are updated at the same time. In SwiftUI, any state, content, or layout declared in a view is automatically updated whenever the source changes, so only one layout is required. Add the @state keyword in front of the property to realize the effect of dynamic UI update every time the data changes.

This example passes@StateManaging private StatevalueAnd drive the display. Clicking different buttons will result in + 1 and -1, respectively, and the number displayed will change accordingly.

Let’s look at the State in the document:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
    public init(wrappedValue value: Value)
    public init(initialValue value: Value)
    public var wrappedValue: Value { get nonmutating set }
    public var projectedValue: Binding<Value> { get }
}
Copy the code

Notice that State is a structure wrapped in @propertyWrapper. @propertywrapper is a propertyWrapper, @state Int is not the same as Int. Property Wrapper does something like this:

  1. Store variables for the underlyingState<Int>Automatically provide a setgettersetterMethod, the structure is preserved in vivoIntThe specific value of;
  2. Before the body is evaluated for the first time, theState<Int>Associate to currentView, for it corresponds to the current in the heapViewAllocate a storage location.
  3. for@StateModified variable set to observe when the value changes, triggering a new onebodyEvaluate and refresh the UI.

Stateinternal

The interior of State is as follows:

struct State<Value> : DynamicProperty { var _value: Value var _location: StoredLocation<Value>? var _graph: ViewGraph? Var wrappedValue: Value {get {_value} set {updateValue(newValue)}} func _linkToGraph(graph: ViewGraph) { if _location == nil { _location = graph.getLocation(self) } if _location == nil { _location = graph.createAndStore(self) } _graph = graph } func _renderView(_ value: Value) {if let graph = _graph {// Valid State Value _value = Value graph.triggerRender(self)}}Copy the code

The @state declaration brings an automatically generated private storage property in the current View to store the real State struct value. As follows:

struct DetailView: View { @State private var value: Int? private var _value: State<Int? > // automatically generated //... }Copy the code

So if you declare @state var value: Int you can’t compile because Int? When initialized, the declaration will default to nil, leaving _value initialized (State
>(_value: nil, _location: nil)); Values that are not Optional need to be explicitly initialized, otherwise the underlying _value will not be initialized when self.value is called. The @state setting only works after the View is added to the graph (before the body is evaluated for the first time).

State, Binding, StateObject, ObservedObject

The @stateObject situation is similar to @State: Views have ownership of the State, and they are not reinitialized with a new View init. This behavior is the opposite of Binding and ObservedObject: Using Binding and ObservedObject means that the View is not responsible for the underlying storage, and the developer has to decide and maintain the declaration cycle for “non-all” states.

So if the subView wants to use the value of the contentView and feed it back to the contentView in a timely manner, using @binding is unambiguous:

To be continued…