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()
    var rooms: [Room] {
        didSet {
            didChange.send()
        }
    }
    
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
}

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

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

Move

Delete

总结

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

你可能感兴趣的:(SwiftUI系列 WWDC Introducing SwiftUI #204 学习笔记)