Inisialisasi baris SwiftUI yang berulang tidak membuktikan bahwa sebuah baris menghilang atau kehilangan statusnya. Untuk mendiagnosis antarmuka Mac yang dapat digulir, lacak konstruksi, status yang dipertahankan, callback kemunculan, dan visibilitas area pandang secara terpisah.

Panduan ini ditujukan bagi pengembang yang menyelidiki log baris berulang di aplikasi macOS mereka sendiri. Tugas konkretnya adalah menentukan apakah pembaruan hanya membuat nilai tampilan lain, mengatur ulang status baris, atau mengubah baris mana yang menempati area pandang gulir. Contoh ini menggunakan label sintetis dan tombol biasa.

Apa yang sebenarnya ditunjukkan oleh setiap sinyal siklus hidup?

Baris SwiftUI biasanya menggunakan struktur, yaitu tipe nilai Swift. Membuat nilai struktur lain berbeda dari membuat objek referensi persisten. Dokumentasi Swift menjelaskan perbedaan ini dalam Value And Reference Types In Swift.

SwiftUI memiliki model identitasnya sendiri. Apple menjelaskan bahwa nilai tampilan sementara dapat mewakili identitas yang sama di berbagai pembaruan, dan masa hidup tampilan mengikuti identitas tersebut. Karena itu, log inisialisator tidak dapat memastikan bahwa masa hidup baris sebelumnya telah berakhir. Lihat Demystify SwiftUI.

Gunakan sinyal berikut untuk menjawab pertanyaan yang berbeda:

Sinyal Pertanyaan yang dijawab Batas interpretasi
Inisialisator baris Apakah nilai baris lain dibuat? Tidak memastikan kemunculan atau penggantian status.
Evaluasi body Apakah deskripsi tampilan ini dievaluasi? Tidak menghitung frame yang ditampilkan.
Status baris yang dipertahankan Apakah nilai diagnostik ini bertahan melewati pembaruan? Menjelaskan kesinambungan status tersebut, bukan keterisian area pandang.
onAppear dan onDisappear Apakah SwiftUI melaporkan transisi kemunculan? Waktunya bergantung pada tipe tampilan.
onScrollVisibilityChange Apakah baris melewati ambang area pandang yang dipilih? Mengukur kondisi visibilitas gulir yang didokumentasikan.

Apple mendokumentasikan bahwa onAppear selesai sebelum frame pertama dirender, dengan waktu persis yang bergantung pada tipe tampilan. onDisappear berjalan setelah tampilan menghilang, juga dengan waktu yang bergantung pada tampilan. Tidak satu pun metode tersebut mendokumentasikan dealokasi status. Pisahkan peristiwa callback itu dari kesimpulan tentang masa hidup penyimpanan. Lihat onAppear(perform:) dan onDisappear(perform:).

Untuk pertanyaan tentang area pandang, tentukan ambangnya secara eksplisit. Pada macOS 15 dan versi lebih baru, onScrollVisibilityChange(threshold:_:) melaporkan apakah tampilan melewati ambang visibilitas yang diberikan; nilai bawaannya adalah 0.5. Apple juga mendokumentasikan callback awal ketika tampilan muncul dengan ambang yang sudah tercapai. Lihat onScrollVisibilityChange(threshold:_:).

Dalam artikel ini, “terlihat di area pandang” berarti memenuhi ambang gulir tersebut. Ini bukan pengukuran apakah seseorang melihat baris itu.

Buat eksperimen kecil yang memisahkan sinyal-sinyal tersebut

Buat aplikasi SwiftUI macOS menggunakan SDK yang menyertakan API macOS 15, lalu tetapkan target deployment ke macOS 15 atau lebih baru. Ganti deklarasi aplikasi dan tampilan konten yang dibuat otomatis dengan program lengkap di bawah. Pertahankan hanya satu titik masuk @main.

Contoh ini menggunakan ScrollView dan LazyVStack agar perilaku area pandang terlihat jelas. Apple menjelaskan bahwa LazyVStack membuat item sesuai kebutuhan. Eksperimen ini tidak mengasumsikan bahwa List memiliki perilaku pemuatan atau retensi yang sama. Lihat LazyVStack.

import SwiftUI
import Foundation

@main
struct RowLifetimeLabApp: App {
    var body: some Scene {
        WindowGroup("Row Lifetime Lab") {
            ProbeView()
        }
    }
}

struct ProbeView: View {
    @State private var revision = 0
    @State private var generation = 0

    var body: some View {
        VStack(spacing: 12) {
            HStack {
                Button("Update rows") {
                    revision += 1
                }
                Button("Reset identity") {
                    generation += 1
                }
                Text("Revision \(revision)")
            }

            ScrollView {
                LazyVStack(spacing: 8) {
                    ForEach(1...60, id: \.self) { rowID in
                        ProbeRow(rowID: rowID, revision: revision)
                    }
                }
                .padding()
            }
            .id(generation)
        }
        .padding()
        .frame(minWidth: 560, minHeight: 360)
    }
}

struct ProbeRow: View {
    let rowID: Int
    let revision: Int
    private let constructionID: UUID

    @State private var appearanceToken: UUID? = nil
    @State private var count = 0

    init(rowID: Int, revision: Int) {
        self.rowID = rowID
        self.revision = revision
        self.constructionID = UUID()
        print("init row=\(rowID) value=\(constructionID)")
    }

    var body: some View {
        let _ = print("body row=\(rowID) value=\(constructionID)")

        HStack {
            VStack(alignment: .leading) {
                Text("Sample row \(rowID)")
                Text("Revision \(revision)")
                Text(appearanceToken?.uuidString ?? "Awaiting appearance")
                    .font(.caption.monospaced())
            }
            Spacer()
            Button("Count \(count)") {
                count += 1
            }
        }
        .padding(12)
        .frame(height: 80)
        .background(Color.gray.opacity(0.12))
        .onAppear {
            if appearanceToken == nil {
                appearanceToken = UUID()
            }
            print("appear row=\(rowID) token=\(tokenDescription)")
        }
        .onDisappear {
            print("disappear row=\(rowID) token=\(tokenDescription)")
        }
        .onScrollVisibilityChange(threshold: 0.5) { visible in
            print("viewport row=\(rowID) visible=\(visible)")
        }
    }

    private var tokenDescription: String {
        appearanceToken?.uuidString ?? "unassigned"
    }
}

Kedua UUID memiliki tujuan berbeda. constructionID memberi label pada setiap pemanggilan inisialisator. appearanceToken dimulai dalam keadaan kosong dan ditetapkan saat kemunculan pertama yang diamati untuk status baris yang dipertahankan. Nilai ini tidak mencatat waktu alokasi status. Jumlah memberi Anda nilai lain yang terlihat dan kesinambungannya dapat diperiksa.

Apple mendokumentasikan bahwa SwiftUI mengelola penyimpanan State dan memperbarui konten antarmuka yang bergantung padanya ketika nilainya berubah. Karena itu, diagnostik ini menyimpan token dan jumlah dalam status privat, sementara informasi konstruksi tetap menjadi properti tersimpan biasa. Lihat State.

Program ini telah diperiksa berdasarkan API yang didokumentasikan, tetapi tidak dikompilasi atau dijalankan di sini karena toolchain Swift tidak tersedia. Contoh log di bawah menjelaskan cara menafsirkan hasil uji Anda; itu bukan hasil yang direkam.

Jalankan tiga pemeriksaan terkontrol dan tafsirkan hasilnya

1. Perbarui input baris tanpa mengubah identitas

Jalankan aplikasi di Xcode dan biarkan konsol debug terbuka. Klik tombol Count pada baris yang terlihat dua kali, catat tokennya, lalu klik Update rows. Revisi yang diberikan kepada baris berubah, sementara pengenal bilangan bulat tetap sama.

Jika pesan inisialisator muncul lagi tetapi baris yang dipilih mempertahankan token dan jumlahnya, Anda memiliki bukti bahwa nilai dibuat berulang kali sementara status diagnostik dipertahankan. Jangan menggolongkan pesan inisialisator tersebut sebagai masa tampil di layar yang terpisah. Revisi yang ditampilkan memastikan bahwa baris menerima input baru.

Token kemunculan ditetapkan melalui status, yang dapat memicu pembaruan lain. Perilaku diagnostik itu memang disengaja. Jangan mengharapkan jumlah pesan inisialisator atau body yang persis, dan hapus probe ini setelah penyelidikan selesai.

2. Gulirkan baris keluar dari area pandang lalu kembali

Gulir perlahan hingga baris yang dipilih melewati ambang 50%. Bandingkan pesan viewport dengan pesan kemunculan, lalu kembali ke baris tersebut. Periksa lagi token dan jumlahnya.

Token yang tetap ada setelah kembali menunjukkan kesinambungan status diagnostik ini sepanjang perubahan tersebut. Token yang berbeda berarti token sebelumnya tidak dipertahankan. Untuk menjelaskan pengaturan ulang dalam aplikasi nyata, selidiki perubahan identitas, penghapusan kondisional, dan perilaku retensi kontainer, alih-alih menyimpulkan penyebabnya hanya dari pengguliran.

Panduan Apple terkini menjelaskan bahwa stack lazy dapat menyiapkan body dan tata letak tampilan sebelum tampilan muncul. Jika arah gulir berbalik, tampilan yang sudah disiapkan mungkin tidak pernah menerima onAppear. Apple juga menjelaskan bahwa status stack lazy di luar layar pada akhirnya dapat dibuang, dan menyarankan agar data yang harus bertahan saat digulir disimpan dalam model atau tampilan luar. Lihat Dive into lazy stacks and scrolling with SwiftUI.

Karena itu, jumlah lokal baris dalam contoh ini berguna sebagai probe. Untuk antarmuka pengeditan nyata, simpan perubahan yang harus bertahan setelah baris dihapus pada pemilik dengan masa hidup lebih panjang, yang dikunci berdasarkan pengenal stabil item tersebut.

3. Atur ulang identitas dengan sengaja

Klik Reset identity. Program mengubah identitas yang diterapkan ke seluruh tampilan gulir, sehingga memberikan perbandingan yang disengaja dengan pembaruan revisi biasa. Apple secara eksplisit mendokumentasikan bahwa mengubah nilai yang diberikan ke id(_:) akan mengatur ulang identitas tampilan, termasuk statusnya. Lihat id(_:).

Periksa token baru dan jumlah awal pada baris yang baru muncul. Karena pengaturan ulang berlaku pada subtree tampilan gulir, jangan gunakan ini sebagai uji terkontrol untuk mempertahankan posisi gulir.

Gunakan panduan keputusan ini saat meninjau jejak Anda sendiri:

Pengamatan Kesimpulan yang didukung Pemeriksaan berikutnya
Pesan inisialisator baru; token dan jumlah bertahan Konstruksi terjadi sementara status diagnostik tetap bertahan. Periksa biaya inisialisator jika pembaruan lambat.
Callback kemunculan berulang; token bertahan Kemunculan berulang tidak mengganti token tersebut. Gunakan callback area pandang untuk perilaku yang bergantung pada ambang.
Token dan jumlah diatur ulang setelah pembaruan biasa Status baris tidak bertahan sebagaimana diharapkan. Periksa ID data, modifier id yang berubah, dan cabang kondisional.
Sebuah baris diinisialisasi tetapi tidak pernah melaporkan kemunculan Konstruksi saja tidak memastikan kemunculan. Pertimbangkan persiapan lazy dan perubahan pengguliran berikutnya.
Visibilitas area pandang menjadi false Ambang gulir yang dipilih tidak lagi terpenuhi. Tentukan apakah ambang tersebut sesuai dengan perilaku yang dimaksud.

Untuk data dinamis, pertahankan pengenal yang melekat pada item dasar di berbagai pembaruan. Pengenal baru yang dibuat setiap kali data dibaca menghalangi kesinambungan. Periksa juga identitas leluhur: ID baris yang stabil tidak dapat mempertahankan baris di dalam subtree yang identitasnya sengaja Anda ganti. Apple membahas pengenal data yang stabil dan identitas struktural dalam Demystify SwiftUI.

Apa jalur yang didukung pada versi macOS lebih lama?

Modifier visibilitas gulir pada contoh ini memerlukan macOS 15. Menurunkan target deployment memerlukan cabang ketersediaan dengan implementasi terpisah; onAppear bukan pengukuran ambang yang setara.

Jika aplikasi Anda sudah memiliki antarmuka pengguliran AppKit, NSScrollView.documentVisibleRect menyediakan bagian tampilan dokumennya yang terlihat melalui tampilan konten, dalam koordinat tampilan dokumen. Alternatif berbasis geometri adalah membandingkan persegi panjang itu dengan persegi panjang baris Anda sendiri dalam sistem koordinat yang sama, lalu menghitung ulang setelah pengguliran dan perubahan tata letak. Lihat documentVisibleRect.

Sebagai contoh, aturan yang dipilih secara eksplisit dapat menggolongkan sebuah baris sebagai terlihat ketika setidaknya setengah area persegi panjangnya beririsan dengan persegi panjang yang terlihat pada dokumen. Ini adalah aturan aplikasi yang diusulkan, bukan klaim bahwa aturan ini mereproduksi implementasi SwiftUI. Tangani persegi panjang kosong dan konversi koordinat secara eksplisit.

SwiftUI menyediakan NSViewRepresentable untuk mengintegrasikan tampilan AppKit yang dibuat dan dikelola aplikasi Anda. Metode pembuatan, pembaruan, koordinasi, dan pembongkaran yang didokumentasikan menyediakan jembatan yang didukung. Hormati kendali SwiftUI atas tata letak tampilan yang direpresentasikan; Apple memperingatkan agar frame atau bounds tampilan itu tidak diubah secara langsung. Lihat NSViewRepresentable.

Solusi alternatif ini berkaitan dengan tampilan dokumen AppKit yang Anda miliki. Solusi ini tidak memerlukan asumsi tentang tampilan pendukung privat dari List SwiftUI.

Apakah tata letak TextKit membuktikan bahwa teks terlihat di layar?

TextKit memerlukan pemisahan yang sama antara persiapan dan geometri area pandang. Ini penting jika baris Anda berisi tampilan teks AppKit dan pertanyaannya menjadi segmen teks mana yang beririsan dengan area yang terlihat.

NSTextViewportLayoutController dari TextKit 2, yang tersedia pada macOS 12 dan versi lebih baru, mengelola area tata letak aktif yang umumnya mencakup area terlihat ditambah area gulir berlebih. viewportBounds-nya mencakup gambar berlebih, dan viewportRange opsionalnya menjelaskan tata letak area pandang saat ini. Keduanya tidak boleh disebut sebagai himpunan karakter yang terlihat secara persis. Lihat NSTextViewportLayoutController.

Untuk implementasi TextKit 2, gunakan geometri tata letak teks, bukan peristiwa alokasi. NSTextLayoutManager dapat menghitung segmen teks untuk rentang teks yang diberikan. Bandingkan persegi panjang segmen yang dihasilkan dengan persegi panjang kliping sebenarnya setelah keduanya ditempatkan dalam sistem koordinat yang sama. Perhitungkan textContainerOrigin milik tampilan teks; jangan mengasumsikan koordinat kontainer teks sama dengan koordinat tampilan dokumen. Lihat enumerateTextSegments(in:type:options:using:) dan textContainerOrigin.

Untuk implementasi TextKit 1 yang sudah ada, NSLayoutManager.glyphRange(forBoundingRect:in:) menerima persegi panjang dalam koordinat kontainer teks. Apple menyatakan bahwa rentang berurutan yang dikembalikan dapat mencakup glif di luar persegi panjang tersebut, sehingga rentang itu merupakan kandidat untuk pemeriksaan lebih lanjut, bukan daftar visibilitas yang persis. Lihat glyphRange(forBoundingRect:in:).

Pertahankan mesin teks saat mendiagnosisnya. Apple mendokumentasikan bahwa mengakses properti layoutManager milik NSTextView atau kontainer teks pada macOS 12 dan versi lebih baru menyebabkan peralihan ke mode kompatibilitas yang menggunakan NSLayoutManager. Untuk jalur TextKit 2, gunakan textLayoutManager beserta API-nya. Lihat NSTextView.

Pilih sinyal yang menjawab pertanyaan antarmuka: log inisialisator untuk konstruksi, status yang dipertahankan untuk kesinambungan, callback kemunculan untuk transisi kemunculan, dan geometri area pandang untuk visibilitas. Eksperimen dengan ID stabil dan satu pengaturan ulang identitas yang disengaja membuat perbedaan-perbedaan itu dapat diamati tanpa menebak dari banyaknya keluaran konsol.

Sumber