본문 바로가기

MOBILE/ios

[iOS] RxSwift 마스터하기

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)
    }