Search code examples
swiftuialertcontroller

UIAlertController Already presenting (null)


I have an app where at first a map is shown with all companies close to my position. Main screen has a search button where I can click and filter results according to my specific needs [![enter image description here][1]][1]

The connection between Mapa View Controller and Filtrar View Controller was built based on a custom protocol (according to https://medium.com/@jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef).

All the communication works fine and I'm able to filter and present companies that meet the criteria. My issue is when the filter returns nothing (there are no companies that meet the criteria). When that happen I wanted to present a UIAltert as an advice to the end user.

Alert is triggered but I get an error message "Attempt to present <UIAlertController: 0x7f9314a47a00> on <xxx.MapaViewController: 0x7f9313e0f9e0> which is already presenting (null)".

I tried one of the suggestions from What is the best way to check if a UIAlertController is already presenting?

   if self.presentedViewController == nil {
       // do your presentation of the UIAlertController
       // ...
    } else {
       // either the Alert is already presented, or any other view controller
       // is active (e.g. a PopOver)
       // ...
    
       let thePresentedVC : UIViewController? = self.presentedViewController as UIViewController?
    
       if thePresentedVC != nil {
          if let thePresentedVCAsAlertController : UIAlertController = thePresentedVC as? UIAlertController {
             // nothing to do , AlertController already active
             // ...
             print("Alert not necessary, already on the screen !")
    
          } else {
             // there is another ViewController presented
             // but it is not an UIAlertController, so do 
             // your UIAlertController-Presentation with 
             // this (presented) ViewController
             // ...
             thePresentedVC!.presentViewController(...)
    
             print("Alert comes up via another presented VC, e.g. a PopOver")
          }
      }
    }

I see "Alert comes up via another presented VC, e.g. a PopOver" printed but I don't know how to use thePresentedVC!.presentViewController(...).

I'm using XCode 11.2.1 and IOS 11.

My detailed code is

Alert Class

import UIKit

class Alerta {
    var titulo : String
    var mensagem : String
    
    init(titulo: String, mensagem: String) {
        self.titulo = titulo
        self.mensagem = mensagem
        
    }
    
    func getAlerta() -> UIAlertController {
        let alerta = UIAlertController(title: titulo, message: mensagem, preferredStyle: .alert)
        let acaoCancelar = UIAlertAction(title: "Ok", style: .cancel, handler: nil)

        alerta.addAction(acaoCancelar)
        return alerta
    }
}

Mapa Class

import UIKit
import MapKit
import ProgressHUD

class MapaViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, FiltroVCDelegate {
    
    @IBOutlet weak var mapa: MKMapView!
    var gerenciadorLocalizacao = CLLocationManager()
    var anotacaoArray = [MinhaAnotacao]()
    // var annotationSearch: [MinhaAnotacao] = [] // Filtered annotation
    var anotacao = MinhaAnotacao()
    var searchArr: [String] = []
    var searching = false
    
    var todasAnotacoes: [(objLat: CLLocationDegrees, objLong: CLLocationDegrees, objName: String, objDesc: String, objId: String, objTel1: String, objTel2: String, objEsp1: String, objEsp2: String, objEst: String, objCid: String)] = []
    var clinicasR: [(objLat: CLLocationDegrees, objLong: CLLocationDegrees, objName: String, objDesc: String, objId: String, objTel1: String, objTel2: String, objEsp1: String, objEsp2: String, objEst: String, objCid: String)] = []
    
    @IBOutlet weak var adicionarOutlet: UIBarButtonItem!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mapa.register(MyAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
        
        // Retrieving Logger user data and hidding "add" button if applicable
        ProgressHUD.show("Carregando...")
        var searching = false
        
        
        // Map - User location
        self.mapa.delegate = self
        self.gerenciadorLocalizacao.delegate = self
        self.gerenciadorLocalizacao.desiredAccuracy = kCLLocationAccuracyBest
        self.gerenciadorLocalizacao.requestWhenInUseAuthorization()
        self.gerenciadorLocalizacao.startUpdatingLocation()

        ProgressHUD.dismiss()
    }
    
    // add annotations to the map
    func addAnnotationsToMap() {
        anotacaoArray = []
        self.mapa.removeAnnotations(mapa.annotations)
        if searching {
            for oneObject in self.clinicasR {
                let umaAnotacao = MinhaAnotacao()
                let oneObjLoc: CLLocationCoordinate2D = CLLocationCoordinate2DMake(oneObject.objLat, oneObject.objLong)
                umaAnotacao.coordinate = oneObjLoc
                umaAnotacao.title = oneObject.objName
                umaAnotacao.subtitle = oneObject.objDesc
                umaAnotacao.identicadorMapa = oneObject.objId
                umaAnotacao.telefone = oneObject.objTel1
                umaAnotacao.telefone2 = oneObject.objTel2
                umaAnotacao.especialidade1 = oneObject.objEsp1
                umaAnotacao.especialidade2 = oneObject.objEsp2
                umaAnotacao.estado = oneObject.objEst
                umaAnotacao.cidade = oneObject.objCid
                umaAnotacao.endereco = oneObject.objDesc
                umaAnotacao.latitude = String(oneObject.objLat)
                umaAnotacao.longitude = String(oneObject.objLong)
                
                self.anotacaoArray.append(umaAnotacao)
            }
            
        } else {
            for oneObject in self.todasAnotacoes {
                let umaAnotacao = MinhaAnotacao()
                let oneObjLoc: CLLocationCoordinate2D = CLLocationCoordinate2DMake(oneObject.objLat, oneObject.objLong)
                umaAnotacao.coordinate = oneObjLoc
                umaAnotacao.title = oneObject.objName
                umaAnotacao.subtitle = oneObject.objDesc
                umaAnotacao.identicadorMapa = oneObject.objId
                umaAnotacao.telefone = oneObject.objTel1
                umaAnotacao.telefone2 = oneObject.objTel2
                umaAnotacao.especialidade1 = oneObject.objEsp1
                umaAnotacao.especialidade2 = oneObject.objEsp2
                umaAnotacao.estado = oneObject.objEst
                umaAnotacao.cidade = oneObject.objCid
                umaAnotacao.endereco = oneObject.objDesc
                umaAnotacao.latitude = String(oneObject.objLat)
                umaAnotacao.longitude = String(oneObject.objLong)
                
                self.anotacaoArray.append(umaAnotacao)
            }
        }
        self.mapa.addAnnotations(self.anotacaoArray)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "clinicaDetailsSegue" {
            let clinicasDetailsViewController = segue.destination as! ClinicasDetailsViewController
            clinicasDetailsViewController.identificador = self.anotacao.identicadorMapa
        } else if segue.identifier == "searchSegue" {
            if let nav = segue.destination as? UINavigationController, let filterVC = nav.topViewController as? FiltrarViewController {
                filterVC.delegate = self
            }
        }
    }
    
    func search(searchAnnotation: [MinhaAnotacao], searchArr: [String]) -> [MinhaAnotacao] {
        var searchArr = searchArr
        // base case - no more searches - return clinics found
        if searchArr.count == 0 {
            return searchAnnotation
        }
        // iterative case - search clinic with next search term and pass results to next search
        let foundAnnotation = searchAnnotation.filter { item in
            (item.title!.lowercased() as AnyObject).contains(searchArr[0]) ||
                item.especialidade1.lowercased().contains(searchArr[0]) ||
                item.especialidade2.lowercased().contains(searchArr[0]) ||
                item.cidade.lowercased().contains(searchArr[0]) ||
                item.estado.lowercased().contains(searchArr[0]) ||
                item.latitude.contains(searchArr[0]) || item.longitude.contains(searchArr[0]) || item.endereco.contains(searchArr[0])
        }
        // remove completed search and call next search
        searchArr.remove(at: 0)
        return search(searchAnnotation: foundAnnotation, searchArr: searchArr)
    }
    
    // From Custom Protocol
    func dadosEscolhidos(nomeClinicaFiltro: String, estadoClinicaFiltro: String, cidadeClinicaFiltro: String, especialidade1ClinicaFiltro: String, especialidade2ClinicaFiltro: String) {
        
        searchArr = []
        clinicasR = []
        searchArr = ["\(nomeClinicaFiltro.lowercased())","\(estadoClinicaFiltro.lowercased())", "\(cidadeClinicaFiltro.lowercased())", "\(especialidade1ClinicaFiltro.lowercased())", "\(especialidade2ClinicaFiltro.lowercased())"]
        searchArr = searchArr.filter({ $0 != ""})
        
        let annotationSearch = search(searchAnnotation: anotacaoArray, searchArr: searchArr) // Filtered Clinicas
        
        if annotationSearch.count > 0 {
            for i in 0...annotationSearch.count - 1 {
                self.clinicasR.append((objLat: Double(annotationSearch[i].latitude)!, objLong: Double(annotationSearch[i].longitude)!, objName: annotationSearch[i].title!, objDesc: annotationSearch[i].endereco, objId: annotationSearch[i].identicadorMapa, objTel1: annotationSearch[i].telefone, objTel2: annotationSearch[i].telefone2, objEsp1: annotationSearch[i].especialidade1, objEsp2: annotationSearch[i].especialidade2, objEst: annotationSearch[i].estado, objCid: annotationSearch[i].cidade))
            }
            
            searching = true
            addAnnotationsToMap()
        } else {
            
            if self.presentedViewController == nil {
               let alerta = Alerta(titulo: "Erro", mensagem: "Nenhuma clínica atende ao filtro definido")

               self.present(alerta.getAlerta(), animated: true, completion: nil)
               print( "e Aqui, chegou? \(annotationSearch.count)")
            } else {
               // either the Alert is already presented, or any other view controller
               // is active (e.g. a PopOver)
               // ...

               let thePresentedVC : UIViewController? = self.presentedViewController as UIViewController?

               if thePresentedVC != nil {
                  if let thePresentedVCAsAlertController : UIAlertController = thePresentedVC as? UIAlertController {
                     // nothing to do , AlertController already active
                     // ...
                     print("Alert not necessary, already on the screen !")

                  } else {
                     // there is another ViewController presented
                     // but it is not an UIAlertController, so do
                     // your UIAlertController-Presentation with
                     // this (presented) ViewController
                     // ...
                    //let alerta = Alerta(titulo: "Erro", mensagem: "Nenhuma clínica atende ao filtro definido")
                    //thePresentedVC!.presentedViewController(alerta)

                     print("Alert comes up via another presented VC, e.g. a PopOver")
                  }
              }
            }
        }
}
}

Filtrar View Controller

import UIKit
import ProgressHUD

protocol FiltroVCDelegate: class {
    func dadosEscolhidos(nomeClinicaFiltro: String, estadoClinicaFiltro: String, cidadeClinicaFiltro: String, especialidade1ClinicaFiltro: String, especialidade2ClinicaFiltro: String)
}


class FiltrarViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
    
    weak var delegate: FiltroVCDelegate?
    
    var nomeSelecionado = ""
    
    var estadosJSON = [Estado]()
    var cidades = [Cidade]()
    var estado : Estado? // Selected State identifier
    var cidade : Cidade? // Selected City identifier
    var estadoSelecionado = "" // Selected State
    var cidadeSelecionada = "" // Selected City
    
    var especialidadesJSON = [Especialidade]()
    var especialidades2 = [Especialidade2]()
    var especialidade1 : Especialidade? // Selected Specialty1 identifier
    var especialidade2 : Especialidade2? // Selected Specialty2 identifier
    var especialidade1Selecionada = ""
    var especialidade2Selecionada = ""
    
    let fontName = "HelveticaNeue"
    var searchArr = [String]()
    
    @IBOutlet weak var nomeClinica: UITextField!
    @IBOutlet weak var especialidadeLabel: UILabel!
    
    @IBOutlet weak var estadoClinicaPicker: UIPickerView!
    @IBOutlet weak var especialidade1Picker: UIPickerView!
    @IBOutlet weak var especialidade2Picker: UIPickerView!
    
    override func viewDidLoad() {
        ProgressHUD.show("Carregando...")
        readJsonEstados()
        readJsonEspecialidades()
        super.viewDidLoad()
        nomeClinica.text = ""
        especialidadeLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
        ProgressHUD.dismiss()
        
    }
    
    
    @IBAction func aplicarFiltro(_ sender: Any) {
        if nomeClinica.text == nil {
            nomeClinica.text = ""
        }
   
        delegate?.dadosEscolhidos(nomeClinicaFiltro: nomeClinica.text!, estadoClinicaFiltro: estadoSelecionado, cidadeClinicaFiltro: cidadeSelecionada, especialidade1ClinicaFiltro: especialidade1Selecionada, especialidade2ClinicaFiltro: especialidade2Selecionada)
        navigationController?.dismiss(animated: true)

    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        
        especialidade1Picker.reloadComponent(0)
        especialidade2Picker.reloadComponent(0)
        estadoClinicaPicker.reloadAllComponents()
        
        if pickerView == estadoClinicaPicker {
            if component == 0 {
                self.estado = self.estadosJSON[row]
                self.cidades = self.estadosJSON[row].cidades
                estadoClinicaPicker.reloadComponent(1)
                estadoClinicaPicker.selectRow(0, inComponent: 1, animated: true)
            } else {
                self.cidade = self.cidades[row]
                estadoClinicaPicker.reloadAllComponents()
            }
        } else if pickerView == especialidade1Picker {
            self.especialidade1 = self.especialidadesJSON[row]
            self.especialidades2 = self.especialidadesJSON[row].especialidade2
            especialidade1Picker.reloadComponent(0)
            especialidade2Picker.reloadComponent(0)
            especialidade2Picker.selectRow(0, inComponent: 0, animated: true)
            
        } else if pickerView == especialidade2Picker {
            self.especialidade2 = self.especialidades2[row]
            especialidade1Picker.reloadComponent(0)
            especialidade2Picker.reloadComponent(0)
        }
        
        let estadoIndiceSelecionado = estadoClinicaPicker.selectedRow(inComponent: 0)
        let cidadeIndiceSelecionada = estadoClinicaPicker.selectedRow(inComponent: 1)
        let especialidade1IndiceSelecionado = especialidade1Picker.selectedRow(inComponent: 0)
        let especialidade2IndiceSelecionado = especialidade2Picker.selectedRow(inComponent: 0)
        
        if estadoIndiceSelecionado >= 0 {
            if cidadeIndiceSelecionada >= 0 {
                estadoSelecionado = estadosJSON[estadoIndiceSelecionado].nome
                cidadeSelecionada = cidades[cidadeIndiceSelecionada].nome
            }
        }
        
        if especialidade1IndiceSelecionado >= 0 {
            if especialidade2IndiceSelecionado >= 0 {
                especialidade1Selecionada = especialidadesJSON[especialidade1IndiceSelecionado].nome
                especialidade2Selecionada = especialidadesJSON[especialidade1IndiceSelecionado].especialidade2[especialidade2IndiceSelecionado].nome
            }
        }
    }
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        if pickerView == estadoClinicaPicker {
            return 2
        } else if pickerView == especialidade1Picker {
            return 1
        } else if pickerView == especialidade2Picker {
            return 1
        }
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if pickerView == estadoClinicaPicker {
            if component == 0 {
                return estadosJSON.count
            } else {
                return cidades.count
            }
        } else if pickerView == especialidade1Picker {
            return self.especialidadesJSON.count
            
        } else if pickerView == especialidade2Picker {
            return especialidades2.count
        }
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        var rowTitle = ""
        let pickerLabel = UILabel()
        
        pickerLabel.textColor = UIColor.black
        
        if pickerView == estadoClinicaPicker {
            if component == 0 {
                rowTitle = estadosJSON[row].nome
            } else {
                rowTitle = cidades[row].nome
            }
        } else if pickerView == especialidade1Picker {
            rowTitle = especialidadesJSON[row].nome
        } else if pickerView == especialidade2Picker {
            rowTitle = especialidades2[row].nome
        }
        
        pickerLabel.text = rowTitle
        pickerLabel.font = UIFont(name: fontName, size: 16.0)
        pickerLabel.textAlignment = .center
        
        return pickerLabel
    }
    
    func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
        if pickerView == estadoClinicaPicker {
            if component == 0 {
                return 50
            } else {
                return 300
            }
        }
        return 300
    }
    
    
    @IBAction func cancel(_ sender: Any) {
        navigationController?.dismiss(animated: true)
   }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        view.endEditing(true)
    }
    
    func readJsonEstados() {
        let url = Bundle.main.url(forResource: "EstadosECidades", withExtension: "json")!
        do {
            let data = try Data(contentsOf: url)
            let jsonResult = try JSONDecoder().decode(RootState.self, from: data)
            
            //handles the array of countries on your json file.
            self.estadosJSON = jsonResult.estado
            self.cidades = self.estadosJSON.first!.cidades
            
        } catch {
            let alerta = Alerta(titulo: "Erro ao Carregar", mensagem: "Erro ao carregar Estados e Cidades. Por favor reinicie o app")
            self.present(alerta.getAlerta(), animated: true, completion: nil)
        }
    }
    
    func readJsonEspecialidades() {
        let url = Bundle.main.url(forResource: "Especialidades", withExtension: "json")!
        do {
            let data = try Data(contentsOf: url)
            let jsonResult = try JSONDecoder().decode(RootEsp.self, from: data)
            
            //handles the array of specialties on your json file.
            self.especialidadesJSON = jsonResult.especialidade
            self.especialidades2 = self.especialidadesJSON.first!.especialidade2
        } catch {
            let alerta = Alerta(titulo: "Erro ao Carregar", mensagem: "Erro ao carregar Especialidades. Por favor reinicie o app")
            self.present(alerta.getAlerta(), animated: true, completion: nil)
        }
    }
        
    /*
     // MARK: - Navigation
     
     // In a storyboard-based application, you will often want to do a little preparation before navigation
     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
     // Get the new view controller using segue.destination.
     // Pass the selected object to the new view controller.
     }
     */
    
}

Thanks [1]: https://i.sstatic.net/e3KCt.png

EDIT

Link to the project https://github.com/afernandes0001/UIAlertController. You just need to click Log in (no validation is done), go to main screen, Map, Click Search and Apply - There is no need to add any data as I have made data static.


Solution

  • import UIKit
    
    class ViewController: UIViewController {
    
    let button = UIButton(type: .system)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        button.backgroundColor = .black
        button.setTitle("Alert", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.addTarget(self, action: #selector(handleAlert), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
    
        view.addSubview(button)
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
        button.widthAnchor.constraint(equalToConstant: 100).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        // Do any additional setup after loading the view.
    }
    
    @objc fileprivate func handleAlert() {
        OperationQueue.main.addOperation {
            showAlert(titulo: "YourTilte", mensagem: "YourMessage", vc: self)
             print("On main thread: \(Thread.current.isMainThread)")
        }
    }
    
    extension UIViewController {
    func showAlert(titulo: String, mensagem: String, vc: UIViewController) {
        let alerta = UIAlertController(title: titulo, message: mensagem, preferredStyle: .alert)
        let acaoCancelar = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
    
        alerta.addAction(acaoCancelar)
        vc.present(alerta, animated: true)
     }
    }
    

    enter image description here

    enter image description here

    it work :)