Git Product home page Git Product logo

hello-combine-with-udemy's Introduction

Hello-Combine-with-Udemy

Combine framework study with Udemy lecture


Hello, Combine with Udemy 스터디 목차


스터디 계획

Udemy 강의를 활용하여 Combine framework 스터디 예정

  • 매주 다수결로 스터디 날짜 지정 후, 2시간 스터디 진행

1주차 스터디

2주차 스터디

3주차 스터디

4주차 스터디

  • 크리스마스 1주일은 쉽니다. 4주차까지의 스터디 내용을 복습 및 보완할 시간 가지기.

5주차 스터디


스터디 방식

  • Udemy 강의를 기반으로 한 스터디 내용을 스터디원 별로 공유 및 토론하며 공용 문서를 정리

  • 각자 학습한 내용은 공용 study git repository에 공유

    • 각자 적당한 의무감을 갖고 스터디하기 위함
  • 개인적으로 공부한 내용은 개인폴더에 기록 (fork 후, 개인 스터디 내용 기록 했다가 공용 repository에 PR)

  • 스터디 주차마다 멤버가 한 명씩 스터디 기록 담당을 돌아가며 맡으며 readme.md 파일의 내용을 보완 및 서로 리뷰

    • 현재 readme.md 내용은 기존 강의 공부한 스터디장이 개인적으로 기록한 내용을 옮겨 놓은 것
    • 스터디 기록 정리 방식에 대한 좋은 의견 자유롭게 공유
  • Udemy 강의 스터디 중에 질문사항이 있으면 채팅을 통해 알려주고, 바로잡고 가기


스터디에서 나온 질문 리스트

  • Combine과 KVO의 연관성은?
  • eraseToAnyPublisher의 역할?
    • Publisher 연산 결과를 추상화, 내부 연산과정은 숨기고, 결과 타입만 외부에 노출시킬때 사용, 이후 중간 연산 과정의 변화에 대한 외부 영향도가 줄어듦
  • share, multicast operator의 역할, 차이점
    • share는 내부적으로 autoconnect()를 사용한다, multicast는 구독 값을 받기 위해 connect()를 사용해주어야 한다.


Section 1: Introduction

1-5. What is functional Programming

  • Imparative(명령형) 프로그래밍과 달리, functional(함수형) 프로그래밍은 상태가 바뀌는 값이 immutable하게 구성될 수 있다.
  • 기존에 사용되던 명령형 프로그래밍과 달리, 함수형 프로그래밍은 immutable 하다는 차이점이 있다. 명령형 프로그래밍은 변수를 선언하고, 특정 위치에서 변수가 변경된다. 이런 mutable state는 Concurrency, Dead Locks, Race Conditions 등에서 문제를 야기할 가능성이 크다.
  • 함수형 프로그래밍은 명령형 프로그래밍과 달리 immutable하게 값이 정의될 수 있다. 함수형 프로그래밍에서는 filter, map, reduce 등 다양한 1급객체의 함수들을 필요에 따라 함께 사용하게 된다.
    • Pure Functions
      • function은 항상 같은 input에 대해 동일한 output을 생산한다.
      • function은 side effects를 만들지 않는다.

1-6. What is Combine Framework?

1-7. RxSwift vs Combine

Section 2: Publishers, Subscribers and Operators

2-8. Hello Publishers and Subscribers

Publishers

  • Publisher는 이벤트를 방출할 수 있다. (RxSwift의 Observable과 유사)
  • Output, Failure 연관 타입을 갖는다.
  • Publisher 프로토콜을 채택하여 정의할 수 있다.
  • Subscriber로부터 구독을 당할 수 있다. 구독을 당할때 Subscription을 만들어서 Subscriber에게 전달한다. 이후 구독이 해지되기 전까지 구독자에게 이벤트를 전달한다.

Subscribers

  • Publisher, Subject를 구독할 수 있다. (RxSwift의 Observer와 유사)
  • Input, Failure 연관 타입을 갖는다.
  • 구독을 했을때 Subscription을 받는 메서드, 구독 간 이벤트를 받는 메서드, 구독이 끝났을때 completed 메서드가 정의된다.
  • Apple에서는 Custom Subscriber의 사용을 권장하지 않는 것으로 알고 있다.
    • assign, sink 등을 사용해서 Custom Subscriber 구현 없이 Apple에서 기본적으로 제공하는 Subscriber를 사용 가능하다.
  • Subscriber가 Publisher를 구독하면, Publisher는 데이터 이벤트를 Subscriber에게 전달합니다.
    • Publisher -----> Stream of Data -----> Subscriber
    • 구독 시 Publisher는 Subscriber에게 subscription을 전달한다. 이때 Subscriber는 얼마나 이벤트를 받을지 요청할 수 있다.
    • Publisher는 요청받은 만큼의 이벤트를 Subscriber에게 전달한다. Subscriber는 이벤트 값을 받으면 이에 맞는 request를 하거나 그냥 받기만 할 수 있다.
    • 구독이 종료되면, Subscriber는 completion 이벤트를 받는다.

2-9. Sending Notifications Using Publisher and Subscriber

  • Combine의 Publisher는 구독(sink)이 가능하다. 클로져를 통해 구독한 Publisher의 이벤트를 수신 받을 수 있다.
    • Combine의 Publisher는 RxSwift의 Observable와 유사, (둘 다 Subject, Subscriber(RxSwift Observer)를 가짐)
  • sink는 AnyCancellable 타입을 반환한다. RxSwift의 subscribe가 Disposable을 반환하는 것과 유사하다.
  • RxSwift, Combine의 구독 예시
let disposable = NotificationCenter.default.rx.notification(notification)
      .observe(on: MainScheduler.instance)
      .subscribe(onNext: { _ in
        print("RxSwift Notification received")
      }).disposed(by: disposeBag)

let publisher = NotificationCenter.default.publisher(for: notification, object: nil)
let subscription = publisher.sink { _ in
  print("Combine Notification received")
}

// 구독이 시작되면, 구독 해지 이전까지 Publisher의 이벤트를 받을 수 있다.
NotificationCenter.default.post(name: notification, object: nil)

2-10. Understanding Cancellable

  • New Paper를 예로 들어보자. 구독을 하다가 취소를 해야 신문을 그만 볼 수 있다.
  • 구독 취소 후의 Observable, Publisher 이벤트는 받을 수 없다.
  • 구독 해지를 할때 구독을 통해 반환받은 AnyCancellable을 갖고 있다가 중간에 nil을 할당하거나, 사용 영역 생애주기가 끝나면 자동으로 해제된다. (Cancellable은 cancel()로 해제해주어야 함), 혹은 store에 Set 인스턴스를 넣어서 구독정보 관리가 가능하다.

2-11. Subscriber

  • Publisher의 이벤트를 감지하기 위해서는 구독이 필요했다. 그 구독을 하는 것이 Subscriber이다. Subscriber는 Input, Failure 연관 타입을 갖고 있는데, Publisher의 Output과 Subscriber의 Input, 양쪽의 Failure가 일치해야 구독을 할 수 있다
  • Publisher와 Subscriber의 구독 후 상호관계 feat. Subscriber 프로토콜을 채택한 StringSubscriber 생성
class StringSubscriber: Subscriber {
  // Publisher            Subscriber 의 상호 관계
  // <-------- subscribes
  //         gives subscription -->
  // <-------- requests values
  //         sends values -------->
  //         sends completion ---->
  
  // subscribe 이후, publisher로부터 subscription을 수신했을때 호출 됩니다.
  func receive(subscription: Subscription) {
    print("Received Subscription")
    subscription.request(.max(3)) // publiser야 최대 3개의 값만 줘봐
  }
  
  // publisher로부터 input 값을 수신했을때 호출됩니다.
  func receive(_ input: String) -> Subscribers.Demand {
    print("Received value : \(input)")
    return .none // publisher한테 더이상 받을 것 없어
//    return .unlimited // 줄 수 있는거 다 받을래
  }
  
  // publisher가 publish 이벤트를 마쳤을때 호출 됩니다.
  func receive(completion: Subscribers.Completion<Never>) {
    print("Completed")
  }

  typealias Input = String
  typealias Failure = Never // Failure를 Never로 지정하면 fail이 발생하지 않습니다.
}
  • Custom Subscriber로 Publisher 구독 후 사용하기
let publisher = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"].publisher
let subscriber = StringSubscriber()
publisher.subscribe(subscriber)
// 출력 결과 
// -> .max(3)으로 request를 했으므로 "A", "B", "C" 세개의 이벤트를 받습니다.
// * unlimited로 input을 받는 경우, 모든 이벤트를 받습니다.
// * 요청하고 싶지 않으면 .none을 반환하면 됩니다.
/*
Received Subscription
Received value : A
Received value : B
Received value : C
 */

2-12. Subjects

  • Publisher 이면서 Subscriber이다. (RxSwift의 Observable이면서 Subscriber인 Subject와 유사)
  • 초기값이 없는 PassthroughSubject, 초기값을 갖는 CurrentValueSubject가 있다. (RxSwift의 PublishSubject, BehaviorSubject와 유사)
  • RxCocoa에서 제공하는 PublishRelay, BehaviorRelay와 같은 Subject는 제공하지 않아 사용을 위해서는 Custom 구현을 해야 한다. (Failure 연관 타입을 Never로 지정)
  • 원하는 시점에 send를 통해 구독자들을 위한 이벤트를 방출할 수 있다.
let subscriber = StringSubscriber()
let subject = PassThroughSubject<String, MyError>()
// Subject는 Publisher처럼 구독이 가능합니다. 즉, Subscriber가 구독하여 이벤트를 받을 수 있으며, Subject에서 원할 때 이벤트를 Subscriber로 보낼 수 있습니다.

// 1) Subscriber는 Subject를 구독할 수 있습니다.
subject.subscribe(subscriber)

// 2) Subject는 Publisher로서 이벤트를 방출할 수도 있습니다.
let subscription = subject.sink { completion in 
  print("Received Completion from sink")
} receiveValue: { value in
  print("Received Value from sink, value: \(value)")
}

subject.send("A") // Received value : A
subject.send("B") // Received value : B
subject.send("C") // .max(2)로 request를 했을 경우, 세번째 이벤트인 C는 방출되지 않습니다. input을 받을때 .none 대신 .max(1)을 반환하면 이벤트를 추가로 받을 수 있다.
subject.send("B")

subscription.cancel()

subject.send("ㅜㅜ") // 구독이 취소되면 이후 이벤트는 방출되지 않아요.
subject.send("ㅠㅠ")

2-13. Type Eraser (eraseToPublisher)

  • 사용한 Publisher 연산 결과의 타입을 가리고 싶을때 Type Eraser로서 eraseToPublisher를 사용할 수 있다.
  • eraseToAnyPublisher를 사용하면 AnyPublisher타입으로 바뀐다. (기존 Publisher 결과타입을 래핑한다.)
  • 다양한 operator를 가져가는 경우 타입이 매우 복잡해지고, 파이프라인이 모두 외부에 노출되는 문제가 있다.
    • 이때 eraseToAnyPublisher를 사용하면 기존의 데이터 스트림과 상관없이 최종적인 형태의 Publisher를 반환한다. 최종적으로 받게 되는 데이터를 전달하는 목적으로만 타입을 변환하여 사용할 수 있다.
let publisher = PassthroughSubject<Int, Never>() // PassthroughSubject<Int, Never>
  .map { $0 } // Publisher.Map<PassthroughSubject<Int, Never>, Int>
  .eraseToAnyPublisher()
// => AnyPublisher<Int, Publishers.Map<PassthroughSubject<Int, Never>, Int>.Failure>

Section 3: Transforming Operators

3-14. Understandinig Transforming operators

  • 기존 Sequence를 각각의 element에 대해 특정 연산을 적용한 새로운 Sequence로 변환시킨다.
// ex) [1, 2, 3] -> ["1", "2", "3"]
// 필요에 따라 사용가능한 다양한 Transformation Operator가 존재한다.

3-15. collect operator

  • collect operator는 방출할 모든 이벤트를 하나로 모아놓은 Array로 반환한다.
  • collect N : Int 인자를 넣으면 N개 단위로 나누어서 Array를 반환한다.
let anyCancellable = ["A", "B", "C", "D"].publisher.collect(3)
  .sink { element in 
  print(element)
}

anyCancellable.cancel()

3-16. map operator

// MARK: 16. map operator
// ex) [100, 23] -> ["one hundred and twenty three"]로 변환하는 방법?

let formatter = NumberFormatter()
formatter.numberStyle = .spellOut

[213, 45, 67].publisher.map {
  // map operator를 통해 Sequence에 있는 각각의 elements를 특정 연산하여 또다른 Sequence를 반환할 수 있다.
  formatter.string(from: NSNumber(integerLiteral: $0))
}.sink { element in
  print(element)
}

3-17. map with keyPath

// MARK: 17. map KeyPath
// map operator에서 KeyPath를 사용하여 structure의 개별 값들을 접근하여 다룰 수 있습니다.

struct Point {
  let x: Int
  let y: Int
}

let publisher = PassthroughSubject<Point, Never>()
publisher.map(\.x, \.y).sink(receiveValue: { x, y in
  print("x is \(x) and y is \(y)")
})

publisher.send(Point(x: 1, y: 2))
publisher.send(Point(x: 10, y: 20))

3-19. replaceNil operator

  • publisher sequence에 nil이 있을 경우 nil을 특정 값으로 변환한 sequence를 반환합니다.
// MARK: 19. replaceNil operator
// replaceNil : publiser sequence에 nil이 있을 경우 nil을 특정 값으로 변환한 sequence를 반환합니다.
// MARK: 20. Challenge - Unwrapping the Optional Values Received from replaceNil
// Q. replaceNil이 반환하는 [String?] 타입 대신 [String] 타입이 내려오게 하는 방법은?
// 1) map { $0! } 을 사용하여 언래핑을 할 수 있다. 강제 옵셔널 언래핑은 안전하지 않은 방법이다. 하지만 replaceNiil을 통해 nil인 값을 다른 값으로 바꾸었기 때문에 정상적으로 언래핑 됨. (그냥 아니면 compactMap 쓰면 됨)
["A", "B", nil, "C"].publisher.replaceNil(with: "x")
  .map { $0! }
  .sink {
    print($0)
  }

3-22. replaceEmpty operator

// MARK: 22. replaceEmpty operator
// Empty<Int, Never> Publisher는 어떠한 값을 방출하지 않으며, 에러또한 방출하지 않습니다.
let empty = Empty<Int, Never>()
//let cancellable = [1, 2, 3, 4, 5].publisher.sink { print($0) }
//cancellable.cancel()

empty
  .replaceEmpty(with: 1) // replaceEmpty operator를 통해 Empty Publisher의 값을 특정 값으로 바꾸어 구독자에게 전달 가능
  .sink(receiveCompletion: {
  print($0) // 1, finished
}, receiveValue: {
  print($0)
})

3-23. scan operator

  • reduce와 사용방식이 유사합니다. 차이점이라면, reduce는 Sequence 연산의 결과 하나만 down stream으로 내려주지만, scan은 Sequence 각 Element 연산 결과를 모두 반환합니다.
// MARK: 23. scan operator
// RxSwift의 scan와 이름이 동일하고 기능도 유사한 operator로 Sequence의 연산 결과를 모두 반환한다.
let publisher = (1...10).publisher
publisher.scan([]) { numbers, value -> [Int] in
// numbers: [Int]에 연산이 누적된다., value: Int 는 publisher의 각각의 element
  return numbers + [value] // publisher 값을 순차적으로 append 하고 있다.
}.sink { scanValue in
  print(scanValue) // scan operator의 appending 연산 과정이 모두 출력된다.
}
/*
// Scan Operator Output
[1]
[1, 2]
[1, 2, 3]
...
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
*/

Section 4: Filtering Operators

4-24. filter operator

  • 특정 조건을 충족하는 Element들을 down stream에 전달합니다.
// MARK: - Section 4. Filtering Operators
// MARK: 24. filter operator
// RxSwift의 filter와 동일하다. 기존 Sequence를 특정 조건을 충족하는 값만 있는 Sequence로 반환한다.
let numbers = (1...20).publisher
numbers.filter { $0 % 2 == 0 }.sink(receiveValue: {
  print($0) // (1...20) 값들 중 짝수값만 출력된다.
})

4-25. removeDuplicates operator

  • Sequence에서 연속적으로 중복되는 이벤트를 무시하고, down stream에 전달합니다.
// MARK: 25. removeDuplicates operator
// removeDuplicates operator를 사용하면 Sequence의 중복값을 제거한 Sequence로 반환받을 수 있다.
// removeDuplicates를 사용할때 모든 중복값이 제거되는 것은 아니다. Sequence에서 연속된 중복값만 한하여 무시하여 필터링한다.
// 중복 문자열이 있는 배열에 대한 publisher를 선언한다.
// 1) 아래 String 문자열은 components(separatedBy:) 에 의해 [String] 타입으로 변환
// 2) String Sequence Publisher에 대한 removeDuplicates 연산으로 2번째 apple은 연속적으로 중복되므로 무시됩니다.
let words = "apple apple fruit apple mango watermelon apple".components(separatedBy: " ").publisher
  .removeDuplicates()
words.sink {
  print($0)
}

/*
// Output
apple
fruit
apple
mango
watermelon
apple
*/

4-26. compactMap operator

  • compactMap operator는 map과 유사한 동작을 하지만 연산 결과가 non-optional인 값만 모아서 Sequence로 변환하는 차이점이 있다. 즉, compactMap operator는 non-optional Sequence만 반환한다.
let strings = ["a", "1.24", "b", "3.45", "6.7"]
  .publisher.compactMap { Float($0) }
  .sink {
    // Float 타입으로 변환 가능한 1.24, 3.45, 6.7만 down stream으로 내려옵니다. map을 사용했다면, 변환이 불가능한 원소는 nil로 내려옵니다.
    print($0) 
  }

4-27. ignoreOutput operator

  • ignoreOutput operator는 Empty Publisher를 반환하는 operator입니다.
  • completion event만 받고 그 이외의 이벤트는 무시하고자 할 때 사용 가능합니다.
let numbers = (1...5000).publisher
numbers
  .ignoreOutput() // ignoreOutput operator를 사용하면 completion 이벤트만 받고 이외의 이벤트는 무시합니다.
  .sink {
  print($0) // finished Completion만 전달 받습니다.
} receiveValue: {
  print($0) // 1...5000의 값은 출력되지 않습니다.
}

4-28~29. first, last operator

  • first operator는 Sequence의 첫번째 혹은 특정 조건에 맞는 첫번째 값을 방출할때 사용할 수 있습니다.

    • 단순 첫번째 값 방출에는 first(), 조건 추가를 위해서는 first(where:) 사용
  • last operator는 Sequence의 마지막 혹은 특정 조건에 맞는 마지막 값을 방출할때 사용할 수 있습니다.

    • 단순 마지막 값 방출에는 last(), 조건 추가를 위해서는 last(where:) 사용
// MARK: 28. first operator
// first operator는 Sequence의 첫번째 혹은 특정 조건에 맞는 첫번째 값을 방출할때 사용할 수 있습니다.
// MARK: 29. last operator
// last operator는 Sequence의 마지막 혹은 특정 조건에 맞는 마지막 값을 방출할때 사용할 수 있습니다.
let numbers = (1...9).publisher

numbers.first(where: { $0 % 2 == 0 }) // 짝수인 첫번째 값을 방출
  .sink {
    print($0) // 2 (sequence publisher의 첫번째 홀수 값
  }

numbers.last(where: { $0 % 2 == 1 }) // 홀수인 마지막 값을 방출
  .sink {
    print($0) // 9 (sequence publisher의 마지막 홀수 값)
  }

4-30~32. dropFirst / drop(while:) / drop(untilOutputFrom:) operator

  • dropFirst는 Sequence에서 최초 N개의 이벤트를 무시하고자 할때 사용할 수 있다.

  • dropWhile은 특정 조건을 충족하는 동안 이벤트를 무시하고자 할때 사용한다.

    • 최초 조건을 미충족하는 시점부터 다시 이벤트를 방출한다.
  • dropUntilOutputFrom은 trigger용 Publisher로부터 이벤트를 받기 전까지 이벤트를 무시할 수 있다.

    • untilOutputFrom 레이블 인자로 넣은 Publisher가 이벤트를 방출하기 전까지 이벤트가 무시됩니다.
// MARK: 30. dropFirst operator
// dropFirst는 Sequence에서 최초 N개의 이벤트를 무시하고자할때 사용가능하다.
let numbers = (1...10).publisher
numbers.dropFirst(5)
  .sink {
    print($0)
  }

// MARK: 31. dropWhile operator
// dropWhile은 Sequence에서 특정 조건을 충족하는 동안은 이벤트를 무시하고 조건에 부합되지 않는 이벤트부터 이벤트를 방출한다.
let numbers = (1...10).publisher
numbers.drop(while: { $0 != 3 }) // 1, 2는 3이 아니므로 무시, 3부터 이벤트가 방출
  .sink {
    print($0)
  }

// MARK: 32. dropUntilOutputFrom operator
// dropUntilOutputFrom operator는 특정 publisher(untilOutputFrom의 인자)로부터 이벤트를 받기 전까지 이벤트를 무시한다.
let taps = PassthroughSubject<Int, Never>() // 이벤트 구독 감지할 taps subject
let isReady = PassthroughSubject<Void, Never>() // trigger용 isReady subject
taps.drop(untilOutputFrom: isReady)
  .sink(receiveValue: {
    print($0)
  })
// isReady publisher가 이벤트를 방출하기 전까지 taps subject의 이벤트는 무시됩니다.
// isReady subject(publisher)가 이벤트를 방출한 이후부터 tap subject의 이벤트가 방출됩니다.
(1...10).forEach { n in
  if n == 6 { isReady.send(()) } // isReady subject에서 이벤트를 방출 하는 시점 부터 taps subject로부터 이벤트를 받음
  taps.send(n) // isReady가 이벤트를 방출한 이후부터 tap subject(publisher)는 이벤트를 방출, 구독 값 수신이 가능
}

4-33. prefix, prefix(while:)

  • prefix operator는 Sequence의 첫번째부터 N개의 이벤트만 방출하도록 할때 사용합니다.
  • prefix(while:) operator는 특정 조건을 충족하지 않는 이벤트가 나오기 전까지의 prefix event를 방출합니다.
    • 최초 조건을 미충족하기 이전 까지 event를 방출합니다.
// MARK: 33. prefix(_:), prefix(while:) operator
let numbers = (1...10).publisher
print("What is the prefix operator in Combine framework?")
numbers
  .prefix(3) // 첫번째 부터 3개의 이벤트만 방출
  .sink { element in
    print(element) // 1, 2, 3
  }

numbers
  .prefix(while: { $0 % 3 != 0 }) // 3으로 나눈 나머지가 3이 아닌 동안 방출
  .sink {
    print($0) // 1, 2
  }

4-34~35. Challenge: Filter all the things

🐵 operator exercise

let publisher = [1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 1]
      .reduce(into: Set<Int>()) { result, value in // 중복 제거
        result.insert(value)
      }
      .sorted() // 중복 제거 후 오름차순 정렬
      .publisher // Publisher 변환 후 구독 진행
      .sink { value in
        print(value) // 1, 2, 3 수신
      }

🐵 operator exercise2

  • Challenge: Filter all the things with solution (dropFirst + prefix + filter)
/*
Challenge: Filter all the things

Create an example that publishes a collection of numbers from 1 through 100, and use filtering operators to:

1. Skip the first 50 values emitted by the upstream publisher.
2. Task the next 20 values after those first 50 values.
3. Only task even numbers.

The output of your example should produce the follwing numbers, one per line:
*/

let publisher = (1...100).publisher
publisher
  .dropFirst(50) // or, drop(while: { $0 <= 50 }), 1) 처음 50개의 이벤트는 무시합니다.
  .prefix(20) // 50개 이벤트 버린 후, 처음 20개의 이벤트는 방출합니다.
  .filter({ $0 & 1 == 0 }) // 방출하는 20개 이벤트 중, 짝수만 방출합니다.
  .sink(receiveValue: {
    print($0)
  })

Section 5: Combining Operators

5-36~37. prepend, append operator

  • prepend
    • prepend operator는 append의 반대로 Sequence 앞에 이벤트를 추가시킬 때 사용합니다.
    • 단일 event 뿐만 아니라 Sequence publisher를 인자로 넣어서 사용할 수도 있습니다.
// MARK: 36. preappend operator
let numbers = (1...5).publisher
let publisher2 = (500...510)
let publisher3 = [0].publisher
numbers
  .prepend(-20, -30) // -20, -30, 1, 2, 3, 4, 5
  .prepend(100, 200, 300) // 100, 200, 300, -20, -30, 1, 2, 3, 4, 5
  .prepend(publisher2) // 500, 501, ... 510, 100, 200, 300, -20, -30, 1, 2, 3, 4, 5
  .prepend(publisher3) // 0, 500, 501, ... 510, 100, 200, 300, -20, -30, 1, 2, 3, 4, 5
  .sink {
    print($0) // 0, 500, 501, ... 510, 100, 200, 300, -20, -30, 1, 2, 3, 4, 5
  }
  • append
    • append operator는 prepend와 반대로 Sequence 끝에 이벤트를 추가할 때 사용합니다.
    • prepend처럼 다른 publisher를 append operator 인자로 사용 가능합니다.
// MARK: 37. append operator
let numbers = (1...10).publisher
let publisher2 = (100...101).publisher
let publisher3 = [-1].publisher
numbers
  .append(99, 98, 97)
  .append(-30, -20, -10)
  .append(publisher2)
  .append(publisher3)
  .sink {
  print($0)
}

5-38. switchToLatest operator

  • PassthroughSubject를 Output으로 갖고 있는 publishers Subject가 있다고 봅시다.
let publishers = PassthroughSubject<PassthroughSubject<String, Never>, Never>()
  • publishers Subject에서 switchToLatest를 사용하면, 이후, publishers Subject가 가장 최근에 방출한 PassthroughSubject에 대한 이벤트만 수신 받을 수 있습니다.
// MARK: 38. switchToLatest operator
// switchToLatest operator는 가장 최근 방출한 publisher에 대한 이벤트를 받고자할때 사용합니다.
// ex) 가장 최근에 publisherA 이벤트 방출했다면, publisherA가 방출한 이벤트만 받는다.
let publisher = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<String, Never>()
let publishers = PassthroughSubject<PassthroughSubject<String, Never>, Never>()
publishers.switchToLatest().sink {
  print($0)
}

// publisher를 방출하면 publisher가 방출하는 이벤트만 수신 가능하다.
publisher.send("Publisher - A")
publishers.send(publisher) // switching to publisher
publisher.send("Publisher - B")
publishers.send(publisher2)
// publishers가 가장 최근에 publisher2를 방출했으므로 publisher2가 아닌 publisher에서 방출된 이벤트는 수신하지 못한다.
publisher.send("Publisher - C") // switcing to publisher2
// publishers에서 가장 최근 방출된 publisher2에 대한 이벤트를 수신 가능하다.
publisher2.send("Publisher2 - A")
publisher2.send("Publisher2 - B")

5-39. switchToLatest operator usecase

// MARK: 39. switchToLatest continued
// switchToLatest operator에 대한 실 사용 예시를 알아보자.
// switchToLatest operator를 활용하면 버튼을 탭하고, 탭 이벤트 이후 이미지를 요청해서 받아올때, 가장 최근에 선택한 index(상태)에 대한 이미지를 불러올 수 있다.
let images = ["Houston", "Denver", "Seattle"]
var index = 0

func getImage() -> AnyPublisher<UIImage?, Never> {
  print("getImage calling")
  return Future<UIImage?, Never> { promise in // future를 사용하면 클로져 내에서 결과값을 방출할 수 있다.
    print("getImage promise closure")
    DispatchQueue.global().asyncAfter(deadline: .now() + 3.0) {
      print("image callback fired")
      promise(.success(UIImage(named: images[index]))) // 비동기적으로 약 3초 후 선택된 인덱스에 맞는 이미지를 콜백으로 전달한다.
    }
  } // -> Future<UIImage?, Never>
  .map { $0 } // -> UIImage?
  .receive(on: RunLoop.main)
  .eraseToAnyPublisher() // -> AnyPublisher<UIImage?, Never>
}

let taps = PassthroughSubject<Void, Never>() // 버튼 탭 예시로 사용되는 subject publisher
let subscription = taps.map { _ in getImage() }
  .print()
  .switchToLatest().sink {
    print($0)
  }

// getImage 메서드는 3초뒤 이미지를 전달한다.
// 1) 첫번째 taps.send() 발생 3초 뒤, index는 그대로 0이므로 subscription을 통해 houston에 대한 UIImage를 이벤트로 받습니다.
taps.send()
// => 3초 뒤 0번째 인덱스의 이미지를 받음

// 2) 6초 뒤에 실행되는 A 비동기 코드
DispatchQueue.main.asyncAfter(deadline: .now() + 6.0, execute: {
  // 3) 6초 뒤, index += 1 후 index는 1이 된다.
  // 4) index가 1이 되고 tap 이벤트가 발생한다. 3초 뒤 이미지를 받을 것이다.
  index += 1
  taps.send()
})

// seattle index(2)일때는 6.5초 뒤에 이벤트를 보낸다. 실질적으로는 6초뒤 실행되었던 위 코드 실행 0.5초 뒤 실행된다.
DispatchQueue.main.asyncAfter(deadline: .now() + 6.5, execute: {
  // 5) 6.5초 뒤(A 비동기 코드 실행 0.5초 후) index가 한번 더 증가한다. index == 2 이다.
  // 6) A 비동기 코드에서 발생한 tap 이벤트에 대한 getImage 콜벡을 수신한다. 이때 index는 2이므로, Denver가 아닌 Seattle에 대한 이미지를 받게된다.
  // => index가 1인 시점에서 A 비동기 코드에 의해 getImage 메서드를 호출했지만, image 콜벡을 받는 3초 동안 이미 index가 2로 바뀌었기 때문에, index == 2 이미지인 Seattle 이미지를 이벤트로 받게 된다.
  //
  index += 1
  taps.send()
})

// Denver가 아닌 최근 index에 대한 이미지인 Seattle 이미지를 받게 된다. 이처럼 switchToLatest operator는 가장 최근 상태에 대한 이벤트를 받고 싶을때 사용할 수 있다.

5-40. merge operator

  • merge operator는 여러개의 publisher를 합칠 수 있고, 시간 순으로 합친 publisher들의 이벤트를 받을 수 있다.
// MARK: 40. merge operator
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
publisher1.merge(with: publisher2).sink {
  print($0)
}

// merge로 합친 여러개의 subject publisher에 대한 이벤트를 시간 순으로 수신할 수 있다.
// Output : 1, 2, 4, 6, 7, 8
publisher1.send(1)
publisher1.send(2)

publisher2.send(4)
publisher2.send(6)

publisher1.send(7)
publisher1.send(8)

5-41. combineLatest operator

  • combineLatest는 RxSwift의 동일 이름 연산자와 동작이 모두 유사하다.
  • publisher들의 가장 최근 값들을 방출한다. (최소 한번씩은 방출이 되어야 쌍으로 방출이 됨)
    • 활용 가능한 케이스 : 모든 텍스트 필드를 채운 이후에 각각의 텍스트 상태를 감지하고 싶을때
// MARK: 41. combineLatest operator
// combineLatest는 RxSwift와 이름 동작이 모두 유사합니다.
// 1) 두개의 publisher 최신 값을 이벤트로 방출합니다.
// 2) 둘 중 어느 하나의 이벤트가 방출될때마다 각 publisher의 최신값을 방출합니다.
// 3) 서로 다른 값 타입의 publisher들에 대해서도 combineLatest operator를 사용하여 최신 이벤트를 전달받을 수 있습니다.
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
publisher1.combineLatest(publisher2)
  .sink {
    print("P1: \($0), P2: \($1)")
  }
publisher1.send(1) // nothing
publisher1.send(2) // nothing

publisher2.send("A") // 2, "A"
publisher2.send("B") // 2, "B"

publisher1.send(3) // 3, "B"

5-42. zip operator

  • zip operator는 각각의 publisher에 대한 동일 순서의 이벤트를 튜플로 묶어서 방출합니다.
  • 만약 동일 순서의 이벤트 쌍을 맞추지 못한다면, 방출되지 않습니다.
// MARK: 42. zip operator
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
publisher1.zip(publisher2)
  .sink {
    print("P1: \($0), P2: \($1)")
  }
publisher1.send(1) // 이벤트 없음
publisher1.send(2) // 이벤트 없음

publisher2.send("A") // 2, "A", publisher1, publisher2의 첫번째 이벤트가 튜플 형태로 방출
publisher2.send("B") // 2, "B", publisher1, publisher2의 두번째 이벤트가 튜플 형태로 방출

publisher1.send(3) // 쌍을 이루지 못했으므로, 해당 값은 방출되지 않음
// Output
/*
P1: 1, P2: A
P1: 2, P2: B
*/

Section 6: Sequence Operators

6-43. min, max operator

  • Sequence의 최솟값, 최댓값을 방출
// MARK: - Section 6. Sequence Operators
// MARK: 43. min and max operator
// Sequence operators는 쉬운 편에 속합니다. publisher 자기 자신의 값에 대한 연산이기 때문입니다.
// min, max : Sequence publisher의 최숏값, 최댓값을 방출한다.
let publisher2 = [1, -45, 3, 35, 30, 100].publisher
publisher2.min().sink {
  print($0) // -45
}

publisher2.max().sink {
  print($0) // 100
}

6-44. first, last operator

// MARK: 44. first and last
// first, last operator는 Sequence publisher의 처음, 마지막 이벤트 or 특정 조건을 충족하는 처음, 마지막 이벤트를 방출할 대 사용한다.
let publisher = ["A", "B", "C", "D", "Bo", "Ba"].publisher

publisher.first().sink { // Sequence publisher의 첫번째 이벤트를 방출
  print($0) // "A"
}

publisher.first(where: { "Cat".contains($0) }).sink { // 특정 조건을 충족하는 첫번째 이벤트를 방출할 수도 있다.
  // "Cat" 문자열에 포함되는 첫번째, "C"를 수신한다.
  print($0) // "C"
}

publisher.last().sink { // Sequence publisher의 마지막 이벤트를 방출
  print($0) // "Ba"
}

publisher.last(where: { "Boy".contains($0) }).sink { // 특정 조건을 충족하는 마지막 이벤트를 방출할 수도 있다.
  // "Boy" 문자열에 포함되는 마지막, "Bo"를 수신한다.
  print($0) // "Bo"
}

6-45. output operator

  • output operator는 Sequence publisher의 특정 인덱스 혹은 특정 범위의 이벤트를 받고 싶을때 사용합니다.
// MARK: 45. output operator
let publisher =  ["A", "B", "C", "D"].publisher
print("Output(at:)")
// 2번째에 있는 이벤트만 방출
publisher.output(at: 2).sink {
  print($0) // "C"
}

print("Output(in:)")
// 특정 범위에 해당되는 이벤트만 방출
publisher.output(in: 0...2).sink { print($0) } // A, B, C
publisher.output(in: 1...).sink { print($0) } // B, C, D

6-46. count operator

  • count operator는 publisher에서 방출되는 값의 갯수를 반환할때 사용합니다.
    • count operator의 특징은 count()를 사용한 Publisher 이벤트가 끝나는 시점에서 이벤트가 방출된다는 점입니다.
// MARK: 46. count operator
let publisher = ["A", "B", "C", "D", "E"].publisher
let publisher2 = PassthroughSubject<Int, Never>()

publisher.count().sink {
  print($0) // publisher value 갯수, 5를 반환
}

publisher2.count().sink {
  print($0)
}

// PassthrouSubject<Int, Never> 타입의 subject는 이벤트를 3개 방출했다.
publisher2.send(10)
publisher2.send(20)
publisher2.send(50)
// subject의 경우 completed 이벤트가 발생하기 전까지 count 결과를 알 수 없습니다.
// subject의 경우 completed 이벤트 발생 후, 지금까지 방출한 이벤트 갯수가 내려옵니다.
publisher2.send(completion: .finished) // 3

[1, 2, 3].publisher.count().sink {
  // subject가 아닌 cold observable 형태의 publisher는 바로 이벤트를 받을 수 있습니다.
  print($0) // 3
}

6-47. contains operator

  • contains operator는 특정 값이 포함되었는지르 확인할때 사용하며, 포함 여부를 Bool 타입으로 반환합니다.
// MARK: 47. contains operator
let publisher = ["A", "B", "C", "D"].publisher

publisher.contains("Z").sink {
  print($0) // false
}

publisher.contains(where: { $0 == "A" }).sink {
  print($0) // true
}

6-48. allSatisfy operator

  • Sequence의 모든 원소가 특정 조건을 모두 충족하면 true, 그 외에는 false를 down stream으로 방출합니다.
[1, 2, 3].publisher.allSatisfy { $0 % 2 == 0 }.sink {
  // Sequence의 모든 원소가 짝수가 아니므로 false
  print($0)
}

[1, 3, 7].publisher.allSatisfy { $0 % 2 == 1 }.sink {
  // Sequence의 모든 원소가 홀수이므로 true
  print($0)
}

6-49. reduce operator

  • reduce operator는 초기값을 지정 후 Sequence publisher 값들에 대한 연산을 누적시킨 결과 값을 반환할 때 사용합니다.
// MARK: 49. reduce operator
let publisher =  [1, 2, 3, 4, 5, 6].publisher
// reduce use case 1)
publisher.reduce(0) { accumulator, value in
  print("accumulator : \(accumulator) and the value is \(value)")
  // accumulator가 immutable 값이며, 누적 연산 결과를 반환해야한다.
  return accumulator + value
}.sink {
  print($0)
}

// reduce use case 2)
// case 1과 동일한 연산 결과를 받을 수 있다.
publisher.reduce(0, +).sink {
  print($0)
}

// reduce use case 3)
// publisher sequence 의 곱 누적 연산 예시
publisher.reduce(1) {
  return $0 * $1 // 1 ~ 6의 누적 곱 연산,
}.sink {
  print($0)
}

Section 7: Combine for Networking

Combine + URLSession extension 을 통한 API 요청

// MARK: - 50. URLSession extensions
// Combine framework를 활용하여 네트워킹에 사용할 URLSession extension을 구성해봅니다.

func getPosts() -> AnyPublisher<Data, URLError> {
  // https://jsonplaceholder.typicode.com/posts
  // https://api.publicapis.org/entries
  guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
    fatalError("Invalid URL Error !!")
  }

  return URLSession.shared.dataTaskPublisher(for: url) // -> DataTaskPublisher (iOS 13+)
    .map { $0.data } // -> DataTaskPublisher의 Output 중 data로 맵핑, .map(\.data) 와 같이 KeyPath 방식 mapping도 가능
    .decode(type: [Post].self, decoder: JSONDecoder()) // 특정 타입으로 decoding 가능
    .eraseToAnyPublisher() // 연산 결과(Output, Failure)를 AnyPublisher 타입으로 추상화하여 반환된다.
}

// getPosts 결과를 정상적으로 출력하기 위해서는 cancellable 할당을 하거나, .store(in:)을 사용하여 구독정보를 메모리에 유지하고, 관리해야한다.
let cancellable = getPosts().sink(receiveCompletion: { _ in
  print("completion called")
}, receiveValue: {
  print("receiveValue closure called")
  print($0)
})

Section 8: Debugging Combine

print operator

  • Publisher의 down stream에서 발생하는 이벤트 결과를 출력할 수 있다.
// MARK: Section 8. Debugging Combine
// MARK: 53. Printing events
// print operator는 디버깅에 사용하는 operator로 인자로 디버깅 레이블을 넣어서 출력할 수도 있다.
// print operator를 현재 이벤트 동작 방식을 확인할때 사용할 수 있다.
let publisher = (1...3).publisher
publisher
  .print("Debug ID") // 라벨 없이 print()도 가능, print 인자로 디버깅 식별자를 넣고, 디버깅 로그를 출력할 수 있다.
  .sink {
    print($0)
  }

handleEvents

  • Publisher의 다양한 이벤트가 발생했을때 실행될 클로져를 제공한다.
// MARK: 54. Acting on events - performing side effects
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
  fatalError("Invalid URL")
}

let request = URLSession.shared.dataTaskPublisher(for: url) // -> URLSession.DataTaskPublisher, (data, response) 튜플 데이터를 방출한다.

// 동작 순서
// 1) Received Request
// 2) Subscription Received
// 3) Received Output
// 4) ~~~~ bytes (Decoding 되지 않은 json Data 수신)
// 5) Received Completion
// 6) finished
let cancellable = request
  .handleEvents(receiveSubscription: { _ in
    print("Subscription Received")
  }, receiveOutput: { _ in
    print("Received Output")
  }, receiveCompletion: { _ in
    print("Received Completion")
  }, receiveCancel: {
    print("Received Cancel")
  }, receiveRequest: { _ in
    print("Received Request")
  })
  .print()
  .sink(receiveCompletion: {
  print($0)
}, receiveValue: { value in
  print(value.data)
})

breakpoint operator

  • 특정 조건을 충족했을때 디버깅 모드를 실행할 수 있다.
import UIKit
import Combine

class ViewController: UIViewController {
  /// Publisher 구독정보를 담을 AnyCancellable 타입 변수 선언
  private var cancellable: AnyCancellable?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    let publisher = (1...10).publisher
    self.cancellable = publisher
      .breakpoint(receiveOutput: { value in
  // 1 ~ 9 까지 이벤트를 방출하고, 10이 되었을때 디버깅 모드가 실행된다.
        return value > 9
      })
      .sink {
      print($0)
    }
  }
}

Section 9: Combine Timers

Timer using Combine

  • 다양한 방법으로 타이머를 구현할 수 있습니다.
    • RunLoop.schedule (Cancellable 타입으로 관리)
    • DispatchQueue.main.schedule (Cancellable 타입으로 관리)
    • Timer.publish (TimerPublisher를 반환하면 구독 시 반환되는 AnyCancellable 타입으로 관리)
      • ConnectablePublisher로, connect() or autoconnect()를 사용해야 원하는 시기에 이벤트를 받을 수 있다.
import UIKit
import Combine
import PlaygroundSupport

// MARK: - Section 9. Combine Timers
// MARK: 56. Using RunLoop
// RunLoop은 timer 기능을 제공합니다. RunLoop.main 을 사용하면 메인스레드에서 timer 이벤트를 사용할 수 있습니다.
// RunLoop 이외의 방식으로도 Combine을 활용해서 타이머 기능을 사용할 수 있습니다.

class MyViewController : UIViewController {
  private let runLoop = RunLoop.main
  private var timerSubscription: Cancellable?
  
  override func loadView() {
    let view = UIView()
    view.backgroundColor = .white
    
    let label = UILabel()
    label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
    label.text = "Hello World!"
    label.textColor = .black
    
    view.addSubview(label)
    self.view = view
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    timerSubscription = self.runLoop.schedule(
      after: runLoop.now,
      interval: .seconds(2), // 2초 간격으로 타이머를 실행합니다.
      tolerance: .milliseconds(100) // 타이머 허용 오차를 지정합니다.
    ) {
      print("Timer fired")
    }
    
    self.runLoop.schedule(
      after: .init(Date(timeIntervalSinceNow: 3.0))
    ) { [weak self] in
      // 3초 뒤 구독을 취소하면서 타이머를 종료 시킬 수 있다.
      print("timer cancelled")
      self?.timerSubscription?.cancel()
    }
  }
}
  • Timer class, Timer.publish
    • RunLoop 방식 이외로도 Timer class의 publish 타입 메서드를 사용하면 타이머 기능을 구현할 수 있다.
// MARK: 57. Timer class
// RunLoop 방식 이외로도 Timer class의 publish 타입 메서드를 사용하면 타이머 기능을 구현할 수 있다.
// * autoconnect() : upstream Connectable Publisher의 이벤트를 자동적으로 받을 수 있도록 해주는 메서드이다. (수동은 connect())
private func usingTimerClass_57() {
  // 1초마다 메인스레드에서 타이머를 동작 시킨다.
  cancellable = Timer.publish(every: 1.0, on: .main, in: .common)
    .autoconnect()
    .scan(0) { counter, _ in
      counter + 1 // scan operator를 사용하여 timer 호출 당 1씩 증가 시킨다.
    }
    .sink { value in
      print("Timer Fired! \(value)")
    }
}
  • DispatchQueue, DispatchQueue.main.schedule
// MARK: 58. Using DispatchQueue
// RunLoop class, Timer class 에 이어 타이머를 사용하는 세번째 방법은 DispatchQueue입니다.
// DispatchQueue 를 통해 타이머 기능을 구현할 수 있습니다.
private func usingDispatchQueue_58() {
  // RunLoop에서 처럼, 메모리에서 holding 할 수 있도록 timer실행 코드에 대한 할당을 해야 정상 동작이 됩니다.
  // Dispatch.main.schedule로 타이머 기능 사용 가능
  timerSubscription = queue.schedule(
    after: queue.now,
    interval: .seconds(1)
  ) { [weak self] in
    guard let self = self else { return }
    // timer 호출마다 source subject에서 counter값 이벤트를 방출합니다.
    self.source.send(self.counter)
    self.counter += 1
  }
  
  cancellable = source.sink {
    if $0 == 5 {
      // timer가 5번째 호출될때 구독을 취소하여 타이머 이벤트를 종료합니다.
      self.cancellable?.cancel()
      return
    }
    print($0)
  }
}

Section 10: Resources in Combine

  • 동일한 API의 다수 요청으로 중복 결과 값을 받는 경우, 비효율적일 수 있다. 이를 share operator로 해결해보자.
  // MARK: - Section 10. Resources in Combine
  // MARK: 59. Understanding the problem
  private func understandingTheProblem_59() {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
      fatalError("Invalid URL")
    }
    
    let request = URLSession.shared.dataTaskPublisher(for: url)
      .map(\.data) // KeyPath를 통해 response빼고 data만 down stream에 넘길 수 있다. (다수의 KeyPath를 지정할 수도 있음.)
      .print() // print operator로 stream 동작상태를 확인할 수 있습니다.
    
    // subscription1, 2가 동일한 데이터를 받아온다. 동일한 결과값을 공유하지 않고 각자 구독하여 받고 있다. 이는 중복 작업으로 비효율적이다.   
    // 이러한 문제를 해결할 방법이 무엇이 있을까? -> share operator로 해결할 수 있다.
    subscription1 = request.sink(receiveCompletion: { _ in }, receiveValue: {
      print($0)
    })
    
    subscription2 = request.sink(receiveCompletion: { _ in }, receiveValue: {
      print($0)
    })
  }

share operator

  • share operator를 사용하면 해당 publisher에 대한 이벤트를 다수의 구독자가 공유하여 중복 작업 문제를 해결할 수 있다.
  • 내부적으로 autoconnect()가 실행되는 operator이다.
  • 하지만, 비동기적으로 몇 초뒤에 API를 호출할 경우 받는 결과는 이벤트가 공유되지 않는다(따로 놀게 됨). 이런 문제는 multicast operator를 통해 해결할 수 있다.
// MARK: - Section 10. Resources in Combine
// MARK: 59. Understanding the problem
// MARK: 60. share operator
// 'How can we share the results of a publisher?'
// -> share operator를 사용하면 동일 publisher에 대한 구독 이벤트를 다수의 구독자가 공유하여 불필요한 중복 작업을 방지할 수 있다.
private func understandingTheProblem_59() {
  guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
    fatalError("Invalid URL")
  }
  
  let request = URLSession.shared.dataTaskPublisher(for: url)
    .map(\.data) // KeyPath를 통해 response빼고 data만 down stream에 넘길 수 있다.
    .print() // print operator로 stream 동작상태를 확인할 수 있습니다.
    .share() // * share operator를 사용하면 해당 publisher에 대한 이벤트를 다수의 구독자가 공유하여 중복 작업 문제를 해결할 수 있다.

  // Subscription 1)
  subscription1 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 1")
    print($0)
  })

  // Subscription 2)
  subscription2 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 2")
    print($0) // share() operator를 사용했을 경우, 두번째 구독자는 Subscription 1)의 데이터를 공유하여 중복 작업을 하지 않게 됩니다.
  })
}

multicast operator

  • multicast operator의 인자로 지정한 Publisher의 값을 해당 operator를 사용한 publisher 구독자 전원에게 동일하게 뿌려줄 수있다.
    1. subject 레이블 인자로 multicast 용 publisher(subjectToMulticast)를 지정한다.
    2. 이벤트를 전파받을 구독자 (A, B, C ...)는 multicast를 지정한 publisher를 구독한다.
    3. subjectToMulticast publisher(subject)가 방출하는 이벤트를 구독자들이 받을 수 있다. (이때 구독자가 많아도 하나의 subscription receive event가 발생)
  • 내부적으로 ConnectablePublisher를 채택한다. share operator와 달리 이벤트를 정상적으로 받기 위해서는 connect(), autoconnect()를 명시해주어야 한다.
  • 관련 reference : https://zeddios.tistory.com/1011
// MARK: - Section 10. Resources in Combine
// MARK: 61. multicast operator

private func understandingTheProblem_59() {
  guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
    fatalError("Invalid URL")
  }
  
  let request = URLSession.shared.dataTaskPublisher(for: url)
    .map(\.data) // KeyPath를 통해 response빼고 data만 down stream에 넘길 수 있다.
    .print() // print operator로 stream 동작상태를 확인할 수 있습니다.
    .multicast(subject: self.subjectToMulticast) // multicast operator를 사용하면 해당 publisher를 구독하는 구독자들이 동일한 subject값을 전달받을 수 있게 된다.

  subscription1 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 1")
    print($0)
  })
  
  subscription2 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 2")
    print($0) // share() operator를 사용했을 경우, 두번째 구독자는 이미 앞서 처리된 데이터를 공유하여 중복 작업을 하지 않게 됩니다.
  })

  self.subscription3 = request.sink(receiveCompletion: { _ in }, receiveValue: {
    print("Subscription 3")
    print($0)
  })
  
  let cancellable = request.connect()
  // multicast operator로 지정한 subject를 통해 request 구독자들에게 동일한 데이터를 전달할 수 있다.
  self.subjectToMulticast.send(Data())
}

Section 11: Integrating Combine with UIKit Application - Weather App

Combine Publisher, Operator, Subscriber를 사용하여 날씨 정보를 조회하는 API 사용하기

// MARK: 65. Implementing Webservice
import Foundation
import Combine

final class WebService {
  func fetchWeather(city: String) -> AnyPublisher<Weather, Error> {
    // Constants의 타입 프로퍼티를 사용하여 weather 관련 URL 주소를 생성
    guard let url = URL(string: Constants.URLs.weather) else {
      fatalError("Invalid URL !!")
    }
    
    // 1) dataTaskPublisher를 통해 data, response를 가진 URLSession.DataTaskPublisher 를 반환
    // 2) map은 keyPath를 통해 특정 변수만 남기도록 맵핑이 가능하다.
    // 3) decode로 특정 decoder를 이용해서 디코딩을 할 수 있다.
    // 4) 디코딩 결과에서 main만 남기는 모습
    // 5) receive(on:)으로 특정 thread에서 동작하도록 지정할 수 있다. UI를 다루는 코드에 사용되므로 main thread에서 동작하도록 한다.
    // 6) 데이터의 내부적인 연산과정은 숨기고, 최종 결과형태만 받아서 구독 가능하도록 eraseToAnyPublisher()로 반환하고 있다.
    return URLSession.shared.dataTaskPublisher(for: url)
      .map(\.data)
      .decode(type: WeatherResponse.self, decoder: JSONDecoder())
      .map { $0.main }
      .receive(on: RunLoop.main) // main thread에서 동작하도록 합니다.
      .eraseToAnyPublisher() // 최종적인 형태로 데이터를 전달할때 eraseToAnyPublisher를 사용할 수 있다.
  }
}

throttle, debounce operator

  • reference : https://felix-mr.tistory.com/10

  • throttle operator

    • 연속 호출 시, 지정된 시간간격을 지나고 나서 최초 값 혹은 latest 값을 publish 한다.
    • 중복 처리, 중복 이벤트를 방지하고자 할때 사용할 수 있다. 이벤트 호출 후 특정 시간동안 동일 이벤트가 발생하기 원치 않을 경우 사용할 수 있다.
    • latest 옵션에 따라, 특정 시간이 지나고 publish 될 이벤트 기준을 바꿀 수 있다. (latest옵션이 false면 초기 이벤트 기준, true면 가장 최근 이벤트 기준)
    • ex) 버튼에 throttleTap을 적용, 단기간에 여러번 터치가 될 경우, 한번만 허용할 수 있다.
  • debounce operator

    • 이벤트 발생 후 일정 시간 지켜본 후 트리거를 한다. 트리거 전에 이벤트가 발생하면 이전 이벤트는 무효화 한다.
    • 자동검색 등, 한글자 한글자 입력할때 API를 호출하지 않고, 타자가 멈춘 후, 일정 시간이 지날때 자동검색 결과를 보이도록 할 수 있다.
    • ex) 자동검색 기능
// MARK: combine debounce operator example
final class DebounceViewModel {
  // TextField에 입력받은 텍스트에 대한 정보를 받는 text subject
  private(set) var text = PassthroughSubject<String?, Never>()
  private(set) var result = PassthroughSubject<String, Never>()
  
  ... 생략 ...
}

private extension DebounceViewModel {
  
  private func bind() {
    // text 입력 후, 0.5초간 추가적인 이벤트가 있으면 이전 이벤트는 무시된다.
    // text 입력 후, 0.5초간 추가적인 이벤트가 없을 경우, down stream 이벤트가 trigger된다.
    text
      .debounce(for: 0.5, scheduler: RunLoop.main)
      .compactMap { $0 }
      .sink { self.result.send($0) }
      .store(in: &cancellables)
  }
}
// MARK: Combine throttle operator example
final class ThrottleViewModel {

  private(set) var touchEvent = PassthroughSubject<Int, Never>()
  @Published private(set) var count = 0
  
  ... 생략 ...
}

private extension ThrottleViewModel {
  func bind() {
    touchEvent
      .throttle(for: 1, scheduler: RunLoop.main, latest, latest: false)
      .sink { self.count += $0 }
      .store(in: &cancellables)
  }
}

hello-combine-with-udemy's People

Contributors

applebuddy avatar jae-eun avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.