[Разработка под iOS, Разработка мобильных приложений, Swift] Использование Enum + Associated Values при навигации и передаче данных между экранами в IOS приложениях
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В этом посте мне бы хотелось затронуть извечный вопрос об организации навигации и передачи данных между экранами в IOS приложениях. В первую очередь, я хотел бы донести концепт своего подхода, а не убедить вас использовать его как волшебную таблетку. Тут не будут рассматриваться различные архитектурные подходы или возможность использования UlStoryboard с segues, в целом я опишу еще один возможный способ достигнуть желаемого со своими плюсами и минусами. И так, начнем!
Предыстория:
Безусловно, на реализацию навигации и организацию транспорта данных в проекте влияет выбор архитектурного подхода, однако и сам подход складывается из ряда обстоятельств: состав команды, time to market, состояние ТЗ, масштабируемость проекта и многое др., определяющими факторами для меня стали:
- обязательное использование MVVM;
- возможность быстро добавлять новые экраны(контроллеры, и их вью модели) в процесс навигации;
- изменения в бизнес- логике не должны затрагивать навигацию;
- изменения в навигации не должны затрагивать бизнес-логику;
- возможность быстро переиспользовать экраны без внесения исправлений в навигацию;
- возможность быстро получить представление о существующих экранах;
- возможность быстро получить представление о зависимостях в проекте;
- не повысить порог вхождения разработчиков на проект.
Ближе к делу
Стоит отметить, что конечное решение было сформировано не за один день, не лишено своих минусов и подходит скорее для маленьких и средних проектов. Для наглядности, тестовый проект можно посмотреть тут: github.com/ArturRuZ/NavigationDemo
1. Чтобы была возможность быстро получить представление о существующих экранах было принято решение завести enum с однозначным названием ControllersList.
enum ControllersList {
case textInputScreen
case textConfirmationScreen
}
2. По ряду причин в проекте не хотелось использовать сторонние решения для DI, a DI получить хотелось, в том числе с возможностью быстрого просмотра зависимостей в проекте, поэтому было решено использовать Assembly для каждого отдельного экрана (закрытого протоколом Assembly) и RootAssembly – в качестве общего scope.
protocol Assembly {
func build() -> UIViewController
}
final class TextInputAssembly: Assembly {
func build() -> UIViewController {
let viewModel = TextInputViewModel()
return TextInputViewController(viewModel: viewModel)
}
}
final class TextConfirmationAssembly: Assembly {
private let text: String
init(text: String) {
self.text = text
}
func build() -> UIViewController {
let viewModel = TextConfirmationViewModel(text: text)
return TextConfirmationViewController(viewModel: viewModel)
}
}
3. Для передачи данных между экранами(там, где это действительно необходимо) ControllersList превратился в enum с Associated Values:
enum ControllersList {
case textInputScreen
case textConfirmationScreen(text: String)
}
4. Для того чтобы ни бизнес-логика не влияла на навигацию, ни навигация на бизнес-логику, а также для быстрого переиспользования экранов, потребовалось навигацию вынести в отдельный слой. Так появился Coordinator и протокол Coordination:
protocol Coordination {
func show(view: ControllersList, firstPosition: Bool)
func popFromCurrentController()
}
final class Coordinator {
private var navigationController = UINavigationController()
private var factory: ControllerBuilder?
private func navigateWithFirstPositionInStack(to: UIViewController) {
navigationController.viewControllers = [to]
}
private func navigate(to: UIViewController) {
navigationController.pushViewController(to, animated: true)
}
}
extension Coordinator: Coordination {
func popFromCurrentController() {
navigationController.popViewController(animated: true)
}
func show(view: ControllersList, firstPosition: Bool) {
guard let controller = factory?.buildController(for: view) else { return }
firstPosition ? navigateWithFirstPositionInStack(to: controller) : navigate(to: controller)
}
}
Тут важно отметить, что протокол может описывать больше методов, в т.ч. как и Coordinator может реализовывать различные протоколы, в зависимости от нужд.
5. При всем при этом, хотелось еще и ограничить набор действий, которые требовалось совершить разработчику, добавляя новый экран в приложение. На текущий момент требовалось помнить о том, что где-то надо прописать зависимости, и возможно сделать еще какие-либо действия для того чтобы навигация заработала.
6. Совсем не хотелось создавать дополнительные роутеры и координаторы. Более того, создание дополнительной логики для навигации могло значительно усложнить как восприятие навигации, так и переиспользование экранов. Все это привело к цепочке изменений, которые в конечном итоге выглядели следующим образом:
//MARK - Dependences with controllers associations
fileprivate extension ControllersList {
typealias scope = AssemblyServices
var assembly: Assembly {
switch self {
case .textInputScreen:
return TextInputAssembly(coordinator: scope.coordinator)
case .textConfirmationScreen(let text):
return TextConfirmationAssembly(coordinator: scope.coordinator, text: text)
}
}
}
//MARK - Services all time in memory
fileprivate enum AssemblyServices {
static let coordinator: СoordinationDependencesRegstration = Coordinator()
static let controllerFactory: ControllerBuilderDependencesRegistration = ControllerFacotry()
}
//MARL: - RootAssembly Implementation
final class RootAssembly {
fileprivate typealias scope = AssemblyServices
private func registerPropertyDependences() {
// this place for propery dependences
}
}
// MARK: - AssemblyDataSource implementation
extension RootAssembly: AssemblyDataSource {
func getAssembly(key: ControllersList) -> Assembly? {
return key.assembly
}
}
Теперь при создании нового экрана, разработчику достаточно было просто внести изменения в ControllersList, а далее компилятор уже сам показывал, где надо еще внести изменения. Добавление новых экранов в ControllersList никак не влияли на текущую схему навигации, а логика управления зависимостями легко прослеживалась. Также, используя ControllersList, можно легко найти все точки вхождения в тот или иной экран, а переиспользовать экраны стало просто.
Заключение
Данный пример является упрощенной реализацией идеи и не покрывает всех кейсов использования, тем не менее сам подход показал себя достаточно гибким и адаптивным.
Из недостатков данного подхода можно выделить следующее:
- Сложно сказать, что координатор в этой реализации действительно координатор, больше это напоминает роутер с областью видимости на весь проект. Также ControllersList можно переименовать в NavigationEvents, а сами кейсы на похожий мотив, но это скорее вопрос восприятия;
- В ряде случаев, наоборот хочется ограничить возможную навигацию и тогда разумнее использовать роутеры и координаторы;
- Возможно могут быть кейсы, которые не покрывает данное решение, или оно потребует глобального переосмысления. В любом случае, перед использованием такого подхода следует оценить потенциальные риски и проблемы для вашего проекта.
Большая часть постов о навигации и передачи данных в IOS приложениях затрагивает либо использования координаторов и роутеров (на каждый или группу экранов), либо навигацию через segue, singleton и т.п., но не один из этих вариантов не подходил мне по тем или иным причинам.
Возможно и вам для решения задач подойдет такой подход, спасибо за уделённое время!
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под iOS, Разработка мобильных приложений, Финансы в IT, IT-компании] В App Store доллар подорожал до 100 рублей
- [Разработка под iOS, Разработка мобильных приложений, Смартфоны] «Красную Кнопку» пустили в App Store
- [Разработка мобильных приложений, Flutter] Flutter: результаты опроса разработчиков за Q3 2020 (перевод)
- [Платежные системы, Разработка мобильных приложений] Опыт Тинькофф Оплаты: улучшили мобильный SDK и сделали оплату в интернете еще удобнее
- [Разработка под iOS, Swift] Single source of truth (SSOT) on MVVM with RxSwift & CoreData
- [Разработка под iOS, Swift, Смартфоны] Назад к BLE или способ автоматизировать рутинные операции
- [Разработка игр, Развитие стартапа, Разработка мобильных приложений, Продвижение игр] Финляндия для разработчиков игр: маленькая страна с большими возможностями
- [Программирование, Разработка мобильных приложений, Разработка под Android] Большие картинки? Deal with it
- [Разработка мобильных приложений, Дизайн мобильных приложений, Разработка под Android] How to Develop Dating Mobile App like Tinder?
- [Разработка мобильных приложений, Управление разработкой, Управление проектами, Agile, Управление продуктом] Сколько стоит разработать мобильное приложение
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_swift, #_swift, #_swift_development, #_ios_development, #_navigation, #_ios_razrabotka (ios разработка), #_enums, #_associated_enums, #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_swift
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:45
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В этом посте мне бы хотелось затронуть извечный вопрос об организации навигации и передачи данных между экранами в IOS приложениях. В первую очередь, я хотел бы донести концепт своего подхода, а не убедить вас использовать его как волшебную таблетку. Тут не будут рассматриваться различные архитектурные подходы или возможность использования UlStoryboard с segues, в целом я опишу еще один возможный способ достигнуть желаемого со своими плюсами и минусами. И так, начнем! Предыстория: Безусловно, на реализацию навигации и организацию транспорта данных в проекте влияет выбор архитектурного подхода, однако и сам подход складывается из ряда обстоятельств: состав команды, time to market, состояние ТЗ, масштабируемость проекта и многое др., определяющими факторами для меня стали:
Ближе к делу Стоит отметить, что конечное решение было сформировано не за один день, не лишено своих минусов и подходит скорее для маленьких и средних проектов. Для наглядности, тестовый проект можно посмотреть тут: github.com/ArturRuZ/NavigationDemo 1. Чтобы была возможность быстро получить представление о существующих экранах было принято решение завести enum с однозначным названием ControllersList. enum ControllersList {
case textInputScreen case textConfirmationScreen } 2. По ряду причин в проекте не хотелось использовать сторонние решения для DI, a DI получить хотелось, в том числе с возможностью быстрого просмотра зависимостей в проекте, поэтому было решено использовать Assembly для каждого отдельного экрана (закрытого протоколом Assembly) и RootAssembly – в качестве общего scope. protocol Assembly {
func build() -> UIViewController } final class TextInputAssembly: Assembly { func build() -> UIViewController { let viewModel = TextInputViewModel() return TextInputViewController(viewModel: viewModel) } } final class TextConfirmationAssembly: Assembly { private let text: String init(text: String) { self.text = text } func build() -> UIViewController { let viewModel = TextConfirmationViewModel(text: text) return TextConfirmationViewController(viewModel: viewModel) } } 3. Для передачи данных между экранами(там, где это действительно необходимо) ControllersList превратился в enum с Associated Values: enum ControllersList {
case textInputScreen case textConfirmationScreen(text: String) } 4. Для того чтобы ни бизнес-логика не влияла на навигацию, ни навигация на бизнес-логику, а также для быстрого переиспользования экранов, потребовалось навигацию вынести в отдельный слой. Так появился Coordinator и протокол Coordination: protocol Coordination {
func show(view: ControllersList, firstPosition: Bool) func popFromCurrentController() } final class Coordinator { private var navigationController = UINavigationController() private var factory: ControllerBuilder? private func navigateWithFirstPositionInStack(to: UIViewController) { navigationController.viewControllers = [to] } private func navigate(to: UIViewController) { navigationController.pushViewController(to, animated: true) } } extension Coordinator: Coordination { func popFromCurrentController() { navigationController.popViewController(animated: true) } func show(view: ControllersList, firstPosition: Bool) { guard let controller = factory?.buildController(for: view) else { return } firstPosition ? navigateWithFirstPositionInStack(to: controller) : navigate(to: controller) } } Тут важно отметить, что протокол может описывать больше методов, в т.ч. как и Coordinator может реализовывать различные протоколы, в зависимости от нужд. 5. При всем при этом, хотелось еще и ограничить набор действий, которые требовалось совершить разработчику, добавляя новый экран в приложение. На текущий момент требовалось помнить о том, что где-то надо прописать зависимости, и возможно сделать еще какие-либо действия для того чтобы навигация заработала. 6. Совсем не хотелось создавать дополнительные роутеры и координаторы. Более того, создание дополнительной логики для навигации могло значительно усложнить как восприятие навигации, так и переиспользование экранов. Все это привело к цепочке изменений, которые в конечном итоге выглядели следующим образом: //MARK - Dependences with controllers associations
fileprivate extension ControllersList { typealias scope = AssemblyServices var assembly: Assembly { switch self { case .textInputScreen: return TextInputAssembly(coordinator: scope.coordinator) case .textConfirmationScreen(let text): return TextConfirmationAssembly(coordinator: scope.coordinator, text: text) } } } //MARK - Services all time in memory fileprivate enum AssemblyServices { static let coordinator: СoordinationDependencesRegstration = Coordinator() static let controllerFactory: ControllerBuilderDependencesRegistration = ControllerFacotry() } //MARL: - RootAssembly Implementation final class RootAssembly { fileprivate typealias scope = AssemblyServices private func registerPropertyDependences() { // this place for propery dependences } } // MARK: - AssemblyDataSource implementation extension RootAssembly: AssemblyDataSource { func getAssembly(key: ControllersList) -> Assembly? { return key.assembly } } Теперь при создании нового экрана, разработчику достаточно было просто внести изменения в ControllersList, а далее компилятор уже сам показывал, где надо еще внести изменения. Добавление новых экранов в ControllersList никак не влияли на текущую схему навигации, а логика управления зависимостями легко прослеживалась. Также, используя ControllersList, можно легко найти все точки вхождения в тот или иной экран, а переиспользовать экраны стало просто. Заключение Данный пример является упрощенной реализацией идеи и не покрывает всех кейсов использования, тем не менее сам подход показал себя достаточно гибким и адаптивным. Из недостатков данного подхода можно выделить следующее:
Большая часть постов о навигации и передачи данных в IOS приложениях затрагивает либо использования координаторов и роутеров (на каждый или группу экранов), либо навигацию через segue, singleton и т.п., но не один из этих вариантов не подходил мне по тем или иным причинам. Возможно и вам для решения задач подойдет такой подход, спасибо за уделённое время! =========== Источник: habr.com =========== Похожие новости:
Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_swift |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:45
Часовой пояс: UTC + 5