Property Wrappers
1. Vấn đề Property Wrapper giải quyết
Phần tiêu đề “1. Vấn đề Property Wrapper giải quyết”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ơistruct Circle { private var _radius: Double = 0 var radius: Double { get { _radius } set { _radius = max(0, newValue) } // Không cho âm }}2. Định nghĩa Property Wrapper
Phần tiêu đề “2. Định nghĩa Property Wrapper”@propertyWrapperstruct 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 = -10print(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 }}3. projectedValue - Truy cập thêm với $
Phần tiêu đề “3. projectedValue - Truy cập thêm với $”Property wrapper có thể “chiếu” thêm giá trị phụ, truy cập qua tiền tố $:
@propertyWrapperstruct 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 = 150print(v.level) // 100 (đã bị giới hạn)print(v.$level) // true (wasClamped)4. Property Wrapper phổ biến trong SwiftUI
Phần tiêu đề “4. Property Wrapper phổ biến trong SwiftUI”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)") }}5. Ví dụ thực tế: @UserDefault
Phần tiêu đề “5. Ví dụ thực tế: @UserDefault”Property wrapper rất hợp để đóng gói việc đọc/ghi UserDefaults:
@propertyWrapperstruct 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 = trueprint(Settings.isDarkMode) // true, đã lưu vào UserDefaults6. Ràng buộc với enforce trong init
Phần tiêu đề “6. Ràng buộc với enforce trong init”@propertyWrapperstruct 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🛠️ Thực hành
Phần tiêu đề “🛠️ Thực hành”Bài tập: Property wrapper @Capitalized tự viết hoa chữ cái đầu
// TODO: @propertyWrapper struct Capitalized// TODO: wrappedValue tự capitalize khi setLời giải:
@propertyWrapperstruct 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"⚠️ Lưu ý quan trọng
Phần tiêu đề “⚠️ Lưu ý quan trọng”- 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. projectedValuelà 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.
📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”@propertyWrapperđóng gói logic get/set lặp lại thành annotation tái sử dụngwrappedValuebắ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