学习SwifUI的最好方式就是,用SwiftUI编写一个程序
文章来源:Introducing SwiftUI: Building Your First App
注:视频中使用的部分API,已经在后续版本中废弃,本文代码根据最新版API进行了调整,确保 Demo工程 能够运行
一:编写第一个SwiftUI程序
- 创建SwiftUI工程
-
左侧是代码区
-
右侧是Canvas
编写代码时,右侧的Canvas能够实时显示出代码的UI预览效果
-
编写UI布局代码
-
通过拖拽增加UI控件
- VStack:SwiftUI常用的一种布局元素,可以用来垂直地叠加视图
- HStack:水平叠加视图
-
cmd+点击VStack,插入HStack
HStack { VStack { Text("Rooms") Text("20 people") } } 复制代码
-
在文字左侧增加一个图片
HStack { // `photo`是系统自带资源库中的图片 Image(systemName: "photo") VStack { Text("Rooms") Text("20 people") } } 复制代码
-
在Canvas中将VStack修改为左对齐
-
设置Text的字号
HStack { Image(systemName: "photo") VStack(alignment: .leading) { Text("Rooms") Text("20 people") .font(.subheadline) } } 复制代码
我们称
.font(.subheadline)
为修饰器(modifier),用来自定义视图的外观或行为 -
设置Text的颜色为
secondary
HStack { Image(systemName: "photo") VStack(alignment: .leading) { Text("Rooms") Text("20 people") .font(.subheadline) .foregroundColor(.secondary) } } 复制代码
-
将HStack替换为List
-
-
设置数据源
-
增加Room模型
在SwiftUI中,需要让模型遵循
Identifiable
协议,实现id
属性
-
在ContentView中使用Room数据来展示UI
当代码发生重大改变(比如增加属性),Xcode会暂停预览,直到我们做好重新更新的准备(点击Resum按钮)
-
-
丰富UI内容
-
设置图片圆角
-
通过拖拽Modifier库来实现
-
-
设置Navigation、NavigationTitle以及给每个单元格设置跳转
NavigationView { List(rooms) { room in NavigationLink(destination: Text(room.name)) { Image(systemName: "photo") .cornerRadius(8.0) VStack(alignment: .leading) { Text(room.name) Text("\(room.capacity) people") .font(.subheadline) .foregroundColor(.secondary) } } } .navigationTitle(Text("Rooms")) } 复制代码
-
-
进入实时模式,查看效果
-
将子视图提成一个单独的视图
-
创建新的页面:RoomDetail
struct RoomDetail: View { let room: Room var body: some View { Image(room.imageName) .resizable() .aspectRatio(contentMode: .fit) } } struct RoomDetail_Previews: PreviewProvider { static var previews: some View { RoomDetail(room: testData[0]) } } 复制代码
-
设置
navigationBarTitle
Image(room.imageName) .resizable() .aspectRatio(contentMode: .fit) .navigationBarTitle(Text(room.name)) 复制代码
此时页面不会更新,因为RoomDetail中缺乏
NavigationView
作为上下文 -
在预览中添加
NavigationView
上下文struct RoomDetail_Previews: PreviewProvider { static var previews: some View { NavigationView { RoomDetail(room: testData[0]) } } } 复制代码
-
调整
navigationBarTitle
的展示模式
-
-
将RoomCell的跳转修改为前往RoomDetail页面
struct RoomCell: View { let room: Room var body: some View { NavigationLink(destination: RoomDetail(room: room)) { // ··· } } } 复制代码
二:Swift UI的工作方式
2.1 View
-
A View Defines a Piece of UI
在SwiftUI中,视图是一种遵守
View
协议的结构,而不是继承自基础类的类 -
A View Defines its Dependencies
2.2 状态属性
-
@State
-
当SwiftUI看到一个带
@State
状态变量的视图时,它会以视图的名义为那个变量分配存储空间。- 绿色部分是APP的内存
- 紫色是SwiftUI所管理的内存
SwiftUI可以观察到
@State
变量合时被读写,同时SwiftUI知道zoom
是从body
中读取的,SwiftUI会在@State
变量发生更改时,使用新的状态值,刷新渲染。
例:实现在RoomDetail中,点击图片修改填充模式
struct RoomDetail: View {
let room: Room
@State private var zoomed = false
var body: some View {
Image(room.imageName)
.resizable()
.aspectRatio(contentMode: zoomed ? .fit : .fill) // 根据zoomed值修改填充模式
.navigationBarTitle(Text(room.name), displayMode: .inline)
.onTapGesture {
self.zoomed.toggle() // 点击修改zoomed的值
}
}
}
复制代码
2.3 事实来源
在SwiftUI中,UI可能因不同的数据,处于不同的状态,我们将这些用来绘制UI的数据称为“事实来源”,“事实来源“由状态变量
和模型
共同组成。
-
属性可以简单地分为:事实来源(Source of Truth)和衍生值(Derived Value)
zoomed
变量是一个事实来源,contentMode
衍生自它,当系统观察到zoomed
变量发生变化时,SwiftUI框架会请求新的body
,刷新渲染,重新生成一个新的宽高比视图,接下来覆盖contentMode
。像RoomDetail中的,
room
属性,也是一个衍生值。 -
数据流原语(Data Flow Primitives)
SwiftUI是数据驱动,而不是事件驱动
三:完善Rooms APP
-
增加动画
.onTapGesture { withAnimation { self.zoomed.toggle() } } 复制代码
-
添加一个ZStack
ZStack(alignment: .topLeading) { Image(room.imageName) .resizable() .aspectRatio(contentMode: zoomed ? .fit : .fill) .navigationBarTitle(Text(room.name), displayMode: .inline) .onTapGesture { withAnimation { self.zoomed.toggle() } } Image(systemName: "video.fill") .font(.title) .padding(.all) } 复制代码
-
固定图标的位置
Image(room.imageName) .frame(minWidth:0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) 复制代码
更多关于SwiftUI布局相关的内容 – > Buiding Custom Views with SwiftUI
-
同时预览多个View
struct RoomDetail_Previews: PreviewProvider { static var previews: some View { Group { NavigationView { RoomDetail(room: testData[0]) } NavigationView { RoomDetail(room: testData[1]) } } } } 复制代码
-
增加动效
- 给视频图标增加过渡动效
if room.hasVideo && !zoomed { Image(systemName: "video.fill") .font(.title) .padding(.all) .transition(.move(edge: .leading)) } 复制代码
-
给图片的动效延长时间
.onTapGesture { withAnimation(.easeInOut(duration: 2)) { self.zoomed.toggle() } } 复制代码
-
支持动态增加
监测数据模型的改变,实时更新UI
-
创建RoomStore储存Room模型
import SwiftUI class RoomStore { var rooms: [Room] init(rooms: [Room] = []) { self.rooms = rooms } } 复制代码
-
遵守
ObservableObject
协议class RoomStore: ObservableObject { @Published var rooms: [Room] // ··· } 复制代码
-
声明
EnvironmentObject
类型变量@EnvironmentObject var store: RoomStore 复制代码
-
传入
EnvironmentObject
类型变量struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .environmentObject(RoomStore(rooms: testData)) } } 复制代码
-
列表中增加一个按钮
List { Button(action:{ }) { Text("Add Room") } // ForEach为它的每个集合项都创建一个视图 ForEach(store.rooms) { room in RoomCell(room: room) } } 复制代码
Button(action:addRoom) { Text("Add Room") } func addRoom() { store.rooms.append(Room(name: "Hall 2", capacity: 2000)) } 复制代码
-
-
修改List的样式
-
修改
listStyle
NavigationView { List { // ··· } .navigationBarTitle(Text("Rooms")) .listStyle(GroupedListStyle()) } 复制代码
-
设置分组
List { Section { Button(action:addRoom) { Text("Add Room") } } Section { ForEach(store.rooms) { room in RoomCell(room: room) } } } 复制代码
-
-
支持动态删除
func delete(at offsets: IndexSet) { store.rooms.remove(atOffsets: offsets) 复制代码
ForEach(store.rooms) { room in RoomCell(room: room) } .onDelete(perform: delete) 复制代码
-
设置NavigationBarItem
NavigationView { List { } .navigationBarItems(trailing: EditButton()) } 复制代码
-
支持列表重新排序
func move(from source: IndexSet, to destination: Int) { store.rooms.move(fromOffsets: source, toOffset: destination) } 复制代码
ForEach(store.rooms) { room in RoomCell(room: room) } .onDelete(perform: delete) .onMove(perform: move) 复制代码
-
设置预览环境
Group { ContentView() .environmentObject(RoomStore(rooms: testData)) // 大字号环境 ContentView() .environmentObject(RoomStore(rooms: testData)) .environment(\.sizeCategory, .extraExtraLarge) // 深色模式 ContentView() .environmentObject(RoomStore(rooms: testData)) .environment(\.colorScheme, .dark) // 布局方向 ContentView() .environmentObject(RoomStore(rooms: testData)) .environment(\.layoutDirection, .rightToLeft) .environment(\.locale, Locale(identifier: "ar")) } 复制代码
总结
-
SwiftUI四个主要设计原则:
- Declarative
- Compositional
- Automatic
- Consistent
-
SwiftUI使用陈述性语法
-
在SwiftUI中,Xcode预览可以让我们浏览、编辑和调试APP,我们甚至不需要运行项目工程