Property Wrapper là một cơ chế Swift thêm một lớp logic vào việc truy cập và sửa đổi thuộc tính mà không cần sao chép mã. Trong SwiftUI, Property Wrappers đã trở thành nền tảng của quản lý trạng thái view: @State, @Binding, @ObservedObject, @StateObject và @Environment. Theo tài liệu Swift (2025), các wrapper thuộc tính giảm mã boilerplate trong dự án trung bình 40%. Hiểu Property Wrapper là cần thiết cho mọi nhà phát triển iOS để làm việc hiệu quả với framework.
Những điểm chính
Property Wrapper — một cấu trúc ngôn ngữ Swift, được giới thiệu trong phiên bản 5.1, cho phép đóng gói logic truy cập thuộc tính vào một kiểu riêng biệt. Thay vì viết getter và setter lặp đi lặp lại trong mỗi class, nhà phát triển khai báo wrapper một lần và áp dụng nó qua chú thích @ trước kiểu. Swift tự động bao bọc thuộc tính trong kiểu đã chỉ định, gọi các phương thức wrappedValue và projectedValue khi đọc và ghi. Theo Apple (WWDC 2019), Property Wrappers đã trở thành một trừu tượng chính cho SwiftUI.
Một wrapper thuộc tính là một struct hoặc class với thuộc tính @propertyWrapper. Bên trong, kiểu như vậy phải triển khai thuộc tính wrappedValue, trả về và đặt giá trị thực tế. Trình biên dịch Swift thay thế các truy cập vào thuộc tính gốc bằng các lệnh gọi wrappedValue, hoàn toàn ẩn triển khai khỏi mã gọi. Ngoài ra, có thể định nghĩa projectedValue — một phép chiếu có thể truy cập qua ký hiệu $.
Lợi thế của Property Wrappers nằm ở tái sử dụng logic. Ví dụ, bạn có thể tạo wrapper cho xác thực email, lưu đệm giá trị hoặc đồng bộ hóa với bộ nhớ — và áp dụng nó cho bất kỳ thuộc tính nào trong dự án. Trong SwiftUI, khái niệm này được sử dụng ở khắp mọi nơi: mỗi cơ chế quản lý trạng thái được triển khai như một Property Wrapper riêng biệt.
Khi khai báo một thuộc tính với chú thích @WrapperType var value: T, trình biên dịch Swift biến đổi mã. Nó tạo một thể hiện của WrapperType và tạo ra truy cập đến thuộc tính qua wrappedValue. Mã nguồn let x = value trở thành let x = _value.wrappedValue, và value = newValue trở thành _value.wrappedValue = newValue. Sự biến đổi này xảy ra tại thời điểm biên dịch, không có chi phí runtime.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
Đoạn mã cho thấy wrapper Capitalized, tự động chuyển đổi chuỗi sang định dạng viết hoa chữ cái đầu. Khi gán giá trị, setter gọi capitalized trước khi lưu. Giờ đây bất kỳ thuộc tính nào với chú thích @Capitalized sẽ chỉ lưu trữ văn bản được định dạng chính xác. Cách tiếp cận này loại bỏ hoàn toàn việc trùng lặp mã xác thực và định dạng.
Phép chiếu (projectedValue) — một kênh giao tiếp bổ sung có thể truy cập qua tiền tố $. Trong SwiftUI, tính năng này được sử dụng ở khắp mọi nơi: $state cung cấp Binding
SwiftUI bao gồm năm Property Wrappers tích hợp để quản lý trạng thái: @State, @Binding, @ObservedObject, @StateObject và @Environment. Mỗi cái giải quyết một nhiệm vụ cụ thể và được sử dụng trong các kịch bản khác nhau. @State dành cho dữ liệu cục bộ đơn giản, @Binding — để truyền tham chiếu dữ liệu đến view con, @ObservedObject và @StateObject — cho các đối tượng phức tạp, @Environment — cho các giá trị hệ thống từ hệ thống phân cấp.
| Wrapper | Mục đích | Quyền sở hữu |
|---|---|---|
| @State | Trạng thái cục bộ của một view | View hiện tại |
| @Binding | Kết nối hai chiều với cha | View cha |
| @ObservedObject | Quan sát đối tượng bên ngoài | Chủ sở hữu bên ngoài |
| @StateObject | Tạo ObservableObject | View hiện tại |
| @Environment | Giá trị hệ thống từ hệ thống phân cấp | Môi trường SwiftUI |
Việc chọn Property Wrapper cụ thể phụ thuộc vào nguồn dữ liệu và vòng đời của nó. Nếu dữ liệu thuộc về một view duy nhất và không cần cho các thành phần con — hãy dùng @State. Nếu view con cần sửa đổi dữ liệu của cha — hãy dùng @Binding. Cho các đối tượng được sử dụng trong nhiều view, @ObservedObject và @StateObject là phù hợp.
@State là Property Wrapper để lưu trữ trạng thái cục bộ bên trong một view. SwiftUI tự động quản lý bộ nhớ cho các thuộc tính @State và vẽ lại view mỗi khi thay đổi. @State phù hợp với các kiểu đơn giản (String, Int, Bool, enum) và cấu trúc chỉ thuộc về view hiện tại. Khi giá trị thay đổi, SwiftUI chạy lại thuộc tính body.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Đếm: \(count)")
Button("Tăng") {
count += 1
}
}
}
}
Trong ví dụ, thuộc tính @State count lưu giá trị hiện tại của bộ đếm. SwiftUI tạo một vùng lưu trữ cho thuộc tính này trên heap và liên kết nó với vòng đời của CounterView. Khi nhấn nút, count tăng lên 1, SwiftUI phát hiện thay đổi và chạy lại body, hiển thị giá trị mới. Quan trọng: @State không nên được sử dụng cho các kiểu tham chiếu phức tạp — @StateObject và @ObservedObject được thiết kế cho mục đích đó.
@Binding tạo một tham chiếu đến nguồn dữ liệu thuộc về một view khác. Binding không tự lưu trữ giá trị — nó đọc và ghi dữ liệu qua @State, @StateObject hoặc Binding khác được truyền từ cha. Điều này cho phép các thành phần con sửa đổi trạng thái của tổ tiên mà không cần sở hữu dữ liệu trực tiếp và không cần callback.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
Trong đoạn mã, ToggleSwitch nhận @BindingBool từ view cha. Cha tạo @State var isToggleOn = false và truyền $isToggleOn vào bộ khởi tạo của ToggleSwitch. Khi người dùng chuyển công tắc bên trong view con, thay đổi ngay lập tức được phản ánh trong @State của cha. Cơ chế Binding loại bỏ hoàn toàn nhu cầu về delegate hoặc closure để truyền thay đổi lên trên hệ thống phân cấp.
@ObservedObject là Property Wrapper để quan sát một thể hiện ObservableObject được truyền từ bên ngoài. View không sở hữu đối tượng này — nó được tạo trong thành phần cha hoặc được tiêm qua Environment. Khi bất kỳ thuộc tính @Published nào trong ObservableObject thay đổi, SwiftUI vẽ lại tất cả các view đã đăng ký qua @ObservedObject.
@StateObject — wrapper để tạo và sở hữu ObservableObject trực tiếp trong view. Không giống @ObservedObject, @StateObject đảm bảo một thể hiện duy nhất của đối tượng trong suốt vòng đời của view. Ngay cả khi SwiftUI tạo lại cấu trúc view (điều thường xảy ra), @StateObject giữ lại đối tượng hiện có và không gọi lại bộ khởi tạo.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("Xin chào, \(settings.username)")
}
}
Trong ví dụ, ProfileView tạo UserSettings qua @StateObject, trở thành chủ sở hữu của đối tượng. ChildProfileView nhận cùng thể hiện qua @ObservedObject — quan sát nhưng không quản lý vòng đời. Khi username thay đổi, cả hai view đều được cập nhật. Nếu ChildProfileView dùng @StateObject thay vì @ObservedObject, một thể hiện mới với giá trị ban đầu sẽ được tạo mỗi lần render.
Quy tắc chính: @StateObject được sử dụng trong view tạo ra đối tượng (nguồn sự thật), trong khi @ObservedObject được sử dụng trong view nhận đối tượng đã được tạo từ cha. Vi phạm quy tắc này dẫn đến mất trạng thái hoặc tạo lại dữ liệu không mong muốn.
Swift cho phép tạo Property Wrappers tùy chỉnh cho bất kỳ logic truy cập thuộc tính lặp lại nào. Chỉ cần khai báo một struct hoặc class với thuộc tính @propertyWrapper và triển khai wrappedValue. Dưới đây là wrapper UserDefaultsWrapper, tự động đồng bộ hóa giá trị với UserDefaults.
@propertyWrapper
struct UserDefaultsWrapper<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 AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
Wrapper UserDefaultsWrapper sử dụng generic T để hoạt động với bất kỳ kiểu dữ liệu nào được UserDefaults hỗ trợ. Getter đọc giá trị theo khóa, setter ghi. Áp dụng @UserDefaultsWrapper(key:defaultValue:) cho thuộc tính theme tự động liên kết nó với bộ nhớ — tất cả logic UserDefaults được ẩn bên trong wrapper. Đây là một ví dụ điển hình về giảm mã boilerplate với Property Wrappers.
Khi tạo wrapper tùy chỉnh, điều quan trọng là xem xét hiệu suất. Vì getter và setter được gọi mỗi khi truy cập thuộc tính, không nên đặt các thao tác I/O nặng trong wrappedValue. Đối với lưu trữ dữ liệu bất đồng bộ, tốt hơn nên kết hợp Property Wrappers với ObservableObject và @Published.
Câu hỏi thường gặp
@State được thiết kế cho các kiểu đơn giản (String, Int, Bool) và cấu trúc, trong khi @StateObject dành cho các kiểu tham chiếu triển khai ObservableObject. @State lưu trữ giá trị trực tiếp trong SwiftUI, @StateObject quản lý một thể hiện class trên heap.
Có, @Binding có thể được tạo từ @StateObject, @ObservedObject hoặc từ Binding khác qua phép chiếu $. Binding cũng có thể được khởi tạo từ ObservableObject qua $object.$publishedProperty hoặc từ InlineBinding qua Binding.constant(value).
Đối với dữ liệu toàn cục, hãy sử dụng @EnvironmentObject hoặc tiêm ObservableObject qua EnvironmentValues. @StateObject phù hợp cho view gốc với việc truyền tiếp qua @ObservedObject đến các thành phần con.
@ObservedObject không sở hữu đối tượng — nếu view cha được tạo lại và truyền một thể hiện mới, @ObservedObject sẽ chuyển sang nó. Để tránh mất trạng thái, view sở hữu nên sử dụng @StateObject.
Có, nhưng dễ dàng hơn khi sử dụng kết hợp ObservableObject với @Published và các hàm bất đồng bộ bên trong class. Property Wrapper đồng bộ về bản chất — wrappedValue được tính toán mỗi khi truy cập, không phù hợp cho các thao tác kéo dài.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm