본문 바로가기
Swift/TCA

[TCA] day 1 TCA에서 Alert와 Dialog를 다루기. GettingStarted-AlertsAndConfirmationDialogs

by 마라민초닭발로제 2024. 4. 19.

 

 

전체 코드

import ComposableArchitecture
import SwiftUI

private let readMe = """
  This demonstrates how to best handle alerts and confirmation dialogs in the Composable \
  Architecture.

  The library comes with two types, `AlertState` and `ConfirmationDialogState`, which are data \
  descriptions of the state and actions of an alert or dialog. These types can be constructed in \
  reducers to control whether or not an alert or confirmation dialog is displayed, and \
  corresponding view modifiers, `alert(_:)` and `confirmationDialog(_:)`, can be handed bindings \
  to a store focused on an alert or dialog domain so that the alert or dialog can be displayed in \
  the view.

  The benefit of using these types is that you can get full test coverage on how a user interacts \
  with alerts and dialogs in your application
  """

@Reducer
struct AlertAndConfirmationDialog {
  
  @ObservableState
  struct State {
    @Presents var alert: AlertState<Action.Alert>?
    @Presents var confirmationDialo: ConfirmationDialogState<Action.ConfirmationDialog>?
    @Presents var customAlert: CustomAlert?
    var count = 0
  }
  
  enum Action: Equatable {
    case alert(PresentationAction<Alert>)
    case alertButtonTapped
    case confirmationDialog(PresentationAction<ConfirmationDialog>)
    case confirmationDialogButtonTapped
    case customAlertTapped
    case customAlert(PresentationAction<CustomAlert>)
    case confirmationStateOfAlertAndDialog
    
    
    @CasePathable
    enum Alert {
      case incrementButtonTapped
    }
    
    @CasePathable
    enum ConfirmationDialog {
      case incrementButtonTapped
      case decrementButtonTapped
    }
    
    @CasePathable
    enum CustomAlert{
      case closeTapped
    }
  }
  
  var body: some Reducer<State, Action> {
    Reduce{ state, action in
      
      switch action {
      case .customAlertTapped:
        state.customAlert = CustomAlert()
        return .none
      case .customAlert(.presented(.closeTapped)):
        return .none
      case .customAlert:
        return .none
        
      case .confirmationStateOfAlertAndDialog :
        print("alert: \(state.alert == nil), dialog: \(state.confirmationDialo == nil)")
        return .none
      case .alert(.presented(.incrementButtonTapped)),
          .confirmationDialog(.presented(.incrementButtonTapped))
        :
        state.alert = AlertState{
          TextState("Incremented")
        }
        state.count += 1
        return .none
        
      case .alert:
        return .none
        
      case .alertButtonTapped:
        state.alert = AlertState {
          TextState("Alert!")
        } actions: {
          
          ButtonState(action: .incrementButtonTapped) {
            TextState("Increment")
          }
        } message: {
          TextState("This is an Alert")
        }
        return .none
    
      case .confirmationDialog(.presented(.decrementButtonTapped)) :
        state.alert = AlertState { TextState("Decremented!")}
        state.count -= 1
        return .none
        
      case .confirmationDialog(.presented(.incrementButtonTapped)) :
        state.alert = AlertState { TextState("increment!!")}
        state.count += 1
        return .none
        
      case .confirmationDialog:
        return .none
        
      case .confirmationDialogButtonTapped:
        state.confirmationDialo = ConfirmationDialogState {
          TextState("Confirmnation dialog")
        } actions: {
          ButtonState(role: .cancel) {
            TextState("Cancel")
          }
          ButtonState(action: .decrementButtonTapped) {
            TextState("DecrementButtonTapped")
          }
          ButtonState(action: .incrementButtonTapped) {
            TextState("incrementButtonTapped")
          }
        }message: {
          TextState("This is a confirmnation Dialog.")
        }
        return .none
      }
    }
    .ifLet(\.$alert, action: \.alert)
    .ifLet(\.$confirmationDialo, action: \.confirmationDialog)
//    .ifLet(\.$customAlert, action: \.confirmationDialog)
  }
}

struct AlertAndConfirmationDialogView: View {
  @Bindable var store: StoreOf<AlertAndConfirmationDialog>

  var body: some View {
    
    Form {
      Section {
        AboutView(readMe: readMe)
      }

      Text("Count: \(store.count)")
      Button("Alert") { store.send(.alertButtonTapped) }
      Button("Confirmation Dialog") { store.send(.confirmationDialogButtonTapped) }
      
      Button("상태 확인하기") {
        store.send(.confirmationStateOfAlertAndDialog)
      }
      Button("커스텀 얼럿!") {
        store.send(.customAlertTapped)
      }
      Section {
        if let alert = store.state.customAlert {
          alert
        }
      }
    }
    .navigationTitle("Alerts & Dialogs")
    .alert($store.scope(state: \.alert, action: \.alert))
    .confirmationDialog($store.scope(state: \.confirmationDialo, action: \.confirmationDialog))
  }
}

struct CustomAlert: View {
  var body: some View {
    VStack{
      Button("Close") {
        
      }
    }
  }
}

 

 

- @Presents

State에 다음과 같은 case 들이 존재합니다.  @Presents var alert, @Presents var confirmationDialog 이 앞에 @Presents들은 무엇일까 고민해봤습니다.  Depth에는 다음과 같이 적혔습니다. 봐도 모르겠네요. Alert와 Dialog 가 Presents될 수 있게 만들어주는 Macro같습니다. 

 /// Wraps a property with ``PresentationState`` and observes it.
  ///
  /// Use this macro instead of ``PresentationState`` when you adopt the ``ObservableState()``
  /// macro, which is incompatible with property wrappers like ``PresentationState``.

 

 

 

- @CasePathable

CasePathable의 매크로를 작성함으로서, Alert와 Dialog의 상세 동작을 적어줍니다. 또한 Reduce에서 분기를 칠 수 있게, Action 에 case 를caseconfirmationDialog(PresentationAction<ConfirmationDialog>) 과 같이 작성합니다. 이렇게 된다면, reduce 에서 Action을 분기할 수 있습니다. 관련 문서에는 다음과 같이 적혀 있습니다.

  /// Defines and implements conformance of the CasePathable protocol.
  ///
  /// This macro conforms the type to the ``CasePathable`` protocol, and adds ``CaseKeyPath``
  /// support for all its cases.
  ///
  /// For example, the following code applies the `CasePathable` macro to the type `UserAction`:
  ///
  /// ```swift
  /// @CasePathable
  /// enum UserAction {
  ///   case home(HomeAction)
  ///   case settings(SettingsAction)
  /// }
  /// ```
  ///
  /// This macro application extends the type with the ability to derive a case key paths from each
  /// of its cases using a familiar key path expression:
  ///
  /// ```swift
  /// // Case key paths can be inferred using the same name as the case:
  /// _: CaseKeyPath<UserAction, HomeAction> = \.home
  /// _: CaseKeyPath<UserAction, SettingsAction> = \.settings
  ///
  /// // Or they can be fully qualified under the type's `Cases`:
  /// \UserAction.Cases.home      // CasePath<UserAction, HomeAction>
  /// \UserAction.Cases.settings  // CasePath<UserAction, SettingsAction>
  /// ```

 

 

 

iflet(\.$alert, action: \.$alert) 은 Reducer를 리턴합니다. Alert가 Reducer가 필요없는 이유가 바로 이것입니다. 이후 뷰에

.alert($store.scope(state: \.alert, action: \.alert)) 다음과 같은 코드가 있는데, iflet을 통해서 생성된 Reducer를 통해서 화면에 띄어주는 역할을 합니다.