Lazy Properties in Swift - Why They Don’t Always Work in SwiftUI

April 11, 2026

Swift provides a powerful set of tools to manage initialization, performance, and memory. Among them, lazy properties stand out as a simple yet highly effective way to defer work until it is actually needed.

At first glance, lazy may look like a small keyword. In practice, it enables better performance, cleaner initialization patterns, and more predictable ownership-when used correctly.

This article explores lazy properties in depth: what they are, how they work, when to use them, and how they behave in real-world SwiftUI applications.


What is a Lazy Property?

A lazy stored property is a property whose initial value is not computed until it is accessed for the first time.

You declare it using the lazy keyword:

class DataLoader {
    lazy var data: [String] = {
        print("Loading data...")
        return ["Apple", "Banana", "Cherry"]
    }()
}

Key Idea

  • Regular stored properties → initialized during instance creation
  • Lazy stored properties → initialized on first access

Why Lazy Properties Matter

Lazy properties solve three common problems in production code.

1. Avoid Unnecessary Work

If a property is expensive to create but not always needed, lazy initialization prevents wasted computation.

class ImageProcessor {
    lazy var filter = createHeavyFilter()

    private func createHeavyFilter() -> String {
        print("Creating filter...")
        return "Filter Ready"
    }
}

If filter is never accessed, the work is never performed.

2. Improve Performance and Startup Time

Lazy properties defer work, which can:

  • Reduce initial load time
  • Improve responsiveness
  • Avoid blocking the main thread early

This is especially important in UI-heavy applications.

3. Enable Self-Dependent Initialization

Lazy properties can reference self, because they are initialized after the instance is fully created.

class User {
    let name: String

    lazy var greeting: String = {
        return "Hello, \(self.name)"
    }()

    init(name: String) {
        self.name = name
    }
}

This is not possible with regular stored properties.


How Lazy Properties Work

Conceptually, Swift implements lazy properties as:

  • A stored optional backing value
  • A one-time initialization closure
  • Cached result after first execution

Equivalent behavior:

private var _data: [String]? = nil

var data: [String] {
    if let value = _data {
        return value
    }
    let newValue = ["Apple", "Banana"]
    _data = newValue
    return newValue
}

Swift abstracts this pattern with the lazy keyword.


Syntax Variations

Simple Lazy Property

lazy var message = "Hello"

Lazy Property with Closure

lazy var formatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter
}()

Lazy Property Using a Function

lazy var data = loadData()

func loadData() -> [Int] {
    return [1, 2, 3]
}

Rules and Characteristics

Must Be Declared with var

lazy var value = 10   // ✅
lazy let value = 10   // ❌

Only Stored Properties Can Be Lazy

Computed properties cannot be lazy.

Initialized Only Once

let loader = DataLoader()

print(loader.data) // Initializes
print(loader.data) // Reuses cached value

Not Thread-Safe by Default

Lazy properties are not guaranteed to be thread-safe.

If accessed from multiple threads simultaneously, initialization may occur more than once.


Real-World Use Cases (SwiftUI + Production Guidance)

Lazy properties are often introduced as a performance optimization. In real-world code, their usefulness depends on lifecycle and ownership especially in SwiftUI.

1. Expensive Object Creation

struct ContentView: View {
    private lazy var formatter: DateFormatter = {
        print("Creating formatter...")
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter
    }()

    var body: some View {
        Text(formatter.string(from: Date()))
    }
}

Production Insight

This looks correct but in SwiftUI:

  • Views are value types
  • They can be recreated frequently

This means:

A lazy property inside a SwiftUI view is not guaranteed to behave like a persistent cache.

Above block of code wont compile, reason being:

Lazy properties require mutation on first access. Since SwiftUI views are structs and body is non-mutating, lazy properties cannot be used directly inside SwiftUI views.

Recommended Approach

Use state or object-backed storage:

struct ContentView: View {
    @State private var formatter = DateFormatter()

    var body: some View {
        Text(formatter.string(from: Date()))
    }
}

Or:

import Combine
class FormatterProvider: ObservableObject {
    lazy var formatter: DateFormatter = {
        print("Creating formatter...")
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter
    }()
}
struct ContentView: View {
    @StateObject private var provider = FormatterProvider()

    var body: some View {
        Text(provider.formatter.string(from: Date()))
    }
}

Why This Works

  • FormatterProvider is a class → lazy works correctly
  • @StateObject ensures stable lifecycle
  • Formatter is created only once

2. Deferring Heavy View Construction

struct DashboardView: View {
    private lazy var chartView: some View = {
        return ChartView(data: loadLargeDataset())
    }()

    var body: some View {
        VStack {
            Text("Dashboard")
            chartView
        }
    }
}

Production Insight

This pattern is misleading and wont compile.

SwiftUI already:

  • Lazily evaluates body
  • Optimizes rendering internally

You rarely need lazy for views themselves.

Better Pattern: Lazy Data, Not Lazy Views

struct DashboardView: View {
    @State private var data: [Int]? = nil

    var body: some View {
        VStack {
            if let data {
                ChartView(data: data)
            } else {
                ProgressView()
                    .onAppear {
                        data = loadLargeDataset()
                    }
            }
        }
    }

    private func loadLargeDataset() -> [Int] {
        return Array(0...10_000)
    }
}

This pattern works because it:

  • Uses persistent state (@State)
  • Triggers work at the right lifecycle moment (onAppear)
  • Keeps UI predictable and declarative

In SwiftUI, you don’t “delay initialization” with lazy.
You control state and lifecycle explicitly.

3. Dependency-Based Initialization

class NetworkManager {
    let baseURL: String

    lazy var fullURL: String = {
        return "\(baseURL)/api/v1"
    }()

    init(baseURL: String) {
        self.baseURL = baseURL
    }
}

This is the ideal use case for lazy:

A value that depends on self, is not needed immediately, and should be computed once and cached.

4. Caching Computed Results

class Fibonacci {
    lazy var result: [Int] = compute()

    private func compute() -> [Int] {
        print("Computing...")
        return [0, 1, 1, 2, 3, 5, 8]
    }
}

This pattern expresses intent directly:
> compute this value once, only when needed, and reuse it.

That’s what makes it:

  • Clean → minimal and readable
  • Efficient → avoids redundant work
  • Expressive → communicates behavior clearly

Lazy vs Computed Properties

Feature Lazy Property Computed Property
Storage Stored Not stored
Execution Once Every access
Performance Cached Recomputed
Use Case Expensive setup Dynamic values

Common Pitfalls

Retain Cycles

Closures can capture self strongly:

lazy var value: String = {
    return self.compute()
}()

Use [weak self] if needed.

Hidden Performance Cost

Lazy defers work but that cost still exists. If triggered on the main thread, it can cause UI delays.

Thread Safety

Lazy properties are not inherently safe in concurrent environments.


Best Practices

  • Prefer lazy in classes (reference types)
  • Use for expensive or optional initialization
  • Avoid in SwiftUI views unless necessary
  • Use for:
    • Formatters
    • Services
    • Cached computations

When NOT to Use Lazy

Avoid lazy when:

  • Value is cheap to compute
  • Always needed immediately
  • Requires strict thread safety
  • You rely on stable lifecycle in SwiftUI

Opinionated Guidelines (Production)

Use lazy when:

  • You want deferred + cached computation
  • Initialization depends on self
  • You are inside a class with stable identity

Avoid lazy when:

  • You are inside SwiftUI struct views
  • You need predictable lifecycle guarantees
  • You are optimizing prematurely

Final Thoughts

Lazy properties are often misunderstood as a micro-optimization. In reality, they are a lifecycle and ownership tool.

Used correctly, they help you:

  • Defer meaningful work
  • Improve performance
  • Simplify initialization logic
  • Used incorrectly, they can introduce subtle bugs-especially in SwiftUI.

The goal is not to use lazy everywhere, but to use it where lifecycle control matters.

If you have suggestions, feel free to connect with me on X and send me a DM. If this article helped you, Buy me a coffee.