Mvi Architecture for SwiftUI Apps. MVI is a unidirectional data flow architecture.

Overview

Mvi-SwiftUI

If you like to read this on Medium , you can find it here MVI Architecture for SwiftUI Apps

MVI Architecture

Model-View-Intent (MVI) is a popular architecture in the Android world. It was introduced by Hannes Dorfmann. You can find it here. For the purpose of this article, a brief introduction to MVI is presented below.

MVI is a cyclical and unidirectional data-flow architecture.

  • The Model represents the state of the application. It contains the properties necessary to render the screen.
  • An Intent is an event to change the state of the system — e.g., user click is an event to change the state of the system.
  • The View observes the state change and updates itself accordingly.

Graphical Representation

Graphial representation

Working

  1. The user interacts with the View to create an Intent.
  2. The Intent changes the state of the application.
  3. A change in state updates the View. The cycle repeats.

Note : Additionally, API and Push notifications also change the state of the application.

MVI Implementation in SwiftUI

For demonstration purposes, we’ll implement a movie-search screen. It has a TextField to type in the keyword to search. Below is the list of movies matching the keyword.

Model (State of the Application)

First up, let’s consider the possible states of the application. At any given time, the application will be in one of the following states. Later, you’ll see that we have a view for each of the corresponding state.

  • InitState: It’s an initial state. Nothing has been queried.
  • Loading: The keyword has been entered, and movies are being fetched from the API.
  • SuccessfullyFetched : When the API returns the matching movies.
  • NoMatchingResults: When no results are found for the query.
  • ApiError: The HTTP request failed or the API returned an error

We will use Swift’s Enum to hold the states.

enum SearchPageState {
    case Init
    case Loading(String) // String is the message to be shown while laoding
    case SuccessfullyFetched([Movie]) // List of matching Movies
    case NoResultsFound
    case ApiError(String) // Error message to be shown
}

SearchPageViewModel(Business layer)

  • It contains the State of the Application.
  • Receives the Intent from the View and updates the State.
  • Publishes the updated State to the View.

Let’s see it’s implementation. SearchPageViewModel is a data-holder class that holds the state of the application. It conforms to the ObservableObject protocol. Anything inside the ObservableObject protocol can announce when its values have changed so the SwiftUI view can update itself.

It has a variable named uiState of the type SearchPageState. It’s wrapped with the Published property wrapper. This way it can notify the view when its value changes.

This class receives the intent, makes API calls to fetch the data, and updates the state of the application.

loadMovies is the function called when there’s an intent to fetch the matching movies. The comments below show how that function is updating the state of the application.

class SearchPageViewModel : ObservableObject {
    
    @Published var uiState: SearchPageState = .Init
    
    let repository: MovieRepository = MovieRepository()
    
    func loadMovies(query: String) {
        
        // 1. state is changed to Loading
        self.uiState = .Loading("Querying for \(query)")
        repository
            .searchMovies(query: query)
            .subscribe(
                onNext: { [weak self] response in
                    
                    if response.results.count == 0 {
                        // 2. State is updated to NoResultsFound
                        self?.uiState = .NoResultsFound
                    } else {
                        // 3. state is updated to SuccesffullyFetched
                        self?.uiState = .Fetched(response)
                    }
                    
                },
                onError: { error in
                    // 4. state is updated to ApiError
                    self.uiState = .ApiError("Results couldnot be fetched")
            }
        )
    }
}

View

A TextField to enter the keyword. Below that, we have a group that shows the view as per the state of the application.

Dry Run: When we the commit (hit the search button), it fires an intent to the SearchPageViewModel to update the state. SearchPageViewModel then makes an API to update the state.

Init → Loading → [SuccessfullyFetched | NoResultsFound | ApiError]

The group has exhaustive cases of the SearchPageState, a corresponding view for each state of the application.

struct SearchPageView: View {
    @ObservedObject var vm: SearchPageViewModel
    
    @State private var query: String = ""
    
    init(viewModel: SearchPageViewModel) {
        self.vm = viewModel
    }
    
    
    var body: some View {
        ScrollView {
            
            // Enter the keyword here and when commited fire an Intent to load the movies
            TextField("search movies", text: $query, onCommit: {
                // Fires an Intent to load the movies for the keyword 
                self.vm.loadMovies(query: self.query)
            })
            
            /**
            * It contains the exhaustive cases. It observes the uiState and
            * updates it's view accordingly
            */
            Group { () -> AnyView in
                
                switch vm.uiState {
                
                case .Init:
                    return AnyView(Text("Please type in to query"))
                
                case .Loading(let message):
                    return AnyView(Text(message))
                
                case .SuccessfullyFetched(let movies):
                    // This displays the list of Movies  matching the keyword
                    return AnyView(SearchedMoviesView(movies: movies))
                    
                case .NoResultsFound:
                    return AnyView(Text("No matching movies found"))
                    
                case .ApiError(let errorMessage):
                    return AnyView(Text(errorMessage))
                }
            }
        }
    }
}

Intent

It is an interaction on the View. The intent is propagated to the SearchPageViewModel which consequently alters it’s state. Example: Pressing the search icon after entering the query is an Intent to load the matching movies.

vm.loadMovies(query: self.query) is an Intent.

struct SearchPageView: View {
  ...
  ...
  var body: some View {
    ..
    ..
      // Enter the keyword here and when commited fire an Intent to load the movies
      TextField("search movies", text: $query, onCommit: {
          // Fires an Intent to load the movies for the keyword 
          self.vm.loadMovies(query: self.query)
      })
  }

}

Closing Points

Now that you have an understanding of MVI and it’s implementation, we’ll see why is this relevant for SwiftUI.

UI applications like mobile apps always try to keep the view in sync with the state of the application. Example: Imagine driving a car. The speed reading on the dashboard is nothing but the state (speed) of the car. If the dashboard shows a wrong reading, then it’s an inconsistency.

UI = f(AppState).

SwiftUI has an infrastructure wherein data is bound to the UI (data binding). In SwiftUI, showing an alert is controlled through binding. The performance impact of rerendering (invalidating) the view consequent to data change is handled by the framework. So using the data to drive the UI (data-driven UI) will wipe out the inconsistencies that exist between the data and the UI.

You might also like...
The demo project to show how to organize code to make SwiftUI apps easy to be test.
The demo project to show how to organize code to make SwiftUI apps easy to be test.

TestableApp I combined the idea to use functional programming instead of an loader instance in ModelView(I prefer to think of it as a service) and Res

A simple confetti view for apps using SwiftUI.
A simple confetti view for apps using SwiftUI.

ConfettiView Create fun animated confetti views with ease! Installation Use Swift Package Manager to install this package: https://github.com/benlmyer

This framework contains SBB (Swiss Federal Railways) UI elements for iOS SwiftUI Apps
This framework contains SBB (Swiss Federal Railways) UI elements for iOS SwiftUI Apps

Framework: Design System Mobile for iOS & SwiftUI This framework contains SBB (Swiss Federal Railways) UI elements for iOS SwiftUI Apps. It allows an

A simple star rating library for SwiftUI apps on macOS and iOS

DLDRating A simple star rating library for SwiftUI apps on macOS and iOS. Features Installation Usage Styling Credits DLDRating was made by Dionne Lie

🖼 Gallery App for Harvest (Elm Architecture + Optics) + SwiftUI + Combine.
🖼 Gallery App for Harvest (Elm Architecture + Optics) + SwiftUI + Combine.

🖼 Harvest-SwiftUI-Gallery Gallery App for Harvest (Elm Architecture + Optics) + SwiftUI + Combine. Examples Todo List Stopwatch GitHub Search TimeTra

SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.
SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

Articles related to this project Clean Architecture for SwiftUI Programmatic navigation in SwiftUI project Separation of Concerns in Software Design C

Open source game built in SwiftUI and the Composable Architecture.
Open source game built in SwiftUI and the Composable Architecture.

isowords This repo contains the full source code for isowords, an iOS word search game played on a vanishing cube. Connect touching letters to form wo

Porting the example app from our Advanced iOS App Architecture book from UIKit to SwiftUI.

SwiftUI example app: Koober We're porting the example app from our Advanced iOS App Architecture book from UIKit to SwiftUI and we are sharing the cod

An iOS template project using SwiftUI, Combine and MVVM-C software architecture
An iOS template project using SwiftUI, Combine and MVVM-C software architecture

SwiftUI-MVVM-C A template project that uses SwiftUI for UI, Combine for event handling, MVVM-C for software architecture. I have done some small proje

Owner
null
UDF (Unidirectional Data Flow) architecture on SwiftUI/Combine

The license The SwiftUI-UDF stays under a dual license (email confirmation required): It can be Free for non-commercial use, public repository or star

Max Kuznetsov 13 Nov 10, 2022
Proof concept of modularized app with SwiftPackages built over MVI + Combine + SwiftUI in a single repo

PKDex-iOS Proof concept of modularized app with SwiftPackages built over MVI + Combine + SwiftUI in a single repo Introduction This project is a proof

Miguel Angel Zapata 13 Nov 22, 2022
An MVI, SwiftUI and Combine exploration project to shop clothe by resemblance 👗

Portfolio is an MVI, SwiftUI and Combine exploration project to keep track of your portfolio over time. Resources MVI MVI Architecture for Android Tut

Bastien Falcou 7 Oct 4, 2022
Unidirectional reactive architecture using new Apple Combine framework

Unidirectional Reactive Architecture. This is a Combine implemetation of ReactiveFeedback and RxFeedback

null 687 Nov 25, 2022
Best architecture for SwiftUI + CombineBest architecture for SwiftUI + Combine

Best architecture for SwiftUI + Combine The content of the presentation: First of the proposed architectures - MVP + C Second of the proposed architec

Kyrylo Triskalo 3 Sep 1, 2022
SwiftUI-TodoList - This project uses MVVM architecture and UserDefaults to persist data

SwiftUI-TodoList | ├── SwiftUITodoList | ├── Library | ├── Models

Yezan Ahmed 0 Mar 9, 2022
A document-based SwiftUI application for viewing and editing EEG data, aimed at making software for viewing brain imaging data more accessible.

Trace A document-based SwiftUI application for viewing and editing EEG data, aimed at making software for viewing brain imaging data more accessible.

Tahmid Azam 3 Dec 15, 2022
Notes App using Core Data for persisting the data ✨

Notes Notes app where we can save and manage our daily notes. App usage The app allow us to add new notes We can delete our existing notes We can edit

Chris 0 Nov 13, 2021
An iPhone Simulator "Wrapper" for SwiftUI Apps on macOS

SwiftUIPhone Run a SwiftUI app (or any SwiftUI view) in an iPhone Simulator "wrapper", directly on macOS! To be clear, this is not an iPhone Simulator

Justin Kaufman 7 May 20, 2022
This repository contains code for building Universal Apps with SwiftUI.

MindLikeWater This Repo This repository contains code for building Universal Apps with SwiftUI. The same codebase can be compiled to produce binaries

Jorge D. Ortiz Fuentes 1 Nov 23, 2021