SwiftUI的認識與使用

 
SwiftUI簡介
SwiftUI是蘋果推出的一個新的UI框架,它使用了聲明的方式,通過視圖,基礎控件和佈局控件來進行頁面的開發。
SwiftUI具有跨平臺性,一份SwiftUI代碼可以同時跑在iOS、macOS、tvOS、watchOS平臺上。
SwiftUI編寫的頁面代碼更簡潔,廣泛使用鏈式調用。
SwiftUI視圖和UIKit視圖可以互相轉換,對於將舊的項目過度到新佈局方式比較友好。
SwiftUI的運行速度優於UIKit,他減少了界面的層次結構,因此可以減少繪製步驟,並且他完全繞過了CoreAnimation,直接進入Metal,可以有優秀的渲染性能。
 
其實聲明式頁面佈局前端已經出現了很久,像React, Vue都是使用的聲明式佈局,聲明式佈局與命令式佈局相比有很多優勢,
如:單向數據流,雙向數據綁定,只要數據狀態改變使用了這些數據的視圖就會自動更新等。
聲明式佈局是UI佈局方式的未來,這次蘋果從命令式編程過度到聲明式編程算是一個大的進步。
 
設計模式
採用Struct組成的樹形結構組織頁面。葉子節點是基本控件。
這棵Struct樹類似於React的抽象語法樹,它會在編譯階段將這些描述信息翻譯成真實的UIKit中的UI控件。
 
視圖結構
APP根入口
APP的根入口是一個Struct結構體,它遵守APP協議
@main
struct WorldLandMarkApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
App協議
public protocol App {
    associatedtype Body : Scene
 
    @SceneBuilder @MainActor var body: Self.Body { get }
 
    @MainActor init()
}
 

 

頁面結構體
some表示返回的是一個遵守了View協議的不透明類型,也就是var body: some View {} 這個計算屬性中,只能return一種類型,不能出現if a {Text()} else {List{}} 這樣的2種類型。
struct LandmarkList: View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
    }
}
View協議
associatedtype Body : View 表示協議中定義了一個新類型Body,這個Body遵守View協議。
Self.Body 表示協議中的Body類型。Self表示類型本身,self表示實例變量本身
public protocol View {
    associatedtype Body : View
    @ViewBuilder @MainActor var body: Self.Body { get }
}
 
每個頁面swift文件中都有2個結構體,一個表示要開發的頁面,另一個是使用Canvas進行展示出來的視圖,其中struct ContentView_Previews: PreviewProvider可以根據Debug需要在外層嵌套導航條,展示Group組。
import SwiftUI
 
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
它們不一定保持一致,如:
struct LandmarkList_Previews: PreviewProvider {
    struct DeviceType: Identifiable {
        var id = UUID()
        var name: String
    }
     
    static var previews: some View {
        //使用ForEach展示多個設備
        ForEach([DeviceType(name: "iPhone 12"),DeviceType(name: "iPhone 13")]){ deviceItem in
            LandmarkList().previewDevice(PreviewDevice(rawValue: deviceItem.name))
                .previewDisplayName(deviceItem.name)
        }
         
    }
}
 

 

狀態雙向綁定
@State單頁面狀態綁定
通過@State修飾的變量是做了雙向綁定的,如果這個變量數據發生了改變,所有使用這個變量的視圖都會自動更新。但是@State的修飾範圍是當前的一個視圖,如果想一個狀態修改,整個APP範圍內使用這個變量的視圖全部都更新,則需要使用全局環境變量的模式。
@State private var isOpen
struct LandmarkList: View {
     
    @State private var isOpen: Bool = false
    //@ObservedObject: 全局環境變量綁定
    @ObservedObject var userData: UserData = UserData()
}
 
@ObservableObject+@Published全局狀態變量
要使用全局狀態變量,則需要創建一個class,並遵守ObservableObject協議。 然後在這個類中定義一個@Published修飾的變量 @Published var userLandmarks, 當@Published修飾的變量更新時,那麼使用了@Published修飾的變量的視圖就會對應更新。
定義一個UserData,遵守ObservableObject協議
import SwiftUI
import Combine
 
class UserData: ObservableObject {
    @Published var userLandmarks:[Landmark] = landmarks
     
}
使用時,也要在對應的視圖中加上 @ObservedObject修飾符,然後更新這個變量self.userData.userLandmarks[self.userLandmarkIndex].isFeatured.toggle()
struct LandmarkDetail: View {
    var landmark: Landmark
    @ObservedObject var userData: UserData
     
    var userLandmarkIndex: Int {
        userData.userLandmarks.firstIndex(where: {$0.id == landmark.id})!
    }
 
    var body: some View {
        Button(action: {
            self.userData.userLandmarks[self.userLandmarkIndex]
                .isFeatured.toggle()
        }){
            if landmark.isFeatured {
                Image("icon_rcxinhua_selected")
                    .resizable().frame(width: 20, height: 20, alignment: .center)
            } else {
                Image("icon_rcxinhua_defaultselected")
                    .resizable().frame(width: 20, height: 20, alignment: .center)
            }
        }
    }
}

 

單向數據流
用戶操作導致@State變量發生了改變,
@State變量改變導致使用了@State變量的UI視圖就會自動更新
繼續等待用戶操作觸發@State變量發生變化
 

 

UIKit控件與SwiftUI中控件的轉換
UIKit轉SwiftUI
通過UIViewRepresentable協議將UIView包裝成SwifUI的View來使用。
struct MapView: UIViewRepresentable {
    let view: UIView = UIView()
    func makeUIView(context: Context) -> some UIView {
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
        view.backgroundColor = .red
    }
}
SwiftUI轉UIKit
通過UIHostingController將SwiftUI包裝成UIView
UIHostingController(rootView: ContentView())

 

Model模型定義
List,ForEach等要求被循環的每個元素都要有一個唯一的標識
這樣數據變更時,可以迅速定位刷新對應的UI,提高性能
所以,元素要遵循Identifiable協議(實現id協議)
struct Landmark: Identifiable {
    var id = UUID()
    let name: String
}

 

Demo地址
https://github.com/zhfei/SwiftBasicKnowledge
 
 
參考文章
https://www.jianshu.com/p/0f7215591b08
https://zhuanlan.zhihu.com/p/436779033
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章