티스토리 뷰

SwiftUI로 개발을 하면서 화면 전환에 대한 불편함을 종종 느끼게 되었는데, 이를 해결할 수 있는 좋은 오픈소스 라이브러리가 있어서 소개해보고자 합니다! 바로 ⭐️LinkNavigator⭐️입니다. 이 라이브러리는 웹에서 URI path를 쓰는 것처럼 path를 이용해 화면전환을 할 수 있게 도와주는데요, 기본적인 push · pop 전환과 stack관리 · 딥링크 기능까지 있습니다. 플리토라는 한국 회사에서 개발한 라이브러리이고 한국어 문서도 잘되어 있어요! 이 라이브러리를 직접 소개하시는 영상이 있는데 참고해보셔도 좋을 것 같습니다!

 

 

GitHub - interactord/LinkNavigator: 🌊 Easy & Powerful navigation library in SwiftUI

🌊 Easy & Powerful navigation library in SwiftUI. Contribute to interactord/LinkNavigator development by creating an account on GitHub.

github.com

 

참고로 해당 라이브러리는 MVI, TCA 아키텍처에 사용하기 가장 좋다고하지만 어떤 아키텍처에 사용해도 상관은 없다고합니다! (저는 사실 MVVM 프로젝트에 적용해볼 예정입니다 ㅎㅎ)

 

설치


우선 SPM을 이용해 설치를 진행해주시면 됩니다.

Xcode > File > Add Pakages.. 를 클릭해주시고 검색창에 깃허브 주소(https://github.com/interactord/LinkNavigator.git)를 입력해주신 뒤 Add Package를 눌러 설치를 진행해주시면 됩니다.

 

초기 설정


이 라이브러리는 UIKit의 UINavigationController를 래핑해서 SwiftUI에서 사용할 수 있게 만들었다고합니다. 따라서 UIKit에 관련된 설정을 포함한 초기 설정이 필요합니다.

 

1.  AppDependency.swift 파일을 추가합니다.

내부에는 DependencyType 프로토콜을 채택하는 AppDependency struct를 생성해줍니다. 

// AppDependency.swift

import LinkNavigator

/// 외부 의존성을 관리하는 타입입니다.
///
/// DependencyType 프로토콜을 채택해야 합니다.
struct AppDependency: DependencyType { }
💭 이 DependencyType를 사용하는 방법이 없길래 내부 구조를 보았는데 resolve 함수가 있는 것으로 보아 DI 관련 기능이 맞긴 한 것 같습니다. 하지만 아직 구현되진 않은 것 같더라구요! 그래서 일단 위와 같이 형식만 써주시면 될 것 같습니다.

 

2.  모든 라우팅을 관리할 AppRouterGroup.swift 파일을 추가합니다.

struct를 만들고 RouteBuilder들을 담을 수 있는 routers 프로퍼티를 추가합니다. 나중에 여기에 RouteBuilder들을 추가하면 path를 이용해 자유롭게 화면전환을 할 수 있게 됩니다.

// AppRouterGroup.swift
// LinkNavigator 를 통해 이동하고 싶은 화면들을 관리하는 타입입니다.

import LinkNavigator

/// LinkNavigator 를 통해 이동하고 싶은 화면들을 관리하는 타입입니다.
struct AppRouterGroup {
  var routers: [RouteBuilder] {
    [
      // 나중에 추가
    ]
  }
}

 

3. AppDelegate.swift 파일을 추가합니다.

SwiftUI 프로젝트에는 AppDelegate가 없으므로 AppDelegate에 대한 설정을 해주어야합니다. 아래와 같이 AppDelegate를 생성하고 네비게이션을 담당해줄 LinkNavigator를 생성해줍니다. 이때 LinkNavigator에는 dependency와 routergroup을 전달해주게 됩니다.

// AppDelegate.swift

import SwiftUI
import LinkNavigator

/// 외부 의존성과 화면을 주입받은 navigator 를 관리하는 타입입니다.
final class AppDelegate: NSObject {
  var navigator: LinkNavigator {
    LinkNavigator(dependency: AppDependency(), builders: AppRouterGroup().routers)
  }
}

extension AppDelegate: UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
    true
  }
}

 

4. @main 어노테이션이 있는 파일로 이동합니다.

SwiftUI 프로젝트를 생성하면 생기는 루트파일(@main이 있는 곳)에 다음 코드들을 추가해줍니다. AppDelegate에서 만든 LinkNavigator를 SwiftUI에서 쓸 수 있게 가져와주는 역할을 하는 코드들입니다. 

WindowGroup내의 navigator.launch 메서드는 앱의 첫 시작 화면을 정할 수 있게 해주는데요, 나중에 router를 만든 후에 path를 채워주겠습니다.

import SwiftUI
import LinkNavigator 

@main
struct ProjectNameApp: App {
  // ✅ 여기부터
  @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate

  var navigator: LinkNavigator {
    appDelegate.navigator
  }
  // ✅ 여기까지

  var body: some Scene {
    WindowGroup {
      // ✅ 여기부터
      navigator
        .launch(paths: ["나중에 작성"], items: [:]) // 'paths' 파라미터의 인자가 시작 페이지로 설정됩니다.
        .onOpenURL { url in
        // 딥링크 기능 사용시 작성
        }
      // ✅ 여기까지
    }
  }
}
❗️ WindowGroup기능을 사용하는 것이 아니라면 WindowGroup내에 ContentView나 다른 View가 있다면 지워주세요! 

 

첫 화면 설정


라우팅을 하기 위해선 RouteBuilder가 필요합니다. 아까 2번에서 RouterGroup에 내부를 비워두었는데요, 여기에 들어갈 RouteBuilder들을 구현해주어야합니다. RouteBuilder는 화면 전환하고 싶은 화면 당 1개씩 구현해주시면 됩니다!

 

1. View에 navigator 속성을 추가합니다.

먼저 그전에 대상이 되는 뷰에 navigator 속성을 추가해줍니다. 이 navigator는 builder를 통해 받을겁니다!

import SwiftUI
import LinkNavigator

struct HomeView: View {
    
    let navigator: LinkNavigatorType
    
    init(navigator: LinkNavigatorType) {
        self.navigator = navigator
    }
    
    var body: some View {
        VStack {
            Text("Home View")
                .font(.title)
        }
    }
}

 

2. RouteBuilder파일과 구조체를 추가합니다.

그 다음 Home화면에 대한 RouteBuilder를 구성합니다. 여기서 전환할 뷰를 인스턴스화 해주게 됩니다. 체크 표시된 부분만 주의해서 알맞게 작성해주시고 나머지는 항상 똑같이 작성해주시면 됩니다.

import LinkNavigator
import SwiftUI

struct HomeRouteBuilder: RouteBuilder {
  var matchPath: String { "home" } // ✅ 화면 전환에 필요한 path

  var build: (LinkNavigatorType, [String: String], DependencyType) -> MatchingViewController? {
    { navigator, items, dependency in
      return WrappingController(matchingKey: matchPath) {
        HomeView(navigator: navigator) // ✅ 화면 전환할 SwiftUI View
      }
    }
  }
}
❗️ 이때 matchPath의 문자열 중복되지 않도록 주의해서 작성해주세요. path를 중복해서 사용하게 되면 RouterGroup 리스트 중에 앞쪽에 있는 RouteBuilder만 실행되게 됩니다. 혹시 헷갈릴 수 있으니 String형 enum을 만들어서 rawValue를 쓰는 것도 괜찮을 것 같네요!

 

3. 생성한 RouteBuilder를 RouterGroup에 추가해줍니다.

아까 생성해둔 RouterGroup에 만든 RouteBuilder를 추가해줍니다.

import LinkNavigator

/// LinkNavigator 를 통해 이동하고 싶은 화면들을 관리하는 타입입니다.
struct AppRouterGroup {
  var routers: [RouteBuilder] {
    [
        HomeRouteBuilder() // ✅
    ]
  }
}

 

4. navigator.launch의 path부분을 작성해줍니다.

앱 루트로 다시 돌아가 navigator.launch의 path를 채워줍니다. 저는 home 화면으로 앱을 시작하려하기 때문에 HomeRouteBuilder의 matchPath인 'home'을 넣어주었습니다.

import SwiftUI
import LinkNavigator

@main
struct ProjectNameApp: App {
    let hkAuthorizationProvider = HKAuthorizationProvider()
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    
    var navigator: LinkNavigator {
      appDelegate.navigator
    }
    
    var body: some Scene {
        WindowGroup {
            navigator
                .launch(paths: ["home"], items: [:]) // ✅
        }
    }
}

 

5. 동작 확인

👍

 

다른 화면으로 전환하기


첫 화면을 띄웠다면 다른 화면으로 전환도 해보아야겠죠? 다른 뷰를 한번 push 해보도록 하겠습니다

 

1. 해당 뷰에 navigation 프로퍼티를 추가합니다.

struct NextView: View {
    
    let navigator: LinkNavigatorType
    
    init(navigator: LinkNavigatorType) {
        self.navigator = navigator
    }
    
    var body: some View {
        VStack {
            Text("Next View")
                .font(.title)
        }
    }
}

 

2. 해당 뷰에 대한 RouteBuilder를 생성합니다.

import LinkNavigator
import SwiftUI

struct NextRouteBuilder: RouteBuilder {
    
  var matchPath: String { "next" }

  var build: (LinkNavigatorType, [String: String], DependencyType) -> MatchingViewController? {
    { navigator, items, dependency in
        return WrappingController(matchPath: matchPath) {
            NextView(navigator: navigator)
      }
    }
  }
}

 

3. RouterGroup에 추가해줍니다.

RouterGroup에 새로 만든 RouteBuilder를 추가해줍니다.

struct AppRouterGroup {
  var routers: [RouteBuilder] {
    [
        HomeRouteBuilder(),
        NextRouteBuilder() // ✅
    ]
  }
}

 

4. 화면전환 이벤트 추가합니다.

이전 뷰에서 action에 navigator.next 호출해줍니다. navigator의 next연산은 가장 기본적으로 화면을 쌓아주는 연산입니다. pahts에 전환하고자하는 화면의 path를 적어주시면 됩니다.

struct HomeView: View {

    let navigator: LinkNavigatorType
    
    var body: some View {
        VStack {
            Text("Home View")
                .font(.title)
            Button(action: {
                // ✅
                navigator.next(paths: ["next"], items: [:], isAnimated: true)
            }) {
                Text("Next View")
            }

        }
    }

 

5. 동작 확인

👍

 

뷰가 아닌 곳에서 화면 전환하기


예를 들면 로그인 후에 화면 전환을 하는 등, 뷰가 아니라 Controller, ViewModel, Intent등에서 발생하는 이벤트에 따라 화면전환을 하고 싶다면 다음과 같이 하면 됩니다.

 

1. ViewModel(혹은 다른 컴포넌트)에 navigator 프로퍼티 추가합니다.

View에 있던 navigator를 ViewModel로 옮겨주세요.

class NextViewModel: ObservableObject {
    
    let navigator: LinkNavigatorType

 

2. RouteBuilder에서 ViewModel에 전달해줍니다.

이제 View가 아닌 ViewModel의 인자로 전달해주세요.

struct NextRouteBuilder: RouteBuilder {
    
  var matchPath: String { "next" }

  var build: (LinkNavigatorType, [String: String], DependencyType) -> MatchingViewController? {
    { navigator, items, dependency in
        return WrappingController(matchPath: matchPath) {
            NextView(preventViewModel: NextViewModel(navigator: navigator)) // ✅
      }
    }
  }
}

그럼 이제 ViewModel에서 navigator를 조작할 수 있으므로 특정 이벤트(로그인 성공 응답 등)에 따라 화면을 전환할 수 있습니다!

💭 사실 View에서 직접 이벤트에 대한 로직을 구현하기 보다는 다른 컴포넌트에 위임하는게 좋으므로 이 방법을 주로 사용하시고 변화 요소가 없는 단순 정보뷰라면 View에서 사용 하시면 될 것 같습니다. MVI 계열 아키텍처라면 이런 부분이 강제되기도 하죠.

 

Navigator 연산 종류


LinkNavigator의 강력한 기능중 하나는 다양하고 자유로운 화면 전환 메서드들입니다. next 이외에도 다양한 기능이 있는데 몇개만 살펴보겠습니다.

 

next (path 여러개)

next메서드를 호출하면서 paths에 여러 path를 작성해주면 한번에 여러개의 뷰가 push 됩니다.

navigator.next(paths: ["page1", "page2"], items: [:], isAnimated: true)

이 경우엔 page1과 page2가 한번에 push되게 됩니다.

 

remove

next(push)된 뷰를 dismiss할 수 있습니다. path가 일치하는 모든 뷰를 삭제합니다. 같은 뷰가 여러번 push되었다면 모두 사라지게 됩니다.

paths에 여러 path를 쓰면 여러개의 뷰가 한번에 없어집니다. 마찬가지로 일치하는 뷰들이 전부 삭제됩니다. 애니메이션 옵션은 없습니다.

navigator.remove(paths: ["pageToRemove"])

 

back

이전 화면으로 돌아가거나, modal을 내리는 메서드입니다.

navigator.back(isAnimated: true)

 

backOrNext

만약 해당 뷰가 push되어 스택에 존재한다면 그 화면까지 이동하며 이전 뷰들을 pop하고, 스택에 뷰가 없다면 새롭게 push합니다.

navigator.backOrNext(path: "targetPage", items: [:], isAnimated: true)

이런 기능들이 진짜 좋은 것 같습니다. 직접 구현하려면 시간이 꽤 걸릴 것 같아요

 

replace

paths대로 스택 전체를 교체하게 됩니다.

navigator.replace(paths: ["main", "depth1", "depth2"], items: [:], isAnimated: true)

 

sheet 관련 기능

push방식이 아닌 sheet방식도 사용할 수 있습니다. sheet은 SwiftUI에서의 sheet과 동일하고, fullSheet은 fullScreenCover 방식입니다.

paths에 여러개를 전달하면 처음 path는 sheet방식으로 뜨고, 그 뒤로는 push방식으로 추가됩니다.

또, 시트가 올라간 상태에서 다시 sheet을 호출하면 이전 시트는 내려가고 새로운 시트가 올라옵니다.

navigator.sheet(paths: ["sheetPage"], items: [:], isAnimated: true)

navigator.fullSheet(paths: ["page1", "page2"], items: [:], isAnimated: true)

sheet을 코드로 내리려면 close를 사용하면 됩니다. fullSheet은 스와이프로 내려지지 않기 때문에 이렇게 내려야합니다.

navigator.close(isAnimated: true) { print("modal dismissed!") }

또한 CustomSheet 메서드를 이용하면 더 많은 sheet 커스텀이 가능하고 아이폰과 아이패드에서도 다르게 표시할 수 있습니다. Style은 UIKit에서 UIModalPresentationStyle로 제공하는 스타일들을 사용할 수 있습니다.

navigator.customSheet(
  paths: ["sheetPage"],
  items: [:],
  isAnimated: true,
  iPhonePresentationStyle: .fullScreen,
  iPadPresentationStyle: .pageSheet,
  prefersLargeTitles: .none)
단, half sheet이나 모달의 radius를 설정할 수 있는 옵션은 없는 것 같습니다. 따라서 더 세세한 모달 커스텀 기능이 필요하시다면 SwiftUI에서 sheet을 올리거나 패키지를 커스텀해야할 것 같습니다.

 

Alert 기능

Alert를 띄울 수 있는 기능도 제공합니다. View가 아닌 곳에서 Alert를 띄워야할 때 유용할 것 같네요.

// Alert 내용
let alertModel = Alert(
  title: "Title",
  message: "message",
  buttons: [.init(title: "OK", style: .default, action: { print("OK tapped") })],
  flagType: .default)
// 띄우기
navigator.alert(target: .default, model: alertModel)

 

 

화면 전환하며 데이터 전달하기


화면을 전환하면서 이전 뷰의 데이터를 전달하고 싶다면 navigator 메서드에 있는 items 파라미터를 이용할 수 있습니다.

case .onTapNextWithMessage:
  navigator.next(
    paths: ["page4"],
    items: ["page4-message": state.messageYouTyped], // ✅ 값 전달
    isAnimated: true)

// RouteBuilder catches `items`'s arguments using exact key.
struct Page4RouteBuilder: RouteBuilder {
  var matchPath: String { "page4" }

  var build: (LinkNavigatorType, [String: String], DependencyType) -> UIViewController? {
    { navigator, items, dep in
      WrappingController(matchPath: matchPath) {
        Page4View(
          store: .init(
            initialState: Page4.State(message: items["page4-message"] ?? ""), // ✅ 받아서 전달
            reducer: Page4()))
      }
    }
  }
}

위와 같이 RouteBuilder에서 전달 받은 items에 접근해 값을 전달할 수 있습니다. query string 같네요. 다만 string형식이라 오타에 유의하셔야합니다! 컴파일 에러가 뜨지 않아요.

 

 

프리뷰


view에서 navigator를 갖고있다보면 프리뷰에서도 주입을 해주어야하는데 다음과 같이 해주시면 됩니다!

strut NextView_Previews: PreviewProvider {
    static var previews: some View {
        NextView(navigator: LinkNavigator(dependency: AppDependency(), builders: AppRouterGroup().routers))
    }
}

이 부분은 DI 라이브러리를 쓰면 더 간결하게 작성할 수 있을 것 같네요!

 

 

마치며


패키지내의 문서 주석이 굉장히 잘 되어있으니 Xcode내에서 Quick Help를 이용하시면 좋을 것 같습니다.

 

사실 LinkNavigator는 딥링크 기능을 통해 화면전환을 할 때 엄청나게 강력한 라이브러리라고 생각되는데요.. 추후에 딥링크 구현과 LinkNavigator에 적용한 예시도 포스팅해보겠습니다.

 

path를 문자열로 작성해야한다는 점은 조금 불안하지만 너무 편한 기능들이 많아 좋은 것 같습니다. 특히 NavigationView나 NavigationStack으로 뷰 밖에서(ViewModel에서) 화면전환을 하는게 이래저래 조금 불편했는데 이 부분을 말끔하게 해결해주네요.👍👍 또 네비게이션 스택이 많이 쌓인 경우에도 쉽게 이전 페이지들을 관리할 수 있다는 점이 엄청난 것 같습니다. ☺️

 

 

ref.

https://github.com/interactord/LinkNavigator(공식 깃허브)

 

GitHub - interactord/LinkNavigator: 🌊 Easy & Powerful navigation library in SwiftUI

🌊 Easy & Powerful navigation library in SwiftUI. Contribute to interactord/LinkNavigator development by creating an account on GitHub.

github.com

https://gist.github.com/Jager-yoo/572c1735cf2560299a22c6f6065914b5(한국어 문서)

 

LinkNavigator README in Korean

LinkNavigator README in Korean. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함