← Back to list

Avoiding Fat ViewModels in SwiftUI: A Practical MVVM Upgrade with Repository + Actor

MVVM is easy to adopt and hard to scale.

Urvashi Tomar · 2026-05-26 07:01 · 0 claps · 1.8 min read
#mvvm-architecture #ios-architecture #iosdev #swiftui
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🏛️ · Architecture

Avoiding Fat ViewModels in SwiftUI: A Practical MVVM Upgrade with Repository + Actor

MVVM is easy to adopt and hard to scale.

Most SwiftUI codebases start clean: a ViewModel with loading flags, a list of items, and one async call. A few sprints later, that same ViewModel is doing API calls, cache validation, local persistence, retries, and UI state updates. It still “works,” but now every change is risky.

This post shows a practical way to keep MVVM clean in real projects by separating responsibilities across:

  1. ViewModel for presentation state
  2. Repository for data orchestration policy
  3. DataSources for implementation details
  4. Actor for shared mutable async cache state

The Problem: The Omniscient ViewModel

A fat ViewModel usually contains:

  • local reads/writes
  • network calls
  • cache expiry logic
  • fallback rules
  • UI transformations

The issue is not code size. The issue is mixed responsibilities.

When policy, infrastructure, and UI state live in one class:

  • tests become painful
  • race conditions become easier to introduce
  • regressions increase when requirements change

A cleaner approach is to let the Repository orchestrate data flow. It becomes the single place that decides when cached data is trustworthy, when the app should revalidate against the API, and how to degrade gracefully if the network fails. That separation keeps the UI layer simple, makes failure handling predictable, and gives the application a clearer, more testable runtime path.

protocol TodoRepositoryProtocol {
    func getTodos() async throws -> [TodoItem]
}

final class TodoRepository: TodoRepositoryProtocol {
    private let remote: TodoRemoteDataSourceProtocol
    private let local: TodoLocalDataSourceProtocol
    private let cacheTracker: CacheValidityTracker

    init(
        remote: TodoRemoteDataSourceProtocol,
        local: TodoLocalDataSourceProtocol,
        cacheTracker: CacheValidityTracker
    ) {
        self.remote = remote
        self.local = local
        self.cacheTracker = cacheTracker
    }

    func getTodos() async throws -> [TodoItem] {
        let cachedTodos = local.loadTodos()

        if await cacheTracker.isFresh(), !cachedTodos.isEmpty {
            return cachedTodos
        }

        do {
            let todos = try await remote.fetchTodos()
            local.saveTodos(todos)
            await cacheTracker.markFresh()
            return todos
        } catch {
            guard !cachedTodos.isEmpty else { throw error }
            return cachedTodos
        }
    }
}
actor CacheValidityTracker {
    private var lastSuccessfulFetch: Date?
    private let ttl: TimeInterval

    init(ttl: TimeInterval) {
        self.ttl = ttl
    }

    func isFresh(now: Date = .now) -> Bool {
        guard let lastSuccessfulFetch else { return false }
        return now.timeIntervalSince(lastSuccessfulFetch) < ttl
    }

    func markFresh(at date: Date = .now) {
        lastSuccessfulFetch = date
    }
}

Why Actor Here?

Actor is useful when:

  • state is mutable
  • state is shared across async calls
  • correctness depends on safe sequencing

Cache freshness metadata is exactly that case.

Without actor, concurrent loads/retries/background triggers can read/update freshness state unsafely. With actor, access is serialised and predictable.


메타데이터
post_id
ccb4d980fe7f
slug
avoiding-fat-viewmodels-in-swiftui-a-practical-mvvm-upgrade-with-repository-actor-ccb4d980fe7f
url
https://medium.com/@singhurvashi2018/avoiding-fat-viewmodels-in-swiftui-a-practical-mvvm-upgrade-with-repository-actor-ccb4d980fe7f
canonical_url
https://medium.com/@singhurvashi2018/avoiding-fat-viewmodels-in-swiftui-a-practical-mvvm-upgrade-with-repository-actor-ccb4d980fe7f
author_url
https://medium.com/@singhurvashi2018
status
ok
fetched_at
2026-07-10 10:46:47