Skip to content
Merged
13 changes: 8 additions & 5 deletions Projects/App/Sources/View/AppRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@ private extension AppRootView {
if let route = currentRoute {
switch route {
case .login:
LoginRootView { nickname in
handleLoginCompleted(nickname: nickname)
} onSignupCompleted: { nickname in
handleSignupCompleted(nickname: nickname)
}
LoginContentView(
onLoginCompleted: { nickname in
handleLoginCompleted(nickname: nickname)
},
onSignupCompleted: { nickname in
handleSignupCompleted(nickname: nickname)
}
)

case .main:
LivithMainTabView(nickname: $nickname)
Expand Down
4 changes: 2 additions & 2 deletions Projects/App/Sources/View/LivithMainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ struct LivithMainTabView: View {
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTab) {
HomeRootView(nickname: $nickname)
HomeContentView(nickname: $nickname, isTabBarHidden: $isTabBarHidden)
.tag(Tab.home)
.toolbar(.hidden, for: .tabBar)

SearchRootView(isTabBarHidden: $isTabBarHidden)
SearchContentView(isTabBarHidden: $isTabBarHidden)
.tag(Tab.search)
.toolbar(.hidden, for: .tabBar)

Expand Down
4 changes: 0 additions & 4 deletions Projects/Core/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ let project = Project.make(
target: .core(.persistence),
product: .framework
),
.make(
target: .core(.router),
product: .framework
),
.make(
target: .core(.livithConcurrency),
product: .framework
Expand Down
15 changes: 0 additions & 15 deletions Projects/Core/Router/Sources/PresentationStyle.swift

This file was deleted.

56 changes: 0 additions & 56 deletions Projects/Core/Router/Sources/Router.swift

This file was deleted.

12 changes: 8 additions & 4 deletions Projects/DSKit/Sources/Component/SearchBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@ public struct SearchBarView: View {

@Binding var input: String
@FocusState private var isFocused: Bool
var onChange: () -> Void
var onClear: () -> Void
var onSubmit: () -> Void

private let onBack: () -> Void
private let onChange: () -> Void
private let onClear: () -> Void
private let onSubmit: () -> Void

// MARK: - LifeCycle

public init(
input: Binding<String>,
onBack: @escaping () -> Void,
onChange: @escaping () -> Void,
onClear: @escaping () -> Void,
onSubmit: @escaping () -> Void
) {
self._input = input
self.onBack = onBack
self.onChange = onChange
self.onClear = onClear
self.onSubmit = onSubmit
Expand All @@ -50,7 +54,7 @@ private extension SearchBarView {
@ViewBuilder
func backButton() -> some View {
Button (action: {
// TODO: 화면 전환 구현
onBack()
}) {
Image.livithIcon(.backLineDefault)
.resizable()
Expand Down
110 changes: 110 additions & 0 deletions Projects/DSKit/Sources/Coordinator/Coordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// Coordinator.swift
// DSKit
//
// Created by 김진웅 on 12/27/25.
// Copyright © 2025 Livith. All rights reserved.
//

import UIKit

/// 화면 전환 흐름을 관리하는 코디네이터 추상화.
///
/// - Note: `@MainActor`에서 동작하며 모든 UI 관련 트랜잭션을
/// 메인 스레드에서 안전하게 처리합니다.
/// - Important: 코디네이터는 네비게이션 상태 일관성을 유지해야 하며,
/// `buildViewController(for:)`는 주어진 `Route`에 대한 화면 생성을
/// 명확하고 예측 가능하게 구현해야 합니다.
///
/// 제네릭 파라미터
/// - `R`: 화면 전환을 정의하는 `Route` 타입
@MainActor
public protocol Coordinator: AnyObject {
associatedtype R: Route

/// 화면 전환을 주도하는 루트 `UINavigationController`.
var navigationController: UINavigationController { get }

/// 주어진 `route`에 대응하는 화면을 생성합니다.
/// - Parameter route: 화면을 생성할 `Route`
/// - Returns: 생성된 `UIViewController`
///
/// - Tip: SwiftUI의 `View`를 사용해 화면을 구성한다면
/// UIKit과의 호환을 위해 `UIHostingController(rootView:)`로 감싸서 반환하세요.
///
/// 예시:
/// ```swift
/// func buildViewController(for route: AppRoute) -> UIViewController {
/// switch route {
/// case .home:
/// let view = HomeView(viewModel: HomeViewModel())
/// return UIHostingController(rootView: view)
/// case .detail(let id):
/// let view = DetailView(id: id)
/// return UIHostingController(rootView: view)
/// }
/// }
/// ```
func buildViewController(for route: R) -> UIViewController

/// 코디네이터 시작 지점. 초기 화면 진입 로직을 구현합니다.
func start()
}

public extension Coordinator {
/// 지정한 `route`로 화면을 푸시합니다.
/// - Parameters:
/// - route: 이동할 대상 `Route`
/// - animated: 애니메이션 여부. 기본값은 `true`
func push(to route: R, animated: Bool = true) {
let viewController = buildViewController(for: route)
navigationController.pushViewController(viewController, animated: true)
}

/// 현재 화면을 하나 뒤로 되돌립니다.
/// - Parameter animated: 애니메이션 여부. 기본값은 `true`
func pop(animated: Bool = true) {
navigationController.popViewController(animated: animated)
}

/// 루트 화면까지 되돌립니다.
/// - Parameter animated: 애니메이션 여부. 기본값은 `true`
func popToRoot(animated: Bool = true) {
navigationController.popToRootViewController(animated: animated)
}

/// 지정한 `route`를 모달로 표시합니다.
/// - Parameters:
/// - route: 표시할 대상 `Route`
/// - animated: 애니메이션 여부. 기본값은 `true`
/// - presentationStyle: `UIModalPresentationStyle` 지정 시 해당 스타일로 표시
/// - transitionStyle: `UIModalTransitionStyle` 지정 시 해당 전환 효과 적용
/// - completion: 표시 완료 후 호출되는 클로저
func present(
to route: R,
animated: Bool = true,
presentationStyle: UIModalPresentationStyle? = nil,
transitionStyle: UIModalTransitionStyle? = nil,
completion: (() -> Void)? = nil
) {
let viewController = buildViewController(for: route)

if let style = presentationStyle {
viewController.modalPresentationStyle = style
}

if let style = transitionStyle {
viewController.modalTransitionStyle = style
}

navigationController.present(viewController, animated: animated, completion: completion)
}

/// 현재 표시 중인 모달을 닫습니다.
/// - Parameters:
/// - animated: 애니메이션 여부. 기본값은 `true`
/// - completion: 닫기 완료 후 호출되는 클로저
func dismiss(animated: Bool = true, completion: (() -> Void)? = nil) {
navigationController.dismiss(animated: animated, completion: completion)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//
// Route.swift
// core
// DSKit
//
// Created by 김진웅 on 10/12/25.
// Created by 김진웅 on 12/27/25.
// Copyright © 2025 Livith. All rights reserved.
//

import Foundation

public protocol Route: Hashable, Identifiable {}
public protocol Route: Hashable {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// EnvironmentValues+HomeCoordinator.swift
// HomeFeature
//
// Created by 김진웅 on 12/27/25.
// Copyright © 2025 Livith. All rights reserved.
//

import SwiftUI

private struct HomeCoordinatorKey: EnvironmentKey {
static let defaultValue: HomeCoordinator? = nil
}

extension EnvironmentValues {
var homeCoordinator: HomeCoordinator? {
get { self[HomeCoordinatorKey.self] }
set { self[HomeCoordinatorKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// HomeContentView.swift
// HomeFeature
//
// Created by 김진웅 on 12/27/25.
// Copyright © 2025 Livith. All rights reserved.
//

import SwiftUI
import UIKit

public struct HomeContentView: View {
@State private var coordinator: HomeCoordinator
@Binding private var isTabBarHidden: Bool

public init(nickname: Binding<String>, isTabBarHidden: Binding<Bool>) {
self._coordinator = State(initialValue: HomeCoordinator(nickname: nickname))
self._isTabBarHidden = isTabBarHidden
}

public var body: some View {
HomeNavigationHost(coordinator: coordinator, isTabBarHidden: $isTabBarHidden)
.ignoresSafeArea()
}
}

// MARK: - NavigationHost

private extension HomeContentView {
struct HomeNavigationHost: UIViewControllerRepresentable {
let coordinator: HomeCoordinator
@Binding var isTabBarHidden: Bool

func makeCoordinator() -> NavDelegate {
NavDelegate(isTabBarHidden: $isTabBarHidden)
}

final class NavDelegate: NSObject, UINavigationControllerDelegate {
@Binding var isTabBarHidden: Bool

init(isTabBarHidden: Binding<Bool>) {
self._isTabBarHidden = isTabBarHidden
}

func navigationController(
_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool
) {
let stackCount = navigationController.viewControllers.count
Task { @MainActor in
isTabBarHidden = stackCount > 1
}
}
}

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

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