You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 22:39:53