관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Reactive Extension 사용 방법, .rx 네임 스페이스, RxSwift6에서 @dynamicMemberLookup을 이용한 rx 접근 본문

RxSwift/RxSwift 응용

[iOS - swift] Reactive Extension 사용 방법, .rx 네임 스페이스, RxSwift6에서 @dynamicMemberLookup을 이용한 rx 접근

jake-kim 2022. 1. 11. 01:32

Reactive Extension을 사용하기 위해 알아야하는 개념

  • KeyPath, WritableKeyPath, ReferenceWritableKeyPath, DynamicMemberLookup 개념 포스팅 글
  • Observable, Observer, Producer, ControlEvent, Binder 개념 (RxSwift, RxCocoa) 포스팅 글

Reactive란? rx 네임 스페이스의 정체

  • 아래와 같이 RxSwift의 Reactive 파일을 보면, ReactiveCompatible에 rx 연산 프로퍼티가 존재하고 getter부분에는 타입을 리턴
  • ReactiveCompatible을 채택하면 해당 클래스에서는 rx프로퍼티로 접근할 수 있고, rx프로퍼티에서는 base인스턴스를 가지고 있으니, 최종적으로 rx.base로 접근 가능
// Reactive.swift
// RxSwift5에서의 Reactive

public struct Reactive<Base> {
    public let base: Base

    public init(_ base: Base) {
        self.base = base
    }
}

public protocol ReactiveCompatible {
    associatedtype ReactiveBase

    @available(*, deprecated, message: "Use `ReactiveBase` instead.")
    typealias CompatibleType = ReactiveBase

    static var rx: Reactive<ReactiveBase>.Type { get set }

    var rx: Reactive<ReactiveBase> { get set }
}

extension ReactiveCompatible {
    public static var rx: Reactive<Self>.Type {
        get {
            return Reactive<Self>.self
        }
        set {
            // this enables using Reactive to "mutate" base type
        }
    }

    public var rx: Reactive<Self> {
        get {
            return Reactive(self)
        }
        set {
            // this enables using Reactive to "mutate" base object
        }
    }
}

import class Foundation.NSObject

/// Extend NSObject with `rx` proxy.
extension NSObject: ReactiveCompatible { }

ex) rx.base 접근

ReactiveCompatible을 채택하면, (myClass.rx).base 형태로 접근 가능(괄호 안 부분이 Reactive 인스턴스)

class MyClass {
  
}

extension MyClass: ReactiveCompatible {}

let myClass = MyClass()
print(myClass.rx.base) // MyClass
  • RxSwift의 Reacitve파일에서 선언된 확장을 통해 NSObject의 후손인 모든 프로퍼티는 rx에 접근 가능
/// Extend NSObject with `rx` proxy.
extension NSObject: ReactiveCompatible { }

.rx 네임 스페이스의 정체

  • UIButton+Rx 파일
    • (button.rx).tap 활성화
    • Reactive에 tap 인스턴스 확장
    • UIButton의 인스턴스는 NSObejct의 후손이기 때문에 rx 인스턴스를 소유
    • 이때 (button.rx)은 Reactive 인스턴스이고, extension으로 tap 인스턴스를 추가하였으니, button.rx.tap으로 접근
extension Reactive where Base: UIButton {
    
    /// Reactive wrapper for `TouchUpInside` control event.
    public var tap: ControlEvent<Void> {
        controlEvent(.touchUpInside)
    }
}

-> 위에서 나오는 ControlEvent의 의미는 이 포스팅 글 참고

 

ex) URLSession 사용할때 .rx로 접근

- Reactive+Extension 파일 생성

// Reactive+Extension

import RxSwift

extension Reactive where Base: URLSession {
  enum RxURLSessionError: Error {
    case unknown
    case invalidResponse(response: URLResponse)
    case requestFailed(response: URLResponse, data: Data)
  }
  
}

rx.response로 접근할 수 있도록 메소드 추가

func response(url: URL) -> Observable<(HTTPURLResponse, Data)> {
  return Observable.create { observer in
    
    let task = self.base.dataTask(with: url) { data, response, error in
      guard
        let response = response,
        let data = data
      else {
        observer.onError(error ?? RxURLSessionError.unknown)
        return
      }
      
      guard let httpResponse = response as? HTTPURLResponse else {
        observer.onError(RxURLSessionError.invalidResponse(response: response))
        return
      }
      
      observer.onNext((httpResponse, data))
      observer.onCompleted()
    }
    
    task.resume()
    return Disposables.create { task.cancel() }
  }
}
  • 내부적으로 위 response 메소드를 호출하여 data, string, json, image를 가져오는 메소드도 정의
    • 외부에서는 rx.data, rx.string, rx.json, rx.image로 접근
func data(url: URL) -> Observable<Data> {
  return response(url: url).map { response, data -> Data in
    guard 200 ..< 300 ~= response.statusCode else {
      throw RxURLSessionError.requestFailed(response: response, data: data)
    }
    return data
  }
}

func string(url: URL) -> Observable<String> {
  return data(url: url).map { data in
    return String(data: data, encoding: .utf8) ?? ""
  }
}

func json(url: URL) -> Observable<Any> {
  return data(url: url).map { data in
    return try JSONSerialization.jsonObject(with: data)
  }
}

func image(url: URL) -> Observable<UIImage> {
  return data(url: url).map { data in
    return UIImage(data: data) ?? UIImage()
   }
}

- 사용하는 쪽

let urlSesson = URLSession()
urlSesson.rx.image(url: "https://ios-development.tistory.com/")
  .bind(to: self.myImageView)
  .disposed(by: self.disposeBag)

RxSwift6에서 DynamicMemberLookup을 이용한 rx 네임 스페이스 확장

  • RxSwift6부터 extension Reactive 없이 .rx 바로 사용 가능
class MyView: UIView {
  var myProperty: String
  
  init(myProperty: String) {
    self.myProperty = myProperty
    super.init(frame: .zero)
  }
  
  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError()
  }
}

class ViewController: UIViewController {
  
  private let myButton: UIButton = {
    return UIButton()
  }()
  
  private let disposeBag = DisposeBag()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let myView = MyView(myProperty: "123")
    self.myButton.rx.tap
      .map { "tap" }
      .bind(to: myView.rx.myProperty) // <- myView.rx.myProperty 접근 가능
      .disposed(by: self.disposeBag)
  }
}
  • 원래 RxSwift5 이하 버전에서 rx를 사용하려면 아래처럼 추가가 필요
extension Reactive where Base: MyView {
  var myProeprty: Binder<String> {
     Binder(base) { base, newProperty in
         base.myProperty = newProperty
     }
  }
}

RxSwift6에서 .rx를 따로 확장하지 않아도 사용 가능한 원리

  • dynamic member lookup 사용 (dynamic member lookup 개념은 해당 포스팅 참고)

RxSwift에서 정의하고 있는 Reactive 구조체

  • @dynamicMemberLookup 관련 코드
    • KeyPath 타입에서 <Base, Property>이고 Base가 AnyObject 
    • Base 타입이 AnyObject이므로 KeyPath 접근 시 Root가 AnyObject형태
    • 즉 클래스형(AnyObject)인 것들은 모두 (someClassInstance.rx).propertyName으로 접근 가능
/// Automatically synthesized binder for a key path between the reactive
/// base and one of its properties
public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Property>) -> Binder<Property> where Base: AnyObject {
    Binder(self.base) { base, value in
        base[keyPath: keyPath] = value
    }
}

-> 위 subscript 코드 이해

1) dynamic member lookup을 사용처는 2가지인데, (문자열을 프로퍼티처럼 접근, KeyPath를 인수로 받으면 Wrapper로 사용 가능)

- subscript메소드 인자에서 KeyPath 인수를 받고 있으므로 Wrapper로 사용하는 것

- 즉, rx가 Wrapper이기 때문에 rx.{someProperty}로 접근하기 위함

 

2) 기존 Reactive extension Binder코드에서는, 클로저 내부에서 base.myProperty = newProperty를 사용하여 값을 변경했지만,

- base[keyPath: keyPath] = value로 base에 keyPath로 내부 프로퍼티에 접근하여 새로운 값(value)를 대입 

- KeyPath는 Root(base)에서 property까지의 접근을 가능하게하기 때문에 어떤 인스턴스든 .rx로 접근 가능하도록 구현된것

// bind 예시 코드

extension Reactive where Base: MyView {
  var myProperty: Binder<String> { // <- Binder 사용
     Binder(base) { base, newProperty in
         base.myProperty = newProperty
     }
  }
}

let myView = MyView()
myView.button.rx.tap
  .bind(to: myView.rx.myProperty)
  .disposed(by: self.disposeBag)

 

* 참고

- RxSwift6 소스 코드

- https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/17-creating-custom-reactive-extensions

Comments