Skip to content

Conversation

@JinUng41
Copy link
Contributor

@JinUng41 JinUng41 commented Dec 26, 2025

PULL REQUEST

📄 작업 내용

  • UINavigationController 기반의 코디네이터를 구현하였습니다.
  • 기존의 라우터가 적용된 UI에 대해서 코디네이터로 대체하였습니다.
  • 사용하지 않는 라우터 모듈을 삭제했습니다.

💻 주요 코드 설명

코디네이터 사용하기

1. 코어 개념

  • Coordinator 프로토콜

    • UIKit UINavigationController를 보유하고 buildViewController(for:)에서 Route별 화면을 생성
    • 제공 메서드: push(to:), pop(), popToRoot(), present(to:presentationStyle:transitionStyle:), dismiss()
  • Route 프로토콜

    • 화면 전환 목적지를 나타내는 enum을 프로젝트별로 정의
  • EnvironmentKey & EnvironmentValues

    • Coordinator를 @Observable 없이 SwiftUI 환경에 주입하기 위한 메커니즘
    • 네비게이션은 상태 관찰이 불필요하므로 Observable 오버헤드를 제거하고 순수 참조만 유지
    • 각 Feature별 독립적인 환경 키로 타입 안전성 확보 및 테스트 Mock 주입 용이

2. How?

1) Route 정의

화면 전환 단위를 enum case로 설계 (필요 데이터는 연관값으로 전달)

import DSKit

enum FooRoute: Route {
    case list
    case detail(id: Int)
    case web(URL)
}

2) EnvironmentKey 정의

각 Feature의 Coordinator를 환경에 주입하기 위한 키 정의

// EnvironmentValues+FooCoordinator.swift
import SwiftUI

private struct FooCoordinatorKey: EnvironmentKey {
    static let defaultValue: FooCoordinator? = nil
}

extension EnvironmentValues {
    var fooCoordinator: FooCoordinator? {
        get { self[FooCoordinatorKey.self] }
        set { self[FooCoordinatorKey.self] = newValue }
    }
}

3) Coordinator 구현

  • buildViewController(for:)는 모든 Route 케이스에 대해 처리
  • SwiftUI View는 UIHostingController로 감싸고 .environment(\.fooCoordinator, self)로 주입
import SwiftUI
import UIKit
import DSKit

final class FooCoordinator: Coordinator {
    typealias R = FooRoute
    let navigationController = UINavigationController()

    init() {
        navigationController.setNavigationBarHidden(true, animated: false)
    }

    func start() {
        push(to: .list, animated: false)
    }

    func buildViewController(for route: FooRoute) -> UIViewController {
        switch route {
        case .list:
            return UIHostingController(
                rootView: FooListView().environment(\.fooCoordinator, self)
            )

        case .detail(let id):
            return UIHostingController(
                rootView: FooDetailView(id: id).environment(\.fooCoordinator, self)
            )

        case .web(let url):
            return UIHostingController(
                rootView: SafariView(url: url).ignoresSafeArea()
            )
        }
    }
}

4) Navigation Host 연결 (Tab 또는 부모 컨테이너에서 시작)

  • 첫 진입 시 start() 호출
  • 탭바 숨김 처리 필요 시 Search/Home처럼 UINavigationControllerDelegate로 스택 변화 감시
import SwiftUI
import UIKit

public struct FooContentView: View {
    @State private var coordinator = FooCoordinator()

    public var body: some View {
        FooNavigationHost(coordinator: coordinator)
            .ignoresSafeArea()
    }
}

private extension FooContentView {
    struct FooNavigationHost: UIViewControllerRepresentable {
        let coordinator: FooCoordinator

        func makeUIViewController(context: Context) -> UINavigationController {
            let nav = coordinator.navigationController
            if nav.viewControllers.isEmpty {
                coordinator.start()
            }
            return nav
        }

        func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
    }
}

5) 뷰에서 Coordinator 접근

  • @Environment(\.fooCoordinator)로 접근
  • Optional이므로 coordinator? 체이닝 사용
import SwiftUI
import DSKit

struct FooListView: View {
    @Environment(\.fooCoordinator) private var coordinator

    var body: some View {
        VStack {
            Button("상세 화면으로 이동") {
                coordinator?.push(to: .detail(id: 123))
            }
        }
    }
}

6) 화면 전환 호출 예시

  • Push: coordinator?.push(to: .detail(id: 123))
  • Modal Present: coordinator?.present(to: .web(url), presentationStyle: .fullScreen, transitionStyle: .crossDissolve)
  • Pop / PopToRoot: coordinator?.pop(), coordinator?.popToRoot()
  • Dismiss: coordinator?.dismiss()

3. 핵심 개념 상세

UIViewControllerRepresentable

  • SwiftUI에서 기존 UIKit의 UIViewController를 생성, 관리하기 위한 브릿지 역할
  • makeUIViewController(context:)는 컨트롤러 생성을, updateUIViewController(_:context:)는 SwiftUI 상태 변경 시 기존 컨트롤러에 필요한 변경을 적용
  • 네비게이션은 Coordinator가 담당하기 때문에 updateUIViewController(_:context:)에서 아무 동작을 수행하지 않음

EnvironmentKey를 사용하는 이유

1. Observable 오버헤드 제거

  • Coordinator는 네비게이션 API만 제공하며, 뷰가 상태 변화를 관찰할 필요 없음
  • @Observable은 상태 관찰 시스템을 활성화하므로 불필요한 경우 제거

2. 타입 안전한 주입

  • EnvironmentKey 방식은 일반 클래스도 환경에 주입 가능
  • Feature별로 독립적인 키(homeCoordinator, loginCoordinator, searchCoordinator)로 명확한 분리

4. 실제 프로젝트 예시

Home Module

  • Route: HomeRoute (home, interest)
  • Environment Key: \.homeCoordinator
  • 사용처: HomeView, InterestTempView

Login Module

  • Route: LoginRoute (login, terms, nickname, loginForbidden, signupFailed, safari)
  • Environment Key: \.loginCoordinator
  • 사용처: LoginView, TermsView, NicknameSettingView

Search Module

  • Route: SearchRoute (explore, search)
  • Environment Key: \.searchCoordinator
  • 사용처: ExploreView, SearchView

📚 참고자료

  • 내 두뇌

🔗 연결된 이슈

@JinUng41 JinUng41 requested a review from youz2me December 26, 2025 17:39
@JinUng41 JinUng41 self-assigned this Dec 26, 2025
@JinUng41 JinUng41 added 👨‍💻 진웅 불좀꺼줄래? 개발하게 🚀 Refactor labels Dec 26, 2025
- 코디네이터로 변경된 후, 상태 관찰이 필요 없어졌기 때문에 Envrionment 키값 방식으로 변경
Copy link
Member

@youz2me youz2me left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~

@JinUng41 JinUng41 merged commit 6de611b into develop Dec 28, 2025
@JinUng41 JinUng41 deleted the refactor/#90-coordinator branch December 29, 2025 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👨‍💻 진웅 불좀꺼줄래? 개발하게 🚀 Refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] UINavigationController 기반 코디네이터 구현

3 participants