Не будем мусолить всем известную проблему с навигацией в SwiftUI до 16 iOS, так как уже много крутых статей на эту тему есть в открытом доступе. Близится релиз 18 iOS, а это значит, что минимальные таргеты поднимутся на единичку ближе к 16 ?
В данной статье хочу представить на `мой взгляд` наиболее удобную реализацию навигации на NavigationStack. Разумеется в концепции старого доброго MVVM, поэтому фанаты UDF извините ?
Итак перейдем к реализации
В основе всего у нас будет лежать моделька с экранами. Пихаем внутрь все, что требуется в этих вьюхах и погнали творить настоящий рок-н-ролл.
/// Например такие роуты enum Route { case goToServices(PasswordViewModel, PasswordTagViewModel) case addPassword(Service, PasswordViewModel, PasswordTagViewModel) case editPassword(PasswordViewModel, PasswordTagViewModel) }
После того, как мы определились с экранами мы переходим к логике роутинга. Для этого нам понадобится какой-нибудь класс, подписанный под ObservableObject. Помечаем его как @MainActor сразу, чтобы не тыкать всякие инструменты синхронизации по типу NSLock().
@MainActor будет гарантировать то, что все методы и свойства в классе Router будут выполняться на главном потоке. Это исключает возможность гонок данных при доступе к path или другим свойствам класса.
@MainActor final class Router: ObservableObject { // MARK: - Public properties @Published var path = NavigationPath() // Сюда уже добавляете любой путь по которому хотите навигироваться // Все зависит от сложности вашего проекта }
После всех этим махинаций напишем функцию, которая будет возвращать нужный нам экран исходи из заданного enum . Разумеется вы можете написать нужное вам количество таких функций, а не пихать все в одну. Все максимально scalable ?
@ViewBuilder func view(for route: Route) -> some View { switch route { case let .goToServices(passwordViewModel, passwordTagViewModel): ServicesView( passwordViewModel: passwordViewModel, passwordTagViewModel: passwordTagViewModel ) case let .addPassword(service, passwordViewModel, passwordTagViewModel): AddPasswordView( service: service, passwordViewModel: passwordViewModel, passwordTagViewModel: passwordTagViewModel ) case let .editPassword(passwordViewModel, passwordTagViewModel): EditPasswordView( passwordViewModel: passwordViewModel, passwordTagViewModel: passwordTagViewModel ) } }
Осталось дело за малым, теперь опишем основную логику роутинга.
@inlinable @inline(__always) func push(_ appRoute: Route) { path.append(appRoute) } @inlinable @inline(__always) func pop() { guard !path.isEmpty else { return } path.removeLast() } @inlinable @inline(__always) func popToRoot() { path.removeLast(path.count) }
Самое страшное позади ?? Теперь, чтобы эта вундервайля начала функционировать, нам потребуется написать корневую роутинг вьюху, которую напишем один раз в жизни и забудем
struct RouterView<Content: View>: View { @inlinable init(@ViewBuilder content: @escaping () -> Content) { self.content = content() } var body: some View { NavigationStack(path: $router.path) { content .navigationDestination(for: Router.Route.self) { router.view(for: $0) .navigationBarBackButtonHidden() } } .environmentObject(router) } @StateObject private var router = Router() private let content: Content }
Если кто не знал, то модификатор .navigationBarBackButtonHidden() ломает немножко нативеый выход с экрана по свайпу. Благо я знаю способ, как это исправить. Так как большая часть SwiftUI под капотом все еще является UIKit`от, то решение будет достаточно простым:
extension UINavigationController { override open func viewDidLoad() { super.viewDidLoad() interactivePopGestureRecognizer?.delegate = nil } }
Отлично, а теперь давайте потыкаем это палкой.
Как теперь внедрить это в код и использовать
Я надеюсь у вас есть какая-то рутовая вьюшка, если ее нет, то придется создать.
import SwiftUI struct RootView: View { var body: some View { RouterView { // Пихаем сюда нужные экраны, пишем логику показа и успех ZStack { TabBarView() .toolbar(.hidden, for: .navigationBar) } } .task { isNeedUpdate = await appUpdateManager.isUpdateRequired() } .sheet(isPresented: $isNeedUpdate) { VStack { Text("Надо обновиться") } .interactiveDismissDisabled() } } // На это не обращаем внимание, тут какая-то @State private var isNeedUpdate = false private let appUpdateManager = AppUpdateManagerImpl() }
Настало время уже наконец потыкать наш роутер. Так как мы умные ребята и не хотим пропихивать в каждый экран ObservedObject , то прибегнем к использованию прекрасного EnvironmentObject. В дальнейшем никакие модификаторы .environmentObject(_) не понадобятся, ибо мы сделали это в роут вьюхе ?
import SwiftUI struct TestView: View { var body: some View { VStack { Button { router.push( .service( passwordViewModel, passwordTagViewModel ) ) } label: { ZStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) .frame(height: 60) Text("Перейти на экран с сервисами") .font( .system( size: 16, weight: .bold, design: .rounded ) ) .foregroundStyle(.white) } } Button { router.push( .addPassword( service, passwordViewModel, passwordTagViewModel ) ) } label: { ZStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) .frame(height: 60) Text("Перейти на экран добавления пароля") .font( .system( size: 16, weight: .bold, design: .rounded ) ) .foregroundStyle(.white) } } Button { router.push( .editPassword( passwordViewModel, passwordTagViewModel ) ) } label: { ZStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) .frame(height: 60) Text("Перейти на экран редактирования пароля") .font( .system( size: 16, weight: .bold, design: .rounded ) ) .foregroundStyle(.white) } } } .padding(.horizontal) } @StateObject private var passwordViewModel = PasswordViewModel(manager: .shared) @StateObject private var passwordTagViewModel = PasswordTagViewModel(manager: .shared) @EnvironmentObject private var router: Router private var service = PasswordService(id: .zero, title: "Habr", url: "habr.com", icon: Data()) }
В нужных местах юзаем кнопки для возврата к предыдущему экрану и сброс до корневой вьюхи
router.pop() router.popToRoot()
Пример использования в продакшене
За все время перепробовал массу способов навигации, но в итоге эта оказалась самая удачная и приятная в имплементации лично для меня. Данный пример хорошо расширяется в любую горизонталь и вертикаль и отлично покрывается UI и Unit тестами.
Пример использовании на одном из проектов:
Здесь можешь почитать много всякого интересного про iOS и SwiftUI — публикую интересные статьи, лучшие практики, типсы, анимации и прочие крутые штуки. Присоединяйся
Еще веду канал с вакансиями для мобильных разработчиков → Присоединяйся
ссылка на оригинал статьи https://habr.com/ru/articles/830392/
Добавить комментарий