자 서버가 개발되고 JSON 이 온다!
이제 서버에 맞게 바꿔야한다.
서버의 데이터를 ViewModel 에 맞게 컨버팅 해주는 작업이 필요하다.
fetching 하는 레거시 코드를 가지고 있다.
class APIService {
static func fetchAllMenus(onComplete: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: MenuUrl)!) { data, res, err in
if let err = err {
onComplete(.failure(err))
return
}
guard let data = data else {
let httpResponse = res as! HTTPURLResponse
onComplete(.failure(NSError(domain: "no data",
code: httpResponse.statusCode,
userInfo: nil)))
return
}
onComplete(.success(data))
}.resume()
}
}
이 기존코드를 rx로 고치지 말고 rx 코드로 레거시 코드를 감쌀 수 있다.
static func fetchAllMenuesRx() -> Observable<Data> {
return Observable.create() {emitter in
fetchAllMenus{ result in
switch result{
case .success(let data):
emitter.onNext(data)
emitter.onCompleted()
break
case .failure(let err):
emitter.onError(err)
break
}
}
return Disposables.create()
}
}
이 코드를 viewModel의 init 부분에서 더미 데이터를 삭제하고 집어넣는다.
그리고 JSON 데이터가 오는 형식인 MenuItem을 viewModel 에 필요한 Menu 라는 모델로 컨버팅 하는 코드를 Menu 에 extension으로 작성한다.
(서버에서 주는 데이터는 name 과 price 이고 viewModel 에는 id, count 도 필요해서 이 작업이 필요하다)
extension Menu{
static func fromMenuItems(id: Int, item : MenuItem) -> Menu {
return Menu(id: 0, name: item.name, price: item.price, count: 0)
}
}
완성된 viewModel 의 init() 부분
init() {
_ = APIService.fetchAllMenuesRx()
.map{ data -> [MenuItem] in
struct Response : Decodable {
let menus : [MenuItem]
}
let response = try! JSONDecoder().decode(Response.self, from: data)
return response.menus
}
.map { menuItems -> [Menu] in
var menus:[Menu] = []
menuItems.enumerated().forEach{ (index, item) in
let menu = Menu.fromMenuItems(id: index, item: item)
menus.append(menu)
}
return menus
}
.take(1)
.bind(to: menuObservable)
}
MVVM
디자인, 서버, 클라이언트 동시에 진행 할 수 있다.
View에 필요한 Model 은 따로 만든 뒤 나중에
실제로 서버에서 데이터가 나오면(Domain Model) 컨버팅해서 view에 필요한 viewModel 을 만든다.
MVC
- Controller 는 UIViewController 가 역할을 하며 사용자 Input action 을 Controller 가 받는다
- 거기에 대한 결과물 출력, 업데이트도 controller 가 시킨다.
- 거기에 대한 데이터는 Model (pure model) 특정 플랫폼(UIKit) 에 종속되지 않아서 Testable 하다
- UIKit에 종속되는 순간 Test하기가 힘들다.
- 컨트롤러의 역할을 좀 제한하자.
MVP
- controller 의 역할중 사용자 입력 받는 부분을 view 쪽으로 넘겨버리자. (viewcontroller)
- controller 의 로직 부분만 presenter에 구현하자.
- view와 presenter 는 1대1 관계이다. (view 가 물어보면 presenter 가 대답한다.)
- view는 멍청하게 테스트가 필요없게 만들어 버린다.(Uikit 있는애들)
- 테스트하기 좋은 애들을 다 presenter 로 넘긴다.
- 그러다 보니 view 와 presenter 가 1대1 관계이다 보니 귀찮다.
- 비슷한 데이터를 처리하는 형태가 여러개가 있더라도 Presenter 를 계속 만들어야한다.
- 그냥 Presenter 를 재사용하면 안되는감?
MVVM
- presenter가 viewModel 로 바뀐것
- viewModel 이 화면에 뭘 그리라고 지시하지 않는다.(화살표)
- view 는 그려야될 요소가 바뀌면 스스로 갱신한다. (데이터 바인딩을 기반으로 view 가 viewModel 을 바라보고 있다)
- 비슷한 형태의 View들이 있고 기반 데이터가 같다면 하나의 ViewModel 로 처리할 수 있다.
- 어떤 뷰는 list, 어떤 뷰는 썸네일일 수 있는데 같은 데이터 기반이면 ViewModel 하나로 처리한다.
Subject
- observable은 데이터가 미리 정해져 있는 형태의 stream 이다.
- create 할 때 부터 정해져 있다.
- 추가적으로 runtime에 외부 control 에 의해서 데이터가 동적으로 생성되는 애가 필요하다.
- 데이터를 넣어줄 수 있고, subsctibe 할 수 있는 양방향성이 필요했다.
- 그놈이 Subject 이다.
RxCocoa
- UIKit의 rx 처리이다.
- UIkit.rx 해서 나오는 코드들이다.
- UI의 특징은 항상 UI 쓰레드에서 돌아야 한다.
- 그래서 observeOn(MainScheduler.instance)는 항상 들어가야한다.
- Outlet 으로 밖에다 Instance 를 만들어 처리하다 보니 Self 를 써서 순환참조가 일어나는 경우가 있다.
- UI는 데이터를 처리하다 에러가 나면 Stream 이 끊어진다. (재활용이 안된다.)
- 화면을 그리다가 에러가 나면 stream 이 끊어져서 더이상 동작하지 않는다.(그렇게 되면 안된다.)
.catchErrorJustReturn("") 으로 에러가 났을때 빈문자열을 그냥 리턴하는 걸로
viewModel.itemsCount
.map{"\($0)"}
.catchErrorJustReturn("")
.observeOn(MainScheduler.instance)
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
그런데 항상 observeOn과 catchErrorJustReturn 이 필요하다. 그래서 UI처리 용도로 Driver 를 쓴다.
viewModel.itemsCount
.map{"\($0)"}
.asDriver(onErrorJustReturn: "")
.drive(itemCountLabel.rx.text)
.disposed(by: disposeBag)
driver 로 바뀌면 error 의 경우 asDriver로 바꿔서 처리하고 driver는 항상 메인 쓰레드에서 알아서 동작한다.
에러가 나도 끊어지지 않는 Subject가 필요해서 Driver를 쓰는 것이다.
에러가 나도 stream 이 끊어지지 않는 Subject
lazy var menuObservable = BehaviorSubject<[Menu]>(value: [])
//이 subject는 UI 에 영향을 끼친다.
RxRelay : Subject랑 똑같은데 에러가 나도 끊어지지 않는다.
lazy var menuObservable = BehaviorRelay<[Menu]>(value: [])
에러가 나도 오로지 받아드리기만 해서 onNext 나 onComplete 을 쓰지 않는다.
accept 를 쓴다.
[기존코드]
func clearAllItemSelections(){
menuObservable
.map{menus in
menus.map { m in
Menu(id:m.id, name: m.name, price: m.price, count: 0)
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
[RxRelay 바꾼코드]
func clearAllItemSelections(){
menuObservable
.map{menus in
menus.map { m in
Menu(id:m.id, name: m.name, price: m.price, count: 0)
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.accept($0)
})
}
정리하자면
UI는 메인쓰레드와 에러나도 Stream 끊어지면 안된다는 이유 때문에
Observable의 UI 용도는 Driver 이고
Subject의 UI 용도는 Relay 이다.
Rx를 처음 배우고 써봤다.
이전에 POS 앱을 개발할 때 이 예시와 비슷하게 메뉴 리스트가 서버로 부터 오고 count, total price 이런 값을 UI 에 갱신해줘야 했다.
그땐 데이터에 따라 업데이트 되는 항목을을 updateUI 메소드에 묶어서 갱신 될 때 마다 호출하였는데 진작에 Rx를 알았더라면 좀더 깔끔하고 직관적으로 구현할 수 있었겠다는 생각을 했다.
또한 Rx는 비동기 통신에만 쓰는 줄로 알았는데 MVVM 의 데이터 바인딩에서 사용할 수 있다는 걸 알게 되었다. 이전에 MVVM 예제를 만들때 데이터 바인딩을 기본적으로 지원해 주지 않아 라이브러리를 써야하거나 completion handler 를 통해 조금 번거롭게 구현해야 했었는데 Rx 하나면 해결 되는 것 같다.
좋은 강의 만들어 주신 Song Chiwon 님께 구독과 좋아요 Follow Star을 남긴다!
'iOS > RxSwift' 카테고리의 다른 글
RxSwift: Observable (0) | 2020.07.09 |
---|---|
RxSwift: Hello RxSwift!🖐 (0) | 2020.07.08 |
RxSwift를 사용한 비동기 프로그래밍 #3 (0) | 2020.05.29 |
RxSwift를 사용한 비동기 프로그래밍 #2 (0) | 2020.05.28 |
RxSwift를 사용한 비동기 프로그래밍 #1 (0) | 2020.05.27 |
댓글