Redrawing Image SwiftUI

41 Views Asked by At

This is the view model

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
            }
        }
    }

}

And This is the related view file import SwiftUI

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()
    }
}

And when I run the project Im getting the error Thread 1: Fatal error: Index out of range on the line favoriteHeroImages[0].favoriteHeroesModifier(). However if Im adding the button, and I use the button action instead of task {} , the app is working correctly without any issue, and I have needed pictures on the screen. But the thing, is that I need it to work without using the button. May be somebody can help me with that, thank you people

1

There are 1 best solutions below

0
On

I would resolve this problem conceptually, by moving Image out of your viewModel, and into a view (since Image is a View, not an actual image, and so it belongs to UI), and you anyway need to convert it from UIImage to Image. So dtore UIImage in the model, while Image in the view, like this:

@Published var favoriteHeroImages: [UIImage]? // Store image, not view

then have a function in the View that does this:

@ViewBuilder func avatarFromIndex(_ index: Int) -> some View {
    if let favoriteHeroImages = viewModel.favoriteHeroImages, favoriteHeroImages.count > index {
        Image(uiImage: favoriteHeroImages[index])
            .favoriteHeroesModifier()
    } else {
        Image("Hero Placeholder")
            .favoriteHeroesModifier()
    }
}

And so change the view to:

HStack {
    avatarFromIndex(0)
    avatarFromIndex(1)
    avatarFromIndex(2)
}

The checks on array could be done more elegantly, see this for example. Also you could use ForEach if the intention is to have dynamic number of images.