Reusable ViewModel Architecture for SwiftUI

SwiftUI has evolved fast — especially with the new @observable macro, improved data flow, and better navigation tools.
But one thing still confuses many developers:

How do you structure ViewModels in a scalable, reusable, testable way?

This post sh…


This content originally appeared on DEV Community and was authored by Sebastien Lato

SwiftUI has evolved fast — especially with the new @observable macro, improved data flow, and better navigation tools.

But one thing still confuses many developers:

How do you structure ViewModels in a scalable, reusable, testable way?

This post shows the best-practice ViewModel architecture I now use in all my apps — simple enough for small projects, clean enough for large ones.

You’ll learn:

  • how to design clean ViewModels
  • how to separate logic from UI
  • how to use @observable correctly
  • how to inject services
  • how to mock data for testing
  • how to structure features
  • how to avoid “massive ViewModels”

Let’s build it the right way. 🚀

🧠 1. The Role of a ViewModel

A ViewModel should:

  • hold UI state
  • expose derived values
  • coordinate services
  • validate user input
  • trigger navigation actions
  • manage async work

A ViewModel should NOT:

  • contain UI layout logic
  • directly manipulate views
  • know about modifiers
  • be tightly coupled to other screens

Keep it clean, testable, and reusable.

🎯 2. Using the @observable Macro Correctly

This is the new recommended pattern:

@Observable
class ProfileViewModel {
    var name: String = ""
    var isLoading: Bool = false

    func loadProfile() async {
        isLoading = true
        defer { isLoading = false }

        // fetch...
    }
}

Benefits:

  • automatic change tracking
  • no need for @Published
  • works perfectly with NavigationStack
  • lightweight and simple

🔌 3. Service Injection (Clean & Testable)

Define service protocols:

protocol UserServiceProtocol {
    func fetchUser() async throws -> User
}

Provide a real implementation:

final class UserService: UserServiceProtocol {
    func fetchUser() async throws -> User {
        // API or local storage
    }
}

Inject into ViewModel:

@Observable
class ProfileViewModel {
    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol) {
        self.userService = userService
    }
}

This makes your screens fully testable.

🧪 4. Mocks for Testing (Huge Productivity Boost)

struct MockUserService: UserServiceProtocol {
    func fetchUser() async throws -> User {
        .init(id: 1, name: "Test User")
    }
}

Use in previews:

#Preview {
    ProfileView(viewModel: .init(userService: MockUserService()))
}

Zero API calls. Instant UI previews. Perfect for rapid design.

🧱 5. The Clean Feature Folder Structure

Features/
│
├── Profile/
│   ├── ProfileView.swift
│   ├── ProfileViewModel.swift
│   ├── ProfileService.swift
│   ├── ProfileModels.swift
│   └── ProfileTests.swift
│
├── Settings/
├── Home/
├── Explore/

Each feature contains:

  • View
  • ViewModel
  • Models
  • Services
  • Tests

Self-contained. Easy to move, refactor, or delete.

⚡ 6. State Derivation (Computed Logic, Not Stored State)

Avoid storing unnecessary state.

Bad ❌

var isValid: Bool = false

Good ✅

var isValid: Bool {
    !name.isEmpty && name.count > 3
}

Derived state should not be stored.

🔄 7. Async Patterns Without Messy State

Clean async loading:

func load() async {
    isLoading = true
    defer { isLoading = false }

    do {
        user = try await userService.fetchUser()
    } catch {
        errorMessage = error.localizedDescription
    }
}

This avoids callback hell and race conditions.

🧭 8. ViewModels + Navigation

Inject navigation through a Router:

@Observable
class Router {
    var path = NavigationPath()

    func push(_ route: Route) { path.append(route) }
    func pop() { path.removeLast() }
}

ViewModel triggers navigation cleanly:

router.push(.settings)

No view-side hacks. Pure MVVM.

♻️ 9. Reusable BaseViewModel (Optional but Powerful)

If several screens share loading/error patterns:

@Observable
class BaseViewModel {
    var isLoading = false
    var errorMessage: String?
}

Then:

class ProfileViewModel: BaseViewModel {
    // profile-specific logic
}

Keeps ViewModels DRY.

🧩 10. Example of a Real Production ViewModel

@Observable
class ProfileViewModel {
    var user: User?
    var isLoading = false
    var errorMessage: String?

    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol) {
        self.userService = userService
    }

    @MainActor
    func load() async {
        isLoading = true
        defer { isLoading = false }

        do {
            user = try await userService.fetchUser()
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    var initials: String {
        String(user?.name.prefix(1) ?? "?")
    }

    var hasProfile: Bool {
        user != nil
    }
}

This is real-world, clean, testable Swift.

🚀 Final Thoughts

A solid ViewModel architecture gives you:

  • predictable state
  • clean separation of concerns
  • massively easier testing
  • reusable features
  • smoother scaling
  • easier onboarding for collaborators

Now you have all the patterns to build scalable SwiftUI features with modern best practices.


This content originally appeared on DEV Community and was authored by Sebastien Lato


Print Share Comment Cite Upload Translate Updates
APA

Sebastien Lato | Sciencx (2025-12-01T02:20:48+00:00) Reusable ViewModel Architecture for SwiftUI. Retrieved from https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/

MLA
" » Reusable ViewModel Architecture for SwiftUI." Sebastien Lato | Sciencx - Monday December 1, 2025, https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/
HARVARD
Sebastien Lato | Sciencx Monday December 1, 2025 » Reusable ViewModel Architecture for SwiftUI., viewed ,<https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/>
VANCOUVER
Sebastien Lato | Sciencx - » Reusable ViewModel Architecture for SwiftUI. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/
CHICAGO
" » Reusable ViewModel Architecture for SwiftUI." Sebastien Lato | Sciencx - Accessed . https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/
IEEE
" » Reusable ViewModel Architecture for SwiftUI." Sebastien Lato | Sciencx [Online]. Available: https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/. [Accessed: ]
rf:citation
» Reusable ViewModel Architecture for SwiftUI | Sebastien Lato | Sciencx | https://www.scien.cx/2025/12/01/reusable-viewmodel-architecture-for-swiftui/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.