맛동산이

SwiftUI) TCA - Navigation하는 방법(Presents, PresentationAction, ifLet) 본문

앱/SwiftUI

SwiftUI) TCA - Navigation하는 방법(Presents, PresentationAction, ifLet)

진ddang 2024. 9. 17. 04:47

두개의 reducer와 state그리고 화면을 이동하는 방법을 사용할때는

  • @Presents
  • PresentationAction
  • ifLet

을 사용하게 된다.

1. 먼저 가고자 하는 뷰의 State를 @Presents로 감싼다.

또한 네이게이션 stack이 될 array을 생성한다. (보편적으로 스택의 경우에는 최상단의 뷰가 가지면 된다.)

밑의 코드에서는 concats가 stack이 될 array

  @ObservableState
  struct State: Equatable {
    @Presents var addContact: AddContactFeature.State?
    var contacts: IdentifiedArrayOf<Contact> = []
  }

해당 뷰로 이동할지 안할지 모르니까 옵셔널 값.

2. 해당 뷰로 이동하는 액션을 정의( PresentationAction )

    case addContact(PresentationAction<AddContactFeature.Action>)

3. ifLet을 통해서 해당 뷰의 값을 받도록 한다.

리듀서에서 .ifLet값을 통해서 리듀서를 받도록 하며

    .ifLet(\\.$addContact, action: \\.addContact) {
      AddContactFeature()
    }

4. 하위뷰에서의 액션을 받을때

      case .addContact(.presented(.saveButtonTapped)):
        guard let contact = state.addContact?.contact
        else { return .none }
        state.contacts.append(contact)
        state.addContact = nil
        return .none

실제로 .presented상태일때 해당 뷰에서 .saveButtonTapped가 날라오면, 다음과 같은 액션을 취하도록 하는 effect 코드이다.

5. 뷰 연결해주기

struct ContactsView: View {
  @Bindable var store: StoreOf<ContactsFeature>
  
  var body: some View {
    NavigationStack {
      List {
        ForEach(store.contacts) { contact in
          Text(contact.name)
        }
      }
      .navigationTitle("Contacts")
      .toolbar {
        ToolbarItem {
          Button {
            store.send(.addButtonTapped)
          } label: {
            Image(systemName: "plus")
          }
        }
      }
    }
    .sheet(
      item: $store.scope(state: \\.addContact, action: \\.addContact)
    ) { addContactStore in
      NavigationStack {
        AddContactView(store: addContactStore)
      }
    }
  }
}

Bindable을 통해서 해당 값이 바인딩 될수 있도록 변경해주고,

scop을 사용해서 해당 state안의 만들어진 AddContactFeature.State? 를 해당 뷰에 주입해준다.

하위 뷰에서 상위뷰로의 액션을 전달해주는 방법

하위뷰에서 상위뷰로 액션을 전달해주고 싶을때는 동일하게 delegate를 사용하지만 이역시 action에서 정의해서 내려줘야한다.

방법은 다음과 같다 .

1. delegate액션과 enum을 정의한다.

struct AddContactFeature {
  @ObservableState
  struct State: Equatable {
    var contact: Contact
  }
  enum Action {
    case cancelButtonTapped
    case delegate(Delegate)
    case saveButtonTapped
    case setName(String)
    enum Delegate: Equatable {
      case cancel
      case saveContact(Contact)
    }
  }

위에서 Delegate가 상위 액션으로 전달될 delegate액션

2. 액션에서 호출 해준다.

    Reduce { state, action in
      switch action {
      case .cancelButtonTapped:
        return .send(.delegate(.cancel))
        
      case .delegate:
        return .none
        
      case .saveButtonTapped:
        return .send(.delegate(.saveContact(state.contact)))
        

이때 해당 뷰의 dismiss를 하기 위해서는 다음과 같은 액션으로 처리하면 된다.

      case .cancelButtonTapped:
        return .run { _ in await self.dismiss() }
        
       혹은 어떠한 액션을 취하고 난뒤라면
      case .saveButtonTapped:
        return .run { [contact = state.contact] send in
          await send(.delegate(.saveContact(contact)))
          await self.dismiss()
      }

3. 상위 PresentationAction에서 정의된 액션에서 해당 액션을 받아서 처리한다.

  enum Action {
    case addButtonTapped
    case addContact(PresentationAction<AddContactFeature.Action>)
  }      
      
      
      case .addContact(.presented(.delegate(.cancel))):
        state.addContact = nil
        return .none
        
      case let .addContact(.presented(.delegate(.saveContact(contact)))):
        // guard let contact = state.addContact?.contact
        // else { return .none }

 


또는 

        .navigationDestination(
            store: store.scope(state: \.$destination.personsList, action: \.destination.personsList)
        ) {
            PersonsListView(store: $0)
        }

이렇게 navigationDestination으로 네비게이션 할수 잇는 방법도 있음.

반응형