iOS, App Architectures

Published by

on

✍️ Note

Some codes and contents are sourced from Apple’s official documentation, google android official site and wikipedia. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

MVC: Model View Controller

The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other. Each of the three types of objects is separated from the others by abstract boundaries and communicates with objects of the other types across those boundaries. The collection of objects of a certain MVC type in an application is sometimes referred to as a layer—for example, model layer.

MVC is central to a good design for a Cocoa application. The benefits of adopting this pattern are numerous. Many objects in these applications tend to be more reusable, and their interfaces tend to be better defined. Applications having an MVC design are also more easily extensible than other applications. Moreover, many Cocoa technologies and architectures are based on MVC and require that your custom objects play one of the MVC roles.

https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

Model Objects

Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. For example, a model object might represent a character in a game or a contact in an address book. A model object can have to-one and to-many relationships with other model objects, and so sometimes the model layer of an application effectively is one or more object graphs. Much of the data that is part of the persistent state of the application (whether that persistent state is stored in files or databases) should reside in the model objects after the data is loaded into the application. Because model objects represent knowledge and expertise related to a specific problem domain, they can be reused in similar problem domains. Ideally, a model object should have no explicit connection to the view objects that present its data and allow users to edit that data—it should not be concerned with user-interface and presentation issues. 

Communication: User actions in the view layer that create or modify data are communicated through a controller object and result in the creation or updating of a model object. When a model object changes (for example, new data is received over a network connection), it notifies a controller object, which updates the appropriate view objects.

Apple

View Objects

A view object is an object in an application that users can see. A view object knows how to draw itself and can respond to user actions. A major purpose of view objects is to display data from the application’s model objects and to enable the editing of that data. Despite this, view objects are typically decoupled from model objects in an MVC application. 

Because you typically reuse and reconfigure them, view objects provide consistency between applications. Both the UIKit and AppKit frameworks provide collections of view classes, and Interface Builder offers dozens of view objects in its Library. 

Communication: View objects learn about changes in model data through the application’s controller objects and communicate user-initiated changes—for example, text entered in a text field—through controller objects to an application’s model objects.

Apple

Controller Objects

A controller object acts as an intermediary between one or more of an application’s view objects and one or more of its model objects. Controller objects are thus a conduit through which view objects learn about changes in model objects and vice versa. Controller objects can also perform setup and coordinating tasks for an application and manage the life cycles of other objects. 

Communication: A controller object interprets user actions made in view objects and communicates new or changed data to the model layer. When model objects change, a controller object communicates that new model data to the view objects so that they can display it.

Apple

UIViewController

A view controller’s main responsibilities include the following:

  • Updating the contents of the views, usually in response to changes to the underlying data
  • Responding to user interactions with views
  • Resizing views and managing the layout of the overall interface
  • Coordinating with other objects — including other view controllers — in your app
https://developer.apple.com/documentation/uikit/uiviewcontroller

MVP: Model View Presenter

Model–View–Presenter

MVP is a user interface architectural pattern engineered to facilitate automated unit testing and improve the separation of concerns in presentation logic:

  • The model is an interface defining the data to be displayed or otherwise acted upon in the user interface.
  • The view is a passive interface that displays data (the model) and routes user commands (events) to the presenter to act upon that data.
  • The presenter acts upon the model and the view. It retrieves data from repositories (the model), and formats it for display in the view.

Normally, the view implementation instantiates the concrete presenter object, providing a reference to itself. 

The degree of logic permitted in the view varies among different implementations. At one extreme, the view is entirely passive, forwarding all interaction operations to the presenter. In this formulation, when a user triggers an event method of the view, it does nothing but invoke a method of the presenter that has no parameters and no return value. The presenter then retrieves data from the view through methods defined by the view interface. Finally, the presenter operates on the model and updates the view with the results of the operation. Other versions of model–view–presenter allow some latitude with respect to which class handles a particular interaction, event, or command. This is often more suitable for web-based architectures, where the view, which executes on a client’s browser, may be the best place to handle a particular interaction or command.

From a layering point of view, the presenter class might be considered as belonging to the application layer in a multilayered architecture system, but it can also be seen as a presenter layer of its own between the application layer and the user interface layer.

https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter

Wikipedia provided an example written in c#

public class Presenter : IPresenter 
{
    public Presenter(IView view) 
    {
        // ...
    }
}

public class View : IView
{
    private IPresenter _presenter;

    public View()
    {
        _presenter = new Presenter(this);
    }
}

I wrote an example written in Swift

//View Interface
protocol AccountViewProtocol: AnyObject {
    var presenter: AccountPresentProtocol! { get set }
    
    //From Presenter
    func display(username: String)
    func showError(message: String)
}

//Presenter Interface
protocol AccountPresentProtocol {
    init(view: AccountViewProtocol, service: AccountServiceInterface)
    
    //Actions from ViewProtocol
    func update(username: String)
}

//Model Interface: Optional
protocol AccountModelProtocol {
    func usernameIsNotEmpty() -> Bool
    mutating func update(username: String) -> Bool
}

struct AccountModel: AccountModelProtocol {
    var username: String
    var password: String
    
    func usernameIsNotEmpty() -> Bool {
        !username.isEmpty
    }
    
    mutating func update(username: String) -> Bool {
        guard usernameIsNotEmpty() else {
            return false
        }
        self.username = username
        return true
    }
}

protocol AccountServiceInterface {
    func getUser() async throws -> AccountModel
}

class AccountService: AccountServiceInterface {
    func getUser() async throws -> AccountModel {
        AccountModel(username: "Sungwook", password: "1234")
    }
}

class AccountPresenter: AccountPresentProtocol {
    private weak var view: AccountViewProtocol?
    private var model: AccountModelProtocol?
    private let service: AccountServiceInterface
    
    required init(view: AccountViewProtocol, service: AccountServiceInterface) {
        self.view = view
        self.service = service
    }
    
    func update(username: String) {
        @Sendable func notifyModelUpdateStatus() {
            let isUpdated = model?.update(username: username) ?? false
            isUpdated ? view?.display(username: username) : view?.showError(message: "Username can't be empty")
        }
        
        if model == nil {
            Task { [weak self] in
                do {
                    let userModel = try await self?.service.getUser()
                    self?.model = userModel
                    notifyModelUpdateStatus()
                }
                catch {
                    self?.view?.showError(message: error.localizedDescription)
                }
            }
        }
        else {
            notifyModelUpdateStatus()
        }
    }
}


class AccountView: UIView, AccountViewProtocol {
    var presenter: AccountPresentProtocol!
    @IBOutlet private weak var usernameTextField: UITextField!
    
    init() {
        super.init(frame: .zero)
        self.presenter = AccountPresenter(view: self, service: AccountService())
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        //Setup Views - e.g, UITextField
    }
    
    @objc func didTapSaveButton(username: String) {
        presenter.update(username: username)
    }
    
    //MARK: Events from Presenter
    func display(username: String) {
        //Update name
        usernameTextField?.text = username
        print("🟢 Username updated -> \(username)")
    }
    
    func showError(message: String) {
        //Show error message
        print("🔴 \(message)")
    }
}

let accountView = AccountView()
accountView.didTapSaveButton(username: "Shawn")

//Prints
Username updated -> Shawn\n"

//This sample is written by Shawn (Me)

MVVM: Model-View-ViewModel

Model–view–viewmodel (MVVM) is an architectural pattern in computer software that facilitates the separation of the development of a graphical user interface (GUI; the view)—be it via a markup language or GUI code—from the development of the business logic or back-end logic (the model) such that the view is not dependent upon any specific model platform.

The viewmodel of MVVM is a value converter,[1] meaning it is responsible for exposing (converting) the data objects from the model in such a way they can be easily managed and presented. In this respect, the viewmodel is more model than view, and handles most (if not all) of the view’s display logic.[1] The viewmodel may implement a mediator pattern, organizing access to the back-end logic around the set of use cases supported by the view.

MVVM is a variation of Martin Fowler‘s Presentation Model design pattern.[2][3] It was invented by Microsoft architects Ken Cooper and Ted Peters specifically to simplify event-driven programming of user interfaces. The pattern was incorporated into the Windows Presentation Foundation (WPF) (Microsoft’s .NET graphics system) and Silverlight, WPF’s Internet application derivative.[3] John Gossman, a Microsoft WPF and Silverlight architect, announced MVVM on his blog in 2005.[3][4]

Model–view–viewmodel is also referred to as model–view–binder, especially in implementations not involving the .NET platform. ZK, a web application framework written in Java, and the JavaScript library KnockoutJS use model–view–binder.

Model

Model refers either to a domain model, which represents real state content (an object-oriented approach), or to the data access layer, which represents content (a data-centric approach).[citation needed]

View

As in the model–view–controller (MVC) and model–view–presenter (MVP) patterns, the view is the structure, layout, and appearance of what a user sees on the screen.[7] It displays a representation of the model and receives the user’s interaction with the view (mouse clicks, keyboard input, screen tap gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.

View model

The view model is an abstraction of the view exposing public properties and commands. Instead of the controller of the MVC pattern, or the presenter of the MVP pattern, MVVM has a binder, which automates communication between the view and its bound properties in the view model. The view model has been described as a state of the data in the model.[8]The main difference between the view model and the Presenter in the MVP pattern is that the presenter has a reference to a view, whereas the view model does not. Instead, a view directly binds to properties on the view model to send and receive updates. To function efficiently, this requires a binding technology or generating boilerplate code to do the binding.[7]Under object-oriented programming, the view model can sometimes be referred to as a data transfer object.[9]

Binder

Declarative data and command-binding are implicit in the MVVM pattern. In the Microsoft solution stack, the binder is a markup language called XAML.[10] The binder frees the developer from being obliged to write boiler-plate logic to synchronize the view model and view. When implemented outside of the Microsoft stack, the presence of a declarative data binding technology is what makes this pattern possible,[5][11] and without a binder, one would typically use MVP or MVC instead and have to write more boilerplate (or generate it with some other tool).

Summary

The MVVM pattern attempts to gain both advantages of separation of functional development provided by MVC, while leveraging the advantages of data bindings and the framework by binding data as close to the pure application model as possible.[3][4][12][clarification needed] It uses the binder, view model, and any business layers’ data-checking features to validate incoming data. The result is that the model and framework drive as much of the operations as possible, eliminating or minimizing application logic which directly manipulates the view (e.g., code-behind).

Criticism

John Gossman has criticized the MVVM pattern and its application in specific uses, stating that MVVM can be “overkill” when creating simple user interfaces. For larger applications, he believes that generalizing the viewmodel upfront can be difficult, and that large-scale data binding can lead to lower performance.

https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel

MVVM is the most popular architecture of iOS app architecture. Binder is the key different with the others. We can implement this feature using Combine framework or RxSwift.

MVVM-C: Model-View-ViewModel + Coordinator

MVVM-C, There is an extra layer. It called coordinator. It handles UI flows likes present / push.

VIPER: View Interactor Presenter Entity Router

This architecture is well explained at objc.io blog.

The main parts of VIPER are:

  • View:
    • displays what it is told to by the Presenter and relays user input back to the Presenter.
    • The View is passive. It waits for the Presenter to give it content to display; it never asks the Presenter for data.
    • The Presenter does not know about the existence of UILabelUIButton, etc. The Presenter only knows about the content it maintains and when it should be displayed. It is up to the View to determine how the content is displayed.
    • The View is an abstract interface, defined in Objective-C with a protocol. A UIViewController or one of its subclasses will implement the View protocol.
    • Views and view controllers also handle user interaction and input. It’s easy to understand why view controllers usually become so large, since they are the easiest place to handle this input to perform some action. To keep our view controllers lean, we need to give them a way to inform interested parties when a user takes certain actions. The view controller shouldn’t be making decisions based on these actions, but it should pass these events along to something that can.
  • Interactor:
    • contains the business logic as specified by a use case.
    • Interactor is a PONSO (Plain Old NSObject) that primarily contains logic, it is easy to develop using TDD.
  • Presenter:
    • contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).
    • The Presenter is a PONSO that mainly consists of logic to drive the UI. It knows when to present the user interface. It gathers input from user interactions so it can update the UI and send requests to an Interactor.
    • Entities are never passed from the Interactor to the Presenter. Instead, simple data structures that have no behavior are passed from the Interactor to the Presenter. This prevents any ‘real work’ from being done in the Presenter. The Presenter can only prepare the data for display in the View.
  • Entity:
    • contains basic model objects used by the Interactor.
    • Entities are only manipulated by the Interactor. The Interactor never passes entities to the presentation layer (i.e. Presenter).
    • Entities also tend to be PONSOs. If you are using Core Data, you will want your managed objects to remain behind your data layer. Interactors should not work with NSManagedObjects.
  • Routing:
    • contains navigation logic for describing which screens are shown in which order.
    • Routes from one screen to another are defined in the wireframes created by an interaction designer.
    • In VIPER, the responsibility for Routing is shared between two objects: the Presenter, and the wireframe. A wireframe object owns the UIWindowUINavigationControllerUIViewController, etc. It is responsible for creating a View/ViewController and installing it in the window.
    • Since the Presenter contains the logic to react to user inputs, it is the Presenter that knows when to navigate to another screen, and which screen to navigate to. Meanwhile, the wireframe knows how to navigate. So, the Presenter will use the wireframe to perform the navigation. Together, they describe a route from one screen to the next.
    • The wireframe is also an obvious place to handle navigation transition animations.

Application Components Fitting in with VIPER

  • Network
    • Apps are usually much more compelling when they are connected to the network. But where should this networking take place and what should be responsible for initiating it? It’s typically up to the Interactor to initiate a network operation, but it won’t handle the networking code directly. It will ask a dependency, like a network manager or API client. The Interactor may have to aggregate data from multiple sources to provide the information needed to fulfill a use case. Then it’s up to the Presenter to take the data returned by the Interactor and format it for presentation.
  • Data Store
    • A data store is responsible for providing entities to an Interactor. As an Interactor applies its business logic, it will need to retrieve entities from the data store, manipulate the entities, and then put the updated entities back in the data store. The data store manages the persistence of the entities. Entities do not know about the data store, so entities do not know how to persist themselves.
    • The Interactor should not know how to persist the entities either. Sometimes the Interactor may want to use a type of object called a data manager to facilitate its interaction with the data store. The data manager handles more of the store-specific types of operations, like creating fetch requests, building queries, etc. This allows the Interactor to focus more on application logic and not have to know anything about how entities are gathered or persisted. One example of when it makes sense to use a data manager is when you are using Core Data.
  • TDD to develop an Interactor
    • it is possible to switch out the production data store with a test double/mock. Not talking to a remote server (for a web service) or touching the disk (for a database) allows your tests to be faster and more repeatable.
    • One reason to keep the data store as a distinct layer with clear boundaries is that it allows you to delay choosing a specific persistence technology. If your data store is a single class, you can start your app with a basic persistence strategy, and then upgrade to SQLite or Core Data later if and when it makes sense to do so, all without changing anything else in your application’s code base.
    • Using Core Data in an iOS project can often spark more debate than architecture itself. However, using Core Data with VIPER can be the best Core Data experience you’ve ever had. Core Data is a great tool for persisting data while maintaining fast access and a low-memory footprint. But it has a habit of snaking its NSManagedObjectContext tendrils all throughout an app’s implementation files, particularly where they shouldn’t be. VIPER keeps Core Data where it should be: at the data store layer.
    • In the to-do list example, the only two parts of the app that know that Core Data is being used are the data store itself, which sets up the Core Data stack, and the data manager. The data manager performs a fetch request, converts the NSManagedObjects returned by the data store into standard PONSO model objects, and passes those back to the business logic layer. That way, the core of the application is never dependent on Core Data, and as a bonus, you never have to worry about stale or poorly threaded NSManagedObjects gunking up the works.
  • Storyboard
    • we tend to make is to choose not to use segues. There may be some cases where using the segue makes sense, but the danger with segues is they make it very difficult to keep the separation between screens — as well as between UI and application logic — intact. As a rule of thumb, we try not to use segues if implementing the prepareForSegue method appears necessary.
    • Otherwise, storyboards are a great way to implement the layout for your user interface, especially while using Auto Layout. We chose to implement both screens for the to-do list example using a storyboard, and use code such as this to perform our own navigation:
  • Writing a test cases
    • Building the Interactor first is a natural fit with TDD. If you develop the Interactor first, followed by the Presenter, you get to build out a suite of tests around those layers first and lay the foundation for implementing those use cases. You can iterate quickly on those classes, because you won’t have to interact with the UI in order to test them. Then, when you go to develop the View, you’ll have a working and tested logic and presentation layer to connect to it. By the time you finish developing the View, you might find that the first time you run the app everything just works, because all your passing tests tell you it will work.

Conclusion

At its core, VIPER is an architecture based on the Single Responsibility Principle.

https://www.objc.io/issues/13-architecture/viper/

You can check an swift version example

App Architecture in SwiftUI

SwiftUI provide a powerful features like StateObject, ObservedObject, ObservableObject, State, Bindable, Binding and more. If you interested in, check this article

Managing user interface state

Encapsulate view-specific data within your app’s view hierarchy to make your views reusable

Store data as state in the least common ancestor of the views that need the data to establish a single source of truth that’s shared across views. Provide the data as read-only through a Swift property, or create a two-way connection to the state with a binding. SwiftUI watches for changes in the data, and updates any affected views as needed.

Don’t use state properties for persistent storage because the life cycle of state variables mirrors the view life cycle. Instead, use them to manage transient state that only affects the user interface, like the highlight state of a button, filter settings, or the currently selected list item. You might also find this kind of storage convenient while you prototype, before you’re ready to make changes to your app’s data model.

https://developer.apple.com/documentation/swiftui/managing-user-interface-state

Managing model data

💡 Below image is no longer available. It has disappeared after Apple introduced Observable macro.

Model data

Manage the data that your app uses to drive its interface.

SwiftUI offers a declarative approach to user interface design. As you compose a hierarchy of views, you also indicate data dependencies for the views. When the data changes, either due to an external event or because of an action that the user performs, SwiftUI automatically updates the affected parts of the interface. As a result, the framework automatically performs most of the work that view controllers traditionally do.

The framework provides tools, like state variables and bindings, for connecting your app’s data to the user interface. These tools help you maintain a single source of truth for every piece of data in your app, in part by reducing the amount of glue logic you write. Select the tool that best suits the task you need to perform:

  • Manage transient UI state locally within a view by wrapping value types as State properties.
  • Share a reference to a source of truth, like local state, using the Binding property wrapper.
  • Connect to and observe reference model data in views by applying the Observable() macro to the model data type. Instantiate an observable model data type directly in a view using a State property. Share the observable model data with other views in the hierarchy without passing a reference using the Environment property wrapper.
https://developer.apple.com/documentation/swiftui/model-data

댓글 남기기