Skip to main content

Understanding MVVM + Clean Architecture in SwiftUI

When building an iOS app, it's important to organize your code in a way that keeps things clean, testable, and easy to maintain. One of the best ways to do that is by combining MVVM (Model–View–ViewModel) with Clean Architecture.



1. Presentation Layer — The SwiftUI Frontline

This is where your app interacts with the user.
In SwiftUI, the View and ViewModel live in this layer.

  • View (SwiftUI View):
    These are your UI screens — for example, ContentView, LoginView, or ProfileView.
    SwiftUI Views are simple and reactive. They observe data and automatically update the UI when something changes.

    struct ContentView: View {
        @StateObject var viewModel = ContentViewModel()
        
        var body: some View {
            VStack {
                if viewModel.isLoading {
                    ProgressView()
                } else {
                    List(viewModel.items) { item in
                        Text(item.title)
                    }
                }
            }
            .onAppear { viewModel.fetchItems() }
        }
    }
    
  • ViewModel:
    The ViewModel acts as the middle layer between your UI and your business logic.
    It fetches data from the Use Case (in the domain layer), handles any transformation needed for display, and exposes it to the SwiftUI View.

    class ContentViewModel: ObservableObject {
        @Published var items: [ItemUIModel] = []
        @Published var isLoading = false
        
        private let getItemsUseCase: GetItemsUseCase
        
        init(getItemsUseCase: GetItemsUseCase = GetItemsUseCase()) {
            self.getItemsUseCase = getItemsUseCase
        }
        
        func fetchItems() {
            isLoading = true
            getItemsUseCase.execute { result in
                DispatchQueue.main.async {
                    self.items = result.map { ItemUIModel(from: $0) }
                    self.isLoading = false
                }
            }
        }
    }
    

2. Domain Layer — The Brain of Your App

This layer holds your business logic — what the app actually does.
It doesn’t care about how data is shown or where it comes from.

  • Use Cases:
    A Use Case defines a specific piece of app logic — for example, “Get all items”, “Login user”, or “Save favorite”.
    It talks to the Repository in the Data Layer to get or save information.

    struct GetItemsUseCase {
        private let repository = ItemRepository()
        
        func execute(completion: @escaping ([Item]) -> Void) {
            repository.fetchItems { items in
                completion(items)
            }
        }
    }
    
  • Entities:
    These are your core business models — pure data structures that represent real-world things in your app.

    struct Item: Identifiable {
        let id: Int
        let title: String
    }
    

3. Data Layer — The Data Engine

This layer handles how and where your data is stored or retrieved — such as from an API, local database, or secure storage.

  • Repository:
    The Repository acts as a bridge between the Domain Layer and the actual data sources.
    It decides whether to get data from the network, cache, or database.

    class ItemRepository {
        private let dataSource = ItemDataSource()
        
        func fetchItems(completion: @escaping ([Item]) -> Void) {
            dataSource.getItemsFromAPI { items in
                completion(items)
            }
        }
    }
    
  • Data Source:
    This is where data fetching actually happens — API calls, database queries, or file reads.

    class ItemDataSource {
        func getItemsFromAPI(completion: @escaping ([Item]) -> Void) {
            // Simulate network fetch
            let items = [Item(id: 1, title: "Apple"), Item(id: 2, title: "Banana")]
            completion(items)
        }
    }
    

4. Service Layer — The Foundation

This is an optional lower layer that includes reusable services like:

  • Network Service: Handles API requests.

  • Database Service: Manages local storage (e.g., Core Data, Realm).

  • Secure Storage: For keychain or sensitive data.

These services are used by the Data Source.


Why This Architecture Is Great for SwiftUI

  • Separation of concerns: Each layer has one clear responsibility.

  • Easier testing: You can test Use Cases or Repositories without running the UI.

  • Scalability: As your app grows, your architecture stays organized.

  • Reusability: Logic and models can be reused in other platforms (like watchOS or macOS).

By combining MVVM with Clean Architecture, your SwiftUI app becomes not just functional — but clean, testable, and ready to scale for years to come. 🚀

Popular posts from this blog

Animating label text update - choosing a better way

Recently I published a countdown app .  At one point of development - I have to show a timer on a UILabel which ticks on each seconds. As usual I started  setting text to a label object - self .timerLabel.text = someString Easy piece of cake right !?   But wait ... it won't take much user attention when timer ticks on every seconds. So I decided to make use of a simple animation while label gets text update. I found there are dozens of ways to animate a label. In this short article, I listed 3 best way you can animate text on a label. ( Spoiler Alert 👀- I decided to go with 3rd option)  1. Fade In - Fade out animation : CATransition class has got transition type `fade`. With timing function of CATransition - I was able to see the below result. let animation: CATransition = CATransition () animation.timingFunction = CAMediaTimingFunction (name: CAMediaTimingFunctionName .easeInEaseOut) animation.type = CATransitionType .fade animation.subtype = C...

UICollectionViewCell shows with wrong size on First time - Solved

We commonly use Collection view where its cell size calculated run time. The flow layout delegate is responsible to return individual cell sizes. BUT in most of the cases, delegate method `collectionView: layout sizeForItem:` expects cell size too early. Before generating actual cell size. extension YourViewController : UICollectionViewDelegateFlowLayout { func collectionView ( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize (width: externalWidth, height: externalHeight) } } For instance, if a cell size depends on external view and its frame is not yet ready - results with wrong (or outdated) cell size. Typically happens for the first time view controller laid out all views. You can find similar queries in StackOverflow community : Collection view sizeForItemNotWorking UICollectionViewCell content wrong size on first load How to refresh UICollec...

Prevent Navigationbar or Tabbar overlapping Subview - solved for Card view

Recently, I started with a Card view added as a subview of UIView in a view-controller. When a view controller created along subviews, it tends to use entire screen bounds and also slips behind Tab bar or Navigation bar. In my current situation, it's second case. Casually new iOS developers will write a patch by additional value for coordinate y and subtracting bar height from its size. A lot of them posted in SO threads too : How to prevent UINavigationBar from covering top of view? View got hidden below UINavigationBar iOS 7 Navigation Bar covers some part of view at Top So, how I got solved ? self.edgesForExtendedLayout = [] This  will avoid all subviews in a view controller get behind any bars. Read full apple  documentation on here. Full Source code below :  //Simple view controller where its view layed-out as a card. class WidgetCardViewController : UIViewController { var containerView = UIView () //MARK:- View Controller Life Cyc...

Implementing autocompletion OTP field in iOS

Long waiting is over. !!  iOS 12 brings Autofill for OTP text field which is close to Android provided a decade back. Previously in iOS we used to toggle between OTP text screen and message inbox.  Which was hard to remember and time consuming resulting a bad user experience. Personally, I have been asked from the client/customer couple of times to implement autocompletion for OTP field and took me a lot of time to convey that it is not possible in iOS. Why Autofill was not possible previously?  We all know that Apple gives at most care for user privacy. When we see iOS architecture, each individual app is like a separate island. There is no inter-app bridge between apps (exception for Keychain and URLSchemes APIs which gives very limited scope). Thus we cannot read message content from inbox. Where to start Autofilling? First of all, the target SMS need to have the OTP Code with prefix string "Code" or "Passcode"on its message content. Beware of OTP c...

Static variable vs Class variable in Swift

How Static properties differs to usual properties ? Used to create type properties with either let or var. These are shared between all objects of a class.  During class loading time, a single memory location allocated for the property if we declared as static. Static property belongs to a class (or struct) Type rather than Instance of class (or struct). Hence it also called  Type Properties What if we declare "static var" ? Since it's a variable, we can change it in future even after initialised. When we change the value of the static variable property, that property is now changed in all future instances. What if we declare "static let" ? static let instance : Singleton = Singleton() Since it's constant, we cannot change once initialised. Its value will remain same even if we share among all the instance or object of a class or struct. Useful to adopt singleton pattern. How class variables differ static variables ? It dif...