
안녕하세요, 별똥별🌠입니다!
이번 포스트에서는 TCA를 활용해 Long-Living Effect를 어떻게 구현하는지 살펴봅니다.
이 예제에서는 뷰가 나타날 때 스크린샷 알림(Notification)을 비동기적으로 지속적으로 수신하고, 해당 알림이 올 때마다 상태를 업데이트하여 스크린샷 횟수를 카운트합니다
Long-Living Effect란?
- 일반적인 Effect는 한 번 실행되고 종료되지만, Long-Living Effect는 뷰가 존재하는 동안 계속 실행되어 외부 이벤트(예: Notification, Timer 등)를 지속적으로 수신합니다.
- TCA에서는 .run { ... } Effect와 함께 비동기 반복문(for await)을 사용하여 이러한 장기 실행 효과를 쉽게 구현할 수 있습니다.
Cancellation
- Long-Living Effect는 뷰가 사라지거나 다른 조건에 따라 중단할 수 있지만, 여기서는 특별한 취소 로직 없이 뷰가 남아있는 한 계속 실행됩니다.
@Reducer
struct LongLivingEffects {
@ObservableState
struct State: Equatable {
var screenshotCount: Int = 0
}
enum Action {
case task
case userDidTaskScreenshotNotification
}
@Dependency(\.screenshots) var screenshots
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .task:
// 뷰가 나타나면, 스크린샷을 찍을 때마다 Effect를 통해 알림을 보냅니다.
return .run { send in
for await _ in await self.screenshots() {
await send(.userDidTaskScreenshotNotification)
}
}
case .userDidTaskScreenshotNotification:
state.screenshotCount += 1
return .none
}
}
}
}
- 상태(State)
- screenshotCount가 스크린샷 횟수를 저장합니다.
- 액션(Action)
- .task : 뷰가 나타날 때 Long-Living Effect를 시작하는 액션
- .userDidTaskScreenshotNotification : 스크린샷 이벤트가 발생할 때마다 전송되는 액션
- 의존성(Dependency)
- screenshots : 외부에서 주입받은 스크린샷 알림 스트림을 나타냅니다.
- Effect 실행
- .task 액션에 대해 .run Effect 내에서 for await 반복문으로 screenshots() 스트림을 구독합니다.
- 스크린샷 알림이 올 때마다 .userDidTaskScreenshotNotification 액션을 보내고, 상태의 screenshotCount가 증가합니다.
struct LongLivingEffectsView: View {
let store: StoreOf<LongLivingEffects>
var body: some View {
Form {
Section {
AboutView(readMe: readMe)
}
Text("A screenshot of this screen has been taken \(store.screenshotCount) times.")
Section {
NavigationLink {
detailView
} label: {
Text("Navigate to anhoter screen.")
}
}
}
.navigationTitle("Long-living effects")
.task { await store.send(.task).finish() }
}
var detailView: some View {
Text(
"""
Take a screenshot of this screen a few times, and then go back to the previous screen to see \
that those screenshots were not counted.
"""
)
.padding(.horizontal, 64)
.navigationBarTitleDisplayMode(.inline)
}
}
- 뷰 구성
- Form 내부에 스크린샷 횟수를 표시하는 Text와, 다른 화면으로 이동할 수 있는 NavigationLink가 있습니다.
- Long-Living Effect 시작
- .task { await store.send(.task).finish() }를 사용하여 뷰가 나타날 때 .task 액션을 보내고, 이에 따라 Long-Living Effect가 시작됩니다.
- 상태 업데이트
- 스크린샷 이벤트 발생 시 screenshotCount가 업데이트되어 UI에 반영됩니다.
extension DependencyValues {
var screenshots: @Sendable () async -> any AsyncSequence<Void, Never> {
get { self[ScreenshotsKey.self] }
set { self[ScreenshotsKey.self] = newValue }
}
}
private enum ScreenshotsKey: DependencyKey {
static let liveValue: @Sendable () async -> any AsyncSequence<Void, Never> = {
NotificationCenter.default
.notifications(named: UIApplication.userDidTakeScreenshotNotification)
.map { _ in }
}
}
주요 포인트
- 의존성 값(DependencyValues)
- screenshots 키를 통해 스크린샷 관련 알림 스트림을 제공합니다.
- liveValue 구현
- NotificationCenter를 통해 UIApplication.userDidTakeScreenshotNotification 알림을 수신하고, 이를 Void 스트림으로 변환합니다.
- 이 방식으로 TCA Reducer에서 스크린샷 이벤트를 비동기적으로 수신할 수 있습니다.
- 뷰가 나타나면 .task 액션을 전송 → Long-Living Effect 시작
- Effect 내에서 screenshots() 스트림을 구독 → 스크린샷 이벤트가 발생하면 .userDidTaskScreenshotNotification 전송
- Reducer가 .userDidTaskScreenshotNotification 액션을 받아 screenshotCount를 증가
- UI는 이 상태 변화를 반영하여 스크린샷 횟수를 업데이트
- Long-Living Effect는 뷰가 존재하는 동안 지속적으로 외부 이벤트(여기서는 스크린샷 알림)를 수신하여 상태를 업데이트할 수 있게 해줍니다.
- TCA의 .run { ... }와 for await를 활용해 비동기 스트림을 처리하며, 이를 통해 장기 실행 효과를 쉽게 구현할 수 있습니다.
- 의존성 주입(Dependency)을 통해 NotificationCenter와 같은 시스템 이벤트를 손쉽게 -0 TCA 환경으로 끌어올 수 있습니다.
이 예제를 통해 TCA의 Long-Living Effect와 Cancellation을 비롯한 비동기 효과 처리 개념을 보다 쉽게 이해할 수 있길 바랍니다. Happy Coding!