0

I'm building an application where a user can store companies information (like name, address, latitude, longitude to a Firebase real-time database). Companies Firebase data

With that information, those companies are then presented in a map as annotations.

My viewDidLoad is shown below

override func viewDidLoad() {
    super.viewDidLoad()

    // Definitions
    let autenticacao = Auth.auth()
    let idUsuarioLogado = (autenticacao.currentUser?.uid)!
    let database = Database.database().reference()
    let usuarios = database.child("usuarios")
    let clinicas = database.child("clinicas")

    // Map - User location
    self.mapa.delegate = self
    self.gerenciadorLocalizacao.delegate = self
    self.gerenciadorLocalizacao.desiredAccuracy = kCLLocationAccuracyBest
    self.gerenciadorLocalizacao.requestWhenInUseAuthorization()
    self.gerenciadorLocalizacao.startUpdatingLocation()

    // retrieve user information
    usuarios.child(idUsuarioLogado).observe(DataEventType.value) { (snapshot) in
        let dados = snapshot.value as? NSDictionary

        let emailUsuario = dados?["email"] as! String
        let nomeUsuario = dados?["nome"] as! String
        let perfilUsuario = dados?["perfil"] as! String
        let idUsuario = snapshot.key

        let usuario = Usuario(email: emailUsuario, nome: nomeUsuario, uid: idUsuario, perfil: perfilUsuario)
        print("User profile \(perfilUsuario)")
    }

    // Clinicas listeners
    clinicas.observe(DataEventType.childAdded) { (snapshot) in
        let dados = snapshot.value as? NSDictionary
        //print("Dados na leitura \(dados)")
        let clinica = Clinica()
        clinica.identificador = snapshot.key
        clinica.nome = dados?["nome"] as! String
        clinica.endereco = dados?["endereco"] as! String
        clinica.cidade = dados?["cidade"] as! String
        clinica.cep = dados?["cep"] as! String
        clinica.estado = dados?["estado"] as! String
        clinica.latitude = dados?["latitude"] as! String
        clinica.longitude = dados?["longitude"] as! String
        clinica.urlImagem = dados?["urlImagem"] as! String
        clinica.idImagem = dados?["idImagem"] as! String
        self.clinicasR.append(clinica)
    }

    // add annotations to the map
    for oneObject in self.todasAnotacoes {
        print("Oneobj \(oneObject)")
        let umaAnotacao = MinhaAnotacao()
        var oneObjLoc: CLLocationCoordinate2D = CLLocationCoordinate2DMake(oneObject.objLat, oneObject.objLong)
        umaAnotacao.coordinate = oneObjLoc
        umaAnotacao.title = oneObject.objName
        umaAnotacao.subtitle = oneObject.objDesc
        umaAnotacao.category = oneObject.objCat
        self.anotacaoArray.append(umaAnotacao)
        self.mapa.addAnnotations(self.anotacaoArray)
    }
}

My viewDidLoad is "structured" in 4 main blocks (even though they run in a different order given they are asynchronous):

  1. Definition;
  2. Retrieve user profile;
  3. Retrieve companies information (name, latitude, longitude);
  4. Add annotations to the map.

As those are asynchronous functions, they run in different order and this is what is causing trouble to me. note: There is still one piece missing here that is to feed all annotations to the variable todasAnotacoes which I'll do once I can retrieve data before triggering add annotation "block".

As annotation info comes from the firebase database, I should only have it executed once the clinicas.observe(DataEventType.childAdded) { (snapshot) in is concluded.

As is today, the sequence Xcode runs is:

  • Definitions
  • Add annotations;
  • retrieve data from firebase with companies details

I've tried adding the add annotation block to the closure, adding a dispatchqueue but none of those really worked. I also did a lot os search in stackoverflow but I couldn't find anything that i can use (or I wasn't able to understand).

So, in summary I need to run the add annotations after retrieving all data from Firebase.

Any ideas on how I could do that?

EDIT 1 - Final code with the suggested updates

override func viewDidLoad() {
    super.viewDidLoad()

    // Retrieving Logger user data and hidding "add" button if applicable
    ProgressHUD.show("Carregando...")
    let autenticacao = Auth.auth()
    let idUsuarioLogado = (autenticacao.currentUser?.uid)!
    let database = Database.database().reference()
    let usuarios = database.child("usuarios")
    var isUserLoaded = false
    var isClinicsLoaded = false

    usuarios.child(idUsuarioLogado).observe(DataEventType.value) { (snapshot) in
        let dados = snapshot.value as? NSDictionary

        let emailUsuario = dados?["email"] as! String
        let nomeUsuario = dados?["nome"] as! String
        let perfilUsuario = dados?["perfil"] as! String
        let idUsuario = snapshot.key

        let usuario = Usuario(email: emailUsuario, nome: nomeUsuario, uid: idUsuario, perfil: perfilUsuario)
        isUserLoaded = true
        if (isUserLoaded && isClinicsLoaded) {
            self.addAnnotationsToMap();
        }
        //print("\(usuario.email) e \(usuario.nome) e \(usuario.uid) e \(usuario.perfil)")
    }


    // Option to code above
    /*if Auth.auth().currentUser != nil {
     if let uid = (Auth.auth().currentUser?.uid) {

     let database = Database.database().reference()
     let usuarios = database.child("usuarios").child(uid)

     usuarios.observe(.value) { (snapshot) in
     let dados = snapshot.value as? NSDictionary

     let emailUsuario = dados?["email"] as! String
     let nomeUsuario = dados?["nome"] as! String
     let perfilUsuario = dados?["perfil"] as! String
     let idUsuario = snapshot.key

     let usuario = Usuario(email: emailUsuario, nome: nomeUsuario, uid: idUsuario, perfil: perfilUsuario)
     print("Got here \(usuario.email) e \(usuario.nome) e \(usuario.uid) e \(usuario.perfil)")
     if perfilUsuario != "admin" {
     self.navigationItem.rightBarButtonItems?.remove(at: 1)
     print("Disable + Button")
     }

     }
     }
     }*/

    // Map - User location
    self.mapa.delegate = self
    self.gerenciadorLocalizacao.delegate = self
    self.gerenciadorLocalizacao.desiredAccuracy = kCLLocationAccuracyBest
    self.gerenciadorLocalizacao.requestWhenInUseAuthorization()
    self.gerenciadorLocalizacao.startUpdatingLocation()
    let clinicas = database.child("clinicas")

    // Clinicas listeners
    clinicas.observe(DataEventType.value) { (snapshots) in
        for child in snapshots.children {
            let snapshot = child as! DataSnapshot

            print("Clinicas Mapeadas - end")
            let dados = snapshot.value as? NSDictionary
            //print("Dados na leitura \(dados)")
            let clinica = Clinica()
            clinica.identificador = snapshot.key
            clinica.nome = dados?["nome"] as! String
            clinica.endereco = dados?["endereco"] as! String
            clinica.cidade = dados?["cidade"] as! String
            clinica.cep = dados?["cep"] as! String
            clinica.estado = dados?["estado"] as! String
            clinica.latitude = dados?["latitude"] as! String
            clinica.longitude = dados?["longitude"] as! String
            clinica.urlImagem = dados?["urlImagem"] as! String
            clinica.idImagem = dados?["idImagem"] as! String

            self.clinicasR.append(clinica)
            self.todasAnotacoes.append((objLat: Double(clinica.latitude) as! CLLocationDegrees, objLong: Double(clinica.longitude) as! CLLocationDegrees, objName: clinica.nome, objDesc: clinica.endereco, objId: clinica.identificador))
        }
        isClinicsLoaded = true
        if (isUserLoaded && isClinicsLoaded) {
            self.addAnnotationsToMap();
        }
    }

    /* NOT IN USE FOR NOW
     let latitude = Double(-23.623558)
     let longitude = Double(-46.580787)
     let localizacao: CLLocationCoordinate2D = CLLocationCoordinate2D.init(latitude: latitude, longitude: longitude)
     let span: MKCoordinateSpan = MKCoordinateSpan.init(latitudeDelta: 0.01, longitudeDelta: 0.01)
     let regiao = MKCoordinateRegion.init(center: localizacao, span: span)
     self.mapa.setRegion(regiao, animated: true)*/

    ProgressHUD.dismiss()
}

// add annotations to the map
func addAnnotationsToMap() {
    anotacaoArray = []
    for oneObject in self.todasAnotacoes {
        for oneObject in self.todasAnotacoes {
            // print("Oneobj \(oneObject)")
            let umaAnotacao = MinhaAnotacao()
            var oneObjLoc: CLLocationCoordinate2D = CLLocationCoordinate2DMake(oneObject.objLat, oneObject.objLong)
            umaAnotacao.coordinate = oneObjLoc
            umaAnotacao.title = oneObject.objName
            umaAnotacao.subtitle = oneObject.objDesc
            umaAnotacao.identicadorMapa = oneObject.objId
            self.anotacaoArray.append(umaAnotacao)
            print("Annotation added \(todasAnotacoes.count) - end")

        }
        self.mapa.addAnnotations(self.anotacaoArray)
        self.todasAnotacoes = []
        self.anotacaoArray = []

        // print("Annotations added 2 - end")

    }
}

1 Answers1

1

The second observer in your code is using .childAdded, which means that closure gets called for each individual child node under clinicas. This makes it hard to know when you're done with clinics, so I recommend first switching that observer over to .value, with something like:

clinicas.observe(DataEventType.value) { (snapshots) in
  for child in snapshots.children {
    let snapshot = child as! DataSnapshot

    let dados = snapshot.value as? NSDictionary
    //print("Dados na leitura \(dados)")
    let clinica = Clinica()
    clinica.identificador = snapshot.key
    clinica.nome = dados?["nome"] as! String
    clinica.endereco = dados?["endereco"] as! String
    clinica.cidade = dados?["cidade"] as! String
    clinica.cep = dados?["cep"] as! String
    clinica.estado = dados?["estado"] as! String
    clinica.latitude = dados?["latitude"] as! String
    clinica.longitude = dados?["longitude"] as! String
    clinica.urlImagem = dados?["urlImagem"] as! String
    clinica.idImagem = dados?["idImagem"] as! String
    self.clinicasR.append(clinica)
  }
  // At this point we're done with all clinics
}

As you'll have notice this code now uses a loop to iterate over snapshots.children. So we're processing all child nodes of clinicas in one closure, which makes it much easier to know when we're done.


Now with the above change we have two closures that get called individually, and you have some code that you want to run when both of them are done. There are a few ways to synchronize this code.

The first way is what you've already tried: putting the clinicas.observe block into the closure of usuarios.child(idUsuarioLogado).observe(. Now that the clinics observer uses .value, you'll find that this is much easier to get working.

But I'd like to show an alternative below, where we use two simple flags to determine if both closures are done. For this you will have to put the code that runs after the data is loaded into a separate function, something like this:

func addAnnotationsToMap() }
  for oneObject in self.todasAnotacoes {
    ...
  }
}

Now in each of the two closures, we're going to set a flag when they're done. And then at the end of each of the closures, we'll detect whether both flags are set, and then call the new function if we have all data we need.

So at the start of viewDidLoad we define our two flags:

let isUserLoaded = false
let isClinicsLoaded = false

And then at the end of loading the user:

usuarios.child(idUsuarioLogado).observe(DataEventType.value) { (snapshot) in
    let dados = snapshot.value as? NSDictionary

    let emailUsuario = dados?["email"] as! String
    let nomeUsuario = dados?["nome"] as! String
    let perfilUsuario = dados?["perfil"] as! String
    let idUsuario = snapshot.key

    let usuario = Usuario(email: emailUsuario, nome: nomeUsuario, uid: idUsuario, perfil: perfilUsuario)

    isUserLoaded = true
    if (isUserLoaded && isClinicsLoaded) {
      addAnnotationsToMap();
    }
}

And similar code (setting the isClinicsLoaded flag) to the other closure.

With these in place, you'll start loading the user data and clinics when the code runs, and then whichever one of them completes last, will call the new addAnnotationsToMap function.


Another alternative is to use a DispatchGroup, which is an Apple-specific construct that can also make this much simpler. Read more about it in this answer (and the links from there): Wait until swift for loop with asynchronous network requests finishes executing

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807