1. escaping closure
swift에서 closure는 일급 객체라서 범위 내에서 캡쳐되는데 만약에 다른 쓰레드에서 closure를 실행하는 코드라면?
안됨... 왜냐면 closure는 downloadJson 함수 안에서만 실행될 수 있는데 이건 비동기로 다른 스레드에서 처리되니까 메서드가 끝나고 난 뒤에 실행되기 때문..
func downloadJson(_ url: String, completion: @escaping (String?) -> Void) {
DispatchQueue.global().async {
let url = URL(string: url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
completion(json)
}
}
}
그래서 escaping을 붙여서 외부에서도 실행되도록 해야함
근데? closure에 optional이 있으면 자동으로 escaping이 되기 때문에 굳이 키워드 안붙여도 됨~
목표: 비동기로 처리되는 결과값을 return 할 수 없을까?
솔루션: RxSwift
나중에 생기는 데이터 -> Observable
근데 나중에 생기는 데이터 타입이 string이면? Observable<string>
근데 나중에 생기는 데이터 타입이 int이면? Observable<Int>
return 할때는 Observable을 만들어서 return하게 됨 -> 즉 나중에 생기는 데이터를 만들겠다는 거임
Observable.create() 를 하면 됨
Observable.create() 해서 나온 observable은 disposable을 return해야함
disposables.create() 해서 하나 새로 만들 수도 있음
return Observable.create { emitter in
emitter.onNext("hello")
emitter.onNext("world!")
emitter.onCompleted()
return Disposables.create()
}
나중에 데이터가 오면(return되면) -> subscribe
subscribe 하면 return 값이 뭐가 올까?: event가 옴
event: onNext, onCompleted, onError
onNext: 이 값으로 그 다음에 뭐할건데? (여러번 부를 수 있음)
onCompleted: 종료 됨
onError: 에러 뜸
subscribe를 실행하고 난 return 값: disposable (아까 만든거)
Observable의 생명 주기
create -> subscribe -> onNext(onError) -> onCompleted -> dispose
그래서 onCompleted/onError/dispose 된다면 다시 사용할 수 없음
즉 한번 끝난 observable은 재사용 불가
Disposable: 버릴 수 있음
Disposable.dispose() 를 하게 되면 작업이 끝나지 않았어도 취소할 수 있음
(예를 들어 작업이 끝나지 않았음에도 view controller를 deinit 할때 등등)
근데 여러개의 Disposable을 dispose 해야한다면? 이때는 DisposableBag에 넣어서 한번에 모든 걸 dispose 하면 됨
그래서 .disposed(by: disposableBag) 을 쓰는것임
disposableBag을 초기화 할려면 그냥 간단하게
bag = DisposableBag
escaping closure라면 weak self 선언 안하는 경우 순환 참조가 생기지 않을까?
-> 순환 참조가 생기는 이유: 클로저를 캡쳐하면서 클로저 안에 있는 self의 count가 증가했기 때문인데 그러면 클로저가 사라진다면? 순환 참조도 없앨 수 있음
아니면 dispose로 취소하던지
클로저가 사라지는 시점: onCompleted, onError
그래서 onNext를 부르고 onCompleted를 호출하면 클로저가 없어지게 되므로 순환 참조 해결 가능
예제
func downloadJson(_ url: String) -> Observable<String?> {
// 1. 비동기로 생기는 데이터를 Observable로 감싸는 방법
return Observable.create { emitter in
let url = URL(string: url)!
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, _, err) in
guard err == nil else {
emitter.onError(err!)
return
}
if let dat = data, let json = String(data: dat, encoding: .utf8) {
emitter.onNext(json)
}
emitter.onCompleted()
})
task.resume()
return Disposables.create() {
task.cancel()
}
}
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(activityIndicator, true)
// 2. Observable로 오는 데이터를 받아서 처리하는 방법
downloadJson(MEMBER_LIST_URL)
.subscribe { event in
switch event {
case let .next(json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
.debug()를 하면 어떤 데이터가 subscribe로 넘어가는지 체크 가능
Operator들
데이터를 하나 보낼 때
// 데이터 하나를 보낼때는 이거 대신에
return Observable.create { emitter in
emitter.onNext("hello world")
emitter.onCompleted()
return Disposables.create()
}
// 이거
return Observable.just("hello world")
// 이러면? 배열 자체가 전달되겠지
return Observable.just(["hello", "world"])
여러개 데이터를 하나씩 보낼 때
return Observable.from(["hello", "world"])
// 결과: "hello" 한번, "world" 두번 전달됨
배열 자체를 데이터로 보내고 싶다면?
return Observable.of("hello", "world")
// 이렇게 하면 하나씩 넘어옴 from과 같음
return Observable.of(["hello", "world"])
// 결과: ["hello", "world"]
subscribe를 onNext만 처리한다면?
.subscribe(onNext: { print($0) })
.subscribe(onNext: { print($0) }, onCompleted: { print("completed") })
main thread에서 실행되도록 한다면
.observeOn(MainScheduler.instance)
.subscribe(...
데이터를 변경: map
.map(json in json?.count ?? 0)
FlatMap: 이벤트를 다른 Observable로 바꾼다
struct Student {
var score: BehaviorRelay<Int>
}
let mary = Student(score: BehaviorRelay(value: 100))
let john = Student(score: BehaviorRelay(value: 90))
let subject = PublishSubject<Student>()
subject.asObservable()
.flatMap {
$0.score.asObservable()
}
.subscribe(onNext: {
print($0)
}).disposed(by: disposeBag)
특정 데이터만 보냄: filter
.filter(cnt in cnt > 0)
ignoreElements(): 모든 .next로 전달되는 데이터 이벤트를 무시함
근데 onCompleted, onError는 전달됨
let subject = PublishSubject<String>()
subject
.ignoreElements()
.subscribe { event in
print(event)
}.disposed(by: disposeBag)
elementAt(1): sequence에서 index에 해당하는 데이터만 next로 보내줌
subject
.elementAt(2)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
subject.onNext("0")
subject.onNext("1")
subject.onNext("2")
// 결과: 2
근데 만약에 데이터가 2개라서 index가 0~1밖에 없다면?
그러면 아무것도 나오지 않음
filter(): 조건에 해당되는 데이터만 걸러서 보내줌
subject
.filter { $0 < 3 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
// 결과: 0 1
Observable.of(1, 2, 3, 4, 5, 6, 7)
.filter { $0 % 2 == 0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 2 4 6
skip(count): count에 해당하는 만큼 앞에서부터 next 데이터를 count개 생략하고 그 다음부터 보내줌
Observable.of(1, 2, 3, 4, 5, 6, 7)
.skip(6)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 7
skipWhile: 특정 조건이 만족할 때까지 next로 전달되는 데이터를 skip함
근데 한번 조건이 충족되지 않는 데이터가 나타나면 그 뒤는 조건과 상관없이 데이터를 항상 전달함
Observable.of(1, 2, 3, 4, 5, 6, 7)
.skipWhile { $0 % 2 == 0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 1 2 3 4 5 6 7
Observable.of(2, 2, 3, 4, 5, 6, 7)
.skipWhile { $0 % 2 == 0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 3 4 5 6 7
skipUntil: trigger가 되는 시퀀스에서 이벤트가 발생하기 전까지 모든 이벤트가 skip
다음 예제에서는 trigger sequence에 이벤트가 발생하지 않았으므로 데이터가 전달안됨
let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()
let disposeBag = DisposeBag()
subject
.skipUntil(trigger)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
subject.onNext("A")
subject.onNext("B")
// 결과: Nothing
다음 처럼 trigger sequence에 trigger라는 데이터가 next로 전달되는 이벤트가 발생했으므로 C 데이터가 전달됨
subject.onNext("A")
subject.onNext("B")
trigger.onNext("trigger")
subject.onNext("C")
// 결과: C
take(count): 처음 발생하는 count개의 이벤트만 받고 뒤는 생략
let subject = PublishSubject<String>()
subject
.take(2)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
subject.onNext("A")
subject.onNext("B")
subject.onNext("C")
// 결과: A B
takeWhile: 특정 조건이 만족하는 동안만 이벤트를 전달함
한번 만족하지 않으면 뒤에는 조건이 만족하더라도 이벤트 전달 안함
Observable.of(2, 4, 6, 7, 8, 9, 10)
.takeWhile{ $0 % 2 == 0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 2 4 6
takeUntil: trigger sequence에 이벤트가 발생할 때까지 이벤트를 받음
한번 trigger sequence에 이벤트가 발생하면 그 뒤로는 skip됨
let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()
let disposeBag = DisposeBag()
subject
.takeUntil(trigger)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
subject.onNext("A")
subject.onNext("B")
subject.onNext("C")
// 결과: A B C
subject.onNext("A")
subject.onNext("B")
subject.onNext("C")
trigger.onNext("trigger")
subject.onNext("D")
subject.onNext("E")
// 결과: A B C
Transforming Operators
toArray: 단일 아이템들을 배열로 만들어서 보내줌
Observable.of(1, 2, 3, 4, 5)
.toArray()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: [1, 2, 3, 4, 5]
Combining Operators
startWith(elements): 이벤트를 시작할 때 데이터의 맨 처음에 elements 데이터를 넣고 전달함
let numbers = Observable.of(2,3,4)
numbers
.startWith(1)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 1 2 3 4
.startWith(1,2)
// 결과 1 2 2 3 4
concat: 여러개의 sequence를 합쳐서 전달함
let seq1 = Observable.of(1,2,3)
let seq2 = Observable.of(4,5, 6)
Observable.concat([seq1, seq2])
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 1 2 3 4 5 6
// [seq2, seq1] 이렇게 하면 순서 반대
merge: 여러개의 Observable을 합쳐서 전달하는데 데이터가 온 순서대로 합치게 됨
let left = PublishSubject<Int>()
let right = PublishSubject<Int>()
Observable.of(left.asObservable(), right.asObservable())
.merge()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
left.onNext(1)
right.onNext(2)
left.onNext(3)
left.onNext(4)
right.onNext(5)
left.onNext(6)
// 결과: 1 2 3 4 5 6
combineLatest: 값을 합쳐서 전달하는데 만약에 다른 Observable sequence에 값이 없다면 최근의 값을 가지고 합쳐서 줌
let left = PublishSubject<Int>()
let right = PublishSubject<Int>()
Observable.combineLatest(left.asObservable(), right.asObservable(), resultSelector: {
lastL, rightR in
"\(lastL) | \(rightR)"
})
.subscribe(onNext: { print($0) })
left.onNext(1)
right.onNext(2)
right.onNext(3)
right.onNext(4)
left.onNext(5)
left.onNext(6)
left.onNext(7)
right.onNext(8)
/* 결괴
1 | 2
1 | 3
1 | 4
5 | 4
6 | 4
7 | 4
7 | 8
*/
withLatestFrom: 첫 번째 Observable에 이벤트가 발생할 때마다 두번째 Observable에서 발생한 데이터 중 가장 최근의 것과 합쳐서 전달해줌
활용: 텍스트 필드에 어떤 값이 생겼다가 지워졌다가 하던지 간데 button 을 탭 하는 순간 그 순간에 텍스트 필드에 있던 값을 전달받을 수 있음 즉 가장 최근의 값을 처리할 수 있음
let button = PublishSubject<String>()
let txtfield = PublishSubject<String>()
let observable = button.withLatestFrom(txtfield)
observable.subscribe(onNext: { print($0) })
txtfield.onNext("issue 1")
txtfield.onNext("issue 2")
txtfield.onNext("issue 3")
button.onNext("")
// 결괴: issue 3
reduce(_ seed:accumulator:) 모든 이벤트를 한번에 계산한 총합을 하나 방출한다
seed: base value 가장 처음에는 뭐에다가 값을 더할것인지
accumulator: 계산을 어케 해야할지?
Observable.of(1, 2, 3, 4, 5)
.reduce(0, accumulator: +)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
Observable.of(1, 2, 3, 4, 5)
.reduce(0, accumulator: { summary, newvalue in
return summary+newvalue
})
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 15
다른 연산자로 실험하기
// 차
Observable.of(1, 2, 3, 4, 5)
.reduce(15, accumulator: -)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 0
// 곱
Observable.of(1, 2, 3, 4, 5)
.reduce(1, accumulator: *)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 120
// 나누기
Observable.of(1, 2, 3, 4, 5)
.reduce(120, accumulator: /)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 1
// 나머지
Observable.of(1, 2, 3, 4, 5)
.reduce(120, accumulator: %)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 결과: 0
scan(_ seed:accumulator:) 모든 이벤트를 하나씩 더할 때 마다 즉 이벤트가 발생할 때마다 계산해서 값을 방출한다
Observable.of(1, 2, 3, 4, 5)
.scan(0, accumulator: +)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
/* 결괴
1
3
6
10
15
*/
예제
_ = downloadJson(MEMBER_LIST_URL)
.map{ json in json?.count ?? 0 }
.filter{ cnt in cnt > 0 }
.observeOn(MainScheduler.instance)
.subscribe(onNext: { jsonCount in
self.editView.text = "\(jsonCount)"
self.setVisibleWithAnimation(self.activityIndicator, false)
})
RxCocoa
bind operator를 통해서 바로 UI에 값을 적용하는 연산자
viewModel.itemsCount
.map { $0.toString() }
.subscribe(onNext: {
self.itemCountLabel.text = $0
}).disposed(by: disposeBag)
// 이렇게 RxCocoa를 써서 바로 bind
viewModel.itemsCount
.map { $0.toString() }
.observeOn(MainScheduler.instance)
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
tableview에 bind를 하려면 tableview.rx.items에다가 해주면 됨
viewModel.menuObservable.bind(to: tableView.rx.items(cellIdentifier: cellIden, cellType: MenuItemTableViewCell.self)) { index, item, cell in
cell.title.text = item.name
cell.price.text = item.price.toString()
cell.count.text = item.count.toString()
}.disposed(by: disposeBag)
근데 만약에 publish subject로 했다면 미리 생성된 데이터는 subscribe된 시점에 데이터가 전달되지 않으니까
behavior나 replay subject로 바꿔야 함
모든 데이터를 삭제하는 것도 view model에 위임 가능
func clearAllItem() {
menuObservable.map { menus in
menus.map {
Menu(name: $0.name, count: 0, price: $0.price)
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
func changeCount(_ item: Menu, _ increase: Int) {
_ = menuObservable.map { menus in
menus.map { menu in
if (menu.id == item.id) {
return Menu(id: menu.id, name: menu.name, count: max(menu.count + increase, 0), price: menu.price)
}
else {
return Menu(id: menu.id, name: menu.name, count: menu.count, price: menu.price)
}
}
}.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
max(menu.count + increase, 0)
이걸로 마이너스를 없애기
서버에서 데이터 fetching 해오기
init() {
APIService.fetchAllMenusRx()
.map { data -> [MenuItem] in
struct Response: Decodable {
let menus: [MenuItem]
}
let response = try! JSONDecoder().decode(Response.self, from: data)
return response.menus
}
.map { menuItems in
var menus: [Menu] = []
menuItems.enumerated().forEach { (index, item) in
menus.append(Menu.fromMenuItems(id: index, item: item))
}
return menus
}
.take(1)
.bind(to: menuObservable)
}
Design Pattern
1. MVVM
Model <-> View <-> View Model
Lean Controller : less code, more testable
View Model 은 화면에 보여줘야 하는 데이터들의 클래스인것임... 이건 서버에서 오는 데이터 모델과는 다름
Model: 도메인의 데이터 모델
View:UIVIewcontroller
View Model: view에서 쓰이는 데이터 모델
근데 View Model이 view에게 화면 요소를 바꾸라고 지정하는게 아니라 view가 view model을 바라보면서
스스로 바뀌는 것을 알아차리도록 구성함 이것이 데이터 바인딩
view model은 view에 보여지는 데이터만 가지고 있음
2. MVP
Model-View-Presenter
View: 이벤트는 모두 View가 담당함 (인풋과 아웃풋)
Presenter: VC가 담당하던 로직만 Presenter가 대신함
즉 view에 그려져야 하는 요소를 Presenter에서 view에게 명령함
view:presenter=1:1
Model:
3. MVC
Model-View-Controller
Controller: UIViewController의 역할
이벤트에 대한 인풋과 아웃풋을 담당. 즉 view와 model 사이를 관장하며 데이터를 받아서 뿌려주는 역할
View: UIView의 역할
Model: data
UIKit에 종속되지 않는 pure한 모델 즉 테스트 가능함(testable)
ex) toDoList app에서 Model은 task, 즉 할일 데이터들임
Subject
데이터를 외부에서도 접근해서 변경할 수 있게 도와줌
1. Publish Subject
let subject = PublishSubject<String>()
subject.onNext("Issue 1")
2. BehaviorSubject
초기값이 있는 subject
subscribe한 뒤 생기는 데이터부터 보내주는 Publish subject 와는 다르게 subscribe한 후에 가장 마지막의 데이터를 보내준다.
let subject2 = BehaviorSubject(value: "initial")
subject2.onNext("Issue 1")
subject2.subscribe { event in
switch event {
case .next(let emitter):
print(emitter)
case .completed:
print("completed")
case .error(let e):
print("error")
}
}
초기값이 있어도 마지막 데이터는 onNext로 전달된 Issue 1이기 때문에 print하면 Issue 1이 출력된다.
3. ReplaySubject
이전까지 발생한 데이터 중 buffer size 만큼 방출한다.
// 최근 2개의 데이터를 subscribe 할때 보내줌
let subject3 = ReplaySubject<String>.create(bufferSize: 2)
subject3.onNext("Issue 1")
subject3.onNext("Issue 2")
subject3.onNext("Issue 3")
subject3.subscribe(onNext: { emitter in
print(emitter)
})
// 결과
// Issue 2
// Issue 3
Variable
Varaiable은 BehaviorSubject 의 현재 값을 State 로 가짐
그냥 일반적인 값을 Observable 처럼 보이기 위해 사용
let checkBoxValid = Variable(true)
checkBoxValid.asObservable()
.bindTo(checkBoxButton.rx.isSelected)
.disposed(by: disposeBag)
checkBoxButton.rx.tap
.subscribe(onNext: {
checkBoxValid.value = !checkBoxValid.value
})
.disposed(by: disposeBag)
- Variable(true) 이렇게 값을 Variable 로 감싸서 표현하고
- 해당 값을 subscribe 하고 싶으면 .asObservable() 을 통해 Observable 로 간주해주어야 한다.
- 값을 꺼내 쓰거나 새로운 값을 넣고 싶으면 .onNext() 대신 checkBoxValie.value 처럼 .value를 사용한다.
- 에러가 발생하지 않음이 보장되고, 할당 해제 시 자동 완료가 되기 때문에 .onError, .onCompleted 역시 필요하지 않다.
BehaviorRelay
Relay 는 RxCocoa 의 클래스, Subject 는 RxSwift 내의 클래스다.
let relay = BehaviorRelay(value: "initial")
relay.asObservable().subscribe {
print($0)
}
relay.accept("issue 1")
relay의 value는 get only이므로 accept() 메소드로 값을 변경할 수 있음
만약에 배열에 값을 append 하고 싶다면?
let relay = BehaviorRelay(value: ["initial"])
relay.accept(relay.value + ["issue 1"])
relay.asObservable().subscribe {
print($0)
}
or like this
let value = relay.value
value.append(["1", "2", "3"])
relay.accept(value)
Observer VS Observable
Observer: 수신기
Observable: 송신기
Observer만 Observable에게 데이터를 Bind to 시킬 수 있는 일방향 관계임
Subject는 Observer와 Observable 두 가지를 모두 할 수 있으니까 이를 명확하게 하기 위해서
즉 하나의 역할만 하도록 하기 위해서 asObserver() asObservable() operator를 제공함
let subject = PublishSubject<Student>()
let subjectObserver = subject.asObserver()
let subjectObservable = subject.asObservable()
subjectObservable
.flatMap {
$0.score.asObservable()
}
.subscribe(onNext: {
print($0)
}).disposed(by: disposeBag)
subjectObserver.onNext(john)
RxCocoa
ControlEvent
RxCocoa에서 이벤트는 ViewController가 Load되면 발생하는 ViewDidLoad에 대한 메세지가 될 수도 있고, User로 인해 발생하는 Tap Event가 될 수도 있음. 이러한 이벤트들의 발생 때 마다 데이터들 전달함.
즉 ControlEvent는 Event들에 대한 시퀀스를 받아올 수 있는 기능
예시
textfield의 return 버튼이 tapped 될 때마다 즉 .editingDidEndOnExit 이벤트 발생마다 트리거 되어서
txtfield의 값을 가져온 뒤 fetchWeather 함수를 실행시킴
self.cityNameTextField.rx.controlEvent(.editingDidEndOnExit)
.asObservable()
.map { self.cityNameTextField.text }
.subscribe(onNext: { city in
guard let city = city else {
return
}
self.fetchWether(of: city)
}).disposed(by: disposeBag)
ControlProperty, Driver
ControlProperty는 Subject처럼 프로퍼티에 값을 주입할 수 있고 동시에 값의 변화도 관찰할 수 있는 타입. ControlProperty를 사용하면 해당하는 프로퍼티의 변경사항을 데이터 시퀀스로 받아올 수 있음
ControlProperty는 bind(to:) 메서드를 통해 subscribe를 수행가능함
Drive는 BehaviorRelay와 비슷함
즉 subscribe 하게 되면 가장 최근 데이터를 하나 내려보내 줌
Driver Feature
1. Units cant' error out
2. Units are observed on main scheduler
3. Units subscribe on main scheduler
4. Units share side effects
예시
drive는 bind 말고 drive 메소드 사용하기
또한 main scheduler에서 실행되기 때문에 ObserveOn이 필요 없음
let search = URLRequest.load(resource: resource)
.asDriver(onErrorJustReturn: WeatherResult.empty)
search.map { "\($0.main.temp) ℉" }
.drive(temperatureLabel.rx.text)
.disposed(by: disposeBag)
search.map { "\($0.main.humidity) 💧" }
.drive(humidityLabel.rx.text)
.disposed(by: disposeBag)
nothing: inside singleone or a view controller which are never released
unowned: inside all view controllers which are released after the closure task is performed
weak: any other case
unowned vs weak
unowned는 항상 참조 값이 존재할 것이라고 생각함 (weak는 아니라서 self?를 쓰고, 참조 값이 없다면 nil을 참조함)
그래서 참조값이 nil이 되면 crash가 발생함
Error Handling
Sort of Error
1. No Internet Connection
2. Invalid Input
3. API Error or HTTP Error
Handle the Error
1. Observable -> Catch -> Subscription
ex. catchErrorJustReturn과 같이 Observable에서 에러를 감지해서 default를 전달함으로써 error handling
2. Observable <-> Retry -> Subscription
error가 발생한 경우 retry를 하여 다시 시도함
예시
static func load<T: Decodable>(resource: Resource<T>) -> Observable<T> {
return Observable.just(resource.url)
.flatMap { url -> Observable<(response: HTTPURLResponse, data: Data)> in
let request = URLRequest(url: url)
return URLSession.shared.rx.response(request: request)
}
.map { (response, data) -> T in
// 200~300 사이에 있거나 같은 경우
if 200..<300 ~= response.statusCode {
return try JSONDecoder().decode(T.self, from: data)
}
else {
throw RxCocoaURLError.httpRequestFailed(response: response, data: data)
}
}
.asObservable()
// catch error
let search = URLRequest.load(resource: resource)
.catch { error in
print(error.localizedDescription)
return Observable.just(WeatherResult.empty)
}
.asDriver(onErrorJustReturn: WeatherResult.empty)
// retry
let search = URLRequest.load(resource: resource)
.retry(3)
.catch { error in
print(error.localizedDescription)
return Observable.just(WeatherResult.empty)
}
.asDriver(onErrorJustReturn: WeatherResult.empty)
MVVM의 View Model의 예시
struct ArticleListViewModel {
let articles: [ArticleViewModel]
init(articles: [Article]) {
self.articles = articles.compactMap(ArticleViewModel.init)
}
func articleAt(_ index: Int) -> ArticleViewModel {
return self.articles[index]
}
}
struct ArticleViewModel {
let article: Article
init(article: Article) {
self.article = article
}
}
extension ArticleViewModel {
var title: Observable<String> {
return Observable<String>.just(article.title)
}
var description: Observable<String> {
return Observable<String>.just(article.description ?? "")
}
}
compactMap(_transform:): 컨테이너의 각 요소를 조건을 지정하여 호출할 때, nil 이 아닌 배열을 반환
즉 Map과 같이 값을 하나씩 까서 각각의 요소마다 값을 매핑해주는 역할을 하는데 nil 없고 optional도 자동으로 없애주는 편리함
transform 는 컨테이너의 요소를 매개변수로 받아 선택적 값을 반환하는 클로저
init(articles: [Article]) {
self.articles = articles.compactMap(ArticleViewModel.init)
}
'MOBILE > ios' 카테고리의 다른 글
[iOS] Alamofire를 사용하여 비동기 서버 통신하기 (0) | 2023.01.15 |
---|---|
[iOS] 화면 전환 method Present(_:)와 stack 메모리 (0) | 2022.12.11 |
[iOS] coreML 사용하여 multi class classification 모델 생성 및 프로젝트 반입하기 (1) | 2022.11.12 |
[iOS] Apple Developer Document: UICollectionView (2) | 2022.08.03 |
[iOS] SnapKit을 사용하여 AutoLayout constraint 설정하기 (0) | 2022.08.03 |