Swift: تعمق في خصائص Swift
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
- Differences and usage of stored vs computed properties
- Monitoring property changes with
willSetanddidSetobservers - Optimizing performance and resource usage with
lazyloading - Sharing data at the type level with
statictype properties - Basic concept of property wrappers
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:
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
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:
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
// ============================================
// 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 📖 للعرض فقط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:
// ============================================
// 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 📖 للعرض فقطCelsius: 25°C Fahrenheit: 77.0°F After setting to 100°F: Celsius: 37.77777777777778°CTip: Read-only computed properties can omit
getand 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
// ============================================
// 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 📖 للعرض فقط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: 3Warning: Property observers cannot be used on computed properties, since computed properties have no stored value to "observe." Do not set the same property inside
willSetordidSet— 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:
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
// ============================================
// 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 📖 للعرض فقط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 ordersWarning:
lazymust be used withvar(letcannot 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:
// ============================================
// 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 📖 للعرض فقطMySwiftApp Version: 1.1, Launches: 2
6. Complete Example: Profile Management System
// ============================================
// 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 📖 للعرض فقط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
init, willSet and didSet are not called. They only trigger on assignment to an already-initialized instance. This is a Swift design decision.get keyword and braces. For example: var doubled: Int { value * 2 }.static properties cannot be overridden. If you need an overridable type property in a class, use the class keyword instead of static.📖 Summary
- Stored properties directly store values; computed properties provide values indirectly via getters/setters
willSettriggers before the value is stored;didSettriggers afterlazy varproperties initialize on first access, ideal for expensive resource creationstaticproperties belong to the type itself and are shared by all instances- Read-only computed properties can omit
getand the return braces - Property observers do not fire during
init
📝 Exercises
- Basic: Define a
Circlestruct with aradiusstored property and anareacomputed property (returning the area, πr²). Create a circle with radius 5 and print the area. - Intermediate: Create a
BankCardclass with abalancestored property and adidSetobserver — print a warning when balance drops below 0 and a low-balance alert when below 10. Implementwithdraw(_:)anddeposit(_:)methods. - Challenge: Implement a
Loggerclass usingstatictype properties to store log level and log history array. Provide astaticmethodlog(_:level:)that appends entries to the history array, and astaticcomputed propertyformattedLogthat returns formatted log text. UsedidSetto automatically check the entry count limit when new logs are added.