SwiftUI优点:

  1. 声明式语法编写UI,简洁方便,类似React,Flutter等UI布局方式

  2. 实时预览UI界面,效率高

  3. 适配WatchOS, TVOS, macOS, iOS和padOS

  4. 类似React,UI组件状态容易维护,复用灵活

SwiftUI缺点:

  1. SwiftUI的接口不稳定,几个发布的beta版本中接口有变化

  2. 只支持iOS13以及最新的其他苹果平台系统

  3. 目前bug较多,SwiftUI的bug

SwiftUI简介

SwiftUI是苹果公司在WWDC2019上推出的一种为iOS, macOS, tvOS, watchOS, padOS等平台编写用户界面的UI库。SwiftUI使用swift5.1编写,最低支持iOS13系统。

Swift5.1的新特性

SwiftUI简洁的写法离不开swift5.1的新特性。swift5.1新增的几个新特性分别是 some关键字,属性代理,Key Path Member Lookup,Function Builder。虽然近期不可能大规模使用SwiftUI来构建我们的App,但有必要了解这几个新特性。

some关键字(Opaque Result Type)

some关键字用来修饰方法返回值,即方法可以把协议作为返回值。如下方代码,CustomView的body属性是一个遵守View协议的Text,对外隐藏了其真实类型Text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SwiftUI中的View协议
public protocol View : _View {
    associatedtype Body : View
    var body: Self.Body { get }
}

struct ContentView: View {
    var body: some View {
        Text("Hello SwiftUI!")
    }
}

属性代理(propertyWrapper)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

上面代码中的showFavoritesOnly使用了@State进行修饰,如果showFavoritesOnly的值发生改变,LandmarkList的UI样式也会变化。这个功能用到了swift5.1中的属性代理特性。 其定义如下:

1
2
3
4
5
6
@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 }
}

可以看到@State是使用propertyWrapper修饰的结构体State,而showFavoritesOnly的定义类似于

1
var showFavoritesOnly = State(initialValue: true)

类似@State的修饰还有@Binding,@Environment,@EnvironmentObject等。
我们可以使用propertyWrapper自定义属性包装器,比如UserDefaults字段读取写入转载至

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@propertyWrapper 
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var value: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

// 应用属性代理 UserDefault
enum GlobalSettings {
    @UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false) 
    static var isFooFeatureEnabled: Bool
    
    @UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false) 
    static var isBarFeatureEnabled: Bool
}

Key Path Member Lookup

Swift4.2时引入了Dynamic Member Lookup特性,目的是使用静态的语法做动态的查找。支持 Dynamic Member Lookup 的类型首先需要用 @dynamicMemberLookup 来修饰,然后实现subscript(dynamicMember member: String)方法就可以使用.propertyname的形式访问属性。示例如下来源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@dynamicMemberLookup
struct Person {
    let name: String
    let age: Int

    subscript(dynamicMember member: String) -> String {
        let properties = ["name": "Leon", "city": "Shanghai"]
        return properties[member, default: "null"]
    }

    subscript(dynamicMember member: String) -> Int {
        return 32
    }
}

// 使用
let p = Person()
let age: Int = p.hello // 32
let name: String = p.name // Leon

Key Path Member Lookup也就是Dynamic Member Lookup特性一样,Swift4.2时引入了通过string动态访问属性,5.1时新增了通过KeyPath来访问属性。
SwiftUI中的Binding使用了这个特性,代码如下:

1
2
3
@propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
    public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }
}

Binding使用Key Path Member Lookup后,可以通过KeyPath对值方便进行存取。

1
2
3
4
5
6
7
8
struct ContentView: View {
    @Binding var slide: Slide
    var body: some View {
        VStack {
            Text("Slide #\(slide.number)")
        }
    }
}

Function Builder

下面构建VStack的代码非常简洁,VStack包含两个Text。

1
2
3
4
5
6
7
8
struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Hello World!")
            Text("Hello SwiftUI!")
        }
    }
}

查看VStack的初始化方法, 会发现content参数使用@ViewBuilder进行了修饰

1
2
3
4
5
// VStack结构体定义
public struct VStack<Content> : View where Content : View {
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
    public typealias Body = Never
}

和属性代理类似,@ViewBuilder是一个使用@_functionBuilder进行修饰的ViewBuilder结构体。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ViewBuilder结构体定义
@_functionBuilder public struct ViewBuilder {
    public static func buildBlock() -> EmptyView
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
// ViewBuilder扩展
extension ViewBuilder {
    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
    ...
}

于是,上面的代码可以等效于:

1
2
3
4
5
6
7
8
9
struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading) { viewBuilder -> Content in
            let text1 = Text("Hello World!")
            let text2 = Text("Hello SwiftUI!")
            return viewBuilder.buildBlock(text1, text2)
        }
    }
}

如何安装运行

SwiftUI库会搭载在iOS13系统上发布,目前还是beta版本,需要安装macOS10.15 beta版本 及Xcode11 beta版本才能编译运行。
注意: 系统的beta版本号和 Xcode11 beta版本号最好一致。比如安装macOS10.15 beta6版本,Xcode11也需要安装beta6版本。

安装macOS10.15 beta

先安装beta版本的profile文件,然后在《系统偏好设置-软件更新》中下载安装最新的beta版系统
下载页面
profile文件

安装Xcode11 beta

下载页面
Xcode11_beta7

参考资料

SwiftUI 的一些初步探索
苹果官方教程
(译)SwiftUI教程1-9
SwiftUI 和 Swift 5.1 新特性(1) some + 协议名称作为返回类型
SwiftUI 和 Swift 5.1 新特性(2) 属性代理Property Delegates
SwiftUI 和 Swift 5.1 新特性(3) Key Path Member Lookup
SwiftUI 和 Swift 5.1 新特性(4) 苹果先斩后奏?Function Builder 造就 SwiftUI 的 DSL