SwiftUI优点:
声明式语法编写UI,简洁方便,类似React,Flutter等UI布局方式
实时预览UI界面,效率高
适配WatchOS, TVOS, macOS, iOS和padOS
类似React,UI组件状态容易维护,复用灵活
SwiftUI缺点:
SwiftUI的接口不稳定,几个发布的beta版本中接口有变化
只支持iOS13以及最新的其他苹果平台系统
目前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