Property Wrapper เป็นกลไกของ Swift ที่เพิ่มชั้นของตรรกะในการเข้าถึงและแก้ไขคุณสมบัติโดยไม่ต้องทำซ้ำโค้ด ใน SwiftUI Property Wrappers กลายเป็นพื้นฐานของการจัดการสถานะของวิว: @State, @Binding, @ObservedObject, @StateObject และ @Environment ตาม เอกสารของ Swift (2025) wrapper คุณสมบัติลดโค้ดเทมเพลตในโปรเจกต์โดยเฉลี่ย 40% การเข้าใจ Property Wrapper เป็นสิ่งจำเป็นสำหรับนักพัฒนา iOS ทุกคนในการทำงานกับเฟรมเวิร์กอย่างมีประสิทธิภาพ
ประเด็นสำคัญ
Property Wrapper — โครงสร้างภาษา Swift ที่เปิดตัวในเวอร์ชัน 5.1 ซึ่งอนุญาตให้ห่อหุ้มตรรกะการเข้าถึงคุณสมบัติในชนิดที่แยกต่างหาก แทนที่จะเขียน getter และ setter ซ้ำ ๆ ในทุกคลาส นักพัฒนาประกาศ wrapper หนึ่งครั้งและนำไปใช้ผ่านคำอธิบายประกอบ @ หน้าชนิด Swift จะห่อหุ้มคุณสมบัติในชนิดที่ระบุโดยอัตโนมัติ เรียกเมธอด wrappedValue และ projectedValue เมื่ออ่านและเขียน ตามข้อมูลของ Apple (WWDC 2019) Property Wrappers กลายเป็นนามธรรมหลักสำหรับ SwiftUI
wrapper คุณสมบัติคือโครงสร้างหรือคลาสที่มีแอตทริบิวต์ @propertyWrapper ภายใน ชนิดดังกล่าวต้อง implement คุณสมบัติ wrappedValue ซึ่งส่งคืนและตั้งค่าจริง คอมไพเลอร์ Swift แทนที่การเข้าถึงคุณสมบัติดั้งเดิมด้วยการเรียก wrappedValue ซ่อนการ implement จากโค้ดที่เรียกใช้โดยสมบูรณ์ นอกจากนี้ สามารถกำหนด projectedValue — การฉายภาพที่เข้าถึงได้ผ่านสัญลักษณ์ $
ข้อดีของ Property Wrappers อยู่ที่ การใช้ซ้ำ ของตรรกะ ตัวอย่างเช่น คุณสามารถสร้าง wrapper สำหรับการตรวจสอบอีเมล การแคชค่าหรือการซิงค์กับพื้นที่จัดเก็บ — และนำไปใช้กับคุณสมบัติใด ๆ ในโปรเจกต์ ใน SwiftUI แนวคิดนี้ถูกใช้ทุกที่: ทุกกลไกการจัดการสถานะถูก implement เป็น Property Wrapper แยกต่างหาก
เมื่อประกาศคุณสมบัติด้วยคำอธิบายประกอบ @WrapperType var value: T คอมไพเลอร์ Swift จะแปลงโค้ด มันสร้าง instance ของ WrapperType และสร้างการเข้าถึงคุณสมบัติผ่าน wrappedValue โค้ดต้นฉบับ let x = value กลายเป็น let x = _value.wrappedValue และ value = newValue กลายเป็น _value.wrappedValue = newValue การแปลงนี้เกิดขึ้นในเวลาคอมไพล์ โดยไม่มีโอเวอร์เฮดรันไทม์
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
รายการแสดง wrapper Capitalized ซึ่งแปลงสตริงเป็นรูปแบบตัวพิมพ์ใหญ่โดยอัตโนมัติ เมื่อกำหนดค่า setter จะเรียก capitalized ก่อนบันทึก ตอนนี้คุณสมบัติใด ๆ ที่มีคำอธิบายประกอบ @Capitalized จะจัดเก็บเฉพาะข้อความที่จัดรูปแบบอย่างถูกต้อง วิธีการนี้ช่วยลดการทำซ้ำโค้ดการตรวจสอบและการจัดรูปแบบโดยสมบูรณ์
การฉายภาพ (projectedValue) — ช่องทางการสื่อสารเพิ่มเติมที่เข้าถึงได้ผ่านคำนำหน้า $ ใน SwiftUI คุณสมบัตินี้ถูกใช้ทุกที่: $state ให้ Binding
SwiftUI มี Property Wrappers ในตัวห้าตัวสำหรับการจัดการสถานะ: @State, @Binding, @ObservedObject, @StateObject และ @Environment แต่ละตัวแก้ไขงานเฉพาะและใช้ในสถานการณ์ต่างกัน @State สำหรับข้อมูลเฉพาะที่ง่าย, @Binding — สำหรับส่งการอ้างอิงข้อมูลไปยังวิวลูก, @ObservedObject และ @StateObject — สำหรับออบเจกต์ที่ซับซ้อน, @Environment — สำหรับค่าระบบจากลำดับชั้น
| Wrapper | วัตถุประสงค์ | ความเป็นเจ้าของ |
|---|---|---|
| @State | สถานะเฉพาะที่ของวิวเดียว | วิวปัจจุบัน |
| @Binding | การเชื่อมต่อสองทิศทางกับแม่ | วิวแม่ |
| @ObservedObject | การสังเกตออบเจกต์ภายนอก | เจ้าของภายนอก |
| @StateObject | การสร้าง ObservableObject | วิวปัจจุบัน |
| @Environment | ค่าระบบจากลำดับชั้น | สภาพแวดล้อม SwiftUI |
การเลือก Property Wrapper เฉพาะขึ้นอยู่กับแหล่งข้อมูลและวงจรชีวิตของมัน หากข้อมูลเป็นของวิวเดียวและไม่จำเป็นต้องใช้โดยคอมโพเนนต์ลูก — ใช้ @State หากวิวลูกต้องการแก้ไขข้อมูลของแม่ — ใช้ @Binding สำหรับออบเจกต์ที่ใช้ในหลายวิว @ObservedObject และ @StateObject เหมาะสม
@State คือ Property Wrapper สำหรับจัดเก็บสถานะเฉพาะที่ภายในวิวเดียว SwiftUI จัดการหน่วยความจำสำหรับคุณสมบัติ @State โดยอัตโนมัติและวาดวิวใหม่ทุกครั้งที่มีการเปลี่ยนแปลง @State เหมาะสำหรับชนิดง่าย (String, Int, Bool, enum) และโครงสร้างที่เป็นของวิวปัจจุบันเท่านั้น เมื่อค่าเปลี่ยน SwiftUI จะรันคุณสมบัติ body อีกครั้ง
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("จำนวน: \(count)")
Button("เพิ่ม") {
count += 1
}
}
}
}
ในตัวอย่าง คุณสมบัติ @State count จัดเก็บค่าปัจจุบันของตัวนับ SwiftUI สร้างพื้นที่จัดเก็บสำหรับคุณสมบัตินี้บนฮีปและเชื่อมโยงกับวงจรชีวิตของ CounterView เมื่อกดปุ่ม count เพิ่มขึ้น 1 SwiftUI ตรวจพบการเปลี่ยนแปลงและรัน body อีกครั้ง แสดงค่าใหม่ สำคัญ: @State ไม่ควรใช้สำหรับชนิดอ้างอิงที่ซับซ้อน — @StateObject และ @ObservedObject ถูกออกแบบมาสำหรับวัตถุประสงค์นั้น
@Binding สร้างการอ้างอิงถึงแหล่งข้อมูลที่เป็นของวิวอื่น Binding ไม่ได้จัดเก็บค่าด้วยตัวเอง — มันอ่านและเขียนข้อมูลผ่าน @State, @StateObject หรือ Binding อื่นที่ส่งมาจากแม่ สิ่งนี้ช่วยให้คอมโพเนนต์ลูกแก้ไขสถานะของบรรพบุรุษโดยไม่ต้องเป็นเจ้าของข้อมูลโดยตรงและไม่ต้องใช้ callback
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
ในรายการ ToggleSwitch รับ @BindingBool จากวิวแม่ แม่สร้าง @State var isToggleOn = false และส่ง $isToggleOn ไปยังตัวเริ่มต้นของ ToggleSwitch เมื่อผู้ใช้สลับสวิตช์ภายในวิวลูก การเปลี่ยนแปลงจะสะท้อนทันทีใน @State ของแม่ กลไก Binding ช่วยขจัดความจำเป็นในการใช้ delegate หรือ closure เพื่อส่งการเปลี่ยนแปลงขึ้นไปในลำดับชั้นโดยสมบูรณ์
@ObservedObject คือ Property Wrapper สำหรับสังเกต instance ObservableObject ที่ส่งมาจากภายนอก วิวไม่ได้เป็นเจ้าของออบเจกต์นี้ — มันถูกสร้างในคอมโพเนนต์แม่หรือถูกฉีดผ่าน Environment เมื่อคุณสมบัติ @Published ใด ๆ ภายใน ObservableObject เปลี่ยน SwiftUI จะวาดวิวทั้งหมดที่สมัครผ่าน @ObservedObject ใหม่
@StateObject — wrapper สำหรับสร้างและเป็นเจ้าของ ObservableObject โดยตรงในวิว แตกต่างจาก @ObservedObject, @StateObject รับประกัน instance เดียวของออบเจกต์ตลอดวงจรชีวิตของวิว แม้ว่า SwiftUI จะสร้างโครงสร้างวิวใหม่ (ซึ่งเกิดขึ้นบ่อยครั้ง) @StateObject จะคงออบเจกต์ที่มีอยู่และไม่เรียกตัวเริ่มต้นอีกครั้ง
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("สวัสดี, \(settings.username)")
}
}
ในตัวอย่าง ProfileView สร้าง UserSettings ผ่าน @StateObject กลายเป็นเจ้าของออบเจกต์ ChildProfileView รับ instance เดียวกันผ่าน @ObservedObject — สังเกตแต่ไม่จัดการวงจรชีวิต เมื่อ username เปลี่ยน วิวทั้งสองอัปเดต ถ้า ChildProfileView ใช้ @StateObject แทน @ObservedObject instance ใหม่พร้อมค่าเริ่มต้นจะถูกสร้างทุกครั้งที่เรนเดอร์
กฎสำคัญ: @StateObject ใช้ในวิวที่สร้างออบเจกต์ (แหล่งความจริง) ในขณะที่ @ObservedObject ใช้ในวิวที่รับออบเจกต์ที่สร้างแล้วจากแม่ การละเมิดกฎนี้นำไปสู่การสูญเสียสถานะหรือการสร้างข้อมูลใหม่โดยไม่คาดคิด
Swift อนุญาตให้สร้าง Property Wrappers แบบกำหนดเองสำหรับตรรกะการเข้าถึงคุณสมบัติที่ทำซ้ำ เพียงประกาศโครงสร้างหรือคลาสด้วยแอตทริบิวต์ @propertyWrapper และ implement wrappedValue ด้านล่างคือ wrapper UserDefaultsWrapper ซึ่งซิงค์ค่าโดยอัตโนมัติกับ 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 ใช้ generic T เพื่อทำงานกับชนิดข้อมูลใด ๆ ที่ UserDefaults รองรับ Getter อ่านค่าตามคีย์ setter เขียน การใช้ @UserDefaultsWrapper(key:defaultValue:) กับคุณสมบัติ theme จะเชื่อมโยงกับพื้นที่จัดเก็บโดยอัตโนมัติ — ตรรกะ UserDefaults ทั้งหมดถูกซ่อนอยู่ภายใน wrapper นี่คือตัวอย่างทั่วไปของการลดโค้ดเทมเพลตด้วย Property Wrappers
เมื่อสร้าง wrapper แบบกำหนดเอง สิ่งสำคัญคือต้องพิจารณา ประสิทธิภาพ เนื่องจาก getter และ setter ถูกเรียกทุกครั้งที่เข้าถึงคุณสมบัติ ไม่ควรวางการดำเนินการ I/O ที่หนักใน wrappedValue สำหรับการจัดเก็บข้อมูลแบบอะซิงโครนัส ควรรวม Property Wrappers กับ ObservableObject และ @Published
คำถามที่พบบ่อย
@State ออกแบบมาสำหรับชนิดง่าย (String, Int, Bool) และโครงสร้าง ในขณะที่ @StateObject สำหรับชนิดอ้างอิงที่ implement ObservableObject @State จัดเก็บค่าโดยตรงใน SwiftUI, @StateObject จัดการ instance คลาสบนฮีป
ได้ @Binding สามารถสร้างจาก @StateObject, @ObservedObject หรือ Binding อื่นโดยใช้การฉายภาพ $ Binding ยังสามารถเริ่มต้นจาก ObservableObject ผ่าน $object.$publishedProperty หรือจาก InlineBinding ผ่าน Binding.constant(value)
สำหรับข้อมูลระดับโลก ใช้ @EnvironmentObject หรือฉีด ObservableObject ผ่าน EnvironmentValues @StateObject เหมาะสำหรับวิวรากโดยส่งต่อผ่าน @ObservedObject ไปยังคอมโพเนนต์ลูก
@ObservedObject ไม่ได้เป็นเจ้าของออบเจกต์ — ถ้าวิวแม่ถูกสร้างใหม่และส่ง instance ใหม่ @ObservedObject จะสลับไปที่ instance นั้น เพื่อหลีกเลี่ยงการสูญเสียสถานะ วิวที่เป็นเจ้าของควรใช้ @StateObject
ได้ แต่ใช้การรวมกันของ ObservableObject กับ @Published และฟังก์ชัน async ภายในคลาสได้ง่ายกว่า Property Wrapper เป็นแบบซิงโครนัสโดยธรรมชาติ — wrappedValue จะถูกคำนวณทุกครั้งที่เข้าถึง ซึ่งไม่เหมาะสำหรับการดำเนินการที่ใช้เวลานาน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม