Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #6] 23 / 03 / 25

오늘은 간단하게 MapKit을 구현해보려 합니다.

 

MapKit의 속성 및 메서드들을 정리해보려고 합니다.

 

 

지도 불러오기

객체 MKMapView를 만든 후, 레이아웃만 잡아주면 됩니다.

class MapViewController: UIViewController {
    
    let mapView = MKMapView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.addSubview(mapView)
        mapView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
}

 

 

속성과 메서드

너무 많아서 전체를 설명하긴 힘들고, 자주 사용하는 것들만 보여드리겠습니다.

자세한 내용은 공식문서 를 참고해 주세요.

 

 

지도를 표시할 유형

크게 3가지로 나뉩니다. 기본값은 standard입니다.

원하는 걸 사용하면 됩니다.

mapView.mapType = MKMapType.standard    // 기본 지도 (지역 정보)
mapView.mapType = MKMapType.satellite   // 위성 지도
mapView.mapType = MKMapType.hybrid      // 위성 지도 + 지역 정보

 

그리고 iOS 16 이상부터는 MKMapType을 쓰는 대신,

preferredConfiguration을 사용한다고 하네요.

 

일단 너무 최신 버전이기 때문에, 간단히 종류만 살펴봅시다.

클래스로 구성돼 있고 유형은 이전 MKMapType과 동일하네요.

 

대신 클래스로 만들어져서 조금 더 Configuration의 옵션을 수정할 수 있게 됐습니다.

(교통 상황, 스타일 등)

mapView.preferredConfiguration = MKStandardMapConfiguration()  // 기본 지도
mapView.preferredConfiguration = MKImageryMapConfiguration()   // 위성 지도
mapView.preferredConfiguration = MKHybridMapConfiguration()    // 위성 + 지역 정보

 

 

지도의 표시 구성

// 줌 가능 여부
mapView.isZoomEnabled = true
// 이동 가능 여부
mapView.isScrollEnabled = true
// 각도 조절 가능 여부 (두 손가락으로 위/아래 슬라이드)
mapView.isPitchEnabled = true
// 회전 가능 여부
mapView.isRotateEnabled = true
// 나침판 표시 여부
mapView.showsCompass = true
// 축척 정보 표시 여부
mapView.showsScale = true
// 위치 사용 시 사용자의 현재 위치를 표시
mapView.showsUserLocation = true

 

 

지도 시야 제어

MapView에서는 설정한 영역을 기준으로 이동 및 확대와 같은 동작을 수행할 수 있습니다.

 

각각의 MapView의 위치 속성으로 접근하여 지도의 시야 제어도 가능하지만,

애니메이션이 포함돼 있는 메서드를 사용하는 게 더 편할 것 같아 보이죠?

 

여기선 시야 제어에서 사용되는 메서드들을 간단히 알아봅시다.

기본적으로 시야를 제어하는 방법은 2가지가 있습니다.

// 원하는 위치 중심으로 이동 및 확대를 시킨다. (애니메이션 포함)
func setRegion(MKCoordinateRegion, animated: Bool)

// 원하는 위치로 이동만 시킵니다. (줌 기능 미포함, 애니메이션 포함)
func setCenter(CLLocationCoordinate2D, animated: Bool)

setCenter 메서드는 단순히 CLLocationCoordinate2D를 통해 위경도만 넣어주면 되므로 설명은 생략하겠습니다.

 

setRegion는 MKCoordinateRegion를 통해 위치를 지정해 줍니다.

func setRegion(_ region: MKCoordinateRegion, animated: Bool)

 

여기 파라미터의 MKCoordinateRegion(영역, 반경)은 두 가지 방법으로 나타낼 수 있습니다.

 

두 가지 방법 모두 중심점(center)의 값이 필수로 필요합니다. 위 경도를 통하여 중심점을 알 수 있습니다.

그리고 보이는 영역 설정은 span 또는 미터(m)로 지정이 가능합니다.

 

먼저, span을 이용한 지도 위치 변경 방법

// 중심값(필수): 위, 경도
let center = CLLocationCoordinate2D(latitude: 37.27,
                                    longitude: 127.43)

// 영역을 확대 및 축소를 한다. (값이 낮을수록 화면을 확대/높으면 축소)
let span = MKCoordinateSpan(latitudeDelta: 0.01,
                            longitudeDelta: 0.01)

// center를 중심으로 span 영역만큼 확대/축소 해서 보여줌
let region = MKCoordinateRegion(center: center,
                                span: span)

mapView.setRegion(region, animated: true)

 

미터(m)를 이용한 지도 위치 변경 방법

span보다는 미터(m)를 사용하는게 저에겐 더 친숙하네요.

// 중심값(필수): 위, 경도
let center = CLLocationCoordinate2D(latitude: 37.27,
                                    longitude: 127.43)

// center를 중심으로 지정한 미터(m)만큼의 영역을 보여줌
let region = MKCoordinateRegion(center: center,
                                latitudinalMeters: 500,
                                longitudinalMeters: 500)

mapView.setRegion(region, animated: true)

 

값을 구분하기 위해 따로따로 설정해서 복잡한 것처럼 보일 수도 있지만,

한번 천천히 살펴보시면 엄청 쉽습니다.

 

 

사용자 위치

만약, 사용자의 현재 위치를 알 수 있다면(위치 권한을 허용 중이라면),

사용자 위치에 대한, 여러 기능들을 사용할 수 있습니다.

// 위치 사용 시 사용자의 현재 위치를 표시
mapView.showsUserLocation = true

// userLocation: 유저 위치에 대한 정보를 알 수 있다.(위치, 업데이트 여부 등)
mapView.userLocation.location
mapView.userLocation.coordinate
mapView.userLocation.isUpdating

// 사용자 위치를 추적합니다. 
// follow : 현재 위치를 보여줍니다.
// followWithHeading : 핸드폰 방향에 따라 지도를 회전시켜 보여줍니다.(앞에 레이더 포함)
mapView.userTrackingMode  = .follow
mapView.userTrackingMode  = .followWithHeading

// 이것도 사용자 위치를 추적하는데, 애니메이션 효과가 추가 되어 부드럽게 화면 확대 및 이동
mapView.setUserTrackingMode(.follow, animated: true)
mapView.setUserTrackingMode(.followWithHeading, animated: true)

 

 

mapView의 델리게이트

MKMapViewDelegate에는 여러 메서드들이 있습니다.

지도의 위치 변경, 위치 추적, 어노테이션 등에 접근하여 수정 및 처리가 가능합니다.

class MapViewController: UIViewController, MKMapViewDelegate {
    
    func setupMapView() {
        // 대리자를 뷰컨으로 설정
        mapView.delegate = self
    }
}

 

자주 사용할 것 같은 것만 정리하겠습니다.

// 지도를 스크롤 및 확대할 때, 호출이 됩니다. 즉, 지도 영역이 변경될 때 호출
// ex. 특정 영역을 확대했을 때, 해당 지역의 정보를 갖고 온다던가...
// 어디에 쓰질지는 고민을 좀 더 해봐야 겠네요
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    print("지도 위치 변경")
}
// 사용자 위치가 업데이트 될 때, 호출됩니다.
// ex. 사용자 위치가 업데이트될 때, 보여질 영역을 확대해서 보여줄 수 있겠네요.
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
    let region = MKCoordinateRegion(center: userLocation.coordinate,
                                    latitudinalMeters: 5000,
                                    longitudinalMeters: 5000)
    mapView.setRegion(region, animated: true)
}
// AnnotationView를 커스터마이징 할 수 있게 해줍니다.
// 직접 AnnotationView의 스타일, 이미지, 컬러 등을 변경해줄 수 있습니다
// 자세한 설명은 아래서...
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    return MKAnnotationView()
}
// MKAnnotationView에서 콜아웃 버튼을 탭할 때 호출합니다.
// ex. 버튼을 클릭 시 다른 뷰컨트롤러로 이동, 작업을 수행
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
    self.present(UIViewController(), animated: true)
}
// Annotation을 클릭 또는 취소 시 호출
// 같은 동작을 하지만 접근할 수 있는 파라미터가 다르다. (사용은 각자 마음대로...)

// AnnotationView에 접근하면 각종 이미지 관련 속성을 수정 가능
func mapView(MKMapView, didSelect: MKAnnotationView) {}
func mapView(MKMapView, didDeselect: MKAnnotationView) {}

// Annotation은 현제 타이틀, 서브타이틀 등 접근 가능
func mapView(MKMapView, didDeselect: MKAnnotation) {}
func mapView(MKMapView, didSelect: MKAnnotation) {}

 

 

Annotation(핀 + 콜아웃)

Annotaion이란?

핀으로 위치 표시와 정보(callout 버튼)를 함께 저장한 데이터입니다.

참고로, 위치 정보는 필수적으로 저장해줘야 합니다.

자세한 내용은 공식문서 를 참고하세요.

 

기본 Annotaion은 이렇게 만들어 줄 수 있습니다.

func createAnnotaion() {
    let annotation = MKPointAnnotation()
    
    annotation.coordinate = CLLocationCoordinate2D(latitude: 37.27543611,
                                                   longitude: 127.4432194)
    
    annotation.title = "HOTDOG"
    annotation.subtitle = "세상에서 가장 맛있는 핫도그 가게"
    
    // 맵뷰에 Annotaion 추가
    mapView.addAnnotation(annotation)
}

 

그리고 MKAnnotation은 프로토콜로 만들어져 있어서,

클래스로 MKAnnotation 프로토콜을 채택하면, 사용하기 편하게 만들 수 있습니다.

class CustomAnnotation: NSObject, MKAnnotation {
    var title: String?
    var subtitle: String?
    @objc dynamic var coordinate: CLLocationCoordinate2D

    init(title: String, subtitle: String, coordinate: CLLocationCoordinate2D) {
        self.title = title
        self.subtitle = subtitle
        self.coordinate = coordinate
    }
}

여기선 @objc dynamic를 무조건 사용해야 한다고 하네요?

coordinate 공식문서

 

이런 식으로 여러 개의 Annotaion을 쉽게 생성 및 관리할 수 있겠죠?

func createAnnotaion() {
    let aPin = CustomAnnotation(title: "핫도그집",
                                subtitle: "맛있는 핫도그",
                                coordinate: CLLocationCoordinate2D(latitude: 37.2,
                                                                   longitude: 127.4))
    let bPin = CustomAnnotation(title: "피자집",
                                subtitle: "맛있는 피자",
                                coordinate: CLLocationCoordinate2D(latitude: 37.1,
                                                                   longitude: 127.5))
    let cPin = CustomAnnotation(title: "떡볶이집",
                                subtitle: "맛있는 떡볶이",
                                coordinate: CLLocationCoordinate2D(latitude: 37.3,
                                                                   longitude: 127.3))
    
    let annotations: [MKAnnotation] = [aPin, bPin, cPin]
    
    mapView.addAnnotations(annotations)
}

 

 

Annotaion 모양을 바꿔 보자

기본적으로 AnnotaionView는 재사용이 가능해야 합니다.

무슨 의미냐 하면, 만약 Annotaion이 지도에 1000개 정도 찍혀 있다고 가정해 봅시다.

 

하지만 항상 지도의 모든 영역에서 Annotaion을 표시하거나,

계속해서 AnnotaionView를 새로 만들게 되면 메모리 낭비가 심하겠죠?

 

그래서 지도에서 보이는 영역에서만 Annotaion이 표시(재사용)되고,

지도 밖 영역은 내부적으로 숨겨 메모리를 관리하도록 만들어 줘야 합니다.

 

 

진짜로 간단히 MapView의 델리게이트(viewFor)를 통해 만들어 봅시다.

 

위에 만들어 둔 Annotaion들을 갖고, 별모양으로 변경해 봅시다.

// 어노테이션을 커스터마이징 할 수 있게 해줍니다.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    
    // 현재 위치 표시(점)도 일종에 어노테이션이기 때문에, 이 처리를 안하게 되면, 유저 위치 어노테이션도 변경 된다.
    // guard !annotation.isKind(of: MKUserLocation.self) else { return nil }
    guard !(annotation is MKUserLocation) else { return nil }

    
    // 식별자
    let identifier = "Custom"
    
    // 식별자로 재사용 가능한 AnnotationView가 있나 확인한 뒤 작업을 실행 (if 로직)
    var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
    
    if annotationView == nil {
        // 재사용 가능한 식별자를 갖고 어노테이션 뷰를 생성
        annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
        
        // 콜아웃 버튼을 보이게 함
        annotationView?.canShowCallout = true
        // 이미지 변경
        annotationView?.image = UIImage(systemName: "star.fill")
        
        // 상세 버튼 생성 후 액세서리에 추가 (i 모양 버튼)
        // 버튼을 만들어주면 callout 부분 전체가 버튼 역활을 합니다
        let button = UIButton(type: .detailDisclosure)
        annotationView?.rightCalloutAccessoryView = button
    }
    
    return annotationView
}

 

이해가 안 되는 게 있는데,

여기선 하나의 AnnotaionView 밖에 없어서 그런가?

아니면, Annotation이 한 종류 밖에 없어서 그런가?

 

reuseIdentifier를 따로 설정 안 해줘도 전체가 적용이 되네요?

음...

 

 

마무리

일단, 오늘은 여기까지...

 

각각의 Annotaion 마다 직접 이미지를 지정하고 싶었는데,

위처럼 설정해 주면 Annotaion들이 모두 같은 이미지로 변경이 되네요...

 

아마도, 커스텀으로 AnnotaionView를 만들어 적용을 시켜야 되나?

Annotaion를 여러 개를 만들어줘야 하나?

 

흠... 이건 내일 찾아보고 다시 공부를 해봐야겠습니다.

 

다음에 이어서 공부해 봅시다!

 

다음 - [Swift/TIL #7] MKAnnotationView를 직접 만들어 보자

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!