2

As mentioned in the headline, I try to load images to a custom object I’ve got the custom object “User” that contains the property “imageLink” that stores the location within the Firebase Storage.

First I load the users frome the Firestore db and then I try to load the images for these users asynchronous from the Firebase Storage and show them on the View. As long as the image has not been loaded, a placeholder shall be shown. I tried several implementations and I always can see in the debugger that I am able to download the images (I saw the actual image and I saw the size of some 100kb), but the loaded images don’t show on the view, I still see the placeholder, it seems that the view does not update after they loaded completely.

From my perspective, the most promising solution was:

FirebaseImage

import Combine
import FirebaseStorage
import UIKit

let placeholder = UIImage(systemName: "person")!

struct FirebaseImage : View {

    init(id: String) {
        self.imageLoader = Loader(id)
    }

    @ObservedObject private var imageLoader : Loader

    var image: UIImage? {
        imageLoader.data.flatMap(UIImage.init)
    }

    var body: some View {
        Image(uiImage: image ?? placeholder)
    }
}

Loader

import SwiftUI
import Combine
import FirebaseStorage

final class Loader : ObservableObject {
    let didChange = PassthroughSubject<Data?, Never>()
    var data: Data? = nil {
        didSet { didChange.send(data) }
    }

    init(_ id: String){
        // the path to the image
        let url = "profilepics/\(id)"
        print("load image with id: \(id)")
        let storage = Storage.storage()
        let ref = storage.reference().child(url)
        ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
            if let error = error {
                print("\(error)")
            }

            DispatchQueue.main.async {
                self.data = data
            }
        }
    }
}

User

import Foundation
import Firebase
import CoreLocation
import SwiftUI

struct User: Codable, Identifiable, Hashable {
    
    var id: String?
    var name: String
    var imageLink: String
    var imagedata: Data = .init(count: 0)
    
    init(name: String, imageLink: String, lang: Double) {
        self.id = id
        self.name = name
        self.imageLink = imageLink
    }
    
    init?(document: QueryDocumentSnapshot) {
        let data = document.data()
        
        guard let name = data["name"] as? String else {
            return nil
        }
        
        guard let imageLink = data["imageLink"] as? String else {
            return nil
        }
        
        id = document.documentID
        self.name = name
        self.imageLink = imageLink
    }
}

extension User {

    var image: Image {
        Image(uiImage: UIImage())
        }
}

extension User: DatabaseRepresentation {
    
    var representation: [String : Any] {
        var rep = ["name": name, "imageLink": imageLink] as [String : Any]
        
        if let id = id {
            rep["id"] = id
        }
        
        return rep
    }
    
}

extension User: Comparable {
    
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }
    
    static func < (lhs: User, rhs: User) -> Bool {
        return lhs.name < rhs.name
    }
}

UserViewModel

import Foundation
import FirebaseFirestore
import Firebase

class UsersViewModel: ObservableObject {
    
    let db = Firestore.firestore()
    let storage = Storage.storage()

    @Published var users = [User]()
    
    
    @Published var showNewUserName: Bool = UserDefaults.standard.bool(forKey: "showNewUserName"){
        didSet {
            UserDefaults.standard.set(self.showNewUserName, forKey: "showNewUserName")
            NotificationCenter.default.post(name: NSNotification.Name("showNewUserNameChange"), object: nil)
        }
    }
    
    
    @Published var showLogin: Bool = UserDefaults.standard.bool(forKey: "showLogin"){
        didSet {
            UserDefaults.standard.set(self.showLogin, forKey: "showLogin")
            NotificationCenter.default.post(name: NSNotification.Name("showLoginChange"), object: nil)
        }
    }
    
    @Published var isLoggedIn: Bool = UserDefaults.standard.bool(forKey: "isLoggedIn"){
        didSet {
            UserDefaults.standard.set(self.isLoggedIn, forKey: "isLoggedIn")
            NotificationCenter.default.post(name: NSNotification.Name("isLoggedInChange"), object: nil)
        }
    }
    
    func addNewUserFromData(_ name: String, _ imageLing: String, _ id: String) {
        do {
            let uid = Auth.auth().currentUser?.uid
            let newUser = User(name: name, imageLink: imageLing, lang: 0, long: 0, id: uid)
            try db.collection("users").document(newUser.id!).setData(newUser.representation) { _ in
                self.showNewUserName = false
                self.showLogin = false
                self.isLoggedIn = true
            }
        } catch let error {
            print("Error writing city to Firestore: \(error)")
        }
    }
    
    func fetchData() {
        db.collection("users").addSnapshotListener { (querySnapshot, error) in
            guard let documents = querySnapshot?.documents else {
                print("No documents")
                return
            }
            
            self.users = documents.map { queryDocumentSnapshot -> User in
                let data = queryDocumentSnapshot.data()
                let id = data["id"] as? String ?? ""
                let name = data["name"] as? String ?? ""
                let imageLink = data["imageLink"] as? String ?? ""
                let location = data["location"] as? GeoPoint
                let lang = location?.latitude ?? 0
                let long = location?.longitude ?? 0
                Return User(name: name, imageLink: imageLink, lang: lang, long: long, id: id)
            }
        }
    }
}

UsersCollectionView

import SwiftUI

struct UsersCollectionView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @EnvironmentObject var usersViewModel: UsersViewModel
    
    
    let itemWidth: CGFloat = (screenWidth-30)/4.2
    let itemHeight: CGFloat = (screenWidth-30)/4.2
    
    var fixedLayout: [GridItem] {
        [
            .init(.fixed((screenWidth-30)/4.2)),
            .init(.fixed((screenWidth-30)/4.2)),
            .init(.fixed((screenWidth-30)/4.2)),
            .init(.fixed((screenWidth-30)/4.2))
        ]
    }
    
    func debugUserValues() {
        for user in usersViewModel.users {
            print("ID: \(user.id), Name: \(user.name), ImageLink: \(user.imageLink)")
        }
    }
    
    var body: some View {
        VStack() {

            ScrollView(showsIndicators: false) {
                LazyVGrid(columns: fixedLayout, spacing: 15) {
                    ForEach(usersViewModel.users, id: \.self) { user in
                        VStack() {

                            FirebaseImage(id: user.imageLink)
                            
                            HStack(alignment: .center) {
                                Text(user.name)
                                    .font(.system(size: 16))
                                    .fontWeight(.bold)
                                    .foregroundColor(Color.black)
                                    .lineLimit(1)
                            }
                        }
                    }
                }
                .padding(.top, 20)
                
                Rectangle()
                    .fill(Color .clear)
                    .frame(height: 100)
            }
            
        }
        .navigationTitle("Find Others")
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading:
                    Button(action: {
                        self.presentationMode.wrappedValue.dismiss()
                    }) {
                        HStack {
                            Image(systemName: "xmark")
                                .foregroundColor(.black)
                                .padding()
                                .offset(x: -15)
                        }
                })
    }
}
Sebastian Fox
  • 1,324
  • 1
  • 10
  • 20

1 Answers1

3

You're using an old syntax from BindableObject by using didChange -- that system changed before SwiftUI 1.0 was out of beta.

A much easier approach would be to use @Published, which your view will listen to automatically:

final class Loader : ObservableObject {
    @Published var data : Data?

    init(_ id: String){
        // the path to the image
        let url = "profilepics/\(id)"
        print("load image with id: \(id)")
        let storage = Storage.storage()
        let ref = storage.reference().child(url)
        ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
            if let error = error {
                print("\(error)")
            }

            DispatchQueue.main.async {
                self.data = data
            }
        }
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94