Шаблон iOS Swift Coordinator и кнопка возврата на контроллере навигации

Я использую паттерн MVVM+Coordinator. Все мои контроллеры созданы coordinators. Но как правильно остановить моих координаторов при нажатии кнопки возврата на контроллере Navigation?

class InStoreMainCoordinator: NavigationCoordinatorType, HasDisposeBag {

    let container: Container

    enum InStoreMainChildCoordinator: String {
        case menu = "Menu"
        case locations = "Locations"
    }

    var navigationController: UINavigationController
    var childCoordinators = [String: CoordinatorType]()

    init(navigationController: UINavigationController, container: Container) {
        self.navigationController = navigationController
        self.container = container
    }

    func start() {
        let inStoreMainViewModel = InStoreMainViewModel()
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel

        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}

Посмотрим правде в глаза: в UIKit "шаблон координатора" хорошо выглядит на бумаге, но очень, очень сложно реализовать правильно. Начнем с вашего высказывания: «Каждые мои контроллеры создаются координаторами». Это эффективно дает реализацию, которая является никогда без проблем, что в конечном итоге приводит к пограничному случаю, когда ваше приложение завершается из-за «фатальных ошибок» или ведет себя неправильно;)

CouchDeveloper 11.07.2021 21:47
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
1
3 153
5

Ответы 5

Мой подход заключается в использовании корневого (родительского) координатора, который управляет дочерними координаторами, поэтому, когда пользователь завершает поток или нажимает кнопку возврата, вызывается метод делегата в корневом координаторе, и он может очистить дочерний координатор и при необходимости создать новый. .

У шаблона координатора есть известная слепая зона в отношении родной кнопки возврата. В основном у вас есть два способа исправить это:

  • Повторно реализуйте собственную кнопку возврата, хотя вы теряете собственный жест проведите назад для возврата.
  • Реализуйте UINavigationControllerDelegate, чтобы обнаруживать, когда представление появилось, чтобы иметь возможность освободить соответствующий координатор.

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

Для второго вы можете реализовать его в самом Координаторе, как это предлагает @mosbah, но я бы посоветовал вам пойти дальше и разделить навигацию до координатора, используя класс NavigationController или Router, чтобы изолировать саму навигацию и сохранить четкое разделение беспокойства.

Я написал кое-что об этом здесь, в котором подробно описаны основные шаги.

Есть третье решение. Используйте функцию вместо класса для представления вашего координатора. Тогда вам не о чем беспокоиться.

Daniel T. 04.11.2020 14:31

Что я делаю сейчас, после прочтения множества статей о координаторах и ознакомления с некоторыми сложными идеями, такими как маршрутизаторы, волшебная магия и настраиваемые делегаты контроллера навигации:

Контроллер представления строго владеет Координатором, а Координатор имеет слабую ссылку на Контроллер представления, если вообще имеет. Координатор имеет слабую ссылку на своего родителя, чтобы поддерживать Цепочку ответственности для связи между объектами Координатора.

(Примером шаблона проектирования цепочки ответственности может быть Responder Chain в iOS.)

В тот момент, когда вы вызываете stop для какого-либо координатора, вы видите, как контроллер выскакивает из стека, освобождает и освобождает координатор. Таким образом, когда нажата кнопка «Назад» и контроллер представления закрывается, координатор освобождается.

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

Первоначально я решил проблему UINavigationControllerDelegate, создав класс NavigationControllerMutliDelegate, который соответствует протоколу UINavigationControllerDelegate. У него была логика регистрации / отмены регистрации. Затем этот объект был передан каждому координатору, чтобы уведомить координатора, когда контроллер представления закрывается. NavigationControllerMutliDelegate был примером шаблона проектирования «Посетитель», у него было множество координаторов, и при отображении / отключении View Controller он уведомлял всех координаторов, отправляя объект каждому.

Но в конце концов, увидев, сколько там кода и ненужной сложности, я просто выбрал View Controller, владеющий координатором. Я просто хочу, чтобы объект находился над контроллером представления, в котором хранятся поставщики данных, службы, модели просмотра и т.д., чтобы контроллер представления был чище. Я не хочу заново изобретать пуш-поп-стек координаторов и решать столько проблем с владельцами. Как будто я хочу что-то облегчить мою жизнь, а не усложнять ее еще больше ..

Мое решение - использовать функцию в качестве координатора вместо класса. Таким образом, у меня вообще нет проблем с владением. Когда нажата кнопка «Назад», представления из контроллера представления испускают завершенные события, и все просто естественным образом раскручивается без каких-либо усилий с моей стороны.

start(), который вы показываете в своем примере, можно выразить гораздо проще, просто:

func startInStore(navigationController: UINavigationController) {
    let inStoreMainViewModel = InStoreMainViewModel()
    let inStoreMainController = InStoreMainController()
    inStoreMainController.viewModel = inStoreMainViewModel

    navigationController.pushViewController(inStoreMainController, animated: true)
}

Пример приложения, использующего этот стиль, можно найти здесь: https://github.com/danielt1263/RxMyCoordinator

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

class InStoreMainCoordinator {
    func start(with navigationController: UINavigationController, container: Container) {
        let inStoreMainViewModel = InStoreMainViewModel()
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel
        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}

Затем просто позвоните InStoreMainCoordinator().start(with: navigationController, container: container), когда захотите запустить этот экран. Вам вообще не нужно постоянно ссылаться на этот InStoreMainCoordinator. Таким образом, у вас не будет проблем с кнопкой возврата, так как вам не нужно освобождать этих координаторов. Они существуют только тогда, когда вы переключаете экран на новый. Чтобы лучше понять этот метод, предположим, что у вас есть другой экран, представленный, например, классом InStoreDetailsController, и этот экран сведений должен запускаться после нажатия чего-либо в InStoreMainController. Затем вы можете реализовать два класса координаторов, связанных с этими контроллерами представления, например:

class InStoreMainCoordinator {
    func start(with navigationController: UINavigationController, container: Container) {
        let inStoreMainViewModel = InStoreMainViewModel(onStoreSelected: { storeId in
            InStoreDetailsCoordinator().start(with: navigationController, container: container, dependencies: .init(storeId: storeId))
        })
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel
        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}
class InStoreDetailsCoordinator {
    struct Dependencies {
        var storeId: String
    }
    func start(with navigationController: UINavigationController, container: Container, dependencies: Dependencies) {
        let inStoreDetailsViewModel = InStoreDetailsViewModel(storeId: dependencies.storeId)
        let inStoreDetailsController = InStoreDetailsController()
        inStoreDetailsController.viewModel = inStoreDetailsViewModel
        navigationController.pushViewController(inStoreDetailsController, animated: true)
    }
}

Как видите, если вы используете замыкания вместо шаблона делегата, вы можете записать все, что связано с одним экраном, в одной функции (включая нажатие на экран и обработку событий, связанных, например, с переходом с этого экрана на другой экран). Таким образом, у вас может быть только один метод для каждого экрана в ваших координаторах, который вы вызываете, когда вам нужно переключить экран, и вам не нужно их сохранять, поскольку все, что необходимо сохранить, сохраняется чем-то еще (в приведенном выше примере модель представления сохраняет обработчик, указанный в параметре onStoreSelected, который используется для переключения на другой экран). Я считаю, что это решение проще, чем использование дочерних координаторов. Он работает нормально и не требует дополнительной обработки кнопки возврата.

Другое альтернативное решение, которое отлично работает, особенно если у вас не очень много экранов в вашем приложении, - это создание методов startNameOfYourScreen (...) для каждого контроллера представления в вашем основном классе Координатор приложений или в другом названии. Как вы можете видеть выше, если вы используете закрытие вместо шаблона делегата, вы можете записать все, что связано с одним экраном, в одной функции, что позволяет сделать это довольно простым. Вы можете при желании разделить эти функции на расширения класса Координатор приложений и поместить их в отдельные файлы, чтобы иметь лучшую организацию в вашем проекте. В этом решении у вас также нет проблем с кнопкой возврата, поскольку вы снова не создаете экземпляры дочерних координаторов, и вам не нужно их освобождать.

Если, однако, по какой-то причине вы решите, что все еще хотите пойти по пути дочерних координаторов, то для справки вот несколько ссылок на статьи о возможных решениях проблемы с кнопкой возврата при использовании дочерних координаторов:

Другие вопросы по теме