Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- ribs
- 리펙터링
- Human interface guide
- swift documentation
- 애니메이션
- map
- Observable
- UITextView
- clean architecture
- 리팩토링
- Clean Code
- MVVM
- UICollectionView
- combine
- RxCocoa
- swiftUI
- uitableview
- ios
- Refactoring
- collectionview
- rxswift
- uiscrollview
- 스위프트
- 리펙토링
- HIG
- Protocol
- SWIFT
- Xcode
- tableView
- 클린 코드
Archives
- Today
- Total
김종권의 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:32Reactive 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 개념은 해당 포스팅 참고)
- @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 소스 코드
'RxSwift > RxSwift 응용' 카테고리의 다른 글
[iOS - swift] 1. RxSwift의 Map, FlatMap 사용하여 비동기를 순서대로 처리 방법 (0) | 2022.01.31 |
---|---|
[iOS - swift] RxSwift, dataSource 처리 방법 (단일 Section, tableView, collectionView) (0) | 2022.01.13 |
[RxSwift] 8. Observable의 구조 (ObservableConvertibleType, ObservableType, Observable, Event) (0) | 2020.09.27 |
[RxSwift] 7. share (0) | 2020.09.27 |
[RxSwift] 6. merge, combineLatest, withLatestFrom, zip, concat, (0) | 2020.09.27 |
Comments