Transforming Operators
publisher로부터 오는 값에 대해 연산을 수행하는 메서드를 Operators라고 합니다.
Cominbe operator들은 실제로 publisher를 반환합니다.
해당 publisher의 upstream 값을 수신하고 데이터를 조작한 다음 downstream으로 보냅니다.
개념적으로 간소화하기 위해 연산자를 사용하고 오류를 처리하는 게 목적이 아니라면 오류를 downstream에 게시할 뿐입니다.
collect
publisher의 개별 값 stream을 해당 값의 배열로 변환합니다.
["A", "B", "C", "D", "E"].publisher
.sink(receiveCompletion: { print($0 },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
// *--- RESULT ---*
// A
// B
// C
// D
// E
// finished
/* ----------- collect ----------- */
["A", "B", "C", "D", "E"].publisher
.collect()
.sink(receiveCompletion: { print($0 },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
// *--- RESULT ---*
// ["A", "B", "C", "D", "E"]
// finished
collect 연산자에서 특정 개수의 값까지만 받도록 지정할 수 있습니다.
["A", "B", "C", "D", "E"].publisher
.collect(2)
.sink(receiveCompletion: { print($0 },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
// *--- RESULT ---*
// ["A", "B"]
// ["C", "D"]
// ["E"]
// finished
map
publisher로부터 방출된 값에 작동한다는 점을 제외하면 Swift의 map과 동일하게 동작합니다.
["A", "p", "p", "l", "e"].publisher
.map {
Character($0).asciiValue ?? ""
}
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
// *--- RESULT ---*
// 65
// 112
// 112
// 108
// 101
map 연산자에는 key path를 사용하여 값의 속성을 하나, 둘 또는 세 개로 매핑할 수 있는 버전도 존재합니다.
struct DiceRoll {
let die1: Int
let die2: Int
}
Just(DiceRoll(die1:Int.random(in:1...6),
die2: Int.random(in:1...6)))
.map(\.die1, \.die2)
.sink { values in
print ("Rolled: \(values.0), \(values.1) (total: \(values.0 + values.1))")
}
// Prints "Rolled: 6, 4 (total: 10)" (or other random values).
tryMap
에러가 발생하면 해당 에러가 downstream으로 전송합니다.
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())"
flatMap
여러 upstream publisher를 하나의 downstream으로 플랫화하는 데 사용할 수 있습니다.
수신하는 upstream publisher와 반환하는 publisher는 동일한 유형이 아닐 수도 있습니다.
import Foundation
import Combine
var subscriptions = Set<AnyCancellable>()
struct Chatter {
public let name: String
public let message: CurrentValueSubject<String, Never>
public init(name: String, message: String) {
self.name = name
self.message = CurrentValueSubject(message)
}
}
let charlotte = Chatter(name: "Charlotte", message: "Hi, I'm Charlotte!")
let chat = CurrentValueSubject<Chatter, Never>(charlotte)
chat
.sink { print($0.message.value) }
.store(in: &subscriptions)
chat
.flatMap { $0.message }
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
flatMap은 수신된 모든 publisher의 출력을 단일 publisher로 평탄화합니다.
downstream으로 내보내는 단일 publisher를 업데이트하기 위해 전송하는 publisher 수만큼의 출력을 버퍼에 저장합니다.
Combine에서는 출력을 비동기로 처리하는데, 수신된 값들을 버퍼에 저장하여 처리 속도에 맞게 전달하면서 대량의 데이터가 동시에 도착하는 경우 메모리의 사용량이 증가할 수 있습니다.
upstream의 publisher를 maxPublishers의 값을 변경하여 제한할 수 있습니다. 기본값을 .unlimited 입니다.
2로 설정하게 되면 2개의 stream만 내보내고 P3는 무시합니다.
var subscriptions = Set<AnyCancellable>()
struct Chatter {
var name: String
var message: CurrentValueSubject<String, Never>
public init(name: String, message: String) {
self.name = name
self.message = CurrentValueSubject(message)
}
mutating func setName(_ name: String) {
self.name = name
self.message.value = "Hi, I'm \(name)!"
}
}
let charlotte = Chatter(name: "Charlotte", message: "Hi, I'm Charlotte!")
var james = Chatter(name: "James", message: "Hi, I'm James!")
let harly = Chatter(name: "harly", message: "Hi, I'm harly!")
let chat = CurrentValueSubject<Chatter, Never>(charlotte)
chat
.flatMap(maxPublishers: .max(2)) { $0.message }
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
chat.send(james)
chat.send(harly)
james.setName("Muchara")
replaceNil
nil 값을 nil이 아닌 값으로 대체할 수 있습니다. optional value가 non-optional로 변환되지 않습니다.
?? 연산자는 다른 옵션을 반환할 수 있지만, replaceNil은 반환할 수 없는 차이점이 있습니다.
["A", nil, "C"].publisher
.replaceNil(with: "-")
.map { $0! }
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
// *--- RESULT ---*
// A
// -
// C
replaceEmpty
publisher가 값을 반환하지 않고 완료되는 경우 연산자를 사용하여 값을 삽입할 수 있습니다.
let empty = Empty<Int, Never>()
empty
.replaceEmpty(with: 1)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
scan
upstream publisher가 클로저에 내보낸 현재 값과 해당 클로저가 반환한 마지막 값을 제공합니다.
reduce와 비슷하다.
var subscriptions = Set<AnyCancellable>()
var stepsPerDay: Int { .random(in: 0...10000) }
let weeks = (0..<7)
.map { _ in stepsPerDay }
.publisher
weeks
.scan(0) { latest, current in
latest + current
}
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
Key points
- publisher의 output에 대한 연산을 수행하는 메서드를 operation이라고 합니다.
- operation도 publisher입니다.
- 변환 연산자는 downstream에서 사용하기에 적합한 output으로 변환합니다.
- 메모리 문제를 피하기 위해 collect 또는 flatMap과 같이 값을 버퍼링 하는 연산자를 사용할 때는 유의해야 합니다.
- Swift 표준 라이브러리의 함수와 combine의 연산자와 다르게 작동하는 경우가 있습니다.
- 하나의 구독에 여러 연산자를 연결할 수 있습니다.
'Language > Swift' 카테고리의 다른 글
[Swift] Reactive Programming Combine - 5: Combining Operators (0) | 2024.05.11 |
---|---|
[Swift] Reactive Programming Combine - 4: Filtering Operators (0) | 2024.05.08 |
[Swift] Reactive Programming Combine - 2: Publishers & Subscribers (0) | 2024.05.04 |
[Swift] Reactive Programming Combine - 1 (1) | 2024.05.01 |
[Swift] Protocol-Oriented Programming: POP (0) | 2024.04.30 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!