SwifUI 코디네이터

Ios_Roy·2025년 8월 11일
0

TIL

목록 보기
18/25
post-thumbnail

SwiftUI에서 “가벼운 코디네이터” 만들기

NavigationControlling + 액션 기반 IntroduceCoordinator

SwiftUI의 NavigationStack만으로 화면 전환을 하다 보면 뒤로 가기, 루트 리셋, 딥링크 같은 공통 로직이 뷰에 흩어지기 쉽습니다.
아래처럼 작은 프로토콜 + 액션 기반 코디네이터를 두면, 뷰는 의도(Action) 만 보내고 라우팅은 코디네이터가 책임집니다.


목표

  • 네비게이션 로직을 뷰에서 분리
  • 공통 동작(뒤로/리셋) 재사용
  • 액션 기반 API로 의도 명확화
  • 딥링크·테스트·확장 용이

1) NavigationControlling 프로토콜

NavigationStack(path:)에 바인딩되는 path와, 공통으로 쓰는 start / goBack / reset 인터페이스를 정의합니다.
기본 구현을 extension으로 제공해, 채택 측(코디네이터)이 최소만 구현하게 합니다.

import SwiftUI

/// NavigationStack 기반 화면 전환을 제어하는 공통 프로토콜
protocol NavigationControlling: AnyObject {
  var path: NavigationPath { get set } // NavigationStack(path:)
  func start()                         // 초기 진입(루트 구성)
  func goBack()                        // 뒤로
  func reset()                         // 루트로
}

extension NavigationControlling {
  func goBack() {
    guard !path.isEmpty else { return }
    path.removeLast()
  }
  func reset() {
    path.removeLast(path.count)
  }
}

2) 라우트 정의 (확장에 열려 있게)

라우트는 단순 enum이 유지보수에 유리합니다. 새 화면 추가 시 영향 범위가 작고, 테스트/프리뷰도 쉽습니다.

enum IntroduceRoute: Hashable {
  case introduceMain
  case teamAgreement
  case teamIntroduce
  case teamBlog
}

✍️ 단순 enum으로 유지하면 새 라우트 추가 시 영향 범위가 작고 테스트·미리보기도 편합니다.
팁: 라우트마다 연관값이 필요하면 case teamBlog(postID: String)처럼 확장하세요. Hashable만 유지되면 됩니다.

3) IntroduceCoordinator (액션 기반)

화면 전환 의도를 Action enum으로 표현합니다.
뷰는 coordinator.send(.presentDetail(.teamAgreement))처럼 의도만 보냅니다.

import SwiftUI
import Combine

final class IntroduceCoordinator: NavigationControlling, ObservableObject {

  @Published var path = NavigationPath()

  // 화면 전환에 대한 "의도"
  enum Action {
    case start
    case presentMain
    case presentDetail(IntroduceRoute) // 연관값으로 확장성 확보
    case replace([IntroduceRoute])     // 스택 교체
    case pop
    case popToRoot
  }

  func send(_ action: Action) {
    switch action {
    case .start:
      reset()
      send(.presentMain)

    case .presentMain:
      path.append(.introduceMain)

    case .presentDetail(let route):
      path.append(route)

    case .replace(let routes):
      replaceStack(routes)

    case .pop:
      goBack()

    case .popToRoot:
      reset()
    }
  }

  // MARK: - NavigationControlling
  func start() {
    reset()
    send(.presentMain)
  }

  // 필요 시 외부에서 직접 호출 가능하도록 유지
  func reset() { path = .init() }
}

스택 교체 헬퍼

정 흐름(루트 → 상세)을 한 번에 구성해야 할 때 씁니다.
애니메이션 on/off도 선택 가능.

extension IntroduceCoordinator {
  /// 현재 스택을 전달된 라우트 목록으로 교체
  func replaceStack(_ routes: [IntroduceRoute], animated: Bool = true) {
    let apply = {
      self.path = .init()
      routes.forEach { self.path.append($0) }
    }
    if animated {
      withAnimation(.default) { apply() }
    } else {
      apply()
    }
  }

  /// 단일 라우트로 교체
  func replaceStack(_ route: IntroduceRoute, animated: Bool = true) {
    replaceStack([route], animated: animated)
  }
}

3) 테스트/프리뷰 팁

• 라우트가 단순 enum이라 스냅샷/내비게이션 시나리오 테스트가 수월합니다.
• 프리뷰에서 IntroduceCoordinator()를 주입해 라우팅 동작을 확인할 수 있어요.

#Preview {
 IntroduceRootView()
   .environmentObject(IntroduceCoordinator())
}

4) 확장 팁 & 베스트 프랙티스

  • 스레드 보장: 내비게이션 변경은 메인 스레드에서만. 필요하면 @MainActor를 코디네이터에 붙이세요.
  • 의존성 주입: 코디네이터에서 뷰모델 생성이 필요하다면 팩토리/DI를 주입해 테스트 가능성을 높이세요.
  • 연관값 라우트: 상세 화면이 식별자를 필요로 하면, case teamBlog(postID: String)처럼 모델 키만 넘기고, 뷰에서 조회하세요.
  • 단일 책임: 액션은 “의도”만 표현하고, 비즈니스 로직은 뷰모델에서 처리합니다.

마무리 요약

  • 작은 프로토콜 + 액션 코디네이터만으로도 라우팅이 정돈됩니다.
  • 뷰는 의도(Action) 를 보낼 뿐, 실제 경로 조작은 코디네이터가 전담합니다.
  • replaceStack으로 딥링크/재진입까지 간단히 처리할 수 있습니다.
  • 라우트는 단순 enum으로 유지해 확장/테스트/프리뷰를 쉽게 만드세요.
profile
iOS 개발자 공부하는 Roy

0개의 댓글