30 мая 2018
6 002
iOS разработка
В каждом приложении, если оно не является одностраничным, обязательно должна быть навигация, позволяющая пользователям перемещаться между экранами, вводить информацию и реагировать на события.
Не важно, используете вы навигационные контроллеры, модальные view-контроллеры или что-то ещё, качественно организовать навигацию, не загоняя себя в угол, может быть весьма непросто. Чаще всего получается так, что view-контроллеры жёстко взаимосвязаны между собой и тянут зависимости по всему приложению.
В этой статье рассмотрим несколько способов организации навигации в приложениях, написанных на Swift.
Один из основных способов организации навигации в iOS — использование UINavigationController, который позволяет добавлять и убирать другие view-контроллеры, например, вот так:
1 2 3 4 5 6 7 8 | 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 // MARK: - Initializer init(navigationController: UINavigationController) { self.navigationController = navigationController } // MARK: - API func activate() { goToNextPageOrFinish() } // MARK: - OnboardingViewControllerDelegate func onboardingViewControllerNextButtonTapped( _ viewController: OnboardingViewController) { goToNextPageOrFinish() } // MARK: - Private 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) nextPageIndex += 1 } } |
Одним из главных преимуществ координаторов является вынесение логики навигации из view-контроллеров. Это позволяет view-контроллерам сосредоточиться на том, что они делают лучше всего — управлять представлениями (views).
Следует отметить, что у OnboardingCoordinator есть собственный делегат. Мы можем сделать так, чтобы AppDelegate управлял координатором, сохранив его у себя и, тем самым, став его делегатом. Или же использовать несколько уровней координаторов, чтобы организовать основную часть навигации в приложении. Например, AppCoordinator может управлять OnboardingCoordinator и другими координаторами на том же уровне навигационной иерархии. Весьма круто, не правда ли?
Другим полезным подходом (особенно для приложений с множеством экранов и сложной системой переходов между ними) будет добавление специально предназначенных для этого типов — навигаторов.
Для того, чтобы это сделать, начнём с создания протокола Navigator. Добавим ему связанный тип, указывающий, к какому виду Destination возможен переход:
1 2 3 4 5 | 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? // MARK: - Initializer init(navigationController: UINavigationController) { self.navigationController = navigationController } // MARK: - Navigator func navigate(to destination: Destination) { let viewController = makeViewController(for: destination) navigationController?.pushViewController(viewController, animated: true) } // MARK: - Private 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.
Часто мы хотим не только упростить саму навигацию, но и позволить другим приложениям и веб-сайтам вызывать наше приложение посредством глубоких ссылок (deep linking). Стандартный способ сделать подобное в iOS — добавить новую URL-схему, чтобы другие приложения могли ссылаться на определённые экраны или функции нашего приложения.
Используя координаторы/навигаторы (по отдельности или вместе), реализовать поддержку URL-адресов и глубоких ссылок становится гораздо проще. Ведь благодаря им у нас есть конкретные места, в которые можно добавить логику обработки ссылок.
Перемещение логики навигации из view-контроллеров в специальные объекты, такие как координаторы и навигаторы, делает переходы между несколькими view-контроллерами намного проще. В координаторах и навигаторах нам нравится то, что с их помощью легко разделить логику навигации на несколько областей видимости (scopes) и объектов, избавившись от централизованной маршрутизации в той или иной форме.
Ещё одно преимущество — отсутствие необходимости использовать неопциональные опционалы (non-optional optionals). К примеру, в навигационной логике не нужно использовать ссылку на navigationController во view-контроллере. Что, как правило, приводит к более предсказуемому и поддерживаемому коду.
Оригинал статьи: тут