지금까지 봤던 다양한 Publisher들에서 알 수 있듯, Combine은 스트림에서 에러 타입을 꼭 정의해주어야 한다.
struct AnyPublisher<Output, Failure> where Failure : Error
Publisher에서 Operator, Subscriber로 흘러가는 동안 에러는 언제 어디서든 발생할 수 있다. 따라서 Combine을 효과적이고 안전하게 사용하기 위해서 에러를 적절하게 처리하는 것은 필수적이다! (사실 Combine뿐 아니라 모든 reactive programming에 다 해당되는 일,,)
Combine에서의 에러 핸들링 방법을 알아보자!✨
본격적인 에러 핸들링 방법을 공부하기 전, Never
에 대해 잠깐 짚고 넘어가보자.
Failure 타입이 Never
인 Publisher는 절대 실패하지 않는다.
따라서 Publisher가 값을 소비하는 데에만 집중할 수 있다.
Combine에서는 절대 실패하지 않을 때만 사용할 수 있는 메소드들이 있다.
func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
let integers = (0...3)
integers.publisher
.sink { print("Received \($0)") }
// Prints:
// Received 0
// Received 1
// Received 2
// Received 3
이 메소드는 subscriber를 생성하고, subscriber를 리턴하기 전에 바로 값을 무한대로 요청한다.
리턴되는 값은 유지되어야 하고, 그렇지 않으면 스트림이 취소된다.
func assign<Root>(
to keyPath: ReferenceWritableKeyPath<Root, Self.Output>,
on object: Root
) -> AnyCancellable
publisher로부터 받은 값을 object의 프로퍼티에 할당해주는 메소드이다.
이전 스터디 주차에서 공부했었던 메소드라 자세한 설명은 생략!!
func setFailureType<E>(to failureType: E.Type) -> Publishers.SetFailureType<Self, E> where E : Error
실패하지 않는 publisher를 실패할 수 있는 publisher로 변환해야 하는 경우가 있을 것이다.
setFailureType
operator를 사용하면 upstream publisher의 failure type을 바꿀 수 있다.
대신 upstream publisher의 failure type은 Never
여야 한다!
(실패할 수 있는 upstream의 에러 타입을 바꾸고 싶다면 mapError(_:)
를 사용하자)
let pub1 = [0, 1, 2, 3, 4, 5].publisher
let pub2 = CurrentValueSubject<Int, Error>(0)
let cancellable = pub1
.setFailureType(to: Error.self)
.combineLatest(pub2)
.sink(
receiveCompletion: { print ("completed: \($0)") },
receiveValue: { print ("value: \($0)")}
)
// Prints: "value: (5, 0)".
combineLatest
는 두 publisher의 Failure 타입이 같아야 하기 때문에, 위 예시에서는 setFailureType
으로 에러 타입을 맞춰주었다.
또한 setFailureType
은 타입에만 영향을 미치기 때문에 실제 에러는 발생하지 않는다.
func assertNoFailure(
_ prefix: String = "",
file: StaticString = #file,
line: UInt = #line
) -> Publishers.AssertNoFailure<Self>
upstream publisher가 실패하면 fatal error를 일으키고, 아니면 받은 input 값을 모두 다시 publish하는 메소드
prefix
: fatal error 메시지 앞에 적을 stringfile
: 에러 메시지에서 사용할 파일명line
: 에러 메시지에서 사용할 라인 넘버public enum SubjectError: Error {
case genericSubjectError
}
let subject = CurrentValueSubject<String, Error>("initial value")
subject
.assertNoFailure()
.sink(receiveCompletion: { print ("completion: \($0)") },
receiveValue: { print ("value: \($0).") }
)
subject.send("second value")
subject.send(completion: Subscribers.Completion<Error>.failure(SubjectError.genericSubjectError))
// Prints:
// value: initial value.
// value: second value.
// The process then terminates in the debugger as the assertNoFailure operator catches the genericSubjectError.
subject
가 세 번째로 genericSubjectError
라는 에러를 보냈고, fatal exception이 발생해서 프로세스가 중단됐다.
Combine 은 에러를 발생시킬 수 있는 operator 와 그렇지 않은 operator 사이에 구분을 제공한다.
이 operator들은 publisher의 Failure
를 Swift의 Error
타입으로 바꾼다.
그외 너무 많다.. 그때그때 try operator가 있는지 찾아보면서 개발하자
map
은 에러를 던질 수 없는 non-throwing 메소드이다. 에러를 던지고 싶을 때는 tryMap
을 사용하자.
func tryMap<T>(_ transform: @escaping (Self.Output) throws -> T) -> Publishers.TryMap<Self, T>
struct ParseError: Error {}
func romanNumeral(from:Int) throws -> String {
let romanNumeralDict: [Int : String] =
[1:"I", 2:"II", 3:"III", 4:"IV", 5:"V"]
guard let numeral = romanNumeralDict[from] else {
throw ParseError()
}
return numeral
}
let numbers = [5, 4, 3, 2, 1, 0]
cancellable = numbers.publisher
.tryMap { try romanNumeral(from: $0) }
.sink(
receiveCompletion: { print ("completion: \($0)") },
receiveValue: { print ("\($0)", terminator: " ") }
)
// Prints: "V IV III II I completion: failure(ParseError())"
func mapError<E>(_ transform: @escaping (Self.Failure) -> E) -> Publishers.MapError<Self, E> where E : Error
struct DivisionByZeroError: Error {}
struct MyGenericError: Error { var wrappedError: Error }
func myDivide(_ dividend: Double, _ divisor: Double) throws -> Double {
guard divisor != 0 else { throw DivisionByZeroError() }
return dividend / divisor
}
let divisors: [Double] = [5, 4, 3, 2, 1, 0]
divisors.publisher
.tryMap { try myDivide(1, $0) }
.mapError { MyGenericError(wrappedError: $0) }
.sink(
receiveCompletion: { print ("completion: \($0)") ,
receiveValue: { print ("value: \($0)", terminator: " ") }
)
// Prints: "0.2 0.25 0.3333333333333333 0.5 1.0 completion: failure(MyGenericError(wrappedError: DivisionByZeroError()))"
func replaceError(with output: Self.Output) -> Publishers.ReplaceError<Self>
스트림에서 받은 에러를 특정한 값으로 대체(replace)하는 operator
struct MyError: Error {}
let fail = Fail<String, MyError>(error: MyError())
cancellable = fail
.replaceError(with: "(replacement element)")
.sink(
receiveCompletion: { print ("\($0)") },
receiveValue: { print ("\($0)", terminator: " ") }
)
// Prints: "(replacement element) finished".
에러를 다시 감싸고 downstream subscriber로 받은 값을 다시 넘겨주고 싶을 때는 catch(_:)
사용
func `catch`<P>(_ handler: @escaping (Self.Failure) -> P) -> Publishers.Catch<Self, P> where P : Publisher, Self.Output == P.Output
upstream publisher로부터 받은 에러를 다른 publisher로 교체하는 메소드
Publishers.Catch
가 상위 Publisher가 에러를 낼 때 다른 Publisher로 교체하는 동작을 제공한다면, Publishers.ReplaceError
는 상위 Publisher가 에러를 낼 때 특정 요소로 교체하는 동작을 제공한다.
func retry(_ retries: Int) -> Publishers.Retry<Self>
upstream publisher를 이용해서 인자로 전달한 값까지 failed subscription을 다시 만든다.
retries
: 재시도할 횟수struct WebSiteData: Codable {
var rawHTML: String
}
let myURL = URL(string: "https://www.example.com")
cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
.retry(3)
.map({ (page) -> WebSiteData in
return WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
})
.catch { error in
return Just(WebSiteData(rawHTML: "<HTML>Unable to load page - timed out.</HTML>"))
}
.sink(receiveCompletion: { print ("completion: \($0)") },
receiveValue: { print ("value: \($0)") }
)
// Prints: The HTML content from the remote URL upon a successful connection,
// or returns "<HTML>Unable to load page - timed out.</HTML>" if the number of retries exceeds the specified value.