Combine: Operator

틀틀보·2025년 4월 23일

Combine

목록 보기
3/4

Publisher로부터 전달되는 값을 변환, 필터링, 결합, 시간 조작 등 다양한 방식의 처리를 지원하는 요소

Filtering Operators

값을 가공 또는 원하는 조건에 따라 필터링 기능을 지원합니다.

filter

주어진 조건을 만족하는 값만 통과

import Combine

let numbers = (1...10).publisher

numbers
    .filter { $0 % 2 == 0 } // 짝수만 통과
    .sink { print($0) }

removeDuplicates

연속으로 중복된 값을 제거합니다. (Set과 다름)

import Combine

let words = ["apple", "apple", "banana", "banana", "apple"].publisher

words
    .removeDuplicates()
    .sink { print($0) }
 // apple banana apple

first

조건을 만족하는 첫 번째 값만 통과시킵니다.

import Combine

let numbers = (1...10).publisher

numbers
    .first(where: { $0 > 5 })
    .sink { print($0) }

그 외

  • dropFirst, drop
  • prefix 등등

Transforming Operators

Publisher로부터 전달된 값을 변환합니다.

map

Swift에서 흔히 사용하는 고차함수인 map과 동일하게 동작
주어진 클로저를 통해 값을 변환

import Combine

let numbers = (1...3).publisher

numbers
    .map { "Number: \($0)" }
    .sink { print($0) }

tryMap

클로저 내에서 오류를 던질 수 있는 경우에 사용
오류 발생 시, 해당 스트림은 실패로 종료

import Combine

enum ConversionError: Error {
    case invalidFormat
}

let strings = ["123", "abc", "456"].publisher

strings
    .tryMap { str -> Int in
        guard let number = Int(str) else {
            throw ConversionError.invalidFormat
        }
        return number
    }
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Completed successfully")
            case .failure(let error):
                print("Failed with error: \(error)")
            }
        },
        receiveValue: { value in
            print("Parsed number: \(value)")
        }
    )

flatMap

각 입렵값을 새로운 Publisher로 변환하고 변환된 Publisher들의 출력을 단일 스트림으로 병합합니다.
비동기 작업을 순차적으로 처리하거나, 중첩된 Publisher를 평탄화할 때 유용

import Combine

func fetchDetails(for id: Int) -> AnyPublisher<String, Never> {
    Just("Details for ID \(id)")
        .delay(for: .milliseconds(100), scheduler: RunLoop.main)
        .eraseToAnyPublisher()
}

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

ids
    .flatMap { fetchDetails(for: $0) }
    .sink { print($0) }

collect

Publisher가 발행하는 여러 값을 하나의 배열로 수집하여 한 번에 전달

import Combine

let numbers = (1...5).publisher

numbers
    .collect()
    .sink { print($0) }
// 결과: [1, 2, 3, 4, 5]

배열에 저장될 값의 개수를 지정해서도 가능

import Combine

let numbers = (1...10).publisher

numbers
    .collect(3)
    .sink { print($0) }
/* 
결과: [1, 2, 3]
	 [4, 5, 6]
	 [7, 8, 9]
	 [10] 
*/

scan

초기값과 클로저를 사용하여 이전 결과와 현재 값을 누적하여 새로운 값을 생성
reduce 고차 함수와 비슷

import Combine

let numbers = (1...5).publisher

numbers
    .scan(0) { accumulated, current in
        accumulated + current
    }
    .sink { print($0) }
/*
결과: 
1
3
6
10
15
*/

replaceNil

값이 Nil일 경우의 기본값을 지정합니다.

import Combine

let values: [String?] = ["A", nil, "C"]

values.publisher
    .replaceNil(with: "-")
    .sink { print($0) }
/*
A
-
C
*/

그 외

  • replaceEmpty
  • switchToLatest 등등

Combining Operators

여러 Publisher의 이벤트를 결합하여 새로운 데이터 흐름을 생성하는데 사용

merge

여러 Publisher의 출력을 하나의 스트림으로 병합, 병합된 Publisher가 값을 발행할 때마다 해당 값이 순서에 상관없이 전달

import Combine

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .merge(with: publisher2)
    .sink { print("Received value: \($0)") }

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

Received value: 1
Received value: 2
Received value: 3

combineLatest

여러 Publisher의 최신 값을 결합하여 Tuple로 전달합니다.
특정 Publisher에서 값이 배출되면 다른 퍼블리셔의 이전 값과 엮어 방출

import Combine

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .combineLatest(publisher2)
    .sink { print("Combined value: \($0), \($1)") }

publisher1.send("A")
publisher2.send(1)
publisher2.send(2)
publisher1.send("B")

Combined value: A, 1
Combined value: A, 2
Combined value: B, 2

zip

combineLatest와 유사하게 동작하나,
각 publisher들에게서 새로운 값들로 Tuple이 만들어 질때만 방출

import Combine

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .zip(publisher2)
    .sink { print("Zipped value: \($0), \($1)") }

publisher1.send("A")
publisher2.send(1)
publisher1.send("B")
publisher2.send(2)

Zipped value: A, 1
Zipped value: B, 2

switchToLatest

Publisher가 Publisher를 발행하는 경우, 가장 최근에 발행된 내부 Publisher의 값만 구독

import Combine

let subject1 = PassthroughSubject<String, Never>()
let subject2 = PassthroughSubject<String, Never>()
let publishers = PassthroughSubject<PassthroughSubject<String, Never>, Never>()

publishers
    .switchToLatest()
    .sink { print("Received: \($0)") }

publishers.send(subject1)
subject1.send("First")
publishers.send(subject2)
subject1.send("Ignored")
subject2.send("Second")

Received: First
Received: Second

그 외

  • prepend
  • append 등등

Timing And Control Operator

비동기 데이터 흐름의 타이밍을 제어하고 조절합니다.

debounce

지정된 시간동안 새로운 이벤트가 발생하지 않을 때까지 대기한 후 마지막 이벤트를 전달합니다.

$searchText
    .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
    .sink { query in
        performSearch(with: query)
    }
    .store(in: &cancellables)

ex) 사용자 입력이 어느정도 멈추었을 때만 검색 요청

throttle

지정된 시간 간격으로 이벤트 전달

$scrollPosition
    .throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
    .sink { position in
        updateUI(for: position)
    }
    .store(in: &cancellables)
  • latest: 마지막 이벤트를 전달할 지 여부
    ex) 스크롤 이벤트와 같이 빠르게 발생하는 이벤트의 빈도를 제한하여 성능 향상

delay

각 이벤트의 전달을 지정된 시간만큼 지연

Just("Hello")
    .delay(for: .seconds(2), scheduler: RunLoop.main)
    .sink { value in
        showAlert(with: value)
    }
    .store(in: &cancellables)

receive

다운스트림 작업(데이터 처리 등)이 지정된 스케줄러에서 실행되도록 합니다.
Publisher로부터 받은 데이터 처리를 어디서 할지를 지정
결과를 어디서 처리 할지
ex) UI에 적용될 값을 메인 스레드에서 작업

backgroundPublisher
    .receive(on: RunLoop.main)
    .sink { value in
        updateUI(with: value)
    }
    .store(in: &cancellables)

subscribe

업스트림 작업(데이터 생성 등)이 지정된 스케줄러에서 실행되도록 합니다.
작업을 어디서 시킬지 지정
계산을 어디서 처리할지

heavyComputationPublisher
    .subscribe(on: DispatchQueue.global(qos: .background))
    .receive(on: RunLoop.main)
    .sink { result in
        updateUI(with: result)
    }
    .store(in: &cancellables)

https://developer.apple.com/documentation/combine
https://blog.devgenius.io/combining-operators-in-combine-framework-37b8ba043a1d

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글