이전글
- 2021/01/03 - [Reactive Programming] - [Combine 책 정리] Chapter1. Hello, Combine!
- 2021/01/06 - [Reactive Programming] - [Combine 책 정리] Chapter2. Publishers & Subscribers
이번 챕터는 Operator
Operators and publishers
- operator method는 사실 publisher를 return 함
- upstream data -> operator 에서 가공 -> downstream으로 전달
- error handling을 위한 operator가 아니면, error를 downstream으로 흘려보내줌
- (이번 챕터에서는 에러 핸들링 다루지 않음)
Collecting Values
collect()
- 개별 value -> array로 변경
- value를 버퍼에 쌓고, completion 때 array로 만들어줌
example(of: "collect") {
["A", "B", "C", "D", "E"].publisher
.collect(2) // stream을 2개씩 묶은 array로 만들어줌
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
——— Example of: collect ———
["A", "B"]
["C", "D"]
["E"] // collect(2)가 채워지기 전에 stream이 끝나서 ["E"]로 출력됨
finished
collect(): 숫자 지정하지 않은 collect는
completion 될 때까지 무한정 array를 채울 수 있기 때문에 메모리 관리에 주의
Mapping values
map(_:)
Swift의 standard 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)
——— Example of: map ———
one hundred twenty-three
four
fifty-six
Map 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))
The coordinate at (10, -8) is in quadrant 4
The coordinate at (0, 5) is in quadrant boundary
- keyPath를 통해 바로 매핑해주는 방법
- 3개까지 프로퍼티 매핑이 가능함
- .map { ($0.x, $0.y) } 보다 조금 더 간결하다는 점은 장점
tryMap(_:)
Just("Directory name that does not exist")
.tryMap { try FileManager.default.contentsOfDirectory(atPath: $0) }
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
failure(Error Domain=NSCocoaErrorDomain Code=260 "The folder “Directory name that does not exist” doesn’t exist." UserInfo={NSUserStringVariant=(
Folder
), NSFilePath=Directory name that does not exist, NSUnderlyingError=0x6000023e1ad0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}})
tryMap을 쓰면 클로저 안에서 error를 throw할 수 있음
Flattening publishers
flatMap(maxPublishers:_:_)
여러개의 publisher upstream -> single downstream으로 변환
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)
Hello, World!
여기서는 publisher에서 방출된 array를 단일 string으로 변경해줌
이걸로는 별로 와닿지 않는다...
다수의 upstream으로 부터 무한정 value가 전달되면 memory 이슈가 발생하게 된다
그래서 maxPublishers에 Demand를 입력하면 되는데...
이번 챕터에서는 이것에 대한 예제가 없다ㅠ 챕터19가서 확인하라고 함
Replacing upstream output
replaceNil(with:)
optional을 특정 값으로 바꿔줌
["A", nil, "C"].publisher
.eraseToAnyPublisher() // Combine Bug 방어 위해 사용
.replaceNil(with: "-") // nil -> "-"
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
A
-
C
eraseToAnyPublisher가 특이하게 끼어들어가 있는데
Combine에 현재는 버그가 있는듯: https://forums.swift.org/t/unexpected-behavior-of-replacenil-with/40800/5
위의 체인에서 eraseToAnyPublisher가 없으면 아래와 같은 결과가 나옴
Optional("A")
Optional("-")
Optional("C")
?? 를 쓰는거랑 replaceNil을 쓰는건 차이가 있음
?? 는 nil result를 만들 수 있음
replaceNil은 nil이 아닌 result만 만들 수 있음
만약 위의 체인에서 아래와 같이 변경한다면
.replaceNil(with: "-" as String?)
이렇게 에러가 뜨게됨
replaceEmpty(with:)
upstream에서 value가 emit되지 않고 completion 되면, value를 하나 넣어주는 것
let empty = Empty<Int, Never>()
empty
.replaceEmpty(with: 1)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
1
finished
Incrementally transforming output
scan(_:_:)
value를 누적해서 계산할 수 있음
var dailyGainLoss: Int { .random(in: -10...10) }
let august2019 = (0..<22)
.map { _ in dailyGainLoss }
.publisher
august2019
.scan(50) { latest, current in
max(0, latest + current)
}
.sink(receiveValue: { _ in })
.store(in: &subscriptions)
Challenge: Create a phone number lookup using transforming operators
도전과제: 전화번호 찾기...!
- 10자의 숫자 또는 문자를 받음
- 연락처를 찾음
input
.map(convert)
.replaceNil(with: 0)
.collect(10)
.map(format)
.map(dial)
.sink(receiveValue: { print($0) })
——— Example of: Create a phone number lookup ———
Contact not found for 000-123-4567
Dialing Marin (408-555-4321)...
Dialing Shai (212-555-3434)...
미리 만들어진 함수들을 operator로 끼워넣었는데,
레고 블럭 조립하는 기분이었다.
그걸 느끼게 해주는 도전과제인듯
'iOS > Combine' 카테고리의 다른 글
[Combine 책 정리] Chapter 5: Combining Operators (0) | 2021.01.18 |
---|---|
[Combine 책 정리] Chapter 4: Filtering Operators (0) | 2021.01.13 |
[Combine 책 정리] Chapter 2: Publishers & Subscribers (0) | 2021.01.06 |
[정리] 토비의 봄 TV - 스프링 리액티브 프로그래밍 (0) | 2021.01.05 |
[Combine 책 정리] Chapter 1: Hello, Combine! (0) | 2021.01.03 |