Адаптируем бизнес-решение под SwiftUI: навигация и конфигурация | OTUS
⚡ Подписка на курсы OTUS!
Интенсивная прокачка навыков для IT-специалистов!
Подробнее

Курсы

Программирование
Team Lead Архитектура и шаблоны проектирования Разработчик IoT C# Developer. Professional PostgreSQL Подготовка к сертификации Oracle Java Programmer (OCAJP) C# ASP.NET Core разработчик
-5%
Kotlin Backend Developer
-8%
iOS Developer. Professional
-8%
Symfony Framework Unity Game Developer. Basic JavaScript Developer. Professional Android Developer. Basic JavaScript Developer. Basic Java Developer. Professional Highload Architect Reverse-Engineering. Professional Java Developer. Basic PHP Developer. Professional Алгоритмы и структуры данных Framework Laravel Cloud Solution Architecture Vue.js разработчик Интенсив «Оптимизация в Java» Супер - интенсив по паттернам проектирования Супер - интенсив по Kubernetes Супер-интенсив "Tarantool" PHP Developer. Basic
Инфраструктура
Мониторинг и логирование: Zabbix, Prometheus, ELK Дизайн сетей ЦОД Разработчик IoT PostgreSQL Экспресс-курс "Версионирование и командная работа с помощью Git"
-30%
Экспресс-курс «Введение в непрерывную поставку на базе Docker» Базы данных Reverse-Engineering. Professional Administrator Linux. Professional Network engineer Cloud Solution Architecture Внедрение и работа в DevSecOps Супер-практикум по работе с протоколом BGP Супер - интенсив по паттернам проектирования Супер - интенсив по Kubernetes Супер-интенсив «СУБД в высоконагруженных системах» Супер-интенсив "Tarantool" Network engineer. Basic
Корпоративные курсы
Безопасность веб-приложений IT-Recruiter Дизайн сетей ЦОД Компьютерное зрение Разработчик IoT Вебинар CERTIPORT Machine Learning. Professional
-6%
NoSQL Пентест. Практика тестирования на проникновение Java QA Engineer. Базовый курс Руководитель поддержки пользователей в IT
-8%
SRE практики и инструменты Cloud Solution Architecture Внедрение и работа в DevSecOps Супер-практикум по работе с протоколом BGP Infrastructure as a code Супер-практикум по использованию и настройке GIT Промышленный ML на больших данных Экспресс-курс «CI/CD или Непрерывная поставка с Docker и Kubernetes» BPMN: Моделирование бизнес-процессов Основы Windows Server
Специализации Курсы в разработке Подготовительные курсы Подписка
+7 499 938-92-02

Адаптируем бизнес-решение под SwiftUI: навигация и конфигурация

В предыдущих статьях мы поговорили об адаптации уже существующего бизнес-приложения под SwiftUI, рассмотрели работу с готовыми библиотеками под UIKit и разобрали особенности архитектуры. Остался еще один интересный момент: навигация.

С изменением описания визуальной части и переходом к декларативному синтаксису изменилось и управление навигацией в приложении 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 и его функционалом.

y27ceeieqhsosve9hkdmhry_q_y_1-1801-fca16d.png

Основным механизмом перехода является 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».

hjphbrs_ggmblirlejkozwj3wig_1-1801-f70978.png

Также в 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, то ситуация становится совсем тяжелой.

Например, у нас есть список новостей с однотипными элементами. Мы еще ни разу не перешли ни на один экран единичной новости, а модели уже висят в памяти:

ramsrarpwaj5p2zq2c5w0gc91ps_1-1801-a50374.png

Что мы можем сделать в этом случае, чтобы облегчить себе жизнь?

Во-первых, вспомним, что 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.

1njemwtm7eb3mjxqibdyh7etqqm_1-1801-c74d6c.png

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

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

Ничего не мешает нам таким же образом вызывать Navigator в любом методе View. Не только внутри body.

Кроме того, что код стал ощутимо чище, мы еще и разгрузили память. Ведь при таком подходе View создастся только при вызове.

_7bbp8aujaqoftrfjo0jywsmrfg_1-1801-51144e.png

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

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

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться