Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intermediate RxCocoa #28

Open
simoniful opened this issue May 20, 2022 · 0 comments
Open

Intermediate RxCocoa #28

simoniful opened this issue May 20, 2022 · 0 comments
Labels

Comments

@simoniful
Copy link
Owner

Ch.13 Intermediate RxCocoa

A. 검색할 동안 activity indicator 표시하기

Begining 정리 때 배웠던 Indicator를 활용한 로딩 뷰 구성

검색 버튼을 누르면 running이란 상태를 기반으로 해당 인디케이터 뷰를 바인딩하여 보여주고, 데이터 패칭이 완료(running == false) 되면서 바인딩한 UI에 데이터를 뿌려주는 작업 수행,. 이 때, running은 모든 검색 활동에 기반하여 검색이 이루어 질 때(Geo, CLLocation, Text)의 상태를 merge 하여 구성했다.

그리고 running에 초기 .startWith(true) 값을 부여한다. 앱이 시작할 때 모든 결과에 대한 UI label을 수동적으로 숨길 필요가 없도록 한다. 따라서, 첫 번째 값은 수동적으로 추출된다는 것을 기반으로 인디케이터를 바인딩하는 부분에선 첫 번째 값은 반드시 skip하여 동작을 방지하고, label은 숨겨지도록 초기 상태를 구성했다.

B. CLLocationManager 확장을 통한 현재 위치 확인하기

RxCocoa는 UI만을 위한 것이 아니다. 기본 목적은 Apple의 공식 프레임워크들을 래핑하여 간단하고 강력한 방법으로 사용자화 하는 것이다. 또한, 현재 튜토리얼에서는 위치와 관련된 UIKit을 활용하므로 항상 사용자의 위치에 대한 인증 같은 선순위로 수행해야하는 부분을 놓쳐서는 안된다.

extension CLLocationManager: HasDelegate {}

// RxCLLocationManagerDelegateProxy는 Observable이 생성되고 구독이 있는 직후 CLLocationManager 인스턴스에 연결하는 프록시가 된다.
// 이것은 RxCocoa가 제공하는 HasDelegate 프로토콜에 의해 단순화.
// 예상대로 래핑을 통하여 CLLocationManagerDelegate 그 자체 역할을 대신한다.

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

  // 이 두 가지 메서드를 사용하면 대리자를 초기화하고 CLLocationManager 인스턴스에서 연결된 관측값으로 데이터를 구동하는 데 사용되는 프록시인 모든 구현을 등록가능
  // RxCocoa의 delegate proxy 패턴을 사용하도록 클래스를 확장

  public init(locationManager: CLLocationManager) {
    super.init(parentObject: locationManager, delegateProxy: RxCLLocationManagerDelegateProxy.self)
  }

  static func registerKnownImplementations() {
    self.register { RxCLLocationManagerDelegateProxy(locationManager: $0) }
  }
}

DelegateProxy라는 RxCocoa에서 제공해주는 프록시 구조 객체를 사용하여, 데이터 제공을 위한 주요 리소스로 delegate(데이터 소스)를 사용하는 모든 프레임워크들과 RxSwift를 연결해주는 솔루션을 구현가능하다 . DelegateProxy 객체는 수신된 모든 데이터를 전용 observable로 표시할 가짜 delegate 객체를 만들어낸다.

// didUpdateLocations을 사용하면 연결된 proxy를 통하여 delegate에서   didUpdateLocations 메서드의 모든 호출을 수신하고 데이터를 가져와서 CLLocation.methodInvoked(_:)의 array로 캐스팅 한다.
// methodInvoked(_:)는 지정된 메서드가 호출될 때마다 다음 이벤트를 보내는 Observable을 return한다. 각 방출된 요소는 메서드가 호출된 매개 변수의 배열이다. 매개 변수[1]로 두 번째 매개 변수인 didUpdateLocations에 액세스하고 CLLocation 배열로 캐스팅한다.

public 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 { parameters in
        return parameters[1] as! [CLLocation]
    }
  }
}

해당 패턴이 구성되면 Reactive를 extension하여 프록시를 연결시켜준다. 연결하게 되면 rx키워드를 통해서 인스턴스 내부의 메서드를 확인이 가능하다. 하지만, 아직 진짜 observable은 진짜 데이터를 받고 있지 않기에 추가적인 수정이 필요하다. 메서드에 대해서 Observable을 받을 수 있도록 methodInvoked(_:)를 활용하여 메서드를 래핑한다. 래핑된 메서드의 파라미터에 대해서 원하는 값을 받도록 구성하면 연결이 완료된다.

C. UIKit mapView 확장하는 법

기존의 UIKit 구성요소들을 커스텀 래핑하는 것도 위와 동일하다. 하지만, 복잡한 부분은 반환 값이 있는 delegate를 Rx로 래핑하는 것에서 조금 더 신경써야하는 부분이 있다. 반환 값이 있는 delegate method는 관찰을 위한 것이 아니라 동작을 사용자화 하기 위한 거라 자동적으로 기본값을 지정하는 것은 지양해야한다. 이를 원활하게 구현하기 위해서 delegate라는 기본 구현을 유지하고 프록시 구성에 있어서 분리하여 호출을 전달한다고 생각해보자

extension MKMapView: HasDelegate {}

class RxMKMapViewDelegateProxy: DelegateProxy<MKMapView, MKMapViewDelegate>, DelegateProxyType, MKMapViewDelegate {
    weak public private(set) var mapView: MKMapView?

    public init(mapView: ParentObject) {
        self.mapView = mapView
        super.init(parentObject: mapView,
                   delegateProxy: RxMKMapViewDelegateProxy.self)
    }

    static func registerKnownImplementations() {
        register { RxMKMapViewDelegateProxy(mapView: $0) }
    }
}

public extension Reactive where Base: MKMapView {
    var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> { RxMKMapViewDelegateProxy.proxy(for: base)
    }

    // 기존 딜리게이트와 같이 사용할 수 있도록 구성해준다.
    func setDelegate(_ delegate: MKMapViewDelegate) -> Disposable {
        RxMKMapViewDelegateProxy.installForwardDelegate(
            delegate,
            retainDelegate: false,
            onProxyForObject: self.base
        )
    }

    // MKOverlay의 모든 인스턴스를 받을 것이고 또한 이들은 현재 지도에 나타내준다
    // Binder의 사용은 bind 또는 drive 함수를 사용할 수 있게 해준다.
    // overlays binding observable 내서는 이전 Overlay들은 매번 Subject에 새 array가 보내질 때마다 사라지고 재생성
    var overlay: Binder<MKOverlay> {
        Binder(base) { mapView, overlay in
            mapView.removeOverlays(mapView.overlays)
            mapView.addOverlay(overlay)
            mapView.setRegion(MKCoordinateRegion(center: overlay.coordinate, span: MKCoordinateSpan(latitudeDelta: 3, longitudeDelta: 3)), animated: true)
        }
    }

    // return이 없는 일반적인 메서드의 관찰은 이전과 동일하다
    var regionDidChangeAnimated: ControlEvent<Bool> {
        let source = delegate
            .methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
            .map { parameters in
                return (parameters[1] as? Bool) ?? false
            }
        return ControlEvent(events: source)
    }
}

DelegateProxyType에는 .installForwardDelegate()라는 메서드가 있다. 프록시 자체에 래핑되지 않은 delegate를 전달할 수 있도록 제공하는 메서드로 이를 통해서 원하는 delegate를 전달하여 프로토콜을 VC에서 설정하는 것이 가능합니다. 이 후에는 VC에서 기존에 delegate 프로코톨을 준수하는 것과 마찬가지로 extension하여 사용이 가능하다.

내부 VC의 구성에서 리턴 값이 있는 경우 해당 객체에 대해서 어떻게 다룰건지 고민하고 또하 이를 활용하는 Binder로 이를 엮어서 원하는 작업을 수행할 수 있다. 튜토리얼을 진행하면서 새로이 notation을 그리는 것과 함께 맵을 이동하도록 구성하는 등 실제로 rx 스럽게 데이터 패칭과 이 후 행동에 관하여 핸들링에 사용하였다

mapView에서 제스쳐를 통한 이동이 발생할 경우의 이벤트를 감지하여, 중앙의 지역 정보를 기반으로 다시금 데이터 패칭을 하는 하도록 구현하였다. 위에서 구현하였던 것과 비슷하면서도 또 뷰의 특성을 조금 더 이해한다면 더 구체적인 활용도 가능할거라 생각한다.

D. Signal

  • RxSwift 4.0에서는 Signal이라는 새로운 trait을 소개했다. 문서에선 다음과 같은 특성을 소개하고 있다.
    • It can't fail 실패할 수 없다.
    • Events are sharing only when connected 이벤트는 연결되었을 때만 공유된다.
    • All events are delivered in the main scheduler 모든 이벤트는 메인 스케줄러로 보내진다.
  • 이렇게 보면 Driver의 대체라고 볼 수도 있다. 하지만 하나 중요한 내용이 있다. 바로 구독한 뒤 마지막 이벤트에 대해서는 replay 하지 않는다는 것이다.
  • Driver와 Signal의 차이점은 BehaviorSubject와 PublishSubject의 차이와 비슷하다.
  • 상황에 따라 어떤 것을 사용해야할지 판단해야할 때는 스스로 "리소스에 연결했을 때, 마지막 이벤트에 대한 replay가 필요한가?"를 생각해보자. 만약 필요없다면 Signal이 좋은 옵션이 될 수 있다. 필요하다면 Driver가 해결책이다.
@simoniful simoniful changed the title RxCocoa Intermediate Intermediate RxCocoa May 20, 2022
@simoniful simoniful added the Rx label May 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant