Verwenden von Enum + Associated Values ​​beim Navigieren und Übertragen von Daten zwischen Bildschirmen in IOS-Anwendungen

In diesem Beitrag möchte ich auf die uralte Frage der Organisation der Navigation und Datenübertragung zwischen Bildschirmen in IOS-Anwendungen eingehen. Zunächst möchte ich das Konzept meines Ansatzes vermitteln und Sie nicht davon überzeugen, es als magische Pille zu verwenden. Hier werden wir nicht auf verschiedene architektonische Ansätze oder die Möglichkeit der Verwendung von UlStoryboard mit Segues eingehen. Im Allgemeinen werde ich einen anderen möglichen Weg beschreiben, um mit seinen Vor- und Nachteilen das zu erreichen, was Sie wollen. Also fangen wir an!



Hintergrund:



Natürlich beeinflusst die Wahl eines Architekturansatzes die Implementierung der Navigation und die Organisation des Datentransports in einem Projekt. Der Ansatz selbst setzt sich jedoch aus einer Reihe von Umständen zusammen: der Zusammensetzung des Teams, der Markteinführungszeit, dem Stand der technischen Spezifikation, der Skalierbarkeit des Projekts usw.



  • obligatorische Verwendung von MVVM;
  • die Möglichkeit, dem Navigationsprozess schnell neue Bildschirme (Controller und ihre Ansichtsmodelle) hinzuzufügen;
  • Änderungen in der Geschäftslogik sollten sich nicht auf die Navigation auswirken.
  • Änderungen in der Navigation sollten keine Auswirkungen auf die Geschäftslogik haben.
  • die Fähigkeit, Bildschirme schnell wiederzuverwenden, ohne Korrekturen an der Navigation vorzunehmen;
  • die Fähigkeit, sich schnell ein Bild von vorhandenen Bildschirmen zu machen;
  • die Fähigkeit, sich schnell ein Bild von den Abhängigkeiten im Projekt zu machen;
  • Erhöhen Sie nicht den Schwellenwert für Entwickler, um an dem Projekt teilzunehmen.




Komm zum Punkt



Es ist zu beachten, dass die endgültige Lösung nicht an einem Tag gebildet wurde, nicht ohne Nachteile ist und sich besser für kleine und mittlere Projekte eignet. Aus Gründen der Übersichtlichkeit kann das Testprojekt hier eingesehen werden: github.com/ArturRuZ/NavigationDemo



1. Um sich schnell ein Bild von den vorhandenen Bildschirmen zu machen, wurde beschlossen, eine Aufzählung mit dem eindeutigen Namen ControllersList zu erstellen.



enum ControllersList {
   case textInputScreen
   case textConfirmationScreen
}


2. Aus einer Reihe von Gründen wollte das Projekt keine Lösungen von Drittanbietern für DI verwenden, und ich wollte DI erhalten, einschließlich der Möglichkeit, die Abhängigkeiten im Projekt schnell anzuzeigen. Daher wurde beschlossen, Assembly für jeden separaten Bildschirm (durch das Assembly-Protokoll geschlossen) und RootAssembly als zu verwenden allgemeiner Geltungsbereich.




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. So übertragen Sie Daten zwischen Bildschirmen (wo dies wirklich erforderlich ist) ControllersList wurde zu einer Aufzählung mit zugehörigen Werten:



enum ControllersList {
   case textInputScreen
   case textConfirmationScreen(text: String)
}


4. Damit die Geschäftslogik weder die Navigation noch die Navigation in der Geschäftslogik beeinflusst und Bildschirme schnell wiederverwendet werden kann, musste die Navigation auf eine separate Ebene verschoben werden. So erschienen der Koordinator und das Koordinierungsprotokoll:




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)
   }
}



Es ist wichtig anzumerken, dass das Protokoll mehr Methoden beschreiben kann, inkl. Wie der Koordinator kann er je nach Bedarf unterschiedliche Protokolle implementieren.



5. Mit all dem wollte ich auch die Anzahl der Aktionen einschränken, die der Entwickler ausführen musste, indem er der Anwendung einen neuen Bildschirm hinzufügte. Im Moment musste man sich daran erinnern, dass Abhängigkeiten irgendwo geschrieben werden müssen und es möglich ist, einige andere Aktionen auszuführen, damit die Navigation funktioniert.



6. Ich wollte überhaupt keine zusätzlichen Router und Koordinatoren erstellen. Darüber hinaus könnte die Schaffung zusätzlicher Logik für die Navigation sowohl die Wahrnehmung der Navigation als auch die Wiederverwendung von Bildschirmen erheblich erschweren. All dies führte zu einer Reihe von Veränderungen, die letztendlich so aussahen:




//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
   }
}


Beim Erstellen eines neuen Bildschirms musste der Entwickler lediglich Änderungen an der ControllersList vornehmen, und dann zeigte der Compiler selbst, wo die Änderungen vorgenommen werden sollten. Das Hinzufügen neuer Bildschirme zur ControllersList hatte keinerlei Auswirkungen auf das aktuelle Navigationsschema, und die Abhängigkeitsverwaltungslogik war leicht zu befolgen. Mit ControllersList können Sie außerdem problemlos alle Einstiegspunkte in einen bestimmten Bildschirm finden und Bildschirme einfach wiederverwenden.



Fazit



Dieses Beispiel ist eine vereinfachte Implementierung der Idee und deckt nicht alle Anwendungsfälle ab. Dennoch hat sich der Ansatz selbst als recht flexibel und anpassungsfähig erwiesen.



Die Nachteile dieses Ansatzes sind die folgenden:



  • , , . ControllersList NavigationEvents, , ;
  • , ;
  • , , . , .


Die meisten Beiträge zur Navigation und Datenübertragung in IOS-Anwendungen betreffen entweder die Verwendung von Koordinatoren und Routern (für jeden oder eine Gruppe von Bildschirmen) oder die Navigation durch Segue, Singleton usw., aber keine dieser Optionen passte für die eine oder andere Gründe dafür.



Vielleicht eignet sich dieser Ansatz für die Lösung von Problemen, danke für Ihre Zeit!



All Articles