본문 바로가기
ios/swift

[rxswift] delegate proxy

by kimyounggyun 2022. 8. 8.

DelegateProxy

DelegateProxy란 delegate method를 호출하는 객체와 구독자 사이에 delegate를 대신 처리하는 객체이다. 특정 delegate method가 호출되는 시점에 구독자로 next 이벤트를 전달한다.

Delegate Pattern을 사용한 코드를 DelegateProxy를 구현해 Rxswift 형식으로 바꿔보자!

// delegate pattern 코드
func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
    // code
}

// rxswift 코드
let locationManager = CLLocationManager()
locationManager.rx.didUpdateLocations
    .subscribe(onNext: { locations in
        // do
    })
    .disposed(by: bag)

구현

DelegateProxy를 구현할 때는 보통 1개의 클래스와 2개의 익스텐션을 만든다.
CoreLocation을 사용하여 위치를 받아와 mapView에 현재 위치를 띄워보자. 

import UIKit
import MapKit
import CoreLocation

import RxSwift
import RxCocoa

class DelegateProxyViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!
    
    private let disposeBag = DisposeBag()
    private let locationManager = CLLocationManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

1. 확장할 객체에 HasDelegate 프로토콜 구현을 추가한다.

extension CLLocationManager: HasDelegate {
    public typealias Delegate = CLLocationManagerDelegate
}

2. DelegateProxy 클래스를 구현한다.

보통 네이밍은 Rx+확장할객체+DelegateProxy이다.
구현하는 클래스는 DelegateProxy를 상속하고 DelegateProxyType와 확장하려고 하는 델리게이트 프로토콜을 채택한다.
DelegateProxy는 제네릭 클래스이고 두 개의 형식 파라미터를 받는다. 첫 번째 파라미터는 확장하려는 클래스이고 두 번째 파라미터는 확장하려는 클래스의 델리게이트 프로토콜이다.

class RxCLLocationManagerDelegateProxy:
    DelegateProxy<CLLocationManager, CLLocationManagerDelegate>,
    DelegateProxyType,
    CLLocationManagerDelegate {}

DelegateProxyType 프로토콜은 구현해야 하는 6개를 멤버를 갖는데 registerKnownImplementations()를 제외한 나머지는 protocol extension으로 기본 구현을 지원하기 때문에 필수로 구현할 필요가 없다.

public protocol DelegateProxyType: AnyObject {
    associatedtype ParentObject: AnyObject
    associatedtype Delegate
    
    static func registerKnownImplementations()

    static var identifier: UnsafeRawPointer { get }

    static func currentDelegate(for object: ParentObject) -> Delegate?

    static func setCurrentDelegate(_ delegate: Delegate?, 
                                    to object: ParentObject)

    func forwardToDelegate() -> Delegate?

    func setForwardToDelegate(_ forwardToDelegate: Delegate?, 
                                retainDelegate: Bool)
}

이제 클래스에 속성을 추가하고 생성자와 registerKnownImplementations()를 구현하면 된다.

class RxCLLocationManagerDelegateProxy:
    DelegateProxy<CLLocationManager, CLLocationManagerDelegate>,
    DelegateProxyType,
    CLLocationManagerDelegate {
    
    weak private(set) var locationManager: CLLocationManager?
    
    init(locationManager: CLLocationManager) {
        self.locationManager = locationManager
        super.init(parentObject: locationManager,
                   delegateProxy: RxCLLocationManagerDelegateProxy.self)
    }
    
    static func registerKnownImplementations() {
        self.register { RxCLLocationManagerDelegateProxy(locationManager: $0) }
    }
}

locationManager는 순환 참조 때문에 weak로 선언해야 한다.

3. Reactive Extension 구현하기

delegate 속성을 추가하는데 DelegateProxy 타입으로 만들어야 한다. 이제 위에서 만든 프록시 클래스의 인스턴스를 반환해야 하는데 생성자를 통해 인스턴스를 만들면 인스턴스가 2개 이상 생성될 수 있으므로 DelegateProxyType이 제공하는 proxy(for:) 메서드를 사용해야 한다. proxy 팩토리가 proxy 생성을 담당하게 되고 이미 인스턴스가 생성되어 있다면 생성된 인스턴스를 반환해주고, 생성된 인스턴스가 없다면 인스턴스를 생성해준다.

그다음 methodInvoked(_ selector)를 사용해서 옵저버블을 반환해주면 된다. methodInvoked(_ selector)는 셀렉터로 넘긴 델리게이트 메서드가 실행되면 next 이벤트를 방출한다. methodInvoked(_ selector) 결과는 [Any] 형식이므로 적절하게 타입 변환을 해줘야 한다.

extension Reactive where Base: CLLocationManager {
    var delegate: DelegateProxy<CLLocationManager,
                                CLLocationManagerDelegate> {
       return RxCLLocationManagerDelegateProxy.proxy(for: base)
    }
    
    var didUpdateLocations: Observable<[CLLocation]> {
        return delegate
            .methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)))
            .map { params in
                return params[1] as! [CLLocation]
            }
    }
}

사용하기

locationManager에서 rx로 접근하여 확장한 didUpdateLocations를 구독하여 사용하면 된다.

class DelegateProxyViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!
    
    private let disposeBag = DisposeBag()
    private let locationManager = CLLocationManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // CLLocationManager Delegate Proxy
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
        locationManager.rx.didUpdateLocations
            .map { $0[0] }
            .bind(to: mapView.rx.center)
            .disposed(by: disposeBag)
    }
}

// MARK: CLLocationManager Delegate Proxy
extension CLLocationManager: HasDelegate {
    public typealias Delegate = CLLocationManagerDelegate
}

 


MKMapViewDelegate도 DelegateProxy로 구현할 수 있다! 

// MARK: MKMapView Delegate Proxy
extension MKMapView: HasDelegate {
    public typealias Delegate = MKMapViewDelegate
}
class RxMKMapViewDelegateProxy:
    DelegateProxy<MKMapView, MKMapViewDelegate>,
    DelegateProxyType,
    MKMapViewDelegate {
    
    weak private(set) var mapView: MKMapView?
    
    init(mapView: MKMapView) {
        self.mapView = mapView
        super.init(parentObject: mapView,
                   delegateProxy: RxMKMapViewDelegateProxy.self)
    }
    
    static func registerKnownImplementations() {
        self.register { RxMKMapViewDelegateProxy(mapView: $0) }
    }
}

extension Reactive where Base: MKMapView {
    var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> {
        return RxMKMapViewDelegateProxy.proxy(for: base)
    }
    
    var regionDidChangeAnimated: Observable<Bool> {
        return delegate.methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
            .map { param in return param[1] as! Bool }
    }
}

(ReactiveX Rxswift에 다양한 라이브러리가 이미 있으므로  있다면 그걸 사용하자. MKMapView도 있다.)

댓글