Rules to write Clean Code in Swift

In this post we will focusfocus on showing how principles like KISS, DRY, and SOLID directly improve Swift code in everyday situations—such as structuring a ViewModel, reducing boilerplate in SwiftUI views, or designing protocols for networking.

In this post we will revieew some roles principles by examples in Swift. This is could be considerered a continuation of a past post called 'S.O.L.I.D. principles in Swift'

Clean Code

Clean code is code that is simple to read, easy to understand, and straightforward to maintain. It avoids unnecessary complexity by following principles like clarity, consistency, and separation of concerns, ensuring that each part of the code has a clear purpose. Clean code favors meaningful names, small and focused functions, and eliminates duplication, making it easier for other developers (and your future self) to work with. The goal is not just to make the computer run the program, but to make the code itself communicate its intent clearly to humans.

Clean Code is the overarching goal of writing software that is easy to read, understand, and maintain, while the SOLID principles are a concrete set of guidelines that help achieve this goal by structuring classes and dependencies in a clear, flexible way. In essence, Clean Code defines what good code should feel like, and SOLID offers how to design object-oriented code that stays true to that vision, ensuring maintainability, scalability, and clarity.

SOC - Separation Of Concepts

Separation of Concerns (SoC) is a clean code principle that says each part of your code should focus on a single, well-defined responsibility. By splitting concerns, you reduce complexity, improve readability, and make code easier to maintain and test. In iOS/Swift, this often means separating data models, networking logic, business logic (ViewModel), and UI (View) instead of mixing them in one place.

import SwiftUI
import Foundation

// MARK: - Model (data only)
struct Post: Decodable, Identifiable {
    let id: Int
    let title: String
}

// MARK: - Networking (concern: fetching data)
final class PostService {
    func fetchPosts() async throws -> [Post] {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Post].self, from: data)
    }
}

// MARK: - ViewModel (concern: state & business logic)
@MainActor
final class PostListViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var errorMessage: String?

    private let service = PostService()

    func loadPosts() async {
        do {
            posts = try await service.fetchPosts()
        } catch {
            errorMessage = "Failed to load posts"
        }
    }
}

// MARK: - View (concern: UI)
struct PostListView: View {
    @StateObject private var vm = PostListViewModel()

    var body: some View {
        NavigationView {
            List(vm.posts) { post in
                Text(post.title)
            }
            .navigationTitle("Posts")
            .task { await vm.loadPosts() }
        }
    }
}

Where’s the SoC?

  • Model (Post) → defines the data only.

  • Service (PostService) → knows how to fetch posts from the network.

  • ViewModel (PostListViewModel) → manages state and error handling.

  • View (PostListView) → displays UI and reacts to state changes.

Each layer does one thing well, instead of mixing networking, logic, and UI together.

DRY - Do not repeat yourself

DRY (Don’t Repeat Yourself) is a clean code principle that says you should avoid duplicating logic across your codebase. Instead, extract common behavior into reusable functions, extensions, or abstractions. This reduces errors, makes updates easier, and improves maintainability.

❌ Without DRY (duplicated logic)

struct User {
    let name: String
    let email: String
}

struct Product {
    let title: String
    let price: Double
}

func printUser(_ user: User) {
    print("----")
    print("Name: \(user.name)")
    print("Email: \(user.email)")
    print("----")
}

func printProduct(_ product: Product) {
    print("----")
    print("Title: \(product.title)")
    print("Price: \(product.price)")
    print("----")
}

The logic for printing the separators (----) and formatting output is repeated in both functions.

✅ With DRY (reusable abstraction)

protocol Printable {
    var descriptionLines: [String] { get }
}

struct User: Printable {
    let name: String
    let email: String
    
    var descriptionLines: [String] {
        [
            "Name: \(name)",
            "Email: \(email)"
        ]
    }
}

struct Product: Printable {
    let title: String
    let price: Double
    
    var descriptionLines: [String] {
        [
            "Title: \(title)",
            "Price: \(price)"
        ]
    }
}

// Generic reusable function
func printEntity(_ entity: Printable) {
    print("----")
    entity.descriptionLines.forEach { print($0) }
    print("----")
}

// Usage
let user = User(name: "Alice", email: "alice@email.com")
let product = Product(title: "iPhone", price: 999.99)

printEntity(user)
printEntity(product)

Now, the separator and printing logic exist in one place (printEntity). If we want to change the format, we update it once, not in every function.

KISS-Keep it simple stupid

KISS (Keep It Simple, Stupid) is a clean code principle that emphasizes writing code in the simplest way possible, avoiding unnecessary complexity or over-engineering. The goal is clarity: solve the problem directly instead of anticipating extra cases you don’t need yet.

❌ Not KISS (too complex for a simple counter)

import SwiftUI

struct ComplexCounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.largeTitle)
            
            HStack {
                Button("Increment") { updateCount(by: 1) }
                Button("Decrement") { updateCount(by: -1) }
            }
        }
    }
    
    // Over-abstracted logic for a simple task
    private func updateCount(by value: Int) {
        let newValue = count + value
        if newValue >= 0 {
            count = newValue
        } else {
            count = 0
        }
    }
}

Too much abstraction (updateCount) and rules for a trivial counter.


✅ KISS (straightforward and clear)

import SwiftUI

struct SimpleCounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.largeTitle)
            
            HStack {
                Button("−") { if count > 0 { count -= 1 } }
                Button("+") { count += 1 }
            }
        }
    }
}

The code is direct, easy to read, and does only what’s needed.

DYC-Document your code

DYC (Document Your Code) is a clean code principle that emphasizes writing code that is self-explanatory whenever possible, but also adding clear, concise documentation when something isn’t obvious. In Swift, this is often done with /// doc comments, which Xcode uses to show inline documentation when hovering over symbols. The idea is to make the code not just work, but also easy to understand and use for other developers (or your future self).

✅ Example in Swift using DYC

import Foundation

/// Represents a user in the system.
struct User {
    /// Unique identifier for the user.
    let id: Int
    
    /// The user's display name.
    let name: String
    
    /// The user's email address.
    let email: String
}

/// Protocol defining operations to fetch users.
protocol UserRepository {
    /// Fetches all available users from a data source.
    /// - Returns: An array of `User` objects.
    /// - Throws: An error if fetching fails.
    func fetchUsers() async throws -> [User]
}

/// Remote implementation of `UserRepository` that calls an API.
final class RemoteUserRepository: UserRepository {
    private let baseURL: URL
    
    /// Creates a new repository with the given API base URL.
    /// - Parameter baseURL: The base URL of the remote API.
    init(baseURL: URL) {
        self.baseURL = baseURL
    }
    
    /// Calls the `/users` endpoint and decodes the response into a list of `User`.
    /// - Throws: `URLError` if the network call fails or the response is invalid.
    /// - Returns: An array of users retrieved from the server.
    func fetchUsers() async throws -> [User] {
        let url = baseURL.appendingPathComponent("users")
        let (data, response) = try await URLSession.shared.data(from: url)
        
        guard (response as? HTTPURLResponse)?.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        
        return try JSONDecoder().decode([User].self, from: data)
    }
}

Why this follows DYC

  • Each type and method has a short description of what it does.

  • Methods document parameters, return values, and possible errors.

  • Xcode can show these comments in Quick Help (Option + Click) for better developer experience.

YAGNI-You're not going to need it

YAGNI (You Aren’t Gonna Need It) is a clean code principle that reminds developers not to add functionality until it’s actually needed. Writing “just in case” code leads to complexity, unused features, and harder maintenance. Instead, implement only what the current requirements demand, and extend later when there’s a real need.


❌ Violating YAGNI (adding unnecessary features)

import SwiftUI

struct OverEngineeredCounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.largeTitle)
            
            HStack {
                Button("Increment") { updateCount(by: 1) }
                Button("Decrement") { updateCount(by: -1) }
                Button("Reset") { resetCount() }          // Not needed (yet)
                Button("Multiply ×2") { multiplyCount() } // Not needed (yet)
            }
        }
    }
    
    private func updateCount(by value: Int) {
        count += value
    }
    
    private func resetCount() { count = 0 }
    private func multiplyCount() { count *= 2 }
}

Only increment and decrement are required, but reset and multiply were added “just in case.”


✅ Following YAGNI (keep it simple until needed)

import SwiftUI

struct SimpleCounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.largeTitle)
            
            HStack {
                Button("−") { count -= 1 }
                Button("+") { count += 1 }
            }
        }
    }
}

The code contains only the features needed today. If requirements change, new functionality can be added later.

LoD-Law of Demeter

The Law of Demeter (LoD), also called the principle of least knowledge, says:
A unit of code should only talk to its immediate collaborators (“friends”), not to the internals of those collaborators (“friends of friends”)’.

In practice, this means avoiding long chains like order.customer.address.city, because it creates tight coupling and makes the code fragile if internal structures change. Instead, each object should expose only what is necessary.

❌ Violating the Law of Demeter

struct Address {
    let city: String
}

struct Customer {
    let address: Address
}

struct Order {
    let customer: Customer
}

func printOrderCity(order: Order) {
    // ❌ Too many “hops” (order → customer → address → city)
    print(order.customer.address.city)
}

If the internal structure of Customer or Address changes, this function breaks.


✅ Following the Law of Demeter

struct Address {
    let city: String
}

struct Customer {
    private let address: Address
    
    init(address: Address) {
        self.address = address
    }
    
    /// Expose only what’s needed
    func getCity() -> String {
        return address.city
    }
}

struct Order {
    private let customer: Customer
    
    init(customer: Customer) {
        self.customer = customer
    }
    
    /// Expose only what’s needed
    func getCustomerCity() -> String {
        return customer.getCity()
    }
}

func printOrderCity(order: Order) {
    // ✅ Only talks to Order, not Order’s internals
    print(order.getCustomerCity())
}

Now printOrderCity only knows about Order. The details of Customer and Address are hidden, so changes in those types won’t ripple outward.

Lesson:

  • ❌ Don’t chain through multiple objects (a.b.c.d).

  • ✅ Provide methods that expose the needed behavior directly.

Conclusions

We have demonstrated how the Clean principles align with Swift, so there is no excuse for not applying them. 

Copyright © 2024-2025 JaviOS. All rights reserved