개발을 시작하는 이야기

사용자의 위치를 확인하고 , 해당 지역으로 필터링 설정하기 본문

개발 이야기/우리동네 문화유산 :: JHeritage

사용자의 위치를 확인하고 , 해당 지역으로 필터링 설정하기

Teiresias 2022. 4. 6. 18:15

문화유산 앱의 지도 텝에서는 문화유산의 지역 정보를 가져와서 지도에 표시해주는 Annotation을 사용한다. 그런데, 문화유산의 수가 만 육천 개가 넘어가기 때문에 지도에 항상 모두를 표시해줄 수는 없기에 지역을 필터링해서 일부분만 보여주기로 하였다.

 

일단은 필터를 선택하는 filterButton과 PickerView를 키보드 영역에 보여주기 위해 빈 TextField를 제작했다. 을 MapView에 만들어 주었다.

let filterButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(named: "plus"), for: .normal)
        button.imageEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
        button.setTitle("", for: .normal)
        button.layer.cornerRadius = 20
        button.tintColor = .customBlack
        button.backgroundColor = .customYellow
        
        return button
}()

let textField: UITextField = {
        let textfield = UITextField()
        
        return textfield
}()

버튼의 이미지를 설정하고 고정 크기로 설정을 해주려 하기 때문에 imageEdgeInsets cornerRadius를 상수로 적용해주었고, textField는 활성화되었을 때 키보드가 생성되는 영역에 PickerView를 보여주는데만 사용할 것이기 때문에 별다른 설정은 해주지 않았다.

 

let mainView = MapView()

override func loadView() {
    super.loadView()
    self.view = mainView
}

override func viewDidLoad() {
    super.viewDidLoad()
    createPickerView()
    mainView.filterButton.addTarget(self, action: #selector(filterButtonClicked), for: .touchUpInside)
}

MapViewController의 loadView 로 MapView를 연결해주고, viewDidLoad에 아까 만들어둔 filterButton에 액션을 연결해준다.

createPickerView()는 PickerView를 생성하는 함수를 호출한다. 

 

    func createPickerView() {
        let pickerView = UIPickerView()
        pickerView.delegate = self
        pickerView.dataSource = self
                
        // 피커뷰 확인 취소 버튼 세팅
        let toolBar = UIToolbar()
        toolBar.sizeToFit()
        
        let btnDone = UIBarButtonItem(title: "확인", style: .done, target: self, action: #selector(onPickDone(_:)))
        let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
        let btnCancel = UIBarButtonItem(title: "취소", style: .done, target: self, action: #selector(onPickCancel))
        toolBar.setItems([btnCancel , space , btnDone], animated: true)
        toolBar.isUserInteractionEnabled = true
            
        // 텍스트필드 입력 수단 연결
        mainView.textField.inputView = pickerView
        mainView.textField.inputAccessoryView = toolBar
    }
    
    @objc func onPickDone(_ sender: UIDatePicker) {
        filerAnnotations()
        mainView.textField.resignFirstResponder()
    }
    
    @objc func onPickCancel() {
        mainView.textField.resignFirstResponder()
    }
    
    @objc func filterButtonClicked() {
        mainView.textField.becomeFirstResponder()
    }

createPickerView()에서 PickerView를 생성해주고, delegatedataSource를 연결해준다. PickerView 위에 ToolBar를 제작해서 확인 버튼과 취소 버튼을 만들어주는데, 버튼을 좌측 우측 끝부분에 위치해주기 위해서 중간에 space라는 빈 공간의 영역을 만들어준다. 그 후 텍스트 필드 입력 수단 연결을 위해 inputView에 pickerView를, inputAccessoryView 에는 toolBar를 연결을 해주도록 한다.

 

그 후, 버튼의 액션을 각각 만들어주는데, 확인을 누른 경우에는 filterAnnotations()를 통해 지도의 Annotation을 필터링해주고 창을 닫고, 취소를 누른 경우엔 그냥 창을 닫도록 해준다.

 

func filerAnnotations(){
    if viewModel.cityCode.value != "00" && viewModel.stockCode.value != 0 {
        let firstTesk =  viewModel.localRealm.objects(Heritage_List.self).filter("ccbaKdcd='\(viewModel.stockCode.value)'")
        viewModel.tasks = firstTesk.filter("ccbaCtcd='\(viewModel.cityCode.value)'")
    } else if viewModel.cityCode.value == "00" && viewModel.stockCode.value != 0 {
        viewModel.tasks = viewModel.localRealm.objects(Heritage_List.self).filter("ccbaKdcd='\(viewModel.stockCode.value)'")
    } else if viewModel.cityCode.value != "00" && viewModel.stockCode.value == 0 {
        viewModel.tasks = viewModel.localRealm.objects(Heritage_List.self).filter("ccbaCtcd='\(viewModel.cityCode.value)'")
    } else {
        viewModel.tasks = viewModel.localRealm.objects(Heritage_List.self)
    }

    let annotiations = mainView.mapView.annotations
    mainView.mapView.removeAnnotations(annotiations)

    for location in viewModel.tasks {
        let heritageLatitude = Double(location.latitude)!
        let heritageLongitude = Double(location.longitude)!

        let heritageCoordinate = CLLocationCoordinate2D(latitude: heritageLatitude, longitude: heritageLongitude)
        let heritageAnnotaion = MKPointAnnotation()

        heritageAnnotaion.coordinate = heritageCoordinate
        heritageAnnotaion.title = location.ccbaMnm1
        heritageAnnotaion.subtitle = location.sn
        mainView.mapView.addAnnotation(heritageAnnotaion)
    }
}

filterAnnotations은 PickerView에서 선택된 결과에 따라 Realm에서 데이터를 필터링하여 지도에 뿌려주게 된다. 피커 뷰의 Components는 2개로 하나는 도시명을, 하나는 문화유산의 종류를 표시해 주게 된다. 필터의 목록은 문화재청의 API 목록을 가져왔었지만 리스트 전체를 '모두' 보여주는 선택지도 넣어주길 원해서 각각의 데이터를 작성해서 만들어주었다. 그래서 유저가 피커 뷰를 선택할 수 있는 경우는 세 가지로 생각해서 결과를 만들었다.

  1. 둘 모두 특정 필터를 선택했다.
  2. 둘 중 하나를 모두를 선택했다.
  3. 둘 다 모두를 선택했다.

조건문으로 위의 세 가지 경우를 처리해주고 for문을 사용해 MapView에 Annotation을 작성해주었다. Annotation의 Title은 문화재명인 ccbaMnm1을, subTitle에는 순번인 sn을 넣어주었다. 

 

*사실 subTitle에는 고유 키값인 no를 넣어주려고 했지만, no는 필터가 되지 않아 sn으로 변경했다. 이 내용은 Annotation Click Event를 연결한 내용을 풀면서 작성해줄 예정.

 

그럼 이제 본론으로 돌아와 지도 텝을 열었을 때 모든 핀을 보여주는 것이 아닌 사용자의 위치에 맞는 핀들만 보여줘야 했다.

extension MapViewController: CLLocationManagerDelegate{
    //사용자가 위치 허용을 한 경우
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        moveLocation(latitudeValue: (location.coordinate.latitude), longtudeValue: (location.coordinate.longitude), delta: 0.01)
        
        CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
            if let pm: CLPlacemark = placemarks?.first {
                let country: String = "\(pm.country ?? "")"
                let cityCode: String = "\(pm.administrativeArea ?? "")"
                
                //대한민국에서 접속하면 시도구분을하고, 해외에서 접속했다면 서울만 띄워줌
                if country == "대한민국" {
                    switch cityCode {
                    case CityCase.seoul.rawValue:
                        self.viewModel.cityCode.value = "11"
                    case CityCase.busan.rawValue:
                        self.viewModel.cityCode.value = "21"
                    case CityCase.daegu.rawValue:
                        self.viewModel.cityCode.value = "22"
                    case CityCase.incheon.rawValue:
                        self.viewModel.cityCode.value = "23"
                    case CityCase.gwangju.rawValue:
                        self.viewModel.cityCode.value = "24"
                    case CityCase.daejeon.rawValue:
                        self.viewModel.cityCode.value = "25"
                    case CityCase.ulsan.rawValue:
                        self.viewModel.cityCode.value = "26"
                    case CityCase.sejong.rawValue:
                        self.viewModel.cityCode.value = "45"
                    case CityCase.gyeonggi.rawValue:
                        self.viewModel.cityCode.value = "31"
                    case CityCase.gangwon.rawValue:
                        self.viewModel.cityCode.value = "32"
                    case CityCase.chungbuk.rawValue:
                        self.viewModel.cityCode.value = "33"
                    case CityCase.chungnam.rawValue:
                        self.viewModel.cityCode.value = "34"
                    case CityCase.jeonbuk.rawValue:
                        self.viewModel.cityCode.value = "35"
                    case CityCase.jeonnam.rawValue:
                        self.viewModel.cityCode.value = "36"
                    case CityCase.gyeongbuk.rawValue:
                        self.viewModel.cityCode.value = "37"
                    case CityCase.gyeongnam.rawValue:
                        self.viewModel.cityCode.value = "38"
                    case CityCase.jeju.rawValue:
                        self.viewModel.cityCode.value = "50"
                    default:
                        self.viewModel.cityCode.value = "ZZ"
                    }
                } else {
                    self.viewModel.cityCode.value = "11"
                }
                self.filerAnnotations()
            }
        })
        locationManager.stopUpdatingLocation()
    }
}

이에 대한 해답은 CLLocationManagerDelegate에서 답을 찾을수 있었다. 사용자가 위치 추적을 허용한 경우, locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])을 통해서 사용자의 위치 정보를 가져오게 된다.

그리고 CLGeocoder().reverseGeocodeLocation(location, completionHandler: 를 사용해서 사용자의 위치 정보를 핸들링하는데, 이때 사용자의 위치의 국가와 도시 정보를 가져온다. "대한민국"에서 접속한 경우, Switch - Case문을 사용해 도시 정보를 가져와서 viewModel에 전달을 해주도록 하고, 국외 지역에서 접속한 경우는 서울의 정보를 전달해 주도록 했다. 그렇게 전달받은 내용을 바탕으로 Annotation을 필터링해주게 되었다.

 

이렇게 작성을 해주고 시뮬레이터를 통해 위치 정보를 변경하며 테스트한 결과 접속 지역에 맞춰 지도의 핀이 잘 변경되는 것을 확인할 수 있었다. 그래도 아직 지도창을 클릭하면 약 3초 ~ 5초 정도의 딜레이가 발생하게 된다. 계속해서 최적화를 해서 속도를 줄여나가는 것을 목표로 하고 있다.

 

프로젝트 깃헙 페이지