preface

If you want to get started with SwiftUI, Apple’s official SwiftUI tutorial will definitely help. This tutorial provides very detailed steps and instructions, and the interactive web pages are first class, making it a worthwhile reference to look at and learn by yourself.

However, there are some notable details in SwiftUI that are not covered in too much detail in the tutorial and can cause some confusion. This article is a supplement to some parts of the tutorial from my personal point of view. I hope it will help you when you follow the tutorial to learn SwiftUI. The recommended way to read this article is to use the SwiftUI tutorial for hands-on implementation, and use this article to deepen your understanding when you arrive at the corresponding steps. I’ve highlighted the tutorial chapters and links below for your reference.

Before we start learning SwiftUI, we need to get a sense of why we need a new UI framework. The original | address

Why SwiftUI

UIKit’s challenges

For Swift developers, the most notable feature of WWDC 19 Keynote and Platforms State of the Union was the SwiftUI announcement. UIKit has been with iOS developers for nearly a decade since iOS SDK 2.0. UIKit inherits the ideas of mature AppKit and MVC, and when it first came out, it provided a good learning curve for iOS developers.

UIKit provides an intuitive, command-and-control flow-based approach to programming. The main idea is to build the user interface and logic by ensuring that the corresponding methods (such as viewDidLoad or a target-action, etc.) are called correctly during the View or View Controller lifecycle and user interaction. However, UIKit faces huge challenges, both in terms of ease of use and stability. I’m a bit of an iOS developer myself, but “falling into the UIKit pit” is pretty much my daily routine. The basic idea of UIKit is that the View Controller does most of the work, it needs to coordinate the Model, the View, and the user interaction. This leads to a huge side effect and a large number of states that, if not properly placed, will get mixed up in the View Controller and act on the View or logic at the same time, making state management more complex and ultimately unmaintainable and causing the project to fail. It’s not just the code that we write ourselves as developers, UIKit itself is often plagued with mutable state, with all sorts of weird bugs.

Declarative interface development

In recent years, with the development of programming technology and ideas, the use of declarative or functional interface development has been more and more accepted and gradually become the mainstream. The original idea probably came from Elm, which was later adopted by React and Flutter. SwiftUI is almost the same as them. In summary, these UI frameworks follow the following steps and principles:

  1. Use individual DSLS that describe “what the UI should look like” rather than lines of code that tell you “how to build the UI”.

    For example, with traditional UIKit, we would add a “Hello World” tag with code like this, which is responsible for “creating label”, “setting text”, and “adding it to view” :

    func viewDidLoad() { super.viewDidLoad() let label = UILabel() label.text = "Hello World" view.addSubview(label) // Omitted layout code}Copy the code

    In contrast, with SwiftUI we just need to tell the SDK that we need a text tag:

    var body: some View {
      Text("Hello World")
    }
    Copy the code
  2. Next, the framework internally reads the declarations of these views and is responsible for rendering them in the appropriate way.

    Note that the declarations of these views are descriptions of pure data structures, not the actual views displayed, so the creation and comparison of these structures does not incur much performance penalty. Rendering descriptive language is the slowest part, and the framework will do it for us in a black box.

  3. If the View needs to change based on a state, we store that state in a variable and use it when declaring the View:

    @State var name: String = "Tom"
    var body: some View {
        Text("Hello (name)")
    }
    Copy the code

    The details of the code can be ignored for now, but we’ll explain more about this later.

  4. When the state changes, the framework calls the declaration part of the code again, calculates the new view declaration, and differentiates it from the original view. After that, the framework is responsible for efficiently redrawing the changed part.

SwiftUI’s idea is exactly the same, and it’s all about these steps. Using descriptive development greatly reduces the chance of problems at the app developer level.

Some detail interpretation

The concept of declarative UI programming is well demonstrated in the official tutorial. There are also a number of new Swift 5.1 features in SwiftUI that will be “refreshing” for those who are used to Swift 4 or 5. Next, I’ll explain and explore some of the official tutorials in several topics.

Tutorial 1 – Creating and Combining Views

Section 1-Step 3: SwiftUI App startup

One of the first things I was curious about when I created the SwiftUI app was how it started.

The tutorial example app through the application in the AppDelegate (_ : configurationForConnecting: options) returns a called “Default Configuration” UISceneConfiguration instances:

func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
Copy the code

The name of the Configuration in the Info. Plist “UIApplicationSceneManifest – > UISceneConfigurations” are defined, Specifies the Scene Session Delegate class as $(PRODUCT_MODULE_NAME).scenedelegate. This section is the new way to manage the app life cycle through Scene in iOS 13, and the code required for multi-window support. That’s not our topic today. After app startup, control is transferred to SceneDelegate, whose scene(_:willConnectTo:options:) will be called for UI configuration:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions)
    {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(rootView: ContentView())
        self.window = window
        window.makeKeyAndVisible()
    }
Copy the code

This is the standard iOS app startup process. UIHostingController is a UIViewController subclass that is responsible for taking a SwiftUI View description and rendering it with UIKit (in iOS). UIHostingController is a common UIViewController, so it is completely possible to integrate the interface created by SwiftUI into the existing UIKit app bit by bit, without building swiftui-based apps from scratch.

Since the Swift ABI is stable, SwiftUI is a Swift framework that runs on users’ iOS systems. So the lowest supported version is iOS 13, and you might want to wait a year or two to use it in an actual project.

Section 1-step 4: Some Views

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

Some may seem unfamiliar at first glance, but to illustrate this, let’s start with View.

View is one of the core protocols of SwiftUI and represents a description of an on-screen element. This protocol contains an AssociatedType:

public protocol View : _View {
    associatedtype Body : View
    var body: Self.Body { get }
}
Copy the code

This protocol with associatedType cannot be used as a type, but only as a type constraint:

// Error
func createView() -> View {

}

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

As a result, we can’t write code like this:

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

For Swift to help automatically infer the type of view. Body, we need to explicitly indicate the actual type of Body. In this case, the actual type of body is Text:

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

Of course we can specify the body type explicitly, but this causes some problems:

  1. Every changebodyWe need to manually change the corresponding type.
  2. Create a newView“We all need to think about what type it will be.
  3. We only care if we return oneViewI’m not really interested in what type it is.

Some View uses Opaque return types of Swift 5.1. It guarantees the compiler that every time the body returns a certain, view-compliant type, the compiler is told not to bother with the specific type. This condition is important because, for example, the following code fails:

let someCondition: Bool // Error: Function declares an opaque return type, // but the return statements in its body do not have // matching underlying types. var body: Some View {if someCondition {// this branch returns Text return Text("Hello World")} else {// this branch returns Button, Return Button(action: {}) {Text("Tap me")}}Copy the code

This is a compile-time feature that uses some to erase specific types while preserving the functionality of the AssociatedType Protocol. This feature is used on SwiftUI to simplify writing and to make different View declarations syntactically uniform.

Section 2-Step 1: Preview SwiftUI

SwiftUI Preview is Apple’s development tool for Hot Reloading RN or Flutter. Apple chose to render directly on macOS this time due to the painful performance lessons of IBDesignable and the cross-Apple platform nature of SwiftUI via UIKit. Therefore, you need to use macOS 10.15 with Swiftui. Framework to see the Xcode Previews interface.

Xcode will statically analyze the code (thanks to the SwiftSyntax framework) and find all types that comply with the PreviewProvider protocol for preview rendering. In addition, you can provide the right data for these previews, which can even allow the entire interface development process to proceed without actually running the app.

The author tries to find out by himself that this mode of development brings more efficiency improvement than Hot Reloading. Hot Reloading requires you to have an overview of the interface and prepare the corresponding data, then run the app, stop at the interface you want to develop, and make adjustments. You also need the Restart app to react if the data state changes. SwiftUI’s Preview is far more efficient than SwiftUI’s Preview, which does not require the app to run and provides any dummy data.

After just one day of use, the Option + Command + P shortcut for refresh Preview is already deep in my muscle memory.

Section 3 – Step 5: About ViewBuilder

The syntax for creating a Stack is interesting:

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
}
Copy the code

At first it looks as if we’re giving two texts that form an array-like [View], but that’s not really the case. Here we call the VStack initialization method:

public struct VStack<Content> where Content : View {
    init(
        alignment: HorizontalAlignment = .center, 
        spacing: Length? = nil, 
        content: () -> Content)
}
Copy the code

The previous alignment and spacing are useless, but the last content is interesting. If you look at the signature, it’s a () -> Content type, but the code we provided when we created the VStack simply lists two texts without actually returning a usable Content.

Another new feature of Swift 5.1 is used here: Funtion Builders. If you actually look at the signature of the VStack initialization method, you’ll see that content actually has an @ViewBuilder tag in front of it:

init(
    alignment: HorizontalAlignment = .center, 
    spacing: Length? = nil, 
    @ViewBuilder content: () -> Content)
Copy the code

ViewBuilder is a struct marked by @_functionBuilder:

@_functionBuilder public struct ViewBuilder { /* */ }
Copy the code

Types tagged with @_functionBuilder (ViewBuilder here) can be used to tag other content (@ViewBuilder for content here). After the ViewBuilder tag is marked with Function Builder, the content input function will be built according to the appropriate buildBlock in the ViewBuilder before being used. If you read the documentation for ViewBuilder, you’ll see that there are a number of buildBlock methods that take a different number of arguments and convert the Text and other possible views listed in the closure into a TupleView and return it. Thus, the content signature () -> content can be satisfied.

In fact, the code that builds the VStack is converted to something like this:

// Equivalent pseudocode, cannot actually compile. VStack(alignment: .leading) { viewBuilder -> Content in let text1 = Text("Turtle Rock").font(.title) let text2 = Text("Joshua Tree National Park").font(.subheadline) return viewBuilder.buildBlock(text1, text2) }Copy the code

Of course, this funtion Builder based approach has certain limitations. ViewBuilder, for example, only implements buildBlock with up to ten arguments, so if you put more than ten Views in a VStack, the compiler will not be too happy. However, for normal UI builds, ten parameters should be enough. If that doesn’t work, you can also consider using TupleView directly to merge views as a pluralist group:

TupleView<(Text, Text)>(
    (Text("Hello"), Text("Hello"))
)
Copy the code

In addition to buildBlock, which accepts and builds views sequentially, the ViewBuilder implements two special methods: buildEither and buildIf. They correspond to the block if… Else syntax and if syntax. That is, you can write code like this in VStack:

var someCondition: Bool

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
    if someCondition {
        Text("Condition")
    } else {
        Text("Not Condition")
    }
}
Copy the code

Other imperative code is not accepted in the VStack’s Content closure, nor the following:

VStack(alignment: .leading) {// let someCondition = model. Condition if someCondition {// let someCondition = model. Text("Condition") } else { Text("Not Condition") } }Copy the code

So far, only the following three notations are acceptable (with the possibility of others emerging as SwiftUI evolves) :

  • The results forViewThe statement of
  • ifstatements
  • if... else...statements

Section 4-Step 7: Chain calls modify View properties

At this point in the tutorial, you’ve got a sense of SwiftUI’s expressive power.

var body: some View {
    Image("turtlerock")
        .clipShape(Circle())
        .overlay(
            Circle().stroke(Color.white, lineWidth: 4))
        .shadow(radius: 10)
}
Copy the code

Just think about how hard it would be to do this in UIKit. I can probably guarantee that 99% of developers have a hard time doing this without using documentation or copy paste, but SwiftUI is pretty darn easy. After the View is created, it can be converted into an object containing the changed content using a chain call. It’s kind of abstract, but let’s look at a concrete example. For example, to simplify the above code:

let image: Image = Image("turtlerock")
let modified: _ModifiedContent<Image, _ShadowEffect> = image.shadow(radius: 10)
Copy the code

Image With a. Shadow modifier, the modified variable type is changed to _ModifiedContent< image, _ShadowEffect>. If you look at the definition of shadow on View, it looks like this:

Opacity: opacity = opacity (.srgblinear, white: 0, opacity: 0.33); opacity: opacity = opacity (.srgblinear, white: 0); Length = 0, y: Length = 0) -> Self.Modified<_ShadowEffect> }Copy the code

Struct Image: View Modified is a TypeAlias on the View. In the struct Image: View implementation, we have:

public typealias Modified<T> = _ModifiedContent<Self, T>
Copy the code

_ModifiedContent is a SwiftUI private type that stores the content to be changed and the Modifier used to implement the change:

struct _ModifiedContent<Content, Modifier> {
    var content: Content
    var modifier: Modifier
}
Copy the code

_ModifiedContent will also adhere to View if Content complies with ViewModifier and Modifier complies with ViewModifier, This is the basis on which we can make chained calls through View’s various Extensions:

extension _ModifiedContent : _View 
    where Content : View, Modifier : ViewModifier 
{
}
Copy the code

In the shadow case, SwiftUI uses the _ShadowEffect ViewModifier internally and places the Image itself and the _ShadowEffect instance in the _ModifiedContent. Either image or modifier is a description of the actual future view, not a direct rendering operation. Before the final render, the ViewModifier body(Content: self.content) -> self.body will be called to give the various attributes required for the final render layer.

More specifically, _ShadowEffect is a type that satisfies the EnvironmentalModifier agreement, which requires that it resolve itself as a specific modifier depending on the environment before use.

Several other chain calls to modify View properties work in much the same way as Shadow.

summary

Here are some explanations for the first part of the SwiftUI tutorial, and in a future article I will explain some of the interesting parts of the rest of the tutorial.

SwiftUI has often been compared to frameworks like the Flutter. On trial, SwiftUI outperformed the combination of Flutter and Dart in view’s expressiveness and app integration. Swift may be open source, but Apple’s hold on it is undiminished. Many of Swift 5.1’s features are almost tailormade for SwiftUI, and we’ve seen examples of Opaque Return Types and Function Builder in this article. We’ll see more of this in the next few tutorials.

In addition, Apple is behind the use of the Combine. Framework, a responsive programming framework, to drive and bind SwiftUI. Framework, compared to the existing RxSwift/RxCocoa or Active swift solutions. Strong support at the language and compiler levels. If I have the opportunity, I think I will also make some exploration and introduction to this aspect.

Sharing:Download iOS related materials