Centered text displaying Swift property wrapper guide with a black gradient over a blurred developer workspace.

Mastering Swift Property Wrappers for Clean Code

We are building an iOS project and we hit a common wall. We have dozens of variables. Every time we assign data to them or read from them, we want to step in and execute standardized logic.

Let us assume we are coding a game character. This character has attributes like health, energy, and volume.

Our strict business rule is simple: These values must never exceed 100, and they must never drop below 0.

1. The Exhausting Legacy Method (Without @propertyWrapper)

If we did not utilize modern Swift features, we would be forced to sit down and write manual validation code for every single variable. Our codebase would devolve into this:

class Player {
    // 1. For Health:
    private var _health: Int = 100
    var health: Int {
        get { return _health }
        set {
            if newValue > 100 { _health = 100 }
            else if newValue < 0 { _health = 0 }
            else { _health = newValue }
        }
    }

    // 2. For Energy => MASSIVE CODE DUPLICATION!
    private var _energy: Int = 100
    var energy: Int {
        get { return _energy }
        set {
            if newValue > 100 { _energy = 100 }
            else if newValue < 0 { _energy = 0 }
            else { _energy = newValue }
        }
    }

    // 3. For Volume => MASSIVE CODE DUPLICATION!
    private var _volume: Int = 50
    var volume: Int {
        get { return _volume }
        set {
            if newValue > 100 { _volume = 100 }
            else if newValue < 0 { _volume = 0 }
            else { _volume = newValue }
        }
    }
}

Now imagine doing this for 50 different variables.

  • First, our codebase becomes incredibly polluted.
  • Second, if product requirements change tomorrow and the team says, “The limit should be 200 instead of 100,” we have to manually sit down and refactor 50 different locations.

In software architecture, this is a scenario we absolutely hate.

2. The Clean Code Method (Using @propertyWrapper)

In modern Swift, we say: Let us extract that repeating if else logic from the set block, strip it out completely, and encapsulate it inside a dedicated container named @Clamped.

If you are setting up an iOS static framework architecture, understanding this type of data isolation is crucial for keeping your modules clean.

Step A: Moving Logic into the Wrapper

We bring our boundary logic directly into a reusable struct exactly as it is:

@propertyWrapper
struct Clamped {
    private var value: Int
    private let min: Int
    private let max: Int

    init(wrappedValue: Int, min: Int, max: Int) {
        self.min = min
        self.max = max
        
        // Enforcing boundaries right at initialization
        if wrappedValue > max { self.value = max }
        else if wrappedValue < min { self.value = min }
        else { self.value = wrappedValue }
    }
    
    var wrappedValue: Int {
        get { return value }
        set {
            // --- OUR BUSINESS LOGIC IS NOW CENTRALIZED HERE ---
            if newValue > max { value = max }
            else if newValue < min { value = min }
            else { value = newValue }
        }
    }
}

Step B: The Clean Implementation

We are now ready to implement. We can throw away all those long private var _volume, get, set, and if else blocks we wrote earlier. Consequently, the inside of our Class becomes impeccably clean:

struct Player {
    // We invoke all the architectural logic with a single tag!
    @Clamped(min: 0, max: 100) var health: Int = 100
    @Clamped(min: 0, max: 100) var energy: Int = 50
}

Industry Use Cases: The @UserDefault Wrapper

In a real enterprise project, we want to skip the manual labor. We do not want to deal with writing to the database, reading from it, tracking string keys, or handling default values constantly. The moment we define a variable, it should be written to memory.

However, doing this professionally requires strict adherence to Testability and Dependency Injection (DI) rules. If you are serious about managing dependencies and IoC in Swift, you know we cannot tightly couple our wrapper directly to UserDefaults. First, we write a protocol.

1. Writing a DI-Compliant Wrapper

By utilizing Generics (<T>), we ensure our wrapper works with any data type. Furthermore, the KeyValueStore protocol allows us to use Mock databases while testing this code without corrupting the actual device memory.

import Foundation

// Abstraction (DI Contract)
public protocol KeyValueStore {
    func object(forKey defaultName: String) -> Any?
    func set(_ value: Any?, forKey defaultName: String)
}

extension UserDefaults: KeyValueStore {}

@propertyWrapper
public struct UserDefault<T: Codable> {
    let key: String
    let defaultValue: T
    let container: KeyValueStore
    
    // Making encoders static for performance (Prevents recreation)
    private static let encoder = JSONEncoder()
    private static let decoder = JSONDecoder()

    public init(key: String, defaultValue: T, container: KeyValueStore = UserDefaults.standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.container = container
    }

    public var wrappedValue: T {
        get {
            // 1. Try reading as a Native (Primitive) type first
            if let value = container.object(forKey: key) as? T {
                return value
            }
            
            // 2. If that fails, fetch as Data and attempt to Decode (Custom Object)
            guard let data = container.object(forKey: key) as? Data else {
                return defaultValue
            }
            
            do {
                let value = try Self.decoder.decode(T.self, from: data)
                return value
            } catch {
                #if DEBUG
                print("UserDefault Decode Error for key: \(key) -> \(error)")
                #endif
                return defaultValue
            }
        }
        set {
            // 1. Nil check (Assigning nil in Generics is tricky, we use a helper)
            if let optional = newValue as? AnyOptional, optional.isNil {
                container.set(nil, forKey: key)
                return
            }
            
            // 2. Are these natively storable types? (Date and Data included!)
            if newValue is Int || newValue is String || newValue is Double || newValue is Bool || newValue is Date || newValue is Data {
                container.set(newValue, forKey: key)
            } else {
                // 3. If it is a Custom Object, Encode it
                do {
                    let data = try Self.encoder.encode(newValue)
                    container.set(data, forKey: key)
                } catch {
                    #if DEBUG
                    print("UserDefault Encode Error for key: \(key) -> \(error)")
                    #endif
                }
            }
        }
    }
}

// Helper used to understand if a generic type T is 'nil'
private protocol AnyOptional { var isNil: Bool { get } }
extension Optional: AnyOptional { var isNil: Bool { self == nil } }

2. Real-World Application (UserManager)

The era of writing UserDefaults.standard.set(...) everywhere is over. We gather all settings cleanly into a single Storage or UserManager file:

enum Storage {
    @UserDefault(key: "has_seen_onboarding", defaultValue: false)
    static var hasSeenOnboarding: Bool
    
    @UserDefault(key: "auth_token", defaultValue: "")
    static var token: String
    
    @UserDefault(key: "app_theme", defaultValue: "light")
    static var theme: String
}

What exactly are we declaring with this structure?

By writing our code this way, we are giving the system two very distinct commands:

  • @UserDefault(...): “Do not hold this data in RAM. Write it directly to persistent memory (the Container). And for the key, use the exact string I provided here.”
  • static var ... : Bool: “I refuse to deal with manual keys or repetitive get/set functions in the rest of my codebase. When I call Storage.hasSeenOnboarding, just hand me a direct true or false.”

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *