概述
各位似秃非秃小码农们都知道,在 SwiftUI 中视图是状态的函数,这意味着状态的改变会导致界面被刷新。
但是,对于有些复杂布局的 SwiftUI 视图来说,它们的界面并不能直接映射到对应的状态上去。这就会造成一个问题:状态的改变并没有及时的引起 UI 的变化。
如上图所示:无论英雄挑战关卡的结果是成功还是失败,在视图的显示中都没有体现出来。这该如何是好呢?
在本篇博文中,您将学到如下内容:
-
- “固执己见”的 SwiftUI 视图
- 1.1 关卡视图 StageView
- 1.2 世界视图 WorldView
相信学完本课后,大家都会掌握只需寥寥几行代码就让 SwiftUI 复杂视图乖乖听话的奥义!
那还等什么呢?Let‘s go!!!;)
1. “固执己见”的 SwiftUI 视图
在上面的示例图中,英雄可以恣意挑战当前关卡,如果挑战成功则进入下一关卡,如果失败则会刷新挑战次数。前者应该在世界视图 WorldView 上有所体现,而后者则必须在关卡视图 StageView 中立即反映出来:
Button {
if try! hero.challengeStage() {
try! hero.moveToNextStage()
}
} label: {
Label("挑战关卡!", systemImage: "figure.fencing")
.foregroundStyle(.white)
}
可惜的是,无论何种情况所有视图都将成为不舞之鹤,无动于衷。这是“肿”么回事呢?
1.1 关卡视图 StageView
我们先到 StageView 中看看与此相关的 UI 布局代码:
let records = stage.queryAllChallengingRecords()
if records.isEmpty {
ContentUnavailableView("一片寂静,无人在此逗留...", systemImage: "eyes")
} else {
ForEach(records) { record in
if let hero = record.hero {
VStack {
heroCell(hero)
HStack {
Text("失败次数: \(record.challengeFailedCount)")
Spacer(minLength: 0)
if let timeString = try! hero.getStageStayRelevantTimeString(stage) {
Text("已徘徊 \(timeString)")
}
}
.foregroundStyle(.gray)
}
}
}
}
理想的情况是:当英雄挑战关卡失败时,将会递增对应关卡挑战记录中挑战的次数,这会引起界面相关显示的变化。
但实际运行发现,界面并没有立即刷新。而只有当视图重建后,失败的挑战次数才能得以更新:
1.2 世界视图 WorldView
WorldView 视图是 StageView 的父视图,它的关键代码如下所示:
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 中的所有区域(Zones),并将结果存入 zones 局部变量中;
- 获取每个 Zone 中的所有关卡(Stages),并将它们放入 LazyVGrid 容器中以便显示;
- 如果我们的英雄恰巧正在挑战某一关卡,则在对应关卡 StageCell 里用黄色的五角星表示出来;
WorldView 视图的显示效果如下所示:
理想情况下,当英雄成功挑战某一关卡并从关卡视图返回世界视图后,世界视图中关卡 StageCell 中的黄色五角星应该自动移动到下一关。但是,从演示图中可以看到,这些都没有发生。这表示 WorldView 视图内容在 Hero 挑战成功后也没有被及时地刷新:
那么,我们不禁要问:到底是什么导致了 StageView 和 WorldView 视图刷新不及时呢?
在下一篇博文中,我们将继续介绍导致上述问题的根本原因,并先提出几个不那么优雅地解决方案唏嘘一番。不见不散!
总结
在本篇博文中,我们发现了一个 SwiftUI 复杂视图中状态的改变并未正确引起界面刷新的现象,并随后深入代码初步分析了故事的前因后果。
感谢观赏,我们下一篇再见!8-)