From d74aa7d2824459178503265115b881a1bfaf7a1f Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Wed, 8 Jan 2020 20:26:58 +0100 Subject: [PATCH 01/18] ios arch --- _posts/2020-01-09-ios-architectures.md | 1006 ++++++++++++++++++++++++ 1 file changed, 1006 insertions(+) create mode 100644 _posts/2020-01-09-ios-architectures.md diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md new file mode 100644 index 00000000..2768fcfc --- /dev/null +++ b/_posts/2020-01-09-ios-architectures.md @@ -0,0 +1,1006 @@ +--- +layout: post +title: iOS Architectures +date: 2020-01-08 16:28:05.000000000 +01:00 +type: post +published: true +status: publish +categories: [Programming] +image: +image2: +author: Valentino Urbano +--- + +*Note: The following code has been written for the new SceneDelegate in iOS13, if you're refactoring a legacy application the code in "scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)" needs to be moved to the AppDelegate's "application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool".* + +## MVP-R + +MVP is the simplest one of the following, its biggest issue is not providing routing capabilities so the Presenter on top of handling User Interaction and communicating with the netork service/persistance layer has to handle the navigation flow as well. With the addition of a Router we solve this problem. + +The ViewController acts as a view that gets esposed to the presenter as a protocol (as it happens in VIPER). The Presenter handles the business logic and dispatches the changes back to the view (controller) for display. The router handles the navigation. + +## MVVM-C + + Coordinator + | + V +ViewController <-> ViewModel -> DataManager/Repository/NetworkService + + + +Reactive programming on iOS is mostly done through RxSwift. During 2019 WWDC Apple introduced SwiftUI and Combine which with time will become a good alternative, but at the moment they're still not mature enough to be used espacially considering the iOS13+ requirement. + +On top of it we have a coordinator that handles the navigation for a certain feature. + +First we create the AppCoordinator. This will be the main coordinator for the application and will handle routing to the root screens of the app (for example the login screen if not authenticated and the logged in screen otherwise). + +``` +import UIKit + +final class AppCoordinator { + let window: UIWindow? + + lazy var rootViewController: UINavigationController = {//1 + return UINavigationController(rootViewController: UIViewController()) + }() + + var childCoordinator: UserCoordinator?//2 + + init(window: UIWindow?) { + self.window = window + } + + func start() { + guard let window = window else { + return + } + + window.rootViewController = rootViewController//3 + window.makeKeyAndVisible() + + addUserCoordinator()//4 + } + + func addUserCoordinator() { + childCoordinator = UserCoordinator(rootViewController: rootViewController) + childCoordinator?.start() + } + +} +``` + +1. We set a newly created navigationController as the rootViewController of the window. +2. We hold a strong reference to the coordinator currently in use as child. In this case we only have the UserCoordinator, but in case it could be multiple we can use a protocol or a superclass depending on the application. Most tutorials have a generic array of childCoordinators here, but this is a premature generalization. You may need it in your app or you may not. If you know that it will always be a UserCoordinator and nothing else there is no point doing that. If in the future it will change you can always go back and refactor it. The key to clean code is to always go back and take time to refactor things that need refactor due to changes. Do not optimize prematurely if it is not needed. +3. We set the navigationController as the root of our window. +4. We initialize the user coordinator by calling start(). + +Open SceneDelegate and add a strong reference to the AppCoordinator. + +``` +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + var appCoordinator: AppCoordinator! + +Inside "willConnectTo" initialize the AppCoordinator and start it: + + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + appCoordinator = AppCoordinator(window: window) + appCoordinator.start() + } +``` + +The UserCoordinator is responsible to instantiate the correct viewController and to inject the right dependencies into it. + +``` +import UIKit + +final class UserCoordinator { + init(rootViewController: UINavigationController) { + self.rootViewController = rootViewController + } + + let rootViewController: UINavigationController + + func start() { + //TODO: Add controller and inject dependencies + } +} +``` + +Before proceeding we need to create the underlying classes we will need for the controller. Our model is a simple struct, it may be a class if we need persistance like Realm or Core Data. + +``` +struct User { + let id: String + let name: String +} +``` + +We can now create the viewModel that will handle all the business logic and communicate back to the controller: + +``` +import UIKit + +class UserViewModel { + init(coordinatorDelegate: UserCoordinator? = nil, model: User? = nil, updateTextLabel: UserViewModel.UserLabelUpdating? = nil) { + self.coordinatorDelegate = coordinatorDelegate + self.model = model + self.updateTextLabel = updateTextLabel + } + + weak var coordinatorDelegate: UserCoordinator? + + var model: User? + var updateTextLabel: UserLabelUpdating? + + typealias UserLabelUpdating = (String?) -> Void + + //MARK: Data Loading + + func loadUser() { + //load data + let user = User(id: "w", name: "Mark") + self.model = user + updateTextLabel?(user.name) + } +} +``` + +Notice how we are using callbacks and not delegates to dispatch the result to the controller. Here you chould also use RxSwift to expose a stream of events. We are also keeping a weak reference to the coordinator since we need it if we want to change screen. + +We can now focus on the Controller. Since we don't care about the UI (the focus here is only on the architecture) I will create the controller from code without AutoLayout. This is obviously something you should never do in any kind of application. + +``` +import UIKit + +final class UserViewController: UIViewController { + + init(viewModel: UserViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("coder: init not supported") + } + + let viewModel: UserViewModel +} +``` + +This way we have easy dependency injection of the viewModel. We can now add the basic UI: + +``` +weak var label: UILabel? + weak var button: UIButton? + weak var nextButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Detail" + + view.backgroundColor = UIColor.white + + setupLabel() + setupCancelButton() + setupDetailButton() + setupNextButton() + + reloadAction() + } + + + @objc func reloadAction() { + } + + @objc func showDetail() { + } + + @objc func pushNext() { + } + + private func setupLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 250, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = UIColor.black + view.addSubview(label) + self.label = label + } + + private func setupCancelButton() { + let button = UIButton(frame: CGRect(x: 0, y: 350, width: 350, height: 55)) + button.setTitle("Reload", for: .normal) + button.setTitleColor(.black, for: .normal) + button.addTarget(self, action: #selector(reloadAction), for: .touchUpInside) + view.addSubview(button) + self.button = button + } + + private func setupDetailButton() { + let button = UIBarButtonItem(title: "Detail", style: .plain, target: self, action: #selector(showDetail)) + navigationItem.setRightBarButton(button, animated: false) + } + + private func setupNextButton() { + let button = UIButton(frame: CGRect(x: 0, y: 450, width: 350, height: 55)) + button.setTitle("Next", for: .normal) + button.setTitleColor(.red, for: .normal) + button.addTarget(self, action: #selector(pushNext), for: .touchUpInside) + view.addSubview(button) + self.nextButton = button + } +} +``` + +And bind it to the viewModel: + +``` + override func viewDidLoad() { + super.viewDidLoad() + + title = "Detail" + + view.backgroundColor = UIColor.white + + setupLabel() + setupCancelButton() + setupDetailButton() + setupNextButton() + + //bind + viewModel.updateTextLabel = { (username) in + self.label?.text = username + } + + reloadAction() + } + + + @objc func reloadAction() { + viewModel.loadUser() + } +``` + +Update the start method of the UserController to instantiate the UserViewController: + +``` + func start() { + let viewModel = UserViewModel() + viewModel.coordinatorDelegate = self + + let vc = UserViewController(viewModel: viewModel) + rootViewController.setViewControllers([vc], animated: false) + } +``` + +If you run the application the label gets populated with username. + +We can now add a way to navigate to different screens. To do that the viewModel needs to communicate to the coordinator about the user's intent. + +Update the viewModel with the actions to navigate to a different screen: + +``` + func showDetail() { + coordinatorDelegate?.goToDetail(withItem: self) + } + + func pushNext() { + coordinatorDelegate?.pushNext(withModel: self) + } + +Update the controller to add a callback to the viewModel for each action: + + @objc func showDetail() { + viewModel.showDetail() + } + + @objc func pushNext() { + viewModel.pushNext() + } +``` + +Finally we update the coordinator with the code to navigate to the final scren: + +``` +import UIKit + +final class UserCoordinator { + init(rootViewController: UINavigationController) { + self.rootViewController = rootViewController + } + + let rootViewController: UINavigationController + + func start() { + let viewModel = UserViewModel() + viewModel.coordinatorDelegate = self + + let vc = UserViewController(viewModel: viewModel) + rootViewController.setViewControllers([vc], animated: false) + } + + func pushNext(withModel model: UserViewModel) { + let vc = UserViewController(viewModel: model) + rootViewController.pushViewController(vc, animated: true) + } +} +``` + +Push next is straightforward since we are just pushing the same controller so it is a continuation of the same flow so we do not need a new coordinator for it, but the detail screen might itself start a flow so we need a different coordinator to handle it: + +``` +import UIKit + +final class UserCoordinator { + init(rootViewController: UINavigationController) { + self.rootViewController = rootViewController + } + + let rootViewController: UINavigationController + + var detailCoordinator: UserDetailCoordinator? + + func start() { + let viewModel = UserViewModel() + viewModel.coordinatorDelegate = self + + let vc = UserViewController(viewModel: viewModel) + rootViewController.setViewControllers([vc], animated: false) + } + + func pushNext(withModel model: UserViewModel) { + let vc = UserViewController(viewModel: model) + rootViewController.pushViewController(vc, animated: true) + } + + //MARK: Detail + + func goToDetail(withItem item: UserViewModel) { + detailCoordinator = UserDetailCoordinator(finishHandler: finish) + detailCoordinator?.start(on: rootViewController, withModel: item) + } + + func finish() { + detailCoordinator = nil + } +} +``` + +Same as in the AppCoordinator we add a strong reference to the child and we add a method to start it. We also need to have a callback when the pop the DetailCoordinator and are back to the UserCoordinator so that we zero the reference otherwise we will keep the strong reference in memory forever. We can also use this callback if we need to do some actions on pop, but this is not the case. + +``` +import UIKit + +final class UserDetailCoordinator { + init(finishHandler: @escaping UserDetailCoordinator.Finish) { + self.finishHandler = finishHandler + } + + weak var presentedController: UIViewController? + + typealias Finish = () -> Void + let finishHandler: Finish + + func start(on rootController: UIViewController, withModel model: UserViewModel) { + let viewModel = UserDetailViewModel(coordinatorDelegate: nil, model: model.model, updateTextLabel: nil) + viewModel.detailCoordinatorDelegate = self + + let vc = UserDetailViewController(viewModel: viewModel) + vc.isModalInPresentation = true //Required for iOS13 + rootController.present(vc, animated: true, completion: nil) + + presentedController = vc + } + + func dismiss() { + presentedController?.dismiss(animated: true, completion: nil) + + presentedController = nil + + finishHandler() + } +} +``` + +This is pretty much the same as in the UserCoordinator. We instantiated our dependencies, we inject them in the controller and we present the controller. +On dismissal we dismiss the controller and call the handler to clean up. + +In the same way the UserDetailController and UserDetailViewModel are similar. + +``` +import UIKit + +final class UserDetailViewController: UIViewController { + + init(viewModel: UserDetailViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("coder: init not supported") + } + + var viewModel: UserDetailViewModel + + weak var label: UILabel? + weak var button: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Detail" + + view.backgroundColor = UIColor.white + setupLabel() + setupCancelButton() + + //bind + viewModel.updateTextLabel = { (username) in + self.label?.text = "Detail - \(username)" + } + + viewModel.loadUser() + } + + + @objc func dismissAction() { + viewModel.dismiss() + } + + private func setupLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 250, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = UIColor.black + view.addSubview(label) + self.label = label + } + + private func setupCancelButton() { + let button = UIButton(frame: CGRect(x: 0, y: 350, width: 350, height: 55)) + button.setTitle("Dismiss", for: .normal) + button.setTitleColor(.black, for: .normal) + button.addTarget(self, action: #selector(dismissAction), for: .touchUpInside) + view.addSubview(button) + self.button = button + } +} + + +final class UserDetailViewModel: UserViewModel { + + weak var detailCoordinatorDelegate: UserDetailCoordinator? + + func dismiss() { + detailCoordinatorDelegate?.dismiss() + } + +} +``` + +You can find the full project on [Github][1]. + +## VIPER + + Wireframe + | + V +View <-> Presenter -> Interactor -> DataManager/Repository/NetworkService + + +If your app is really complicated VIPER gives you a clear split or responsibilities. The offside is having to maintain a lot of files for each screen. + +Create the AppWireframe in the SceneDelegate and keep a strong reference to it so it does not get deallocated: + +``` +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + var wireframe = AppWireframe() +``` +Inside willConnectTo instantiate the wireframe and start it: + +``` +if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + self.window = window + wireframe.initialize(on: window) + } +``` + +The AppWireframe will start the BaseWireframe of the app for the current start and start it: + +``` +import UIKit + +final class AppWireframe { + var rootWireframe = CountWireframe() + + func initialize(on window: UIWindow) { + let nav = UINavigationController() + window.rootViewController = nav + window.makeKeyAndVisible() + rootWireframe.presentRootController(on: nav) + } + + func getNavigationController(from window: UIWindow) -> UINavigationController { + guard let navigationController = window.rootViewController as? UINavigationController else { + fatalError() + } + return navigationController + } +} +``` + +The CountWireframe will initialize all the dependecies and present the viewController (which we will call the view from now on). + +``` +import UIKit + +final class CountWireframe { + weak var appWireframe: AppWireframe? + + weak var navigationController: UINavigationController? + + func presentRootController(on nav: UINavigationController) { + self.navigationController = nav + + let interactor = CountInteractor() + let presenter = CountPresenter(wireframe: self, interactor: interactor) + let vc = CountViewController(eventHandler: presenter) + + nav.viewControllers = [vc] + } +} +``` + +We can now start from the right and move towards the left of the chart. Starting with the Model and DataManager/Repository: + +``` +struct CountItem { + let count: String +} + + + +final class CountDataManager { + private lazy var count: [CountItem] = { + loadData() + }() + + private func loadData() -> [CountItem] { + return ["First", "Second", "Third"].map{ CountItem(count: $0) } + } + + func findItems() -> [CountItem] { + return count + } + + func findItem(withIndex index: Int) -> CountItem? { + guard index < count.count else { + return nil + } + return count[index] + } +} +``` + +Not muc going on here. In the interactor we have the business logic and it acts as a middleman between the presenter and the DataManager: + +``` +final class CountInteractor { + let dataManager: CountDataManager = CountDataManager() + weak var output: CountPresenter? + + func findItem(withIndex index: Int) { + guard let item = getItem(withIndex: index) else { + assertionFailure() + return + } + output?.foundItem(item: item) + } + + private func getItem(withIndex index: Int) -> CountItemViewModel? { + guard let item = dataManager.findItem(withIndex: index) else { + return nil + } + let mapped = CountItemViewModel(item: item) + return mapped + } +} +``` + +We get the items, convert it to viewModels and pass them to the presenter. + +Next we create the presenter that handles user interactions and dispatches events to the interactor or to the wireframe: + +``` +final class CountPresenter { + init(wireframe: CountWireframe, interactor: CountInteractor) { + self.wireframe = wireframe + self.interactor = interactor + interactor.output = self + } + + weak var wireframe: CountWireframe? + weak var userInterface: CountViewInterface? + let interactor: CountInteractor + + var index = 0 + var currentItem: CountItemViewModel? + + //MARK: Input + + func loadData() { + interactor.findItem(withIndex: index) + } + + //MARK: Output + + func foundItem(item: CountItemViewModel) { + self.currentItem = item + + userInterface?.setTitle(title: item.title) + userInterface?.setMessage(message: item.message) + } +} +``` + +We can now create the protocol for the view: + +``` +protocol CountViewInterface: class { + func setTitle(title: String) + func setMessage(message: String) +} +``` + +Finally it is time for the view (viewController). We first set up the initializer: + +``` +final class CountViewController: UIViewController { + + let eventHandler: CountPresenter + + weak var titleLabel: UILabel? + weak var label: UILabel? + weak var nextButton: UIButton? + + init(eventHandler: CountPresenter) { + self.eventHandler = eventHandler + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("coder: init not supported") + } +} +``` + +Now we setup the views: + +``` + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + + setupTitleLabel() + setupLabel() + setupDetailButton() + setupNextButton() + + } + + @objc func showDetail() { + } + @objc func showNext() { + } + + + private func setupTitleLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 150, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = .red + view.addSubview(label) + self.titleLabel = label + } + private func setupLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 250, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = .red + view.addSubview(label) + self.label = label + } + private func setupDetailButton() { + let button = UIBarButtonItem(title: "Detail", style: .plain, target: self, action: #selector(showDetail)) + navigationItem.setRightBarButton(button, animated: false) + } + private func setupNextButton() { + let button = UIButton(frame: CGRect(x: 0, y: 350, width: 350, height: 55)) + button.setTitle("Next", for: .normal) + button.setTitleColor(.red, for: .normal) + button.addTarget(self, action: #selector(showNext), for: .touchUpInside) + view.addSubview(button) + self.nextButton = button + } +} +``` + +Next we link the actions to the presenter: + +``` + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + + setupTitleLabel() + setupLabel() + setupDetailButton() + setupNextButton() + + eventHandler.loadData() + } + + @objc func showDetail() { + //TODO + } + @objc func showNext() { + //TODO + } +``` + +Finally we implement the interface: + +``` +extension CountViewController: CountViewInterface { + func setTitle(title: String) { + titleLabel?.text = title + } + + func setMessage(message: String) { + label?.text = message + } +} +``` + +Now let's talk routing. We first need to dispatch the event from the view (viewController): + +``` + @objc func showDetail() { + eventHandler.showDetail() + } + @objc func showNext() { + eventHandler.showNext() + } +``` + +In the Presenter after foundItem we add: + +``` + func foundNextItem(item: CountItemViewModel) { + wireframe?.pushNext(withItem: item, index: index + 1) + } + + //MARK: Navigation + + func showNext() { + interactor.findNextItem(currentIndex: index) + } + + func showDetail() { + guard let currentItem = currentItem else { + assertionFailure() + return + } + wireframe?.presentDetail(withItem: currentItem) + } +``` + +In the interactor again after findItem we add: + +``` + func findNextItem(currentIndex: Int) { + guard let item = getItem(withIndex: currentIndex + 1) else { + //TODO: Show error last page reached + return + } + output?.foundNextItem(item: item) + } +``` + +Finally we can tackle the wireframe. Add a property to CountWireframe pointing at the detail wireframe: + +``` +final class CountWireframe { + var detailWireframe: DetailWireframe? +``` + +And add the methods to present the detail wireframe or push a new controller to the navigation stack. + +``` + func presentDetail(withItem item: CountItemViewModel) { + detailWireframe = DetailWireframe(finishHandler: finish) + guard let navigationController = navigationController else { + assertionFailure() + return + } + detailWireframe?.presentDetail(from: navigationController, withItem: item) + } + + func pushNext(withItem item: CountItemViewModel, index: Int) { + guard index > 0 else { + assertionFailure() + return + } + + let interactor = CountInteractor() + let presenter = CountPresenter(wireframe: self, interactor: interactor) + let vc = CountViewController(eventHandler: presenter) + presenter.userInterface = vc + + presenter.index = index + presenter.foundItem(item: item) + + navigationController?.pushViewController(vc, animated: true) + } + + + func finish() { + detailWireframe = nil + } +} +``` + +Note: In this example the next screen is the same screen as the current screen. In a real application the Wireframe only handles navigation for one view. In that case pushNext will push a different view so that needs to be a separate Wireframe as well. + +We can now create the detail screen starting again with the wireframe: + +``` +import UIKit + +final class DetailWireframe { + init(finishHandler: @escaping Finish) { + self.finishHandler = finishHandler + } + + weak var presentedController: UIViewController? + + typealias Finish = () -> Void + let finishHandler: Finish + + func presentDetail(from source: UIViewController, withItem item: CountItemViewModel) { + let presenter = DetailPresenter(wireframe: self, item: item) + let detail = DetailViewController(eventHandler: presenter) + presenter.userInterface = detail + + presenter.populateView() + + detail.isModalInPresentation = true + source.present(detail, animated: true) + presentedController = detail + } + + func dismissDetail() { + presentedController?.dismiss(animated: true) + + finishHandler() + } + +} +``` + +In this case we do not need an interactor since we already have the model object and the deatil screen is only showing it and not acting on it (editing or adding) so we only need to create the presenter and the view. + +``` +final class DetailPresenter { + init(wireframe: DetailWireframe, item: CountItemViewModel) { + self.wireframe = wireframe + self.item = item + } + + weak var wireframe: DetailWireframe? + weak var userInterface: DetailViewInterface? + + let item: CountItemViewModel + + func populateView() { + userInterface?.setMessage(message: item.message) + } + + func dismiss() { + wireframe?.dismissDetail() + } +} +``` + +Since the model does not change it can also be injected as a dependency. + +The protocol for the view is really simple: + +``` +protocol DetailViewInterface: class { + func setMessage(message: String) +} +``` + +And likewise the view itself: + +``` +import UIKit + +final class DetailViewController: UIViewController { + + let eventHandler: DetailPresenter + + weak var label: UILabel? + weak var button: UIButton? + + init(eventHandler: DetailPresenter) { + self.eventHandler = eventHandler + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("coder: init not supported") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Detail" + + view.backgroundColor = UIColor.blue + setupLabel() + setupCancelButton() + + eventHandler.loadData() + } + + + @objc func dismissAction() { + eventHandler.dismiss() + } + + private func setupLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 250, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = UIColor.white + view.addSubview(label) + self.label = label + } + + private func setupCancelButton() { + let button = UIButton(frame: CGRect(x: 0, y: 350, width: 350, height: 55)) + button.setTitle("Cancel", for: .normal) + button.setTitleColor(.white, for: .normal) + button.addTarget(self, action: #selector(dismissAction), for: .touchUpInside) + view.addSubview(button) + self.button = button + } +} +``` + +Finally we implement the protocol: + +``` +extension DetailViewController: DetailViewInterface { + func setMessage(message: String) { + label?.text = message + } +} +``` + +Note how most of the view's code in both cases is set up the various subviews and link the actions that the user can perform. That's the whole range of responsibilities of the view. The presenter will convert that user intent to an action to dispatch to either the interactor or the wireframe and those two will act. + +You can find the full project on [Github][1]. + +## What should I use? + +You should not just use of architecture and stick with it no matter what. What architecture to use depends heavily on what kind of application you're building and what it is trying to do. An application that shows a couple of screens from a JSON request is completely different from an application that handles a social networking application with user login, registration, front page, timeline, private messaging, ... + +Getting so stuck with one architecture and using it everywhere indiscriminately means fitting a round peg in a square hole. \ No newline at end of file From 2cd759d0dbeedd4fb9631b271c92fa8e53a436c0 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Fri, 10 Jan 2020 18:17:41 +0100 Subject: [PATCH 02/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 35 ++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 2768fcfc..102e83dd 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -11,15 +11,16 @@ image2: author: Valentino Urbano --- -*Note: The following code has been written for the new SceneDelegate in iOS13, if you're refactoring a legacy application the code in "scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)" needs to be moved to the AppDelegate's "application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool".* +*Note: The code in this article has been written for the new SceneDelegate in iOS13. If you're refactoring a legacy application all the code that we write inside "scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)" needs to be moved to the corresponding AppDelegate file inside "application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool".* -## MVP-R +Apple development has for long followed the Apple dictated guideline of using the MVC paradigm of Model/View/Controller. This has been the facto standard since the introduction of the AppStore and iOS SDK in 2008. -MVP is the simplest one of the following, its biggest issue is not providing routing capabilities so the Presenter on top of handling User Interaction and communicating with the netork service/persistance layer has to handle the navigation flow as well. With the addition of a Router we solve this problem. +As time progressed and apps grew in size and features scaling MVC up became problematic. For that reason many different solutions that promise to solve all the problems with MVC have risen up, and with them strong advocates praising one solution over the other. This is not going to be one of those articles where I rise one up and declare the rest to be useless. -The ViewController acts as a view that gets esposed to the presenter as a protocol (as it happens in VIPER). The Presenter handles the business logic and dispatches the changes back to the view (controller) for display. The router handles the navigation. +We are going to analyze the most popular choices we have for iOS Development in 2020. This is not going to be a list of all the possible architecture for an iOS application, but only of what I consider relevant today in the industry. ## MVVM-C +Model/ViewModel/Coordinator Coordinator | @@ -30,7 +31,9 @@ ViewController <-> ViewModel -> DataManager/Repository/NetworkService Reactive programming on iOS is mostly done through RxSwift. During 2019 WWDC Apple introduced SwiftUI and Combine which with time will become a good alternative, but at the moment they're still not mature enough to be used espacially considering the iOS13+ requirement. -On top of it we have a coordinator that handles the navigation for a certain feature. +Since we're focusing on the architecture here I'll be showing a simpler model using callbacks instead of bindings, but the process is the same. + +On top of everything we have a coordinator that handles the navigation flow for a certain feature. First we create the AppCoordinator. This will be the main coordinator for the application and will handle routing to the root screens of the app (for example the login screen if not authenticated and the logged in screen otherwise). @@ -148,7 +151,9 @@ class UserViewModel { } ``` -Notice how we are using callbacks and not delegates to dispatch the result to the controller. Here you chould also use RxSwift to expose a stream of events. We are also keeping a weak reference to the coordinator since we need it if we want to change screen. +Notice again how we are using callbacks and not delegates to dispatch the result to the controller. If you used standard MVVM you would have RxSwift to expose a stream of events instead of callbacks. + +We are also keeping a weak reference to the coordinator since we need it if we want to change screen. We can now focus on the Controller. Since we don't care about the UI (the focus here is only on the architecture) I will create the controller from code without AutoLayout. This is obviously something you should never do in any kind of application. @@ -999,8 +1004,22 @@ Note how most of the view's code in both cases is set up the various subviews an You can find the full project on [Github][1]. +## MVP-R + +MVP is the simplest one of the following, its biggest issue is not providing routing capabilities so the Presenter on top of handling User Interaction and communicating with the netork service/persistance layer has to handle the navigation flow as well. With the addition of a Router we solve this problem. + +The ViewController acts as a view that gets esposed to the presenter as a protocol (as it happens in VIPER). The Presenter handles the business logic and dispatches the changes back to the view (controller) for display. The router handles the navigation. + +//TODO: + +## MVC + +//TODO: + ## What should I use? -You should not just use of architecture and stick with it no matter what. What architecture to use depends heavily on what kind of application you're building and what it is trying to do. An application that shows a couple of screens from a JSON request is completely different from an application that handles a social networking application with user login, registration, front page, timeline, private messaging, ... +You should not just use of architecture and stick with it no matter what. What architecture to use depends heavily on what kind of application you're building and what it is trying to make. + +An application that shows a couple of screens from a JSON request is completely different from an application that handles a social networking application with user login, registration, front page, timeline, private messaging, ... -Getting so stuck with one architecture and using it everywhere indiscriminately means fitting a round peg in a square hole. \ No newline at end of file +Getting so stuck with one architecture and using it everywhere indiscriminately means fitting a round peg in a square hole. From e90d1362dcc635ccaa76fecf02797efdd8a0053b Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sun, 12 Jan 2020 21:16:51 +0100 Subject: [PATCH 03/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 102e83dd..7149de22 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -175,7 +175,7 @@ final class UserViewController: UIViewController { } ``` -This way we have easy dependency injection of the viewModel. We can now add the basic UI: +This way we have easy dependency injection of the viewModel. We initialize all the views from code, but obviously they can be created however you like. ``` weak var label: UILabel? @@ -1006,7 +1006,7 @@ You can find the full project on [Github][1]. ## MVP-R -MVP is the simplest one of the following, its biggest issue is not providing routing capabilities so the Presenter on top of handling User Interaction and communicating with the netork service/persistance layer has to handle the navigation flow as well. With the addition of a Router we solve this problem. +MVP is the simplest one of the alternatives (exclusing MVC), its biggest issue is not providing routing capabilities so the Presenter on top of handling User Interaction and communicating with the netork service/persistance layer has to handle the navigation flow as well. With the addition of a Router we solve this problem. The ViewController acts as a view that gets esposed to the presenter as a protocol (as it happens in VIPER). The Presenter handles the business logic and dispatches the changes back to the view (controller) for display. The router handles the navigation. From faae4b4ba9e46a412de7a6c80105d3b5501cd84b Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Mon, 13 Jan 2020 19:00:29 +0100 Subject: [PATCH 04/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 7149de22..fcdeb753 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1004,17 +1004,17 @@ Note how most of the view's code in both cases is set up the various subviews an You can find the full project on [Github][1]. -## MVP-R +## MVP-R [Coming Soon] MVP is the simplest one of the alternatives (exclusing MVC), its biggest issue is not providing routing capabilities so the Presenter on top of handling User Interaction and communicating with the netork service/persistance layer has to handle the navigation flow as well. With the addition of a Router we solve this problem. The ViewController acts as a view that gets esposed to the presenter as a protocol (as it happens in VIPER). The Presenter handles the business logic and dispatches the changes back to the view (controller) for display. The router handles the navigation. -//TODO: + -## MVC +## MVC [Coming Soon] -//TODO: + ## What should I use? From a347d07e34174e07ef02d1100b16abb2bcdb37fb Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Mon, 13 Jan 2020 19:01:28 +0100 Subject: [PATCH 05/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index fcdeb753..2f55f06b 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1006,11 +1006,15 @@ You can find the full project on [Github][1]. ## MVP-R [Coming Soon] + + TODO + + --> ## MVC [Coming Soon] From 8242c7793a4294af8c2cc45402b926acb2b9eaf1 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Tue, 21 Jan 2020 17:48:31 +0100 Subject: [PATCH 06/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 2f55f06b..49dcf39c 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -13,7 +13,7 @@ author: Valentino Urbano *Note: The code in this article has been written for the new SceneDelegate in iOS13. If you're refactoring a legacy application all the code that we write inside "scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)" needs to be moved to the corresponding AppDelegate file inside "application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool".* -Apple development has for long followed the Apple dictated guideline of using the MVC paradigm of Model/View/Controller. This has been the facto standard since the introduction of the AppStore and iOS SDK in 2008. +Apple development has, for a long time, followed the Apple dictated guideline of using the MVC paradigm of Model/View/Controller. This has been the de facto standard since the introduction of the AppStore and iOS SDK in 2008. As time progressed and apps grew in size and features scaling MVC up became problematic. For that reason many different solutions that promise to solve all the problems with MVC have risen up, and with them strong advocates praising one solution over the other. This is not going to be one of those articles where I rise one up and declare the rest to be useless. @@ -307,7 +307,7 @@ Update the controller to add a callback to the viewModel for each action: } ``` -Finally we update the coordinator with the code to navigate to the final scren: +Finally we update the coordinator with the code to navigate to the final screen: ``` import UIKit @@ -334,7 +334,7 @@ final class UserCoordinator { } ``` -Push next is straightforward since we are just pushing the same controller so it is a continuation of the same flow so we do not need a new coordinator for it, but the detail screen might itself start a flow so we need a different coordinator to handle it: +`Push next` is straightforward since we are just pushing the same controller. It is a continuation of the same flow so we do not need a new coordinator for it. On the other hand the detail screen might start a new navigation flow so we need a different coordinator to handle it: ``` import UIKit @@ -496,7 +496,7 @@ You can find the full project on [Github][1]. View <-> Presenter -> Interactor -> DataManager/Repository/NetworkService -If your app is really complicated VIPER gives you a clear split or responsibilities. The offside is having to maintain a lot of files for each screen. +If your app is really complicated VIPER gives you a clear separation of responsibilities. The offside is having to maintain a lot of files for each screen. Create the AppWireframe in the SceneDelegate and keep a strong reference to it so it does not get deallocated: @@ -593,7 +593,7 @@ final class CountDataManager { } ``` -Not muc going on here. In the interactor we have the business logic and it acts as a middleman between the presenter and the DataManager: +Not much going on here. In the interactor we have the business logic and it acts as a middleman between the presenter and the DataManager: ``` final class CountInteractor { @@ -1022,7 +1022,7 @@ The ViewController acts as a view that gets esposed to the presenter as a protoc ## What should I use? -You should not just use of architecture and stick with it no matter what. What architecture to use depends heavily on what kind of application you're building and what it is trying to make. +You should not just use one architecture and stick with it no matter what. What architecture to use depends heavily on what kind of application you're building and what it is trying to do. An application that shows a couple of screens from a JSON request is completely different from an application that handles a social networking application with user login, registration, front page, timeline, private messaging, ... From 538bda25efc9d7b231ee41d17bd402753858bd96 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sat, 15 Feb 2020 20:14:44 +0100 Subject: [PATCH 07/18] Adds mvc and mvp --- _posts/2020-01-09-ios-architectures.md | 511 ++++++++++++++++++++++++- 1 file changed, 502 insertions(+), 9 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 49dcf39c..487f9b6f 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1002,23 +1002,510 @@ extension DetailViewController: DetailViewInterface { Note how most of the view's code in both cases is set up the various subviews and link the actions that the user can perform. That's the whole range of responsibilities of the view. The presenter will convert that user intent to an action to dispatch to either the interactor or the wireframe and those two will act. -You can find the full project on [Github][1]. - -## MVP-R [Coming Soon] +You can find the full project on [Github][2]. - -## MVC [Coming Soon] - + (Coordinator / Router) + | + V +View <- Controller <- Presenter -> DataManager/Repository/NetworkService + +In the case of MVP (or even MVC for that matter) you can add coordinators just like in MVVM-C. For this article we won't go through it since it is already explained in the MVVM example. + +It is a minimal version of VIPER for smaller application which purpose is to separate all the business logic in the presenter and leave the controller as a simple manager for the view. It is common and advisable to further separate the controller from the presenter by having the whole communication happen only through the use of protocols. This will increase the code needed, but on the other hand makes the presenter easily reusable by different controllers that want to implement the same behaviour. + +Since we are not using a Coordinator we need to set up the presenter and the controller from the SceneDelegate. The better way would be to create both in the Coordinator and set up the dependencies there, just like we did for both VIPER and MVVM-C. + +''' +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + let vc = setupInitialController() + window.rootViewController = vc + } + } + + + private func setupInitialController() -> UIViewController { + let presenter = Presenter() + let vc = ViewController(presenter: presenter) + presenter.userInterface = vc + return vc + } +''' + +The Presenter is kept alive by the controller which has a strong reference to it. In the controller we only do the bare minimum we need to set up the views, populate them with data and pass actions back to the presenter, but we do not have any business logic. Everything else is handled in the presenter. + + +class ViewController: UIViewController { + + init(presenter: PresenterInput) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError() + } + + let presenter: PresenterInput + weak var titleLabel: UILabel? + weak var nextButton: UIButton? + + +In viewDidLoad we set up the controller's UI and we let the presenter know that we are now in the view hierarchy + + override func viewDidLoad() { + super.viewDidLoad() + setupTitleLabel() + setupNextButton() + + presenter.initialLoading() + } + + @objc func goToDetail() { + presenter.goToDetail() + } + + private func setupTitleLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 150, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = .red + view.addSubview(label) + self.titleLabel = label + } + private func setupNextButton() { + let button = UIButton(frame: CGRect(x: 0, y: 450, width: 350, height: 55)) + button.setTitle("Next", for: .normal) + button.setTitleColor(.red, for: .normal) + button.addTarget(self, action: #selector(goToDetail), for: .touchUpInside) + view.addSubview(button) + self.nextButton = button + } + +We can now take a look at the presenter. The first thing to do is to figure out what actions should the controller pass up to the presenter and what results does the presenter pass back to the controller. We are going to formalize them as protocols. + +protocol PresenterInput: class { + var userInterface: PresenterOutput? { get set } + func initialLoading() + func goToDetail() +} + +protocol PresenterOutput: class { + func setupLabel(text: String) + func showScreen(vc: UIViewController) +} + +Now onto the presenter itself. We set up the label once loaded and we set up the new controller once the "go to detail" button is pressed + +class Presenter: PresenterInput { + + weak var userInterface: PresenterOutput? + + func initialLoading() { + userInterface?.setupLabel(text: "Home") + } + + func goToDetail() { + + } + +} + +Here we can either use a router, instantiate a view controller or perform a segue. I tend to dislike segues in MVP and prefer to instantiate th controller directly if we do not have a routing layer, but both solutions are obviously not great. With the segue you need to find the storyboard to see what it means on top of having strings (but that can be solved by using Swiftgen or R). Instantiating a controller would mean importing UIKit which you should avoid doing in a Presenter. + + func goToDetail() { + //TODO: Should use Router/Coordinator instead + let presenter = DetailPresenter(dismissalAction: { [weak self] in + self?.userInterface?.setupLabel(text: "Dismissed") + }) + let detail = DetailViewController(presenter: presenter) + presenter.userInterface = detail + + userInterface?.showScreen(vc: detail) + } + +Finally back in the ViewController we implement the delegate + +extension ViewController: PresenterOutput { + func setupLabel(text: String) { + titleLabel?.text = text + } + + func showScreen(vc: UIViewController) { + present(vc, animated: true, completion: nil) + } + +} + +Time for the detail view. Here we are simply going to have a dismiss button. + +The controller is very similar to the previous controller + +import UIKit + +class DetailViewController: UIViewController { + + init(presenter: DetailPresenterInput) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError() + } + + let presenter: DetailPresenterInput + weak var backButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + setupBackButton() + } + + @objc func goBack() { + presenter.backAction() + } + + private func setupBackButton() { + let button = UIButton(frame: CGRect(x: 0, y: 450, width: 350, height: 55)) + button.setTitle("Dismiss", for: .normal) + button.setTitleColor(.blue, for: .normal) + button.addTarget(self, action: #selector(goBack), for: .touchUpInside) + view.addSubview(button) + self.backButton = button + } +} + +The same goes for the presenter, with the distinction of having a reference to the side effect to perform on dismissal + + +protocol DetailPresenterInput: class { + var userInterface: DetailPresenterOutput? { get set } + + func backAction() +} + +protocol DetailPresenterOutput: class { + func dismiss() +} + +class DetailPresenter: DetailPresenterInput { + + init(dismissalAction: @escaping Dismissal) { + self.dismissalAction = dismissalAction + } + + weak var userInterface: DetailPresenterOutput? + private let dismissalAction: Dismissal + + func backAction() { + dismissalAction() + userInterface?.dismiss() + } +} + +We finally implement the protocol in the controller + +extension DetailViewController: DetailPresenterOutput { + func dismiss() { + dismiss(animated: true, completion: nil) + } +} + +The last thing we miss is loading some data. We can load a label to show in the detail screen once the view loaded. In the controller we call the presenter asking it to load the data and in the presenter we pass the request to the repository that has the responsibility of loading the data (from the network, fielsystem,...). + +In DetailController: + + override func viewDidLoad() { + super.viewDidLoad() + setupBackButton() + + presenter.initialLoading() + } + +In DetailPresenter we first update the Input: + +protocol DetailPresenterInput: class { + var userInterface: DetailPresenterOutput? { get set } + + func backAction() + func initialLoading() +} + + +The Output: + +protocol DetailPresenterOutput: class { + func showDetail(withText: String) + func dismiss() +} + +And finally the Presenter by calling the repository, fetching the daqta and returning it to the controller: + + + private let repository = DetailRepository() + + func initialLoading() { + repository.loadData { [weak self] (data) in + self?.userInterface?.showDetail(withText: data.name) + } + } + +We can now create the Repository: + +final class DetailRepository { + var model: DetailModel? + + typealias Completion = (DetailModel) -> Void + + func loadData(completion: Completion) { + //TODO: Get model from network / db + let model = DetailModel(name: "Detail") + self.model = model + completion(model) + } +} + + +And the model: + +struct DetailModel { + let name: String +} + +Finally we update the controller by adding the label and setting it up: + + let presenter: DetailPresenterInput + weak var titleLabel: UILabel? + weak var backButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + setupTitleLabel() + setupBackButton() + + + .... + + + + private func setupTitleLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 150, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = .red + view.addSubview(label) + self.titleLabel = label + } + + +And finally populate it: + + +extension DetailViewController: DetailPresenterOutput { + func showDetail(withText text: String) { + titleLabel?.text = text + } + + func dismiss() { + dismiss(animated: true, completion: nil) + } +} + +You can find the source code on [Github][3]. + + +## MVC + + +View <- Controller <-> Model + +MVC is the standard in iOS development. Model for the data, view for the UI and the Controller to manage the screen. + +The AppDelegate is the same as the other examples, we simply setup the SceneDelegate: + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + +} + + +In the SceneDelegate we setup the initial controller: + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + if let windowScene = scene as? UIWindowScene { + let window = UIWindow(windowScene: windowScene) + window.rootViewController = ViewController() + } + } +} + +We set up the controller: + + +class ViewController: UIViewController { + weak var titleLabel: UILabel? + weak var nextButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + setupTitleLabel() + setupNextButton() + + } + + //MARK: Private UI Setup + + private func setupTitleLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 150, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = .red + view.addSubview(label) + self.titleLabel = label + } + private func setupNextButton() { + let button = UIButton(frame: CGRect(x: 0, y: 450, width: 350, height: 55)) + button.setTitle("Next", for: .normal) + button.setTitleColor(.red, for: .normal) + button.addTarget(self, action: #selector(goToDetail), for: .touchUpInside) + view.addSubview(button) + self.nextButton = button + } + +And we load whatever we need to load and update the ui: + + override func viewDidLoad() { + super.viewDidLoad() + setupTitleLabel() + setupNextButton() + + loadData() + } + + ... + + + //MARK: Actions + + private func loadData() { + titleLabel?.text = "Home" + } + + @objc func goToDetail() { + let vc = DetailViewController(repository: DetailRepository()) + present(vc, animated: true, completion: nil) + } +} + +Now onto the detail screen. Similarly to the MVP example we inject the dependency in the controller and we setup the screen: + +class DetailViewController: UIViewController { + init(repository: DetailRepository) { + self.repository = repository + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError() + } + + private let repository: DetailRepository + + weak var titleLabel: UILabel? + weak var backButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + setupTitleLabel() + setupBackButton() + + loadData() + } + + //MARK: Private UI Setup + + private func setupTitleLabel() { + let label = UILabel(frame: CGRect(x: 0, y: 150, width: 350, height: 55)) + label.textAlignment = .center + label.textColor = .red + view.addSubview(label) + self.titleLabel = label + } + private func setupBackButton() { + let button = UIButton(frame: CGRect(x: 0, y: 450, width: 350, height: 55)) + button.setTitle("Dismiss", for: .normal) + button.setTitleColor(.blue, for: .normal) + button.addTarget(self, action: #selector(goBack), for: .touchUpInside) + view.addSubview(button) + self.backButton = button + } + + +Calling loadData will now load the data from the Repository: + + + + //MARK: Actions + + private func loadData() { + self.repository.loadData { [weak self] (data) in + self?.titleLabel?.text = data.name + } + } + + @objc func goBack() { + dismiss(animated: true, completion: nil) + } + +} + +The repository and the model are the same as in the previous example: + +final class DetailRepository { + var model: DetailModel? + + typealias Completion = (DetailModel) -> Void + + func loadData(completion: Completion) { + //TODO: Get model from network / db + let model = DetailModel(name: "Detail") + self.model = model + completion(model) + } +} + +struct DetailModel { + let name: String +} + +You can find the source code on [Github][4]. ## What should I use? @@ -1027,3 +1514,9 @@ You should not just use one architecture and stick with it no matter what. What An application that shows a couple of screens from a JSON request is completely different from an application that handles a social networking application with user login, registration, front page, timeline, private messaging, ... Getting so stuck with one architecture and using it everywhere indiscriminately means fitting a round peg in a square hole. + + +[1]: https://github.com/valeIT/mvvm-ios +[2]: https://github.com/valeIT/viper-ios +[3]: https://github.com/valeIT/mvp-ios +[4]: https://github.com/valeIT/mvc-ios \ No newline at end of file From 639886d377a41e68d729caeaee0e329f7199dae2 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Mon, 17 Feb 2020 18:22:06 +0100 Subject: [PATCH 08/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 487f9b6f..d42e6ad5 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1021,9 +1021,9 @@ In the case of MVP (or even MVC for that matter) you can add coordinators just l It is a minimal version of VIPER for smaller application which purpose is to separate all the business logic in the presenter and leave the controller as a simple manager for the view. It is common and advisable to further separate the controller from the presenter by having the whole communication happen only through the use of protocols. This will increase the code needed, but on the other hand makes the presenter easily reusable by different controllers that want to implement the same behaviour. -Since we are not using a Coordinator we need to set up the presenter and the controller from the SceneDelegate. The better way would be to create both in the Coordinator and set up the dependencies there, just like we did for both VIPER and MVVM-C. +Since we are not using a Coordinator we need to set up the presenter and the controller from the SceneDelegate. The better way would be to create both in the Coordinator and set up the dependencies there, just like we did for both VIPER and MVVM-C, but for this basic example this is still better than doing it from the controller itself. The controller should have a reference to the interface of the presenter, but it should not have a strong link to the presenter iself. -''' +``` class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? @@ -1044,11 +1044,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { presenter.userInterface = vc return vc } -''' +``` The Presenter is kept alive by the controller which has a strong reference to it. In the controller we only do the bare minimum we need to set up the views, populate them with data and pass actions back to the presenter, but we do not have any business logic. Everything else is handled in the presenter. - +``` class ViewController: UIViewController { init(presenter: PresenterInput) { @@ -1063,10 +1063,11 @@ class ViewController: UIViewController { let presenter: PresenterInput weak var titleLabel: UILabel? weak var nextButton: UIButton? - +``` In viewDidLoad we set up the controller's UI and we let the presenter know that we are now in the view hierarchy +``` override func viewDidLoad() { super.viewDidLoad() setupTitleLabel() @@ -1094,6 +1095,7 @@ In viewDidLoad we set up the controller's UI and we let the presenter know that view.addSubview(button) self.nextButton = button } +``` We can now take a look at the presenter. The first thing to do is to figure out what actions should the controller pass up to the presenter and what results does the presenter pass back to the controller. We are going to formalize them as protocols. @@ -1519,4 +1521,4 @@ Getting so stuck with one architecture and using it everywhere indiscriminately [1]: https://github.com/valeIT/mvvm-ios [2]: https://github.com/valeIT/viper-ios [3]: https://github.com/valeIT/mvp-ios -[4]: https://github.com/valeIT/mvc-ios \ No newline at end of file +[4]: https://github.com/valeIT/mvc-ios From 96a182422e8cbde44641c0d36e0297d8503b838a Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Tue, 18 Feb 2020 20:06:35 +0100 Subject: [PATCH 09/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index d42e6ad5..a3379f5a 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1065,7 +1065,7 @@ class ViewController: UIViewController { weak var nextButton: UIButton? ``` -In viewDidLoad we set up the controller's UI and we let the presenter know that we are now in the view hierarchy +In viewDidLoad we set up the controller's UI and we let the presenter know that we are now in the view hierarchy so it can start setting up and loading the data. ``` override func viewDidLoad() { @@ -1099,6 +1099,7 @@ In viewDidLoad we set up the controller's UI and we let the presenter know that We can now take a look at the presenter. The first thing to do is to figure out what actions should the controller pass up to the presenter and what results does the presenter pass back to the controller. We are going to formalize them as protocols. +``` protocol PresenterInput: class { var userInterface: PresenterOutput? { get set } func initialLoading() @@ -1109,9 +1110,11 @@ protocol PresenterOutput: class { func setupLabel(text: String) func showScreen(vc: UIViewController) } +``` -Now onto the presenter itself. We set up the label once loaded and we set up the new controller once the "go to detail" button is pressed +Now onto the presenter itself. We set up the label once loaded and we set up the new controller once the "go to detail" button is pressed. In this case we are simply setting up a label, here you would connect to the database or webservice and load your model. +``` class Presenter: PresenterInput { weak var userInterface: PresenterOutput? @@ -1125,6 +1128,7 @@ class Presenter: PresenterInput { } } +``` Here we can either use a router, instantiate a view controller or perform a segue. I tend to dislike segues in MVP and prefer to instantiate th controller directly if we do not have a routing layer, but both solutions are obviously not great. With the segue you need to find the storyboard to see what it means on top of having strings (but that can be solved by using Swiftgen or R). Instantiating a controller would mean importing UIKit which you should avoid doing in a Presenter. From 0becc8ba714ad0d869f656ae26c80765af6f2a0c Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Wed, 19 Feb 2020 19:03:14 +0100 Subject: [PATCH 10/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index a3379f5a..11b3c12b 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1130,8 +1130,12 @@ class Presenter: PresenterInput { } ``` -Here we can either use a router, instantiate a view controller or perform a segue. I tend to dislike segues in MVP and prefer to instantiate th controller directly if we do not have a routing layer, but both solutions are obviously not great. With the segue you need to find the storyboard to see what it means on top of having strings (but that can be solved by using Swiftgen or R). Instantiating a controller would mean importing UIKit which you should avoid doing in a Presenter. +Here we can either use a router, instantiate a view controller or perform a segue. I tend to dislike segues in MVP and prefer to instantiate the controller directly if we do not want to have a routing layer, but both solutions are obviously not great. Having an actual routing component would be a much better choice of course, but for some simple apps it may be too much complexity. +With the segue you need to find the storyboard to see what it means on top of having strings (but that can be solved by using Swiftgen or R). Instantiating a controller would mean importing UIKit which you should avoid doing in a Presenter. + + +``` func goToDetail() { //TODO: Should use Router/Coordinator instead let presenter = DetailPresenter(dismissalAction: { [weak self] in @@ -1142,6 +1146,7 @@ Here we can either use a router, instantiate a view controller or perform a segu userInterface?.showScreen(vc: detail) } +``` Finally back in the ViewController we implement the delegate From b8c2ec43bd41aa75846720fe79ab69b3fe392df9 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Thu, 20 Feb 2020 18:31:18 +0100 Subject: [PATCH 11/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 61 +++++++++++++++++++------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 11b3c12b..f8c7d8d1 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1148,8 +1148,11 @@ With the segue you need to find the storyboard to see what it means on top of ha } ``` -Finally back in the ViewController we implement the delegate +You can take a look on how to implement a Router or Coordinator from either the MVVM or VIPER examples above. +Going back to the ViewController we can finish by implementing the delegate of the presenter in order to receive its callbacks. We have already set ourselves as delegate in viewDidLoad so there is no need to do that again. + +``` extension ViewController: PresenterOutput { func setupLabel(text: String) { titleLabel?.text = text @@ -1160,11 +1163,13 @@ extension ViewController: PresenterOutput { } } +``` -Time for the detail view. Here we are simply going to have a dismiss button. +It is now time to add something more to the application to see how we handle different screens. As in the other examples this will simply consist of a basic detail view where we are simply going to have a dismiss button. -The controller is very similar to the previous controller +The controller is very similar to the previous examples for MVVM and VIPER. Notice how we use Dipendency Injection to populate the presenter before delegating back to the default initializer so that we avoid having optionals. Since a Controller has no sense in MVP without a presenter there can never be a case where the former exists without the latter so optionality doesn't make sense. +``` import UIKit class DetailViewController: UIViewController { @@ -1199,10 +1204,11 @@ class DetailViewController: UIViewController { self.backButton = button } } +``` -The same goes for the presenter, with the distinction of having a reference to the side effect to perform on dismissal - +The same goes for the presenter which is very similar to the VIPER solution. It has the difference from the main view `Presenter` of having a reference to the side effect to perform on dismissal. Again we are passing it in the initializer. +``` protocol DetailPresenterInput: class { var userInterface: DetailPresenterOutput? { get set } @@ -1227,46 +1233,54 @@ class DetailPresenter: DetailPresenterInput { userInterface?.dismiss() } } +``` -We finally implement the protocol in the controller +We finally implement the protocol back in the controller to implement the dismissal action. +``` extension DetailViewController: DetailPresenterOutput { func dismiss() { dismiss(animated: true, completion: nil) } } +``` The last thing we miss is loading some data. We can load a label to show in the detail screen once the view loaded. In the controller we call the presenter asking it to load the data and in the presenter we pass the request to the repository that has the responsibility of loading the data (from the network, fielsystem,...). In DetailController: +``` override func viewDidLoad() { super.viewDidLoad() setupBackButton() presenter.initialLoading() } +``` In DetailPresenter we first update the Input: +``` protocol DetailPresenterInput: class { var userInterface: DetailPresenterOutput? { get set } func backAction() func initialLoading() } - +``` The Output: +``` protocol DetailPresenterOutput: class { func showDetail(withText: String) func dismiss() } +``` And finally the Presenter by calling the repository, fetching the daqta and returning it to the controller: - +``` private let repository = DetailRepository() func initialLoading() { @@ -1274,9 +1288,11 @@ And finally the Presenter by calling the repository, fetching the daqta and retu self?.userInterface?.showDetail(withText: data.name) } } +``` We can now create the Repository: +``` final class DetailRepository { var model: DetailModel? @@ -1289,16 +1305,19 @@ final class DetailRepository { completion(model) } } - +``` And the model: +``` struct DetailModel { let name: String } +``` Finally we update the controller by adding the label and setting it up: +``` let presenter: DetailPresenterInput weak var titleLabel: UILabel? weak var backButton: UIButton? @@ -1320,11 +1339,11 @@ Finally we update the controller by adding the label and setting it up: view.addSubview(label) self.titleLabel = label } - +``` And finally populate it: - +``` extension DetailViewController: DetailPresenterOutput { func showDetail(withText text: String) { titleLabel?.text = text @@ -1334,6 +1353,7 @@ extension DetailViewController: DetailPresenterOutput { dismiss(animated: true, completion: nil) } } +``` You can find the source code on [Github][3]. @@ -1347,6 +1367,7 @@ MVC is the standard in iOS development. Model for the data, view for the UI and The AppDelegate is the same as the other examples, we simply setup the SceneDelegate: +``` @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -1361,10 +1382,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - +``` In the SceneDelegate we setup the initial controller: +``` class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? @@ -1377,10 +1399,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } } +``` We set up the controller: - +``` class ViewController: UIViewController { weak var titleLabel: UILabel? weak var nextButton: UIButton? @@ -1409,9 +1432,11 @@ class ViewController: UIViewController { view.addSubview(button) self.nextButton = button } +``` And we load whatever we need to load and update the ui: +``` override func viewDidLoad() { super.viewDidLoad() setupTitleLabel() @@ -1434,9 +1459,11 @@ And we load whatever we need to load and update the ui: present(vc, animated: true, completion: nil) } } +``` Now onto the detail screen. Similarly to the MVP example we inject the dependency in the controller and we setup the screen: +``` class DetailViewController: UIViewController { init(repository: DetailRepository) { self.repository = repository @@ -1477,12 +1504,12 @@ class DetailViewController: UIViewController { view.addSubview(button) self.backButton = button } - +``` Calling loadData will now load the data from the Repository: - +``` //MARK: Actions private func loadData() { @@ -1496,9 +1523,10 @@ Calling loadData will now load the data from the Repository: } } - +``` The repository and the model are the same as in the previous example: +``` final class DetailRepository { var model: DetailModel? @@ -1515,6 +1543,7 @@ final class DetailRepository { struct DetailModel { let name: String } +``` You can find the source code on [Github][4]. From a3afab2857fe8a9f5299a0f9bbb848091d4fc9d8 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Fri, 21 Feb 2020 18:15:09 +0100 Subject: [PATCH 12/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index f8c7d8d1..898fef15 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1247,7 +1247,7 @@ extension DetailViewController: DetailPresenterOutput { The last thing we miss is loading some data. We can load a label to show in the detail screen once the view loaded. In the controller we call the presenter asking it to load the data and in the presenter we pass the request to the repository that has the responsibility of loading the data (from the network, fielsystem,...). -In DetailController: +In DetailController as usual we aske the presenter to load the data: ``` override func viewDidLoad() { From 79dc07800024c7af15fa1c3e074e92f2a3a1a6ef Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sat, 22 Feb 2020 20:28:30 +0100 Subject: [PATCH 13/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 898fef15..102502f1 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1247,7 +1247,7 @@ extension DetailViewController: DetailPresenterOutput { The last thing we miss is loading some data. We can load a label to show in the detail screen once the view loaded. In the controller we call the presenter asking it to load the data and in the presenter we pass the request to the repository that has the responsibility of loading the data (from the network, fielsystem,...). -In DetailController as usual we aske the presenter to load the data: +In DetailController as usual we ask the presenter to load the data: ``` override func viewDidLoad() { @@ -1258,7 +1258,9 @@ In DetailController as usual we aske the presenter to load the data: } ``` -In DetailPresenter we first update the Input: +We still are not able to build though. This is because we updated both the controller and the presenter, but forgot to update the protocols they use to communicate with each other. In DetailPresenter we can now go ahead and update the two protocols. + +First the Input. ``` protocol DetailPresenterInput: class { @@ -1269,7 +1271,7 @@ protocol DetailPresenterInput: class { } ``` -The Output: +And then the Output. ``` protocol DetailPresenterOutput: class { @@ -1278,7 +1280,7 @@ protocol DetailPresenterOutput: class { } ``` -And finally the Presenter by calling the repository, fetching the daqta and returning it to the controller: +Next we update the Presenter so that it calls the repository, fetching the needed data and returning it to the controller. ``` private let repository = DetailRepository() @@ -1290,7 +1292,7 @@ And finally the Presenter by calling the repository, fetching the daqta and retu } ``` -We can now create the Repository: +We can now create the Repository. Its only goal is to load the data and pass it back to the presenter. ``` final class DetailRepository { @@ -1307,7 +1309,7 @@ final class DetailRepository { } ``` -And the model: +Next the model. In this case it is really simple, but you are surely going to use a more complicated model object in a real application. ``` struct DetailModel { From e33d5d0829c9966153b6579cbf2ec8aa9a7b20cf Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sun, 23 Feb 2020 15:57:53 +0100 Subject: [PATCH 14/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 102502f1..b3c0b734 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1343,7 +1343,7 @@ Finally we update the controller by adding the label and setting it up: } ``` -And finally populate it: +Finally we populate the controller with the data from the Presenter. We also pass the dismiss call to UIKit. In a solution with a Router or a Coordinator the responsibility of such navigation action would be of the Router or Coordinator. ``` extension DetailViewController: DetailPresenterOutput { @@ -1357,6 +1357,8 @@ extension DetailViewController: DetailPresenterOutput { } ``` +Using MVP is going to be ideal for smaller applications where the flow between the ViewControllers is simple and having a Routing layer is not necessary. Once the application becomes more complicated it is easy to switch over to something more complex like VIPER since the foundation is already there. The only thing to do is to add the routing layer and move out some responsibilities from the Presenter and Repository to the Interactor. + You can find the source code on [Github][3]. From fec84306a3147c854391bc25e6194c4e3e70a20c Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Mon, 24 Feb 2020 18:56:14 +0100 Subject: [PATCH 15/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index b3c0b734..9e77b5df 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1369,7 +1369,11 @@ View <- Controller <-> Model MVC is the standard in iOS development. Model for the data, view for the UI and the Controller to manage the screen. -The AppDelegate is the same as the other examples, we simply setup the SceneDelegate: +A single controller is tipically used for one screen of content, but this is not a must. Thanks to controller containment you can use a controller similarly on how fragments are used in Android. This way you can better separate responsibilities between controllers and keep your code more clean. + +As for the other examples we are going to set up a simple application using the MVC architecture. + +The AppDelegate is the same as the other examples, we simply setup the new iOS13 SceneDelegate. There we are going to initialize the Controller. ``` @UIApplicationMain @@ -1388,7 +1392,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } ``` -In the SceneDelegate we setup the initial controller: +In the SceneDelegate we setup the initial controller and set it as root for the window. We use code for the initialization since it is easier to see in written form, but also because it is better for dependency injection. ``` class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -1405,7 +1409,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } ``` -We set up the controller: +In the controller we setup the various UI elements that we are going to use in the app. In MVC the controller is also responsible for fetching the data and updating the UI. That does not mean that you need to fetch the data directly from the Controller itself by for example initializing a new UrlSession. You should use an external class for that, for example a Repository. In this case we are going to load static data so we do it directly from the Controller. + +First we set up the UI. ``` class ViewController: UIViewController { From a0dd2917658c1330d23d0076e6176e2dc5cb33e1 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Thu, 27 Feb 2020 20:45:52 +0100 Subject: [PATCH 16/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 9e77b5df..5bbf44f9 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1444,7 +1444,7 @@ class ViewController: UIViewController { } ``` -And we load whatever we need to load and update the ui: +And we load whatever we need to load and update the UI: ``` override func viewDidLoad() { @@ -1516,7 +1516,9 @@ class DetailViewController: UIViewController { } ``` -Calling loadData will now load the data from the Repository: +The difference here would be that the request to load the data comes directly from the controller itself which is here not only responsible to handle the state and the updates for the view, but also to coordinate the loading of data. + +The loading itself does not happen in the controller though. Some people do, but it is bad practise since you are further injecting business logic into the Controller. Here calling `loadData` will now load the data from the Repository: ``` From 157c468d8088d650c020b0d08e8b20d781b349d9 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Fri, 28 Feb 2020 21:30:35 +0100 Subject: [PATCH 17/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 1 + 1 file changed, 1 insertion(+) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index 5bbf44f9..cdccdb46 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -1536,6 +1536,7 @@ The loading itself does not happen in the controller though. Some people do, but } ``` + The repository and the model are the same as in the previous example: ``` From 785f8c376230287a504074886fbe5ab322515f34 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sun, 12 Mar 2023 14:32:15 +0100 Subject: [PATCH 18/18] Update 2020-01-09-ios-architectures.md --- _posts/2020-01-09-ios-architectures.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_posts/2020-01-09-ios-architectures.md b/_posts/2020-01-09-ios-architectures.md index cdccdb46..d24452b8 100644 --- a/_posts/2020-01-09-ios-architectures.md +++ b/_posts/2020-01-09-ios-architectures.md @@ -20,6 +20,7 @@ As time progressed and apps grew in size and features scaling MVC up became prob We are going to analyze the most popular choices we have for iOS Development in 2020. This is not going to be a list of all the possible architecture for an iOS application, but only of what I consider relevant today in the industry. ## MVVM-C + Model/ViewModel/Coordinator Coordinator @@ -1566,7 +1567,7 @@ You should not just use one architecture and stick with it no matter what. What An application that shows a couple of screens from a JSON request is completely different from an application that handles a social networking application with user login, registration, front page, timeline, private messaging, ... -Getting so stuck with one architecture and using it everywhere indiscriminately means fitting a round peg in a square hole. +**Getting so stuck with one architecture and using it everywhere indiscriminately means fitting a round peg in a square hole.** [1]: https://github.com/valeIT/mvvm-ios