Combine 스터디 (4) - Combining Operators

Seoyoung Lee·2024년 3월 21일
0

Combine 스터디

목록 보기
4/5
post-thumbnail

Prepending

prepend
add (something) to the beginning of something else.

prepend(Output)

func prepend(_ elements: Self.Output...) -> Publishers.Concatenate<Publishers.Sequence<[Self.Output], Self.Failure>, Self>

publisher의 output 앞에 특정한 값을 붙이는 operator

  • elements: publisher의 항목 전에 publish될 항목
    • 항목의 개수는 제한 없음!
let dataElements = (0...10)
cancellable = dataElements.publisher
    .prepend(0, 1, 255)
    .sink { print("\($0)", terminator: " ") }

// Prints: "0 1 255 0 1 2 3 4 5 6 7 8 9 10"
  • 0, 1, 255 가 먼저 publish 되고 dataElements 의 항목들이 publish된다.
let publisher = [3, 4].publisher
  
publisher
  .prepend(1, 2) 
  .prepend(-1, 0)
  .sink(receiveValue: { print($0) })

/*
 -1
 0
 1
 2
 3
 4
*/

마지막에 연결된 prepend 가 가장 앞에 온다.

prepend(1, 2) 를 실행해서 1, 2가 붙어서 새로 publish 되고, 그 다음 prepend(-1, 0) 가 실행되어서 그 앞에 또 -1, 0이 붙어서 다시 publish 되니까?!

Publishers.Concatenate

struct Concatenate<Prefix, Suffix> where Prefix : Publisher, Suffix : Publisher, Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output

한 publisher의 모든 값을 다른 publisher 전에 방출하는 publisher

  • prefix: suffix 가 값을 다시 방출하기 전에 모든 값을 다시 publish
  • suffix: prefix가 종료된 후에 다시 값을 publish 하는 publisher

prepend(Sequence)

파라미터로 sequence를 받을 수도 있다.

잠깐,,

  • Sequence가 뭐지?

    배열이나 Range 등을 publisher로 변환하면 Publishers.Sequence 타입이 된다.

    Publishers.Sequence는 여러 항목을 연속으로 publish하는 publisher이다.

    이 Sequence는 생성할 때 Elements 를 파라미터로 받는데, 이 친구는 Sequence 라는 프로토콜을 준수하는 타입이어야 한다.

    Swift의 Array, Set, Dictionary 등은 Collection 프로토콜을 준수하고 있는데, CollectionSequence 를 준수하고 있다.

그래서 Array 같은 Collection 타입들을 publisher로 변환하면 Publishers.Sequence 라는 타입이 되었던 것!

// 1
let publisher = [5, 6, 7].publisher
   
// 2
publisher
  .prepend([3, 4])
  .prepend(Set(1...2))
	.prepend(stride(from: 6, to: 11, by: 2))
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

/*
 6
 8
 10
 1
 2
 3
 4
 5
 6
 7
*/
  • Set 은 순서를 보장하지 않기 때문에 위처럼 1, 2 가 아닌 2, 1 처럼 출력될 수 있다.
  • stride 처럼 다양한 Sequence 타입을 전달할 수 있다.

prepend(Publisher)

👀 combining operator라면서 ,, 단순한 값이 아니라 다른 publisher의 값도 붙일 수는 없나?

func prepend<P>(_ publisher: P) -> Publishers.Concatenate<P, Self> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output

publisher의 output 앞에 매개변수로 받은 publisher가 방출한 항목을 붙이는 operator

  • 어떤 publisher의 항목을 다른 publisher의 항목 앞에 붙이고 싶을 때 사용한다.
let prefixValues = [0, 1, 255]
let dataElements = (0...10)
cancellable = dataElements.publisher
    .prepend(prefixValues.publisher)
    .sink { print("\($0)", terminator: " ") }

// Prints: "0 1 255 0 1 2 3 4 5 6 7 8 9 10"

dataLements 가 값을 publish 하기 전에 prefixValues 가 값을 publish 한다.

// 1
let publisher1 = [3, 4].publisher
let publisher2 = PassthroughSubject<Int, Never>()

// 2
publisher1
  .prepend(publisher2)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

// 3
publisher2.send(1)
publisher2.send(2)

/*
1
2
*/

먼저 진행된 publisher가 반드시 완료된 후에 다음 publisher가 값을 방출할 수 있다.

publisher2.send(completion: .finished)

publisher2 를 종료시키면 publisher1 도 값을 방출할 수 있다.

1
2
3
4

Appending

prepend와 비슷하다. 대신 publisher의 뒤에 값을 추가해줌!

append(Output…)

func append(_ elements: Self.Output...) -> Publishers.Concatenate<Self, Publishers.Sequence<[Self.Output], Self.Failure>>

publisher의 output 뒤에 특정한 값들을 추가하는 operator

let dataElements = (0...10)
cancellable = dataElements.publisher
    .append(0, 1, 255)
    .sink { print("\($0)", terminator: " ") }

// Prints: "0 1 2 3 4 5 6 7 8 9 10 0 1 255"

dataElements 의 모든 값들을 다시 publish 한 다음에 파라미터로 받은 값들을 publish 한다.

prepend와 차이점

append와 prepend의 가장 큰 차이점은 원래 publisher가 .finished 로 종료된 후에 작동한다는 것이다.

append()의 리턴 타입이 Publishers.Concatenate<Self, Publishers.Sequence<[Self.Output], Self.Failure>> 었고, Concatenate의 suffix는 preffix가 종료된 후에 다시 publish 된다고 했기 때문

// 1
let publisher = PassthroughSubject<Int, Never>()

publisher
    .append(3, 4)
    .append(5)
    .sink(receiveValue: { print($0) })
  
// 2
publisher.send(1)
publisher.send(2)

/*
1
2
*/

publisher 가 종료되지 않았기 때문에 append() 가 아무런 영향을 주지 않는다.

publisher.send(completion: .finished)

publisher를 종료시키면 원하는 결과를 얻을 수 있다.

1
2
3
4
5

Publishers.Concatenate<Prefix, Suffix> 는 항상 앞에 애가 finish 되어야 뒤에 애의 값을 republish 할 수 있다는 걸 기억하자!!

append(Sequence)

Sequence를 파라미터로 받는 append operator

// 1
let publisher  = [1, 2, 3].publisher

publisher
	.append([4, 5])	//  2
	.append(Set([6, 7]))	// 3
	.append(stride(from: 8, to: 11, by: 2))	// 4
	.sink(receiveValue: { print($0) })
	.store(in: & subscriptions)

/*
1
2
3
4
5
7	// 순서가 바뀔 수 있음
6	// 순서가 바뀔 수 있음
8
10
*/

prepend와 마찬가지로 다양한 종류의 Sequence를 append 할 수 있다.

append(Publisher)

Publisher를 파라미터로 받는 append operator

 // 1
 let publisher1 = [1, 2].publisher
 let publisher2 = [3, 4].publisher
   
 // 2
 publisher1
 	.append(publisher2)
 	.sink(receiveValue: { print($0) })

/*
1
2
3
4
*/

고급 Combining Operator

switchToLatest()

가장 최신의 publisher가 값을 방출하면 기존 publisher의 구독을 취소하고 최신 publisher로 구독을 전환(switching)하는 operator

// 1
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let publisher3 = PassthroughSubject<Int, Never>()
	
// 2
let publishers = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()
	
// 3
publishers
	.switchToLatest()
	.sink(receiveCompletion: { _ in print("Completed!") },
		receiveValue: { print($0) })
	.store(in: &subscriptions)
	
// 4
publishers.send(publisher1)
publisher1.send(1)
publisher1.send(2)
	
// 5
publishers.send(publisher2)
publisher1.send(3)
publisher2.send(4)
publisher2.send(5)
	
// 6
publishers.send(publisher3)
publisher2.send(6)
publisher3.send(7)
publisher3.send(8)
publisher3.send(9)
	
// 7
publisher3.send(completion: .finished)
publishers.send(completion: .finished)
  1. 3개의 PassthroughSubjects 를 만듦

  2. PassthroughSubjects 를 받는 PassthroughSubjects 를 만듦

  3. publishersswitchToLatest() 적용

  4. publisher1publishers 로 보내고 1, 2 값을 보냄

    → 현재 publisherspublisher1 구독!

  5. publisherspublisher2 를 보냄

    publisherspublisher2 를 구독해서 publisher1 의 값은 무시됨

  6. publisherspublisher3 을 보냄

    publisherspublisher3 을 구독해서 publisher2 의 값은 무시됨

  7. publisher3publishers 을 종료해서 활성화된 모든 구독을 완료함

 1
 2
 4
 5
 7
 8
 9
 completed!

Publishers.SwitchToLatest

struct SwitchToLatest<P, Upstream> where P : Publisher, P == Upstream.Output, Upstream : Publisher, P.Failure == Upstream.Failure

🤔 위 예시에서 publishers 의 Output 타입은 PassthroughSubject 인데.. 왜 Int 값이 출력되지?

⇒ switchToLatest()는 publisher들을 납작하게 (flatten) 만든 publisher를 리턴하기 때문이다!

  • switchToLatest()를 적용할 publisher는 publisher를 방출해야 한다.
  • 여러 publisher들이 값을 방출하지만, downstream subscriber들에게는 하나의 publisher로부터 연속적인 데이터 스트림을 구독하는 것처럼 보인다!
  • upstream publisher로부터 새 publisher를 받으면 이전 구독은 취소된다.
  • 앞에 있는 publisher가 불필요한 작업을 하는 것을 막을 때 사용할 수 있다. 예: 자주 업데이트 되는 UI publisher로부터 네트워크 요청을 하는 publisher를 만드는 경우
     let url = URL(string: "https://source.unsplash.com/random")!
       
     // 1
     func getImage() -> AnyPublisher<UIImage?, Never> {
     	return URLSession.shared
     		.dataTaskPublisher(for: url)
     		.map { data, _ in UIImage(data: data) }
     		.print("image")
     		.replaceError(with: nil)
     		.eraseToAnyPublisher()
     }
     
     // 2
     let taps = PassthroughSubject<Void, Never>()
     
     taps
     	.map { _ in getImage() } // 3
     	.switchToLatest() // 4
     	.sink(receiveValue: { _ in })
     	.store(in: &subscriptions)
     
     // 5
     taps.send()
     
     DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
     	taps.send()
     }
     DispatchQueue.main.asyncAfter(deadline: .now() + 3.1) {
     	taps.send()
     }
    1. 랜덤 이미지를 가져오는 API를 호출하는 메소드 getImage() 작성

    2. 사용자가 버튼을 터치하는 이벤트를 방출하는 PassthroughSubject 선언

    3. 버튼을 터치하면 getImage() 가 호출되고, switchToLatest()도 적용

    4. taps 에 이벤트를 3번 보냄

       image: receive subscription: (DataTaskPublisher)
       image: request unlimited
       image: receive value: (Optional(<UIImage:0x600000364120 anonymous {1080, 720}>))
       image: receive finished
       image: receive subscription: (DataTaskPublisher)
       image: request unlimited
       image: receive cancel
       image: receive subscription: (DataTaskPublisher)
       image: request unlimited
       image: receive value: (Optional(<UIImage:0x600000378d80 anonymous {1080, 1620}>))
       image: receive finished

      실제로는 2개의 이미지만 가져와진다. 두 번째 send 0.1초 뒤에 다음 send가 불려서 두 번째 구독은 취소되었기 때문!

merge(with:)

func merge(with other: Self) -> Publishers.MergeMany<Self>

같은 타입의 다른 publisher와 합쳐서 값들을 상호 교차해서 전달해주는 operator

  • upstream publisher들이 이벤트를 방출하면 리턴되는 publisher가 이벤트를 방출한다.
  • 최대 8개의 publisher들을 합칠 수 있다
 // 1
 let publisher1 = PassthroughSubject<Int, Never>()
 let publisher2 = PassthroughSubject<Int, Never>()
 
 // 2
 publisher1
 	.merge(with: publisher2)
 	.sink(receiveCompletion: { _ in print("Completed") },
       		receiveValue: { print($0) })
 	.store(in: &subscriptions)
 
 // 3
 publisher1.send(1)
 publisher1.send(2)
 
 publisher2.send(3)
 
 publisher1.send(4)
 
 publisher2.send(5)
 
 // 4
 publisher1.send(completion: .finished)
 publisher2.send(completion: .finished)
 
/*
1
2
3
4
5
Completed
*/

combineLatest

func combineLatest<P>(_ other: P) -> Publishers.CombineLatest<Self, P> where P : Publisher, Self.Failure == P.Failure

다른 publisher를 구독하고, publisher들로부터 마지막으로 받은 output의 튜플을 publish하는 operator

  • 다른 타입의 publisher도 결합할 수 있다! 단, Failure 타입은 같아야 함
  • 각 upstream마다 버퍼 크기 1을 사용해서 가장 최근 값을 저장한다.
  • 최대 4개의 publisher 결합 가능
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()

cancellable = pub1
    .combineLatest(pub2)
    .sink { print("Result: \($0).") }

pub1.send(1)
pub1.send(2)
pub2.send(2)
pub1.send(3)
pub1.send(45)
pub2.send(22)

// Prints:
//    Result: (2, 2).    // pub1 latest = 2, pub2 latest = 2
//    Result: (3, 2).    // pub1 latest = 3, pub2 latest = 2
//    Result: (45, 2).   // pub1 latest = 45, pub2 latest = 2
//    Result: (45, 22).  // pub1 latest = 45, pub2 latest = 22

모든 upstream publisher들이 종료되면 이 publisher도 종료된다.

upstream publisher가 값을 publish하지 않으면 이 publisher도 절대 종료되지 않는다.

→ combineLatest로 결합된 모든 publisher들은 최소 한 번 이상 값을 방출해야 한다.

zip

func zip<P>(_ other: P) -> Publishers.Zip<Self, P> where P : Publisher, Self.Failure == P.Failure

다른 publisher와 결합하고, 그 값들의 쌍을 튜플 형태로 전달하는 operator

  • 각 publisher들이 마지막으로 방출한 값을 튜플 형태로 downstream에 방출한다.
  • 리턴되는 publisher는 모든 publisher가 이벤트를 방출할 때까지 기다린다. 그 후 가장 마지막으로 소비되지 않은 이벤트를 subscriber에게 넘겨준다. ⇒ 다시말해, 같은 인덱스 상에 있는 값들을 튜플 형태로 조합해서 방출한다. 지퍼를 잠그는 느낌,,
 let numbersPub = PassthroughSubject<Int, Never>()
 let lettersPub = PassthroughSubject<String, Never>()

 cancellable = numbersPub
     .zip(lettersPub)
     .sink { print("\($0)") }
 numbersPub.send(1)    // numbersPub: 1      lettersPub:        zip output: <none>
 numbersPub.send(2)    // numbersPub: 1,2    lettersPub:        zip output: <none>
 letters.send("A")     // numbers: 1,2       letters:"A"        zip output: <none>
 numbers.send(3)       // numbers: 1,2,3     letters:           zip output: (1,"A")
 letters.send("B")     // numbers: 1,2,3     letters: "B"       zip output: (2,"B")

 // Prints:
 //  (1, "A")
 //  (2, "B")

upstream publisher 중 하나라도 정상적으로 종료되거나 실패하면 zip 된 publisher도 똑같이 종료된다.

merge vs combineLatest vs zip

  • 여러 Publisher로부터 가장 마지막 값 하나를 받고 싶을 때는 merge
  • 여러 publisher들이 마지막으로 방출한 값들을 묶어서 받고 싶을 때는 combineLatest
  • 여러 publisher들이 방출한 값을 쌍을 맞춰서 받고 싶을 때는 zip
profile
나의 내일은 파래 🐳

0개의 댓글