Navigator 패턴 이용해보기

2025. 9. 28. 15:54·iOS/SwiftUI
반응형

👋 들어가기 전

 

SwiftUI에서 네비게이션을 쓸 때, 생각보다 번거로운 점이 있다.

 

어떻게 관리해야하고, 어디서 넣어줘여하는 지 고민됐다.

 

그래서 한번 추상화를 통해 개선해봤다.


1️⃣ 네비게이션은 뷰모델을 넘김으로써, 시작된다.

 

NavigationStack<Data, Root> : View where Root : View {
...
@MainActor @preconcurrency public init(
	path: Binding<NavigationPath>, 
	@ViewBuilder root: () -> Root) where Data == NavigationPath
}

 

네비게이션 Stack의 생성자를 보면 NavigationPath가 있다.

저 path 파라미터에 값이 append될 때, 네비게이션은 다음 스택으로 들어간다.

 

이 때 중요한점은 path에 들어값 값은 반드시 Hashable 해야한다.

 

이후, 값을 넣어주면, navigationDestination함수에서, 넘겨준 값과 함께, 이동될 하면을 설정한다

 

아래는 Int타입을 넘겨주면, 해당 숫자를 Text뷰에 표시할 화면으로 넘어가는 것 

 

위 내용응 정리하면, Hashable한 ViewModel을 path에 넘겨주면, 원하는 어떨까???

 

바로 이런식으로 사용할 수 있게된다.


2️⃣ 같은 Navigation Stack을 공유하면, path를 알고 있어야한다.

 

예를들어 다음과 같이 이동한다고 가정해보자. AView(RootView) -> BView -> CView

 

1번 규칙에 따르면, 이동하기위해 path에 다음뷰의 뷰모델을 넣어줘야하는데

새로운 path를 각 화면에서 생성하면, 전혀 이어지지 앟는 구조다.

 

또한 viewModel이 Path를 관리하는게 맞을까?? 라는 고민이 든다.

그래서 나는 Navigation을 담당하는 객체 Navigator를 만들어 각 ViewModel은 Navigation할 때

 

Navigator에게 요청하면되는 구조로 만들려한다.

 

정리하면 다음과 같은 구조가 나올 것 같다.

 


3️⃣ 구현

 

📎 HashableObject 프로토콜

import Foundation

public protocol HashableObject: AnyObject, Hashable, Identifiable {}

public extension HashableObject {
  nonisolated static func == (lhs: Self, rhs: Self) -> Bool {
    lhs === rhs
  }

  nonisolated func hash(into hasher: inout Hasher) {
    hasher.combine(self.id)
  }
}

 

🙏 NavigatableViewModel 수퍼클래스

import Foundation

@MainActor
open class NavigatableViewModel: HashableObject {
  public weak var navigator: (any Navigatable)?

  public func setNavigator(_ navigator: Navigatable?) {
    self.navigator = navigator
  }

  public func navigate(_ viewModel: any HashableObject) {
    navigator?.navigate(viewModel)
  }

  public func navigate(_ value: any Hashable) {
    navigator?.navigate(value)
  }

  public func pop() {
    navigator?.pop()
  }
}

 

뷰모델 내부적으로 navigator를 갖고있는 뷰모델을 의미하며, navigator에 요청할 api들이 제공된다.

다음 ViewModel을 전달하는 경우와, 단순 value로 보여질 때 모두를 고려하고 있다.

 

✈️ Navigator

import SwiftUI

@MainActor
public protocol Navigatable: AnyObject {
  var path: NavigationPath { get set }

  func navigate(_ viewModel: any HashableObject)

  func navigate(_ value: any Hashable)

  func pop()
}

@Observable
public final class Navigator: Navigatable {
  public var path: NavigationPath = .init()

  public func navigate(_ viewModel: any HashableObject) {
    path.append(viewModel)
  }

  public func navigate(_ value: any Hashable) {
    path.append(value)
  }

  public func pop() {
    if !path.isEmpty {
      let _ = path.removeLast()
    }
  }
}

 

viewModel로 부터 다음 viewModel 또는 value를 받아 path에 넘겨준다.

 

👔 실제 사용

@Observable
final class AViewModel: NavigatableViewModel {
  func navigate() {
    let viewModel = BViewModel()
    viewModel.setNavigator(navigator)
    navigator?.navigate(viewModel)
  }
}

struct ContentView: View {
  @State var navigator = Navigator()
  @State var viewModel: AViewModel

  init(viewModel: AViewModel) {
    self._viewModel = State(initialValue: viewModel)
  }

  var body: some View {
    NavigationStack(path: $navigator.path) {
      Button {
        viewModel.navigate()
      } label: {
        Text("Navigate to B")
      }
      .navigationDestination(for: BViewModel.self) { viewModel in
        BView(viewModel: viewModel)
      }
      .navigationDestination(for: Int.self) { number in
        Text("\(number)")
      }
      .navigationTitle("A View")
    }
    .onAppear {
      viewModel.setNavigator(navigator)
    }
  }
}

// ============================== AView ====================================

@Observable
final class BViewModel: NavigatableViewModel {

  override init() {
    print("🍯 init \(Self.self)")
  }

  deinit{
    print("🍯 Deinit \(Self.self)")
  }

  func navigate() {
    let viewModel = CViewModel()
    viewModel.setNavigator(navigator)
    navigator?.navigate(viewModel)
  }
}

struct BView: View {
  @State var viewModel: BViewModel

  init(viewModel: BViewModel) {
    self._viewModel = .init(initialValue: viewModel)
  }

  var body: some View {
    VStack {
      Button {
        viewModel.navigate()
      } label: {
        Text("Navigate to C")
      }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.green)
    .navigationTitle("B View")
    .navigationDestination(for: CViewModel.self) { viewModel in
      CView(viewModel: viewModel)
    }
  }

}

// ============================== BView ====================================

@Observable
final class CViewModel: NavigatableViewModel{
  override init() {
    print("🍯 init \(Self.self)")
  }

  deinit{
    print("🍯 Deinit \(Self.self)")
  }

}

struct CView: View {
  @State var viewModel: CViewModel

  init(viewModel: CViewModel) {
    self._viewModel = .init(initialValue: viewModel)
  }

  var body: some View {
    VStack {
      Button {
        viewModel.pop()
      } label: {
        Text("Back")
      }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.yellow)
    .navigationTitle("C View")
  }
}

🔬 결과


출처

 

반응형

'iOS > SwiftUI' 카테고리의 다른 글

SwiftUI 동작 방식  (0) 2025.10.18
textContentType  (0) 2025.10.03
List 사용하다 불편한 내용 정리  (0) 2025.09.27
TextField에서 자동 포맷적용하기  (0) 2025.08.28
TextField 커스텀 PlaceHolder 넣기  (0) 2025.08.28
'iOS/SwiftUI' 카테고리의 다른 글
  • SwiftUI 동작 방식
  • textContentType
  • List 사용하다 불편한 내용 정리
  • TextField에서 자동 포맷적용하기
Hamp
Hamp
남들에게 보여주기 부끄러운 잡다한 글을 적어 나가는 자칭 기술 블로그입니다.
  • Hamp
    Hamp의 분리수거함
    Hamp
  • 전체
    오늘
    어제
    • 분류 전체보기 (309) N
      • CS (30)
        • 객체지향 (2)
        • Network (7)
        • OS (6)
        • 자료구조 (1)
        • LiveStreaming (3)
        • 이미지 (1)
        • 잡다한 질문 정리 (0)
        • Hardware (2)
        • 이론 (6)
        • 컴퓨터 그래픽스 (0)
      • Firebase (3)
      • Programing Langauge (38)
        • swift (32)
        • python (5)
        • Kotlin (1)
      • iOS (132)
        • UIKit (37)
        • Combine (1)
        • SwiftUI (32)
        • Framework (7)
        • Swift Concurrency (22)
        • Tuist (6)
        • Setting (11)
        • Modularization (1)
        • Instruments (6)
      • PS (59)
        • 프로그래머스 (24)
        • 백준 (13)
        • LeetCode (19)
        • 알고리즘 (3)
      • Git (18)
        • 명령어 (4)
        • 이론 (2)
        • hooks (1)
        • config (2)
        • action (7)
      • Shell Script (2)
      • Linux (6)
        • 명령어 (5)
      • Spring (15) N
        • 어노테이션 (3) N
        • 튜토리얼 (11)
      • CI-CD (4)
      • Android (0)
        • Jetpack Compose (0)
      • AI (2) N
        • 이론 (2) N
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    lifecycle
    dispatch
    boostcamp
    SwiftUI
    프로그래머스
    AVFoundation
    Spring
    투포인터
    property
    dfs
    CS
    Swift
    Tuist
    concurrency
    백준
    IOS
    dp
    GIT
    protocol
    UIKit
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
Hamp
Navigator 패턴 이용해보기
상단으로

티스토리툴바