Ponowna inicjalizacja wiersza SwiftUI nie dowodzi, że wiersz zniknął lub utracił swój stan. Aby zdiagnozować przewijany interfejs na komputerze Mac, osobno śledź tworzenie, zachowany stan, wywołania zwrotne pojawienia się i widoczność w obszarze przewijania.
Ten przewodnik jest przeznaczony dla programisty badającego powtarzające się logi wierszy we własnej aplikacji macOS. Celem jest ustalenie, czy aktualizacja jedynie tworzy kolejną wartość widoku, resetuje stan wiersza, czy zmienia to, które wiersze zajmują obszar przewijania. Przykład używa syntetycznych etykiet i zwykłych przycisków.
Co właściwie mówi każdy sygnał cyklu życia?
Wiersz SwiftUI zazwyczaj korzysta ze struktury, czyli typu wartości w języku Swift. Utworzenie kolejnej wartości struktury różni się od utworzenia trwałego obiektu referencyjnego. Dokumentacja Swift wyjaśnia tę różnicę w artykule Value And Reference Types In Swift.
SwiftUI dodaje własny model tożsamości. Apple wyjaśnia, że tymczasowe wartości widoku mogą reprezentować tę samą tożsamość w kolejnych aktualizacjach, a czas życia widoku zależy od tej tożsamości. W związku z tym log inicjalizatora nie dowodzi, że czas życia poprzedniego wiersza dobiegł końca. Zobacz Demystify SwiftUI.
Poszczególne sygnały odpowiadają na różne pytania:
| Sygnał | Na jakie pytanie odpowiada | Ograniczenie interpretacji |
|---|---|---|
| Inicjalizator wiersza | Czy utworzono kolejną wartość wiersza? | Nie dowodzi pojawienia się ani zastąpienia stanu. |
Ewaluacja body |
Czy opis tego widoku został poddany ewaluacji? | Nie zlicza wyświetlonych klatek. |
| Zachowany stan wiersza | Czy ta wartość diagnostyczna przetrwała aktualizację? | Opisuje ciągłość tego stanu, a nie zajętość obszaru przewijania. |
onAppear i onDisappear |
Czy SwiftUI zgłosił zmianę stanu pojawienia się? | Czas zależy od typu widoku. |
onScrollVisibilityChange |
Czy wiersz przekroczył wybrany próg widoczności w obszarze przewijania? | Mierzy udokumentowany warunek widoczności podczas przewijania. |
Apple dokumentuje, że onAppear kończy się przed pierwszą renderowaną klatką, a dokładny czas zależy od typu widoku. onDisappear jest wywoływane po zniknięciu widoku, również w czasie zależnym od jego typu. Żadna z tych metod nie dokumentuje zwalniania pamięci stanu. Nie utożsamiaj tych zdarzeń wywołań zwrotnych z wnioskami o czasie życia pamięci. Zobacz onAppear(perform:) i onDisappear(perform:).
Jeśli interesuje Cię obszar przewijania, jawnie wybierz próg. W systemie macOS 15 i nowszych onScrollVisibilityChange(threshold:_:) zgłasza, czy widok przekroczył podany próg widoczności; wartość domyślna to 0.5. Apple dokumentuje także początkowe wywołanie zwrotne, gdy widok się pojawi, a próg jest już osiągnięty. Zobacz onScrollVisibilityChange(threshold:_:).
W tym artykule „widoczny w obszarze przewijania” oznacza spełnienie tego progu przewijania. Nie jest to pomiar tego, czy ktoś spojrzał na wiersz.
Przygotuj mały eksperyment, który rozdziela te sygnały
Utwórz aplikację SwiftUI dla macOS przy użyciu SDK obejmującego API macOS 15 i ustaw minimalną wersję wdrożeniową na macOS 15 lub nowszą. Zastąp wygenerowane deklaracje aplikacji i widoku zawartości kompletnym programem poniżej. Pozostaw tylko jeden punkt wejścia @main.
Przykład używa ScrollView i LazyVStack, aby zachowanie obszaru przewijania było jawne. Apple opisuje LazyVStack jako tworzący elementy w miarę potrzeb. Ten eksperyment nie zakłada, że List zachowuje się identycznie pod względem ładowania lub utrzymywania widoków. Zobacz 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"
}
}
Dwa identyfikatory UUID służą różnym celom. constructionID oznacza każde wywołanie inicjalizatora. appearanceToken początkowo jest puste i zostaje przypisane podczas pierwszego zaobserwowanego pojawienia się, do zachowanego stanu wiersza. Nie oznacza czasu przydzielenia pamięci dla stanu. Licznik daje kolejną widoczną wartość, której ciągłość możesz sprawdzić.
Apple dokumentuje, że SwiftUI zarządza pamięcią dla State i aktualizuje zależną zawartość interfejsu po zmianie jego wartości. Dlatego diagnostyka przechowuje token i licznik w prywatnym stanie, a informacje o tworzeniu pozostają zwykłą zapisaną właściwością. Zobacz State.
Program sprawdzono pod kątem zgodności z udokumentowanymi interfejsami API, ale nie został tutaj skompilowany ani uruchomiony, ponieważ środowisko Swift nie było dostępne. Poniższe przykładowe logi opisują, jak interpretować własny przebieg; nie są zarejestrowanymi wynikami.
Wykonaj trzy kontrolowane próby i zinterpretuj wyniki
1. Zaktualizuj dane wejściowe wierszy bez zmiany ich tożsamości
Uruchom aplikację w Xcode i pozostaw otwartą konsolę debugowania. Kliknij dwa razy przycisk Count widocznego wiersza, zanotuj jego token, a następnie kliknij Update rows. Zmienią się numery rewizji przekazywane wierszom, a stałe identyfikatory całkowite pozostaną takie same.
Jeśli ponownie pojawią się komunikaty inicjalizatora, ale wybrany wiersz zachowa token i licznik, masz dowód na ponowne tworzenie wartości przy zachowaniu stanu diagnostycznego. Nie klasyfikuj tych komunikatów inicjalizatora jako odrębnych czasów widoczności na ekranie. Wyświetlana rewizja potwierdza, że wiersz otrzymał nowe dane wejściowe.
Token pojawienia się jest przypisywany za pośrednictwem stanu, co samo może spowodować kolejną aktualizację. To zamierzone zachowanie diagnostyczne. Nie oczekuj dokładnej liczby komunikatów inicjalizatora ani body i usuń te sondy po zakończeniu badania.
2. Przewiń wiersz poza obszar przewijania, a następnie z powrotem
Powoli przewijaj, aż wybrany wiersz przekroczy próg 50%. Porównaj komunikaty viewport z komunikatami pojawienia się, a potem wróć do wiersza. Ponownie sprawdź jego token i licznik.
Zachowany token po powrocie wskazuje na ciągłość tego stanu diagnostycznego podczas przewijania. Inny token oznacza, że poprzedni token nie został zachowany. Aby wyjaśnić reset w rzeczywistej aplikacji, zbadaj zmiany tożsamości, warunkowe usuwanie oraz zachowanie kontenera związane z utrzymywaniem widoków, zamiast wyciągać wnioski wyłącznie na podstawie przewijania.
Aktualne wskazówki Apple wyjaśniają, że stosy leniwe mogą wstępnie pobrać zawartość i układ widoku przed jego pojawieniem się. Jeśli kierunek przewijania się odwróci, wstępnie przygotowany widok może nigdy nie otrzymać onAppear. Apple wyjaśnia też, że stan widoku poza ekranem w leniwym stosie może ostatecznie zostać odrzucony, i zaleca przechowywanie danych, które muszą przetrwać przewijanie, w modelu lub widoku nadrzędnym. Zobacz Dive into lazy stacks and scrolling with SwiftUI.
Dzięki temu licznik lokalny dla wiersza jest przydatną sondą. W rzeczywistym interfejsie edycji przechowuj zmiany, które muszą przetrwać usunięcie wiersza, w obiekcie o dłuższym czasie życia, powiązanym ze stabilnym identyfikatorem elementu.
3. Celowo zresetuj tożsamość
Kliknij Reset identity. Program zmienia tożsamość przypisaną do całego widoku przewijania, co pozwala na celowe porównanie ze zwykłą aktualizacją rewizji. Apple wyraźnie dokumentuje, że zmiana wartości przekazanej do id(_:) resetuje tożsamość widoku wraz z jego stanem. Zobacz id(_:).
Sprawdź nowo pojawiające się wiersze pod kątem nowych tokenów i początkowych wartości liczników. Ponieważ reset obejmuje poddrzewo widoku przewijania, nie używaj go jako kontrolowanej próby zachowania pozycji przewijania.
Podczas analizowania własnego śladu użyj tej ściągi:
| Obserwacja | Wspierany wniosek | Następny krok |
|---|---|---|
| Nowe komunikaty inicjalizatora; token i licznik pozostają | Nastąpiło tworzenie, a stan diagnostyczny został zachowany. | Jeśli aktualizacje są powolne, sprawdź koszt inicjalizatora. |
| Powtarzają się wywołania zwrotne pojawienia się; token pozostaje | Ponowne pojawienie się nie zastąpiło tego tokenu. | W przypadku zachowania zależnego od progu użyj wywołań zwrotnych dotyczących widoczności w obszarze przewijania. |
| Token i licznik resetują się po zwykłej aktualizacji | Stan wiersza nie przetrwał zgodnie z oczekiwaniami. | Sprawdź identyfikatory danych, zmieniające się modyfikatory id oraz gałęzie warunkowe. |
| Wiersz został zainicjalizowany, ale nie zgłasza pojawienia się | Samo utworzenie nie potwierdziło pojawienia się. | Weź pod uwagę leniwe przygotowanie i późniejsze zmiany podczas przewijania. |
| Widoczność w obszarze przewijania zmienia się na false | Wybrany próg przewijania nie jest już spełniony. | Zdecyduj, czy ten próg odpowiada zamierzonemu zachowaniu. |
W przypadku dynamicznych danych zachowuj identyfikatory przypisane elementom podczas aktualizacji. Nowy identyfikator generowany przy każdym odczycie uniemożliwia zachowanie ciągłości. Sprawdź też tożsamość widoków nadrzędnych: stabilne identyfikatory wierszy nie zachowają wiersza wewnątrz poddrzewa, którego tożsamość celowo zastępujesz. Apple omawia stabilne identyfikatory danych i tożsamość strukturalną w artykule Demystify SwiftUI.
Jaka ścieżka jest obsługiwana we wcześniejszych wersjach macOS?
Modyfikator widoczności w obszarze przewijania z przykładu wymaga systemu macOS 15. Obniżenie minimalnej wersji wdrożeniowej wymaga gałęzi sprawdzającej dostępność z osobną implementacją; onAppear nie jest równoważnym pomiarem progu.
Jeśli Twoja aplikacja już zarządza interfejsem przewijania AppKit, NSScrollView.documentVisibleRect udostępnia część widoku dokumentu widoczną przez widok zawartości, wyrażoną we współrzędnych widoku dokumentu. Alternatywą opartą na geometrii jest porównywanie tego prostokąta z prostokątami własnych wierszy w tym samym układzie współrzędnych i ponowne obliczanie wyniku po przewinięciu oraz zmianach układu. Zobacz documentVisibleRect.
Na przykład jawnie wybrana reguła może uznawać wiersz za widoczny, gdy co najmniej połowa jego prostokątnego obszaru przecina prostokąt widocznej części dokumentu. To proponowana reguła aplikacji, a nie twierdzenie, że odtwarza implementację SwiftUI. Obsłuż jawnie puste prostokąty i przeliczanie współrzędnych.
SwiftUI udostępnia NSViewRepresentable, aby zintegrować widok AppKit tworzony i zarządzany przez aplikację. Udokumentowane metody jego tworzenia, aktualizowania, koordynacji i usuwania zapewniają obsługiwany most. Respektuj kontrolę SwiftUI nad układem reprezentowanego widoku; Apple ostrzega przed bezpośrednią zmianą jego ramki lub granic. Zobacz NSViewRepresentable.
To rozwiązanie awaryjne dotyczy widoku dokumentu AppKit, którym zarządzasz. Nie wymaga żadnych założeń dotyczących prywatnych widoków bazowych elementu List w SwiftUI.
Czy układ TextKit dowodzi, że tekst jest widoczny na ekranie?
TextKit również wymaga rozdzielenia przygotowania od geometrii obszaru przewijania. Ma to znaczenie, jeśli własny wiersz zawiera widok tekstowy AppKit i chcesz ustalić, które fragmenty tekstu przecinają widoczny obszar.
NSTextViewportLayoutController z TextKit 2, dostępny w systemie macOS 12 i nowszych, zarządza aktywnym obszarem układu, który zwykle obejmuje obszar widoczny wraz z obszarem przewijania poza jego granice. Jego viewportBounds uwzględnia obszar wykraczający poza widoczny, a opcjonalne viewportRange opisuje bieżący układ obszaru przewijania. Żadnego z nich nie należy określać jako dokładnego zestawu widocznych znaków. Zobacz NSTextViewportLayoutController.
W implementacji TextKit 2 używaj geometrii układu tekstu zamiast zdarzeń alokacji. NSTextLayoutManager może wyliczać segmenty tekstu dla podanego zakresu. Porównaj wynikowe prostokąty segmentów z rzeczywistym prostokątem przycinania, sprowadziwszy je wcześniej do tego samego układu współrzędnych. Uwzględnij textContainerOrigin widoku tekstowego; nie zakładaj, że współrzędne kontenera tekstu są takie same jak współrzędne widoku dokumentu. Zobacz enumerateTextSegments(in:type:options:using:) i textContainerOrigin.
W istniejącej implementacji TextKit 1 metoda NSLayoutManager.glyphRange(forBoundingRect:in:) przyjmuje prostokąt we współrzędnych kontenera tekstu. Apple stwierdza, że zwrócony ciągły zakres może obejmować glify poza tym prostokątem, więc jest to zakres kandydacki do dalszej analizy, a nie dokładna lista widoczności. Zobacz glyphRange(forBoundingRect:in:).
Podczas diagnostyki zachowaj używany silnik tekstu. Apple dokumentuje, że dostęp do właściwości layoutManager obiektu NSTextView lub kontenera tekstu w systemie macOS 12 i nowszych powoduje przejście do trybu zgodności korzystającego z NSLayoutManager. W przypadku ścieżki TextKit 2 używaj textLayoutManager i jego interfejsów API. Zobacz NSTextView.
Wybierz sygnał odpowiadający na pytanie dotyczące interfejsu: logi inicjalizatora do badania tworzenia, zachowany stan do oceny ciągłości, wywołania zwrotne pojawienia się do śledzenia zmian pojawienia się, a geometrię obszaru przewijania do oceny widoczności. Eksperyment ze stabilnym identyfikatorem i jednym celowym resetem tożsamości pozwala zaobserwować te różnice bez zgadywania na podstawie ilości tekstu w konsoli.
Źródła
- 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