Herhaalde initialisatie van SwiftUI-rijen bewijst niet dat een rij is verdwenen of zijn status heeft verloren. Om een scrollende Mac-interface te diagnosticeren, houd je het aanmaken, de behouden status, verschijningscallbacks en zichtbaarheid in de viewport afzonderlijk bij.
Deze walkthrough is bedoeld voor een ontwikkelaar die herhaalde rijlogberichten in diens eigen macOS-app onderzoekt. De concrete taak is vaststellen of een update alleen een nieuwe view-waarde aanmaakt, de rijstatus reset of verandert welke rijen de scrollviewport vullen. Het voorbeeld gebruikt synthetische labels en gewone knoppen.
Wat vertelt elk levenscyclussignaal precies?
Een SwiftUI-rij gebruikt doorgaans een structuur, een waardetype in Swift. Een nieuwe structuurwaarde aanmaken is iets anders dan een persistent referentieobject maken. De documentatie van Swift legt dit onderscheid uit in Waarde- en referentietypen in Swift.
SwiftUI voegt daar een eigen identiteitsmodel aan toe. Apple legt uit dat tijdelijke view-waarden bij updates dezelfde identiteit kunnen vertegenwoordigen en dat de levensduur van een view die identiteit volgt. Een logbericht uit een initializer kan dus niet aantonen dat de levensduur van de vorige rij is geëindigd. Zie SwiftUI ontrafeld.
Gebruik deze signalen om verschillende vragen te beantwoorden:
| Signaal | Vraag waarop het antwoord geeft | Beperking van de interpretatie |
|---|---|---|
| Rij-initializer | Is er een nieuwe rijwaarde aangemaakt? | Bewijst niet dat de rij is verschenen of dat de status is vervangen. |
Evaluatie van body |
Is deze view-beschrijving geëvalueerd? | Telt geen weergegeven frames. |
| Behouden rijstatus | Heeft deze diagnostische waarde de update overleefd? | Beschrijft de continuïteit van die status, niet welke inhoud de viewport vult. |
onAppear en onDisappear |
Heeft SwiftUI een verschijnings- of verdwijningsgebeurtenis gemeld? | De timing hangt af van het view-type. |
onScrollVisibilityChange |
Is de rij over een gekozen viewportdrempel gegaan? | Meet de gedocumenteerde zichtbaarheidsvoorwaarde voor scrollen. |
Apple documenteert dat onAppear wordt uitgevoerd voordat het eerste frame wordt gerenderd; de precieze timing hangt af van het view-type. onDisappear wordt uitgevoerd na het verdwijnen, eveneens met view-afhankelijke timing. Geen van beide methoden documenteert het dealloceren van status. Houd deze callbackgebeurtenissen gescheiden van conclusies over de levensduur van opslag. Zie onAppear(perform:) en onDisappear(perform:).
Kies voor vragen over de viewport expliciet een drempel. Op macOS 15 en hoger meldt onScrollVisibilityChange(threshold:_:) of de view de opgegeven zichtbaarheidsdrempel overschrijdt; de standaardwaarde is 0,5. Apple documenteert ook een initiële callback wanneer de view verschijnt en de drempel al is bereikt. Zie onScrollVisibilityChange(threshold:_:).
In dit artikel betekent ‘zichtbaar in de viewport’ dat aan die scrolldrempel wordt voldaan. Het zegt niet of iemand naar de rij heeft gekeken.
Bouw een klein experiment dat de signalen uit elkaar houdt
Maak een macOS SwiftUI-app met een SDK die macOS 15-API’s bevat en stel het implementatiedoel in op macOS 15 of hoger. Vervang de gegenereerde app- en content-viewdeclaraties door het volledige programma hieronder. Behoud slechts één @main-ingangspunt.
Het voorbeeld gebruikt ScrollView en LazyVStack, zodat het viewportgedrag expliciet is. Apple beschrijft LazyVStack als een container die items aanmaakt wanneer dat nodig is. Dit experiment gaat er niet van uit dat List hetzelfde laad- of behoudgedrag heeft. Zie 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"
}
}
De twee UUID’s hebben verschillende doelen. constructionID labelt elke aanroep van de initializer. appearanceToken is aanvankelijk leeg en wordt bij de eerste waargenomen verschijning toegewezen aan de behouden rijstatus. Het registreert niet wanneer de status is aangemaakt. De teller geeft je nog een zichtbare waarde waarvan je de continuïteit kunt inspecteren.
Apple documenteert dat SwiftUI de opslag voor State beheert en afhankelijke interface-inhoud bijwerkt wanneer de waarde verandert. Daarom houdt de diagnostiek de token en teller bij in private status, terwijl constructiegegevens gewone opgeslagen eigenschappen blijven. Zie State.
Het programma is gecontroleerd aan de hand van de gedocumenteerde API’s, maar hier niet gecompileerd of uitgevoerd, omdat er geen Swift-toolchain beschikbaar was. De logvoorbeelden hieronder leggen uit hoe je de uitvoer interpreteert; het zijn geen vastgelegde resultaten.
Voer drie gecontroleerde checks uit en interpreteer de resultaten
1. Werk rij-invoer bij zonder de identiteit te veranderen
Voer de app uit in Xcode en houd de debugconsole open. Klik tweemaal op de knop Count van een zichtbare rij, noteer de token en klik vervolgens op Update rows. De revisie die aan de rijen wordt doorgegeven verandert, maar de vaste gehele getallen als identificatie blijven gelijk.
Als er opnieuw initializerberichten verschijnen maar de geselecteerde rij zijn token en teller behoudt, is dat bewijs dat er opnieuw waarden zijn aangemaakt terwijl de diagnostische status behouden bleef. Classificeer die initializerberichten niet als afzonderlijke perioden waarin de rij op het scherm stond. De weergegeven revisie bevestigt dat de rij de nieuwe invoer heeft ontvangen.
De verschijningstoken wordt via status toegewezen, wat zelf weer een update kan veroorzaken. Dat is opzettelijk diagnostisch gedrag. Verwacht geen exact aantal initializer- of body-berichten en verwijder deze meetpunten zodra het onderzoek is afgerond.
2. Scroll een rij uit de viewport en terug
Scroll langzaam tot de geselecteerde rij de drempel van 50% passeert. Vergelijk de berichten van viewport met de verschijningsberichten en ga vervolgens terug naar de rij. Controleer de token en teller opnieuw.
Een behouden token nadat je terugkeert toont dat deze diagnostische status tijdens die scrollbeweging behouden bleef. Een andere token betekent dat de eerdere token niet behouden is. Als je wilt verklaren waarom de status in een echte app is gereset, onderzoek dan identiteitswijzigingen, voorwaardelijke verwijdering en het behoudgedrag van de container; leid de oorzaak niet alleen af uit het scrollen.
De huidige richtlijnen van Apple leggen uit dat lazy stacks de body en lay-out van een view vooraf kunnen voorbereiden, vóór de verschijning. Als het scrollen wordt omgekeerd, krijgt een vooraf voorbereide view mogelijk nooit onAppear. Apple legt ook uit dat status van views in lazy stacks buiten beeld uiteindelijk kan worden verwijderd en raadt aan gegevens die scrollen moeten overleven op te slaan in een model of een bovenliggende view. Zie Duik in lazy stacks en scrollen met SwiftUI.
Daarom is de rijlokale teller in het voorbeeld bruikbaar als meetpunt. Sla wijzigingen in een echte bewerkingsinterface die verwijdering van een rij moeten overleven op bij een langer levende eigenaar, gekoppeld aan de stabiele identificatie van het item.
3. Reset de identiteit bewust
Klik op Reset identity. Het programma verandert de identiteit die aan de volledige scrollview is gekoppeld, zodat je dit bewust kunt vergelijken met de gewone revisie-update. Apple documenteert expliciet dat een wijziging van de waarde die aan id(_:) wordt doorgegeven de identiteit van de view reset, inclusief de status. Zie id(_:).
Controleer of de nieuw verschijnende rijen nieuwe tokens en begintellers hebben. Omdat de reset op de volledige scrollview-subboom wordt toegepast, is dit geen gecontroleerde test voor het behoud van de scrollpositie.
Gebruik deze beslisgids bij het beoordelen van je eigen trace:
| Waarneming | Ondersteunde conclusie | Volgende controle |
|---|---|---|
| Nieuwe initializerberichten; token en teller blijven behouden | Er heeft constructie plaatsgevonden terwijl de diagnostische status behouden bleef. | Bekijk de kosten van initialisatie als updates traag zijn. |
| Verschijningscallbacks worden herhaald; token blijft behouden | Herhaald verschijnen heeft die token niet vervangen. | Gebruik viewportcallbacks voor gedrag dat van een drempel afhangt. |
| Token en teller worden gereset na een gewone update | De rijstatus is niet zoals verwacht behouden gebleven. | Controleer gegevens-ID’s, veranderende id-modifiers en voorwaardelijke vertakkingen. |
| Een rij wordt geïnitialiseerd maar meldt nooit dat deze is verschenen | Alleen constructie bewijst niet dat de rij is verschenen. | Houd rekening met lazy voorbereiding en daaropvolgende scrollbewegingen. |
| Zichtbaarheid in de viewport wordt onwaar | Er wordt niet langer aan de gekozen scrolldrempel voldaan. | Bepaal of die drempel past bij het beoogde gedrag. |
Behoud bij dynamische gegevens de identificaties van de onderliggende items tussen updates. Een nieuw gegenereerde identificatie bij elke uitlezing verhindert continuïteit. Controleer ook de identiteiten van bovenliggende views: stabiele rij-ID’s kunnen een rij niet behouden binnen een subboom waarvan je de identiteit bewust vervangt. Apple bespreekt stabiele gegevensidentificaties en structurele identiteit in SwiftUI ontrafeld.
Wat is de ondersteunde aanpak voor eerdere macOS-versies?
De zichtbaarheidsmodifier voor scrollen in het voorbeeld vereist macOS 15. Als je het implementatiedoel verlaagt, heb je een beschikbaarheidsvertakking met een afzonderlijke implementatie nodig; onAppear is geen gelijkwaardige drempelmeting.
Als je app al een AppKit-scrollinterface beheert, levert NSScrollView.documentVisibleRect het deel van de documentview dat zichtbaar is via de contentview, uitgedrukt in coördinaten van de documentview. Een alternatief op basis van geometrie is deze rechthoek te vergelijken met je eigen rijrechthoeken in hetzelfde coördinatensysteem en opnieuw te berekenen na scroll- en lay-outwijzigingen. Zie documentVisibleRect.
Een expliciet gekozen regel kan bijvoorbeeld bepalen dat een rij zichtbaar is wanneer minstens de helft van het rechthoekige oppervlak overlapt met de zichtbare documentrechthoek. Dit is een voorgestelde toepassingsregel, geen bewering dat deze de implementatie van SwiftUI nabootst. Behandel lege rechthoeken en coördinatentransformaties expliciet.
SwiftUI biedt NSViewRepresentable om een AppKit-view te integreren die je app zelf aanmaakt en beheert. De gedocumenteerde methoden voor aanmaken, bijwerken, coördinatie en afbreken bieden hiervoor een ondersteunde brug. Respecteer de controle van SwiftUI over de lay-out van de weergegeven view; Apple waarschuwt ervoor om het frame of de bounds van die view rechtstreeks te wijzigen. Zie NSViewRepresentable.
Deze terugvaloptie gaat over een AppKit-documentview die je zelf beheert. Je hoeft hiervoor geen aannames te doen over de private achterliggende views van een SwiftUI-List.
Bewijst de TextKit-lay-out dat tekst op het scherm staat?
Ook bij TextKit moet je voorbereiding en viewportgeometrie uit elkaar houden. Dit is van belang als je eigen rij een AppKit-tekstview bevat en de vraag wordt welke tekstdelen het zichtbare gebied overlappen.
NSTextViewportLayoutController van TextKit 2, beschikbaar vanaf macOS 12, beheert een actief lay-outgebied dat doorgaans het zichtbare gebied plus een overscrollgebied omvat. De viewportBounds omvat overdraw en de optionele viewportRange beschrijft de huidige viewportlay-out. Geen van beide mag worden aangeduid als een exacte verzameling zichtbare tekens. Zie NSTextViewportLayoutController.
Gebruik voor een implementatie met TextKit 2 de geometrie van de tekstlay-out, niet allocatiegebeurtenissen. NSTextLayoutManager kan tekstdelen opsommen voor een opgegeven tekstbereik. Vergelijk de resulterende segmentrechthoeken met de daadwerkelijke cliprechthoek nadat je beide in hetzelfde coördinatensysteem hebt geplaatst. Houd rekening met textContainerOrigin van de tekstview; ga er niet van uit dat de coördinaten van de tekstcontainer gelijk zijn aan die van de documentview. Zie enumerateTextSegments(in:type:options:using:) en textContainerOrigin.
Voor een bestaande implementatie met TextKit 1 accepteert NSLayoutManager.glyphRange(forBoundingRect:in:) een rechthoek in tekstcontainercoördinaten. Apple geeft aan dat het teruggegeven aaneengesloten bereik glyphs buiten die rechthoek kan bevatten. Het is dus een kandidaatbereik voor verdere inspectie en geen exacte zichtbaarheidslijst. Zie glyphRange(forBoundingRect:in:).
Behoud de tekstengine tijdens het diagnosticeren. Apple documenteert dat toegang tot de eigenschap layoutManager van een NSTextView of tekstcontainer op macOS 12 en hoger terugval naar de compatibiliteitsmodus met NSLayoutManager veroorzaakt. Gebruik voor een TextKit 2-pad textLayoutManager en de bijbehorende API’s. Zie NSTextView.
Kies het signaal dat de interfacevraag beantwoordt: initializerlogberichten voor constructie, behouden status voor continuïteit, verschijningscallbacks voor verschijningsovergangen en viewportgeometrie voor zichtbaarheid. Een experiment met stabiele ID’s en één bewuste identiteitsreset maakt die verschillen zichtbaar, zonder te hoeven gissen op basis van de hoeveelheid console-uitvoer.
Bronnen
- Waarde- en referentietypen in Swift — Swift.org
- SwiftUI ontrafeld — Apple
onAppear(perform:)— AppleonDisappear(perform:)— AppleonScrollVisibilityChange(threshold:_:)— AppleLazyVStack— AppleState— Apple- Duik in lazy stacks en scrollen met SwiftUI — Apple
id(_:)— AppledocumentVisibleRect— AppleNSViewRepresentable— AppleNSTextViewportLayoutController— AppleenumerateTextSegments(in:type:options:using:)— AppletextContainerOrigin— AppleglyphRange(forBoundingRect:in:)— AppleNSTextView— Apple