Адаптируем существующее бизнес-решение под SwiftUI. Часть 4. Навигация и конфигурация

—

от автора

Доброго всем времени суток! С вами я, Анна Жаркова, ведущий мобильный разработчик компании «Usetech»
Теперь поговорим об еще одном интересном моменте в SwiftUI, о навигации.
Если вы пропустили предыдущие статьи цикла, то можете ознакомиться с ними по ссылке:
часть 1
часть 2
часть 3

С изменением описания визуальной части и переходом к декларативному синтаксису изменилось и управление навигацией в приложении SwiftUI. Использование UIViewContoller напрямую отрицается, UINavigationController напрямую не используется. На смену ему приходит NavigationView.

@available(iOS 13.0, OSX 10.15, tvOS 13.0, *) @available(watchOS, unavailable) public struct NavigationView<Content> : View where Content : View {      public init(@ViewBuilder content: () -> Content)      //.... } 

По сути обертка над UINavigationController и его функционалом.


Основным механизмом перехода является NavigationLink (аналог segue), который задается сразу же в коде body View.

public struct NavigationLink<Label, Destination> : View where Label : View,                                                                                             Destination : View { .     public init(destination: Destination, @ViewBuilder label: () -> Label)      public init(destination: Destination, isActive: Binding<Bool>,                     @ViewBuilder label: () -> Label)      public init<V>(destination: Destination, tag: V, selection: Binding<V?>,                           @ViewBuilder label: () -> Label) where V : Hashable //.... } 

При создании NavigationLink указывает View, на который осуществляется переход, а также View, который NavigationLink оборачивает, т.е при взаимодействии с которым NavigationLink активизируется. Больше информации о возможных способах инициализации NavigationLink в документации Apple

Однако стоит учитывать, что из-за инкапсуляции прямого доступа к стеку View нет, навигация программно задается только вперед, возврат возможен только на 1 уровень назад и то через инкапсулированный код для кнопки «Back»

Также в SwiftUI нет динамической программной навигации. Если переход привязан не к триггерному-событию, например, нажатию на кнопку, а следует как результат какой-то логики, то просто так это не сделать. Переход на следующий View обязательно привязывается к механизму NavigationLink, которые задаются декларативно сразу же при описании содержащего их View. Все.
Если наш экран должен содержать переход на много разных экранов, то код становится громоздким:

  NavigationView{             NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {                 Text("Profile")             }             NavigationLink(destination: Settings(), isActive: self.$isSettings) {                            Text("Settings")                        }             NavigationLink(destination: Favorite(), isActive: self.$isFavorite) {                 Text("Favorite")             }             NavigationLink(destination: Login(), isActive: self.$isLogin) {                 Text("Login")             }             NavigationLink(destination: Search(), isActive: self.$isSearch) {                 Text("Search")             } } 

Управлять ссылками мы можем несколькими способами:
— управление активностью NavigationLink через @Binding свойство

 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {                 Text("Profile")             } 

— управление созданием ссылки через условие (переменные состояния)

       if self.isProfile {             NavigationLink(destination: ProfileView()) {                 Text("Profile")             } } 

Первый способ добавляет нам работы по контролю за состоянием управляющих переменных. Если у нас планируется навигация на более, чем 1 уровень вперед, то это весьма тяжелая задача.
В случае экрана списка однотипных элементов все выглядит компактно:

 NavigationView{         List(model.data) { item in             NavigationLink(destination: NewsItemView(item:item)) {             NewsItemRow(data: item)             }         } 

Самой серьезной проблемой NavigationLink, на мой взгляд, является то, что все указываемые в ссылках View не lazy. Они создаются не в момент срабатывания ссылки, а в момент создания. Если у нас список на множество элементов или переходы на много разных тяжелых по контенту View, то это не лучшим образом сказывается на performance нашего приложения. Если же еще у нас к этим View привязаны ViewModel с логикой, в реализации которой не учтен или учтен не верно life-cycle View, то ситуация становится совсем тяжелой.
Например, у нас есть список новостей с однотипными элементами. Мы еще ни разу не перешли ни на один экран единичной новости, а модели уже висят в памяти:

Что мы можем сделать в этом случае, чтобы облегчить себе жизнь?
Во-первых, вспомним, что View существуют не в вакууме, а рендерятся в UIHostingController.

open class UIHostingController<Content> : UIViewController where Content : View {      public init(rootView: Content)      public var rootView: Content //... } 

А это UIViewController. Значит, мы можем сделать следующее. Мы перенесем всю ответственность за переход на следующий View внутри нового UIHostingController на контроллер текущего View. Создадим модули навигации и конфигурации, которые будем вызывать из нашего View.

Навигатор, работающий с UIViewController, будет иметь такой вид:

class Navigator {     private init(){}          static let shared = Navigator()          private weak var view: UIViewController?          internal weak var nc: UINavigationController?             func setup(view: UIViewController) {         self.view = view     }         internal func open<Content:View>(screen: Content.Type, _ data: Any? = nil) {      if let vc = ModuleConfig.shared.config(screen: screen)?         .createScreen(data) {         self.nc?.pushViewController(vc, animated: true)         }    } 

По тому же принципу мы создадим фабрику конфигураторов, которая будет нам выдавать реализацию конфигуратора конкретного модуля:

protocol IConfugator: class {     func createScreen(_ data: Any?)->UIViewController }  class ModuleConfig{     private init(){}     static let shared = ModuleConfig()          func config<Content:View>(screen: Content.Type)->IConfugator? {         if screen == NewsListView.self {             return NewsListConfigurator.shared         }       //код какой-то         return nil     } } 

Навигатор по типу экрана запрашивает конфигуратор конкретного модуля, передает ему всю необходимую информацию.

class NewsListConfigurator: IConfugator {     static let shared = NewsListConfigurator()          func createScreen(_ data: Any?) -> UIViewController {          var view = NewsListView()         let presenter = NewsListPresenter()          let interactor = NewsListInteractor()                  interactor.output = presenter         presenter.output = view         view.output = interactor                  let vc = UIHostingController<ContainerView<NewsListView>>                     (rootView: ContainerView(content: view))         return vc     } } 

Конфигуратор отдает UIViewController, который Navigator и кладет в общий стек UINavigationController.

Заменим NavigationLink в коде на вызов Navigator. В качестве триггера у нас будет событие нажатия на элемент списка:

  List(model.data) { item in             NewsItemRow(data: item)                 .onTapGesture {                 Navigator.shared.open(screen: NewsItemView.self, item)             }         } 

Ничего не мешает нам таким же образом вызывать Navigator в любом методе View. Не только внутри body.
Кроме того, что код стал ощутимо чище, мы еще и разгрузили память. Ведь при таком подходе View создастся только при вызове.

Теперь наше приложение SwiftUI проще расширять и модифицировать. Код чистый и красивый.
Код примера вы найдете по ссылке

В следующий раз поговорим про более глубокое внедрение Combine.

ссылка на оригинал статьи https://habr.com/ru/post/500492/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *