반응형 프로그래밍의 핵심 아이디어는 시간에 따른 비동기 이벤트 흐름을 모델링하는 것.
Combine에서는 시퀀스가 시간에 따라 값에 반응하고 변환하는 다양한 연산자들을 제공.
Time shifting
delay(for:tolerance:scheduler:options)
(Rx의 delay)Upstream Publisher
가 값을 내보낼 때마다 delay
연산자는 잠시 동안 값을 유지한 다음 사용자가 지정한 스케줄러에서 요청한 지연 시간 후에 값을 내보냄.
초마다 하나의 값을 내보내는 퍼블리셔를 만든 다음 1.5초씩 지연시키고 두 타임라인을 동시에 표시하여 비교.
import Combine
import SwiftUI
import PlaygroundSupport
var subscriptions = Set<AnyCancellable>()
let valuesPerSecond = 1.0
let delayInSeconds = 1.5
// 1. sourcePublisher는 타이머가 내보내는 날짜를 공급할 Subject
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2. delayedPublisher는 sourcePublisher에서 값을 delay시켜 main scheduler에서 값을 방출
let delayedPublisher = sourcePublisher.delay(for: .seconds(delayInSeconds),
scheduler: DispatchQueue.main)
// 3. main thread에서 초당 하나의 값을 전달하는 타이머를 만듦
// autoconnect()를 사용해서 즉시 시작하고 sourcePublisher를 통해 방출되는 값을 제공
let subscription = Timer
.publish(every: 1.0 / valuesPerSecond,
on: .main,
in: .common)
.autoconnect()
.subscribe(sourcePublisher)
아래는 이벤트를 시각화할 TimelineView
세팅.
// 4.Timer의 값을 표시할 TimelineView 생성
let sourceTimeline = TimelineView(title: "방출된 값 (\(valuesPerSecond)초마다 방출):")
// 5. Delay된 Value를 표시할 TimelineView 생성
let delayedTimeline = TimelineView(title: "Delay된 값들 (\(delayInSeconds)초 delay):")
// 6. 위 두 TimelineView를 VStack으로
let view = VStack(spacing: 50) {
sourceTimeline
delayedTimeline
}
// 7. PlaygroundPage에 liveView 설정
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
위 TimeLineView
는 아래처럼 source Publisher를 Publisher extension으로 displayEvents로 넣어주면
오른쪽에서부터 순서대로 그려주는 뷰. 상세 구현은 생략.
sourcePublisher.displayEvents(in: sourceTimeline)
delayedPublisher.displayEvents(in: delayedTimeline)

Collecting Values
collect(_:options:) - strategy를 byTime으로
(Rx의 toArray)
지정된 시간간격으로 Publisher
로부터 값을 collect해야할 때에 필요. buffering의 형태.
(Transform Operator 중 하나인 collect
의 오버로드. 기존 collect
는 지정된 갯수만큼 묶어 배열로 방출하지만)
이 collect
는 지정된 시간 단위로 묶어 배열로 방출.
let valuesPerSecond = 1.0
let collectTimeStride = 4
// 1. Timer가 방출할 값을 전달할 sourcePublisher
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2. colect 연산자로 'collectTimeStride의 보폭 동안 수신하는 값을 수집하는 collectedPublisher'를 생성
// 연산자는 이러한 값 그룹을 지정된 스케줄러(DispatchQueue.main)에 배열로 방출
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
방출된 값과 collect
된 값들의 방출을 TimelineView
로 확인.
// 방출된 값과 collect된 값들의 방출을 보여줄 TimelineView들
let sourceTimeline = TimelineView(title: "방출된 값들:")
let collectedTimeline = TimelineView(title: "Collect된 값들 (매 \(collectTimeStride)초마다):")
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
// TimelineView들에 각 소스들 표시
sourcePublisher.displayEvents(in: sourceTimeline)
collectedPublisher.displayEvents(in: collectedTimeline)
4초마다 collect
된 배열이 방출되는 것을 확인.

배열내 값을 확인해보기 위해 collectedPublisher
에 flatmap
을 추가.
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
.flatMap { dates in dates.publisher } // 추가

collect(_:options:) - strategy를 byTimeOrCount로
strategy
를 byTimeOrCount
로 하면,
일정 시간 간격으로 방출된 값을 collect하면서 값의 갯수를 제한할 수 있음.
위 collect
예제에서 제한할 갯수(collectMaxCount
)를 추가하고,
let collectMaxCount = 2 // 추가
byTimeOrCount
로 수집할 Publisher
도 추가하고,
let collectedPublisher2 = sourcePublisher // 추가
.collect(.byTimeOrCount(DispatchQueue.main,
.seconds(collectTimeStride),
collectMaxCount))
.flatMap { dates in dates.publisher }
TimelineView
도 추가하고,
let collectedTimeline2 = TimelineView(title: "Collect된 값들 (최대 \(collectMaxCount)개씩 매 \(collectTimeStride)초마다):") // 추가
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
collectedTimeline2 // 추가
}
TimelineView
에 Publisher
추가하고 타임라인 확인.
collectedPublisher2.displayEvents(in: collectedTimeline2) // 추가
최대갯수(2
)가 될 때와 Time(4초
)가 될 때마다 값을 방출하는 것을 확인.

Event의 보류(Holding off)
텍스트필드에서 값이 연속으로 들어올 때마다 반응하지 않고,
한동안 값을 입력하지 않았을 때만 입력했던 값을 받아 처리하는 것과 같이 이벤트를 Holding Off시키고 싶을 때 필요한 오퍼레이터.
Combine
에서는 debounce
와 throttle
을 제공.
debounce(for:scheduler:)
(Rx의 debounce)
받은 값을 1초마다 방출시키는 debounce
예시.
// 1. String을 방출할 subject
let subject = PassthroughSubject<String, Never>()
// 2. debounce로 1초간 기다렸다가 값을 전달.
// 1초 간격으로 전달된 마지막 값이 있는 경우 해당값을 방출. 초당 1번만 방출하게 됨.
let debounced = subject
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
.share() // 3. debounce된 Publisher를 여러번 subscribe하더라도 일관된 값을 받도록 하기 위해 share 연산자 사용.
타이핑을 대신하기 위한 TimeInterval
과 String
배열.
0.6초간 입력 후 멈추었다가 2.0초부터 다시 입력을 시작.
public let typingHelloWorld: [(TimeInterval, String)] = [
(0.0, "H"),
(0.1, "He"),
(0.2, "Hel"),
(0.3, "Hell"),
(0.5, "Hello"),
(0.6, "Hello "),
(2.0, "Hello W"),
(2.1, "Hello Wo"),
(2.2, "Hello Wor"),
(2.4, "Hello Worl"),
(2.5, "Hello World")
]
TimelineView
에 방출된 값과 Debounce
된 값을 설정하고,
시간 간격을 print
하도록 작성.
let subjectTimeline = TimelineView(title: "값 방출")
let debouncedTimeline = TimelineView(title: "Debounce된 값")
let view = VStack(spacing: 100) {
subjectTimeline
debouncedTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
subject.displayEvents(in: subjectTimeline)
debounced.displayEvents(in: debouncedTimeline)
let subscription1 = subject
.sink { string in
print("+\(deltaTime)초: Subject 방출됨: \(string)")
}
let subscription2 = debounced
.sink { string in
print("+\(deltaTime)초: Debounce후 방출됨: \(string)")
}
subject.feed(with: typingHelloWorld)
입력이 멈추었을 때, debounce
된 값이 들어오는 것을 확인.

+0.0초: Subject 방출됨: H
+0.1초: Subject 방출됨: He
+0.2초: Subject 방출됨: Hel
+0.3초: Subject 방출됨: Hell
+0.5초: Subject 방출됨: Hello
+0.6초: Subject 방출됨: Hello
+1.6초: Debounce후 방출됨: Hello
+2.0초: Subject 방출됨: Hello W
+2.0초: Subject 방출됨: Hello Wo
+2.3초: Subject 방출됨: Hello Wor
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Subject 방출됨: Hello World
+3.5초: Debounce후 방출됨: Hello World
throttle(for:scheduler:latest:)
(Rx의 throttle)throttle
은 지정된 시간 간격만큼 기다린 다음, 해당 간격 중 받았던 가장 첫번째 값(latest: false
)이나 최신값(latest: true
)를 받음.
아래는 throttle
의 예시.
import Combine
import SwiftUI
import PlaygroundSupport
let throttleDelay = 1.0
// 1. String을 방출할 Subject
let subject = PassthroughSubject<String, Never>()
// 2. latest를 false로 하여 throttled subject는
// 각 1초 간격으로 subject로부터 받은 첫번째값만 방출
let throttled = subject
.throttle(for: .seconds(throttleDelay),
scheduler: DispatchQueue.main,
latest: false)
.share() // 3. 모든 subscriber가 throttled subject에서 동일한 값을 share받음
그리고 이를 확인하기 위한 TimelineView
작성.
let subjectTimeline = TimelineView(title: "방출된 값")
let throttledTimeline = TimelineView(title: "throttle된 값")
let view = VStack(spacing: 100) {
subjectTimeline
throttledTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
subject.displayEvents(in: subjectTimeline)
throttled.displayEvents(in: throttledTimeline)
let subscription1 = subject
.sink { string in
print("+\(deltaTime)초: Subject 방출됨: \(string)")
}
let subscription2 = throttled
.sink { string in
print("+\(deltaTime)초: Throttled 방출됨: \(string)")
}
subject.feed(with: typingHelloWorld)
방출된 값과 throttle
된 값 확인.

로그를 통해 throttle
의 동작을 확인. 1초 간격으로 기다렸다가 그 사이에 제일 먼저 받았던 값(latest: false
)을 방출.
+0.0초: Subject 방출됨: H
+0.0초: Throttled 방출됨: H
+0.1초: Subject 방출됨: He
+0.2초: Subject 방출됨: Hel
+0.3초: Subject 방출됨: Hell
+0.5초: Subject 방출됨: Hello
+0.6초: Subject 방출됨: Hello
+1.1초: Throttled 방출됨: He
+2.0초: Subject 방출됨: Hello W
+2.0초: Subject 방출됨: Hello Wo
+2.1초: Throttled 방출됨: Hello W
+2.3초: Subject 방출됨: Hello Wor
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Subject 방출됨: Hello World
+3.1초: Throttled 방출됨: Hello Wor
throttle
의 latest
를 true
로 수정하면
let throttled = subject
.throttle(for: .seconds(throttleDelay),
scheduler: DispatchQueue.main,
latest: true)
.share()
로그를 통해 1초 간격으로 기다렸다가 그 사이에 제일 최신값(latest: true
)을 방출하는 것을 확인.
+0.0초: Subject 방출됨: H
+0.0초: Throttled 방출됨: H
+0.1초: Subject 방출됨: He
+0.2초: Subject 방출됨: Hel
+0.3초: Subject 방출됨: Hell
+0.5초: Subject 방출됨: Hello
+0.6초: Subject 방출됨: Hello
+1.0초: Throttled 방출됨: Hello
+2.1초: Subject 방출됨: Hello W
+2.1초: Throttled 방출됨: Hello W
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Subject 방출됨: Hello World
+3.1초: Throttled 방출됨: Hello World
시간 초과 처리
timeout
(Rx의 timeout)timeout
연산자가 실행이 되면 publisher
가 complete
되거나 정의된 error
를 발생시키고 종료시킴.
아래는 subject
가 5초간 아무값도 발생시키지 않았을 때 timeout
되는 예시.
subject
가 error
가 Never
이기 때문에 failure없이 complete.
import Combine
import SwiftUI
import PlaygroundSupport
let subject = PassthroughSubject<Void, Never>()
// 1. upstream publisher가 5초간 아무값을 방출하지 않으면 시간 초과가 됩니다.
let timedOutSubject = subject.timeout(.seconds(5), scheduler: DispatchQueue.main)
버튼을 누르면 subject
가 방출하도록 설정하고 TimelineView
작성.
let timeline = TimelineView(title: "Button 탭")
let view = VStack(spacing: 100) {
// 1. 버튼을 누르면 subject 방출
Button(action: { subject.send() }) {
Text("5초 내로 버튼 탭")
}
timeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
timedOutSubject.displayEvents(in: timeline)
5초 내로 탭하지 않았을 때 subject
가 complete
되는 것을 확인.

시간 초과 시 에러를 발생시키기 위해서는 아래와 같이 수정.
enum TimeoutError: Error { // 추가
case timedOut
}
let subject = PassthroughSubject<Void, TimeoutError>() // TimeoutError로 수정
let timedOutSubject = subject.timeout(.seconds(5),
scheduler: DispatchQueue.main,
customError: { .timedOut }) // customError 추가
시간 초과시 에러를 방출하는 것을 확인

시간 측정
measureInterval(using:)
시간 조작은 하지 않고 측정만 하는 연산자.
measureInterval(using:)
는 Publisher
가 2개의 연속된 값 사이의 경과한 시간을 측정해야할 때 사용할 수 있는 도구.
import Combine
import SwiftUI
import PlaygroundSupport
let subject = PassthroughSubject<String, Never>()
// 1. subject를 main 큐에서 값을 방출하게 하고, 측정하도록 지정
let measureSubject = subject.measureInterval(using: DispatchQueue.main)
let subjectTimeline = TimelineView(title: "방출된 값")
let measureTimeline = TimelineView(title: "측정된 값")
let view = VStack(spacing: 100) {
subjectTimeline
measureTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
subject.displayEvents(in: subjectTimeline)
measureSubject.displayEvents(in: measureTimeline)
// subject와 measure 발출된 값 print
let subscription1 = subject.sink {
print("+\(deltaTime)초: Subject 방출됨: \($0)")
}
let subscription2 = measureSubject.sink {
print("+\(deltaTime)초: Measure 방출됨: \($0)")
}
subject.feed(with: typingHelloWorld)
출력값을 확인.
TimeInterval
은 제공된 스케줄러(여기서는 DispatchQueue
)가 제공하는 시간 단위.
DispatchQueue
의 경우 TimeInterval
은 nanoseconds
단위로 생성된 DispatchTimeInterval
로 정의.
+0.0초: Measure 방출됨: Stride(_nanoseconds: 40468500)
+0.0초: Subject 방출됨: H
+0.1초: Measure 방출됨: Stride(_nanoseconds: 64299875)
+0.1초: Subject 방출됨: He
+0.2초: Measure 방출됨: Stride(_nanoseconds: 102388250)
+0.2초: Subject 방출됨: Hel
+0.3초: Measure 방출됨: Stride(_nanoseconds: 108301875)
+0.3초: Subject 방출됨: Hell
+0.5초: Measure 방출됨: Stride(_nanoseconds: 208391209)
+0.5초: Subject 방출됨: Hello
+0.6초: Measure 방출됨: Stride(_nanoseconds: 105173166)
+0.6초: Subject 방출됨: Hello
+2.1초: Measure 방출됨: Stride(_nanoseconds: 1469857959)
+2.1초: Subject 방출됨: Hello W
+2.2초: Measure 방출됨: Stride(_nanoseconds: 104921375)
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Measure 방출됨: Stride(_nanoseconds: 785583)
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Measure 방출됨: Stride(_nanoseconds: 315478292)
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Measure 방출됨: Stride(_nanoseconds: 139916)
+2.5초: Subject 방출됨: Hello World
보기 좋게 출력하기 위해 아래처럼 수정이 필요.
let subscription2 = measureSubject.sink {
print("+\(deltaTime)초: Measure 방출됨: \(Double($0.magnitude) / 1_000_000_000.0)") // 수정
}
출력을 확인.
+0.0초: Measure 방출됨: 0.024960375
+0.0초: Subject 방출됨: H
+0.1초: Measure 방출됨: 0.077610083
+0.1초: Subject 방출됨: He
+0.2초: Measure 방출됨: 0.107842584
+0.2초: Subject 방출됨: Hel
+0.3초: Measure 방출됨: 0.104186958
+0.3초: Subject 방출됨: Hell
+0.5초: Measure 방출됨: 0.2083225
+0.5초: Subject 방출됨: Hello
+0.6초: Measure 방출됨: 0.10464775
+0.6초: Subject 방출됨: Hello
+2.1초: Measure 방출됨: 1.471396875
+2.1초: Subject 방출됨: Hello W
+2.2초: Measure 방출됨: 0.103586167
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Measure 방출됨: 0.000129
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Measure 방출됨: 0.316555708
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Measure 방출됨: 0.000130167
+2.5초: Subject 방출됨: Hello World
DispatchQueue
가 아닌 Runloop
메인으로 바꿨을 때는?
let measureSubject2 = subject.measureInterval(using: RunLoop.main) // 추가
let subscription3 = measureSubject2.sink { // 추가
print("+\(deltaTime)s: Measure2 emitted: \($0)")
}
Runloop
에서는 초단위로 방출이 측정되는 것을 확인.
+0.0초: Measure 방출됨: 0.029805917
+0.0s: Measure2 방출: Stride(magnitude: 0.030153989791870117)
+0.0초: Subject 방출됨: H
+0.1초: Measure 방출됨: 0.074119875
+0.1s: Measure2 방출: Stride(magnitude: 0.07328498363494873)
+0.1초: Subject 방출됨: He
+0.2초: Measure 방출됨: 0.103644875
+0.2s: Measure2 방출: Stride(magnitude: 0.1036679744720459)
+0.2초: Subject 방출됨: Hel
+0.3초: Measure 방출됨: 0.108386083
+0.3s: Measure2 방출: Stride(magnitude: 0.1084979772567749)
+0.3초: Subject 방출됨: Hell
+0.5초: Measure 방출됨: 0.208423542
+0.5s: Measure2 방출: Stride(magnitude: 0.20862603187561035)
+0.5초: Subject 방출됨: Hello
+0.6초: Measure 방출됨: 0.10648925
+0.6s: Measure2 방출: Stride(magnitude: 0.10615503787994385)
+0.6초: Subject 방출됨: Hello
+2.1초: Measure 방출됨: 1.471713875
+2.1s: Measure2 방출: Stride(magnitude: 1.4720739126205444)
+2.1초: Subject 방출됨: Hello W
+2.2초: Measure 방출됨: 0.102555375
+2.2s: Measure2 방출: Stride(magnitude: 0.10231101512908936)
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Measure 방출됨: 0.000357041
+2.2s: Measure2 방출: Stride(magnitude: 0.0001990795135498047)
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Measure 방출됨: 0.315149417
+2.5s: Measure2 방출: Stride(magnitude: 0.3153599500656128)
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Measure 방출됨: 0.000406625
+2.5s: Measure2 방출: Stride(magnitude: 0.00017905235290527344)
+2.5초: Subject 방출됨: Hello World
정리
1. delay(for:tolerance:scheduler:options)
- 스케줄러에 지정한 시간만큼 방출한 값을 유지했다가 해당 시간이 지나면 방출시켜줌.
2. collect(_:options:)는 strategy에 따라
- byTime를 strategy으로 했을 때, 지정된 시간간격으로 Publisher로부터 값을 collect시켜줌. buffering의 형태.
- byTimeOrCount로 strategy으로 했을 때, 일정 시간 간격으로 방출된 값을 collect하면서 값의 갯수를 제한시켜줌.
3. debounce(for:scheduler:)
- 스케줄러에 지정한 시간만큼 값이 들어오지 않기를 기다렸다가, 값 방출이 멈추기 전까지 받았던 값들을 배열로 한꺼번에 방출시켜줌.
4. throttle(for:scheduler:latest:)
- 지정된 시간 간격만큼 기다린 다음, 해당 간격 중 받았던 가장 첫번째 값(latest: false)이나 최신값(latest: true)를 받음.
5. timeout
- timeout 연산자가 실행이 되면 publisher가 complete되거나 정의된 error를 발생시키고 종료
6. measureInterval(using:)
- Publisher가 2개의 연속된 값 사이의 경과한 시간을 측정해야할 때 사용할 수 있는 도구.
- TimeInterval은 제공된 스케줄러(여기서는 DispatchQueue)가 제공하는 시간 단위(DispatchQueue는 nanoseconds로 runloop는 초단위로 제공)
반응형 프로그래밍의 핵심 아이디어는 시간에 따른 비동기 이벤트 흐름을 모델링하는 것.
Combine에서는 시퀀스가 시간에 따라 값에 반응하고 변환하는 다양한 연산자들을 제공.
Time shifting
delay(for:tolerance:scheduler:options)
(Rx의 delay)Upstream Publisher
가 값을 내보낼 때마다 delay
연산자는 잠시 동안 값을 유지한 다음 사용자가 지정한 스케줄러에서 요청한 지연 시간 후에 값을 내보냄.
초마다 하나의 값을 내보내는 퍼블리셔를 만든 다음 1.5초씩 지연시키고 두 타임라인을 동시에 표시하여 비교.
import Combine
import SwiftUI
import PlaygroundSupport
var subscriptions = Set<AnyCancellable>()
let valuesPerSecond = 1.0
let delayInSeconds = 1.5
// 1. sourcePublisher는 타이머가 내보내는 날짜를 공급할 Subject
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2. delayedPublisher는 sourcePublisher에서 값을 delay시켜 main scheduler에서 값을 방출
let delayedPublisher = sourcePublisher.delay(for: .seconds(delayInSeconds),
scheduler: DispatchQueue.main)
// 3. main thread에서 초당 하나의 값을 전달하는 타이머를 만듦
// autoconnect()를 사용해서 즉시 시작하고 sourcePublisher를 통해 방출되는 값을 제공
let subscription = Timer
.publish(every: 1.0 / valuesPerSecond,
on: .main,
in: .common)
.autoconnect()
.subscribe(sourcePublisher)
아래는 이벤트를 시각화할 TimelineView
세팅.
// 4.Timer의 값을 표시할 TimelineView 생성
let sourceTimeline = TimelineView(title: "방출된 값 (\(valuesPerSecond)초마다 방출):")
// 5. Delay된 Value를 표시할 TimelineView 생성
let delayedTimeline = TimelineView(title: "Delay된 값들 (\(delayInSeconds)초 delay):")
// 6. 위 두 TimelineView를 VStack으로
let view = VStack(spacing: 50) {
sourceTimeline
delayedTimeline
}
// 7. PlaygroundPage에 liveView 설정
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
위 TimeLineView
는 아래처럼 source Publisher를 Publisher extension으로 displayEvents로 넣어주면
오른쪽에서부터 순서대로 그려주는 뷰. 상세 구현은 생략.
sourcePublisher.displayEvents(in: sourceTimeline)
delayedPublisher.displayEvents(in: delayedTimeline)

Collecting Values
collect(_:options:) - strategy를 byTime으로
(Rx의 toArray)
지정된 시간간격으로 Publisher
로부터 값을 collect해야할 때에 필요. buffering의 형태.
(Transform Operator 중 하나인 collect
의 오버로드. 기존 collect
는 지정된 갯수만큼 묶어 배열로 방출하지만)
이 collect
는 지정된 시간 단위로 묶어 배열로 방출.
let valuesPerSecond = 1.0
let collectTimeStride = 4
// 1. Timer가 방출할 값을 전달할 sourcePublisher
let sourcePublisher = PassthroughSubject<Date, Never>()
// 2. colect 연산자로 'collectTimeStride의 보폭 동안 수신하는 값을 수집하는 collectedPublisher'를 생성
// 연산자는 이러한 값 그룹을 지정된 스케줄러(DispatchQueue.main)에 배열로 방출
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
방출된 값과 collect
된 값들의 방출을 TimelineView
로 확인.
// 방출된 값과 collect된 값들의 방출을 보여줄 TimelineView들
let sourceTimeline = TimelineView(title: "방출된 값들:")
let collectedTimeline = TimelineView(title: "Collect된 값들 (매 \(collectTimeStride)초마다):")
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
// TimelineView들에 각 소스들 표시
sourcePublisher.displayEvents(in: sourceTimeline)
collectedPublisher.displayEvents(in: collectedTimeline)
4초마다 collect
된 배열이 방출되는 것을 확인.

배열내 값을 확인해보기 위해 collectedPublisher
에 flatmap
을 추가.
let collectedPublisher = sourcePublisher
.collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
.flatMap { dates in dates.publisher } // 추가

collect(_:options:) - strategy를 byTimeOrCount로
strategy
를 byTimeOrCount
로 하면,
일정 시간 간격으로 방출된 값을 collect하면서 값의 갯수를 제한할 수 있음.
위 collect
예제에서 제한할 갯수(collectMaxCount
)를 추가하고,
let collectMaxCount = 2 // 추가
byTimeOrCount
로 수집할 Publisher
도 추가하고,
let collectedPublisher2 = sourcePublisher // 추가
.collect(.byTimeOrCount(DispatchQueue.main,
.seconds(collectTimeStride),
collectMaxCount))
.flatMap { dates in dates.publisher }
TimelineView
도 추가하고,
let collectedTimeline2 = TimelineView(title: "Collect된 값들 (최대 \(collectMaxCount)개씩 매 \(collectTimeStride)초마다):") // 추가
let view = VStack(spacing: 40) {
sourceTimeline
collectedTimeline
collectedTimeline2 // 추가
}
TimelineView
에 Publisher
추가하고 타임라인 확인.
collectedPublisher2.displayEvents(in: collectedTimeline2) // 추가
최대갯수(2
)가 될 때와 Time(4초
)가 될 때마다 값을 방출하는 것을 확인.

Event의 보류(Holding off)
텍스트필드에서 값이 연속으로 들어올 때마다 반응하지 않고,
한동안 값을 입력하지 않았을 때만 입력했던 값을 받아 처리하는 것과 같이 이벤트를 Holding Off시키고 싶을 때 필요한 오퍼레이터.
Combine
에서는 debounce
와 throttle
을 제공.
debounce(for:scheduler:)
(Rx의 debounce)
받은 값을 1초마다 방출시키는 debounce
예시.
// 1. String을 방출할 subject
let subject = PassthroughSubject<String, Never>()
// 2. debounce로 1초간 기다렸다가 값을 전달.
// 1초 간격으로 전달된 마지막 값이 있는 경우 해당값을 방출. 초당 1번만 방출하게 됨.
let debounced = subject
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
.share() // 3. debounce된 Publisher를 여러번 subscribe하더라도 일관된 값을 받도록 하기 위해 share 연산자 사용.
타이핑을 대신하기 위한 TimeInterval
과 String
배열.
0.6초간 입력 후 멈추었다가 2.0초부터 다시 입력을 시작.
public let typingHelloWorld: [(TimeInterval, String)] = [
(0.0, "H"),
(0.1, "He"),
(0.2, "Hel"),
(0.3, "Hell"),
(0.5, "Hello"),
(0.6, "Hello "),
(2.0, "Hello W"),
(2.1, "Hello Wo"),
(2.2, "Hello Wor"),
(2.4, "Hello Worl"),
(2.5, "Hello World")
]
TimelineView
에 방출된 값과 Debounce
된 값을 설정하고,
시간 간격을 print
하도록 작성.
let subjectTimeline = TimelineView(title: "값 방출")
let debouncedTimeline = TimelineView(title: "Debounce된 값")
let view = VStack(spacing: 100) {
subjectTimeline
debouncedTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
subject.displayEvents(in: subjectTimeline)
debounced.displayEvents(in: debouncedTimeline)
let subscription1 = subject
.sink { string in
print("+\(deltaTime)초: Subject 방출됨: \(string)")
}
let subscription2 = debounced
.sink { string in
print("+\(deltaTime)초: Debounce후 방출됨: \(string)")
}
subject.feed(with: typingHelloWorld)
입력이 멈추었을 때, debounce
된 값이 들어오는 것을 확인.

+0.0초: Subject 방출됨: H
+0.1초: Subject 방출됨: He
+0.2초: Subject 방출됨: Hel
+0.3초: Subject 방출됨: Hell
+0.5초: Subject 방출됨: Hello
+0.6초: Subject 방출됨: Hello
+1.6초: Debounce후 방출됨: Hello
+2.0초: Subject 방출됨: Hello W
+2.0초: Subject 방출됨: Hello Wo
+2.3초: Subject 방출됨: Hello Wor
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Subject 방출됨: Hello World
+3.5초: Debounce후 방출됨: Hello World
throttle(for:scheduler:latest:)
(Rx의 throttle)throttle
은 지정된 시간 간격만큼 기다린 다음, 해당 간격 중 받았던 가장 첫번째 값(latest: false
)이나 최신값(latest: true
)를 받음.
아래는 throttle
의 예시.
import Combine
import SwiftUI
import PlaygroundSupport
let throttleDelay = 1.0
// 1. String을 방출할 Subject
let subject = PassthroughSubject<String, Never>()
// 2. latest를 false로 하여 throttled subject는
// 각 1초 간격으로 subject로부터 받은 첫번째값만 방출
let throttled = subject
.throttle(for: .seconds(throttleDelay),
scheduler: DispatchQueue.main,
latest: false)
.share() // 3. 모든 subscriber가 throttled subject에서 동일한 값을 share받음
그리고 이를 확인하기 위한 TimelineView
작성.
let subjectTimeline = TimelineView(title: "방출된 값")
let throttledTimeline = TimelineView(title: "throttle된 값")
let view = VStack(spacing: 100) {
subjectTimeline
throttledTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
subject.displayEvents(in: subjectTimeline)
throttled.displayEvents(in: throttledTimeline)
let subscription1 = subject
.sink { string in
print("+\(deltaTime)초: Subject 방출됨: \(string)")
}
let subscription2 = throttled
.sink { string in
print("+\(deltaTime)초: Throttled 방출됨: \(string)")
}
subject.feed(with: typingHelloWorld)
방출된 값과 throttle
된 값 확인.

로그를 통해 throttle
의 동작을 확인. 1초 간격으로 기다렸다가 그 사이에 제일 먼저 받았던 값(latest: false
)을 방출.
+0.0초: Subject 방출됨: H
+0.0초: Throttled 방출됨: H
+0.1초: Subject 방출됨: He
+0.2초: Subject 방출됨: Hel
+0.3초: Subject 방출됨: Hell
+0.5초: Subject 방출됨: Hello
+0.6초: Subject 방출됨: Hello
+1.1초: Throttled 방출됨: He
+2.0초: Subject 방출됨: Hello W
+2.0초: Subject 방출됨: Hello Wo
+2.1초: Throttled 방출됨: Hello W
+2.3초: Subject 방출됨: Hello Wor
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Subject 방출됨: Hello World
+3.1초: Throttled 방출됨: Hello Wor
throttle
의 latest
를 true
로 수정하면
let throttled = subject
.throttle(for: .seconds(throttleDelay),
scheduler: DispatchQueue.main,
latest: true)
.share()
로그를 통해 1초 간격으로 기다렸다가 그 사이에 제일 최신값(latest: true
)을 방출하는 것을 확인.
+0.0초: Subject 방출됨: H
+0.0초: Throttled 방출됨: H
+0.1초: Subject 방출됨: He
+0.2초: Subject 방출됨: Hel
+0.3초: Subject 방출됨: Hell
+0.5초: Subject 방출됨: Hello
+0.6초: Subject 방출됨: Hello
+1.0초: Throttled 방출됨: Hello
+2.1초: Subject 방출됨: Hello W
+2.1초: Throttled 방출됨: Hello W
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Subject 방출됨: Hello World
+3.1초: Throttled 방출됨: Hello World
시간 초과 처리
timeout
(Rx의 timeout)timeout
연산자가 실행이 되면 publisher
가 complete
되거나 정의된 error
를 발생시키고 종료시킴.
아래는 subject
가 5초간 아무값도 발생시키지 않았을 때 timeout
되는 예시.
subject
가 error
가 Never
이기 때문에 failure없이 complete.
import Combine
import SwiftUI
import PlaygroundSupport
let subject = PassthroughSubject<Void, Never>()
// 1. upstream publisher가 5초간 아무값을 방출하지 않으면 시간 초과가 됩니다.
let timedOutSubject = subject.timeout(.seconds(5), scheduler: DispatchQueue.main)
버튼을 누르면 subject
가 방출하도록 설정하고 TimelineView
작성.
let timeline = TimelineView(title: "Button 탭")
let view = VStack(spacing: 100) {
// 1. 버튼을 누르면 subject 방출
Button(action: { subject.send() }) {
Text("5초 내로 버튼 탭")
}
timeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
timedOutSubject.displayEvents(in: timeline)
5초 내로 탭하지 않았을 때 subject
가 complete
되는 것을 확인.

시간 초과 시 에러를 발생시키기 위해서는 아래와 같이 수정.
enum TimeoutError: Error { // 추가
case timedOut
}
let subject = PassthroughSubject<Void, TimeoutError>() // TimeoutError로 수정
let timedOutSubject = subject.timeout(.seconds(5),
scheduler: DispatchQueue.main,
customError: { .timedOut }) // customError 추가
시간 초과시 에러를 방출하는 것을 확인

시간 측정
measureInterval(using:)
시간 조작은 하지 않고 측정만 하는 연산자.
measureInterval(using:)
는 Publisher
가 2개의 연속된 값 사이의 경과한 시간을 측정해야할 때 사용할 수 있는 도구.
import Combine
import SwiftUI
import PlaygroundSupport
let subject = PassthroughSubject<String, Never>()
// 1. subject를 main 큐에서 값을 방출하게 하고, 측정하도록 지정
let measureSubject = subject.measureInterval(using: DispatchQueue.main)
let subjectTimeline = TimelineView(title: "방출된 값")
let measureTimeline = TimelineView(title: "측정된 값")
let view = VStack(spacing: 100) {
subjectTimeline
measureTimeline
}
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))
subject.displayEvents(in: subjectTimeline)
measureSubject.displayEvents(in: measureTimeline)
// subject와 measure 발출된 값 print
let subscription1 = subject.sink {
print("+\(deltaTime)초: Subject 방출됨: \($0)")
}
let subscription2 = measureSubject.sink {
print("+\(deltaTime)초: Measure 방출됨: \($0)")
}
subject.feed(with: typingHelloWorld)
출력값을 확인.
TimeInterval
은 제공된 스케줄러(여기서는 DispatchQueue
)가 제공하는 시간 단위.
DispatchQueue
의 경우 TimeInterval
은 nanoseconds
단위로 생성된 DispatchTimeInterval
로 정의.
+0.0초: Measure 방출됨: Stride(_nanoseconds: 40468500)
+0.0초: Subject 방출됨: H
+0.1초: Measure 방출됨: Stride(_nanoseconds: 64299875)
+0.1초: Subject 방출됨: He
+0.2초: Measure 방출됨: Stride(_nanoseconds: 102388250)
+0.2초: Subject 방출됨: Hel
+0.3초: Measure 방출됨: Stride(_nanoseconds: 108301875)
+0.3초: Subject 방출됨: Hell
+0.5초: Measure 방출됨: Stride(_nanoseconds: 208391209)
+0.5초: Subject 방출됨: Hello
+0.6초: Measure 방출됨: Stride(_nanoseconds: 105173166)
+0.6초: Subject 방출됨: Hello
+2.1초: Measure 방출됨: Stride(_nanoseconds: 1469857959)
+2.1초: Subject 방출됨: Hello W
+2.2초: Measure 방출됨: Stride(_nanoseconds: 104921375)
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Measure 방출됨: Stride(_nanoseconds: 785583)
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Measure 방출됨: Stride(_nanoseconds: 315478292)
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Measure 방출됨: Stride(_nanoseconds: 139916)
+2.5초: Subject 방출됨: Hello World
보기 좋게 출력하기 위해 아래처럼 수정이 필요.
let subscription2 = measureSubject.sink {
print("+\(deltaTime)초: Measure 방출됨: \(Double($0.magnitude) / 1_000_000_000.0)") // 수정
}
출력을 확인.
+0.0초: Measure 방출됨: 0.024960375
+0.0초: Subject 방출됨: H
+0.1초: Measure 방출됨: 0.077610083
+0.1초: Subject 방출됨: He
+0.2초: Measure 방출됨: 0.107842584
+0.2초: Subject 방출됨: Hel
+0.3초: Measure 방출됨: 0.104186958
+0.3초: Subject 방출됨: Hell
+0.5초: Measure 방출됨: 0.2083225
+0.5초: Subject 방출됨: Hello
+0.6초: Measure 방출됨: 0.10464775
+0.6초: Subject 방출됨: Hello
+2.1초: Measure 방출됨: 1.471396875
+2.1초: Subject 방출됨: Hello W
+2.2초: Measure 방출됨: 0.103586167
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Measure 방출됨: 0.000129
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Measure 방출됨: 0.316555708
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Measure 방출됨: 0.000130167
+2.5초: Subject 방출됨: Hello World
DispatchQueue
가 아닌 Runloop
메인으로 바꿨을 때는?
let measureSubject2 = subject.measureInterval(using: RunLoop.main) // 추가
let subscription3 = measureSubject2.sink { // 추가
print("+\(deltaTime)s: Measure2 emitted: \($0)")
}
Runloop
에서는 초단위로 방출이 측정되는 것을 확인.
+0.0초: Measure 방출됨: 0.029805917
+0.0s: Measure2 방출: Stride(magnitude: 0.030153989791870117)
+0.0초: Subject 방출됨: H
+0.1초: Measure 방출됨: 0.074119875
+0.1s: Measure2 방출: Stride(magnitude: 0.07328498363494873)
+0.1초: Subject 방출됨: He
+0.2초: Measure 방출됨: 0.103644875
+0.2s: Measure2 방출: Stride(magnitude: 0.1036679744720459)
+0.2초: Subject 방출됨: Hel
+0.3초: Measure 방출됨: 0.108386083
+0.3s: Measure2 방출: Stride(magnitude: 0.1084979772567749)
+0.3초: Subject 방출됨: Hell
+0.5초: Measure 방출됨: 0.208423542
+0.5s: Measure2 방출: Stride(magnitude: 0.20862603187561035)
+0.5초: Subject 방출됨: Hello
+0.6초: Measure 방출됨: 0.10648925
+0.6s: Measure2 방출: Stride(magnitude: 0.10615503787994385)
+0.6초: Subject 방출됨: Hello
+2.1초: Measure 방출됨: 1.471713875
+2.1s: Measure2 방출: Stride(magnitude: 1.4720739126205444)
+2.1초: Subject 방출됨: Hello W
+2.2초: Measure 방출됨: 0.102555375
+2.2s: Measure2 방출: Stride(magnitude: 0.10231101512908936)
+2.2초: Subject 방출됨: Hello Wo
+2.2초: Measure 방출됨: 0.000357041
+2.2s: Measure2 방출: Stride(magnitude: 0.0001990795135498047)
+2.2초: Subject 방출됨: Hello Wor
+2.5초: Measure 방출됨: 0.315149417
+2.5s: Measure2 방출: Stride(magnitude: 0.3153599500656128)
+2.5초: Subject 방출됨: Hello Worl
+2.5초: Measure 방출됨: 0.000406625
+2.5s: Measure2 방출: Stride(magnitude: 0.00017905235290527344)
+2.5초: Subject 방출됨: Hello World
정리
1. delay(for:tolerance:scheduler:options)
- 스케줄러에 지정한 시간만큼 방출한 값을 유지했다가 해당 시간이 지나면 방출시켜줌.
2. collect(_:options:)는 strategy에 따라
- byTime를 strategy으로 했을 때, 지정된 시간간격으로 Publisher로부터 값을 collect시켜줌. buffering의 형태.
- byTimeOrCount로 strategy으로 했을 때, 일정 시간 간격으로 방출된 값을 collect하면서 값의 갯수를 제한시켜줌.
3. debounce(for:scheduler:)
- 스케줄러에 지정한 시간만큼 값이 들어오지 않기를 기다렸다가, 값 방출이 멈추기 전까지 받았던 값들을 배열로 한꺼번에 방출시켜줌.
4. throttle(for:scheduler:latest:)
- 지정된 시간 간격만큼 기다린 다음, 해당 간격 중 받았던 가장 첫번째 값(latest: false)이나 최신값(latest: true)를 받음.
5. timeout
- timeout 연산자가 실행이 되면 publisher가 complete되거나 정의된 error를 발생시키고 종료
6. measureInterval(using:)
- Publisher가 2개의 연속된 값 사이의 경과한 시간을 측정해야할 때 사용할 수 있는 도구.
- TimeInterval은 제공된 스케줄러(여기서는 DispatchQueue)가 제공하는 시간 단위(DispatchQueue는 nanoseconds로 runloop는 초단위로 제공)