만약 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
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 됩니다.
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!