각 컴바인 연산자는 publisher를 반환합니다.
일반적으로 게시자는 업스트림 이벤트를 받고, 조작한 다음 조작한 이벤트를 소비자에게 다운스트림으로 보냅니다.
게시자는 개별값 또는 컬래션 값을 방출할 수 있습니다.
이 연산자는 개별 값 스트림을 단일 배열로 변환하는 편리한 방법을 제공합니다.
example(of: "collect") {
print("---none---")
["A", "B", "C", "D", "E"].publisher
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
print("---collect()---")
["A", "B", "C", "D", "E"].publisher
.collect()
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
print("---collect(2)---")
["A", "B", "C", "D", "E"].publisher
.collect(2)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
// print
——— Example of: collect ———
---none---
A
B
C
D
E
finished
---collect()---
["A", "B", "C", "D", "E"]
finished
---collect(2)---
["A", "B"]
["C", "D"]
["E"]
finished
이러한 값을 어떤 식으로든 변환시키려 하는경우가 종종 있습니다.
컴바인은 이러한 목적을 위해 여러 매핑 연산자를 제공합니다.
스위프트의 표준 map
과 동일하게 작동하지만, publisher로 방출되는 값이라는 점만 다릅니다.
example(of: "map") {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
[123, 4, 56].publisher
.map {
formatter.string(for: NSNumber(integerLiteral: $0)) ?? ""
}
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
// print
——— Example of: map ———
백이십삼
사
오십육
map연산자에는 키경로를 이용하여 1, 2 또는 3개의 프로퍼티로 매핑할 수 있는 세가지 버전이 포함되어있습니다.
map<T>(_:)
map<T0, T1>(_: _:)
map<T0, T1, T2>(_: _: _:)
struct Coordinate {
var x: Int
var y: Int
}
func quadrantOf(x: Int, y: Int) -> String {
return "어느 사분면"
}
example(of: "mapping key paths") {
let publisher = PassthroughSubject<Coordinate, Never>()
publisher
.map(\.x, \.y)
.sink(receiveValue: { x, y in
print(
"The coordinate at (\(x), \(y)) is in quadrant",
quadrantOf(x: x, y: y)
)
})
.store(in: &subscriptions)
publisher.send(Coordinate(x: 10, y: -8))
publisher.send(Coordinate(x: 0, y: 5))
}
// print
——— Example of: mapping key paths ———
The coordinate at (10, -8) is in quadrant 어느 사분면
The coordinate at (0, 5) is in quadrant 어느 사분면
이 예제에서는 키 경로를 통해서 두개의 프로퍼티로 매핑되는 map을 사용합니다.
map을 포함한 몇몇 연산자에는 throwing을 받을 수 있는 try접두사가 있는 연산자가 있습니다.
만약 오류가 발생하면, 연산자는 해당 오류를 다운스트림으로 내보냅니다.
func convert(num: String) throws -> Int {
guard let number = Int(num) else {
throw NSError(domain: "변환이 불가능한 문자열입니다.", code: 0)
}
return number
}
example(of: "tryMap") {
Just("???")
.tryMap { try convert(num: $0) }
.sink(receiveCompletion: { print("completion:", $0) },
receiveValue: { print("value:", $0) })
.store(in: &subscriptions)
}
// print
——— Example of: tryMap ———
completion: failure(Error Domain=변환이 불가능한 문자열입니다. Code=0 "(null)")
오류가 발생하면 completion failure로 종료됨
이 연산자는 여러 업스트림 publisher를 단일 다운스트림 publisher로 평평하게 하거나, 더 구체적으로는 이 publisher의 배출을 평평하게 만듭니다.
flatMap에 의해 반환된 게시자는 수신하는 업스트림 게시자와 같은 타입이 아니며 종종 그렇지 않습니다.
컴바인의 flatMap에 대한 일반적인 사용예시는 한 게시자가 방출한 요소를 게시자를 반환하는 메서드로 전달하고 궁극적으로 두번째 게시자가 방출한 요소를 구독하려는 경우입니다. (요약: Puiblisher를 반환하는 함수를 전달하고, 그 연산된 결과를 이용해서 구독하려는 경우)
example(of: "flatMap") {
func decode(_ codes: [Int]) -> AnyPublisher<String, Never> {
Just( codes
.compactMap { code in
guard (32...255).contains(code) else { return nil }
return String(UnicodeScalar(code) ?? " ")
}
.joined() )
.eraseToAnyPublisher()
}
[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]
.publisher
.collect()
.flatMap(decode)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
// print
——— Example of: flatMap ———
Hello, World!
flatMap은 수신된 모든 publisher의 출력을 단일 publisher로 평평하게 만듭니다.
이 것은 메모리 문제를 일으킬 수 있습니다.
다운스트림으로 방출되는 단일 publisher를 업데이트하기 위해서 당신이 보내는 만큼의 publisher를 버퍼링 하기때문입니다.
flatMap은 p1,p2,p3 3개의 publisher를 받습니다.
이 게시자 각각은 value
프로퍼티를 가지고있습니다.
flatMap은 P1, P2의 publisher의 값인 value
를 방출하지만, max가 2이기 때문에 P3은 무시합니다.
컴바인에는 항상 값을 전달하고자 할 때 사용할 수 있는 연산자도 포함됩니다.
옵셔널값을 받고 nil을 지정한 값으로 바꿉니다.
example(of: "replaceNil") {
["A", nil, "C"].publisher
.eraseToAnyPublisher()
.replaceNil(with: "-")
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
["A", nil, "C"].publisher
.replaceNil(with: "-")
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
// print
——— Example of: replaceNil ———
A
-
C
Optional("A")
Optional("-")
Optional("C")
혼동을 일으킬 수 있는 점이 있으므로 잘못 선택할 수 있습니다.
완전히 언래핑되는 대신에 Optional<String>
타입이 남습니다.
위의 코드는 eraseToAnyPublisher()
르를 사용하여 해당 버그를 해결합니다.
이문제는 이 링크에서 확일 할 수 있습니다. swift포럼
주의
rx처럼 nil을 대체하는 연산자를 사용하면 반환타입이 옵셔널이 제거된 형태로 나오길 기대하지만
위의 코드를 사용하면 Optional형태가 나옴 (버그인듯)
업스트림에 eraseToAnyPublisher
를 사용하여 제거해야함
publisher가 값을 방출하지않고 완료되면 값을 바꾸거나 실제로 값을 삽입 할 수 있습니다.
아래의 마블 다이어그램을 보면, 게시자는 아무것도 방출하지않고 완료하고 그 시점에서 replaceEmpty(with:)연산자는 값을 삽입하여 다운 스트림으로 publish합니다
example(of: "replaceEmpty(with:)") {
let empty = Empty<Int, Never>()
empty
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
empty
.replaceEmpty(with: 1)
.sink(receiveCompletion: { print("replace:", $0) },
receiveValue: { print("replace:", $0) })
.store(in: &subscriptions)
}
// print
——— Example of: replaceEmpty(with:) ———
finished
replace: 1
replace: finished
Empty publisher타입을 사용해서 즉시 .finished 완료이벤트를 방출하는 publisher를 만들 수 있습니다.
또한 completeImmediately
프로퍼티에 false를 설정해서 아무것도 방출하지 않도록 할 수 있습니다. 기본값은 true입니다.(false로 설정하면 Never
publisher와 같음 )
컴바인에는 업스트림 게시자로부터 받은 값을 조작할 수 있는 몇가지 트릭이 있습니다.
업스트림 게시자가 클로저에서 방출한 현재 값과 해당 클로저에서 반환한 마지막값을 제공합니다.
다이어그램에서 scan은 0을 저장하고 시작합니다.
게시자로부터 값을 수신할때마다 이전에 저장된 값에 추가한다음 결과를 저장하고 방출합니다.
(초기값을 지정하고 현재값과 이전값을 연산한 결과를 방출함)
example(of: "scan") {
var dailyGainLoss: Int { -2 }
let august2019 = (0..<22)
.map { _ in dailyGainLoss }
.publisher
august2019
.scan(50) { latest, current in
max(0, latest + current)
}
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
//print
——— Example of: scan ———
48
46
44
42
40
38
36
34
32
30