Try it free
Write faster with AI writing tools
WriterPilots gives you 10+ free AI writing tools — blog writer, email writer, paraphraser, grammar fixer and more. Free to try, no credit card needed.
Try Blog Writer → Paraphraser Summarizer Grammar Fixer
AI Writing Tools

Refactoring a Messy SwiftUI View into Clean, Production-Ready Code

March 22, 2026 · 6 min read · 17 views

Refactoring a Messy SwiftUI View into Clean, Production-Ready Code

Many SwiftUI examples you find start with everything in one file: networking code, decoding, state variables, business logic and UI all mashed together. That style might be OK for a playground or quick POC, but it becomes brittle and hard to maintain in production.

This article demonstrates a disciplined refactor: identify what's wrong in a typical single-file SwiftUI view, then transform it into a clean, testable, and reusable structure emphasizing readability, separation of concerns, naming, and reusability—without changing functionality.

The (Simplified) Problematic Starting Point

Here's a representative compact example of a view that works but mixes concerns. It fetches items from a JSON endpoint and displays them in a list.

// BEFORE — Single file, mixed concerns
import SwiftUI
struct ContentView: View {
@State private var items: [Item] = []
@State private var isLoading = false
@State private var errorMessage: String? = nil
var body: some View {
NavigationView {
List(items) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
}
}
.navigationTitle("Items")
.toolbar { Button("Refresh") { fetch() } }
.overlay { if isLoading { ProgressView() } }
.alert(item: $errorMessage) { msg in
Alert(title: Text("Error"), message: Text(msg), dismissButton: .default(Text("OK")))
}
}
.onAppear { fetch() }
}
func fetch() {
guard let url = URL(string: "https://example.com/items.json") else { return }
isLoading = true
errorMessage = nil
URLSession.shared.dataTask(with: url) { data, res, err in
DispatchQueue.main.async {
isLoading = false
if let err = err { errorMessage = err.localizedDescription; return }
guard let data = data else { errorMessage = "No data"; return }
do {
items = try JSONDecoder().decode([Item].self, from: data)
} catch {
errorMessage = "Decoding error"
}
}
}.resume()
}
}
struct Item: Identifiable, Codable {
let id: Int
let title: String
let subtitle: String
}
What's wrong with this implementation?
  • Mixed concerns: Networking and decoding happen in the view. This makes the view heavy and hard to test.
  • Poor naming and structure: Generic names like fetch() and using raw strings (URL) inline increase fragility.
  • Old concurrency style: Using URLSession.dataTask + DispatchQueue.main.async instead of structured concurrency (async/await) is error-prone and harder to reason about.
  • Error handling and state: The view is responsible for all UI state (loading, error), which should be the view model's job.
  • Testability: Hard to unit test networking when it's embedded directly in the view with concrete URLSession.shared.

Refactor Goals

  • Separate networking, decoding, and UI concerns.
  • Introduce a ViewModel (MVVM) to own state and business logic.
  • Use async/await for clearer concurrency.
  • Abstract networking via a protocol for easier testing and swapping implementations.
  • Improve naming and reusability of small UI components.

Refactored Structure (What It Looks Like)

We'll end up with these pieces:

  • Item — model (same as before, maybe moved to its own file)
  • ItemsServiceProtocol & ItemsAPIService — networking abstraction
  • ItemsViewModel — ObservableObject, owns state and logic
  • ItemsView — small, declarative SwiftUI view composed from components
// AFTER — Clean, testable MVVM + service abstraction
import SwiftUI
// MARK: - Model
struct Item: Identifiable, Codable {
let id: Int
let title: String
let subtitle: String
}
// MARK: - Networking Abstraction
protocol ItemsServiceProtocol {
func fetchItems() async throws -> [Item]
}
struct ItemsAPIService: ItemsServiceProtocol {
private let url: URL
init(url: URL = URL(string: "https://example.com/items.json")!) { self.url = url }
func fetchItems() async throws -> [Item] {
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Item].self, from: data)
}
}
// MARK: - ViewModel
@MainActor
final class ItemsViewModel: ObservableObject {
@Published private(set) var items: [Item] = []
@Published var isLoading = false
@Published var alertMessage: String? = nil
private let service: ItemsServiceProtocol
init(service: ItemsServiceProtocol) {
self.service = service
}
func load() async {
isLoading = true
alertMessage = nil
do {
let fetched = try await service.fetchItems()
items = fetched
} catch {
alertMessage = error.localizedDescription
}
isLoading = false
}
func refresh() async {
await load()
}
}
// MARK: - View
struct ItemsView: View {
@StateObject private var viewModel: ItemsViewModel
init(viewModel: ItemsViewModel = ItemsViewModel(service: ItemsAPIService())) {
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
NavigationView {
Group {
if viewModel.items.isEmpty && viewModel.isLoading {
ProgressView("Loading…")
} else {
List(viewModel.items) { item in
ItemRow(item: item)
}
.refreshable {
await viewModel.refresh()
}
}
}
.navigationTitle("Items")
.toolbar { Button("Refresh") { Task { await viewModel.refresh() } } }
.alert(item: $viewModel.alertMessage) { msg in
Alert(title: Text("Error"), message: Text(msg), dismissButton: .default(Text("OK")))
}
.task { await viewModel.load() }
}
}
}
// Small reusable UI component
struct ItemRow: View {
let item: Item
var body: some View {
VStack(alignment: .leading) {
Text(item.title).font(.headline)
Text(item.subtitle).font(.subheadline).foregroundColor(.secondary)
}
.padding(.vertical, 6)
}
}

What I Improved and Why

  • Separation of concerns: Networking moved to ItemsAPIService. The view no longer performs network calls.
  • Testability: The service is behind ItemsServiceProtocol. In tests you can inject a mock service that returns deterministic data.
  • Clear responsibilities: The ItemsViewModel owns all state (items, loading, error) and exposes intent methods (load(), refresh()).
  • Modern concurrency: async/await replaces completion handlers and manual DispatchQueue hops—this simplifies flow and removes potential threading bugs.
  • Nicer UI composition: Small, focused components like ItemRow are easier to style, reuse and test.
  • Better naming: Names are explicit (e.g., ItemsViewModel, ItemsAPIService, fetchItems()), improving discoverability.
  • Error handling: Errors are surfaced through the view model and localized via error.localizedDescription—the view just renders UI for those states.
  • Dependency injection: The default initializer uses the real service, but callers can inject a mock for testing or previewing.

Additional Notes & Best Practices

  • Mark view models @MainActor so published state is updated on the main thread automatically.
  • Prefer immutable or private(set) published properties where appropriate—only expose what needs to be mutated externally.
  • Keep views declarative and free from side effects; move side effects into the view model and call them via .task or explicit user intents.
  • Use protocols for external dependencies (networking, persistence) to make unit testing straightforward.
  • Break UI into small components—views should be easy to reason about and recomposition-friendly.
  • Use Swift's error types (custom errors) to provide better, localized messages to the user.

Wrap-up

The refactor shown preserves the original behavior (fetch items, show loading and errors, list items and provide refresh) while making the codebase far more maintainable, testable, and readable. Production-quality SwiftUI code values structure and clear responsibility boundaries as much as concise UI code. Adopt MVVM, abstract external dependencies, and favor Swift's structured concurrency to deliver robust apps with minimal cognitive overhead.

If you'd like, I can take your actual code file and refactor it directly—paste it and I will output a focused, production-ready refactor with tests and a preview configuration.

Related Posts