Swift 10주차 - 맵 뷰로 지도 나타내기

2024. 5. 11. 03:18swift

Swift 10주차 - 맵 뷰로 지도 나타내기

 

이번 주차에서는 지도, 위치 관련 앱에서 활용할 수 있는 맵 뷰(Map View)를 사용하여 지도를 나타내고, 원하는 곳에 핀을 선택하여 원하는 글자를 나타내는 실습을 진행할 예정이다.

 

 

목차

     

     

    맵 뷰 앱 화면 꾸미기

     

    지도 화면에서 현재 위치 및 특정 위치를 선택할 수 있는 '세그먼트 컨트롤(Segmented Control)'을 추가하자.

     

     

     

     

    세그먼트 컨트롤은 여러 세그먼트로 구성된 수평 컨트롤이며, 세그먼트는 각 세분화된 기능을 가진 버튼을 말한다.

     

    기능상으로 버튼과 동일하지만 버튼들을 한 곳에 모아놓고 '선택'의 개념이 더해진 것이 바로 세그먼트 컨트롤이라고 볼 수 있다.

     

     

     

    다음으로 세그먼트 컨트롤의 세그먼트를 추가해보자.

     

    인스펙터 영역에서 Segments를 이용해 세그먼트의 개수를 조정할 수 있다.

     

     

     

    그다음 각각의 세그먼트에 타이틀을 변경 및 추가하자.

     

    Segment에서 세그먼트를 하나 선택하고 그 아래 Title에서 글자를 수정할 수 있다.

     

    필자는 임의로 현재위치, 인덕대, 남산타워, 구글본사, NASA, 백악관 으로 수정했다.

     

     

     

     

    이제 지도를 보여줄 맵 뷰를 추가해보자.

     

     

     

     

     

    마지막으로 위치 정보를 표기할 레이블을 추가하자.

     

     

     

     

    아웃렛 변수와 액션 함수 추가하기

     

    맵 뷰에 대한 아웃렛 변수를 추가해보자.

     

     

    이름은 'myMap'으로 지정하고 연결하자.

     

     

     

    아웃렛 변수가 생성되고 나면 에러가 발생한 것을 확인할 수 있는데 이유는 아웃렛 변수 myMap의 타입을 MKMapView로 설정했는데 해당 타입이 정의되어 있는 MapKit이 import 되지 않았기 때문이다.

     

    아래처럼 import UIKit 아래에 import MapKit을 추가하여 에러를 해결할 수 있다.

     

    맵 킷(MapKit)은 지도를 확대, 축소 및 이동하는 등 지도에 관한 여러 기능을 제공한다.

     

     

     

     

    그다음 맵뷰 아래 레이블을 연결하자.

     

     

    이름은 'lblLocationInfo1'으로 지정하고 연결하자.

     

    같은 방법으로 아래 레이블도 이름을 'lblLocationInfo2'로 지정하고 연결하자.

     

     

    마지막으로 세그먼트 컨트롤에 대한 액션 함수를 추가해보자.

     

     

    이름은 'sgChangeLocation'으로 지정하고 타입은 'UISegmentedControl'으로 변경한 후 연결하자.

     

     

    이렇게 해서 모든 아웃렛 변수와 액션함수를 추가했다.

     

     

     

     

    지도 보여주기

     

    이제 앱을 처음 실행했을 때 지도를 보여주기 위한 변수 선언과 초기 작업을 진행해보자.

     

    지도를 보여주기 위해 변수와 델리게이트 선언을 해주자.

     

     

     

     

    앱을 실행하면 지도가 나타나도록 viewDidLoad 함수에 아래 코드를 추가해보자.

     

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        lblLocationInfo1.text = "" // 1
        lblLocationInfo2.text = "" // 1
        locationManager.delegate = self // 2
        locationManager.desiredAccuracy = kCLLocationAccuracyBest // 3
        locationManager.requestWhenInUseAuthorization() // 4
        locationManager.startUpdatingLocation() // 5
        myMap.showsUserLocation = true // 6
    }

     

     

    1. 위치 정보를 표시할 레이블을 공백으로 초기화한다.
    2. 상수 locationManager의 델리게이트를 self로 설정한다.
    3. 정확도를 최고로 설정한다.
    4. 위치 데이터를 추적하기 위해 사용자에게 승인을 요구한다.
    5. 위치 업데이트를 시작한다.
    6. 위치 보기 값을 true로 설정한다.

     

    이제 앱이 사용자 위치에 접근하려고 할 때 사용자에게 알람창으로 접근 허용을 요청해 접근을 허용할 수 있도록 설정해보자.

     

    왼쪽의 내비게이터 영역에서 Info.plist를 선택해 커서를 Information Property List 위로 가져가 +가 표시되면 클릭하자.

     

     

     

    리스트가 추가되면 'Privacy - Location When In Use Usage Description'을 선택하자.

     

     

     

    Value를 더블클릭 하여 'App needs location servers for stuff.'를 입력하자.

     

     

     

     

    에뮬레이터를 실행해 보면 앱이 사용자 위치에 접근하도록 허용할 것인지 물어보는 알람이 뜨게 된다.

     

    '앱을 사용하는 동한 허용'을 눌러 허용하자.

     

     

    위도와 경도로 원하는 위치 표시하기

     

    앞에서는 우리나라의 전체 지도를 보여주었다면 이제는 사용자가 원하는 위도와 경도의 지도만 보여주기 위해 코드를 추가해보자.

     

    이를 위해 먼저 아래와 같이 goLocation 함수를 추가해주자.

     

    함수 위치는 sgChangeLocation 함수 위에 작성하면 된다.

     

    func goLocation(latitudeValue: CLLocationDegrees, longitudeValue: CLLocationDegrees, delta span: Double) {
    
        let pLocation = CLLocationCoordinate2DMake(latitudeValue, longitudeValue) // 1
        let spanValue = MKCoordinateSpan(latitudeDelta: span, longitudeDelta: span) // 2
        let pRegion = MKCoordinateRegion(center: pLocation, span: spanValue) // 3
            
        myMap.setRegion(pRegion, animated: true) // 4
    }

     

    1. 위도와 경도값을 매개변수로 하여 CLLocationCoordinate2DMake 함수를 호출하고 리턴값을 pLocation으로 받는다.
    2. 범위 값을 매개변수로 하여 MKCoordinateSpan 함수를 호출하고 리턴값을 spanValue로 받는다.
    3. pLocation과 spanValue값을 매개변수로 하여 MKCoordinateRegion 함수를 호출하고 리턴값을 pRegion으로 받는다.
    4. pRegion값을 매개변수로 하여 myMap.setRegion 함수를 호출한다.

     

    위치가 업데이트되었을 때 지도에 위치를 나타내기 위해 locationManager 함수를 추가하자.

     

    함수는 goLocation 아래에 작성해주면 된다.

     

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            
        let pLocation = locations.last // 1
        goLocation(latitudeValue: (pLocation?.coordinate.latitude)!, longitudeValue: (pLocation?.coordinate.longitude)!, delta: 0.01) // 2
    }

     

    1. 위치가 업데이트 되면 먼저 마지막 위치값을 찾아낸다.
    2. 마지막 위치의 위도와 경도값을 가지고 앞에서 만든 goLocation 함수를 호출한다. 이때 delta값은 지도의 크기를 정하는데 값이 적을수록 지도가 확대되는 효과가 있다. delta값을 0.01로 설정하였으므로 1의 값보다 지도를 100배 확대하여 보여줄 것이다.

     

     

    위치 정보를 추출해 텍스트로 표시하기

     

    이제 위도와 경도값을 이용해 위치 정보를 가져오고 나리, 지역 및 도로명을 찾아 레이블에 표시해보자.

     

    위도와 경도값을 이용해 주소를 찾기 위해 아래와 같이 locationManager 함수를 수정하자.

     

     

     

    핸들러의 익명 함수를 아래 코드처럼 수정하자.

     

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            
        let pLocation = locations.last
        goLocation(latitudeValue: (pLocation?.coordinate.latitude)!, longitudeValue: (pLocation?.coordinate.longitude)!, delta: 0.01)
        CLGeocoder().reverseGeocodeLocation(pLocation!, completionHandler: {
            (placemarks, error) -> Void in
    
            let pm = placemarks!.first // 1
            let country = pm!.country // 2
            var address: String = country! // 3
                
            if pm!.locality != nil { // 4
                address += " "
                address += pm!.locality!
            }
            if pm!.thoroughfare != nil { // 5
                address += " "
                address += pm!.thoroughfare!
            }
    
            self.lblLocationInfo1.text = "현재 위치" // 6
            self.lblLocationInfo2.text = address // 7
        })
        locationManager.stopUpdatingLocation() // 8
    }

     

    1. placemarks 값의 첫부분만 pm 상수에 넣는다.
    2. pm 상수에서 나라 값을 country 상수에 넣는다.
    3. 문자열 address에 country 상수의 값을 대입한다.
    4. pm 상수에서 지역 값이 존재하면 address 문자열에 추가한다.
    5. pm 상수에서 도로 값이 존재하면 address 문자열에 추가한다.
    6. 레이블에 "현재 위치" 텍스트를 표시한다.
    7. 레이블에 address 문자열의 값을 표시한다.
    8. 마지막으로 위치가 업데이트 되는 것을 멈춘다.

     

    에뮬레이터를 실행해보면 지도가 제대로 나타나고 위치 정보도 제대로 표시되는 것을 볼 수 있다.

     

     

     

    시뮬레이터에서 현재 위치 바꾸기

     

    시뮬레이터의 메뉴에서 Features -> Location 에서 현재 위치를 바꿀 수 있다.

     

     

    애플 사의 위치로 변경하고 싶다면 Apple를 선택 한 후 앱을 다시 실행하면 지도에 미국 애플 사가 표시된다.

     

     

     

    만약 임의의 위치로 변경하고 싶다면 아까 메뉴창에서 Custom Location... 을 선택한 후 위도와 경도값을 수정하면 된다.

    위도와 경도값은 구글 지도에서 찾을 수 있다.

     

    구글 지도에서 원하는 위치에서 우클릭하여 '이곳이 궁금한가요?'를 선택한다.

     

     

     

    그러면 아래처럼 회색 핀이 나타나게 되는데 해당 회색 핀을 클릭하자.

     

     

    그러면 아래처럼 해당 위치의 위도와 경도값을 확인할 수 있다.

     

     

    이 위도와 경도값을 아까 Custom Location 에 입력한 후 다시실행해보면 설정한 위치로 현재 위치가 바뀐것을 확인할 수 있다.

     

     

     

    위도와 경도로 원하는 핀 설치하기

     

    이제 특정 위도와 경도에 핀을 설치하고 그 핀을 클릭하면 특정 문자를 나타내도록 추가로 코드를 작성해보자.

     

     

    먼저 setAmmotation 함수를 만들어 원하는 곳에 핀을 설치해보자.

     

    이 함수는 locationManager 함수 바로 위에 추가하자.

     

    func setAnnotation(latitudeValue: CLLocationDegrees, longitudeValue: CLLocationDegrees, delta span: Double, title strTitle: String, subtitle strSubtitle: String) {
    
        let annotation = MKPointAnnotation() // 1
        
        annotation.coordinate = goLocation(latitudeValue: latitudeValue, longitudeValue: longitudeValue, delta: span) // 2
    }

     

    1. 핀을 설치하기 위해 MKPointAnnotation 함수를 호출하여 리턴값을 annotation으로 받는다.
    2. annotation의 coordinate 값을 goLocation 함수로부터 CLLocationCoordinate2D 형태로 받아야 하는데 이를 위해서 goLocation 함수를 수정해야 한다.

     

    앞에서 작성했던 goLocation 함수의 리턴타입을 CLLocationCoordinate2D 로 지정하고 리턴값 pLcation을 추가하자.

     

     

     

    goLocation 함수를 반환값을 가지는 함수로 수정하였으므로 locationManager 함수 내의 goLocation 함수 부분을 수정해주자.

     

     

     

     

    setAnnotation 함수에 핀의 타이틀과 서브 타이틀을 세팅하고 맵 뷰에 변수 annotation 값을 추가하는 코드를 작성하자.

     

     

     

     

    이제 세그먼트 컨트롤의 액션함수를 수정하자.

     

    현재위치, 인덕대, 남산타워, 구글본사, NASA, 백악관 이렇게 6개의 세그먼트를 인덱스 값으로 구분하는 if문을 추가해보자.

     

    @IBAction func sgChangeLocation(_ sender: UISegmentedControl) {
        if sender.selectedSegmentIndex == 0 {
            // 현재 위치 표시
        } else if sender.selectedSegmentIndex == 1 {
            // 인덕대 표시
        } else if sender.selectedSegmentIndex == 2 {
            // 남산타워 표시
        } else if sender.selectedSegmentIndex == 3 {
            // 구글본사 표시
        } else if sender.selectedSegmentIndex == 4 {
            // NASA 표시
        } else if sender.selectedSegmentIndex == 5 {
            // 백악관 표시
        }
    }

     

     

    언덕대의 핀을 설치하기 위해 아래처럼 setAnnotation 함수를 호출하자.

     

    그리고 레이블의 값도 함께 수정하자.

     

     

     

    나머지 위치도 위와 같은 방법으로 코드를 작성하자.

     

     

     

     

    이제 시뮬레이터를 실행하여 결과를 확인해보면 해당 위치에 핀이 설치되고 레이블도 바뀌는 것을 확인할 수 있다.

     

    핀을 클릭하면 타이틀과 서브 타이틀이 나타난다.

     

     

     

     

    현재 위치 표시하기

     

    마지막으로 현재 위치가 표시되도록 레이블 값을 공백으로 초기화하고, locationManager.startUpdaingLocation 함수를 호출하자.

     

     

     

    시뮬레이터를 실행해보면 모든 기능들이 정상적으로 동작하는 것을 확인할 수 있다.

     

     

     

     

     

    맵 뷰 앱 전체 소스 코드

     

    import UIKit
    import MapKit
    
    class ViewController: UIViewController, CLLocationManagerDelegate {
        
        @IBOutlet var myMap: MKMapView!
        @IBOutlet var lblLocationInfo1: UILabel!
        @IBOutlet var lblLocationInfo2: UILabel!
        
        let locationManager = CLLocationManager()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            lblLocationInfo1.text = ""
            lblLocationInfo2.text = ""
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.requestWhenInUseAuthorization()
            locationManager.startUpdatingLocation()
            myMap.showsUserLocation = true
        }
        
        func goLocation(latitudeValue: CLLocationDegrees, longitudeValue: CLLocationDegrees, delta span: Double) -> CLLocationCoordinate2D {
    
            let pLocation = CLLocationCoordinate2DMake(latitudeValue, longitudeValue)
            let spanValue = MKCoordinateSpan(latitudeDelta: span, longitudeDelta: span)
            let pRegion = MKCoordinateRegion(center: pLocation, span: spanValue)
            
            myMap.setRegion(pRegion, animated: true)
            
            return pLocation
        }
        
        func setAnnotation(latitudeValue: CLLocationDegrees, longitudeValue: CLLocationDegrees, delta span: Double, title strTitle: String, subtitle strSubtitle: String) {
    
            let annotation = MKPointAnnotation()
            
            annotation.coordinate = goLocation(latitudeValue: latitudeValue, longitudeValue: longitudeValue, delta: span)
            annotation.title = strTitle
            annotation.subtitle = strSubtitle
            myMap.addAnnotation(annotation)
        }
        
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            
            let pLocation = locations.last
            _ = goLocation(latitudeValue: (pLocation?.coordinate.latitude)!, longitudeValue: (pLocation?.coordinate.longitude)!, delta: 0.01)
            CLGeocoder().reverseGeocodeLocation(pLocation!, completionHandler: {
                (placemarks, error) -> Void in
    
                let pm = placemarks!.first
                let country = pm!.country
                var address: String = country!
                
                if pm!.locality != nil {
                    address += " "
                    address += pm!.locality!
                }
                if pm!.thoroughfare != nil {
                    address += " "
                    address += pm!.thoroughfare!
                }
    
                self.lblLocationInfo1.text = "현재 위치"
                self.lblLocationInfo2.text = address
            })
            locationManager.stopUpdatingLocation()
        }
    
        @IBAction func sgChangeLocation(_ sender: UISegmentedControl) {
            if sender.selectedSegmentIndex == 0 {
                // 현재 위치 표시
                self.lblLocationInfo1.text = ""
                self.lblLocationInfo2.text = ""
                locationManager.startUpdatingLocation()
            } else if sender.selectedSegmentIndex == 1 {
                // 인덕대 표시
                setAnnotation(latitudeValue: 37.631579, longitudeValue: 127.055576, delta: 0.01, title: "인덕대학교", subtitle: "서울시 노원구 월계동 산76")
                self.lblLocationInfo1.text = "보고 계신 위치"
                self.lblLocationInfo2.text = "인덕대학교"
            } else if sender.selectedSegmentIndex == 2 {
                // 남산타워 표시
                setAnnotation(latitudeValue: 37.551079, longitudeValue: 126.988303, delta: 0.01, title: "남산타워", subtitle: "서울특별시 용산구 남산공원길 105")
                self.lblLocationInfo1.text = "보고 계신 위치"
                self.lblLocationInfo2.text = "남산타워"
            } else if sender.selectedSegmentIndex == 3 {
                // 구글본사 표시
                setAnnotation(latitudeValue: 37.42234088647785, longitudeValue: -122.08434167994471, delta: 0.01, title: "구글본사", subtitle: "Mountain View, CA 94043 미국")
                self.lblLocationInfo1.text = "보고 계신 위치"
                self.lblLocationInfo2.text = "구글본사"
            } else if sender.selectedSegmentIndex == 4 {
                // NASA 표시
                setAnnotation(latitudeValue: 38.88306321121613, longitudeValue: -77.01627974772747, delta: 0.01, title: "NASA", subtitle: "Washington, DC 20546 미국")
                self.lblLocationInfo1.text = "보고 계신 위치"
                self.lblLocationInfo2.text = "NASA"
            } else if sender.selectedSegmentIndex == 5 {
                // 백악관 표시
                setAnnotation(latitudeValue: 38.897718, longitudeValue: -77.036508, delta: 0.01, title: "백악관", subtitle: "Washington, DC 20500 미국")
                self.lblLocationInfo1.text = "보고 계신 위치"
                self.lblLocationInfo2.text = "백악관"
            }
        }
        
    }