Naver Maps SDK에 Delegate Proxy 적용시켜보자
NaverMaps 와 같이 외부 프레임워크를 사용할때 delegate사용은 필수적이다. RxSwift를 활용한 MVVM을 학습하던 도중에 Delegate Proxy 에 관련 된 개념이 나와서 적용해보기로 했다.
이 글은 NaverMaps iOS SDK , NaverMaps API Reference를 참고하여 작성하였습니다.
Delegate Proxy란?
RxSwift에서 DelegateProxy.swift 와 DelegateProxyType.swift 가 존재하는것을 확인할 수 있다.
- delegateProxy.swift
- delegateProxyType.swift
위의 두 파일은 delegate를 사용하는 외부 프레임워크와 RxSwift의 매개체 같은 역할을 해주는 파일 이라고 생각하면된다. NaverMaps도 NMFMapView
에 관련된 상호작용이나 지도 좌표를 제어하기 위해서는 NMFMapViewDelegate
나 NMFLocationManagerDelegate
와 같이 delegate를 활용해야한다. Delegate Proxy의 두 파일을 이용하면 RxSwift와의 결합을 통한 작업이 가능해 진다는 것이다.
// NMFNaverMapView는 NMFMapViewDelegate의 delegate 객체를 가지고 있다.
@interface NMFNaverMapView : UIView
@property(nonatomic, weak, nullable) IBOutlet id<NMFMapViewDelegate> delegate;
지금 까지는 Delegate안의 메서드를 사용할려면 다음과 같이 직접 delegate안의 함수들을 사용하여서 코드를 작성하였다.
// MARK: - NMFMapViewDelegate
extension MainViewController: NMFMapViewDelegate {
// 네이버 지도 위 터치 된 곳의 좌표 반환
func didTapMapView(_ point: CGPoint, latLng latlng: NMGLatLng) {
print("\(latlng.lat), \(latlng.lng)")
}
}
// MARK: - NMFLocationManagerDelegate
extension MainViewController: NMFLocationManagerDelegate {
// 현재 위치 반환
func locationManager(_ locationManager: NMFLocationManager!, didUpdateLocations locations: [Any]!) {
guard let curLocation = locations.last as? CLLocation else { return }
print(curLocation)
}
}
우리는 이제 RxSwift를 사용하기 때문에 이를 RxSwift로 아래와 같이 사용하고 싶은 것이다.
class NMViewController: UIViewController {
@IBOutlet weak var naverMapView: NMFNaverMapView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
initialize()
}
func bind() {
naverMapView.rx.didTapMapView
.asObservable()
.subscribe(onNext: { (point) in
print("\(latlng.lat), \(latlng.lng)")
})
.disposed(by: disposeBag)
naverMapView.rx.locationManager
.asObservable()
.subscribe(onNext: { (locations) in
guard let curLocation = locations.last as? CLLocation else { return }
print(curLocation)
})
.disposed(by: disposeBag)
}
...
}
그럼 어떻게 외부 프레임워크의 Delegate를 Rx와 연결하는지 DelegateProxy의 구조를 먼저 보자
delegateProxy.swift
open class DelegateProxy<P: AnyObject, D>: _RXDelegateProxy {
public typealias ParentObject = P
public typealias Delegate = D
private var _sentMessageForSelector = [Selector: MessageDispatcher]()
private var _methodInvokedForSelector = [Selector: MessageDispatcher]()
/// Parent object associated with delegate proxy.
private weak var _parentObject: ParentObject?
fileprivate let _currentDelegateFor: (ParentObject) -> AnyObject?
fileprivate let _setCurrentDelegateTo: (AnyObject?, ParentObject) -> Void
/// Initializes new instance.
///
/// - parameter parentObject: Optional parent object that owns `DelegateProxy` as associated object.
public init<Proxy: DelegateProxyType>(parentObject: ParentObject, delegateProxy: Proxy.Type)
where Proxy: DelegateProxy<ParentObject, Delegate>, Proxy.ParentObject == ParentObject, Proxy.Delegate == Delegate {
self._parentObject = parentObject
self._currentDelegateFor = delegateProxy._currentDelegate
self._setCurrentDelegateTo = delegateProxy._setCurrentDelegate
MainScheduler.ensureRunningOnMainThread()
#if TRACE_RESOURCES
_ = Resources.incrementTotal()
#endif
super.init()
}
...
DelegateProxy
클래스는 위와 같이 Object와 Delegate를 세팅값으로 받고 있다.
- Object :
NMFNaverMapView
- Delegate:
NMFMapViewDelegate
delegateProxyType.swift
public protocol DelegateProxyType: class {
associatedtype ParentObject: AnyObject
associatedtype Delegate
/// It is require that enumerate call `register` of the extended DelegateProxy subclasses here.
static func registerKnownImplementations()
/// Unique identifier for delegate
static var identifier: UnsafeRawPointer { get }
static func currentDelegate(for object: ParentObject) -> Delegate?
...
delegateProxyType의 경우 프로토콜로 정의 되어 있고 필수적으로 구현해야하는 함수들의 정의 되어있다.
대략적인 구조는 보고 직접 구현하면서 익혀보자. CustomDelegateProxy 를 구현하기 위해서는 아래와 같이 3가지가 필요하다.
DelegateProxy<NMFNaverView , NMFMapViewDelegate>
DelegateProxyType
NMFMapViewDelegate
이제 이 것들을 채택하고 상속한 RxNMFMapViewDelegateProxy
를 선언해 보자
RxNMFMapViewDelegateProxy
import RxSwift
import RxCocoa
import NMapsMap
class RxNMFMapViewDelegateProxy: DelegateProxy<NMFNaverMapView, NMFMapViewDelegate>, DelegateProxyType, NMFMapViewDelegate {
static func registerKnownImplementations() {
self.register { (naverMapView) -> RxNMFMapViewDelegateProxy in
RxNMFMapViewDelegateProxy(parentObject: naverMapView, delegateProxy: self)
}
}
// 현재 obeject 즉 NMFNaverMapView의 delegate 객체 반환
static func currentDelegate(for object: NMFNaverMapView) -> NMFMapViewDelegate? {
return object.delegate
}
// delegate 객체 설정 -> NMFNaverMapView.delegate = self 와 같은 역할
static func setCurrentDelegate(_ delegate: NMFMapViewDelegate?, to object: NMFNaverMapView) {
object.delegate = delegate
}
}
위와 같이 DelegateProxy 프로토콜에 필요한 메서드 세개를 반드시 구현해주어야한다.
registerKnownImplementation()
currentDelegate
setCurrentDelegate
이제는 NMFLocationManager
에 관련된 DelegateProxy를 구현해줄려고한다.
그런데 한가지 문제가 있었다. NMFLocationManagerDelegate
의 경우 NMFLocationManager
가 delegate 객체를 가지고 있지 않고 sharedInstance()
메서드를 통해 아래의 함수들로 delegate 를 설정해주고 있었다.
대신에 NMFLocationManager
도 결국 CLLocationManagerDelegate
를 채택하여 사용하기 때문에 CLLocationManager
를 이용해서 현재 위치의 경위도 좌표를 가져와 보자
CLLocationManagerDelegateProxy
실시간으로 사용자의 현재 경위도 좌표를 반환하는 CoreLocation에 CLLocationManageDelegate
의 CallBack 함수를 사용한다. 우선적으로 위와 같이 동일하게 delegate proxy를 설정한다.
import RxCocoa
import RxSwift
import CoreLocation
class RxCLLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
static func registerKnownImplementations() {
self.register { (manager) -> RxCLLocationManagerDelegateProxy in
RxCLLocationManagerDelegateProxy(parentObject: manager, delegateProxy: self)
}
}
static func currentDelegate(for object: CLLocationManager) -> CLLocationManagerDelegate? {
return object.delegate
}
static func setCurrentDelegate(_ delegate: CLLocationManagerDelegate?, to object: CLLocationManager) {
object.delegate = delegate
}
}
extension Reactive where Base: CLLocationManager {
var delegateCLLocationManager: DelegateProxy<CLLocationManager, CLLocationManagerDelegate> {
return RxCLLocationManagerDelegateProxy.proxy(for: self.base)
}
var didUpdateLocations: Observable<[CLLocation]> {
return delegateCLLocationManager.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)))
.map { return $0[1] as? [CLLocation] ?? [CLLocation]() }
}
}
아래 두개의 delegate를 rx와 연동을 하였고 Delegate proxy 작업은 모두 끝났다
NMFMapViewDelegate
CLLocationManagerDelegate
이제 bind를 통해 어떻게 호출하는지 확인해보자.
Custom DelegateProxy 사용
import UIKit
import RxSwift
import RxCocoa
import NMapsMap
class NMViewController: UIViewController {
@IBOutlet weak var naverMapView: NMFNaverMapView!
@IBOutlet weak var navigationButton: UIButton!
@IBOutlet weak var latLabel: UILabel!
@IBOutlet weak var lngLabel: UILabel!
let locationManager = CLLocationManager()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setUpNaverMapView()
locationManager.requestWhenInUseAuthorization()
// 사용자의 현재 위치 업데이트 시작 -> 이 함수를 호출해야 callback함수가 호출된다.
locationManager.startUpdatingLocation()
bind()
}
private func setUpNaverMapView() {
naverMapView.positionMode = .direction
naverMapView.mapView.zoomLevel = 15
naverMapView.showLocationButton = true
naverMapView.showIndoorLevelPicker = true
}
func bind() {
// 지도를 터치했을때 해당 위치의 경위도 좌표를 반환하는 콜백 메서드
naverMapView.rx.didTapMapView
.asObservable()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
// 지도가 표시하고 있는 영역이 변경되었을 때 호출되는 콜백 메서드
naverMapView.rx.regionDidChangeAnimated
.asObservable()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
// latLabel
locationManager.rx.didUpdateLocations
.asObservable()
.map {
return "경도 : \($0.lat)"
}
.bind(to: latLabel.rx.text)
.disposed(by: disposeBag)
// lngLabel
locationManager.rx.didUpdateLocations
.asObservable()
.map {
return "위도 : \($0.lng)"
}
.bind(to: lngLabel.rx.text)
.disposed(by: disposeBag)
}
}
총 3개의 delegate proxy를 활용하여 delegate 내부의 메서드를 Rx와 연결했다.
이렇게 굳이 extension을 통해 ViewController라 massive 하는 것을 막고 또 가장 중요한 Rx와 delegate 메서드를 통해 제어 할 수 있다는 것이 가능해졌다.
delegate proxy가 가능한지 우선 알아보는것이 중요한거 같다 내부에 delegate 객체가 우선 존재해야한다. delegateProxyType 프로토콜의 함수에서 delegate를 설정하고 반환하는 함수가 있기때문에 이를 위해서 객체 우선 있는지 파악해야한다.
NMFLocationManager
의 경우 이것이 불가능해서 혹시 다른 방법이 있는지 공부하면서 찾아봐야겠다.