Swift: Deep Dive into Swift Properties

Properties associate values with Swift types. Beyond basic stored properties, Swift offers computed properties, property observers, lazy loading, and other powerful features that give you precise control over property read and write behavior.

1. What You'll Learn


2. A Real-World Mobile Developer Story

(1) Pain Point: User Avatar Downloads from Network on Every Display

Bob implemented a user profile page that fetches the avatar from the network every time it is displayed:

SWIFT
class UserProfile {
    var avatarUrl: String
    func displayAvatar() {
        // Downloads avatar from network every time
        downloadImage(from: avatarUrl) { image in
            // Display image
        }
    }
}

Every time the profile is opened, the avatar is downloaded again. If the user navigates in and out repeatedly, the same network request executes over and over, wasting bandwidth and loading slowly.

(2) Solution: Lazy Loading + Computed Properties

SWIFT
class UserProfile {
    var avatarUrl: String
    lazy var cachedAvatar: UIImage? = {
        // Executes only once — downloads on first access
        return downloadImage(from: avatarUrl)
    }()
}

Combined with computed properties for real-time calculation, lazy loading runs only once — subsequent accesses return the cached value directly.

(3) Benefit: On-Demand Loading, Automatic Caching

Dimension Direct Download Lazy Loading
Loading timing Every access First access
Duplicate requests Downloads every time Downloads once
Memory usage Loaded even when not displayed Allocated on demand
Startup speed Slow (preloads irrelevant data) Fast (deferred initialization)
Code complexity Manual cache management Swift manages automatically

3. Stored Properties and Computed Properties

(1) Stored Properties

A stored property is a constant or variable stored as part of a class or struct instance:

100%
graph TB
    A[Property Types] --> B[Stored Property]
    A --> C[Computed Property]
    B --> D["let constant stored property"]
    B --> E["var variable stored property"]
    C --> F["getter — computed on read"]
    C --> G["setter — processed on write"]
Feature Stored Property Computed Property
Memory Consumes instance memory Does not consume instance memory
Read/Write Direct read/write Via getter/setter
Type let/var var only
Observers Supports willSet/didSet Not supported
Initialization Must be initialized or assigned in init No initialization needed

▶ Example: Stored Properties

SWIFT
// ============================================
// Basic stored property usage
// ============================================
struct User {
    let id: Int           // Constant stored property
    var name: String       // Variable stored property
    var email: String
}
var user = User(id: 1, name: "Alice", email: "alice@example.com")
user.name = "Alice Smith"  // Variable property can be modified
// user.id = 2             // Compile error! Constant property cannot be modified
print("\(user.name) (\(user.email))")

Output:

TEXT 📖 Display only
Alice Smith (alice@example.com)

(2) Computed Properties

A computed property does not store a value; it computes and returns a value every time it is accessed:

SWIFT
// ============================================
// Computed property — Celsius/Fahrenheit conversion
// ============================================
struct Temperature {
    var celsius: Double
    // Computed property — calculates fahrenheit from celsius
    var fahrenheit: Double {
        get {
            return celsius * 9 / 5 + 32
        }
        set(newFahrenheit) {
            celsius = (newFahrenheit - 32) * 5 / 9
        }
    }
}
var temp = Temperature(celsius: 25)
print("Celsius: \(temp.celsius)°C")
print("Fahrenheit: \(temp.fahrenheit)°F")
// Setting fahrenheit indirectly modifies celsius
temp.fahrenheit = 100
print("After setting to 100°F:")
print("Celsius: \(temp.celsius)°C")

Output:

TEXT 📖 Display only
Celsius: 25°C
Fahrenheit: 77.0°F
After setting to 100°F:
Celsius: 37.77777777777778°C

Tip: Read-only computed properties can omit get and the braces: var doubled: Int { value * 2 }.


4. Property Observers

Property observers monitor changes to a stored property, triggering before and after the value changes.

Observer Timing Parameter Common Uses
willSet Before the value is stored newValue Pre-update validation, logging
didSet After the value is stored oldValue UI updates, data sync

▶ Example: willSet and didSet

SWIFT
// ============================================
// Property observers monitoring score changes
// ============================================
class Player {
    var name: String
    var score: Int = 0 {
        willSet {
            print("Score will change from \(score) to \(newValue)")
        }
        didSet {
            print("Score changed from \(oldValue) to \(score)")
            if score > 100 {
                print("Congrats \(name) on the high score!")
            }
        }
    }
    var level: Int {
        // Read-only computed property based on score
        switch score {
        case 0..<50: return 1
        case 50..<100: return 2
        default: return 3
        }
    }
    init(name: String) {
        self.name = name
    }
}
let player = Player(name: "Alice")
player.score = 60
print("Level: \(player.level)")
player.score = 120
print("Level: \(player.level)")

Output:

TEXT 📖 Display only
Score will change from 0 to 60
Score changed from 0 to 60
Score will change from 60 to 120
Score changed from 60 to 120
Congrats Alice on the high score!
Level: 3

Warning: Property observers cannot be used on computed properties, since computed properties have no stored value to "observe." Do not set the same property inside willSet or didSet — it will cause an infinite loop.


5. Lazy Loading and Type Properties

(1) lazy Loading

A lazy var is initialized only when the property is first accessed, ideal for properties that are expensive to create or not always needed:

100%
graph LR
    A["Declare lazy var property"] --> B["Instance created"]
    B --> C["property is uninitialized"]
    C --> D["First access to property"]
    D --> E["Execute initialization closure"]
    E --> F["Subsequent accesses: return cached value"]
Scenario Non-lazy Lazy
Initialization timing On instance creation On first access
Large file reading Loaded whether used or not Loaded only when needed
Network requests Preloaded On-demand loading
Complex computation Computed immediately Deferred computation

▶ Example: Lazy Loading Configuration

SWIFT
// ============================================
// lazy loading: Database configuration
// ============================================
class DatabaseManager {
    let configFile: String
    // Lazy — only initializes connection when a query is executed
    lazy var connection: String = {
        print("Establishing database connection (one-time only)...")
        // Simulate establishing a connection
        return "Connected to \(configFile)"
    }()
    init(configFile: String) {
        self.configFile = configFile
    }
    func query(_ sql: String) {
        print("Using connection: \(connection) executing query: \(sql)")
    }
}
let db = DatabaseManager(configFile: "app.db")
print("DatabaseManager created, connection not initialized")
db.query("SELECT * FROM users")
db.query("SELECT * FROM orders")
// Second access to connection does not re-initialize

Output:

TEXT 📖 Display only
DatabaseManager created, connection not initialized
Establishing database connection (one-time only)...
Using connection: Connected to app.db executing query: SELECT * FROM users
Using connection: Connected to app.db executing query: SELECT * FROM orders

Warning: lazy must be used with var (let cannot be lazily initialized). Lazy properties are not thread-safe — simultaneous first access from multiple threads may cause multiple initializations.

(2) Type Properties (static)

Type properties belong to the type itself, not to any particular instance. All instances share the same data:

SWIFT
// ============================================
// static type properties
// ============================================
struct AppConfig {
    static let appName = "MySwiftApp"
    static var version = "1.0"
    static var launchCount = 0
    static func incrementLaunch() {
        launchCount += 1
    }
}
// Access directly via type name, no instance needed
print(AppConfig.appName)
AppConfig.version = "1.1"
AppConfig.incrementLaunch()
AppConfig.incrementLaunch()
print("Version: \(AppConfig.version), Launches: \(AppConfig.launchCount)")

Output:

TEXT 📖 Display only
MySwiftApp
Version: 1.1, Launches: 2

6. Complete Example: Profile Management System

SWIFT
// ============================================
// Complete example: Profile management system
// Features: Stored/computed properties + observers + lazy + static
// ============================================
import Foundation
// 1. Global configuration (type properties)
struct Config {
    static let maxAvatarSizeMB = 5.0
    static var apiBaseURL = "https://api.example.com"
    static var userCount = 0
}
// 2. User profile
class UserProfile {
    // Stored properties
    let id: Int
    var name: String {
        didSet {
            print("Name updated: \(oldValue) -> \(name)")
        }
    }
    var avatarUrl: String
    // Computed property — generate initials from name
    var initials: String {
        name.split(separator: " ").compactMap { $0.first }.map { String($0) }.joined()
    }
    // Lazy — compute age description on first access
    lazy var ageDescription: String = {
        print("Computing age description for the first time...")
        return "\(name)'s profile"
    }()
    // Property observer
    var email: String {
        willSet {
            print("Updating email...")
        }
        didSet {
            print("Email updated to \(email)")
        }
    }
    init(id: Int, name: String, email: String, avatarUrl: String) {
        self.id = id
        self.name = name
        self.email = email
        self.avatarUrl = avatarUrl
        Config.userCount += 1
    }
    deinit {
        Config.userCount -= 1
    }
}
// 3. Usage
let profile = UserProfile(
    id: 1,
    name: "Alice Johnson",
    email: "alice@example.com",
    avatarUrl: "https://example.com/avatar.jpg"
)
print("Initials: \(profile.initials)")
print("Age: \(profile.ageDescription)")
print("Active users: \(Config.userCount)")
// Triggers observers
profile.email = "alice@newdomain.com"
profile.name = "Alice Smith"

Output:

TEXT 📖 Display only
Initials: AJ
Computing age description for the first time...
Age: Alice Johnson's profile
Active users: 1
Updating email...
Email updated to alice@newdomain.com
Name updated: Alice Johnson -> Alice Smith

❓ FAQ

Q What's the difference between lazy and computed properties?
A Lazy properties compute and store the result on first access (returning the cached value thereafter); computed properties recompute every time (no caching). Lazy is good for one-time costs; computed is good for dynamic values.
Q Do property observers fire inside initializers?
A No. When a property is set in init, willSet and didSet are not called. They only trigger on assignment to an already-initialized instance. This is a Swift design decision.
Q Can a computed property have no setter?
A Yes. Read-only computed properties can omit the get keyword and braces. For example: var doubled: Int { value * 2 }.
Q Can type properties be overridden by subclasses?
A static properties cannot be overridden. If you need an overridable type property in a class, use the class keyword instead of static.
Q Can a property have both an observer and a setter?
A No. Property observers only apply to stored properties. Computed property changes are handled in the setter and don't need observers. The two are mutually exclusive.

📖 Summary


📝 Exercises

  1. Basic: Define a Circle struct with a radius stored property and an area computed property (returning the area, πr²). Create a circle with radius 5 and print the area.
  2. Intermediate: Create a BankCard class with a balance stored property and a didSet observer — print a warning when balance drops below 0 and a low-balance alert when below 10. Implement withdraw(_:) and deposit(_:) methods.
  3. Challenge: Implement a Logger class using static type properties to store log level and log history array. Provide a static method log(_:level:) that appends entries to the history array, and a static computed property formattedLog that returns formatted log text. Use didSet to automatically check the entry count limit when new logs are added.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏