Разделяемый координатор в SwiftUI

от автора

Splittable Coordinator

Splittable Coordinator

Использование координатора совместно с NavigationStack является общепризнанной практикой на протяжении последних двух лет — быстро, удобно, надежно. Однако, в том случае если выбор конечных точек пути описывается перечислением, то по мере роста размеров проекта, начинает разрастаться и класс координатора. Пока количество конечных экранов приложения находится в пределах пяти десятков – это не является проблемой, поскольку Pascal / Camel / Snake нотация легко секционирует группы экранов. Но на долгих проектах количество экранов переваливает за 2-3 сотни, и, в этом случае, перечисления на несколько сот строк становятся катастрофой. Особенно, тогда, когда над проектом работает команда разработчиков.

Рано или поздно приходит осознание необходимости существенно изменить работу координатора. К наиболее популярным способам относится:

  1. координация на основе пути: путь передается текстовой строкой, и по ней строится навигация вложенных экранов.

  2. создание букета соподчиненных координаторов.

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

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

Второй – еще и на «удобство». Создание соподчиненного координатора не только вносит дополнительные расходы на вызов конечного экрана, но и вызывает сложности при автоматической навигации «назад».

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

Углубляясь в историю

Некоторые разработчики эпохи до Swift 3.0 были знакомы со следующим трюком: в базовом и производном классе создавались перечисления c одним и тем же названием. После этого, такое перечисление содержало все элементы обоих перечислений. Фактически, это выглядело так, как будто некоторые элементы были унаследованы (или перечисление было раздроблено на партиции). Увы, Swift не поддерживает парциальных классов, а трюк с не изолированными элементами признали опасным, и устранили в третьей версии языка.

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

Так если ранее, мы имели перечисление вида:

enum Screen {   case authScreen1, authScreen2, profileScreen1,  profileScreen2 }

и, соответственно делали вызовы вида:

Coodinator.next(.authScreen2) Coodinator.next(.profileScreen2)

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

enum Screen {   case auth(_ val: AuthScreen)   case profile(_ val: ProfileScreen) }  enum AuthScreen {   case screen1, screen2, screen3 }  enum ProfileScreen {   case screen1, screen2, screen3 }  Coodinator.next(.auth(.screen3)) Coodinator.next(.profile(.screen3))

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

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

  1. По Back повторяя путь навигации в обратном порядке

  2. По Root – сразу перемещаясь на коневой экран.

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

Экраны в приложении разделены на секции A и B. В каждой секции присутствуют экраны a, b, c.

Каждая из секций может создавать свой набор вью (не смотря на то, что элементы называются одинаково).

demo

demo

Реализация

Код класса CoordinatorService состоит из перечисления с секциями, и методов применяемых для управлением путем:

import SwiftUI  let Coordinator = CoordinatorService()  class CoordinatorService: ObservableObject {          enum Step: Hashable {         case a(_ val: CoordinatorA)         case b(_ val: CoordinatorB)                  var view: some View {             Group {                 switch self {                     case .a(let value): value.view                     case .b(let value): value.view                 }             }         }     }          @Published var path: [Step] = []          func next(_ step: Step) {         Task {             await MainActor.run {                 withAnimation {                     self.path += [step]                 }             }         }     }          func root() {         Task {             await MainActor.run {                 withAnimation {                     self.path = []                 }             }         }     } }

Координатор присутствует все время существования приложения, поэтому, нет смысла прилагать усилия для управления памятью в виде DI или Service Locator.

Создание необходимого вью, для указанного элемента перечисления осуществляется в отдельных перечислениях (в отдельных swift файлах):

import SwiftUI   enum CoordinatorA: String {     case a, b, c          var view: some View {         Group {             let title = "Coordinator A, screen: \(self.rawValue)."             switch self {                 case .a: DummyView(title, .red)                 case .b: DummyView(title, .blue)                 case .c: DummyView(title, .yellow)             }         }     } }

CoordinatorB имеет аналогичную структуру. Вместо DummyView можно использовать любое другое View Вашего проекта.

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

import SwiftUI  struct DummyView: View {     var title: String     var color: Color             init(_ title: String, _ color: Color) {         self.title = title         self.color = color     }          var body: some View {         VStack {             Text(self.title)                 .foregroundStyle(.black)                 .font(.headline)                          VStack(spacing: 24) {                 Image(systemName: "globe")                     .imageScale(.large)                     .foregroundStyle(.tint)                  Button("A a") { Coordinator.next(.a(.a)) }                 Button("A b") { Coordinator.next(.a(.b)) }                 Button("A c") { Coordinator.next(.a(.c)) }                 Button("B a") { Coordinator.next(.b(.a)) }                 Button("B b") { Coordinator.next(.b(.b)) }                 Button("B c") { Coordinator.next(.b(.c)) }                 Button("Root") { Coordinator.root() }              }             .padding()         }         .frame(maxWidth: .infinity, maxHeight: .infinity)         .overlay(             RoundedRectangle(cornerRadius: 0).fill(self.color).opacity(0.25)         )         .background(.white)         .ignoresSafeArea()              }   }  #Preview {     DummyView("Preview", .white) }

Ну и в заключении, контент управления координатора:

import SwiftUI  struct ContentView: View {     @ObservedObject private var model = Coordinator     var body: some View {         ZStack {             NavigationStack(path: $model.path) {                                                  DummyView("Select screen", .white)                  Group {}                     .navigationDestination(for: CoordinatorService.Step.self) { destination in                         destination.view                                                  }                                  }         }         .ignoresSafeArea()     } }  #Preview {     ContentView() }

Как видно, код получается проще и короче, чем тот, который был продемонстрирован пару лет назад в статье iOS: Навигация по-новому.

Полный пример можно загрузить с GitHub.

Подробности реализации можно обсудить на телеграмм канале.


ссылка на оригинал статьи https://habr.com/ru/articles/892758/