Перейти к содержимому

Swift как с модального экрана делать pushviewcontroller

  • автор:

Как поменять UIViewController внутри модального окна?

Добрый день. Я делаю приложение со страницей входа с помощью Swift 4 и Xcode 10. На главной странице приложения у меня 2 кнопки: вход и регистрация. При нажатии на любую из них я презентую модальное окно с помощью

navigationController?.present(LoginScreen(), animated: false)

На странице LoginScreen() у меня форма входа и кнопка. Если вход успешно произведен, то я делаю

navigationController?.pushViewController(MainScreen(), animated: false)

но уже с модального окна.
Проблема в том, что pushViewController почему-то не работает с модального окна, хотя если вызвать со страницы с кнопками, то все прекрасно работает. Просто страница входа так и остается висеть.

  • Вопрос задан более трёх лет назад
  • 259 просмотров

Вопросы с меткой [uikit]

Ещё никто не добавил руководство по использованию для метки uikit, но есть описание этой метки.

82 вопроса
Конкурсные
Неотвеченные

  • Конкурсные 0
  • Неотвеченные
  • Цитируемые
  • Рейтинг
  • Неотвеченные (мои метки)

Swift animation

Всем привет, у меня появился насущный вопрос как можно реализовать анимацию в свифт без использования uikit или как совместить uikit с swift ui. Делаю такую себе игру и у меня есть анимация персонажа .

задан 9 окт 2023 в 22:33
29 показов

Swift системный bottom Sheet при звонке

Подскажите, возможно ли изменить цвет текста на системном сообщении этом, где номер?
задан 2 окт 2023 в 14:33

Как я могу перевести значения в dict из модели, содержащей переменные двух типов, в строки?

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

задан 29 сен 2023 в 10:24
28 показов

Округление картинок UITableView

Я пытаюсь округлить изображения в tableView, но почему-то округлить не удается. В чем может быть дело? class Picture < var picture: UIImage? init(picture: UIImage?) < self.picture .

задан 8 сен 2023 в 11:07
17 показов

UIKit Accordion для Joomla 4 всегда закрытый первый пункт, как?

UIKit 2.27.4 — да, именно эта версия, разраб по своим соображения не хочет пока обновлять до более высокой версии. Шаблон на Gantry 5, с встроенным UIKit. Суть такова: хочу в тексте статьи запихать .

задан 26 авг 2023 в 22:34
35 показов

Как инкапсулировать navigation cotroller в tab bar controller? (Программно)

Имеется стартовый tab bar import UIKit class StartTabBarController: UITabBarController < override func viewDidLoad() < super.viewDidLoad() generateTabBar() >private .
задан 15 авг 2023 в 23:16

UILabel как сделать внутренние отступы

Как сделать у UILabel, как на картинке ниже? Сейчас пробую делать так, но не работает. import UIKit class ViewController: UIViewController < override func viewDidLoad() < super.

задан 17 мая 2023 в 18:22
20 показов

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

Пытаюсь сделать приложение-тест с пятью вопросами и четырьмя вариантами ответов на каждый вопрос. Все пять вопросов следуют один за другим, то есть каждый вопрос с четырьмя вариантами ответов .

задан 15 мая 2023 в 14:44
142 показа

UILabel по центру UICollectionVIewCell с отступами по краям

Как разместить UILabel внутри UICollectionViewCell по центру с отступами по бокам, как на картинке ниже? Пытался делать так, но такой вариант не работает: import UIKit import SnapKit class .

задан 6 мая 2023 в 16:00
10 показов

Не работает свойство у UIImage?

не работает свойсво withRenderingMode и withTintColor также не знаю почему изображения png формата задний фон — это просто вьюха с закруглеными углами

задан 8 мар 2023 в 15:02
35 показов

Метод возвращает пустую функцию, а хотелось бы массив с объектами

Есть метод fetch в структуре, очень хочется, чтобы он вернул массив. Но метод с замыканием ничего не возвращает, хотя на этапе finishedCh = data все как надо тип присутствует. Если даже методу .

задан 2 мар 2023 в 20:37
66 показов

Не могу остановить скролинг листа UITableView

У меня есть длинный список и во время прокрутки его вниз или вверх, при попытке остановить его, ничего не происходит, саписок продолжает прокручиваться. Нужно дождаться пока оно само остановится. .

задан 27 фев 2023 в 12:34

Swift — Как остановить таймер?

Как остановить таймер? Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) < _ in if self.progressView.progress != 0 < self.progressView.progress -= 0.01 >else < self. задан 13 фев 2023 в 21:31 313 показов

Swift как поменять ширину и высоту картинки в коде?

Как изменить ширину и высоту картинки в кнопке в коде(независимо от реального размера картинки)?
задан 7 фев 2023 в 16:28

Swift таймер запускается сразу после запуска приложения а не при переходе на другой Экран и как это исправить?

Есть два ViewController-a первый это «MenuVC» и второй «ViewController» Первоначальный экран это «MenuVC» к нему же припоял navigatation Controller и на этом view .

задан 4 фев 2023 в 14:57
15 30 50 на странице

    Важное на Мете

Связанные метки

Подписаться на ленту

Лента новых вопросов с меткой [uikit]

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.8.3130

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Как реализовать сложную навигацию в приложениях на iOS для iPhone

В каждом приложении, если оно не является одностраничным, обязательно должна быть навигация, позволяющая пользователям перемещаться между экранами, вводить информацию и реагировать на события.
Не важно, используете вы навигационные контроллеры, модальные view-контроллеры или что-то ещё, качественно организовать навигацию, не загоняя себя в угол, может быть весьма непросто. Чаще всего получается так, что view-контроллеры жёстко взаимосвязаны между собой и тянут зависимости по всему приложению.
В этой статье рассмотрим несколько способов организации навигации в приложениях, написанных на Swift.

Проблема организации навигации

Один из основных способов организации навигации в iOS — использование UINavigationController, который позволяет добавлять и убирать другие view-контроллеры, например, вот так:

class ImageListViewController : UITableViewController {
override func tableView ( _ tableView : UITableView ,
didSelectRowAt indexPath : IndexPath ) {
let image = images [ indexPath . row ]
let detailVC = ImageDetailViewController ( image : image )
navigationController? . pushViewController ( detailVC , animated : true )
}
}

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

Аргументация в пользу координаторов

Одним из способов сделать навигацию более гибкой (при этом избавить view-контроллеры от необходимости знать друг о друге) — использовать шаблон проектирования «Координатор». Идея заключается в том, чтобы добавить промежуточный/родительский объект, который бы координировал несколько view-контроллеров.
Предположим, мы создаем серию экранов для ознакомления с приложением, где пользователю кратко рассказывается об основной функциональности. Вместо того, чтобы заставлять каждый view-контроллер самостоятельно добавлять в navigationController следующий экран, можно поручить эту задачу координатору.
Начнем с создания протокола делегата, с помощью которого наши контроллеры смогут уведомлять своего владельца о нажатии кнопки перехода на следующий экран:

1
2
3
4
5
6
7
8
9
10
11
12
13

protocol OnboardingViewControllerDelegate : AnyObject {
func onboardingViewControllerNextButtonTapped (
_ viewController : OnboardingViewController
)
}

class OnboardingViewController : UIViewController {
weak var delegate : OnboardingViewControllerDelegate?

private func handleNextButtonTap ( ) {
delegate? . onboardingViewControllerNextButtonTapped ( self )
}
}

Затем добавим класс координатора, который будет выполнять роль делегата для всех view-контроллеров и управлять навигацией между ними с помощью навигационного контроллера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

class OnboardingCoordinator : OnboardingViewControllerDelegate {
weak var delegate : OnboardingCoordinatorDelegate?

private let navigationController : UINavigationController
private var nextPageIndex = 0

init ( navigationController : UINavigationController ) {
self . navigationController = navigationController
}

func activate ( ) {
goToNextPageOrFinish ( )
}

func onboardingViewControllerNextButtonTapped (
_ viewController : OnboardingViewController ) {
goToNextPageOrFinish ( )
}

private func goToNextPageOrFinish ( ) {
// We use an enum to store all content for a given onboarding page
guard let page = OnboardingPage ( rawValue : nextPageIndex ) else {
delegate? . onboardingCoordinatorDidFinish ( self )
return
}

let nextVC = OnboardingViewController ( page : page )
nextVC . delegate = self
navigationController . pushViewController ( nextVC , animated : true )

Одним из главных преимуществ координаторов является вынесение логики навигации из view-контроллеров. Это позволяет view-контроллерам сосредоточиться на том, что они делают лучше всего — управлять представлениями (views).
Следует отметить, что у OnboardingCoordinator есть собственный делегат. Мы можем сделать так, чтобы AppDelegate управлял координатором, сохранив его у себя и, тем самым, став его делегатом. Или же использовать несколько уровней координаторов, чтобы организовать основную часть навигации в приложении. Например, AppCoordinator может управлять OnboardingCoordinator и другими координаторами на том же уровне навигационной иерархии. Весьма круто, не правда ли?

Куда идём, навигатор?

Другим полезным подходом (особенно для приложений с множеством экранов и сложной системой переходов между ними) будет добавление специально предназначенных для этого типов — навигаторов.
Для того, чтобы это сделать, начнём с создания протокола Navigator. Добавим ему связанный тип, указывающий, к какому виду Destination возможен переход:

protocol Navigator {
associatedtype Destination

func navigate ( to destination : Destination )
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

class LoginNavigator : Navigator {
// Here we define a set of supported destinations using an
// enum, and we can also use associated values to add support
// for passing arguments from one screen to another.
enum Destination {
case loginCompleted ( user : User )
case forgotPassword
case signup
}

// In most cases it’s totally safe to make this a strong
// reference, but in some situations it could end up
// causing a retain cycle, so better be safe than sorry 🙂
private weak var navigationController : UINavigationController?

init ( navigationController : UINavigationController ) {
self . navigationController = navigationController
}

func navigate ( to destination : Destination ) {
let viewController = makeViewController ( for : destination )
navigationController? . pushViewController ( viewController , animated : true )
}

private func makeViewController ( for destination : Destination ) -> UIViewController {
switch destination {
case . loginCompleted ( let user ) :
return WelcomeViewController ( user : user )
case . forgotPassword :
return PasswordResetViewController ( )
case . signup :
return SignUpViewController ( )
}
}
}

С помощью навигаторов переход к другому view-контроллеру осуществляется простым вызовом navigator.navigate(to: destination), и нам вовсе не требуется несколько уровней делегирования, чтобы это сделать. Единственное, что требуется каждому view-контроллеру, — хранить ссылку на навигатор, поддерживающий все необходимые состояния:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class LoginViewController : UIViewController {
private let navigator : LoginNavigator

init ( navigator : LoginNavigator ) {
self . navigator = navigator
super . init ( nibName : nil , bundle : nil )
}

private func handleLoginButtonTap ( ) {
performLogin { [ weak self ] result in
switch result {
case . success ( let user ) :
self ? . navigator . navigate ( to : . loginCompleted ( user : user ) )
case . failure ( let error ) :
self ? . show ( error )
}
}
}

private func handleForgotPasswordButtonTap ( ) {
navigator . navigate ( to : . forgotPassword )
}

private func handleSignUpButtonTap ( ) {
navigator . navigate ( to : . signup )
}
}

Мы можем пойти еще дальше и объединить навигаторы с помощью шаблона проектирования «Фабрика», чтобы вынести создание view-контроллеров из самих навигаторов и сделать логику ещё более разделённой:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class LoginNavigator : Navigator {
private weak var navigationController : UINavigationController?
private let viewControllerFactory : LoginViewControllerFactory

init ( navigationController : UINavigationController ,
viewControllerFactory : LoginViewControllerFactory ) {
self . navigationController = navigationController
self . viewControllerFactory = viewControllerFactory
}

func navigate ( to destination : Destination ) {
let viewController = makeViewController ( for : destination )
navigationController? . pushViewController ( viewController , animated : true )
}

private func makeViewController ( for destination : Destination ) -> UIViewController {
switch destination {
case . loginCompleted ( let user ) :
return viewControllerFactory . makeWelcomeViewController ( forUser : user )
case . forgotPassword :
return viewControllerFactory . makePasswordResetViewController ( )
case . signup :
return viewControllerFactory . makeSignUpViewController ( )
}
}
}

Используя вышеописанный подход, у нас появляется замечательная возможность передавать во view-контроллеры различные типы навигаторов таким образом, чтобы они не знали друг о друге. К примеру, с помощью «Фабрики» можно воспользоваться WelcomeNavigator внутри WelcomeViewController, при этом LoginNavigator не будет в курсе о существовании WelcomeNavigator.

URL-адреса и глубокие ссылки

Часто мы хотим не только упростить саму навигацию, но и позволить другим приложениям и веб-сайтам вызывать наше приложение посредством глубоких ссылок (deep linking). Стандартный способ сделать подобное в iOS — добавить новую URL-схему, чтобы другие приложения могли ссылаться на определённые экраны или функции нашего приложения.
Используя координаторы/навигаторы (по отдельности или вместе), реализовать поддержку URL-адресов и глубоких ссылок становится гораздо проще. Ведь благодаря им у нас есть конкретные места, в которые можно добавить логику обработки ссылок.

Выводы

Перемещение логики навигации из view-контроллеров в специальные объекты, такие как координаторы и навигаторы, делает переходы между несколькими view-контроллерами намного проще. В координаторах и навигаторах нам нравится то, что с их помощью легко разделить логику навигации на несколько областей видимости (scopes) и объектов, избавившись от централизованной маршрутизации в той или иной форме.
Ещё одно преимущество — отсутствие необходимости использовать неопциональные опционалы (non-optional optionals). К примеру, в навигационной логике не нужно использовать ссылку на navigationController во view-контроллере. Что, как правило, приводит к более предсказуемому и поддерживаемому коду.

Композиция UIViewController-ов и навигация между ними (и не только)

В этой статье я хочу поделиться опытом который мы успешно используем уже несколько лет в наших iOS приложениях, 3 из которых в данный момент находятся в Appstore. Данный подход хорошо зарекомендовал себя и недавно мы сегрегировали его от остального кода и оформили в отдельную библиотеку RouteComposer о которой собственно и пойдет речь.

Но, для начала, давайте попробуем разобраться что подразумевается под композицией вью контроллеров в iOS.

Прежде чем переходить собственно к пояснениям, я напомню, что в iOS чаще всего подразумевается под вью контролером или UIViewController . Это класс унаследованный от стандартного UIViewController , который является базовым контроллером паттерна MVC, который Apple рекомендует использовать для разработки iOS приложений.

Можно использовать альтернативные архитектурные паттерны такие как MVVM, VIP, VIPER, но и в них UIViewController будет задействован так или иначе, а, значит, данная библиотека может использоваться вместе с ними. Cущность UIViewController используется для контроля UIView , которая чаще всего представляет собой экран или значительную часть экрана, обработки событий от него и отображения в нем неких данных.

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

Стандартными контейнер вью контроллерами поставляемыми с Cocoa Touch можно считать: UINavigationConroller , UITabBarController , UISplitController , UIPageController и некоторые другие. Также, пользователь может создавать свои кастомные контейнер вью контролеры следуя правилам Cocoa Touch описаным в документации Apple.

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

Почему же стандартное решение для композиции вью контроллеров оказалось для нас не оптимальным и мы разработали библиотеку облегчающую наш труд.

Давайте рассмотрим для начала композицию некоторых стандартных контейнер вью контролеров для примера:

Примеры композиции в стандартных контейнерах

UINavigationController

let tableViewController = UITableViewController(style: .plain) // Вставка первого вью контролера в контролер навигации let navigationController = UINavigationController(rootViewController: tableViewController) // . // Вставка второго вью контролера в контролер навигации let detailViewController = UIViewController(nibName: "DetailViewController", bundle: nil) navigationController.pushViewController(detailViewController, animated: true) // . // Вернуться к первому контроллеру navigationController.popToRootViewController(animated: true)
UITabBarController

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() // Создание контейнера let tabBarController = UITabBarController() // Вставка двух вью контролеров в таб бар контролер tabBarController.viewControllers = [firstViewController, secondViewController] // Один из программных способов переключения видимого контролера tabBarController.selectedViewController = secondViewController
UISplitViewController

let firstViewController = UITableViewController(style: .plain) let secondViewController = UIViewController() // Создание контейнера let splitViewController = UISplitViewController() // Вставка первого вью контролера в сплит контролер splitViewController.viewControllers = [firstViewController] // Вставка второго вью контролера в сплит контролер splitViewController.showDetailViewController(secondViewController, sender: nil)

Примеры интеграции (композиции) вью котроллеров в стек

Установка вью контролера рутом
let window: UIWindow = //. window.rootViewController = viewController window.makeKeyAndVisible()
Модальная презентация вью контролера
window.rootViewController.present(splitViewController, animated: animated, completion: nil)

Почему мы решили создать библиотеку для композиции

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

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

Было бы здорово просто вызывать методы вроде goToAccount() , goToMenu() или goToProduct(withId: «012345») по нажатии пользователем на некоторую кнопку или по получении приложением универсальной ссылки из другого приложения и не задумываться о интеграции данного вью контроллера в стек, зная что создатель этого вью контроллера уже предоставил эту реализацию.

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

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

Остается добавить, что это все умножится на N как только ваша маркетинговая команда изъявит желание провести A/B тестирование на живых пользователях и проверить какой способ навигации работает лучше, например, таб бар или гамбургер меню?

  • Давайте отрежем Сусанину ноги Давайте показывать 50% пользователей Таб Бар, а другим Гамбургер меню и через месяц мы вам скажем какие пользователи видят больше наших специальных предложений?

Я попробую рассказать вам как мы подошли к решению этой проблемы и в конце концов выделили его в библиотеку RouteComposer.

Сусанин Route Composer

Проанализировав все сценарии композиции и навигации мы попытались абстрагировать приведенный в примерах выше код и выделили 3 основных сущности из которых состоит и которыми оперирует библиотека RouteComposer — Factory , Finder , Action . Кроме того, в библиотеке присутствуют 3 вспомогательные сущности которые отвечают за небольшой тюнинг который может потребоваться в процессе навигации — RoutingInterceptor , ContextTask , PostRoutingTask . Все эти сущности должны быть сконфигурированы в цепь зависимостей и переданы Router у — объекту, который и будет строить ваш стек вью контролеров.

Но, о каждой из них по порядку:

Factory

Как и следует из названия Factory отвечает за создание вью контролера.

public protocol Factory < associatedtype ViewController: UIViewController associatedtype Context func build(with context: Context) throws ->ViewController >

Здесь же важно оговориться о понятии контекста. Контекстом в рамках библиотеки мы называем все что может понадобиться вью контролеру для того что бы быть созданным. Например, для того что бы показать вью контроллер отображающий детали продукта — необходимо в него передать некий productID например в виде String . Сущностью контекста может быть все что угодно: объект, структура, блок или тупл(tuple). Если же вашему контроллеру ничего не нужно для того что бы быть созданным — контекст можно указать как Any? и устанавливать в nil .

class ProductViewControllerFactory: Factory < func build(with productID: UUID) throws ->ProductViewController < let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) productViewController.productID = productID // В целом, данное действие лучше переложить на `ContextAction`, но об этом далее return productViewController >>

Из реализации выше становится понятно, что данная фабрика загрузит образ вью контроллера из XIB файла и установит в него переданный productID. Помимо стандартного протокола Factory , библиотека предоставляет несколько стандартных реализаций этого протокола для того что бы избавить вас от написания банального кода (в частности приведенного в примере выше).

Далее я воздержусь от приведения описаний протоколов и примеров их реализаций, так как с ними можно подробно ознакомиться скачав пример, поставляемый вместе с библиотекой. Там приведены различные реализации фабрик для обычных вью контролеров и контейнеров, а так же способы их конфигурации.

Action

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

Самой банальной реализацией Action является модальная презентация контроллера:

class PresentModally: Action < func perform(viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (_: ActionResult) ->Void) < guard existingController.presentedViewController == nil else < completion(.failure("\(existingController) is already presenting a view controller.")) return >existingController.present(viewController, animated: animated, completion: < completion(.continueRouting) >) > >

Библиотека содержит реализацию большинства стандартных способов интеграции вью контролеров в стек и вам скорее всего не придется создавать свои самостоятельно до тех пор, пока вы не используете некий кастомный контейнер вью контроллер или способ презентации. Но создание кастомных Action не должно вызвать проблем если вы ознакомитесь с примерами.

Finder

Сущность Finder отвечает роутеру на вопрос — А создан ли уже такой вью контроллер и есть ли он уже в стеке? Возможно, ничего создавать не требуется и достаточно показать то что уже есть?.

public protocol Finder < associatedtype ViewController: UIViewController associatedtype Context func findViewController(with context: Context) ->ViewController? >

Если вы храните ссылки на все созданные вами вью контроллеры — то в вашей реализации Finder вы можете просто возвращать ссылку на искомый вью контроллер. Но чаще всего это не так, ведь стек приложения, особенно особенно если оно большое, меняется довольно динамично. Кроме того, вы можете иметь несколько одинаковых вью контроллеров в стеке показывающих разные сущности (например несколько ProductViewController показывающие разные товары с разным productID), поэтому реализация Finder может потребовать кастомной имплементации и поиска соответствующего вью контролера в стеке. Библиотека облегчает эту задачу предоставляя StackIteratingFinder как расширение Finder — протокол с соответствующими настройками, позволяющий упростить эту задачу. В реализации StackIteratingFinder вам потребуется только ответить на вопрос — является ли данный вью контролер тем который роутер ищет по вашему запросу.

Пример такой реализации:

class ProductViewControllerFinder: StackIteratingFinder < let options: SearchOptions init(options: SearchOptions = .currentAndUp) < self.options = options >func isTarget(_ productViewController: ProductViewController, with productID: UUID) -> Bool < return productViewController.productID == productID >>

Вспомогательные сущности

RoutingInterceptor

RoutingInterceptor позволяет перед началом композиции вью контролеров выполнить некоторые действия и сообщить роутеру, можно ли интегрировать вью контроллеры в стек. Самым банальным примером такой задачи является аутентификация (но совсем не банальным в реализации). Например, вы хотите показать вью контроллер с деталями пользовательского аккаунта, но, для этого, пользователь должен быть залогинен в системе. Вы можете реализовать RoutingInterceptor и добавить его к конфигурации вью контроллера пользовательских деталей и внутри проверить: если пользователь залогинен — разрешить роутеру продолжить навигацию, если же нет — показать вью контроллер который предложит пользователю залогиниться и если данное действие пройдет успешно — разрешить роутеру продолжить навигацию или отменить ее, если пользователь откажется от входа в систему.

class LoginInterceptor: RoutingInterceptor < func execute(for destination: AppDestination, completion: @escaping (_: InterceptorResult) ->Void) < guard !LoginManager.sharedInstance.isUserLoggedIn else < // . // Показать LoginViewController и по резульату действий пользователя вызвать completion(.success) или completion(.failure("User has not been logged in.")) // . return >completion(.success) > > 

Реализация такого RoutingInterceptor с комментариями содержится в примере поставляемом с библиотекой.

ContextTask

Сущность ContextTask , если вы ее предоставите, может быть применена по отдельности к каждому вью контроллеру в конфигурации вне зависимости от того был ли он только что создан роутером или был найден в стеке, и, вы просто хотите обновить в нем данные и или установить некоторые дефолтные параметры (например показывать кнопку закрыть или не показывать).

PostRoutingTask

Реализация PostRoutingTask будет вызвана роутером после успешного завершения интеграции запрошенного вью контроллера в стек. В его реализации удобно добавлять различную аналитику или дергать различные сервисы.

Более подробно с реализацией всех описанных сущностей можно ознакомиться в документации к библиотеке а также в прилагаемом примере.

PS: Количество вспомогательных сущностей которое может быть добавлено в конфигурацию не ограничено.

Конфигурация

Все описанные сущности хороши тем что разбивают процесс композиции на маленькие, взаимозаменяемые и хорошо трестируемые блоки.

Теперь перейдем к самому главному — к конфигурации, то есть соединению этих блоков между собой. Для того что бы собрать данные блоки между собой и объединить в цепочку шагов, библиотека предоставляет билдер класс StepAssembly (для контейнеров — ContainerStepAssembly ). Его реализация позволяет нанизать блоки композиции в единый конфигурационный объект как бусины на ниточку, а также указать зависимости от конфигураций других вью контроллеров. Что делать с конфигурацией в дальнейшем — зависит от вас. Можете скормить ее роутеру с необходимыми параметрами и он построит для вас стек вью контроллеров, можете сохранить в словарь и использовать в последствии по ключу — зависит от вашей конкретной задачи.

Рассмотрим банальный пример: Допустим, по нажатии на ячейку в списке или получении приложением универсальной ссылки из сафари или почтового клиента, нам нужно показать модально вью контроллер продукта с неким productID. При этом вью контролер продукта должен быть построен внутри UINavigationController -а, что бы на его панели управления он мог показать свое название и кнопку закрыть. Кроме того, этот продукт можно показывать только пользователям которые вошли в систему, в противном случае предложить им войти в систему.

Если разобрать этот пример без использования библиотеки, то выглядеть он будет примерно вот так:

class ProductArrayViewController: UITableViewController < let products: [UUID]? let analyticsManager = AnalyticsManager.sharedInstance // Методы UITableViewControllerDelegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) < guard let productID = products[indexPath.row] else < return >// Уйдет в LoginInterceptor guard !LoginManager.sharedInstance.isUserLoggedIn else < // Много кода показывающего LoginViewController и обрабатывающего его результаты и в последствии вызывающего `showProduct(with: productID)` return >showProduct(with: productID) > func showProduct(with productID: String) < // Уйдет в ProductViewControllerFactory let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil) // Уйдет в ProductViewControllerContextTask productViewController.productID = productID // Уйдет в NavigationControllerStep и PushToNavigationAction let navigationController = UINavigationController(rootViewController: productViewController) // Уйдет в GenericActions.PresentModally present(alertController, animated: navigationController) < [weak self] Уйдет в См. ProductViewControllerPostTask self?.analyticsManager.trackProductView(productID: productID) >> >

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

Рассмотрим конфигурацию этого примера с использованием библиотеки:

let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) // Вспомогательные сущности: .adding(LoginInterceptor()) .adding(ProductViewControllerContextTask()) .adding(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) // Цепочка зависимостей: .using(PushToNavigationAction()) .from(NavigationControllerStep()) // NavigationControllerStep -> StepAssembly(finder: NilFinder(), factory: NavigationControllerFactory()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble()

Если перевести это на человеческий язык:

  • Проверить что пользователь вошел в систему, и если нет предложить ему вход
  • Если пользователь успешно вошел в систему, продолжить
  • Искать продукт вью контроллер предоставленным Finder
  • Если был найден — сделать видимым и закончить
  • Если не был найден — создать UINavigationController , интегрировать в него вью контролер созданный ProductViewControllerFactory используя PushToNavigationAction
  • Встроить полученый UINavigationController используя GenericActions.PresentModally от текущего вью контролера

Конфигурирование требует некоторого изучения как и многие комплексные решения, например концепция AutoLayout и, на первый взгляд, может показаться сложным и излишним. Однако, количество решаемых задач приведенным фрагментом кода охватывает все аспекты от авторизации до дип-линкига, а разбитие на последовательности действий дает возможность легко менять конфигурацию без необходимости внесения изменений в код. Кроме того, реализация StepAssembly поможет вам избежать проблем с незаконченой цепочкой шагов, а контроль типов — проблем с несовместимостью входных параметров у разных вью котроллеров.

Рассмотрим псевдокод полного приложения в котором в неком ProductArrayViewController выводится список продуктов и, если пользователь выбирает этот продукт, показывает его в зависимости от того залогинен пользователь или нет, или предлагает войти в систему и показывает после успешного входа:

Объекты конфигурации

// `RoutingDestination` протокол обертка для роутера. Можно добавить туда дополнительные параметры при желании для ваших обработчиков. struct AppDestination: RoutingDestination < let finalStep: RoutingStep let context: Any? >struct Configuration < // Является статическим только для примера, вы можете создать протокол и подменять его реализации в зависимости от задачи static func productDestination(with productID: UUID) ->AppDestination < let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor()) .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(PushToNavigationAction()) .from(NavigationControllerStep()) .using(GenericActions.PresentModally()) .from(CurrentControllerStep()) .assemble() return AppDestination(finalStep: productScreen, context: productID) >>

Реализация списка продуктов

class ProductArrayViewController: UITableViewController < let products: [UUID]? //. // DefaultRouter - реализация Router класса предоставляемая библиотекой, создается внутри UIViewController для примера let router = DefaultRouter() override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) < guard let productID = products[indexPath.row] else < return >router.navigate(to: Configuration.productDestination(with: productID)) > >

Реализация универсальных ссылок

@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate < //. func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) ->Bool < guard let productID = UniversalLinksManager.parse(url: url) else < return false >return DefaultRouter().navigate(to: Configuration.productDestination(with: productID)) == .handled > > 

Вот в сущности и все что требовалось для реализации всех условий из поставленного примера.

Следует также упомянуть что конфигурация может быть гораздо сложнее и состоять из зависимостей. Например, если вам нужно показывать продукт не просто от текущего контроллера, но, в случае если пользователь пришел в него по универсальной ссылке — то под ним должен оказаться обязательно ProductArrayViewController, который должен находиться обязательно внутри UINavigationController после условного HomeViewController — то это все может быть указано в конфигурации StepAssembly используя from() . Если ваше приложение охвачено RouteComposer полностью, сделать это не составит труда (Смотрите приложение в примере к библиотеке). Кроме того, вы можете создать несколько реализаций Configuration и передавать их в один и тот же вью контроллер для реализации разных вариантов композиции. Или выбирать один из них, если в вашем приложении проводится A/B тестирование, в зависимости к какой фокус группе относится ваш пользователь.

Вместо заключения

На данный момент, описаный выше подход используется в 3х приложениях в продакшене и хорошо зарекомендовал себя. Разбиение задачи композиции на маленькие, легко читаемые, взаимозаменяемые блоки облегчает понимание и поиск багов. Дефолтная реализация Fabric , Finder и Action позволяет для большинства задач сразу начать с конфигурации без необходимости создавать свои. А самое главное, что дает данный подход — возможность автономного создания вью контроллеров без необходимости внесения в их код знания о том, как они будут построены, и как пользователь будет двигаться в дальнейшем. Вью контроллер должен лишь по действию пользователя вызвать нужную конфигурацию, что может быть так же абстрагировано.

Библиотека, как и предоставляемая ей реализация роутера, не использует никаких трюков с objective c рантаймом и полностью следует всем концепциям Cocoa Touch, лишь помогая разбить процесс композиции на шаги и выполняет их в заданной последовательности. Библиотека протестирована с версиями iOS с 9 по 12.

Данный подход вписывается во все архитектурный паттерны которые подразумевают работу с UIViewController стеком (MVC, MVVM, VIP, RIB, VIPER и т.д.)

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

Буду рад вашим комментариям и предложениям.

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

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