一發(fā)入魂:極簡(jiǎn)解決 SwiftUI 復(fù)雜視圖未能正確刷新的問(wèn)題(上)

概述

各位似禿非禿小碼農(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)容:

  1. “固執(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-)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容