
안녕하세요, 별똥별🌠입니다!
이번 글에서는 Composable Architecture (TCA)를 사용해 애니메이션과 상태를 관리하는 방법을 소개합니다.
드래그 제스처, 색상 변경, 스케일 효과 등 다양한 UI 동작을 TCA로 어떻게 제어할 수 있는지 살펴보겠습니다. 🚀
Animations Reducer는 사용자의 제스처와 애니메이션 상태를 관리하며, 색상 변경, 스케일 토글, 초기화 등의 기능을 구현합니다.
@Reducer
struct Animations {
@ObservableState
struct State: Equatable {
@Presents var alert: AlertState<Action.Alert>?
var circleCenter: CGPoint?
var circleColor = Color.black
var isCircleScaled = false
}
enum Action: Sendable {
case alert(PresentationAction<Alert>)
case circleScaleToggleChanged(Bool)
case rainbowButtonTapped
case resetButtonTapped
case setColor(Color)
case tapped(CGPoint)
@CasePathable
enum Alert: Sendable {
case resetConfirmationButtonTapped
}
}
@Dependency(\.continuousClock) var clock
private enum CancelID { case rainbow }
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .alert(.presented(.resetConfirmationButtonTapped)):
state = State()
return .cancel(id: CancelID.rainbow)
case .alert:
return .none
case let .circleScaleToggleChanged(isScaled):
state.isCircleScaled = isScaled
return .none
case .rainbowButtonTapped:
return .run { send in
for color in [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black] {
await send(.setColor(color), animation: .linear)
try await clock.sleep(for: .seconds(1))
}
}
.cancellable(id: CancelID.rainbow)
case .resetButtonTapped:
state.alert = AlertState {
TextState("Reset state?")
} actions: {
ButtonState(
role: .destructive,
action: .send(.resetConfirmationButtonTapped, animation: .default)) {
TextState("Reset")
}
ButtonState(role: .cancel) {
TextState("Cancel")
}
}
return .none
case let.setColor(color):
state.circleColor = color
return .none
case let .tapped(point):
state.circleCenter = point
return .none
}
}
.ifLet(\.$alert, action: \.alert)
}
}
주요 포인트
- State 정의
- circleCenter: 사용자가 화면을 터치한 위치를 저장합니다.
- circleColor: 원의 색상을 나타냅니다.
- isCircleScaled: 원의 크기를 제어합니다.
- alert: 경고 창 상태를 관리합니다.
- Action 정의
- rainbowButtonTapped: 색상을 무지개 순서로 변경합니다.
- resetButtonTapped: 초기화 경고창을 표시합니다.
- tapped: 화면 터치 위치를 기록합니다.
- Reducer 로직
- rainbowButtonTapped:
* 1초 간격으로 색상을 변경하며, CancelID.rainbow를 사용해 취소 가능합니다.- resetButtonTapped:
* 초기화 경고창을 생성하며, 초기화 버튼 클릭 시 상태를 재설정합니다.
AnimationsView는 Reducer의 상태와 액션을 기반으로 사용자 인터페이스를 구성합니다.
struct AnimationsView: View {
@Bindable var store: StoreOf<Animations>
var body: some View {
VStack(alignment: .leading) {
Text(template: readMe, .body)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.gesture(
DragGesture(minimumDistance: 0).onChanged { gesture in
store.send(
.tapped(gesture.location),
animation: .interactiveSpring(response: 0.25, dampingFraction: 0.1)
)
}
)
.overlay {
GeometryReader { proxy in
Circle()
.fill(store.circleColor)
.colorInvert()
.blendMode(.difference)
.frame(width: 50, height: 50)
.scaleEffect(store.isCircleScaled ? 2 : 1)
.position(
x: store.circleCenter?.x ?? proxy.size.width / 2,
y: store.circleCenter?.y ?? proxy.size.height / 2
)
.offset(y: store.circleCenter == nil ? 0 : -44)
}
.allowsHitTesting(false)
}
Toggle("Big mode", isOn: $store.isCircleScaled.sending(\.circleScaleToggleChanged)
.animation(.interactiveSpring(response: 0.25, dampingFraction: 0.1))
)
.padding()
Button("Rainbow") { store.send(.rainbowButtonTapped, animation: .linear) }
.padding([.horizontal, .bottom])
Button("Reset") { store.send(.resetButtonTapped) }
.padding([.horizontal, .bottom])
}
.alert($store.scope(state: \.alert, action: \.alert))
.navigationBarTitleDisplayMode(.inline)
}
}
주요 포인트
- 드래그 제스처
- 사용자가 화면을 터치하면 해당 위치를 기록하고 원의 중심을 이동시킵니다.
- 애니메이션 효과
- scaleEffect를 활용해 원의 크기를 조정하며, 스프링 애니메이션으로 부드럽게 전환됩니다.
- 버튼 동작
- "Rainbow" 버튼: 색상이 순차적으로 변경됩니다.
- "Reset" 버튼: 초기화 경고창을 표시합니다.
Preview를 통해 UI 동작을 테스트합니다.
#Preview {
NavigationStack {
AnimationsView(
store: Store(initialState: Animations.State()) {
Animations()
}
)
}
.environment(\.colorScheme, .dark)
}

- TCA로 애니메이션 상태 관리
- Reducer에서 애니메이션 상태와 동작을 관리해 뷰 로직과 상태를 분리할 수 있습니다.
- DragGesture 활용
- 제스처 이벤트와 애니메이션을 결합해 역동적인 UI를 구현할 수 있습니다.
- cancellable로 작업 취소
- 긴 작업은 CancelID를 통해 안전하게 취소할 수 있습니다.
이번 글에서는 TCA를 활용해 애니메이션과 제스처를 관리하는 방법을 살펴봤습니다.
TCA의 강력한 상태 관리 기능과 SwiftUI의 유연한 애니메이션 API를 결합하면 더욱 직관적이고 역동적인 사용자 경험을 제공할 수 있습니다. 😊
다음 글에서는 또 다른 TCA 활용 사례로 찾아뵙겠습니다! 🌠