Home [Combine 책 정리] Chatper 3: Transforming Operators
Post
Cancel

[Combine 책 정리] Chatper 3: Transforming Operators

이번 챕터는 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로 만들어줌

collect

1
2
3
4
5
6
7
8
example(of: "collect") {
    ["A", "B", "C", "D", "E"].publisher
        .collect(2) // stream을 2개씩 묶은 array로 만들어줌
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}

1
2
3
4
5
6
——— Example of: collect ———
["A", "B"]
["C", "D"]
["E"] // collect(2)가 채워지기 전에 stream이 끝나서 ["E"]로 출력됨
finished

collect(): 숫자 지정하지 않은 collect는

completion 될 때까지 무한정 array를 채울 수 있기 때문에 메모리 관리에 주의

Mapping values

map(_:)

Swift의 standard map 처럼 동작함

다운로드 (3)

1
2
3
4
5
6
7
8
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut

[123, 4, 56].publisher
    .map { formatter.string(for: NSNumber(integerLiteral: $0)) ?? "" }
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

1
2
3
4
——— Example of: map ———
one hundred twenty-three
four
fifty-six

Map key paths

1
2
3
4
5
6
7
8
9
10
11
12
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))

1
2
3
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(_:)

1
2
3
4
5
6
Just("Directory name that does not exist")
    .tryMap { try FileManager.default.contentsOfDirectory(atPath: $0) }
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

1
2
3
4
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으로 변환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

1
Hello, World!

여기서는 publisher에서 방출된 array를 단일 string으로 변경해줌

이걸로는 별로 와닿지 않는다…

다수의 upstream으로 부터 무한정 value가 전달되면 memory 이슈가 발생하게 된다

다운로드 (4)

그래서 maxPublishers에 Demand를 입력하면 되는데…

이번 챕터에서는 이것에 대한 예제가 없다ㅠ 챕터19가서 확인하라고 함

Replacing upstream output

replaceNil(with:)

optional을 특정 값으로 바꿔줌

다운로드 (5)

1
2
3
4
5
6
["A", nil, "C"].publisher
    .eraseToAnyPublisher() // Combine Bug 방어 위해 사용
    .replaceNil(with: "-") // nil -> "-"
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

1
2
3
4
A
-
C

eraseToAnyPublisher가 특이하게 끼어들어가 있는데

Combine에 현재는 버그가 있는듯: https://forums.swift.org/t/unexpected-behavior-of-replacenil-with/40800/5

위의 체인에서 eraseToAnyPublisher가 없으면 아래와 같은 결과가 나옴

1
2
3
Optional("A")
Optional("-")
Optional("C")

?? 를 쓰는거랑 replaceNil을 쓰는건 차이가 있음

?? 는 nil result를 만들 수 있음

replaceNil은 nil이 아닌 result만 만들 수 있음

만약 위의 체인에서 아래와 같이 변경한다면

1
.replaceNil(with: "-" as String?)

다운로드 (6)

이렇게 에러가 뜨게됨

replaceEmpty(with:)

upstream에서 value가 emit되지 않고 completion 되면, value를 하나 넣어주는 것

1
2
3
4
5
6
7
8
9
let empty = Empty<Int, Never>()

empty
    .replaceEmpty(with: 1)
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)


1
2
3
1
finished

Incrementally transforming output

scan(_:_:)

value를 누적해서 계산할 수 있음

다운로드 (7)

1
2
3
4
5
6
7
8
9
10
11
12
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)

다운로드 (8)

Challenge: Create a phone number lookup using transforming operators 

도전과제: 전화번호 찾기…!

  1. 10자의 숫자 또는 문자를 받음
  2. 연락처를 찾음
1
2
3
4
5
6
7
8
    input
        .map(convert)
        .replaceNil(with: 0)
        .collect(10)
        .map(format)
        .map(dial)
        .sink(receiveValue: { print($0) })

1
2
3
4
5
——— 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로 끼워넣었는데,

레고 블럭 조립하는 기분이었다.

그걸 느끼게 해주는 도전과제인듯

This post is licensed under CC BY 4.0 by the author.

[Combine 책 정리] Chapter 2: Publishers & Subscribers

Cocoa Internals: 1장 객체 (1.1 ~ 1.2)