Agar baris daftar SwiftUI mencerminkan perubahan pada item ObservableObject, teruskan item tersebut ke tampilan baris dengan @ObservedObject dan publikasikan properti yang dapat diubah. Panduan ini membuat editor label Mac sederhana dan menjelaskan peran terpisah dari pembaruan koleksi, pengamatan baris, dan identitas.
Mengapa perubahan pada item tidak mengubah baris?
Misalkan ada store dengan properti @Published var items: [LabelItem], yang setiap LabelItem-nya berupa kelas. Menambahkan item akan mengubah array. Menetapkan judul baru ke item yang sudah ada akan mengubah objek yang dirujuk. Keduanya adalah mutasi yang berbeda: array Swift adalah tipe nilai, sedangkan instance kelas memiliki semantik referensi. Array yang berisi referensi kelas tetap menyimpan referensi ke instance bersama. Tipe Nilai dan Referensi di Swift.
Konsekuensi praktisnya, memublikasikan array tidak otomatis membuat setiap properti di dalam setiap objek yang terkandung di dalamnya ikut diamati. Panduan migrasi Apple menyebut pelacakan koleksi objek sebagai salah satu manfaat sistem Observation yang lebih baru. Untuk implementasi ObservableObject yang sudah ada, hubungkan setiap baris secara tersendiri ke objek yang ditampilkannya. Migrasi dari protokol Observable Object ke makro Observable.
Cara praktis untuk mendiagnosis masalah ini adalah dengan mengidentifikasi dua tanggung jawab: tampilan pembungkus mengamati item yang menjadi bagian dari daftar; setiap baris mengamati konten itemnya yang dapat berubah. Apple mendokumentasikan @ObservedObject sebagai wrapper untuk ObservableObject yang diterima, yang perubahan terpublikasinya harus memperbarui tampilan penerima. ObservedObject.
Buat contoh macOS lengkap dengan baris yang diamati
Gunakan versi ini jika aplikasi Anda sudah menggunakan ObservableObject dari Combine, termasuk jika aplikasi mendukung versi macOS sebelum 14. Contoh ini memerlukan macOS 11 atau lebih baru karena pemiliknya menggunakan @StateObject, yang menurut Apple tersedia mulai macOS 11. Gunakan kompiler Swift di Xcode versi terkini. StateObject.
Buat target aplikasi macOS dengan antarmuka SwiftUI. Ganti file entry aplikasi yang dibuat secara otomatis dengan program lengkap di bawah ini, dan hapus tampilan konten yang dibuat secara otomatis jika tidak lagi diperlukan. Pastikan hanya ada satu deklarasi @main di target tersebut. Semua label dalam contoh ini bersifat sintetis, dan semua interaksi berlangsung di dalam jendela aplikasi itu sendiri.
import SwiftUI
import Combine
import Foundation
@MainActor
final class LabelItem: ObservableObject, Identifiable {
let id = UUID()
@Published var title: String
@Published var isEnabled: Bool
init(title: String, isEnabled: Bool = true) {
self.title = title
self.isEnabled = isEnabled
}
}
@MainActor
final class LabelStore: ObservableObject {
@Published var items: [LabelItem] = [
LabelItem(title: "Amber"),
LabelItem(title: "Cobalt", isEnabled: false)
]
func reviseFirstItem() {
guard let first = items.first else { return }
first.title = first.title == "Amber" ? "Copper" : "Amber"
first.isEnabled.toggle()
}
func addItem() {
items.append(LabelItem(title: "New label"))
}
func removeLastItem() {
guard !items.isEmpty else { return }
items.removeLast()
}
}
@MainActor
struct LabelRow: View {
@ObservedObject var item: LabelItem
var body: some View {
HStack {
TextField("Label", text: $item.title)
.textFieldStyle(.roundedBorder)
.frame(minWidth: 120)
Text(item.title)
.frame(minWidth: 100, alignment: .leading)
Toggle("Enabled", isOn: $item.isEnabled)
}
.padding(.vertical, 4)
}
}
@MainActor
struct LabelListView: View {
@StateObject private var store = LabelStore()
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Button("Change first label") {
store.reviseFirstItem()
}
.disabled(store.items.isEmpty)
Button("Add label") {
store.addItem()
}
Button("Remove last") {
store.removeLastItem()
}
.disabled(store.items.isEmpty)
}
Text("Rows: \(store.items.count)")
List(store.items) { item in
LabelRow(item: item)
}
}
.padding()
.frame(minWidth: 540, minHeight: 300)
}
}
@main
@MainActor
struct LabelRowsApp: App {
var body: some Scene {
WindowGroup {
LabelListView()
}
}
}
Store memiliki instance item. LabelListView membuat dan mempertahankan store menggunakan @StateObject; baris menerima instance yang sudah ada menggunakan @ObservedObject. Apple menjelaskan bahwa objek state bertahan selama identitas kontainer yang mendeklarasikannya masih hidup, dan objek yang diamati umumnya sebaiknya menjadi input, bukan nilai default yang baru dibuat. StateObject, ObservedObject.
Kolom teks dan toggle menggunakan binding ke properti item yang dipublikasikan. Teks di sebelahnya membaca judul yang sama, sehingga Anda dapat melihat bahwa pengeditan dan tampilan menggunakan satu model yang sama. @MainActor memastikan operasi model dan antarmuka dalam contoh ini berjalan di aktor utama, yang menurut Apple eksekutornya setara dengan antrean dispatch utama. ObservedObject, MainActor.
Program ini telah diperiksa berdasarkan API yang terdokumentasi, tetapi tidak dikompilasi di sini karena lingkungan yang tersedia tidak memiliki toolchain Swift. Bangun dan jalankan program ini di Xcode untuk memverifikasi perilakunya pada target deployment Anda.
Verifikasi perubahan item secara terpisah dari perubahan koleksi
Gunakan urutan berikut untuk membedakan kedua jalur pembaruan. Hasil ini merupakan hasil yang diharapkan berdasarkan program, bukan hasil pengukuran pengujian yang dilaporkan.
- Jalankan aplikasi. Aplikasi seharusnya menampilkan dua baris, yang awalnya berlabel
AmberdanCobalt. - Klik
Change first label. Baris pertama seharusnya menampilkanCopper, dan toggleEnabled-nya seharusnya berubah. Jumlah baris tetap dua. - Edit kolom
Labelpada baris pertama. Teks di sebelahnya seharusnya menampilkan judul yang sama. - Klik
Add label. Baris ketiga bernamaNew labelseharusnya muncul, dan jumlahnya bertambah. - Klik
Remove lastsampai daftar kosong. Tombol penghapusan dan tombol untuk label pertama seharusnya dinonaktifkan; tombol untuk menambahkan label harus tetap tersedia.
Langkah kedua sangat berguna karena mutasi dilakukan melalui aksi tombol di tampilan pembungkus. Langkah tersebut mengubah item yang sudah ada tanpa mengganti array. Karena itu, pembaruan baris yang berhasil secara langsung memeriksa jalur pengamatan item. Langkah penambahan dan penghapusan secara terpisah menguji array terpublikasi milik store.
Jika penambahan berfungsi tetapi perubahan judul tidak, periksa deklarasi properti pada baris yang membaca judul. Dalam arsitektur ini, item harus diterima melalui @ObservedObject, dan judul yang dapat diubah harus menggunakan @Published. Publikasi melekat pada properti yang memiliki wrapper tersebut; itu bukan deklarasi rekursif untuk seluruh graf objek. Published, ObservedObject.
Diagnosis kegagalan lain dengan urutan yang tepat
Pastikan penulis dan baris menggunakan instance yang sama
Telusuri objek yang diteruskan ke LabelRow(item:) dan objek yang diubah oleh aksi. Membuat LabelItem lain dengan judul yang sama akan menghasilkan instance terpisah. Demikian pula, menyalin judul ke state lokal baris akan membuat nilai lain yang perlu disinkronkan. Untuk editor ini, lakukan binding langsung ke item bersama, kecuali jika memang diperlukan alur kerja draf dan penerapan perubahan. Ini mengikuti pembedaan yang didokumentasikan Swift antara menyalin nilai dan berbagi referensi kelas. Tipe Nilai dan Referensi di Swift.
Pertahankan identitas saat konten berubah
Contoh ini memberi setiap item pengenal tersimpan yang tidak dapat diubah. ForEach, yang menjadi dasar konstruksi daftar dinamis yang umum, memerlukan elemen yang dapat diidentifikasi atau pengenal eksplisit. Identitas memungkinkan SwiftUI mengaitkan konten yang dihasilkan dengan elemen data terkait selama pembaruan. Identitas tidak menggantikan koneksi pengamatan pada baris. ForEach.
Hindari menambahkan .id(UUID()) yang berubah sebagai mekanisme penyegaran rutin. Apple mendokumentasikan bahwa perubahan identitas tampilan akan mereset state-nya, termasuk state fokus. Membuat ulang baris untuk menampilkan judul yang diedit menambahkan perubahan siklus hidup tersendiri pada perubahan konten; bangun dulu jalur pengamatannya. StateObject.
Perlakukan ringkasan di tampilan induk sebagai konsumen tambahan
Misalkan Anda menambahkan jumlah item aktif di atas daftar. Dalam desain lama yang ditunjukkan di sini, mengamati LabelStore tidak otomatis membuat tampilan induk mengamati publisher isEnabled setiap item. Karena itu, memperbaiki baris saja belum membangun dependensi yang diperlukan untuk ringkasan tersebut.
Tentukan desain pembaruannya secara eksplisit: gabungkan notifikasi dari objek turunan di store, gunakan item bertipe nilai yang perubahannya ditulis kembali melalui koleksi terpublikasi, atau gunakan Observation jika target deployment Anda memungkinkan. Ini adalah pilihan arsitektur yang diturunkan dari pembedaan antara koleksi dan item; contoh ini hanya menerapkan pengamatan baris langsung. Panduan migrasi Apple menjelaskan kemampuan sistem yang lebih baru untuk melacak koleksi dan properti yang benar-benar dibaca oleh tampilan. Migrasi dari protokol Observable Object ke makro Observable.
Jika Anda menerapkan agregasi berbasis publisher, perhatikan waktu notifikasinya. Apple menetapkan bahwa @Published memublikasikan pada willSet: subscriber menerima nilai yang akan masuk sebelum properti tersimpan berubah. Membaca ulang properti di dalam callback tersebut dapat menghasilkan nilai sebelumnya. Gunakan nilai yang dikirimkan saat menghitung berdasarkan peristiwa itu. Published.
Kapan sebaiknya beralih ke Observation di macOS 14 atau lebih baru?
Untuk target deployment minimum macOS 14 atau lebih baru, SwiftUI mendukung sistem Observation. Dengan model @Observable, SwiftUI melacak pembacaan properti yang dapat diamati selama evaluasi tampilan. Panduan Apple secara khusus menunjukkan koleksi objek yang dapat diamati, yang judulnya masing-masing diperbarui saat judul tersebut berubah. Mengelola data model di app Anda.
Untuk memigrasikan contoh ini, ubah LabelItem dan LabelStore agar menggunakan @Observable dengan import Observation, hapus konformansi ObservableObject keduanya, dan hapus @Published. Ubah properti store pemilik dari @StateObject menjadi @State. Karena baris ini berisi kontrol yang dapat diedit, ubah wrapper item yang diterima dari @ObservedObject menjadi @Bindable. Perubahan ini mengikuti jalur migrasi yang didokumentasikan Apple. Migrasi dari protokol Observable Object ke makro Observable.
Baris yang hanya menampilkan data dapat menerima item yang dapat diamati melalui properti tersimpan biasa dan membaca judulnya di body. @Bindable menyediakan binding yang diperlukan kontrol seperti TextField dan Toggle; fungsinya berbeda dari @ObservedObject. Jangan membungkus model yang hanya menggunakan sistem Observation baru dengan @ObservedObject, karena batasan generiknya mensyaratkan ObservableObject. Bindable, ObservedObject.
Keputusannya sederhana: pertahankan solusi baris teramati untuk memperbaiki model Combine yang sudah ada beserta rentang target deployment-nya; pertimbangkan Observation jika macOS 14 dapat diterima sebagai versi minimum dan pelacakan dependensi tingkat properti sesuai dengan bagian antarmuka lainnya.
Apa yang berubah jika baris menyematkan tampilan teks AppKit?
Contoh SwiftUI ini menggunakan kontrol standar dan tidak memerlukan integrasi TextKit khusus. Jika baris Anda sendiri sudah membungkus tampilan AppKit dengan NSViewRepresentable, periksa batas kedua setelah memastikan pengamatan model: Apple mengarahkan Anda untuk memperbarui konfigurasi tampilan AppKit di updateNSView(_:context:) saat state SwiftUI terkait berubah. Membuat tampilan native sekali saja tidak menangani pembaruan konfigurasi berikutnya. updateNSView(_:context:).
Untuk NSTextView yang disematkan, bedakan sinkronisasi model ke tampilan dari tata letak teks. Apple menjelaskan NSTextView sebagai front end sistem teks AppKit dan mendokumentasikan sebuah sakelar kompatibilitas ketika properti layoutManager diakses secara eksplisit pada macOS 12 atau lebih baru. Hindari mengubah mesin tata letak saat mendiagnosis koneksi pengamatan baris yang hilang. NSTextView.
Untuk editor label, perbaikan konkretnya selesai pada batas baris: pertahankan store, publikasikan properti item yang dapat diedit, dan amati setiap item yang diterima di tempat item itu ditampilkan. Verifikasi mutasi item tanpa mengubah koleksi, lalu verifikasi penambahan dan penghapusan. Urutan ini memberi jawaban yang jelas tentang koneksi mana yang perlu diperhatikan.
Sumber
- Tipe Nilai dan Referensi di Swift — Swift.org
- ObservedObject — Dokumentasi Developer Apple
- StateObject — Dokumentasi Developer Apple
- Published — Dokumentasi Developer Apple
- ForEach — Dokumentasi Developer Apple
- MainActor — Dokumentasi Developer Apple
- Mengelola data model di app Anda — Dokumentasi Developer Apple
- Migrasi dari protokol Observable Object ke makro Observable — Dokumentasi Developer Apple
- Bindable — Dokumentasi Developer Apple
- updateNSView(_:context:) — Dokumentasi Developer Apple
- NSTextView — Dokumentasi Developer Apple