⬆️ ⬇️

Router and Data Passing Architecture Clean Swift

Hello, reader!



In the previous article, I talked about the VIP cycle of the Clean Swift architecture. Now we will touch on one of the most important topics - the transfer and transfer of data between scenes.







Theory



For the logic of navigation and data transfer is the component Router , which is part of the scene (optional of course). It is initialized in ViewController , along with Interactor and Presenter .

')

Router implements two protocols - RoutingLogic and DataPassing , which we will fill with our functionality. RoutingLogic should contain methods that are responsible for the transition to a specific scene. DataPassing contains a dataStore variable that refers to the DataStore protocol. Scene Interactor implements the DataStore protocol and works with variables stored in it. The Router itself contains a link to its scene's ViewController .



Using a link to ViewController , Router transitions between scenes. To do this, you can use Segue or create a scene to which you need to make the transition, programmatically. What method is used is not important, the main thing for us is to have a link to an instance of the ViewController class, to which we make the transition.



Using a link to the DataStore we will transfer data from the Interactor of one scene to the Interactor of the scene we are switching to. And, as mentioned earlier, Router should know how to do this.



Practice



For an example, we will transfer the text from TextField to Label of another scene. Consider two ways to switch between scenes - by Segue and programmatically.



The Router class contains 3 semantic groups of methods:



  1. Methods from the implementation of RoutingLogic (routeTo)
  2. Methods responsible for navigation (navigateTo, transition without segue)
  3. Methods for data transfer (passDataTo, if there is data to transfer)






If we make a transition using Segue , for example, when we press a button, then in ViewController we must override the prepare (for: sender :) method. This extension will allow you to automatically call methods from Router by the name of Segue .



The override prepare (for: sender :) is not required when working with Segue. You can exclude it from the code and call performSegue (withIdentifier: sender :) in the Router method. Prepare is only needed if you need to use Segue along with data transfer.



final class HomeViewController: UIViewController {
// ...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Если у Segue есть идентификатор
if let scene = segue.identifier {
// Создаем селектор, для вызова метода с этим именем и параметрами в Router
// router?.routeToNAME(segue:)
let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
// Если есть метод с таким селектором,
// вызываем его и передаем в него Segue
if let router = router, router.responds(to: selector) {
router.perform(selector, with: segue)
}
}
}
// ...
}
final class HomeViewController: UIViewController {
// ...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Segue
if let scene = segue.identifier {
// , Router
// router?.routeToNAME(segue:)
let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
// ,
// Segue
if let router = router, router.responds(to: selector) {
router.perform(selector, with: segue)
}
}
}
// ...
}




Now we finally come to the most interesting - to the code Router'a . The example contains comments, so we consider only the key points.



In this Router, we work with two scenes - Home and Detail . The transition from the Home scene is processed in two ways - by Segue and programmatically . Data is transferred from the Home scene to the Detail scene.



All methods in the RoutingLogic protocol should be named on the principle routeToNAME , where NAME is the name of the Segue (Identifier) ​​that we specify when working with Storyboard . This is necessary not only for convenience and beauty of use, but also for our selector of methods in prepare (for: sender :) ViewController'a , which we redefined earlier.



Also in the HomeRouter class there are methods starting with navigateTo and passDataTo . The first are responsible for the logic of the transition, and the second for the transfer of data. The navigateTo methods are created only if the transition is carried out programmatically.



In the example, we have the routeToDetail (segue :) method. The segue parameter is optional, since The method contains an implementation that allows you to call it without using Segue . In both transitions, we get the non-optional values ​​of our HomeViewController and HomeDataStore of the Home scene, as well as links to the ViewController and DataStore of the Detail scene. Here it is worth paying attention to the fact that detailDS is a variable and is passed to the passDataToDetail method using the pass -through parameter (inout). This is important because without inout, we will have to mark all the DataStore protocols as “possible to implement only by classes” (protocol DetailDataStore: class), and this implies a lot of difficulties, including the capture of strong links.



import UIKit
/// Помечен как @objc для создания селектора
/// в методе prepare во View Controller'e
@objc protocol HomeRoutingLogic {
/// Переход на Detail View Controller
func routeToDetail(segue: UIStoryboardSegue?)
}
protocol HomeDataPassing {
var dataStore: HomeDataStore? { get }
}
final class HomeRouter: NSObject, HomeRoutingLogic, HomeDataPassing {
// MARK: - Private
// MARK: - Public
weak var viewController: HomeViewController?
var dataStore: HomeDataStore?
// MARK: - HomeRoutingLogic
func routeToDetail(segue: UIStoryboardSegue?) {
if let segue = segue {
// Данный участок кода срабатывает
// только при переходе по Segue
// Получаем ссылку на Detail View Controller
// И на его Data Store в Router'e
guard
let homeDS = dataStore,
let detailVC = segue.destination as? DetailViewController,
var detailDS = detailVC.router?.dataStore
else { fatalError("Fail route to detail") }
// Далее, полученные данные, мы пробрасываем в метод,
// который "знает" как передавать данные
passDataToDetail(source: homeDS, destination: &detailDS)
} else {
// Данный участок кода срабатывает,
// когда вызывается переход без Segue
// Иницилизируем Detail View Controller из Storyboard'a
// И получаем ссылку на его Data Store в Router'e
guard
let viewController = viewController,
let homeDS = dataStore,
let storyboard = viewController.storyboard,
let detailVC = storyboard.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
var detailDS = detailVC.router?.dataStore
else { fatalError("Fail route to detail") }
passDataToDetail(source: homeDS, destination: &detailDS)
// Метод, который отвечает за переход и "знает" как это делать
navigateToDetail(source: viewController, destination: detailVC)
}
}
// MARK: - Navigation
private func navigateToDetail(source: HomeViewController, destination: DetailViewController) {
source.navigationController?.pushViewController(destination, animated: true)
}
// MARK: - Passing data
/// Параметр destination помечен как inout,
/// что бы избежать мутации Data Store и захвата ссылки при передаче данных
private func passDataToDetail(source: HomeDataStore, destination: inout DetailDataStore) {
// Передаем текст сообщения из HomeDataStore в DetailDataStore
destination.message = source.message
}
}
import UIKit
/// @objc
/// prepare View Controller'e
@objc protocol HomeRoutingLogic {
/// Detail View Controller
func routeToDetail(segue: UIStoryboardSegue?)
}
protocol HomeDataPassing {
var dataStore: HomeDataStore? { get }
}
final class HomeRouter: NSObject, HomeRoutingLogic, HomeDataPassing {
// MARK: - Private
// MARK: - Public
weak var viewController: HomeViewController?
var dataStore: HomeDataStore?
// MARK: - HomeRoutingLogic
func routeToDetail(segue: UIStoryboardSegue?) {
if let segue = segue {
//
// Segue
// Detail View Controller
// Data Store Router'e
guard
let homeDS = dataStore,
let detailVC = segue.destination as? DetailViewController,
var detailDS = detailVC.router?.dataStore
else { fatalError("Fail route to detail") }
// , , ,
// ""
passDataToDetail(source: homeDS, destination: &detailDS)
} else {
// ,
// Segue
// Detail View Controller Storyboard'a
// Data Store Router'e
guard
let viewController = viewController,
let homeDS = dataStore,
let storyboard = viewController.storyboard,
let detailVC = storyboard.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
var detailDS = detailVC.router?.dataStore
else { fatalError("Fail route to detail") }
passDataToDetail(source: homeDS, destination: &detailDS)
// , ""
navigateToDetail(source: viewController, destination: detailVC)
}
}
// MARK: - Navigation
private func navigateToDetail(source: HomeViewController, destination: DetailViewController) {
source.navigationController?.pushViewController(destination, animated: true)
}
// MARK: - Passing data
/// destination inout,
/// Data Store
private func passDataToDetail(source: HomeDataStore, destination: inout DetailDataStore) {
// HomeDataStore DetailDataStore
destination.message = source.message
}
}




That's all. Thank you for reading to the end! Below I will leave a link to the project if you want to try out the article in the case.



Article series



  1. Clean Swift architecture overview
  2. Router and Data Passing in Clean Swift architecture (you are here)
  3. Workers in Clean Swift architecture
  4. Testing the application on the Clean Swift architecture
  5. An example of a simple online store on Clean Swift architecture


Link to the project

Help with writing an article: Bastien

Source: https://habr.com/ru/post/454032/



All Articles