티스토리 뷰
지금까지 Combine 내용에선 무조건 성공하는 케이스만 다뤄봤는데,
이번엔 실패했을경우 에러 처리를 어떻게 해야하는지, 어떤 에러처리 방법이 있는지 다뤄보도록 하겠습니다.
Never
Failure 타입이 Never인 publisher는 erorr가 없는 경우 입니다.
example(of: "Never sink") { Just("Hello") .sink(receiveValue: { print($0) }) .store(in: &subscriptions) }
Just의 경우 내부를 보면 Failure = Never인걸 확인할 수 있습니다.
만약 Never 타입에서 동작하는 operator(연산자)는 몇개 더 있습니다. 대표적으로 많이 쓰는 `setFailureType`을 먼저 봅시다.
setFailureType
import Foundation import Combine var subscriptions = Set<AnyCancellable>() enum MyError: Error { case ohNo } example(of: "setFailureType") { Just("Hello") .setFailureType(to: MyError.self) .sink(receiveCompletion: { completion in switch completion { case .failure(.ohNo): print("Finished with Oh no") case .finished: print("Finished successfully") } }, receiveValue: { value in print("Got value: \(value)") } ) .store(in: &subscriptions) } // ——— Example of: setFailureType ——— // Got value: Hello // Finished successfully
Just가 실패하지 않는 타입이기 때문에 결과값은 위와 같이 나옵니다.
assign(to:on)
assign은 Just가 이벤트를 방출할 때마다 값을 바꿔줍니다.
*setFailureType과 같이 Never에서만 동작합니다.import Foundation import Combine var subscriptions = Set<AnyCancellable>() example(of: "assign(to:on:)") { // 1 class Person { let id = UUID() var name = "Unknown" } // 2 let person = Person() print("1", person.name) Just("Peppo") .handleEvents( receiveCompletion: { _ in print("2", person.name) } ) .assign(to: \.name, on: person) // 4 .store(in: &subscriptions) } //——— Example of: assign(to:on:) ——— //1 Unknown //2 Peppo
1. id, name 프로퍼티를 가진 Person class를 정의
2. Person()을 인스턴스화 해 name을 프린트
3. handleEvents를 사용해, publisher가 완료 이벤트(completion event)를 보내면 person name을 다시 프린트
4. publisher가 방출하는대로 person name을 세팅하기 위해 assign 사용
assign(to:) - memory leak 방지
들어가기전에 아래 코드 예시에서 assign(to:on)을 사용했을때 문제점을 봅시다.import Foundation import Combine var subscriptions = Set<AnyCancellable>() example(of: "assign(to:)") { class MyViewModel: ObservableObject { // 1 @Published var currentDate = Date() init() { Timer.publish(every: 1, on: .main, in: .common) // 2 .autoconnect() .prefix(3) // 3 .assign(to: \.currentDate, on: self) // 4 .store(in: &subscriptions) } } // 5 let vm = MyViewModel() vm.$currentDate .sink(receiveValue: { print($0) }) .store(in: &subscriptions) } //——— Example of: assign(to:) ——— //2023-02-22 00:30:23 +0000 //(1초후)2023-02-22 00:30:24 +0000 //(1초후)2023-02-22 00:30:25 +0000 //(1초후)2023-02-22 00:30:26 +0000
1. MyViewModel 내부에 @Published 프로퍼티 currentDate를 정의 해주고, 초기화 시켜줍니다.
2. 매 1초마다 현재 날짜,시간을 방출하는 timer publisher를 만들어줍니다.
3. 3개의 업데이트만 받기 위해 prefix 연산자(operator)를 사용합니다.
4. assign(to:on:) 연산자를 사용해, @Published 프로퍼티 (currentDate)를 매번 date 업데이트 해줍니다.
5. currentDate를 구독(sink)해 방출되는 값을 print 합니다.
정상 작동하는것 처럼 보이지만, 아래 그림과 같이 memory leak 이 나고 있습니다.
아래 그림을 보면,
assign(to:on:)를 호출하게 되면, 강하게 유지하는 self의 subscription을 만듭니다.
즉, self가 subscription에 메어 있고, subscription도 self에 메어있는 상태로 retain cycle이 일어나기 때문에 memory leak이 발생합니다.
이걸 해결하기 위해 assign(to:)를 사용합니다.import Foundation import Combine var subscriptions = Set<AnyCancellable>() example(of: "assign(to:)") { class MyViewModel: ObservableObject { @Published var currentDate = Date() init() { Timer.publish(every: 1, on: .main, in: .common) .autoconnect() .prefix(3) .assign(to: &$currentDate) // 수정 } } // 5 let vm = MyViewModel() vm.$currentDate .sink(receiveValue: { print($0) }) .store(in: &subscriptions) } //——— Example of: assign(to:) ——— //2023-02-22 00:30:23 +0000 //(1초후)2023-02-22 00:30:24 +0000 //(1초후)2023-02-22 00:30:25 +0000 //(1초후)2023-02-22 00:30:26 +0000
내부적으로 subscription에 대한 메모리 관리를 자동으로 처리하므로 .store(in: &subscriptions) 를 생략할 수 있습니다.
assertNoFailure
에러를 감지하면 fatalError를 발생시키고(?), 아니면 받았던 모든 input을 republish 합니다.
import Foundation import Combine var subscriptions = Set<AnyCancellable>() enum MyError: Error { case ohNo } example(of: "assertNoFailure") { Just("Hello") .setFailureType(to: MyError.self) .assertNoFailure() .sink(receiveValue: { print("God value: \($0) ")}) .store(in: &subscriptions) } //——— Example of: assertNoFailure ——— //God value: Hello
여기서 tryMap을 사용해 에러를 감지 시켜보면 ?import Foundation import Combine var subscriptions = Set<AnyCancellable>() enum MyError: Error { case ohNo } example(of: "assertNoFailure") { Just("Hello") .setFailureType(to: MyError.self) .tryMap { _ in throw MyError.ohNo} // 추가 .assertNoFailure() .sink(receiveValue: { print("God value: \($0) ")}) .store(in: &subscriptions) } // fatalerror
근데 어디에 쓰는지는 잘 모르겠다..
Dealing with failure
지금까지 에러가 발생하지 않는경우만 봤는데,
에러가 발생하는 경우에 대해 알아 보겠습니다.
try* operators
try-prefixed 의 연산자를 사용해서 알아보겠습니다.
먼저 아래코드를 보면
import Foundation import Combine var subscriptions = Set<AnyCancellable>() example(of: "tryMap") { enum NameError: Error { case tooShort(String) case unowned } let names = ["Marin", "Shai", "Florent"].publisher names .tryMap { value -> Int in let length = value.count guard length >= 5 else { throw NameError.tooShort(value) } return value.count } .sink(receiveCompletion: { print("Completed with \($0)") }, receiveValue: { print("Got value: \($0)") } ) } //——— Example of: tryMap ——— //Got value: 5 //Completed with failure(...NameError.tooShort("Shai"))
위 코드는 length가 5미만인 요소인경우 NameError.tooShort의 에러로 throw 됩니다.
Mapping errors (map vs try map)
* map, tryMap의 차이점
map - publihser의 값만 조작, 기존 에러타입을 반영
tryMap - error throwing, 에러타입을 Swift Error로 변형(erase) 함.
아래는 map 사용했을때의 코드입니다.
import Foundation import Combine var subscriptions = Set<AnyCancellable>() example(of: "map vs tryMap") { enum NameError: Error { case tooShort(String) case unowned } Just("Hello") .setFailureType(to: NameError.self) // 1 .map { $0 + " World!" } .sink( receiveCompletion: { completion in switch completion { case .finished: print("Done!") case .failure(.tooShort(let name)): print("\(name) is too short!") case .failure(.unowned): print("An unknown name error occurred") } }, receiveValue: { print("Got value \($0)") } ) .store(in: &subscriptions) } //——— Example of: map vs tryMap ——— //Got value Hello World! //Done!
1. setFailureType을 사용해서 실패유형을 NameError로 세팅.
여기서 map을 tryMap으로 바꿔보면 에러가 나오는데,
이유는 즉 아직 Swift에서 try-prefixed 연산자를 사용할 경우 typed Error (e.g. NameError)를 throw하지 못합니다.
대안으로 Combine에서 mapError를 제공합니다.example(of: "map vs tryMap") { enum NameError: Error { case tooShort(String) case unowned } Just("Hello") .setFailureType(to: NameError.self) .tryMap { $0 + " World!" } // map → tryMap .mapError { $0 as? NameError ?? .unowned } // 추가 .sink( receiveCompletion: { completion in switch completion { case .finished: print("Done!") case .failure(.tooShort(let name)): print("\(name) is too short!") case .failure(.unowned): print("An unknown name error occurred") } }, receiveValue: { print("Got value \($0)") } ) .store(in: &subscriptions) } //——— Example of: map vs tryMap ——— //Got value Hello World! //Done!
mapError는 upstream publisher에서 에러가 발생했을시 타입캐스팅된 에러타입으로 error thrown합니다.
아래처럼 바로 error thrown 할 수도 있습니다.example(of: "map vs tryMap") { enum NameError: Error { case tooShort(String) case unowned } Just("Hello") .setFailureType(to: NameError.self) .tryMap { throw NameError.tooShort($0) } // 변경 .mapError { $0 as? NameError ?? .unowned } .sink( receiveCompletion: { completion in switch completion { case .finished: print("Done!") case .failure(.tooShort(let name)): print("\(name) is too short!") case .failure(.unowned): print("An unknown name error occurred") } }, receiveValue: { print("Got value \($0)") } ) .store(in: &subscriptions) } //——— Example of: map vs tryMap ——— //Hello is too short!
'iOS' 카테고리의 다른 글
[iOS] AVAssetWriter로 비디오 저장 (2) | 2023.03.26 |
---|---|
RxSwift - Reactive Extensions (.rx) (0) | 2023.03.11 |
[iOS] Camera 설정 - AVCaptureSession(feat. 카메라 권한 설정) (0) | 2023.02.26 |
[Combine] Chapter13 : Resource Management (0) | 2023.01.19 |
[Combine] Chapter12 : Key-Value Observing (KVO) (0) | 2023.01.15 |
- Total
- Today
- Yesterday
- RTCCameraVideoCapturer
- Swift joined()
- Swift init
- RIBs tutorial
- Swift ModernRIBs
- Swift 프로그래머스
- Swift 알고리즘
- 2023년 회고
- iOS error
- swift property
- Swift joined
- CS 네트워크
- Swift Error Handling
- Swift RIBs
- Swift
- swift (programmers)
- ios
- Swift 내림차순
- swift 고차함수
- Swift final
- swift protocol
- Swift inout
- Swift 프로퍼티
- removeLast()
- Class
- 원티드 프리온보딩
- swift reduce
- swift programmers
- Swift Leetcode
- Combine: Asynchronous Programming with Swift
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |