Publisher: 시간이 경과함에 따라 일련의 값을 발행하는 객체
비동기 작업의 결과를 한 번만 발행하는 Publisher
let future = Future<Int, Error> { promise in
let success = true // 테스트용
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
if success {
promise(.success(44))
}
else {
promise(.failure(.somethingWentWrong))
}
}
}
let cancellable = future
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("작업 완료")
case .failure(let error):
print("작업 실패: \(error)")
}
},
receiveValue: { value in
print("받은 값: \(value)")
}
)
2초 후 44를 발행하고 완료.
단일 값을 즉시 발행하고 완료하는 Publisher
간단한 값 전달과 기본값 설정에 유용
Just("Hello, Combine!")
.sink(receiveValue: { print($0) })
Publisher의 생성을 지연시켜, 구독자가 생길 때마다 새로운 Publisher를 생성하는 친구
let deferredFuture = Deferred {
Future<Int, Never> { promise in
print("Future 실행")
promise(.success(Int.random(in: 1...100)))
}
}
값을 발행하지 않고 즉시 완료되는 Publisher
Empty<Int, Never>()
.sink(receiveCompletion: { print("완료: \($0)") })
즉시 실패 이벤트를 발행하는 Publisher
Fail<Int, MyError>(error: .invalidInput)
.sink(receiveCompletion: { print("실패: \($0)") })
미리 정의된 값과 완료 이벤트를 순차적으로 발행하는 Publisher
Record(output: [1, 2, 3], completion: .finished)
.sink(receiveValue: { print($0) })
Subscriber: Publisher를 구독해 발행받은 값을 처리하는 객체
import Combine
let publisher = Just("Hello, Combine!")
let cancellable = publisher.sink(
receiveCompletion: { completion in
print("완료: \(completion)")
},
receiveValue: { value in
print("받은 값: \(value)")
}
)
발행된 값과 완료 이벤트에 대해 개별 처리 가능
import Combine
class MyViewModel: ObservableObject {
@Published var message = ""
}
let viewModel = MyViewModel()
let cancellable = Just("안녕하세요!")
.assign(to: \.message, on: viewModel)
print(viewModel.message) // "안녕하세요!"
Never 이어야 사용가능 (Failure == Never)@Published일 경우 SwiftUI View에 자동 업데이트 가능Publisher와 Subscriber 간의 구독을 관리하고 취소할 수 있는 프로토콜
import Combine
var cancellables = Set<AnyCancellable>()
let publisher = Just("Hello")
publisher
.sink { value in
print("받은 값: \(value)")
}
.store(in: &cancellables)
// or
/*
let cancellable = Just("Hello")
.sink { value in
print("받은 값: \(value)")
}
*/
Set 자료구조로 여러 구독을 저장하고 필요할 때 일괄적으로 취소가 가능하다.
AnyCancellable로 관리되는 구독들은 type-erasing 래퍼로 내부 구독 정보를 알 수 없게 되어 각각의 구독들을 식별 불가
해결 방법
let cancellable = publisher.sink { value in
print("받은 값: \(value)")
}
cancellable.cancel()
구독하던 publisher 객체가 데이터를 발행하더라도 구독취소 시점부터 수신X
Cancellable 객체는 Publisher와 Subscriber를 강한 참조로 가지고 있음.Cancellable 객체가 자신을 소유하는 객체가 메모리에서 해제되었을 때, Cancellable 객체도 메모리에서 해제Publisher와 Subscriber의 관계를 끊어주기 위한 cancel 동작이 필요함.cancel이 되지 않으면 각각의 Ref Count가 처리되지 못해 메모리에 남아있게 됨.public final class AnyCancellable: Cancellable, Hashable {
...
deinit {
_cancel?()
}
}
개발자를 위해 내부적으로 deinit시점에 cancel을 호출해주는 로직이 포함되어 있어 알아서 순환참조 문제를 해결하고, 해제 시점에 cancel에 대해 신경안써도 됨.
https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombine/AnyCancellable.swift
다만 Subscriber가 self를 강하게 참조할 경우 이 또한 순환참조 문제가 생기므로, 약한 참조를 사용해주어야 한다.
구체적인 타입을 공통된 인터페이스로 추상화하여 유연하고 재사용 가능한 코드를 작성할 수 있게 하는 기법
위의 store 메서드 예시를 보자
import Combine
var cancellables = Set<AnyCancellable>()
let publisher = Just("Hello")
publisher
.sink { value in
print("받은 값: \(value)")
}
.store(in: &cancellables)
// or
/*
let cancellable = Just("Hello")
.sink { value in
print("받은 값: \(value)")
}
*/
이 상황에서 cancellables 변수가 Just만을 구독 관리할 수 있다면 각각의 다른 Publisher 마다 관리 해줄 cancellables 변수가 필요로 하게 된다.
하지만 여기서 Type Erasure로 특정 Publisher 타입에 대해 국한하지 않고 AnyCancellable 이라는 Type Erasure 기법이 적용된 타입을 넣어 하나의 변수로 모든 Publisher를 관리할 수 있게 할 수 있다.
또 다른 예시로 AnyPublisher 가 있다.
var validatedPassword: Publishers.Map<
Publishers.CombineLatest<
Published<String>.Publisher, Published<String>.Publisher>,
String?> {
return $password
.combineLatest($passwordAgain)
.map { password, passwordAgain in
guard password == passwordAgain, password.count > 1 else { return nil }
return password
}
}
나는 그저 두 Publisher가 뱉는 값을 가공하고 다시 Publisher로 내보내고 싶었을 뿐이다.
하지만 위 예시처럼 구체적으로 리턴될 타입을 써야함으로써 코드가 읽기 힘들어지는 문제가 있다.
var validatedPassword: AnyPublisher<String?, Never> {
return $password
.combineLatest($passwordAgain)
.map { password, passwordAgain in
guard password == passwordAgain, password.count > 1 else { return nil }
return password
}
.eraseToAnyPublisher()
}
하지만 AnyPublisher를 씀으로써 리턴될 타입을 추상화하여 코드가 한결 읽기 쉬워지는 장점이 있다.
출처: https://mini-min-dev.tistory.com/298