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
- 이터레이터패턴
- 데코레이터패턴
- Scenedelegate
- ViewController
- 커맨드패턴
- 스트래터지패턴
- 상태패턴
- 템플릿메서드
- unowned
- 스테이트패턴
- 프록시패턴
- 옵저버패턴
- WKWebView
- SWIFT
- Xcode
- 팩토리메서드패턴
- 컴파운드패턴
- 파사드패턴
- RxSwift
- Mobile
- 디자인패턴
- DispatchQueue
- 컴포지트패턴
- 어댑터패턴
- Lifecycle
- 추상팩토리패턴
- cocoapods
- ios
- 전략패턴
- 싱글턴패턴
Archives
- Today
- Total
ios dev kangwook.
9. Beginning RxCocoa 본문
- Rx는 다중 플랫폼 프레임워크
- RxSwift는 RxPython, RxRuby, RxJS 및 기타 모든 플랫폼이 준수하는 일반 API 디자인을 밀접하게 따르므로 iOS 또는 macOS 용 개발을 지원하는 UIKit 또는 Cocoa 와의 특정 기능이나 통합이 포함되어 있지 않다.
- RxCocoa는 독립형 라이브러리로, 미리 빌드된 많은 기능을 사용하여 UIKit 및 Cocoa와 더 잘 통합할 수 있다.
- RxCocoa는 반응형 네트워킹을 수행하고, 사용자 상호 작용에 반응하고, 데이터 모델을 UI 컨트롤에 바인딩하는 등의 작업을 수행할 수 있는 기본 클래스를 제공
Getting Started
- Wundercast라는 iOS 애플리케이션을 예로 프로젝트 시작 → OpenWeatherMap(http://openweathermap.org) 에서 제공하는 현재 날씨 정보를 사용하는 날씨 애플리케이션
- 이 앱에선 UILabel과 UITextField를 자주 사용
- UITextField+Rx.swift
- 50줄 미만의 코드로 매우 짧고 유일한 속성은 ControlProperty<String?> 으로 명명된 text
- 이 타입은 구독할 수 있고 새 값을 삽입할 수 있는 특수 Subject와 유사한 타입
- UILabel+Rx.swift
- text 와 attributetext 라는 속성이 있는데 둘 다 Binder 라는 새로운 타입이 사용
- 이 ObserverType을 준수하는 객체는 새 값을 수락할 수 있지만 구독할 수 없는 쓰기 전용 엔티티를 원하는 경우 전용의 특수 타입
- Binder는 UI를 기본 로직과 바인딩 하는데 자주 사용
- 또 중요한 사실은 오류를 받아들일 수 없다는 점
- Binder에 오류를 보내면 디버그 모드에서 fatalError() 가 발생하며, 프로덕션 모드에서 앱을 실행할 때 런타임 오류가 로그에 출력된다.
- UITextField+Rx.swift
Using RxCocoa with basic UIKit controls
Displaying the data using RxCocoa
- 현재 ViewController.swift는 이 프로젝트에 존재하는 단 하나의 ViewController
- 이 프로젝트의 목표는 ViewController.swift에 데이터를 제공할 ApiController.swift를 연결하는 것
- Observable은 모든 구독자에게 일부 데이터가 도착했음을 알리고 처리할 값을 푸시할 수 있는 엔티티
- 이러한 이유로 ViewController에서 작업하는 동안 Observable을 구독할 올바른 위치는 viewDidLoad 내부 → 가능한 빨리 구독해야하지만 View가 로드된 후에만 구독해야하기 때문
- 나중에 구독하면 누락된 이벤트가 발생하거나 데이터를 바인딩하기 전에 UI의 일부가 표시될 수 있다.
- 따라서 응용 프로그램이 처리하고 사용자에게 표시해야하는 데이터를 생성하거나 요청하기 전에 모든 구독을 생성해야 한다.
private let disposeBag = DisposeBag()
override func viewDidLoad() {
// ...
APIController.shared.currentWeather(city: "RxSwift")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
self.tempLabel.text = "\(data.temperature)° C"
self.iconLabel.text = data.icon
self.humidityLabel.text = "\(data.humidity)%"
self.cityNameLabel.text = data.cityName
})
.disposed(by: disposeBag)
}
- RxCocoa는 Cocoa에 많은 것을 추가하므로 이 기능을 사용하여 궁극적인 목표를 달성할 수 있다.
- 프레임워크는 프로토콜 확장 기능을 사용하고 많은 UIKit 구성 요소에 rx 네임 스페이스를 추가
- TextField를 보면 UITextField+Rx.swift에서 확인한 text가 있다.
- ObserverType을 모두 준수하는 ControlProperty<String?> 인 Observable을 반환하므로 구독하고 여기에 새 값을 추가하여 TextField를 설정할 수 있다.
override func viewDidLoad() {
// ...
searchCityName.rx.text.orEmpty
.filter { !$0.isEmpty }
.flatMap { text in
return ApiController.shared.currentWeather(city: text)
.catchErrorJustReturn(ApiController.Weather.empty)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
self.tempLabel.text = "\(data.temperature)° C"
self.iconLabel.text = data.icon
self.humidityLabel.text = "\(data.humidity)%"
self.cityNameLabel.text = data.cityName
})
.disposed(by: disposeBag)
}
Retrieving data from the Open Weather API
- API는 구조화 된 JSON 응답을 반환하며 유용한 정보를 가지고 있다.
- ApiController.swift 내부에는 String을 가져와서 애플리케이션의 현재 날씨를 시각적으로 나타내는 날씨 아이콘의 UTF-8 코드인 또 다른 String을 반환하는 iconNameToChar라는 메소드가 존재
- 같은 파일에는 네트워크 요청을 생성하는 편리한 메소드인 buildRequest가 존재
- 이것은 URLSession에 RxCocoa의 래퍼를 사용하여 네트워크 요청을 수행
- GET(또는 POST) 요청을 올바르게 빌드하기 위해 기본 URL을 가져오고 구성 요소를 추가
- API 키를 사용
- 요청의 콘텐츠 유형을 application / json 으로 설정
- metric 단위로 요청
- 나중에 currentWeather 메소드에서 JSONDecoder를 사용하여 디코딩 될 Observable 데이터를 반환
- 마지막 부분은 단일 리턴 라인으로 축소
// ApiController - buildRequest
// ...
return session.rx.data(request: request)
- 이것은 데이터 메소드를 사용하는 URLSession 주위에 RxCocoa의 rx 확장을 사용
- 그러면 Observable<Data>가 반환
- 나중에 JSONDecoder를 사용하여 이 데이터를 디코딩
// ApiController currentWeather
func currentWeather(city: String) -> Observable<Weather> {
return buildRequest(pathComponent: "weather", params: [("q", city)]
.map { data in
let decoder = JSONDecoder()
return try decoder.decode(Weather.self, from: data)
}
}
- 이 요청은 Decodable 프로토콜을 준수하기 때문에 Weather 구조체로 디코딩 할 수 있는 Data 객체를 반환
- 일반적으로 Rx로 작업할 때는 항상 시각화를 사용하는 것이 좋으며, 좀 더 자세한 내용이 포함된 업데이트 된 다이어그램은 ApiController 내부에서 무엇이 작동하는지 이해하는데 도움이 된다.
Binding Observables
- RxCocoa는 프레임워크에 포함된 몇 가지 유형에만 의존하는 다소 간단한 솔루션을 제공
- 중요한 점은 RxCocoa에서 바인딩이 단방향 데이터 스트림이라는 점
What are binding observables?
- 바인딩을 이해하는 가장 쉬운 방법은 관계를 두 엔티티 간의 연결로 생각하는 것
- value를 생산하는 생산자
- 생산자의 값을 처리하는 소비자
- 소비자는 값을 반환할 수 없다. → RxSwift의 바인딩을 사용할 때 일반적인 규칙
- 바인딩의 기본 방법은 bind(to: ) 이며 Observable을 다른 엔티티에 바인딩하는데 사용
- 소비자는 ObserverType을 준수해야 한다.
- ObserverType 인 RxSwift와 함께 번들로 제공한 유일한 타입은 실제로 Subject이며, 이는 ObserverType과 ObservableType을 모두 준수하기 때문에 이벤트를 기록할 뿐만 아니라 구독할 수도 있다.
- bine(to: ) 는 사용자 인터페이스를 기본 데이터에 바인딩하는 것 뿐만 아니라 다른 용도로도 사용할 수 있다.
- 예를 들어, bind(to: ) 를 사용하여 종속 프로세스를 만들 수 있으므로 특정 Observable은 화면에 아무것도 표시하지 않고 일부 백그라운드 작업을 수행하는 Subject를 트리거 한다.
Note
ObserverType을 준수하는 객체 외에도 Relay에서 bind(to: )를 사용할 수 있다.이러한 bind(to: ) 메소드는 Relay가 실제로 ObserverType을 따르지 않기 때문에 별도의 오버로드
- 마지막으로 bind(to: )가 실제로 subscribe()의 별칭이라는 점
- bind(to: )를 호출하면 내부적으로 subscribe(observer)를 호출
Using binding observables to display data
- 첫 번째 변경 사항은 subscribe(onNext: )를 사용하여 데이터를 올바른 UILabel에 할당하는 Observable을 리팩토링
override func viewDidLoad() {
// ...
let search = searchCityName.rx.text.orEmpty
.filter { !$0.isEmpty }
.flatMapLatest { text in
return ApiController.shared.currentWeather(city: text)
.catchErrorJustReturn(ApiController.Weather.empty)
}
.shared(replay: 1)
.observeOn(MainScheduler.instance)
}
- flatMapLatest는 검색 결과를 재사용 가능하게 만들고 일회성 데이터 소스를 multi-use Observable로 변환
- 이 작은 변경으로 다른 구독의 모든 단일 매개 변수를 처리하여 표시하는데 필요한 값을 매핑할 수 있다.
- 예를 들어, Observable 공유 데이터 소스에서 온도를 문자열로 가져오는 방법
search.map { "\($0.temperature)° C" }
.bind(to: tempLabel.rx.text)
.disposed(by: disposeBag)
// 이 후 다른 데이터도 각 Label에 바인딩
search.map { $0.icon }
.bind(to: iconLabel.rx.text)
.disposed(by: disposeBag)
search.map { "\($0.humidity)%" }
.bind(to: humidityLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.cityName }
.bind(to: cityNameLabel.rx.text)
.disposed(by: disposeBag)
Improving the code with Traits
- RxCocoa는 Cocoa 및 UIKit을 쉽게 사용할 수 있도록 더욱 고급 기능을 제공
- bind(to: ) 외에도 Observable의 특수 구현을 제공하며, 그 중 일부는 UI: Traits 와 함께 사용하기 위해 생성
- Trait은 클래스 그룹으로, 특히 UI로 작업할 때 간단하고 쓰기 쉬운 코드를 만드는데 도움이 되는 특수 Observable 항목
What are ControlProperty and Driver?
- Trait은 공식 문서에서 다음과 같이 설명
- 인터페이스 경계를 넘어 통신하고 Observable 시퀀스 속성을 보장
- 처음에는 혼란스러울 수 있지만 RxCocoa의 특성을 사용하는 규칙은 전체 개념을 조금 더 이해하기 쉽게 만든다. 규칙은 다음과 같다.
- 에러를 내보낼 수 없다.
- MainScheduler에서 관찰된다.
- MainScheduler에서 subscribe
- Signal을 제외하고 리소스를 공유
- 이러한 엔티티는 무언가가 항상 사용자 인터페이스에 표시되고 항상 사용자 인터페이스에서 처리할 수 있도록 한다.
- RxCocoa의 Traits
- ControlProperty and ControlEvent
- ControlProperty → 기존에 사용하던 rx 네임 스페이스를 사용하여 데이터를 올바른 사용자 인터페이스 구성요소에 바인딩 할 때 사용
- ControlEvent → TextField를 편집하는 동안 키보드의 "Return" 버튼을 누르는 것과 같은 UI 구성요소의 특정 이벤트를 수신하는데 사용
- Driver → 동일한 제약 조건을 가진 특수 관찰이 가능하므로 오류가 발생하지 않는다. 모든 프로세스가 메인 스레드에서 실행되도록 보장하여 백그라운드 스레드에서 UI 변경을 방지
- Signal → 리소스를 공유하지 않는다는 점을 제외하고 Driver와 똑같은 속성을 공유하므로 구독자에게 아이템을 재생하지 않는다.
- ControlProperty and ControlEvent
- 일반적으로 Trait은 프레임워크의 선택적 부분 → 강제로 사용하지 않아도 된다.
- 적절한 스케줄러에서 올바른 작업을 만들고 있는지 확인하면서 Observable 항목과 Subject를 자유롭게 사용해도 된다.
- 그러나 컴파일을 하는 동안 몇 가지 검사와 UI를 처리할 때 예측 가능한 규칙이 필요한 경우 이러한 구성요소는 매우 강력하고 시간을 절약할 수 있다.
Improving the project with Driver and ControlProperty
- 이러한 멋진 개념을 응용 프로그램에 적용하고 모든 작업이 올바른 스레드에서 수행되는지 확인하고 오류가 발생하지 않고 구독이 결과를 제공하지 못하도록 해야한다.
- 첫 번째 단계는 Observable 날씨 데이터를 Driver로 변환하는 것
let search = searchCityName.rx.text.orEmpty
.filter { !$0.isEmpty }
.flatMapLatest { text in
return ApiController.shared.currentWeather(city: text)
.catchErrorJustReturn(ApiController.Weather.empty)
}
.asDriver(onErrorJustReturn: ApiController.Weather.empty) // Observable을 Driver로 변환하는 방법.
// onErrorJustReturn 매개 변수는 Observable 오류가 발생하는 경우 사용할 기본값을 지정하여 Driver 자체에서 오류를 발생시킬 가능성을 제거
}
- asDriver(onErrorDriverWith: ) → 이 메소드를 사용하면 오류를 수동으로 처리할 수 있다. 이 용도로만 생성된 새 Driver를 반환
- asDriver(onErrorRecover: ) → 다른 기존 Driver와 함께 사용할 수 있다. 방금 오류가 발생한 현재 Driver를 복구하기 위해 작동
// UI update with drive
search.map { "\($0.temperature)° C" }
.drive(tempLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.icon }
.drive(iconLabel.rx.text)
.disposed(by: disposeBag)
search.map { "\($0.humidity)%" }
.drive(humidityLabel.rx.text)
.disposed(by: disposeBag)
search.map { $0.cityName }
.drive(cityNameLabel.rx.text)
.disposed(by: disposeBag)
- 이렇게 하면 Driver의 성능을 활용하면서 애플리케이션의 올바른 UI 동작을 복원
- drive() 는 bind(to: ) 보다 RxCocoa의 Trait을 사용하는 동안 의도를 더 잘 표현
- RxCocoa의 많은 부분을 활용했지만 여전히 개선할 수 있는 부분이 있다.
- 응용 프로그램은 문자를 입력할 때마다 요청을 실행하기 때문에 너무 많은 리소스를 사용하고 너무 많은 API 요청을 만든다.
// replace search
// 기존
let search = searchCityName.rx.text.orEmpty
// 변경
let search = searchCityName.rx.controlEvent(.editingDidEndOnExit)
.map { self.searchCityName.text ?? "" }
- 이렇게 변경하면 사용자가 "검색" 버튼을 누를 때만 날씨를 검색
- 불필요한 네트워크 요청을 하지 않고 코드는 Traits에 의해 컴파일 타임에 제어된다.
- 또한 currentWeather(city: )에서 반환된 Observable에 대한 catchErrorJustReturn() 호출을 제거
- 원래 스키마는 전체 UI를 업데이트 한 단일 Observable 항목을 사용
- 여러 블록의 분석을 통해 subscribe에서 bindTo로 전환하고 ViewController 전체에서 동일한 Observable을 재사용
- 이 접근 방식은 코드를 재사용하므로 매우 작업하기 쉽게 만든다.
- 예를 들어, 현재 기압을 사용자 인터페이스에 추가할 경우 속성을 구조체에 추가한 후 다른 UILabel을 추가하고 해당 속성을 매핑하기만 하면 된다.
Disposing with RxCocoa
- 기본 ViewController가 할당 해제될 때 모든 구독을 처리하는 DisposeBag이 존재.
- 그러나 이 예에서 모든 클로저에 weak 또는 unowned 가 사용되지 않음.
- 그 이유는 이 예의 경우 단일 ViewController이며 MainViewController는 애플리케이션이 실행되는 동안 항상 화면에 표시되므로 메모리를 방지할 필요가 없다.
Unowned VS weak with RxCocoa
- weak 과 unowned 를 사용하는 규칙은 일반 Swift 클로저를 사용할 때 따르는 것과 동일하며 subscribe(onNext: ) 와 같은 Rx의 클로저 변형을 호출할 때 주로 관련
- 클로저가 @escaping 클로저인 경우 항상 weak 또는 unowned 클로저를 사용하는 것이 좋다.
- 그렇지 않으면 retain cycle 이 생기고 구독이 해제되지 않는다.
- weak을 사용하면 self에 대한 optional 참조가 제공되고 unowned를 사용하면 self에 대한 non-optional 참조가 제공된다.
- unowned 는 self!를 제공하므로 주의해야 한다.
Summary of RxSwift and RxCocoa
'RxSwift' 카테고리의 다른 글
8. Time-Based Operators (0) | 2022.12.27 |
---|---|
7. Combining Operators (1) | 2022.12.23 |
6. Transforming Operators (0) | 2022.12.22 |
5. Filtering Operators (0) | 2022.12.19 |
4. Observables & Subjects in Practice (1) | 2022.12.13 |
Comments