[iOS/TCA] 01-GettingStarted-Animations
전체 소스 코드
import ComposableArchitecture
import SwiftUI
private let readMe = """
This screen demonstrates how changes to application state can drive animations. Because the \
`Store` processes actions sent to it synchronously you can typically perform animations in the \
Composable Architecture just as you would in regular SwiftUI.
To animate the changes made to state when an action is sent to the store, you can also pass \
along an explicit animation, or you can call `store.send` in a `withAnimation` block.
To animate changes made to state through a binding, you can call the `animation` method on \
`Binding`.
To animate asynchronous changes made to state via effects, use the `Effect.run` style of \
effects, which allows you to send actions with animations.
Try out the demo by tapping or dragging anywhere on the screen to move the dot, and by flipping \
the toggle at the bottom of the screen.
"""
@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)
}
}
struct AnimationsView: View {
@Bindable var store: StoreOf<Animations>
var body: some View {
VStack(alignment: .leading) {
Text(template: readMe, .body)
.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)
}
}
시연 영상
아직 모르는 부분
1..ifLet(\.$alert, action: \.alert)
2.@CasePathable
3.@Presents var alert: AlertState<Action.Alert>?
4..gesture(
DragGesture(minimumDistance: 0).onChanged { gesture in
store.send(
.tapped(gesture.location),
animation: .interactiveSpring(response: 0.25, dampingFraction: 0.1)
)
}
)
5.$store.isCircleScaled.sending(\.circleScaleToggleChanged)
6..alert($store.scope(state: \.alert, action: \.alert))
2.@CasePathable
@CasePathable 개요
@CasePathable은 ComposableArchitecture의 Action 열거형에서 특정 케이스를 다루기 쉽게 해주는 속성이다.
이 속성을 통해 각 케이스에 대한 패턴 매칭을 더 간편하게 수행할 수 있으며, 이를 통해 상태와 액션을 더 깔끔하게 처리할 수 있습니다.
주요 기능
- 케이스 매칭 간소화:
- @CasePathable을 사용하면, 열거형의 특정 케이스를 보다 쉽게 매칭하고 처리할 수 있다.
- 일반적으로 @CasePathable 속성은 enum 내부의 특정 케이스를 다룰 때 유용하다
- 패턴 매칭:
- 이 속성을 사용하면, Reducer의 body에서 케이스 패턴 매칭을 할 때 코드가 더 간결해진다.
- 이를 통해 복잡한 상태 관리와 액션 처리를 단순화할 수 있다.
enum Action {
case alert(PresentationAction<Alert>)
case alertButtonTapped
case confirmationDialog(PresentationAction<ConfirmationDialog>)
case confirmationDialogButtonTapped
@CasePathable
enum Alert {
case incrementButtonTapped
}
}
@CasePathable 속성을 사용하면, Action enum에서 alert 케이스를 다룰 때 Alert enum의 각 케이스를 직접적으로 처리할 수 있다.
예를 들어, Action.alert(.presented(.resetConfirmationButtonTapped))와 같은 표현식이 가능하다.
5. sending
public func sending(_ action: CaseKeyPath<Action, Value>) -> Binding<Value> {
self.bindable[state: self.keyPath, action: action]
}
@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)
isOn: $store.isCircleScaled.sending(\.circleScaleToggleChanged)
Store값 Observable 값이 Action에 들어갈떄 사용한다.