SwiftUI加载Dota英雄图片时数组越界问题求助
问题描述
在开发SwiftUI应用时,基于Firebase存储与Firestore实现Dota用户资料加载功能,运行项目时在favoriteHeroImages[0].favoriteHeroesModifier()行触发Thread 1: Fatal error: Index out of range错误。但通过按钮触发fetchHeroImages()方法则能正常显示图片,需要实现无需按钮自动加载且避免报错的方案。
相关代码如下:
ViewModel代码
import SwiftUI import Firebase import FirebaseStorage import FirebaseFirestore @MainActor final class DotaProfileExistingViewModel: ObservableObject { @Published var avatar: Image? @Published var avatarIsLoading = false @Published var nicknameTitle = "" @Published var rankTitle = "" @Published var position = "" @Published var favoriteHeroImages: [Image]? @Published var imagesAreLoading = false var favoriteHeroes = [String]() var avatarPath = "" func fetchDotaProfileData() throws { guard let user = Auth.auth().currentUser else { throw ErrorManager.noCurrentUser } let db = Firestore.firestore() let reference = db.collection("User Profiles").document(user.uid) reference.getDocument { snapshot, error in guard error == nil else { print(error!.localizedDescription) return } if let snapshot = snapshot { self.nicknameTitle = snapshot["nickName"] as? String ?? "" self.rankTitle = snapshot["rank"] as? String ?? "" self.position = snapshot["position"] as? String ?? "" self.favoriteHeroes = snapshot["favoriteHeroes"] as? [String] ?? [String]() self.avatarPath = snapshot["userAvatarPath"] as? String ?? "" } } } func fetchHeroImages() { imagesAreLoading = true Task { do { var heroImages = [Image]() for hero in favoriteHeroes { let data = try await StorageManager.shared.getData(heroName: hero + ".png") if let uiImage = UIImage(data: data) { let image = Image(uiImage: uiImage) heroImages.append(image) } } favoriteHeroImages = heroImages imagesAreLoading = false } catch { print("Error fetching Images of Heroes") } } } func fetchAvatar() { avatarIsLoading = true Task { do { let data = try await StorageManager.shared.fetchUserPhoto(userPhotoPath: avatarPath) if let uiImage = UIImage(data: data) { self.avatar = Image(uiImage: uiImage) avatarIsLoading = false } } catch { print("Error fetching Avatar") avatarIsLoading = false } } } }
View代码
struct DotaProfileExistingView: View { @StateObject private var viewModel = DotaProfileExistingViewModel() var body: some View { VStack { if let avatar = viewModel.avatar, !viewModel.avatarIsLoading { avatar.userAvatarModifier() } else { UserAvatarPlaceholder() } HStack { Text("Nickname:") Text(viewModel.nicknameTitle) } HStack { Text("Rank:") Text(viewModel.rankTitle) } HStack { Text("Position:") Text(viewModel.position) } HStack { if let favoriteHeroImages = viewModel.favoriteHeroImages, !viewModel.imagesAreLoading { favoriteHeroImages[0].favoriteHeroesModifier() favoriteHeroImages[1].favoriteHeroesModifier() favoriteHeroImages[2].favoriteHeroesModifier() } else { Image("Hero Placeholder") .favoriteHeroesModifier() Image("Hero Placeholder") .favoriteHeroesModifier() Image("Hero Placeholder") .favoriteHeroesModifier() } } .onAppear { do { try viewModel.fetchDotaProfileData() } catch { print("Error") } } } .task { viewModel.fetchAvatar() viewModel.fetchHeroImages() } } } struct DotaProfileExistingView_Previews: PreviewProvider { static var previews: some View { DotaProfileExistingView() } }
问题原因
错误核心是加载顺序不匹配:
.task修饰符会在视图出现时立即执行fetchHeroImages(),但此时fetchDotaProfileData()可能还未完成Firestore的数据拉取,favoriteHeroes为空数组- 空数组情况下,
fetchHeroImages()生成的favoriteHeroImages也是空数组,视图中直接通过favoriteHeroImages[0]硬索引就会触发越界错误 - 按钮触发时,
fetchDotaProfileData()已完成,favoriteHeroes有有效数据,因此fetchHeroImages()能生成正常数组,不会报错
解决方案
1. 修正数据依赖顺序,确保先拉取Profile再加载英雄图
将favoriteHeroes改为@Published属性,使其更新时能触发后续操作;同时在fetchDotaProfileData完成后自动调用fetchHeroImages():
修改ViewModel:
// 将普通属性改为@Published @Published var favoriteHeroes = [String]() func fetchDotaProfileData() throws { guard let user = Auth.auth().currentUser else { throw ErrorManager.noCurrentUser } let db = Firestore.firestore() let reference = db.collection("User Profiles").document(user.uid) reference.getDocument { snapshot, error in guard error == nil else { print(error!.localizedDescription) return } if let snapshot = snapshot { self.nicknameTitle = snapshot["nickName"] as? String ?? "" self.rankTitle = snapshot["rank"] as? String ?? "" self.position = snapshot["position"] as? String ?? "" self.favoriteHeroes = snapshot["favoriteHeroes"] as? [String] ?? [String]() self.avatarPath = snapshot["userAvatarPath"] as? String ?? "" // 拉取完Profile数据后,自动加载英雄图 Task { @MainActor in self.fetchHeroImages() } } } }
同时移除View中.task内的fetchHeroImages()调用:
.task { viewModel.fetchAvatar() // 删除 viewModel.fetchHeroImages() }
2. 视图中避免硬索引,添加安全判断
即使favoriteHeroImages长度不足3,也要保证不越界,用循环+索引判断渲染:
修改View中的HStack部分:
HStack { // 循环渲染3个位置,不足则显示占位符 ForEach(0..<3) { index in if let images = viewModel.favoriteHeroImages, !viewModel.imagesAreLoading, index < images.count { images[index].favoriteHeroesModifier() } else { Image("Hero Placeholder") .favoriteHeroesModifier() } } }
3. 优化fetchHeroImages的空数组处理
在fetchHeroImages开头添加判断,若favoriteHeroes为空则直接结束加载状态,避免无意义请求:
func fetchHeroImages() { imagesAreLoading = true // 空数组直接终止流程 guard !favoriteHeroes.isEmpty else { favoriteHeroImages = [] imagesAreLoading = false return } Task { do { var heroImages = [Image]() for hero in favoriteHeroes { let data = try await StorageManager.shared.getData(heroName: hero + ".png") if let uiImage = UIImage(data: data) { let image = Image(uiImage: uiImage) heroImages.append(image) } } favoriteHeroImages = heroImages imagesAreLoading = false } catch { print("Error fetching Images of Heroes") imagesAreLoading = false } } }
额外优化建议
- 将
fetchAvatar()也移到fetchDotaProfileData的回调中,确保avatarPath有值后再拉取头像,避免无效请求 - 将
favoriteHeroImages设为非可选空数组(@Published var favoriteHeroImages = [Image]()),简化视图中的判断逻辑
内容的提问来源于stack exchange,提问作者kruchman
相关产品推荐
相关产品推荐

