iOS 프로젝트/개인 앱 - 일공이

iOS MapKit) 저장 버튼 클릭 시 어노테이션 방문 처리 (feat. addObserver & removeObserver)

Dev.Andy 2023. 10. 13. 14:03

목차

    머리말

    구현 내용

    기능 버튼 클릭 시 DB 저장 & 방문 처리 모달창 변화 & 어노테이션 선택 여부
    GIF
    MapKit -버튼 클릭 시 DB 저장하기
    내용 post 메서드에 의한 `addObserver`

    (1) annotation의 데이터 저장
    `saveAnnotationToRealm`


    (2) annotation 변경
    `toggleAnnotation`
    뷰의 생명 주기에 따른 annotation의 선택 여부 결정

    (1) 뷰가 나타나려고 할 때 (viewWillAppear)
    → selectAnnotation


    (2) 뷰가 사라지려고 할 때(viewWillDisappear)
    → annotation 선택 해제 deselectAnnotation

     

    GitHub PR 링크

    annotation 저장 시 realm에 저장 및 방문 처리

    NotificationCenter를 이용한 MapKit의 annotation 방문 처리

    1) 가독성을 위한 extension

    편의성을 위해 NSNotification.Name을 `extension`으로 처리했다.

    extension Notification.Name {
        static let stampButtonClicked = Notification.Name("stampButtonClicked")
        static let selectAnnotation = Notification.Name("selectAnnotation")
        static let deselectAnnotation = Notification.Name("deselectAnnotation")
    }

    2) 작동할 `addObserver` 사용

    `addObserver`는 이벤트가 발생했음을 감지하는 역할을 한다.

    final class StampMapViewController: BaseViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            NotificationCenter.default.addObserver(self, selector: #selector(updateAnnotation), name: NSNotification.Name.stampButtonClicked, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(selectAnnotation), name: NSNotification.Name.selectAnnotation, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(deselectAnnotation), name: NSNotification.Name.deselectAnnotation, object: nil)
        }
    }

    3) `post` (feat. 뷰의 생명 주기)

    `NotificationCenter.default.post`

    이벤트가 발생했다고 알려주는 역할

    class PlaceArrivalViewController: BaseViewController {
        
        // ...
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        // 뷰가 나타나려고 할 때
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            
            NotificationCenter.default.post(name: NSNotification.Name.selectAnnotation, object: nil)
        }
        
        // 뷰가 사라지려고 할 때
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            
            NotificationCenter.default.post(name: NSNotification.Name.deselectAnnotation, object: nil)
        }
        
        // 스탬프 버튼 클릭 시
        @objc
        private func stampButtonClicked() {
            NotificationCenter.default.post(name: NSNotification.Name.stampButtonClicked, object: nil)
            dismiss(animated: true) // 뷰 내리기
        }
    }

    Q & A) removeObserver가 필요한가?

    요약) addObserver의 종류에 따라 다르다

    (2023-10-16) 업데이트

    개발 도중 의문이 들었다.

    addObserver를 했으니 반대로 제거를 해야 메모리 유수를 대비할 수 있지 않을까

     

    그래서 공식 문서를 찾아 보았다.

    `removeObserver`공식 문서

    removeObserver(_:) | Apple Developer Documentation

    a) `forName`의 addObserver

    이 경우에는 반드시 사용해야 한다

    If you used addObserver(forName:object:queue:using:) to create your observer, you should call this method or removeObserver(_:name:object:) before the system deallocates any object that addObserver(forName:object:queue:using:) specifies.

    `addObserver(forName:object:queue:using:)` 메서드로 옵저버를 만들었다면, 운영체제가 해당 메서드가 명시한 객체를  메모리에서 해제하기 전에, `removeObserver(_:)` 또는 `removeObserver(_:name:object:)`를 반드시 호출해야 한다.

    b) `selector`의 addObserver

    이때는 사용하지 않아도 된다

    If your app targets iOS 9.0 and later or macOS 10.11 and later, and you used addObserver(_:selector:name:object:), you do not need to unregister the observer. If you forget or are unable to remove the observer, the system cleans up the next time it would have posted to it.

    해당 메서드는 운영체제에서 알아서 이를 비워주기 때문에 observer에 대한 제거를 생각하지 않아도 된다는 내용이 있었다. 다행히도 나는 2번 케이스에 해당하여 사용하지 않았다. 휴... 다행이다

    코드

    (1) 버튼 클릭 시 Realm에 데이터 저장 및 방문 처리

    post에 따라 addObserver가 작동

    • annotation의 데이터 저장 (saveAnnotationToRealm)
    • annotation 변경 (toggleAnnotation)
    final class StampMapViewController: BaseViewController {
    
        // ...
        
        @objc
        private func updateAnnotation() {
            saveAnnotationToRealm()
            toggleAnnotation()
        }
        
        // 실제 Realm에 데이터를 업데이트 하는 함수
        private func saveAnnotationToRealm() {
            guard let nearestAnnotation = nearestAnnotation,
                  let place = nearestAnnotation.place else { return }
            
            let task = PlaceRealm(
                title: place.title,
                subtitle: place.subtitle,
                category: place.category,
                address: place.address,
                town: place.town,
                image: place.image,
                url: place.url,
                detail: place.detail,
                isCreatedAt: Date()
            )
            
            repository.createItem(task)
        }
        
        // 해당 annotation을 기존에서 제거하고 변경된 annotation view를 적용하여 다시 추가
        private func toggleAnnotation() {
            guard let nearestAnnotation = nearestAnnotation else { return }
            
            mapView.removeAnnotation(nearestAnnotation)
            
            let visitedAnnotationView = VisitedPlaceAnnotationView(annotation: nearestAnnotation, reuseIdentifier: VisitedPlaceAnnotationView.reuseIdentifier)
            
            guard let visitedAnnotation = visitedAnnotationView.annotation else { return }
            
            mapView.addAnnotation(visitedAnnotation)
            
            placeAnnotations = placeAnnotations.filter { $0 !== nearestAnnotation }
            
            isArrivedToPlace = false
        }
    }

    (+) 실제로 realm 파일에 저장된 annotation 데이터

    realm 파일에 저장된 이미지

    (2) 모달창 변화에 따른 annotation 선택 처리 (feat. 뷰의 생명주기)

    뷰의 생명 주기에 따라 annotation의 선택 처리

    • 뷰가 나타나려고 할 때 (viewWillAppear) → annotation 선택(selectAnnotation)
    • 뷰가 사라지려고 할 때(viewWillDisappear) → annotation 선택 해제(deselectAnnotation)
    class PlaceArrivalViewController: BaseViewController {
        
        // ...
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        // 뷰가 나타나려고 할 때
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            
            NotificationCenter.default.post(name: NSNotification.Name.selectAnnotation, object: nil)
        }
        
        // 뷰가 사라지려고 할 때
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            
            NotificationCenter.default.post(name: NSNotification.Name.deselectAnnotation, object: nil)
        }
        
        // 스탬프 버튼 클릭 시
        @objc
        private func stampButtonClicked() {
            NotificationCenter.default.post(name: NSNotification.Name.stampButtonClicked, object: nil)
            dismiss(animated: true) // 뷰 내리기
        }
    }
    final class StampMapViewController: BaseViewController {
    
        // ...
        
        @objc
        private func selectAnnotation() {
            guard let nearestAnnotation else { return }
            mapView.selectAnnotation(nearestAnnotation, animated: true)
        }
        
        @objc
        private func deselectAnnotation() {
            guard let nearestAnnotation else { return }
            mapView.deselectAnnotation(nearestAnnotation, animated: true)
        }
    }

    꼬리말

    수정일: 2024-01-02 화