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
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
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:
print("Fetching data...")
// lots of stuff
print("Fetching data...")
After:
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:
let formatter = DateFormatter()
formatter.dateStyle = .medium
let dateString = formatter.string(from: Date())
After:
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:
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:
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:
func printIntArray(_ array: [Int]) {
array.forEach { print($0) }
}
func printStringArray(_ array: [String]) {
array.forEach { print($0) }
}
After:
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:
class UserViewModel {
func log() { print("UserViewModel log") }
}
class ProductViewModel {
func log() { print("ProductViewModel log") }
}
After:
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. 🙂