Bỏ qua để đến nội dung

Property Wrappers

Nhiều property cần logic lặp lại mỗi khi get/set (validate, log, lưu vào UserDefaults…). Thay vì viết computed property thủ công ở khắp nơi, Property Wrapper đóng gói logic đó thành một annotation tái sử dụng được.

// Không có property wrapper: lặp code validate ở mọi nơi
struct Circle {
private var _radius: Double = 0
var radius: Double {
get { _radius }
set { _radius = max(0, newValue) } // Không cho âm
}
}
@propertyWrapper
struct NonNegative {
private var value: Double = 0
var wrappedValue: Double {
get { value }
set { value = max(0, newValue) }
}
init(wrappedValue: Double) {
self.wrappedValue = wrappedValue
}
}
struct Circle {
@NonNegative var radius: Double
}
var circle = Circle(radius: 5)
circle.radius = -10
print(circle.radius) // 0 - đã bị chặn bởi wrapper

@NonNegative var radius: Double tương đương với việc Swift tự sinh:

private var _radius = NonNegative(wrappedValue: 0)
var radius: Double {
get { _radius.wrappedValue }
set { _radius.wrappedValue = newValue }
}

Property wrapper có thể “chiếu” thêm giá trị phụ, truy cập qua tiền tố $:

@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
private(set) var wasClamped = false
var wrappedValue: Value {
get { value }
set {
if newValue < range.lowerBound {
value = range.lowerBound
wasClamped = true
} else if newValue > range.upperBound {
value = range.upperBound
wasClamped = true
} else {
value = newValue
wasClamped = false
}
}
}
var projectedValue: Bool { wasClamped }
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct Volume {
@Clamped(0...100) var level: Int = 50
}
var v = Volume()
v.level = 150
print(v.level) // 100 (đã bị giới hạn)
print(v.$level) // true (wasClamped)

SwiftUI dùng property wrapper làm nền tảng cho quản lý state:

import SwiftUI
struct CounterView: View {
// @State: state cục bộ của view, SwiftUI tự re-render khi thay đổi
@State private var count = 0
// @Binding: tham chiếu tới state ở view cha
// @ObservedObject / @StateObject: theo dõi ObservableObject
// @EnvironmentObject: lấy dữ liệu chia sẻ từ môi trường
var body: some View {
VStack {
Text("Count: \(count)")
Button("Tăng") {
count += 1
}
}
}
}
import Combine
class UserSettings: ObservableObject {
@Published var username: String = "Guest" // Tự phát thông báo khi thay đổi
}
struct ProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("Xin chào, \(settings.username)")
}
}

Property wrapper rất hợp để đóng gói việc đọc/ghi UserDefaults:

@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct Settings {
@UserDefault(key: "isDarkMode", defaultValue: false)
static var isDarkMode: Bool
}
Settings.isDarkMode = true
print(Settings.isDarkMode) // true, đã lưu vào UserDefaults
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
struct LoginForm {
@Trimmed var username: String
}
var form = LoginForm(username: " admin ")
print(form.username) // "admin" - đã tự động trim

Bài tập: Property wrapper @Capitalized tự viết hoa chữ cái đầu

// TODO: @propertyWrapper struct Capitalized
// TODO: wrappedValue tự capitalize khi set

Lời giải:

@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.prefix(1).uppercased() + newValue.dropFirst() }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
struct Student {
@Capitalized var name: String
}
var s = Student(name: "bumbii")
print(s.name) // "Bumbii"
s.name = "an"
print(s.name) // "An"
  • Property wrapper không thể dùng trực tiếp trên local variable trong hầu hết trường hợp trước Swift 5.4 — từ Swift 5.4 trở đi có thể dùng cho local variable.
  • Property wrapper type phải có var wrappedValue — đây là yêu cầu bắt buộc duy nhất.
  • projectedValue là optional, chỉ cần khi muốn expose thêm thông tin qua cú pháp $name.
  • Trong SwiftUI, phân biệt đúng: @State (state cục bộ, value type), @StateObject (khởi tạo và sở hữu 1 lần), @ObservedObject (nhận từ bên ngoài), @EnvironmentObject (chia sẻ toàn app) — dùng sai loại là lỗi phổ biến nhất khi mới học SwiftUI.

  • @propertyWrapper đóng gói logic get/set lặp lại thành annotation tái sử dụng
  • wrappedValue bắt buộc, projectedValue (truy cập qua $) là tùy chọn
  • SwiftUI dùng property wrapper cho quản lý state: @State, @Binding, @Published, @ObservedObject, @EnvironmentObject
  • Phù hợp cho: validate dữ liệu, trim chuỗi, lưu UserDefaults, giới hạn giá trị (clamping)

Tiếp theo: Async/Await