Publisher로부터 전달되는 값을 변환, 필터링, 결합, 시간 조작 등 다양한 방식의 처리를 지원하는 요소
값을 가공 또는 원하는 조건에 따라 필터링 기능을 지원합니다.
주어진 조건을 만족하는 값만 통과
import Combine
let numbers = (1...10).publisher
numbers
.filter { $0 % 2 == 0 } // 짝수만 통과
.sink { print($0) }
연속으로 중복된 값을 제거합니다. (Set과 다름)
import Combine
let words = ["apple", "apple", "banana", "banana", "apple"].publisher
words
.removeDuplicates()
.sink { print($0) }
// apple banana apple
조건을 만족하는 첫 번째 값만 통과시킵니다.
import Combine
let numbers = (1...10).publisher
numbers
.first(where: { $0 > 5 })
.sink { print($0) }
그 외
Publisher로부터 전달된 값을 변환합니다.
Swift에서 흔히 사용하는 고차함수인 map과 동일하게 동작
주어진 클로저를 통해 값을 변환
import Combine
let numbers = (1...3).publisher
numbers
.map { "Number: \($0)" }
.sink { print($0) }
클로저 내에서 오류를 던질 수 있는 경우에 사용
오류 발생 시, 해당 스트림은 실패로 종료
import Combine
enum ConversionError: Error {
case invalidFormat
}
let strings = ["123", "abc", "456"].publisher
strings
.tryMap { str -> Int in
guard let number = Int(str) else {
throw ConversionError.invalidFormat
}
return number
}
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed successfully")
case .failure(let error):
print("Failed with error: \(error)")
}
},
receiveValue: { value in
print("Parsed number: \(value)")
}
)
각 입렵값을 새로운 Publisher로 변환하고 변환된 Publisher들의 출력을 단일 스트림으로 병합합니다.
비동기 작업을 순차적으로 처리하거나, 중첩된 Publisher를 평탄화할 때 유용
import Combine
func fetchDetails(for id: Int) -> AnyPublisher<String, Never> {
Just("Details for ID \(id)")
.delay(for: .milliseconds(100), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
let ids = [1, 2, 3].publisher
ids
.flatMap { fetchDetails(for: $0) }
.sink { print($0) }
Publisher가 발행하는 여러 값을 하나의 배열로 수집하여 한 번에 전달
import Combine
let numbers = (1...5).publisher
numbers
.collect()
.sink { print($0) }
// 결과: [1, 2, 3, 4, 5]
배열에 저장될 값의 개수를 지정해서도 가능
import Combine
let numbers = (1...10).publisher
numbers
.collect(3)
.sink { print($0) }
/*
결과: [1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]
*/
초기값과 클로저를 사용하여 이전 결과와 현재 값을 누적하여 새로운 값을 생성
reduce고차 함수와 비슷
import Combine
let numbers = (1...5).publisher
numbers
.scan(0) { accumulated, current in
accumulated + current
}
.sink { print($0) }
/*
결과:
1
3
6
10
15
*/
값이 Nil일 경우의 기본값을 지정합니다.
import Combine
let values: [String?] = ["A", nil, "C"]
values.publisher
.replaceNil(with: "-")
.sink { print($0) }
/*
A
-
C
*/
그 외
여러 Publisher의 이벤트를 결합하여 새로운 데이터 흐름을 생성하는데 사용
여러 Publisher의 출력을 하나의 스트림으로 병합, 병합된 Publisher가 값을 발행할 때마다 해당 값이 순서에 상관없이 전달
import Combine
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1
.merge(with: publisher2)
.sink { print("Received value: \($0)") }
publisher1.send(1)
publisher2.send(2)
publisher1.send(3)
Received value: 1
Received value: 2
Received value: 3
여러 Publisher의 최신 값을 결합하여 Tuple로 전달합니다.
특정 Publisher에서 값이 배출되면 다른 퍼블리셔의 이전 값과 엮어 방출
import Combine
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1
.combineLatest(publisher2)
.sink { print("Combined value: \($0), \($1)") }
publisher1.send("A")
publisher2.send(1)
publisher2.send(2)
publisher1.send("B")
Combined value: A, 1
Combined value: A, 2
Combined value: B, 2
combineLatest와 유사하게 동작하나,
각 publisher들에게서 새로운 값들로 Tuple이 만들어 질때만 방출
import Combine
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1
.zip(publisher2)
.sink { print("Zipped value: \($0), \($1)") }
publisher1.send("A")
publisher2.send(1)
publisher1.send("B")
publisher2.send(2)
Zipped value: A, 1
Zipped value: B, 2
Publisher가 Publisher를 발행하는 경우, 가장 최근에 발행된 내부 Publisher의 값만 구독
import Combine
let subject1 = PassthroughSubject<String, Never>()
let subject2 = PassthroughSubject<String, Never>()
let publishers = PassthroughSubject<PassthroughSubject<String, Never>, Never>()
publishers
.switchToLatest()
.sink { print("Received: \($0)") }
publishers.send(subject1)
subject1.send("First")
publishers.send(subject2)
subject1.send("Ignored")
subject2.send("Second")
Received: First
Received: Second
그 외
비동기 데이터 흐름의 타이밍을 제어하고 조절합니다.
지정된 시간동안 새로운 이벤트가 발생하지 않을 때까지 대기한 후 마지막 이벤트를 전달합니다.
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { query in
performSearch(with: query)
}
.store(in: &cancellables)
ex) 사용자 입력이 어느정도 멈추었을 때만 검색 요청
지정된 시간 간격으로 이벤트 전달
$scrollPosition
.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
.sink { position in
updateUI(for: position)
}
.store(in: &cancellables)
latest: 마지막 이벤트를 전달할 지 여부각 이벤트의 전달을 지정된 시간만큼 지연
Just("Hello")
.delay(for: .seconds(2), scheduler: RunLoop.main)
.sink { value in
showAlert(with: value)
}
.store(in: &cancellables)
다운스트림 작업(데이터 처리 등)이 지정된 스케줄러에서 실행되도록 합니다.
Publisher로부터 받은 데이터 처리를 어디서 할지를 지정
결과를 어디서 처리 할지
ex) UI에 적용될 값을 메인 스레드에서 작업
backgroundPublisher
.receive(on: RunLoop.main)
.sink { value in
updateUI(with: value)
}
.store(in: &cancellables)
업스트림 작업(데이터 생성 등)이 지정된 스케줄러에서 실행되도록 합니다.
작업을 어디서 시킬지 지정
계산을 어디서 처리할지
heavyComputationPublisher
.subscribe(on: DispatchQueue.global(qos: .background))
.receive(on: RunLoop.main)
.sink { result in
updateUI(with: result)
}
.store(in: &cancellables)
https://developer.apple.com/documentation/combine
https://blog.devgenius.io/combining-operators-in-combine-framework-37b8ba043a1d