Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #7] 23 / 03 / 27 ~ 23 / 03 / 28

자 오늘도 Mapkit을 할 건데요.

이번엔 MKAnnotationView에 관련하여 알아보겠습니다.

 

공식문서 예제 를 참고하여 만들었습니다.

 

시작

자 그러면 이제 MKAnnotationView를 상속받아

커스텀으로 AnnotationView를 만들어 보겠습니다.

 

타이틀과 사진을 포함한 AnnotationView을 만들어 보려고 하는데요.

AnnotationView도 일종에 View라고 생각하고 만드시면 됩니다.

 

Annotation, AnnotationView 및 처리까지

한번 직접 구현을 해봅시다.

 

우린 [Swift/TIL #6] MapKit을 사용해 보자 에서 직접 Annotation을 만들어봤습니다.

 

이번에는 이미지도 같이 사용할 것이므로 이미지 이름 속성도 추가해 줍시다.

class CustomAnnotation: NSObject, MKAnnotation {

    // This property must be key-value observable, which the `@objc dynamic` attributes provide.
    @objc dynamic var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    var imageName: String?
    
    init(title: String, coordinate: CLLocationCoordinate2D) {
        self.title = title
        self.coordinate = coordinate
    }
}

공식문서 에서는 Mapkit의 coordinate를 사용 시

동적 디스패치에서 사용되는 KVO를 준수해야 한다고 하네요.

 

 

바로 AnnotationView를 만들어 보겠습니다.

 

원하는 모양으로 레이아웃을 잡고 사용할 데이터를 정의해 주면 됩니다.

직접 CAShapeLayer를 통해 모양 구현도 가능하지만... 오래 걸리므로 패스

class CustomAnnotationView: MKAnnotationView {
    
    lazy var backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .darkGray
        return view
    }()
    
    lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.font = .preferredFont(forTextStyle: .caption1)
        label.textColor = .orange
        label.textAlignment = .center
        return label
    }()
    
    lazy var customImageView: UIImageView = {
        let view = UIImageView()
        view.contentMode = .scaleAspectFit
        view.backgroundColor = .lightGray
        return view
    }()
    
    lazy var stacView: UIStackView = {
        let view = UIStackView(arrangedSubviews: [titleLabel, customImageView])
        view.spacing = 5
        view.axis = .vertical
        return view
    }()
    
    
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        
        configUI()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configUI() {
        self.addSubview(backgroundView)
        backgroundView.addSubview(stacView)
        
        backgroundView.snp.makeConstraints {
            $0.width.height.equalTo(70)
        }
        
        stacView.snp.makeConstraints {
            $0.edges.equalTo(backgroundView).inset(5)
        }
    }
    
    // Annotation도 재사용을 하므로 재사용 전 값을 초기화 시켜서 다른 값이 들어가는 것을 방지
    override func prepareForReuse() {
        super.prepareForReuse()
        customImageView.image = nil
        titleLabel.text = nil
    }
    
    // 이 메서드는 annotation이 뷰에서 표시되기 전에 호출됩니다. 
    // 즉, 뷰에 들어갈 값을 미리 설정할 수 있습니다
    override func prepareForDisplay() {
        super.prepareForDisplay()
        
        guard let annotation = annotation as? CustomAnnotation else { return }
        
        titleLabel.text = annotation.title
        
        guard let imageName = annotation.imageName,
              let image = UIImage(named: imageName) else { return }
        
        customImageView.image = image
        
        // 이미지의 크기 및 레이블의 사이즈가 변경될 수도 있으므로 레이아웃을 업데이트 한다.
        setNeedsLayout()
        
        // 참고. drawing life cycle :
        // setNeedsLayout를 통해 다음 런루프에서 레이아웃을 업데이트하도록 예약
        // -> layoutSubviews을 통해 레이아웃 업데이트
        
        // layoutSubviews를 쓰려면 setNeedsLayout도 항상 같이 사용해야 한다고 하네요.
    }
    

    override func layoutSubviews() {
    super.layoutSubviews()
    
    }
}

 

이제 사용할 ViewController에서

Annotation 생성 및 만든 CustomAnnotationView를 사용해 봅시다.

 

일단 이와 같이 Annotation을 추가를 해줍니다.

private func addAnnotation() {
    let annotation = CustomAnnotation(title: "My Home",
                                      coordinate: CLLocationCoordinate2D(latitude: 37.2719952,
                                                                         longitude: 127.4348221))
    // 이미지 이름 설정                                                                         
    annotation.imageName = "myProfile"
    // mapView에 Annotation 추가
    mapView.addAnnotation(annotation)
}

 

그리고 Annotation은 재사용이 필요하므로

MKMapView에서 AnnotationView를 register 등록을 해줍니다.

 

커스텀 AnnotationView를 만들어 사용하려면 필수로 등록해줘야 한다!

재사용 이유는 [Swift/TIL #6] MapKit을 사용해 보자 를 참고해 주세요.

// 재사용을 위해 식별자 생성
func registerMapAnnotationViews() {
    // NSStringFromClass 클래스 타입자체 이름을 string 반환
    mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(CustomAnnotationView.self))
}

 

식별자를 만들어 줬으므로 이 식별자를 갖고

AnnotationView를 생성하는 메서드를 만들어 줍시다.

// 식별자를 갖고 Annotation view 생성
func setupAnnotationView(for annotation: CustomAnnotation, on mapView: MKMapView) -> MKAnnotationView {
    // dequeueReusableAnnotationView: 식별자를 확인하여 사용가능한 뷰가 있으면 해당 뷰를 반환
    return mapView.dequeueReusableAnnotationView(withIdentifier: NSStringFromClass(CustomAnnotationView.self), for: annotation)
}

 

마지막으로 [Swift/TIL #6] MapKit을 사용해 보자 에서 배운

AnnotationView를 커스터마이징 하는 델리게이트에서 정의해 주면 완성!

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    // 현재 위치 표시(점)도 일종에 어노테이션이기 때문에, 이 처리를 안하게 되면, 유저 위치 어노테이션도 변경 된다.
    guard !annotation.isKind(of: MKUserLocation.self) else { return nil }
    
    var annotationView: MKAnnotationView?
    
    // 다운캐스팅이 되면 CustomAnnotation를 갖고 CustomAnnotationView를 생성
    if let customAnnotation = annotation as? CustomAnnotation {
        annotationView = setupAnnotationView(for: customAnnotation, on: mapView)
    }
    
    return annotationView
}

 

이런 모양의 카드 Annotation이 완성이 됐습니다.

 

 

문제점 - 1

AnnotationView의 사진을 클릭하면 callout이 나오지 않는 문제가 발생했습니다.

canShowCallout를 true로 설정을 해줘도 마찬가지로 나오지 않네요.

 

하지만 Annotation의 origin(0, 0) 부분 근처를 선택하면 callout이 작동을 하긴 하네요.

 

몇 번의 삽질 끝에 알아낸 건!

우리는 그냥 UIView, UILabel, UIImageView만 만든 뒤 화면에 올려준 겁니다.

 

따로 AnnotationView 자체 크기를 조절을 안 해줘서 문제가 생긴 겁니다.

(문제점: 이미지를 클릭하면 동작을 안 함, 하지만 원점(0, 0) 클릭하면 동작)

 

알기 쉽게 그림으로 가정해 보면 이런 느낌일 겁니다.

빨간 점이 AnnotationView의 자체 크기 (클릭 가능한 범위)

대충 문제를 알았으니 한번 해결해 봅시다.

 

아마도 AnnotationView의 frame을 조절하면 선택 가능한 영역도 커질 것 같네요.

배경크기만큼 size를 한번 설정해 봅시다.

// MKAnnotationView의 layoutSubviews를 재정의 해줍니다.
override func layoutSubviews() {
    super.layoutSubviews()
    // MKAnnotationView 크기를 backgroundView 크기 만큼 정해줌.
    bounds.size = CGSize(width: 70, height: 70)
}

 

클릭해 보니 callout도 중간에 위치하고

빨간 영역만큼 클릭이 가능해졌습니다!!!

 

 

문제점 - 2

어... 분명히 어노테이션 크기만 키웠는데?

어노테이션의 위치가 조금 변한 거 같이 보이더라고요.

(AnnotationView가 설정한 위치를 가리는 이슈가 발생)

좌: 기본, 우: 크기 변경

 

정확한 위치 확인을 위해 print(frame.origin)를 찍어보니,

 

bounds.size = CGSize(width: 70, height:70)일 때

즉 사이즈가 70일 때, 원점이 (-35, -35)만큼 이동하더라고요.

 

frame.size가 커지게 되면

원점의 위치도 size의 절반만큼 반대 방향으로 이동하는 걸 확인했습니다.

 

이를 통해서 위치를 살짝 조정을 해보겠습니다.

마찬가지로 layoutSubviews에서 정의를 해줍시다.

override func layoutSubviews() {
    super.layoutSubviews()
    
    bounds.size = CGSize(width: 70, height: 70)
    // 중심점을 기준으로 크기의 절반만큼 이동
    centerOffset = CGPoint(x: 0, y: 35)
}

 

AnnotationView 위치를 아래로 35만큼 이동시켜 줘서

설정한 위치를 가리지 않으면서 윗변의 중앙에 위치하게 됐네요!

 

 

마무리

인간적으로... MapKit에 대한 정보가 너무 없네요.

 

이미 이 프로젝트에서는 MapKit을 쓰기로 결정했으니...

문제점들을 해결하며 끝까지 알아봅시다 ㅠ.ㅠ

 

나중에 수정해야 할 곳

일단 사용자가 보기엔 저 네모박스가 어디를 가르지고 있는지 모르니,

CAShapeLayer를 이용해 화살표를 그려줘 조금 더 직관적으로 보이게 만들어야겠습니다.

 

그리고 아마도 다음엔 MKClusterAnnotation에 대해서 알아볼까 합니다.

MKClusterAnnotation를 간단히 설명하면, 지도에 Annotation이 많이 존재한다고 가정해봅시다.

확대나 축소 시, Annotation들을 그룹화시켜서 간소화 시키는 역할을 한다고 하네요.

 

 

참고

https://developer.apple.com/documentation/mapkit

https://developer.apple.com/documentation/mapkit/mapkit_annotations

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

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