
概述
各位似禿非禿小碼農(nóng)們都知道,在 SwiftUI 中視圖是狀態(tài)的函數(shù),這意味著狀態(tài)的改變會(huì)導(dǎo)致界面被刷新。
但是,對(duì)于有些復(fù)雜布局的 SwiftUI 視圖來(lái)說(shuō),它們的界面并不能直接映射到對(duì)應(yīng)的狀態(tài)上去。這就會(huì)造成一個(gè)問(wèn)題:狀態(tài)的改變并沒(méi)有及時(shí)的引起 UI 的變化。

如上圖所示:無(wú)論英雄挑戰(zhàn)關(guān)卡的結(jié)果是成功還是失敗,在視圖的顯示中都沒(méi)有體現(xiàn)出來(lái)。這該如何是好呢?
在本篇博文中,您將學(xué)到如下內(nèi)容:
- “固執(zhí)己見(jiàn)”的 SwiftUI 視圖
1.1 關(guān)卡視圖 StageView
1.2 世界視圖 WorldView
相信學(xué)完本課后,大家都會(huì)掌握只需寥寥幾行代碼就讓 SwiftUI 復(fù)雜視圖乖乖聽(tīng)話的奧義!
那還等什么呢?Let‘s go?。。?)
1. “固執(zhí)己見(jiàn)”的 SwiftUI 視圖
在上面的示例圖中,英雄可以恣意挑戰(zhàn)當(dāng)前關(guān)卡,如果挑戰(zhàn)成功則進(jìn)入下一關(guān)卡,如果失敗則會(huì)刷新挑戰(zhàn)次數(shù)。前者應(yīng)該在世界視圖 WorldView 上有所體現(xiàn),而后者則必須在關(guān)卡視圖 StageView 中立即反映出來(lái):
Button {
if try! hero.challengeStage() {
try! hero.moveToNextStage()
}
} label: {
Label("挑戰(zhàn)關(guān)卡!", systemImage: "figure.fencing")
.foregroundStyle(.white)
}
可惜的是,無(wú)論何種情況所有視圖都將成為不舞之鶴,無(wú)動(dòng)于衷。這是“腫”么回事呢?
1.1 關(guān)卡視圖 StageView
我們先到 StageView 中看看與此相關(guān)的 UI 布局代碼:
let records = stage.queryAllChallengingRecords()
if records.isEmpty {
ContentUnavailableView("一片寂靜,無(wú)人在此逗留...", systemImage: "eyes")
} else {
ForEach(records) { record in
if let hero = record.hero {
VStack {
heroCell(hero)
HStack {
Text("失敗次數(shù): \(record.challengeFailedCount)")
Spacer(minLength: 0)
if let timeString = try! hero.getStageStayRelevantTimeString(stage) {
Text("已徘徊 \(timeString)")
}
}
.foregroundStyle(.gray)
}
}
}
}
理想的情況是:當(dāng)英雄挑戰(zhàn)關(guān)卡失敗時(shí),將會(huì)遞增對(duì)應(yīng)關(guān)卡挑戰(zhàn)記錄中挑戰(zhàn)的次數(shù),這會(huì)引起界面相關(guān)顯示的變化。

但實(shí)際運(yùn)行發(fā)現(xiàn),界面并沒(méi)有立即刷新。而只有當(dāng)視圖重建后,失敗的挑戰(zhàn)次數(shù)才能得以更新:

1.2 世界視圖 WorldView
WorldView 視圖是 StageView 的父視圖,它的關(guān)鍵代碼如下所示:
Form {
let zones = world.zoneSortByNumberAry
ForEach(zones) { zone in
Section {
HStack {
Image(systemName: "map")
Text("\(zone.number). \(zone.name ?? "")")
}
.font(.title2.bold())
if let desc = zone.desc, !desc.isEmpty {
Text(desc)
.foregroundStyle(.gray)
}
ScrollView {
LazyVGrid(columns: [GridItem](repeating: .init(), count: 3)) {
ForEach(zone.stageSortByNumberAry) { stage in
NavigationLink {
StageView(stage: stage)
} label: {
VStack(alignment: .leading) {
HStack {
stageLogoImage(stage)
Text("\(stage.number)")
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.title3.bold())
.foregroundStyle(stageColor(stage))
Text("\(stage.name ?? "")")
.monospaced()
.minimumScaleFactor(0.8)
.foregroundStyle(stageColor(stage))
Spacer()
HStack {
let challengingHeros = stage.queryAllChallengingRecords().count
let victoryHeros = stage.queryAllVictoryRecords().count
VStack(alignment: .leading) {
Image(systemName: "person.3")
Text("\(challengingHeros)/\(victoryHeros)")
}
.minimumScaleFactor(0.5)
.font(.subheadline)
.foregroundStyle(.teal)
Spacer(minLength: 0)
let includeHeros = stageChallengingMyHerosCount(stage)
if includeHeros > 0 {
HStack(spacing: 0) {
Image(systemName: "star.hexagon")
Text("\(includeHeros)")
}
.font(.subheadline)
.foregroundStyle(.yellow)
}
}
}
.padding()
.frame(maxWidth: .infinity)
.frame(height: 150)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 11))
}
}
}
}
.listRowSeparator(.hidden)
}
}
}
.navigationTitle("世界地圖")
在上面的代碼中,我們主要做了這樣幾件事:
- 獲取 World 中的所有區(qū)域(Zones),并將結(jié)果存入 zones 局部變量中;
- 獲取每個(gè) Zone 中的所有關(guān)卡(Stages),并將它們放入 LazyVGrid 容器中以便顯示;
- 如果我們的英雄恰巧正在挑戰(zhàn)某一關(guān)卡,則在對(duì)應(yīng)關(guān)卡 StageCell 里用黃色的五角星表示出來(lái);
WorldView 視圖的顯示效果如下所示:

理想情況下,當(dāng)英雄成功挑戰(zhàn)某一關(guān)卡并從關(guān)卡視圖返回世界視圖后,世界視圖中關(guān)卡 StageCell 中的黃色五角星應(yīng)該自動(dòng)移動(dòng)到下一關(guān)。但是,從演示圖中可以看到,這些都沒(méi)有發(fā)生。這表示 WorldView 視圖內(nèi)容在 Hero 挑戰(zhàn)成功后也沒(méi)有被及時(shí)地刷新:

那么,我們不禁要問(wèn):到底是什么導(dǎo)致了 StageView 和 WorldView 視圖刷新不及時(shí)呢?
在下一篇博文中,我們將繼續(xù)介紹導(dǎo)致上述問(wèn)題的根本原因,并先提出幾個(gè)不那么優(yōu)雅地解決方案唏噓一番。不見(jiàn)不散!
總結(jié)
在本篇博文中,我們發(fā)現(xiàn)了一個(gè) SwiftUI 復(fù)雜視圖中狀態(tài)的改變并未正確引起界面刷新的現(xiàn)象,并隨后深入代碼初步分析了故事的前因后果。
感謝觀賞,我們下一篇再見(jiàn)!8-)