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.
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"]
}()
}Lazy properties solve three common problems in production code.
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.
Lazy properties defer work, which can:
This is especially important in UI-heavy applications.
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.
Conceptually, Swift implements lazy properties as:
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.
lazy var message = "Hello"lazy var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()lazy var data = loadData()
func loadData() -> [Int] {
return [1, 2, 3]
}varlazy var value = 10 // ✅
lazy let value = 10 // ❌Computed properties cannot be lazy.
let loader = DataLoader()
print(loader.data) // Initializes
print(loader.data) // Reuses cached valueLazy properties are not guaranteed to be thread-safe.
If accessed from multiple threads simultaneously, initialization may occur more than once.
Lazy properties are often introduced as a performance optimization. In real-world code, their usefulness depends on lifecycle and ownership especially in SwiftUI.
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:
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 lifecyclestruct 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:
bodyYou rarely need
lazyfor 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:
@State)onAppear)In SwiftUI, you don’t “delay initialization” with lazy.
You control state and lifecycle explicitly.
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.
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:
| Feature | Lazy Property | Computed Property |
|---|---|---|
| Storage | Stored | Not stored |
| Execution | Once | Every access |
| Performance | Cached | Recomputed |
| Use Case | Expensive setup | Dynamic values |
Closures can capture self strongly:
lazy var value: String = {
return self.compute()
}()Use [weak self] if needed.
Lazy defers work but that cost still exists. If triggered on the main thread, it can cause UI delays.
Lazy properties are not inherently safe in concurrent environments.
lazy in classes (reference types)Avoid lazy when:
selfstruct viewsLazy properties are often misunderstood as a micro-optimization. In reality, they are a lifecycle and ownership tool.
Used correctly, they help you:
The goal is not to use
lazyeverywhere, 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.