
什么是SwiftUI?
SwiftUI是2019年WWDC大會上,蘋果發(fā)布的基于Swift語言構(gòu)建的全新UI框架,開發(fā)者可通過它快速為所有的Apple平臺創(chuàng)建美觀、動態(tài)的應(yīng)用程序;
SwiftUI的運(yùn)行速度優(yōu)于UIKit,他減少了界面的層次結(jié)構(gòu),因此可以減少繪制步驟,并且他完全繞過了CoreAnimation,直接進(jìn)入Metal,可以有優(yōu)秀的渲染性能;
SwiftUI 就是?種聲明式的構(gòu)建界面的用戶接口工具包;
SwiftUI使用聲明式的語法構(gòu)建UI,我們只需要向系統(tǒng)聲明UI的View樣式,以及View如何轉(zhuǎn)換狀態(tài),其他的過程都交給系統(tǒng)去處理;
聲明式語法和指令式語法的區(qū)別:
聲明式的我們需要提前聲明好每個view的各種狀態(tài),以及狀態(tài)轉(zhuǎn)變的條件。后續(xù)界面和用戶在互動時,系統(tǒng)會幫我們自動進(jìn)行狀態(tài)切換;
指令式的我們需要給每個view先設(shè)置好默認(rèn)狀態(tài),后續(xù)界面和用戶在互動時,需要通過指令不停的去轉(zhuǎn)變view的狀態(tài);
因此聲明式的UI是提前聲明好各種狀態(tài),系統(tǒng)會自動幫我們進(jìn)行狀態(tài)切換。指令式的UI是通過我們設(shè)定的指令來轉(zhuǎn)換狀態(tài);
比如界面調(diào)整、用戶交互、機(jī)型適配,UIKit都需要手動調(diào)整view,對于SwiftUI我們只需要聲明好我們想要的樣式,系統(tǒng)會幫我們?nèi)フ{(diào)整view;
總結(jié)起來,SwiftUI比UIKit更加抽象化;
1)SwiftUI的優(yōu)點(diǎn)
1.1)統(tǒng)一蘋果終端
在 SwiftUI 出現(xiàn)之前,蘋果不同的設(shè)備之前的開發(fā)框架并不互通,增加開發(fā)者所需消耗的時間精力,也不利于構(gòu)建跨平臺的軟件體驗;
SwiftUI具有了跨平臺性,蘋果的平臺都可以使用,iOS、macOS、tvOS、watchOS;
1.2)降低界面開發(fā)難度
UIKit 的基本思想要求ViewController 承擔(dān)絕?部分職責(zé),它需要協(xié)調(diào) model,view 以及??交互。這帶來了巨?的sideeffect 以及?量的狀態(tài);
SwiftUI是聲明式的構(gòu)建方式,我們只需要聲明好界面系統(tǒng)會自動轉(zhuǎn)換狀態(tài),搭建界面更加的簡單;
1.3)更加高效
默認(rèn)使用Metal渲染,性能非常高,比UIKit要好;
更扁平化的內(nèi)聯(lián)數(shù)據(jù)結(jié)構(gòu)去分配內(nèi)存,值類型。占用內(nèi)存很少(所以在輕應(yīng)用的開發(fā)更適合使用SwiftUI);
代碼量相比UIKit要更少,效率更高;
1.4)更好的配合Swift語言
SwiftUI 使用了大量 Swift 的語言特性;
2)SwiftUI的特性
2.1)聲明式語法
與UIKit布局相比,更加的抽象化,只需要向系統(tǒng)聲明界面樣式以及樣式變化條件,其他的系統(tǒng)會幫我們實現(xiàn),不需要我們自己去調(diào)整視圖;
2.2)界面元素的組件化
UIKit耦合了很多的操作邏輯,很難進(jìn)行移植,更遑論組件化了;
而SwiftUI僅僅聲明界面樣式,所以是可以將復(fù)雜視圖的拆分出來組件化;
甚至還可以在其他平臺使用,以此跨平臺;
一般我個人會將視圖組件區(qū)分為基礎(chǔ)組件、布局組件和功能組件;
2.3)與UIKit互相兼容
把 UIKit 中已有的部分進(jìn)行封裝,提供給 SwiftUI 使用。開發(fā)者需要做的僅僅是遵循UIViewRepresentable協(xié)議即可;
并且在已有的項目中,也可以僅用 SwiftUI 制作一部分的 UI 界面;
兩種代碼的風(fēng)格是截然不同的,但在使用上卻基本沒有性能的損失。在最終的運(yùn)行效果上,用戶也無法分辨出兩種界面框架的不同;
2.4)真實數(shù)據(jù)源(Source of truth)(重點(diǎn)特性)
SwiftUI中的數(shù)據(jù)源一定會是真實的,也就是準(zhǔn)確的;
在UIKit中,一個view的狀態(tài)由多種因素導(dǎo)致的,不同的來源,不同的邏輯操作(因此需要考慮及時更新界面);
在SwiftUI中,只要在屬性聲明時加上@State關(guān)鍵詞,就可以將該屬性和界面元素聯(lián)系起來,在每次數(shù)據(jù)改動后,都有機(jī)會決定是否更新視圖,系統(tǒng)將所有的屬性都集中到一起進(jìn)行管理和計算,也不再需要手寫刷新的邏輯;
2.5)設(shè)計工具和快速預(yù)覽功能
Xcode 包含直觀的設(shè)計工具,只需拖放操作就能使用 SwiftUI 輕松構(gòu)建界面,同時支持實時預(yù)覽頁面的變化;
SwiftUI中常用的View和Modifiers
SwiftUI通過View視圖搭建界面,使用Modifiers修飾器來修飾視圖。系統(tǒng)提供了大量的視圖和修飾器,并且還可以讓我們自定義修飾器。
既可以手動寫,也可以直接拖出到代碼區(qū)或者預(yù)覽區(qū)。這三種方式的結(jié)果都是一樣的。
1)Text
顯示一行或多行的只讀文本視圖,類似于UIKit中的UIlabel;
Text("我是一個Text").foregroundColor(.red)
2)Label
顯示一個標(biāo)簽組件,支持圖片與標(biāo)題的展示;
Label("Rain", systemImage: "cloud.rain")
3)Button
顯示一個按鈕組件,類似于UIKit中的UIButton;
Button {
print("button點(diǎn)擊響應(yīng)")
} label: {
Text("我是按鈕")
}
4)Link
通過提供目標(biāo)URL和標(biāo)題來創(chuàng)建鏈接;
Link(destination: URL(string:"https://www.baidu.com/")!) {
Text("Link")
}
5)Image
顯示一個圖片組件;
Image("image name")
.resizable()
.aspectRatio(contentMode: .fit)
也可以通過AsyncImage實現(xiàn)異步加載網(wǎng)絡(luò)圖片的組件;
AsyncImage(url: URL(string: "image url")) { image in
image.resizable()
} placeholder: {
Image("placeHolder image")
}
6)TextEditor
顯示可編輯文本界面的控件。相當(dāng)于UITextView;
TextEditor(text:
.constant("Placeholder"))
.frame(width: 100, height: 30, alignment: .center)
7)TextField
顯示文本輸入框。相當(dāng)于UITextView;
TextField("首字母默認(rèn)大寫", text: $str).frame(width: 100, height: 56, alignment: .center)
.textInputAutocapitalization(.never)
.onSubmit {
print("我點(diǎn)擊了!")
}
textInputAutocapitalization為設(shè)置自動大小寫的屬性;
8)NavigationView
用于做頁面間的導(dǎo)航跳轉(zhuǎn);
var body: some View {
NavigationView{
List{
VStack {
...
}
}.navigationBarTitle("Todo Items")
}
}
- 使用navigationBarTitle方法給控件設(shè)置導(dǎo)航欄的標(biāo)題;
- 注意navigationBarTitle修飾符屬于列表視圖,而不是導(dǎo)航視圖;
- 這是因為導(dǎo)航視圖從右邊通過push來顯示新界面;
- 每個界面都有自己的標(biāo)題。如果標(biāo)題是附加到導(dǎo)航視圖,標(biāo)題就被固定了;
- 通過附加的標(biāo)題到導(dǎo)航視圖的里面內(nèi)容,標(biāo)題可以更改為其內(nèi)容的變化;
NavigationView {
VStack {
...
NavigationLink(destination:
VStack {
Text(todo.name)
Image(todo.category).resizable().frame(width: 200, height: 200)
}
){
HStack {
...
}
}
}.navigationBarTitle("Nav Title")
}
- 注意,我們必須向NavigationLink提供一個參數(shù)destination,也就是點(diǎn)擊項目時顯示的視圖;
- 這里代碼中可以看到,視圖將包括:Text和Image;
- 當(dāng)運(yùn)行應(yīng)用程序,點(diǎn)擊一個item就會跳轉(zhuǎn)到另一個界面,界面顯示選擇的項目的詳細(xì)信息;
- 新界面的頂部欄也會顯示帶有上一個項目的符號;
SwiftUI中的布局
UI通常由多種不同類型的視圖組合而成。我們?nèi)绾螌λ麄冞M(jìn)行分組以及布局定位?此時就需要使用stacks。我們可以使用三種堆棧來對UI進(jìn)行分組:
- HStack - 水平排列其子視圖;
- VStack - 垂直排列其子視圖;
- ZStack -根據(jù)深度排列子視圖(例如從后到前);
在這三種Stack的基礎(chǔ)上還有一種懶加載的Stack,叫l(wèi)azyStack;
除此之外還需要了解絕地位置和相對位置的使用;
注意: SwiftUI沒有坐標(biāo)系這種說法,使用彈性布局。類似于HTML的布局方式;
SwiftUI中List的使用
1)List的創(chuàng)建
var body: some View {
List{
HStack{
Image("work").resizable().frame(width: 50, height: 50)
Text("Write SwiftUI book")
}
HStack{
Image("personal").resizable().frame(width: 50, height: 50)
Text("Read Bible")
}
HStack{
Image("family").resizable().frame(width: 50, height: 50)
Text("Bring kids out to play")
}
HStack{
Image("family").resizable().frame(width: 50, height: 50)
Text("Fetch wife")
}
HStack{
Image("family").resizable().frame(width: 50, height: 50)
Text("Call mum")
}
}
}
- 通過List添加多行數(shù)據(jù);
- 每一行包含一個圖像和一個水平文本,通過HStack來包裝;
- 因為圖像大小不同,大的圖像會被擴(kuò)展,除了屏幕大小,只顯示了一部分。為了解決這個問題,我們應(yīng)用. resizable修改器使圖像適合于使用面積;
- 然后應(yīng)用.frame修飾符將圖像的大小限制為一個自定義的框架;
2)List的動態(tài)性
可通過@State修飾數(shù)據(jù)源實現(xiàn)List列表的實時刷新;
3)ID標(biāo)識
通過ForEach構(gòu)建List元素時可以為每一個item設(shè)置id,一般可以通過數(shù)據(jù)源內(nèi)對應(yīng)該item的數(shù)據(jù)中的內(nèi)容定義id,也可以直接使用數(shù)據(jù)本身self;
var body: some View {
List{
ForEach(datas, id:\.name){ data in
HStack{
Image(datas.category) .resizable().frame(width: 50, height: 50)
Text(datas.name)
}
}
}
}
SwiftUI中的屬性包裝器
1)@State
SwiftUI管理聲明為state的存儲屬性。當(dāng)值發(fā)生變化時,SwiftUI會更新視圖層次結(jié)構(gòu)中依賴于該值的部分。使用@State作為存儲在視圖層次結(jié)構(gòu)中的給定值的唯一真值來源;
@State修飾的屬性雖然是存儲屬性,但是我們可以進(jìn)行讀寫操作;
父視圖和子視圖進(jìn)行傳遞該屬性只能是值傳遞;
需要在屬性名稱前加上一個美元符號$來獲得這個值;
struct ContentView: View {
@State private var str: String = ""
var body: some View {
VStack {
TextField("Placeholder", text: $str)
Text("\(str)")
}
}
}
- 在str上設(shè)置了@State修飾,那么我在文本輸入框中輸入的數(shù)據(jù),就會傳入到str中;
- 同時str又綁定在文本視圖上,所以會將文本輸入框輸入的文本顯示到文本視圖上;
- 這就是數(shù)據(jù)綁定的快捷實現(xiàn);
2)@Binding
@State修飾的屬性是值傳遞,因此在父視圖和子視圖之間傳遞屬性時。子視圖針對屬性的修改無法傳遞到父視圖上;
Binding修飾后會將屬性會變?yōu)橐粋€引用類型,視圖之間的傳遞從值傳遞變?yōu)榱艘脗鬟f,將父視圖和子視圖的屬性關(guān)聯(lián)起來。這樣子視圖針對屬性的修改,會傳遞到父視圖上;
需要在屬性名稱前加上一個美元符號$來獲得這個值。因為它是投影屬性;
下面代碼在主視圖上添加一個BtnView視圖,視圖上添加一個按鈕,按鈕點(diǎn)擊后修改isShowText變量。這里的變量通過傳入?yún)?shù)與主視圖的isShowText進(jìn)行綁定。綁定到主視圖的isShowText變量上。主視圖的變量用來決定文本視圖的隱藏和顯示;
struct BtnView: View {
@Binding var isShowText: Bool
var body: some View {
Button {
isShowText.toggle()
} label: {
Text("點(diǎn)擊")
}
}
}
struct ContentView: View {
@State private var isShowText: Bool = true
var body: some View {
VStack {
if(isShowText) {
Text("點(diǎn)擊后會被隱藏")
} else {
Text("點(diǎn)擊后會被顯示").hidden()
}
BtnView(isShowText: $isShowText)
}
}
}
- 按鈕在BtnView視圖中,并且通過點(diǎn)擊,修改isShowText的值;
- 將BtnView視圖添加到ContentView上作為它的子視圖。并且傳入isShowText;
- 此時的傳值是指針傳遞,會將點(diǎn)擊后的屬性值傳遞到父視圖上;
- 父視圖拿到后也作用在自己的屬性,因此他的文本視圖會依據(jù)該屬性而隱藏或顯示;
- 如果將@Binding改為@State,會發(fā)現(xiàn)點(diǎn)擊后不起作用。這是因為值傳遞子視圖的更改不會反映到父視圖上;
3)@ObservableObject
對實例進(jìn)行監(jiān)聽,其用處和@State非常相似,只不過必須是對象,而且這個被監(jiān)聽的對象可以被多個視圖使用。需要注意用法;
class DelayedUpdater: ObservableObject {
@Published var value = 0
init() {
for i in 1...10 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
self.value += 1
}
}
}
}
struct ContentView: View {
@ObservedObject var updater = DelayedUpdater()
var body: some View {
VStack {
Text("\(updater.value)").padding()
}
}
}
- 綁定的數(shù)據(jù)是一個對象;
- 被修飾的對象,其類必須遵守ObservableObject協(xié)議;
- 此時這個類中被@Published修飾的屬性都會被綁定;
- 使用@ObservedObject修飾這個對象,綁定這個對象;
- 被@Published修飾的屬性發(fā)生改變時,SwiftUI就會進(jìn)行更新;
- 這里當(dāng)value值會隨著時間發(fā)生改變。所以updater對象也會發(fā)生改,此時文本視圖的內(nèi)容就會不斷更新;
4)@EnvironmentObject
在多視圖中,為了避免數(shù)據(jù)的無效傳遞,可以直接將數(shù)據(jù)放到環(huán)境中,供多個視圖進(jìn)行使用,相當(dāng)于全局?jǐn)?shù)據(jù);
struct EnvView: View {
@EnvironmentObject var updater: DelayedUpdater
var body: some View {
Text("\(updater.value)")
}
}
struct BtnvView: View {
@EnvironmentObject var updater: DelayedUpdater
var body: some View {
Text("\(updater.value)")
}
}
struct ContentView: View {
let updater = DelayedUpdater()
var body: some View {
VStack {
EnvView().environmentObject(updater)
BtnvView().environmentObject(updater)
}
}
}
- 給屬性添加@EnvironmentObject修改,就將其放到了環(huán)境中;
- 其他視圖中想要獲取該屬性,可以通過.environmentObject從環(huán)境中獲取;
- 可以看到分別將EnvView和BtnvView的屬性分別放到了環(huán)境中;
- 之后我們ContentView視圖中獲取數(shù)據(jù)時,可以直接通過環(huán)境獲??;
- 不需要將數(shù)據(jù)傳遞到ContentView,而是直接通過環(huán)境獲取,這樣避免了無效的數(shù)據(jù)傳遞,更加高效;
- 如果是在多層級視圖之間進(jìn)行傳遞,會有更明顯的效果;
SwiftUI-Demo-仿App Store頁面的實現(xiàn)

1)頁面整體結(jié)構(gòu)

- 頁面整體使用ScrollView + VStack的布局方式;
- 在VStack中定義需要縱向展示的子頁面;
2)標(biāo)題頁布局

- iOS15.0之后,SwiftUI推出了一個全新的屬性overlay,用以在某一控件上添加子控件;
3)推薦游戲頁布局

- 推薦游戲頁整體采用TabView + VStack的布局方式;
- TabView提供了一個可以左右分頁滑動的列表界面;
- 使用AsyncImage加載網(wǎng)絡(luò)圖片;
- 使用AsyncImage的overlay屬性添加子控件;
- 子控件采用HStack布局;
4)其他游戲頁布局

- 其他游戲頁整體采用VStack布局;
- 列表部分使用LazyVStack的布局方式(也可以使用List);
- 列表item使用HStack布局;