Swift/Swift CS

[iOS/TCA] 01-GettingStarted-Animations

힛해 2024. 8. 29. 17:11
728x90

전체 소스 코드

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 열거형에서 특정 케이스를 다루기 쉽게 해주는 속성이다.

이 속성을 통해 각 케이스에 대한 패턴 매칭을 더 간편하게 수행할 수 있으며, 이를 통해 상태와 액션을 더 깔끔하게 처리할 수 있습니다.

주요 기능

  1. 케이스 매칭 간소화:
    • @CasePathable을 사용하면, 열거형의 특정 케이스를 보다 쉽게 매칭하고 처리할 수 있다.
    • 일반적으로 @CasePathable 속성은 enum 내부의 특정 케이스를 다룰 때 유용하다
  2. 패턴 매칭:
    • 이 속성을 사용하면, 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에 들어갈떄 사용한다.