/Router

Primary LanguageSwiftMIT LicenseMIT

Router

SwiftUI에서 NavigationPresentation을 하나로 관리하는 화면 전환 인터페이스입니다.

Router는 다음과 같은 목표를 위해 구현되었습니다.

  • 복잡한 화면 제어가 가능
  • 확장 및 협업에 용이하도록 PathType Enum의 분리
  • 제한된 방식으로만 NavigationStackPath 접근 가능
  • Push, Pop 외의 다른 화면 이동 로직(sheet, fullScreenCover)도 한 번에 관리

장점

  1. 화면 전환 로직에 사용되는 Enum을 분리된 파일에서 관리
  • 기능별로 화면을 관리하는 데 용이합니다.
  • 협업 시 하나의 Enum에서 path 타입을 관리했을 때, 충돌이 발생하는 문제를 해결합니다.
// 기존 방식

// PathType.swift
enum PathType {
    case first
    case second

    ... // 동시에 여러 화면 전환 작업 시 충돌

    case twentyth
}

// Router 방식

// FirstRoute.swift
enum FirstRoute: Routable {
    case first
    case second
    ...
}

// SecondRoute.swift
enum SecondRoute: Routable {
    case first
    case second
    ...
}

// ThirdRoute.swift
enum ThirdRoute: Routable {
    case first
    case second
    ...
}

  1. NavigationStack의 Path를 제한된 메서드로만 접근할 수 있습니다.
  • 기존 Path 변수를 외부에서 주입해줘야했던 것과 달리, 외부로 Path가 노출되지 않습니다.
  • 예상치 못한 방법으로 Path가 수정되는 것을 방지합니다.
// 기존 방식
NavigationStack(path: $path) { ... } // 외부로 Path 변수가 노출

// Router 방식
Router.shared.Stack { ... } // Router 내부에서 Private 하게 path 변수를 관리합니다.

Description

v1 - PushRouter 제공 메서드

extension Router {
    // 1개 화면 push
    public func push(to routeType: any Routable) { ... }
    
    // N개 화면 push
    public func push(to routeTypes: [any Routable]) { ... }
    
    // 1개 화면 pop
    public func pop() { ... }
    
    // Root 화면으로 pop
    public func popToRoot() { ... }
    
    // Flow의 Root 화면으로 pop
    public func popToRoot(of routeType: RouteType) { ... }
    
    // Flow 전체를 pop
    public func endRoute(of routeType: RouteType) { ... }
}

v2 - Router 추가 제공 메서드

  • 단, sheet, coverpresentation 된 이후의 push는 지원하지 않습니다.
extension Router {
    public func sheet(
        to routeType: any Routable,
        detents: Set<PresentationDetent>, // sheet 사이즈 조절
        indicatorVisibility: Visibility, // indicator 여부
        onDismiss: (() -> Void)?
    ) { ... }

    public func fullScreenCover(to routeType: any Routable, onDismiss: (() -> Void)?) { ... }
    
    public func dismiss() { ... }
}

Getting Started

  1. RouteType EnumRoute 타입을 추가합니다.
enum RouteType {
    case root

    // 여기에 RouteType을 추가
    case login
}
  1. Routable protocol을 준수하는 Enum을 선언합니다.
enum LoginRoute: Routable {

    // Login 관련 Route 종류
    case login1
    case login2
    case loginWithData(data: Data) // 주입할 데이터가 있는 경우
    
    // Flow 타입 -> login Flow에 있는 화면들!
    var type: RouteType { .login }
    
    // navigationDestination에 매핑될 뷰
    @ViewBuilder
    internal func NavigatingView() -> some View {
        switch self {
        case .login1:
            LoginView1()
        case .login2:
            LoginView2()
        case loginWithData(let data): // 주입할 데이터가 있는 경우
            LoginWithDataView(data: data)
        }
    }
    
    // Route의 네이밍
    var description: String {
        switch self {
        case .login1:
            "login1"
        case .login2:
            "login2"
        case loginWithData:
            "loginWithData"
        }
    }
}
  1. RouterStack을 최상단에 선언합니다.
struct ContentView: View {
    var body: some View {
        Router.shared.Stack { // path가 외부에 노출되지 않습니다.
            ...
        }
    }
}
  1. 화면 전환이 일어나는 곳에서 메서드를 호출합니다.
struct LoginRootView: View {
    var body: some View {
        Button("Push Login1") {
            Router.shared.push(to: LoginRoute.login1)
        }
        
        Button("Pop All Login") {
            Router.shared.endRoute(of: .login)
        }
        
        Button("Pop to Root") {
            Router.shared.popToRoot()
        }
    }
}