如何自定義SwiftUI的組件

SwiftUI作為一個聲明式UI框架,如何自定義組件,是很多小伙伴都非常關心的問題。

struct MyView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

上述代碼就是一個標準的自定義組件實現。通過上述代碼,我們可以看到:

  1. 自定義組件必須被聲明為struct。
  2. 自定義組件必須滿足View協議。
  3. 自定義組件必須定義符合協議的計算屬性:body。

我們可以通過上述的規范,來定義需要的元素,組成組件。

當然,我們的組件也可以傳參:

struct MyView: View {
    var text: String
    
    var body: some View {
        Text(text)
            .font(.largeTitle)
            .padding()
            .background(Color.blue)
            .clipShape(Capsule())
    }
}

struct ContentView: View {
    var body: some View {
        MyView(text: "Hello World")
    }
}

如果傳入的參數發生了變化,當前組件也會重新渲染。

這里有一點和UIKit不同的是,在UIKit里,我們處理的往往是一個固定的組件,內部結構和元素基本上都是確定的,沒有變化,如果涉及到需要定義某一部分元素的,UITableView, UICollectionView等也都幫我們處理好了。
但是這個也不是沒有代價的,這里的代價就是我們只能使用UIKit提供的布局組件,有很多限制和學習成本。 同時我們想自行定義布局時,也很麻煩。

而SwiftUI不同,我們在SwiftUI中定義布局組件的成本還是相當低的。
比如下面的代碼就是我們定義一個GridView所需要的代碼:

struct GridStack<Content: View>: View {
    let rows: Int
    let columns: Int
    let content: (Int, Int) -> Content
    
    var body: some View {
        VStack {
            ForEach(0..<rows, id: \.self) { row in
                HStack {
                    ForEach(0..<self.columns, id: \.self) { column in
                        self.content(row, column)
                    }
                }
            }
        }
    }
}

在這里,我們看到,GridStack只定義了Content如何擺放,如何重復,卻沒有定義Content到底是什么組件。看起來是不是有點像UICollectionView呢?
上述的代碼,可以以下面的形式進行調用。

struct ContentView1: View {
    var body: some View {
        GridStack(rows: 3, columns: 3) { (row, col) in
            Text("R\(row)C\(col)")
        }
    }
}

但是這樣聲明還是有問題的,就是Content無法一次性傳入多個元素,只能傳入一個參數。

為了解決這個問題,我們需要引入ViewBuilder。

struct GridStack<Content: View>: View {
    let rows: Int
    let columns: Int
    let content: (Int, Int) -> Content
    
    init(rows: Int, columns: Int, @ViewBuilder _ content: @escaping (Int, Int) -> Content) {
        self.rows = rows
        self.columns = columns
        self.content = content
    }
    
    var body: some View {
        VStack {
            ForEach(0..<rows, id: \.self) { row in
                HStack {
                    ForEach(0..<self.columns, id: \.self) { column in
                        self.content(row, column)
                    }
                }
            }
        }
    }
}

在聲明時,給content參數加上@ViewBuilder的注解,就可以一次性傳入多個元素。類似下面這樣。

struct ContentView: View {
    var body: some View {
        GridStack(rows: 3, columns: 3) { (row, col) in
            Text("R\(row)C\(col)")
            Text("R\(row)C\(col)")
            Text("R\(row)C\(col)")
        }
    }
}

但是這里仍然有問題,就是這個組件內部無法傳遞超過10個元素。這個是ViewBuilder的設計限制的。

參考:

View
ViewBuilder
awesome-function-builders

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容