Upprepad initiering av SwiftUI-rader bevisar inte att en rad försvann eller förlorade sitt tillstånd. För att diagnostisera ett rullande Mac-gränssnitt ska du spåra konstruktion, bevarat tillstånd, callbackar för visning och synlighet i visningsområdet separat.
Den här genomgången riktar sig till en utvecklare som undersöker upprepade radloggar i sin egen macOS-app. Den konkreta uppgiften är att avgöra om en uppdatering bara skapar ytterligare ett vyvärde, återställer radens tillstånd eller ändrar vilka rader som ryms i rullningsområdets visningsyta. Exemplet använder syntetiska etiketter och vanliga knappar.
Vad säger varje livscykelsignal egentligen?
En SwiftUI-rad använder vanligtvis en struktur, som är en värdetyp i Swift. Att konstruera ytterligare ett strukturvärde skiljer sig från att skapa ett beständigt referensobjekt. Swifts dokumentation förklarar skillnaden i Value And Reference Types In Swift.
SwiftUI har sin egen identitetsmodell. Apple förklarar att tillfälliga vyvärden kan representera samma identitet mellan uppdateringar och att en vy har samma livslängd som den identiteten. Därför kan en initieringslogg inte visa att den föregående radens livslängd tog slut. Se Demystify SwiftUI.
Använd de här signalerna för att besvara olika frågor:
| Signal | Fråga den besvarar | Begränsning i tolkningen |
|---|---|---|
| Radinitierare | Konstruerades ytterligare ett radvärde? | Fastställer inte att raden visades eller att dess tillstånd ersattes. |
Utvärdering av body |
Utvärderades den här vyns beskrivning? | Räknar inte visade bildrutor. |
| Bevarat radtillstånd | Överlevde det här diagnostikvärdet uppdateringen? | Beskriver kontinuiteten hos det tillståndet, inte vilka rader som ryms i visningsområdet. |
onAppear och onDisappear |
Rapporterade SwiftUI en övergång till visning eller frånvaro? | Tidpunkten beror på vytypen. |
onScrollVisibilityChange |
Passerade raden en vald gräns för visningsområdet? | Mäter det dokumenterade villkoret för rullningens synlighet. |
Apple dokumenterar att onAppear slutförs före den första renderade bildrutan, och att den exakta tidpunkten beror på vytypen. onDisappear körs efter att vyn försvunnit, även här med vyberoende tidpunkt. Ingen av metoderna dokumenterar att tillståndet avallokeras. Håll dessa callbackhändelser åtskilda från slutsatser om lagringens livslängd. Se onAppear(perform:) och onDisappear(perform:).
Om frågan gäller visningsområdet ska du uttryckligen välja en tröskel. I macOS 15 och senare rapporterar onScrollVisibilityChange(threshold:_:) om vyn passerar den angivna synlighetströskeln; standardvärdet är 0,5. Apple dokumenterar också en inledande callback när vyn visas och tröskeln redan har uppnåtts. Se onScrollVisibilityChange(threshold:_:).
I den här artikeln betyder ”synlig i visningsområdet” att rullningströskeln uppfylls. Det är inte ett mått på om någon tittade på raden.
Skapa ett litet experiment som skiljer signalerna åt
Skapa en macOS SwiftUI-app med ett SDK som innehåller macOS 15-API:er och ange macOS 15 eller senare som distributionsmål. Ersätt de genererade app- och innehållsvydeklarationerna med hela programmet nedan. Behåll bara en @main-ingångspunkt.
Exemplet använder ScrollView och LazyVStack för att göra beteendet i visningsområdet tydligt. Apple beskriver LazyVStack som en vy som skapar objekt efter behov. Det här experimentet förutsätter inte att List har samma laddnings- eller bevarandebeteende. Se 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 två UUID-värdena har olika syften. constructionID märker varje initieringsanrop. appearanceToken börjar tomt och tilldelas vid den första observerade visningen för det bevarade radtillståndet. Det tidsstämplar inte tillståndets allokering. Räknaren ger dig ytterligare ett synligt värde vars kontinuitet du kan granska.
Apple dokumenterar att SwiftUI hanterar lagring för State och uppdaterar beroende gränssnittsinnehåll när värdet ändras. Diagnostiken behåller därför sin token och räknare i privat tillstånd, medan konstruktionsinformationen förblir en vanlig lagrad egenskap. Se State.
Programmet har granskats mot de dokumenterade API:erna, men kompilerades eller kördes inte här eftersom en Swift-verktygskedja inte var tillgänglig. Loggexemplen nedan beskriver hur du tolkar din körning; de är inte insamlade resultat.
Kör tre kontrollerade tester och tolka resultaten
1. Uppdatera radindata utan att ändra identiteten
Kör appen i Xcode och håll felsökningskonsolen öppen. Klicka två gånger på knappen Count för en synlig rad, anteckna dess token och klicka sedan på Update rows. Revisionen som skickas till raderna ändras medan de fasta heltalsidentifierarna förblir desamma.
Om initieringsmeddelanden visas igen men den valda raden behåller sin token och räknare har du belägg för upprepad värdekonstruktion med bevarat diagnostiktillstånd. Klassificera inte dessa initieringsmeddelanden som separata tidsperioder då raden varit på skärmen. Den visade revisionen bekräftar att raden fick den nya indatan.
Visningstoken tilldelas via tillstånd, vilket i sig kan orsaka ytterligare en uppdatering. Det är avsiktligt diagnostikbeteende. Förvänta dig inte ett exakt antal initieringsmeddelanden eller meddelanden från body, och ta bort dessa sonder när undersökningen är klar.
2. Rulla bort en rad från visningsområdet och tillbaka
Rulla långsamt tills den valda raden passerar 50-procentströskeln. Jämför meddelandena från viewport med visningsmeddelandena och gå sedan tillbaka till raden. Granska token och räknaren igen.
En bevarad token när du återvänder visar att det här diagnostiktillståndet var kontinuerligt under den utflykten. En annan token innebär att den tidigare tokenen inte bevarades. Om du vill förklara en återställning i en verklig app ska du undersöka identitetsändringar, villkorad borttagning och containerns bevarandebeteende i stället för att dra slutsatsen att rullningen ensam orsakade den.
Aktuell vägledning från Apple förklarar att lata stackar kan förbereda en vykomponents kropp och layout innan visning. Om rullningen vänds kan det hända att en förberedd vy aldrig får onAppear. Apple förklarar också att tillstånd utanför skärmen i lata stackar till slut kan kastas bort och rekommenderar att data som måste överleva rullning lagras i en modell eller en yttre vy. Se Dive into lazy stacks and scrolling with SwiftUI.
Därför är radens lokala räknare användbar som en mätsond i exemplet. I ett verkligt redigeringsgränssnitt ska ändringar som måste överleva att rader tas bort lagras hos en ägare med längre livslängd, med artikelns stabila identifierare som nyckel.
3. Återställ identiteten med avsikt
Klicka på Reset identity. Programmet ändrar identiteten som kopplats till hela rullningsvyn, vilket ger en avsiktlig jämförelse med den vanliga revisionsuppdateringen. Apple dokumenterar uttryckligen att en ändring av värdet som skickas till id(_:) återställer vyns identitet, inklusive dess tillstånd. Se id(_:).
Granska de nytillkomna raderna efter nya token och initiala räknarvärden. Eftersom återställningen gäller rullningsvyns underträd ska du inte använda den som ett kontrollerat test av om rullningspositionen bevaras.
Använd den här vägledningen när du granskar din egen spårning:
| Observation | Slutsats som stöds | Nästa kontroll |
|---|---|---|
| Nya initieringsmeddelanden; token och räknare består | Konstruktion skedde medan diagnostiktillståndet bestod. | Granska initieringskostnaden om uppdateringar går långsamt. |
| Visningscallbackar upprepas; token består | Den upprepade visningen ersatte inte tokenen. | Använd callbackar för visningsområdet vid beteende som beror på tröskelvärden. |
| Token och räknare återställs efter en vanlig uppdatering | Radtillståndet bevarades inte som väntat. | Granska data-ID:n, modifierare som ändrar id och villkorliga grenar. |
| En rad initieras men rapporterar aldrig visning | Enbart konstruktionen fastställde inte att raden visades. | Ta hänsyn till lat förberedelse och efterföljande ändringar i rullningen. |
| Synligheten i visningsområdet blir falsk | Den valda rullningströskeln uppfylls inte längre. | Avgör om tröskeln motsvarar det avsedda beteendet. |
För dynamiska data ska identifierarna följa de underliggande objekten mellan uppdateringar. En identifierare som skapas på nytt vid varje läsning förhindrar kontinuitet. Granska även föräldrarnas identiteter: stabila rad-ID:n kan inte bevara en rad i ett underträd vars identitet du avsiktligt ersätter. Apple diskuterar stabila dataidentifierare och strukturell identitet i Demystify SwiftUI.
Vilken metod stöds i äldre macOS-versioner?
Exemplets modifierare för rullningssynlighet kräver macOS 15. Om du sänker distributionsmålet behöver du en tillgänglighetsgren med en separat implementering; onAppear är ingen likvärdig tröskelmätning.
Om din app redan har ett AppKit-baserat rullningsgränssnitt ger NSScrollView.documentVisibleRect den del av dess dokumentvy som är synlig genom innehållsvyn, uttryckt i dokumentvyns koordinater. Ett geometribaserat alternativ är att jämföra rektangeln med dina egna radrektanglar i samma koordinatsystem och räkna om efter rullning och layoutändringar. Se documentVisibleRect.
En uttryckligt vald regel kan till exempel klassificera en rad som synlig när minst halva dess rektangulära yta överlappar den dokumentvisibla rektangeln. Det här är en föreslagen regel för appen, inte ett påstående om att den återskapar SwiftUIs implementation. Hantera tomma rektanglar och koordinatkonvertering uttryckligen.
SwiftUI tillhandahåller NSViewRepresentable för att integrera en AppKit-vy som appen skapar och hanterar. Dess dokumenterade metoder för skapande, uppdatering, samordning och nedmontering erbjuder en stödd brygga. Respektera SwiftUIs styrning av den representerade vyns layout; Apple varnar för att direkt ändra den vyns bildruta eller gränser. Se NSViewRepresentable.
Den här reservlösningen gäller en AppKit-dokumentvy som du äger. Den kräver inga antaganden om de privata bakomliggande vyerna i en SwiftUI List.
Bevisar TextKit-layout att texten visas på skärmen?
TextKit kräver samma åtskillnad mellan förberedelse och geometri för visningsområdet. Det här är relevant om den egna raden innehåller en AppKit-textvy och frågan blir vilka textsegment som skär den synliga ytan.
TextKit 2:s NSTextViewportLayoutController, som finns i macOS 12 och senare, hanterar ett aktivt layoutområde som i allmänhet omfattar det synliga området plus ett område för överskjutning. Dess viewportBounds omfattar överritning, och dess valfria viewportRange beskriver den aktuella visningsområdeslayouten. Inget av dem bör ombenämnas till en exakt uppsättning synliga tecken. Se NSTextViewportLayoutController.
Använd textlayoutgeometri i en TextKit 2-implementation i stället för allokeringshändelser. NSTextLayoutManager kan räkna upp textsegment för ett angivet textintervall. Jämför de resulterande segmentrektanglarna med den faktiska klippningsrektangeln efter att båda placerats i samma koordinatsystem. Ta hänsyn till textvyns textContainerOrigin; anta inte att textbehållarens koordinater är desamma som dokumentvyns koordinater. Se enumerateTextSegments(in:type:options:using:) och textContainerOrigin.
För en befintlig TextKit 1-implementation accepterar NSLayoutManager.glyphRange(forBoundingRect:in:) en rektangel i textbehållarens koordinater. Apple anger att det sammanhängande intervall som returneras kan innehålla glyfer utanför den rektangeln, så det är ett kandidatområde att granska vidare, inte en exakt synlighetslista. Se glyphRange(forBoundingRect:in:).
Bevara textmotorn under diagnostiseringen. Apple dokumenterar att åtkomst till egenskapen layoutManager hos en NSTextView eller textbehållare på macOS 12 och senare leder till reservläge för kompatibilitet med NSLayoutManager. Använd i stället textLayoutManager och dess API:er för en TextKit 2-väg. Se NSTextView.
Välj den signal som besvarar frågan om gränssnittet: initieringsloggar för konstruktion, bevarat tillstånd för kontinuitet, visningscallbackar för övergångar till visning och geometri för visningsområdet för synlighet. Ett experiment med stabila ID:n och en avsiktlig identitetsåterställning gör skillnaderna observerbara utan att du behöver gissa utifrån konsolutskrifternas mängd.
Källor
- Value And Reference Types In Swift — Swift.org
- Demystify SwiftUI — Apple
onAppear(perform:)— AppleonDisappear(perform:)— AppleonScrollVisibilityChange(threshold:_:)— AppleLazyVStack— AppleState— Apple- Dive into lazy stacks and scrolling with SwiftUI — Apple
id(_:)— AppledocumentVisibleRect— AppleNSViewRepresentable— AppleNSTextViewportLayoutController— AppleenumerateTextSegments(in:type:options:using:)— AppletextContainerOrigin— AppleglyphRange(forBoundingRect:in:)— AppleNSTextView— Apple