이번에 새롭게 시작한 SwiftUI 프로젝트에 Coordinator Pattern을 적용해보았다. UIKit은 구현방식이 어느정도 비슷한 느낌을 받았는데 SwiftUI 방식은 정형화된 방식이 없는것처럼 느껴졌다. 그중에 나름 이해가 잘되고 직관적으로 느껴지는 방법을 정해서 구현을 해보았다.
첫번째로 고민했던점은 앱에서 Coordinator를 어떠한 기준으로 나누냐를 고민하게 되었다. UIKit에서는 화면한개당 하나의 Coordinator가 존재한다. 하지만 예전에 적용했던 SwiftUI에서 방식은 한개의 코디네이터로 화면을 이동해도 겉으로 나타나는 문제는 없었다.
내가 이해한 UIkit에서의 Coordinator는 flow마다 Coordinator를 나누는 느낌으로 받아들였다. 예를들어 a, b, c 라는 메뉴를 가진
탭바가 있다면 TapCoordinator, ACoordinator, BCoordinator, CCoordinator 이런식으로 Coordinator라는 부분을 나눈것을 볼 수 있다.
기존에 잘 모르고 구현했던 Coordinator는 앱의 모든 경로를 한개의 Coordinator 객체에서 관리헀다 하지만 이 방법은 이러한 이유로 좋지못한 방법이라고 생각했다.
- 화면이 많아지면서 Coordinator 파일이 길어지는 문제
- 계층별로 나누지 않아서 앱의 flow 파악이 어려워짐
- 어떠한 곳에서도 접근이 가능하기 때문에 앱의 flow가 복잡해짐
SwiftUI도 마찬가지로 flow마다 Coordinator를 별도로 나눈게 맞다고 생각했다. 구현할때 NavigationStack을 사용했다. NavigationStack은 iOS 16 이상만 지원한다.
protocol Coordinator: AnyObject {
associatedtype Scene: Hashable
var paths: [Scene] { get set }
func push(_ scene: Scene)
func pop()
func popToRoot()
associatedtype ViewType = View
@ViewBuilder func buildScreen(scene: Scene) -> ViewType
}
extension Coordinator {
func push(_ scene: Scene) {
paths.append(scene)
}
func pop() {
_ = paths.popLast()
}
func popToRoot() {
paths.removeAll()
}
}
공통으로 사용할 Coordinator 프로토콜을 추가했다. Coordinator 프로토콜을 사용함으로써 Coordinator 구현에 필요한 필수 속성이나 메서드를 강제하고 extension에 기본구현을 추가하여 나름 사용성을 추가해보았다.
associatedtype을 사용하여 프로토콜 에서도 타입을 동적으로 받을 수 있도록 했다. Coordinator를 나누기로 했으니 화면의 타입도 각자의 Coordinator에서 구현 해주면된다. View 타입을 리턴하는 buildScreen 메서드도 View 라는 프로토콜을 리턴하게 하기위해서 associatedtype에 View를 지정해주었다.
protocol AuthCoordinatorType: Coordinator, ObservableObject {}
final class AuthCoordinator: AuthCoordinatorType {
var paths: [Scene] = []
enum Scene: Hashable {
case loginOnboarding
case signUp
}
@ViewBuilder
func buildScreen(scene: Scene) -> some View {
switch scene {
case .loginOnboarding:
LoginView()
case .signUp:
SignUpView()
}
}
}
인증 flow를 담당하는 AuthCoordinator를 생성한다. 동적인 타입을 받는 Scene은 구현시 enum으로 구현하여 화면의 종류를 설정해준다. 그리고 아래의 buildScreen에서 화면의 타입을 처리하여 뷰를 리턴해준다.
protocol AppCoordinatorType: Coordinator, ObservableObject {}
final class AppCoordinator: AppCoordinatorType {
enum Scene: Hashable {
case home
case menu
}
@Published var paths: [Scene] = []
@ViewBuilder
func buildScreen(scene: Scene) -> some View {
switch scene {
case .home:
HomeView()
case .menu:
MenuView()
}
}
}
마찬가지로 앱의 메인코디네이터를 생성해준다.
WindowGroup {
AuthenticationView()
.environmentObject(appCoorinator)
.environmentObject(authCoordinator)
.environmentObject(AuthenticationViewModel())
.environmentObject(TimerViewModel())
.environmentObject(CalendarViewModel())
}
앱의 진입점에서 해당 코디네이터를 enviromentObject로 주입해준다.
VStack {
switch viewModel.authenticationState {
case .unauthenticated:
NavigationStack(path: $authCoordinator.paths) {
LoginView()
.navigationDestination(for: AuthCoordinator.Scene.self) { scene in
authCoordinator.buildScreen(scene: scene)
}
}
case .authenticated:
NavigationStack(path: $appCoordinator.paths) {
HomeView()
.navigationDestination(for: AppCoordinator.Scene.self) { scene in
appCoordinator.buildScreen(scene: scene)
}
}
}
}
인증상태에 따라서 각각 다른 코디네이터가 설정된 NavigationStack을 리턴한다. 이제 앱의 flow는 메인코디네이터에서 앱의 인증 flow는 AuthCoordinator에서 관리하면 된다 만약 다른 코디네이터의 화면이 필요하다면 Scene에 화면타입을 추가하고 View를 재사용하는 방식을 사용하면 될것같다.
현재까지도 공부하는 취준생 입장이기 때문에 틀린내용이 있을 수 있다고 생각합닌다. 틀리거나 정정할 내용이 있다면 피드백 달아주시면 감사하겠습니다.
'iOS' 카테고리의 다른 글
[iOS/SwiftUI] firebase로 피드 데이터 가져오기 (1) | 2023.12.24 |
---|---|
[SwiftUI] UIViewRepresentable을 이용하여 SwiftUI에서 UIKit 뷰 사용하기 (0) | 2023.12.16 |
[iOS] Swift File I/O (1) | 2023.11.21 |
[iOS/SwiftUI] @StateObject, @ObservedObject의 차이점 (1) | 2023.11.20 |