Clean, Reusable Swift Code Using DRY Principle

Hey Switfers! đź‘‹
Today we will explore the DRY principle to make our Swift code clean and reusable. So have you ever written a function, copy-pasted it somewhere else, tweaked a line or two, and thought, this looks kinda familiar? If so, you have probably broken a rule we all learn (and forget) early on The DRY Principle.

Okay, So What is DRY?

DRY stands for:

Don’t Repeat Yourself

It’s a programming principle that encourages you to avoid duplicating logic. Instead, reuse your code through functions, extensions, generics, or other Swift tools. Why? Because duplicated code is harder to maintain, it increases the risk of bugs and makes your codebase messy and harder to read. Let’s see how to write DRY Swift code with simple examples you can actually use in your app today.

The Problem

Here’s a common beginner mistake

Swift
func greetUser(name: String) {
    print("Hello, \(name)!")
}

func welcomeUser(name: String) {
    print("Hello, \(name)!")
    print("Welcome to our app.")
}

These two functions have duplicated logic. What happens when you want to change how “Hello” is printed? You would need to update it in both places and thats not DRY.

The DRY Version

Swift
func greet(name: String) {
    print("Hello, \(name)!")
}

func welcomeUser(name: String) {
    greet(name: name)
    print("Welcome to our app.")
}

Now the greeting is in one place. If you ever want to update it (or say localize it), you only change one function. And thats already clean and maintainable.

DRY Tools in Swift

Let’s take it a step further. Swift gives you some awesome tools to help keep your code DRY:

1. Functions

Functions is one of the great ways to help you conform to the DRY concept. Does your code have any repeated logic? Wrap it in a function. Here is a brief before and after example:

Before:

Swift
print("Fetching data...")
// lots of stuff
print("Fetching data...")

After:

Swift
func showLoadingMessage() {
    print("Fetching data...")
}

showLoadingMessage()
// lots of stuff
showLoadingMessage()

2. Extensions: Add functionality without subclassing

Let’s say you keep formatting dates the same way everywhere. Here is a quick example of how you can transform that using DRY code:

Before:

Swift
let formatter = DateFormatter()
formatter.dateStyle = .medium
let dateString = formatter.string(from: Date())

After:

Swift
extension Date {
    /// Formats the date as a localized "Medium" style: e.g. "Jun 5, 2025"
    func formattedMedium() -> String {
        self.formatted(.dateTime.year().month().day())
    }
}

// Usage:
let dateString = Date().formattedMedium()

Again, as you can see it more clean and can be used through your codebase.

3. Computed Properties

Instead of writing the same logic again and again this is how you can use computed properties to make your logic reusable:

Before:

Swift
struct User {
    let age: Int
}

// Later in code:

let user = User(age: 12)

if user.age > 18 {
    // do adult-specific logic
}

// Somewhere else:
guard user.age > 18 else {
    print("Not an adult.")
    return
}

After:

Swift
struct User {
    let age: Int
    
    var isAdult: Bool {
        age > 18
    }
}

// Usage:

let user = User(age: 19)
 
if user.isAdult {
    // do adult-specific logic
}

guard user.isAdult else {
    print("Not an adult.")
    return
}

4. Generics

If you’re repeating logic for multiple types, generics might be your bet.

Before:

Swift
func printIntArray(_ array: [Int]) {
    array.forEach { print($0) }
}

func printStringArray(_ array: [String]) {
    array.forEach { print($0) }
}

After:

Swift
func printArray<T>(_ array: [T]) {
    array.forEach { print($0) }
}

// Usage:
printArray([1, 2, 3])     // prints Ints
printArray(["a", "b", "c"]) // prints Strings

As you can see, one generic function handles it all.

5. Protocol Extensions

This is like combining generics + extensions which is extremely powerful. Let’s say multiple view models need a log() function.

Before:

Swift
class UserViewModel {
    func log() { print("UserViewModel log") }
}

class ProductViewModel {
    func log() { print("ProductViewModel log") }
}

After:

Swift
protocol Loggable {
    func log()
}

extension Loggable {
    func log() {
        print("\(Self.self) log")
    }
}

class UserViewModel: Loggable {}
class ProductViewModel: Loggable {}

let userVM = UserViewModel()
userVM.log() // Prints: UserViewModel log

Now all conforming types get a default log() function and you can override it if needed.

That All Seems Great But When to Avoid DRY

Yep, even DRY has limits. If the logic looks similar but serves different purposes, keeping them separate might be clearer. Ask yourself this:

  • Would merging this make the code harder to understand?
  • Are these things likely to change independently in the future?

Sometimes repeating 2 lines is better than overengineering.

Well that was all about how you can tranform your Swift codebase using DRY principle. I hope you liked this one and i will see you in the next one. 🙂


Posted

in

, , , , ,