Hello, SwiftUI
Apple officially launched Project Catalyst (formerly Marzipan) on WWDC19, which allows developers to port iPadOS apps to macOS. SwiftUI was also unveiled, officially unifying Apple’s UI development solution across all platforms. Just a few days ago, Google unveiled Jetpack Compose at its I/O conference, a new Android native UI development framework that marks the full embrace of declarative UI development by both mobile operating system camps.
The past and present of declarative UI
In fact, declarative UI is not a new technology, as early as 2006, Microsoft has released its new generation of interface development framework WPF, which uses XAML markup language, supports bidirectional data binding, reusable templates and other features.
In 2010, the Qt team, led by Nokia, released its next-generation interface solution, Qt Quick, which was also Declarative. Qt Quick was originally named Qt Declarative. QML also supports data binding, modularity, and built-in JavaScript, allowing developers to create simple, interactive prototypes using only QML.
Declarative UI frameworks have exploded in recent years, culminating in Web development. React laid a solid foundation for declarative UI and will continue to lead its future development. The subsequent release of Flutter brings the idea of declarative UI to mobile development…
What exactly is a declarative
Imagine we want to implement the following interface:
Turn on the switch so that the label below shows on, and vice versa. If we want to implement in a non-declarative way, i.e., imperative, then we need:
- To create a
UISwitch
And sets its change event handler - To create a
UILabel
- To create a
UIStackView
, set the direction to vertical - Add the two views created in 1 and 2 to
UIStackView
中 - The change event reads the current state of the switch and sets the corresponding string to the label
We can still do this with one state, but as the application gets more complex, the states become more and more complex, and the order in which the states change can even affect the correctness of the application logic, because every event we do is an incremental change to the interface. Once the first state has an error, it’s going to add errors, and then multiple threads are going to mix in, and boom, your application is going to crash.
Declarative means that we describe what interface we want, rather than telling the computer what to do step by step. So the above example looks like this in declarative form:
“I need an interface, which is a VStack (vertical layout), with a switch in it, whose value is bound to the Boolean value of the switchValue, and a Text in the VStack, whose value is foo when The switchValue is true, or bar otherwise.”
We can see that there are no commands in the whole text, but only descriptions of what the interface is like. SwitchValue is called “The Source of Truth”, and The state of Toggle and Text content are bound to it. When the state changes, the interface is re-” rendered “as described previously to get an interface with absolutely correct state. This is where the declarative advantage lies, reducing the complexity of interface maintenance as state increases.
Similarities and differences between SwiftUI and other frameworks
SwiftUI’s relationship with React and Flutter has been discussed all over the web since its debut. After these two days of research, I would like to briefly talk about my views: (Disclaimer: I have not read the source code, nor participated in the on-site Lab, everything is my personal idea)
The idea of Flutter starts from 0, that is, the language, the base library, the rendering engine, the layout engine, and the framework itself are all implemented by itself. The rendering engine Skia only needs the operating system to provide a GL Context to complete the rendering of all graphics. This makes Flutter very cross-platform. So far, Windows, Linux, macOS, and Fuchsia have all been officially supported by Flutter.
In my opinion, there are pros and cons to this approach. First of all, all platforms have the same behavior, including scrolling views, Material Design controls and blur effects, which are not available on other platforms. Developers do not need to adapt between platforms. There are drawbacks, of course. The Flutter approach is similar to that of a game engine. It does not use any of the UI features provided by the platform, so it is not easy to interact with the native View.
Unlike Flutter, SwiftUI didn’t go back to the drawing board. This new framework still uses UIKit, AppKit, etc as its foundation. But it’s not a declarative wrapper around UIKit, as you can see from Xcode’s debug view:
Many basic components, such as Text, Button is not used directly UILabel, UIButton but a named DisplayList ViewUpdater. Platform. CGDrawingView UIView subclass. They use custom drawing, but are hosted in a UIKit environment, so I guess SwiftUI only provides a custom rendering and layout engine for components, and the underlying technologies are Core Animation, Core Graphics, Core Text, etc. Using custom drawing to implement components can be understood as cross-platform convenience, after all, a button to distinguish between UIButton and NSButton is a bit of a hassle to implement. However, some complex controls still use existing CLASSES in UIKit, such as UISwitch. Since it’s not out of UIKit, embedding a UIView is very easy. You don’t need any external textures because the Flutter context is the same and the coordinate system is the same.
Therefore, I think SwiftUI is more similar to React Native. It uses components provided by the system framework, but the drawing and layout can be realized by itself. There are related frameworks such as Yoga and ComponentKit before SwiftUI.
SwiftUI’s type system
The type systems of Flutter and React are not strongly constrained. Having one Text in an interface is the same as having two Text types. React uses JavaScript and is even less typed. SwiftUI differs from them in that it uses strong type constraints. Here’s an example:
VStack {
Text("Hello")}Copy the code
with
VStack {
Text("Hello")
Text("World")}Copy the code
with
VStack {
Text("Hello")
.color(Color.red)
}
Copy the code
The types are different. First of all, this syntax is called Function Builders, and it’s something That Apple smuggled into Swift. SwiftUI basically uses a concrete type, not a protocol type. First, VStack is both a struct and a concrete type. Its constructor accepts a closure. This closure uses the ViewBuilder structure decorated with @functionBuilder as the Builder, so the second code above is converted at compile time to:
VStack {
let v1 = ViewBuilder.buildExpression(Text("Hello"))
let v2 = ViewBuilder.buildExpression(Text("World"))
return ViewBuilder.buildBlock(v1, v2)
}
Copy the code
Then look at the signature of the viewBuilder. buildBlock overload above:
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
Copy the code
So a Text and two Text, their parent container VStack type is different! Also, buildBlock has a maximum of 10 generic parameters:
Highlight: Your view hierarchy (currently) cannot have more than 10 subviews. And the post-compiler error message doesn’t show this at all. Knowing this will save you a lot of time!
Different states have different views, but they are of the same type. What does that mean? That is, no more diff-patch.
Imagine the following scenario:
VStack {
if something {
Text("something is true")}Text("something else")
if! something {Text("something is not true")}}Copy the code
How should the view change when something changes? React and Flutter have no concept of type and can only take two snapshots at a time (one for the current state and one for the new state). They have two options to complete the interface update:
- Remove all old views and add new ones
- Find the differences and modify the view accordingly
The first method is the simplest, but performs poorly and does not save the state of the view itself. The second approach requires efficient algorithmic support and seems to solve our problem, but it is not necessary.
SwiftUI updates the interface according to the type. The type of the code above is:
VStack<TupleView<Text? .Text.Text? >>Copy the code
With the type framework, you can do static optimization, which is similar to some of the optimizations made by the front-end frameworks Svelte and Vue.js 3.0, which can be called AOT.
In the case of no type, each time the state changes, there are only two Text controls in the interface, but the content is different. In this case, the framework uses diff to determine that the Text controls in the interface have not changed, only the content has changed, so it sets new content for them.
However, this is not the case. When something changes, the Text displayed on the interface is different. The Text in the middle always shows “something else”. When the framework gets a new view, it can check for differences in the order of its generic arguments:
Before update: VStack(TupleView(Text(...) , Text(...) , nil)) After update: VStack(TupleView(nil, Text(...) , Text(...) ))Copy the code
Their relative positions are written in the type, so that the middle view is not modified, and there is no type information or other meta-information, which is absolutely impossible.
Modified
is used to modify the font and position of a SwiftUI View. Therefore, there are different types of views with or without these parameters, which will help the framework to do some static optimization.
I will probably update this post later on about how to use SwiftUI in more detail, but this post is just to briefly discuss my understanding of the macro level of the framework. Have a great WWDC week
References:
- Forums.swift.org/t/pitch-fun…
- Github.com/apple/swift…
- Github.com/apple/swift…
- Github.com/apple/swift…
- Forums.swift.org/t/important…
- developer.apple.com/wwdc19/402
- svelte.dev/
- Twitter.com/unixzii/sta…