SwiftUI系列 WWDC Introducing SwiftUI #204 学习笔记

开场白

  • 本文目标:开发一个 Room 的小应用。
  • 建议学习方法:全程跟完 WWDC #204,亲手用代码实现视频中项目的每一个细节。
  • 建议学习时间:1.5 小时,其中视频全长 55 分钟,自己敲代码时间 35 分钟。

视频时间线

  • 00 : 00 ~ 06 : 00 了解 Image,Text 控件的基本用法,以及 HStack,VStack 基本布局。
  • 06 : 00 ~ 16 : 00 了解 NavigationView,NavigationLink 控件的基本用法,以及 Image 对图片大小的自适应处理。
  • 16 : 00 ~ 30 : 00 举点击图片缩放的例子来说明 SwiftUI 数据绑定相对于传统命令式编程进行状态管理的好处。
  • 30 : 00 ~ 38 : 00 @State 变量的用法,动手实现上面图片缩放的例子,包括了点击时间处理、动画处理、状态刷新。
  • 38 : 00 ~ 47 : 00 ObserverdObject,PassthroughObject 的基本用法,实现了对 cell 的增加、删除、移动功能,ForEach 的使用,List 样式的修改。
  • 47 : 00 ~ 54 : 00 在 Preview 中设置 Accessablity 大字体、暗黑模式、本地化的进行 Debug。

正文

App入口变化

首先,确保你新建工程的时候,开启了SwiftUI。完成之后会发现,多出了 SceneDelegate 这个文件。



App的根窗口会去加载 ContentView 作为根视图


声明式语法

在ContenView文件里,所以的 UI 布局代码都写在 body 的 get 方法里,并且布局的实时改变都能反馈到右边的 Canvas 上。启动 Canvas 的快捷键是 Option + Command + p,但是至今为止的 Xcode 遇到这个快捷键的时候会遇到莫名 bug,所以推荐直接用鼠标点击 Resume 启动。



Preview 的入口在下面这个方法里:


image.png

布局基础容器

有三种容器:HStack、VStack、ZStack 可以帮助你布局。顾名思义,H 代表水平方向,V 代表垂直方向,Z 代表垂直于屏幕的 “Z” 轴方向。很显然,你已经知道,Text 控件相当于 UIKit 中的 UILabel,我们在 VStack 中声明两个 Text 控件,看看效果。



红色的错误是 XCode 的 bug。如果熟悉 Flex 布局,应该很快能够反应过来,VStack 就相当于主轴是在垂直方向,但是 SwiftUI 默认容器内的布局都是从 center 开始。
我们可以通过修改 VStack 的初始化方法来调整水平方向的对齐方式:

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("How are you?")
            Text("Fine. Thank you")
        }
    }
}

效果:

Image 控件

下面我们引入 Image 控件:

struct ContentView: View {
    var body: some View {
        HStack {
            Image(systemName: "video.fill")
            VStack(alignment: .leading) {
                Text("How are you?")
                Text("Fine. Thank you")
            }
        }
    }
}

效果:


List 控件

对一个控件点击 Command + 左键可以方便地将其嵌入 List 控件中。



嵌入之后,我们发现 List 的效果相当于 UITableView,List 中内嵌的代码相当于 UITableViewCell,效果如下:


image.png

List 接受数组作为参数,来决定 List 中 cell 的个数,尾随闭包中的 item 参数是数组中的每一个元素。
struct ContentView: View {
    var body: some View {
        List(0 ..< 5) { item in
            Image(systemName: "video.fill")
            VStack(alignment: .leading) {
                Text("How are you?")
                Text("Fine. Thank you")
            }
        }
    }
}

NavigationView 控件

下面将页面加上导航栏,我们需要用到 NavigationView 控件

struct ContentView: View {
    var body: some View {
        NavigationView {
            List(/*@START_MENU_TOKEN@*/0 ..< 5/*@END_MENU_TOKEN@*/) { item in
                Image(systemName: "video.fill")
                VStack(alignment: .leading) {
                    Text("How are you?")
                    Text("Fine. Thank you")
                }
            }
        }
    }
}

我们可以看到,导航栏部分的控件空出来了(红色部分是我的强调):


image.png

下面为导航栏设置标题,我们直接在 List 控件尾部通过点语法进行链式调用,来改变 UI 控件的展示,这种操作在 SwiftUI 叫做 Modifier。(但是这里我比较不理解的是,为什么 Modifier没有跟在 NavigationView 的后面,因为我觉得 title 毕竟是导航栏的属性)

struct ContentView: View {
    var body: some View {
        NavigationView {
            List(0 ..< 5) { item in
                Image(systemName: "video.fill")
                VStack(alignment: .leading) {
                    Text("How are you?")
                    Text("Fine. Thank you")
                }
            }.navigationBarTitle(Text("My First SwiftUI App"))
        }
    }
}

效果如下:


image.png

我们还可以继续调用 Modifier,将导航栏展示改成我们传统的方式

struct ContentView: View {
    var body: some View {
        NavigationView {
            List(0 ..< 5) { item in
                Image(systemName: "video.fill")
                VStack(alignment: .leading) {
                    Text("How are you?")
                    Text("Fine. Thank you")
                }
            }.navigationBarTitle(Text("My First SwiftUI App"), displayMode: .inline)
        }
    }
}

效果如下:


image.png

按照视频中的步骤,我们也造一些测试数据,然后为每一个 List 的 cell 增加点击事件,用到的是 NavigationLink 控件(注意原视频中的 NavigationButton 已经弃用),其中 destination 参数可以是 Text 类型,用于显示下一个页面的中间的 text;也可以是一个 View,下面我们跟着视频一起创建 RootDetail 这个类,敲过代码后,你会学习到 Image 控件的缩放适配处理、手势点击处理、动画处理、设置frame的最大值和最小值、@State 变量与 UI 状态的绑定。

struct RoomDetail: View {
    let room: Room
    @State private var zoomed = false
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Image(room.imageName)
                .resizable()
                .aspectRatio(contentMode:zoomed ? .fill : .fit)
                
                .onTapGesture {
                    withAnimation(Animation.easeIn(duration: 2)) {
                        self.zoomed.toggle()
                    }
                }.frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity, minHeight: 0, idealHeight: .infinity, maxHeight: .infinity).navigationBarTitle(Text(room.name), displayMode: .inline)
            
            if room.hasVideo && !zoomed {
                Image(systemName: "video.fill").font(.title).padding(.all).transition(.move(edge: .leading))
            }
        }
    }
}

效果如下:


2020-04-03 14.54.35.gif

ObservedObject 用法

在 ContentView 中,用 @ObservedObject (原视频中的 @BindingObject 已经弃用)声明实例变量 store,意思说是 store 内部发出的通知会刷新 body 内的状态。

struct ContentView: View {
    @ObservedObject var store = RoomStore()
    
    var body: some View {
        NavigationView {
            List() {
                Section {
                    Button(action: addRoom) {
                        Text("Add Room")
                    }
                }
                Section {
                    ForEach(store.rooms) { room in
                        RommCell(room: room)
                    }
                    .onDelete(perform: delete)
                    .onMove(perform: move(from:to:))
                }
                
            }
            .navigationBarTitle(Text("My First SwiftUI App"), displayMode: .inline)
            .navigationBarItems(trailing: EditButton())
            .listStyle(GroupedListStyle())
        }
    }
    
    func addRoom() {
        store.rooms.append(Room(name: "Hall", capacity: 2000))
    }
    
    func delete(from offset: IndexSet) {
        store.rooms.remove(atOffsets: offset)
    }
    
    func move(from source: IndexSet, to destination: Int) {
        store.rooms.move(fromOffsets: source, toOffset: destination)
    }
}

RoomStore 必须继承 ObservableObject,在 rooms 发生改变的时候 PassthroughSubject 的实例调用 send 方法对外发出通知。

import SwiftUI
import Combine

class RoomStore: ObservableObject {
    var didChange = PassthroughSubject<Void, Never>()
    var rooms: [Room] {
        didSet {
            didChange.send()
        }
    }
    
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
}

然鹅实际上,当rooms发生改变的时候,有向外发出通知,UI 并没有更新,不知道具体原因。我改用替代的方法,对 rooms 变量加上 @ Published 后解决。

class RoomStore: ObservableObject {
    let didChange = PassthroughSubject<Void, Never>()
    @Published var rooms: [Room]    
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
}
Add

Move

Delete

总结

如果能把视频中的代码准确无误敲出来,那目标就达到了。这些新知识,先不要纠结于底层原理 (Why),能达到熟练使用 How 的级别就可以了。后面的章节,会详细分析原理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容