@StateObject คือ Property Wrapper ใน SwiftUI สำหรับสร้างและเป็นเจ้าของอินสแตนซ์ ObservableObject โดยตรงภายใน view SwiftUI รับประกันว่าออบเจ็กต์จะถูกเริ่มต้นหนึ่งครั้งต่อวงจรชีวิตของวิวและจะไม่ถูกสร้างใหม่เมื่อมีการเรนเดอร์ซ้ำ ตาม เอกสารสำหรับนักพัฒนา Apple (2025) @StateObject แนะนำสำหรับวิวรากที่สร้างแหล่งข้อมูล @StateObject เป็นตัวเลือกที่ถูกต้องสำหรับการเป็นเจ้าของ ObservableObject ในลำดับชั้นของ SwiftUI
ประเด็นสำคัญ
@StateObject คือ Property Wrapper ที่เปิดตัวใน SwiftUI 2.0 (iOS 14) ซึ่งรวมความสามารถของ @ObservedObject และ @State เข้าด้วยกัน เช่นเดียวกับ @ObservedObject มันสมัครรับการเปลี่ยนแปลงของ ObservableObject เช่นเดียวกับ @State มันรับประกันว่าข้อมูลจะอยู่รอดผ่านการเริ่มต้นโครงสร้างวิวซ้ำ ๆ @StateObject สร้างออบเจ็กต์หนึ่งครั้งเมื่อวิวปรากฏบนหน้าจอครั้งแรกและเก็บไว้ในฮีปของ SwiftUI
ก่อน @StateObject นักพัฒนาใช้ @ObservedObject สำหรับ ObservableObjects ทั้งหมด รวมถึงที่สร้างในวิว สิ่งนี้นำไปสู่การสูญเสียข้อมูลบ่อยครั้งเมื่อวิวแม่ถูกอัปเดต ทำให้โครงสร้างวิวถูกสร้างใหม่และพาอินสแตนซ์ @ObservedObject ไปด้วย @StateObject แก้ปัญหานี้โดยเพิ่มการรับประกันความเสถียร
กฎหลัก: @StateObject ถูกใช้ในวิวที่สร้างออบเจ็กต์ในตัวเริ่มต้นเริ่มต้น (let model = ViewModel()) วิวลูกที่รับออบเจ็กต์นี้ใช้ @ObservedObject การแยกนี้รับประกันแหล่งความจริงเดียวทั่วทั้งลำดับชั้น
SwiftUI จัดการวงจรชีวิตของ @StateObject ผ่านตัวจัดการพื้นที่เก็บข้อมูลที่คล้ายกับ @State เมื่อวิวปรากฏครั้งแรก SwiftUI จัดสรรหน่วยความจำสำหรับออบเจ็กต์และเก็บไว้ในพื้นที่ถาวร เมื่อเรนเดอร์ซ้ำ (การเรียก body) ออบเจ็กต์จะไม่ถูกสร้างใหม่—ใช้อินสแตนซ์ที่มีอยู่ ออบเจ็กต์มีชีวิตตราบเท่าที่วิวยังอยู่ในลำดับชั้น
เมื่อวิวถูกลบออกจากลำดับชั้น SwiftUI ทำลาย @StateObject และเรียก deinit เมื่อวิวถูกเพิ่มกลับเข้าไปในลำดับชั้น จะสร้างอินสแตนซ์ใหม่ สิ่งสำคัญที่ต้องพิจารณาเมื่อออกแบบ: หากคุณต้องการเก็บข้อมูลระหว่างการลบวิว ให้ใช้เลเยอร์บริการ (singleton หรือ DI) หรือ @AppStorage เพื่อความคงทน
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
ในตัวอย่าง TimerViewModel ถูกสร้างผ่าน @StateObject และมีชีวิตตราบเท่าที่ TimerView อยู่บนหน้าจอ ตัวจับเวลาเริ่มใน onAppear และหยุดใน deinit หากใช้ @ObservedObject ทุกครั้งที่ TimerView เรนเดอร์จะสร้าง TimerViewModel ใหม่ด้วย seconds = 0 และตัวจับเวลาจะไม่ทำงานอย่างถูกต้อง @StateObject รับประกันว่า viewModel มีเอกลักษณ์และเสถียร
การเลือกระหว่าง @StateObject และ @ObservedObject ขึ้นอยู่กับใครเป็นเจ้าของออบเจ็กต์ ถ้าวิวสร้างออบเจ็กต์—@StateObject ถ้าวิวนรับออบเจ็กต์สำเร็จรูป—@ObservedObject กฎนี้สำคัญมากจน Xcode แสดงคำเตือนเมื่อใช้ @StateObject ในวิวลูกที่รับออบเจ็กต์ผ่านตัวเริ่มต้น
| สถานการณ์ | Wrapper ที่แนะนำ |
|---|---|
| วิวสร้างโมเดลผ่าน ViewModel() | @StateObject |
| วิวรับโมเดลจากแม่ | @ObservedObject |
| โมเดลถูกใช้ในวิวเดียว | @StateObject |
| โมเดลถูกส่งผ่าน Environment | @EnvironmentObject |
| โมเดลจำเป็นสำหรับตัวอย่างดูก่อน | @ObservedObject + mock |
ในทางปฏิบัติ เมื่อเริ่มโปรเจกต์ @StateObject มักใช้ในวิวรากและ @ObservedObject ในวิวลูกทั้งหมด เมื่อแอปพลิเคชันเติบโตขึ้น อินสแตนซ์ @StateObject บางตัวสามารถถูกแทนที่ด้วย @EnvironmentObject เพื่อทำให้ลำดับชั้นง่ายขึ้น อย่างไรก็ตาม @StateObject ยังคงเป็นตัวเลือกที่ดีที่สุดสำหรับหน้าจอแบบโมดูลาร์ที่มีตรรกะของตัวเอง
รูปแบบแรก—MVVM กับ @StateObject ViewModel ในฐานะ ObservableObject ถูกสร้างในวิวผ่าน @StateObject ViewModel มีคุณสมบัติ @Published และตรรกะทางธุรกิจ วิวสมัครรับการเปลี่ยนแปลงและอัปเดตอินเทอร์เฟซ วิธีการนี้ให้การแยกที่ทดสอบได้: ViewModel สามารถทดสอบโดยไม่ต้องมี UI โดยการสร้างอินสแตนซ์โดยตรง
รูปแบบที่สอง—@StateObject ที่มีการพึ่งพา ถ้า ViewModel ต้องการบริการ ให้ใช้การเริ่มต้นด้วยพารามิเตอร์ ตัวอย่างเช่น @StateObject var viewModel = UserViewModel(api: APIClient.shared) อย่างไรก็ตามโปรดระวัง: พารามิเตอร์ถูกคำนวณทุกครั้งที่ body เรนเดอร์ แต่ออบเจ็กต์ถูกสร้างเพียงครั้งเดียว SwiftUI ไม่สนใจการเริ่มต้น @StateObject ครั้งต่อมา
รูปแบบที่สาม—@StateObject ซ้อนกัน ใน SwiftUI คุณสามารถมี @StateObject หลายตัวในวิวเดียว แต่สิ่งนี้ไม่ค่อยสมเหตุสมผล โดยปกติ @StateObject หนึ่งตัวจัดการชุดข้อมูลทั้งหมดของวิว ถ้าตรรกะซับซ้อนเกินไป ให้แบ่งมันเป็นการประกอบของบริการ @ObservedObject ภายใน @StateObject เดียว
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
ในตัวอย่าง AppView สร้าง @StateObject สองตัว: NavigationRouter สำหรับจัดการการนำทางและ AuthViewModel สำหรับการตรวจสอบสิทธิ์ ออบเจ็กต์ทั้งสองถูกฉีดเข้าไปใน Environment ผ่าน environmentObject วิวลูกใด ๆ สามารถเข้าถึงมันผ่าน @EnvironmentObject โดยไม่ต้องผ่านลูกโซ่ตัวเริ่มต้น
@StateObject รองรับการเริ่มต้นด้วยพารามิเตอร์ใด ๆ แต่มีข้อสำคัญ: ตัวเริ่มต้นถูกเรียกเพียงครั้งเดียว เมื่อเรนเดอร์ body ซ้ำ ค่าพารามิเตอร์ใหม่จะถูกไม่สนใจ ซึ่งหมายความว่าถ้าคุณส่ง @State var id: Int = 5 ไปยัง @StateObject var vm = ViewModel(id: id) เมื่อ id เปลี่ยน ViewModel จะไม่ได้รับค่าใหม่
เพื่อแก้ปัญหานี้ ให้ใช้ onReceive หรือ onAppear สำหรับการซิงโครไนซ์ สมัครรับการเปลี่ยนแปลงพารามิเตอร์ภายใน ViewModel ผ่าน Combine หรือส่งพารามิเตอร์ผ่านเมธอด .onChange(of:) ที่ระดับวิว อีกทางเลือกคือใช้ @ObservedObject แทน @StateObject ถ้าออบเจ็กต์ควรตอบสนองต่อการเปลี่ยนแปลงภายนอกแบบไดนามิก
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
วิธีการที่ถูกต้อง: DetailView รับ itemId เป็นคุณสมบัติ let (ส่งผ่านตัวเริ่มต้นของโครงสร้าง) และ @StateObject สร้าง DetailViewModel โดยไม่มีพารามิเตอร์ ใน onAppear เมธอด load(id:) ถูกเรียกเพื่อโหลดข้อมูลสำหรับ ID ที่ส่ง สิ่งนี้รับประกันว่า ViewModel ถูกสร้างโดยกลไก @StateObject แต่ข้อมูลจะถูกโหลดทุกครั้งที่วิวปรากฏด้วย ID ปัจจุบัน
ข้อผิดพลาดหลัก—การใช้ @StateObject ในวิวลูกที่รับออบเจ็กต์จากแม่ ถ้า ParentView สร้าง @StateObject model และ ChildView ประกาศ @StateObject var model: ModelType (พร้อมพารามิเตอร์เริ่มต้น) ChildView จะสร้างอินสแตนซ์อิสระของตัวเอง ออบเจ็กต์แม่และลูกจะไม่เชื่อมต่อกัน และการเปลี่ยนแปลงในอันหนึ่งจะไม่สะท้อนในอีกอัน
ข้อผิดพลาดที่สอง—การวาง @StateObject ใน List หรือ ForEach แต่ละองค์ประกอบของรายการสร้าง @StateObject ของตัวเอง นำไปสู่อินสแตนซ์อิสระหลายตัว สำหรับรายการ วิธีการที่ถูกต้องคือส่ง ObservableObject หนึ่งตัวไปยังองค์ประกอบทั้งหมดผ่าน @ObservedObject หรือใช้โครงสร้าง Identifiable กับ @State ภายใน List
ปัญหาที่สาม—ขาดการล้างข้อมูลใน deinit @StateObject มีชีวิตตลอดวงจรชีวิตของวิวทั้งหมด ถ้าออบเจ็กต์สร้างตัวจับเวลา การสมัครรับ Combine หรือคำขอเครือข่าย deinit ต้องยกเลิก它们 มิฉะนั้น หน่วยความจำรั่วและการทำงานพื้นหลังต่อเนื่องหลังจากปิดหน้าจอเป็นสิ่งที่หลีกเลี่ยงไม่ได้ ใช้พื้นที่เก็บ Cancellable ของ Combine เสมอหรือทำให้ตัวจับเวลาไม่ถูกต้องใน deinit
คำถามที่พบบ่อย
@StateObject ถูกเพิ่มใน SwiftUI 2.0 ที่ WWDC 2020 พร้อมกับ iOS 14, macOS 11, watchOS 7 และ tvOS 14 ก่อนหน้านั้น @ObservedObject เป็นวิธีเดียวในการทำงานกับ ObservableObject ซึ่งมักนำไปสู่บักการสูญเสียข้อมูล
ไม่ @StateObject ไม่รองรับชนิด Optional ออบเจ็กต์ต้องถูกเริ่มต้นที่การประกาศ หากคุณต้องการออบเจ็กต์ที่เป็นทางเลือก ให้ใช้ @ObservedObject หรือ @EnvironmentObject พร้อมชนิดที่เป็นทางเลือก
เพิ่ม print(#function) ในตัวเริ่มต้นและ deinit ของ ObservableObject ถ้า init ไม่ถูกเรียกเมื่อเรนเดอร์ซ้ำ—@StateObject ทำงานถูกต้อง ถ้า init ถูกเรียกทุกครั้ง—แทนที่ @ObservedObject ด้วย @StateObject
ใช่ @StateObject ทำงานในวิว SwiftUI ที่ฝังใน UIKit ผ่าน UIHostingController วงจรชีวิตของออบเจ็กต์ผูกกับวิว SwiftUI ไม่ใช่ UIViewController ถ้าวิว SwiftUI ถูกแทนที่ @StateObject จะถูกทำลาย
@StateObject เล็ก ๆ หลายตัว ที่มีความรับผิดชอบแยกกัน สิ่งนี้ช่วยปรับปรุงความสามารถในการทดสอบ การใช้ซ้ำ และประสิทธิภาพ—เมื่อออบเจ็กต์หนึ่งเปลี่ยนแปลง เฉพาะส่วนที่สมัครรับของอินเทอร์เฟซเท่านั้นที่ถูกวาดใหม่ ไม่ใช่ทั้งวิว
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ