更有效率的 LazyHStack & LazyVStack

開發 SwiftUI App 時,我們時常用 HStack & VStack 排版。雖然它們很方便,不過卻有個令人小小在意的小缺點,當 stack 裡裝了很多 view 時,它們會一次全部生成,影響 App 的效能。

比方以下呈現 5 月 31 天的例子,當 ContentView 顯示時,31 個 DayView 會一次全部生成。(ps: 5/11 是 Wendy 的生日,所以特別放大)

struct DayView: View {
let day: Int

var body: some View {
VStack {
Image(systemName: "calendar.circle")
Text("5/\(day)")
}
.font(day == 11 ? .largeTitle : .body)
}
}
struct ContentView: View {
var body: some View {

ScrollView(.horizontal) {
HStack {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
}
}
}

我們也可以在 DayView 加入 init,證明彼得潘真心不騙,它真的在一開始生成 31 個 DayView。

struct DayView: View {
init(day: Int) {
self.day = day
print("create DayView \(day)")
}

更有效率的 LazyHStack & LazyVStack

在 SwiftUI 2,Apple 發明更有效率的 LazyHStack & LazyVStack,LazyHStack & LazyVStack 裡的元件將在需要時才生成。

我們將剛剛例子的 HStack 改成 LazyHStack 試試吧。

struct ContentView: View {
var body: some View {

ScrollView(.horizontal) {
LazyHStack {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
}
}
}

從 App 畫面看不出差異,但魔鬼藏在細節裡,讓我們仔細觀察印出的訊息。一開始只印到 create DayView 13,表示一開始只產生 13 個 DayView,當我們捲動 scroll view,想看接下來的日期時,才會再生成需要顯示的 DayView。當我們 scroll view 捲到底,看完所有內容時,才會生成全部 31 個 DayView。

同樣的,我們也可以改用 LazyVStack,裡面的元件一樣會在需要時才生成。

struct ContentView: View {
var body: some View {

ScrollView(.vertical) {
LazyVStack {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
}
}
}

LazyHStack & LazyVStack 的大小

LazyHStack & LazyVStack 還有一些小地方跟 HStack & VStack 不太一樣,比方它們的大小。

HStack & VStack 預設的大小將是剛好容納內容的大小,比方下圖的 HStack。

而 LazyHStack & LazyVStack 預設的大小將佔滿可用的空間,因此下圖的 LazyHStack 佔據了扣除 Safe Area 的螢幕高度。

我們也可以利用 frame 另外控制它的大小,比方將 LazyHStack 的高度設為 300。

ScrollView(.horizontal) {
LazyHStack {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
.frame(height: 300)
}

若想讓剛剛 LazyHStack 的高度剛好等於內容的高度,我們也可以呼叫 modifier fixedSize。(ps: 假設 LazyHStack 裡每個元件的高度是一樣的,如果元件彼此的高度不一樣,此做法將有問題。)

ScrollView(.horizontal) {
LazyHStack {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
.fixedSize()
.background(Color.yellow)
}

LazyHStack & LazyVStack 的 alignment & spacing

我們也可以設定 LazyHStack & LazyVStack 裡元件的對齊(alignment)和間距(spacing)。

struct ContentView: View {
var body: some View {



VStack {
ScrollView(.horizontal) {
LazyHStack(alignment: .top, spacing: 30) {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
.frame(height: 200)
.background(Color.yellow)
}

ScrollView(.horizontal) {
LazyHStack(alignment: .center, spacing: 30) {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
.frame(height: 200)
.background(Color.yellow)
}

ScrollView(.horizontal) {
LazyHStack(alignment: .bottom, spacing: 30) {
ForEach(1..<32) { (index) in
DayView(day: index)
}
}
.frame(height: 200)
.background(Color.yellow)
}
}
}
}

LazyHStack & LazyVStack 的 header & footer

LazyHStack & LazyVStack 裡可分成多個 Section,每個 Section 可包含 header,content & footer。

struct DayView: View {
let month: Int
let day: Int

var body: some View {
VStack {
Image(systemName: "calendar.circle")
Text("\(month)/\(day)")
}
}
}
struct ContentView: View {

var monthDic = [
"5": 31,
"6": 30
]

var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(5..<7) { (month) in
Section(header: Text("\(month)月"), footer: Text("\(monthDic["\(month)"]!)天")) {
ForEach(1..<monthDic["\(month)"]!+1) { (day) in
DayView(month: month, day: day)
}
}

}

}
}
}
}

以上程式以 Section 呈現每個月的日期,header 顯示幾月,footer 顯示天數,content 則是每一天的日期。

卡住不動(pinned)的 header & footer

我們也可以讓 header & fotter 在滑動時卡住不動,比方滑動 5 月的日期時,剛剛例子的 header 5 月將卡住不動,直到將滑完 5 月的日期,要顯示 6 月的日期時,header 5 月才會滑到螢幕外。

生成 LazyHStack 時,參數 pinnedViews 可傳入 sectionHeaders & sectionFooters 控制 header & footer 是否卡住。

struct ContentView: View {

var monthDic = [
"5": 31,
"6": 30
]

var body: some View {
ScrollView(.horizontal) {
LazyHStack(pinnedViews: [.sectionHeaders, .sectionFooters]) {
ForEach(5..<7) { (month) in
Section(header: Text("\(month)月")
.background(Color.yellow), footer: Text("\(monthDic["\(month)"]!)天")) {
ForEach(1..<monthDic["\(month)"]!+1) { (day) in
DayView(month: month, day: day)
}
}
}

}
}
}
}

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com