티스토리 뷰

iOS

RxSwift - Reactive Extensions (.rx)

Peppo 2023. 3. 11. 21:09
728x90

현업에선 RxSwift를 사용하고 있다.

그중 가장 많이 보이는건 .rx 이 친구.. 도대체 뭘까?

Button.rx

TextField.rx 이런건 봤는데 

 

따로 커스텀해서 만들어져있는걸 보고 공부해야겠다 안되겠다..하고 미루고 미루던게 드디어 오늘이다.

한번 파보자 !!

 


.rx 

Observable 타입을 표현하는 확장자로, 비동기적으로 데이터를 생성하고나 다른 이벤트를 전달 하는 시퀀스 인데 기본형태로는 아래와 같이 쓰입니다.

extension Reactive where Base: SomeType { }

 

가장 끝 SomeType 쪽에 (UIButton, UITextField, URLSession 등) 감지하고 싶은 특정 reactive extension을 구현합니다.

 

예시)

self.someButton
    .rx
    .tap
    .bind {
      print("tapped!")
    }
    .disposed(by: self.disposeBag)

위 코드는 someButton의 이벤트를 감지하고 있다가 눌리게 되면 "tapped" 를 프린트하는 로직 입니다.

 

아래는 Reactive의 내부 코드를 가져와 봤어요.

👇👇👇 Reactive.Swift 코드 👇👇👇

더보기
public struct Reactive<Base> {
    public let base: Base

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

    public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Property>) -> Binder<Property> where Base: AnyObject {
        Binder(self.base) { base, value in
            base[keyPath: keyPath] = value
        }
    }
}

public protocol ReactiveCompatible {
    associatedtype 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 { Reactive<Self>.self }
        set { }
    }

    public var rx: Reactive<Self> {
        get { Reactive(self) }
        set { }
    }
}

import Foundation

extension NSObject: ReactiveCompatible { }

 

여기서 주목할건 ReactiveCompatible 이라는 프로토콜 입니다.

이 프로토콜을 채택한 class에서는 .rx라는 프로퍼티를 쓸 수 있게 됩니다.

 

아래는 Reactive Extension을 사용해 만든 URLSession 입니다. 

 

URLSession 예시 )

1. API 호출

2. Data타입의 응답값 받음

3. Data타입 디코딩

// URLSessionEx

import Foundation
import RxSwift

public enum RxURLSessionError: Error {
  case unknown
  case invalidResponse(response: URLResponse)
  case requestFailed(response: HTTPURLResponse, data: Data?)
  case deserializationFailed
}

extension Reactive where Base: URLSession {
    // response
    func response(request: URLRequest) -> Observable<(HTTPURLResponse, Data)> {
        return Observable.create { observer in
            let task = self.base.dataTask(with: request) { 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() }
        }
    }
    
    // data
    func data(request: URLRequest) -> Observable<Data> {
        if let url = request.url?.absoluteString,
           let data = internalCache[url] {
            return Observable.just(data)
        }
        
        return response(request: request).cache().map { response, data -> Data in
            guard 200..<300 ~= response.statusCode else {
                throw RxURLSessionError.requestFailed(response: response, data: data)
            }
            
            return data
        }
    }
    
    // decodable
    func decodable<D: Decodable>(request: URLRequest,
                                 type: D.Type) -> Observable<D> {
        return data(request: request).map { data in
            let decoder = JSONDecoder()
            return try decoder.decode(type, from: data)
        }
    }
}

 

Usage

 

let urlSession = URLSession.shared 
let url = URL(string: "https://peppo.tistory.com/")!
var request = URLRequest(url: url)

urlSession
  .rx
  .decodable(request: request, type: SomeResponse.self)
  .disposed(by: disposeBag)

 

 

TestFiled 예시 )

 

1. textField에 text가 입력되면 해당 이벤트를 받아 print 해줌.

textField.rx.text
  .subscribe(onNext: { text in
    print(text ?? "")
  })
  .disposed(by: disposeBag)

 

 

 

여러가지 예시를 들면서 공부해보니까 조금은 트이는것 같다.

특히 네트워크 통신같은 경우는 현업에서도 구현되어 있어서 로직파악이 어려웠는데 

이번에 공부로 많은 도움이 된거 같다. 


참고

 

 

  1. https://www.kodeco.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/17-creating-custom-reactive-extensions 
  2. https://muizidn.medium.com/custom-rxswift-extension-made-easy-4553d050fa7c
  3. https://ios-development.tistory.com/838
  4. https://zeddios.tistory.com/1250
728x90