SwiftUI的基礎(chǔ)與應(yīng)用

平安喜樂

什么是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ū)別:

  1. 聲明式的我們需要提前聲明好每個view的各種狀態(tài),以及狀態(tài)轉(zhuǎn)變的條件。后續(xù)界面和用戶在互動時,系統(tǒng)會幫我們自動進(jìn)行狀態(tài)切換;

  2. 指令式的我們需要給每個view先設(shè)置好默認(rèn)狀態(tài),后續(xù)界面和用戶在互動時,需要通過指令不停的去轉(zhuǎn)變view的狀態(tài);

  3. 因此聲明式的UI是提前聲明好各種狀態(tài),系統(tǒng)會自動幫我們進(jìn)行狀態(tài)切換。指令式的UI是通過我們設(shè)定的指令來轉(zhuǎn)換狀態(tài);

  4. 比如界面調(diào)整、用戶交互、機(jī)型適配,UIKit都需要手動調(diào)整view,對于SwiftUI我們只需要聲明好我們想要的樣式,系統(tǒng)會幫我們?nèi)フ{(diào)整view;

  5. 總結(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)

image.png
1)頁面整體結(jié)構(gòu)
image.png
  • 頁面整體使用ScrollView + VStack的布局方式;
  • 在VStack中定義需要縱向展示的子頁面;
2)標(biāo)題頁布局
image.png
  • iOS15.0之后,SwiftUI推出了一個全新的屬性overlay,用以在某一控件上添加子控件;
3)推薦游戲頁布局
image.png
  • 推薦游戲頁整體采用TabView + VStack的布局方式;
  • TabView提供了一個可以左右分頁滑動的列表界面;
  • 使用AsyncImage加載網(wǎng)絡(luò)圖片;
  • 使用AsyncImage的overlay屬性添加子控件;
  • 子控件采用HStack布局;
4)其他游戲頁布局
image.png
  • 其他游戲頁整體采用VStack布局;
  • 列表部分使用LazyVStack的布局方式(也可以使用List);
  • 列表item使用HStack布局;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容