Wydajność i interoperacyjność SwiftUI w iOS 27
LazyVStack nie wie, jak jest wysoki. Szacuje własną wysokość na podstawie średniego rozmiaru już rozmieszczonych widoków oraz liczby tych, które jego zdaniem jeszcze pozostają, a następnie koryguje to oszacowanie na żywo w trakcie przewijania.1 Trzy sesje WWDC26 zespołu UI Frameworks biorą ten jeden fakt (oraz jego odpowiedniki w grafice i interoperacyjności) i przekształcają go w działający model tego, jak SwiftUI zachowuje się pod obciążeniem w iOS 27: jak przewijanie pozostaje płynne, jak komponują się efekty GPU oraz jak SwiftUI wpasowuje się w aplikację AppKit lub UIKit, którą już wcześniej wydano.
Te trzy sesje czyta się jak jeden argument przedstawiony na trzy sposoby. Leniwe stosy działają dobrze, gdy przestaje się walczyć z ich szacowaniem; efekty shaderów komponują się, gdy traktuje się każdy modyfikator jako etap potoku; a interoperacyjność działa, gdy pozwala się, by szew niosły @Observable oraz protokoły reprezentujące. Żadna z tych trzech sesji nie jest zapowiedzią funkcji. Każda z nich to mechanizm wyjaśniony na tyle dobrze, że można przewidzieć zachowanie frameworka zamiast je zgadywać.
TL;DR / Kluczowe wnioski
LazyVStackocenia jedynie widoki wypełniające widoczny prostokąt; wysokości poza ekranem oraz przesunięcie zawartości są szacowane, więc bezwzględne odczyty przesunięcia zawartości są niestabilne i warto preferować API względnej widoczności, takie jak.onScrollTargetVisibilityChange.12- Wstępne pobieranie (prefetching) rozkłada pracę nad wyświetleniem widoku na wiele klatek przed jego pojawieniem się; widoki należy konfigurować w ich inicjalizatorze (a nie w
onAppear), aby wstępnie pobrana praca nie została odrzucona.1 - Należy unikać dynamicznej liczby podwidoków w liściu
ForEach: filtrowanie warunkiem wbodyutrzymuje widoki przy życiu według indeksu, więc lepiej filtrować na poziomie danych (PredicatewQuery).1 - SwiftUI udostępnia trzy punkty wejścia dla shaderów (
colorEffect,distortionEffect,layerEffect) o rosnącej mocy; jedynielayerEffectpotrafi próbkować sąsiednie piksele, czego wymagają rozmycie i deformacja dziedziny (domain-warp).3456 - Shadery są bezstanowe, więc animacja powstaje przez podanie znacznika czasu z
TimelineViewjako parametru; nic nie przenosi się między klatkami.37 - AppKit i UIKit otrzymują automatyczne przerysowanie dzięki
@Observable(koniec z ręcznymneedsDisplay), a SwiftUI wnika do istniejącej aplikacji poprzezNSHostingView,NSGestureRecognizerRepresentable,NSHostingMenuorazNSHostingSceneRepresentation.8910
Leniwe stosy działają na oszacowaniach
LazyVStack rozmieszcza zawartość od góry do dołu i zatrzymuje się, gdy widoczny prostokąt zostanie wypełniony, szacując resztę.
Pierwsza rzecz, którą trzeba przyswoić na temat leniwego stosu, to fakt, że celowo wymienia on poprawność na wydajność. W przeciwieństwie do VStack, LazyVStack nie ocenia ani nie renderuje widoków, które nie są widoczne; rozmieszcza swoje widoki od góry do dołu i zatrzymuje się, gdy widoczny prostokąt zostanie wypełniony, dodając widoki w miarę ich wsuwania się na ekran i usuwając je, gdy z niego wysuwają.1 Korzyść jest oczywista. Koszt jest subtelny: ponieważ stos nigdy nie ładuje wszystkich widoków, wysokości widoków poza ekranem są szacowane na podstawie średniej z tych, które wystąpiły wcześniej, idealna szerokość zapada się do szerokości pierwszego podwidoku, a przestrzeń nad widocznym obszarem sama w sobie jest przybliżona.1
To szacowanie nie jest błędem, który omija się raz; to podłoże, na którym opiera się każda inna decyzja dotycząca leniwego stosu. Sesja 321 czyni tę konsekwencję namacalną na przykładzie zmiany orientacji. Po obróceniu iPhone’a najwyższy widoczny widok pozostaje zakotwiczony, ale stos nie zmierzył jeszcze dokładnego nowego układu widoków znajdujących się powyżej. Po przewinięciu z powrotem na górę stos musi dokonać uzgodnienia: koryguje oszacowaną przestrzeń nad widocznym obszarem i aktualizuje przesunięcie zawartości widoku przewijania o tę samą wartość, tak aby przesunięcie zawartości na górze wylądowało na zerze.1 Leniwy stos i otaczający go widok przewijania precyzyjnie koordynują pozycję i przesunięcie, tak by w miarę aktualizacji oszacowań względna pozycja widocznych podwidoków nigdy nie skakała.1
Praktyczny wniosek to reguła dotycząca tego, którym API przewijania ufać. Ponieważ bezwzględne przesunięcie zawartości jest szacowane, jego odczyt (na przykład za pomocą .onScrollGeometryChange, by ukryć przycisk po 100 punktach) daje próg, który dryfuje w miarę ustalania się oszacowań.2 Stabilnym sygnałem jest względna widoczność. Modyfikator .onScrollTargetVisibilityChange uruchamia się, gdy zmienia się zbiór podwidoków widocznych w widoku przewijania, więc przycisk „przewiń, aby zaprezentować” może powiązać swoją widoczność z tym, które wiersze są na ekranie, z progiem (sesja używa 80%) zamiast z niestabilną liczbą pikseli.21 Ta sama logika obciąża .scrollTransition: transformacja wypychająca widok poza jego pierwotną ramkę może sprawić, że stos uzna widoczny widok za znajdujący się poza ekranem i usunie go zbyt wcześnie, dlatego każde przejście przewijania musi powstrzymywać widoki, które normalnie nie byłyby widoczne, od bycia wepchniętymi do widocznego prostokąta.111
Dlaczego struktury widoków nie są podwidokami
Podwidoki ładowane przez leniwy stos nie odwzorowują się jeden do jednego na struktury widoków, które napisano. ForEach zawierający StepView rozwija się do jednego StepView na każdy krok, ale jeśli każde body w StepView zwraca dwa widoki najwyższego poziomu (diagram oraz instrukcje) bez otaczającego układu, stos ładuje każdy z nich osobno.1 Dla stosu liczy się rozwinięta liczba podwidoków, a nie liczba struktur.
Pułapką jest dynamiczna liczba podwidoków. Jeśli StepView zwraca jeden podwidok lub zero w zależności od wartości środowiska, stos nie może już ufać indeksom, ponieważ liczba wcześniejszych widoków mogłaby się zmienić. Dlatego utrzymuje wcześniejsze instancje StepView przy życiu na wszelki wypadek, co oznacza, że niepowiązana zmiana środowiska może wyzwolić ocenę body dla widoków przewiniętych poza ekran, a stos nie zwolni ich stanu.1 Rozwiązaniem jest przeniesienie filtra z widoku na dane: jeśli używa się SwiftData, warunek należy umieścić w Predicate w Query, tak by liczba podwidoków była znana bez konstruowania jakichkolwiek widoków.1 Rozpakowanie wartości opcjonalnej w body ma ten sam efekt utrzymywania przy życiu; czystszym posunięciem jest pokazanie ContentUnavailableView wyżej w hierarchii, zamiast pozwalać leniwemu stosowi przechowywać częściowo rozwinięte wiersze.1
Wstępne pobieranie to mechanizm, który sprawia, że oszacowania sprawiają wrażenie szybkich. Podczas przewijania widok przewijania ma czas jedynie do terminu klatki na aktualizację przesunięcia, wyrenderowanie widoków i wykonanie pracy reagującej na zmianę przesunięcia; jeśli wyświetlenie nowego widoku przekroczy ten budżet, klatka zostaje pominięta i widać zacięcie.1 Aby temu zapobiec, leniwy stos sprawdza, czy jest wolny czas, i jeśli tak, wykonuje z wyprzedzeniem część pracy nad widokiem, który wkrótce się pojawi (ocenia jego body i układ, a nawet rozkłada zagnieżdżony LazyHStack na wiele klatek), tak by w chwili pojawienia się widoku większość pracy była już wykonana.1 Dlatego właśnie sesja kładzie nacisk na onAppear: jeśli widok konfiguruje się w onAppear, odrzuca się wstępnie pobraną pracę i wymusza jej ponowne wykonanie w chwili pojawienia, czasem wciągając więcej widoków niż potrzeba i pogarszając przewijanie. Widok należy konfigurować w jego inicjalizatorze, tak by przybywał w rozsądnym stanie, a onAppear rezerwować dla pracy faktycznie związanej z pojawieniem się, takiej jak pobieranie następnej strony w nieskończonym przewijaniu.1 Przy odwróconym przewijaniu body może nawet wykonać się podczas wstępnego pobierania, podczas gdy onAppear w ogóle się nie uruchomi.1
Zaawansowana grafika to tylko potok
Sesja 322 przeformułowuje „zaawansowaną grafikę” jako kompozycję. Każdy modyfikator SwiftUI to rura, która przyjmuje dane, przekształca je i przekazuje dalej; zaawansowany rezultat tkwi w tym, jak łączy się rury, a nie w jakimkolwiek pojedynczym złożonym API.3 Sesja buduje widok tekstów na żywo w stylu Apple Music, łącząc zwyczajne etapy w łańcuch: rozmywa okładkę, by się cofnęła, uruchamia na niej shader, steruje tym shaderem za pomocą czasu i synchronizuje przewijanie transkrypcji z tym samym źródłem czasu.3
Etap shadera jest miejscem, w którym leży prawdziwy wybór. SwiftUI wywołuje funkcje shaderów Metal przez trzy punkty wejścia efektów, które rosną pod względem możliwości. colorEffect przekształca kolor każdego piksela na podstawie jego pozycji i pierwotnego koloru, co wystarcza do czegoś takiego jak konwersja do skali szarości.4 distortionEffect zamiast tego odwzorowuje jedną pozycję na drugą (nakazuje się SwiftUI próbkować kolor tej pozycji z tamtej pozycji), co obsługuje deformacje geometryczne bez udziału koloru.5 layerEffect jest najbardziej elastyczny: przekazuje shaderowi całą warstwę widoku, dzięki czemu piksel wyjściowy może próbkować swoich sąsiadów lub cały obszar, a właśnie tego wymagają rozmycie i bogatsze deformacje.63
Tło z deformacją dziedziny w sesji wykorzystuje layerEffect. Jednolite przesunięcie float2 przemieszcza każdy piksel o tę samą wartość, co jedynie przesuwa obraz; organiczny ruch wymaga zmienności na poziomie pojedynczego piksela, więc shader próbkuje wstępnie obliczoną NoiseTexture (przekazaną jako obraz, docierającą po stronie Metal jako texture2d), której kanały czerwony i zielony dostarczają inne przesunięcie X i Y w każdej współrzędnej UV.3 Jednokrotne próbkowanie szumu skręca obraz; dwukrotne próbkowanie, za drugim razem w pozycji przesuniętej przez pierwszą próbkę, daje płynące plamy. Ta technika drugiego rzędu to deformacja dziedziny (domain warping), a sesja wskazuje na możliwą do pobrania przykładową aplikację z podglądem parametrów na żywo.3
Dwa fakty dotyczące frameworka sprawiają, że animacja działa. Shadery są bezstanowe: nie zachowują pamięci poprzedniej klatki, a wynik zależy wyłącznie od przekazanych parametrów.3 Ruch nie może więc pochodzić z wnętrza shadera; musi zostać podany, a TimelineView to rura, która go dostarcza, uruchamiając się przy każdej klatce ze znacznikiem czasu według harmonogramu animacji.73 Wystarczy podać ten znacznik czasu do shadera, dodać go do pozycji próbkowania szumu, a wzór zaczyna płynąć. Strona transkrypcji wykorzystuje to samo źródło czasu z drugiej strony: znacznik czasu odtwarzania wybiera bieżącą linię (pogrubioną i wyraźną, reszta przygaszona), a onChange utrzymuje tę linię wyśrodkowaną w miarę upływu czasu.123 Pływający znacznik czasu na aktywnej linii jest pozycjonowany nie za pomocą offset (który wymagałby rozmiarów obu widoków), lecz przez nadpisanie wytycznej wyrównania, które semantycznie redefiniuje punkt wyrównania, tak że górna krawędź podwidoku przylega do dolnej krawędzi kontenera bez ręcznego przesunięcia.133
SwiftUI wnika do aplikacji AppKit lub UIKit
Sesja poświęcona interoperacyjności otwiera się stwierdzeniem, które przeformułowuje całe pytanie o adopcję: większość aplikacji już teraz używa SwiftUI w sposób ukryty. W nowym projekcie kontrolki AppKit, takie jak NSSlider, NSSwitch i NSSegmentedControl, są renderowane z użyciem SwiftUI pod spodem, a Liquid Glass również dzieli duże części swojej implementacji między frameworkami za pośrednictwem SwiftUI.8 Zatem „adopcja SwiftUI” jest nie tyle przepisaniem, ile decyzją o tym, gdzie uczynić szew jawnym.
Pierwszy krok nie wymaga w ogóle SwiftUI. AppKit i UIKit obserwują teraz typy @Observable automatycznie: wystarczy oznaczyć klasę modelu jako @Observable, odczytać jej właściwości wewnątrz metody rysującej, takiej jak drawKnob, a AppKit śledzi każdy dostęp i przerysowuje, gdy zmienia się którakolwiek z odczytanych właściwości, wycofując ręczne needsDisplay = true, które trzeba było pisać, ilekroć wartość jednego suwaka wpływała na wygląd drugiego.814 Obserwacja wykracza poza draw(_:) i obejmuje updateConstraints(), layout(), updateLayer() oraz odpowiedniki w NSViewController, a w UIKit sięga jeszcze dalej, do UIButton, UICollectionViewCell i nie tylko.8 Jest włączona domyślnie w wydaniach z 2026 roku i możliwa do wstecznego wdrożenia w macOS 15 (NSObservationTrackingEnabled) oraz iOS 18 (UIObservationTrackingEnabled) za pośrednictwem Info.plist.8
Gdy model jest już @Observable, faktyczny szew SwiftUI jest niewielki. Sesja przebudowuje oparty na suwakach selektor kolorów na okrągłą kontrolkę SwiftUI rysowaną za pomocą Canvas (API trybu natychmiastowego analogicznego do drawRect, z withCGContext do ponownego wykorzystania istniejącego kodu Core Graphics), wykorzystując ponownie ten sam @Observable ColorModel.158 Aby osadzić ją tam, gdzie AppKit oczekuje widoku, wystarczy opakować ją w NSHostingView, podklasę NSView; ponieważ model już steruje aktualizacjami, to opakowanie jest wszystkim, co jest potrzebne.168 Istniejący kod gestów przenosi się bez przepisywania: ForceClickGestureRecognizer dociera do widoku SwiftUI poprzez NSGestureRecognizerRepresentable (implementując makeNSGestureRecognizer oraz handleNSGestureRecognizerAction), po czym dołącza się go zwykłym modyfikatorem .gesture i współistnieje on z własnym gestem przeciągania SwiftUI.178 Ta sama rodzina protokołów reprezentujących obejmuje NSViewRepresentable do osadzania widoków NSView w przeciwnym kierunku.8
Szew skaluje się w górę do menu i scen. Widok SwiftUI View zawierający Button i Picker staje się prawdziwym menu poprzez NSHostingMenu (podklasę NSMenu), ustawioną jako podmenu elementu NSMenuItem dodanego do menu głównego, przy czym keyboardShortcut nadaje akcji ścieżkę niezależną od gestów dla urządzeń wejściowych, które nie potrafią wykonać force-click.188 Całe sceny SwiftUI również się dołącza: MenuBarExtra dociera do istniejącej aplikacji poprzez NSHostingSceneRepresentation, dodawane za pomocą addSceneRepresentation w applicationWillFinishLaunching, przy czym Toggle w scenie Settings kontroluje, czy dodatek jest wstawiany, a akcja środowiskowa openSettings() otwiera ustawienia z @IBAction.198 Zamykające stwierdzenie sesji jest tym, na którym wszystko się opiera: każde API, które omawia, pojawia się w wydaniach z 2026 roku lub wcześniej, i nie ma oczekiwania, że aplikacja musi być w całości oparta na SwiftUI, by na tym skorzystać.8 Gdzie indziej w tym cyklu nacisk na modernizację ma jednak ostrzejszy charakter: iOS 27 czyni cykl życia oparty na scenach w UIKit wymogiem uruchamiania, więc aplikacja przebudowana przy użyciu najnowszego SDK nie uruchomi się, jeśli nigdy nie zaadoptowała scen.
Co zespół SwiftUI dodał w laboratoriach
Dwa wyjaśnienia z laboratoriów grupowych grupy UI Frameworks na WWDC26 wyostrzają model unieważniania opisany w sesjach. Oba są parafrazą lokalnie sporządzonej transkrypcji nagrania; Apple nie publikuje oficjalnych napisów do laboratoriów — ta sama luka w napisach przewijała się przez własne laboratorium grupowe zespołu Swift, gdzie inżynierowie języka odpowiadali na pytania dotyczące współbieżności i mapy drogowej.
Przeniesienie kodu z body do właściwości obliczanej nie przynosi żadnej korzyści w zakresie unieważniania. SwiftUI nadal ponownie uruchamia tę właściwość, ilekroć ponownie uruchamia body, więc to przeniesienie to wyłącznie zysk dla czytelności i nic więcej.20 Granica wydajności pojawia się o jeden poziom wyżej: gdy wyodrębni się osobny typ widoku, SwiftUI może unieważnić go niezależnie, ponownie uruchamiając tylko ten widok, gdy zmieniają się jego dane wejściowe, zamiast całego otaczającego body. Gdy chce się, by SwiftUI wykonywał mniej pracy, należy sięgać po nowy widok, a nie po nową właściwość.
Każda zmiana środowiska unieważnia wszystkie widoki, które odczytują tę wartość środowiska. Odczyt środowiska jest tani; rotacja środowiska już nie.21 Szybko zmieniające się wartości należy trzymać poza środowiskiem (przykładem z panelu jest bieżący czas), ponieważ wartość aktualizująca się przy każdej klatce wciąga każdego czytelnika w ponowną ocenę za każdym razem, gdy się zmienia. Wartości zmienne warto przekazywać ścieżką, która ich potrzebuje, a środowisku pozostawić to, co pozostaje niezmienne.
Późniejsza sesja laboratoryjna dodaje trzy kolejne szczegóły mechanizmu, wszystkie parafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026, do którego Apple nie publikuje oficjalnych napisów.
Częściowa ocena grafu wyjaśnia, dokąd faktycznie trafia wstępnie pobrana praca. Panel opisał leniwy stos oceniający body nadchodzących komórek w czasie pozostałym po wyrenderowaniu bieżącej klatki, a następnie zatrzymujący się tuż przed rozpoczęciem kolejnej klatki.22 Haczyk, który zasygnalizowali: onAppear, które rekonfiguruje komórkę tak, że trzeba ją ponownie rozmieścić, ponieważ zmienia jej rozmiar, odrzuca tę wstępnie pobraną pracę. Ich wskazówka brzmiała: pracę nad ustalaniem rozmiaru wykonywać w inicjalizatorze komórki, a nie w body czy onAppear, tak by wstępne pobranie przetrwało.22
Model myślowy interoperacyjności pochodził od panelisty z ponad dekadą doświadczenia w UIKit. UIKit rozmieszcza zawartość z góry w dół, od okna w głąb ku liściom, podczas gdy SwiftUI buduje od dołu w górę, od najgłębiej zagnieżdżonego węzła na zewnątrz.22 Gdy oba się przeplatają, panel nazwał ten układ naprzemiennych warstw kanapką lub ciastem, dlatego głębokie przeplatanie staje się subtelne: każdy framework chce sterować przejściem układu z przeciwnego końca.22
Powyższy punkt o środowisku zyskał barwny, rzeczywisty przykład. Zapytani o najgorsze nadużycie środowiska, jakie widzieli, paneliści przytoczyli umieszczenie pozycji przewijania w środowisku, która aktualizuje się przy każdej klatce podczas przewijania i tym samym unieważnia każdego czytelnika przy każdej klatce.22
Druga sesja laboratorium grupowego SwiftUI dodała cztery kolejne szczegóły mechanizmu, wszystkie parafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026 (sesja 2); Apple nie publikuje oficjalnych napisów do laboratoriów.
Modyfikator onGeometryChange(for:of:action:) czyta się lepiej, niż brzmi, ze względu na to, jak jego dwa domknięcia dzielą pracę. Domknięcie transformujące uruchamia się z aktualną geometrią przy każdej klatce, ale tylko wartość, którą zwraca, decyduje o tym, czy akcja się uruchomi, ponieważ typ wyniku jest Equatable, a akcja wykonuje się jedynie wtedy, gdy ta wartość się zmieni.23 Zatem zwrócenie wartości zgrubnej (przedziału rozmiaru lub punktu granicznego układu, a nie surowego rozmiaru) przekształca sygnał o częstotliwości klatek w taki, który uruchamia się dwukrotnie, na progach, zamiast w sposób ciągły. Panel połączył to z ostrzeżeniem, że GeometryReader jest kosztowny dla podwidoków, które opakowuje, i powinien być ograniczony do tła, tak by mierzył bez sterowania głównym układem.23
Dobra właściwość dynamiczna może wprost zastąpić większość pracy onChange. Metoda update() właściwości DynamicProperty uruchamia się bezpośrednio przed body widoku, więc niestandardowy opakowywacz właściwości może dostarczyć już zbuforowaną wartość (przykładem z panelu był obraz) synchronicznie w tym momencie i pominąć podróż w obie strony przez onAppear oraz wyzwalane przez nią ponowne renderowanie.24 Ujęcie panelu brzmiało, że większość zastosowań onChange można zastąpić dobrze zbudowaną właściwością dynamiczną.24
Widok, który rysuje poza zgłoszonymi granicami swojego układu wewnątrz ScrollView, może zostać odcięty (culling), ponieważ system uznaje go za znajdujący się poza ekranem na podstawie granic, o których został poinformowany, a nie na podstawie tego, gdzie faktycznie maluje.25 Panel wskazał to jako konkretne zachowanie awaryjne stojące za niestandardowymi listami rozwijanymi i nakładkami, które wykraczają poza swój widok-gospodarza, a następnie znikają w trakcie przewijania, i zauważył, że to samo zagrożenie dotyczy zawartości overlay, która wykracza poza swoją kotwicę.25
Wyświetlenie pełnoekranowej nakładki ponad wszystkim, włącznie z arkuszami (sheets), nie ma czystego, czysto-SwiftUI rozwiązania. Wskazówka panelu brzmiała: zejść do nowego UIWindow poprzez cykl życia oparty na scenach w UIKit, z głębszą zasadą, że ostatnie okno wygrywa i musi istnieć jedno źródło prawdy co do tego, co znajduje się na wierzchu.26 Dodali, że lepszym rozwiązaniem jest często przywrócenie stosu nawigacji, którego oczekuje użytkownik, zamiast narzucania wszystkiemu zakłócającej pokrywy.26
Co zaadoptować w pierwszej kolejności
Wydanie wyjaśnione jako mechanizm nagradza porządkowanie według dźwigni, a nie nowości.
- Przełącz klasy modeli na
@Observablew kodzie AppKit/UIKit. Usuwa to ręczne wywołanianeedsDisplay, nadaje każdej obserwującej metodzie rysującej i układającej automatyczne przerysowanie i jest warunkiem wstępnym, który czyni późniejsze osadzenieNSHostingViewtrywialnym.814 To posunięcie o najniższym ryzyku i najwyższej natychmiastowej korzyści ze wszystkich trzech sesji. - Przeprowadź audyt leniwych stosów pod kątem dynamicznej liczby podwidoków. Każdy liść
ForEach, który warunkowo zwraca zero lub jeden podwidok albo rozpakowuje wartość opcjonalną w swoimbody, utrzymuje widoki przy życiu według indeksu; przeniesienie filtra doPredicatewQuery(lub wyżej w hierarchii) poprawia zarówno zużycie pamięci, jak i wydajność przewijania do elementu.1 - Przenieś konfigurację widoku z
onAppeardo inicjalizatorów. Wstępne pobieranie pomaga tylko wtedy, gdy wstępnie pobrana praca przetrwa; konfiguracja, która zmienia rozmiar lub zawartość wonAppear, odrzuca tę pracę.1 To cicha, szeroko zakrojona wygrana dla płynności przewijania. - Zastąp bezwzględne odczyty przesunięcia przewijania API względnej widoczności. Wszystko powiązane z przesunięciem zawartości będzie dryfować;
.onScrollTargetVisibilityChangewiąże się z tym, które wiersze są faktycznie widoczne.21 - Sięgaj po shadery tylko tam, gdzie drobny efekt zasługuje na swoje miejsce. Zacznij od
colorEffectlubdistortionEffect; eskaluj dolayerEffecttylko wtedy, gdy efekt musi próbkować sąsiadów, a wszelki ruch steruj znacznikiem czasu zTimelineView, zamiast oczekiwać stanu wewnątrz shadera.4567
Wątek przewodni wszystkich trzech sesji: przewiduj zachowanie frameworka, zanim zaczniesz na niego naciskać. Leniwe stosy szacują, shadery zapominają, a interoperacyjność to szew, a nie przepisywanie. Buduj z tymi trzema faktami na uwadze, a reszta wynika z nich sama.
FAQ
Dlaczego pozycja przewijania mojego leniwego stosu SwiftUI skacze lub dryfuje?
Ponieważ LazyVStack nie ładuje widoków poza ekranem, szacuje ich wysokości oraz przestrzeń nad widocznym obszarem, więc bezwzględne przesunięcie zawartości jest oszacowaniem, które framework koryguje, w miarę jak poznaje rzeczywisty układ (po zmianie orientacji, na przykład, uzgadnia oszacowanie, gdy przewinie się z powrotem na górę).1 Jeśli powiąże się interfejs z bezwzględnym przesunięciem, próg dryfuje w miarę ustalania się oszacowań. Zamiast tego należy używać .onScrollTargetVisibilityChange, który uruchamia się na podstawie tego, które podwidoki są faktycznie widoczne.2
Jak utrzymać płynne przewijanie w leniwym stosie SwiftUI w iOS 27?
Pozwól, by wstępne pobieranie wykonywało swoją pracę: widoki konfiguruj w ich inicjalizatorze, tak by praca, którą leniwy stos wykonuje przed pojawieniem się widoku, nie została odrzucona, i unikaj zmieniania rozmiaru lub zawartości widoku w onAppear.1 Unikaj też dynamicznej liczby podwidoków w liściach ForEach oraz zmian układu (takich jak wysokość sterowana przez onGeometryChange) po pojawieniu się widoku, ponieważ oba zmuszają stos do ponownego wykonania pracy lub przeliczenia pozycji w trakcie przewijania.1
Kiedy stosować colorEffect, distortionEffect czy layerEffect?
colorEffect należy stosować do przekształcania koloru każdego piksela na podstawie jego pozycji i pierwotnego koloru (na przykład filtr skali szarości).4 distortionEffect należy stosować do efektów geometrycznych, w których odwzorowuje się pozycję wyjściową na pozycję źródłową, z której ma nastąpić próbkowanie.5 layerEffect należy stosować, gdy piksel wyjściowy zależy od więcej niż jednego piksela wejściowego, ponieważ daje on shaderowi całą warstwę widoku do próbkowania sąsiadów lub całego obszaru, czego wymagają rozmycie i deformacja dziedziny.6
Jak animować shader Metal w SwiftUI?
Shadery są bezstanowe: nie zachowują pamięci poprzedniej klatki i zależą wyłącznie od swoich parametrów, więc nie da się animować z wnętrza shadera.3 Należy podać wartość, która zmienia się w czasie. TimelineView na harmonogramie animacji uruchamia się przy każdej klatce ze znacznikiem czasu; ten znacznik czasu należy przekazać do shadera jako parametr (sesja dodaje go do pozycji próbkowania szumu), a efekt animuje się.73
Czy mogę dodać SwiftUI do istniejącej aplikacji AppKit lub UIKit bez jej przepisywania?
Tak, a sesja wyraźnie stwierdza, że żadna aplikacja nie musi być w całości oparta na SwiftUI, by na tym skorzystać.8 Wystarczy oznaczyć model jako @Observable, aby AppKit i UIKit przerysowywały się automatycznie, a następnie osadzić widoki SwiftUI za pomocą NSHostingView, przenieść istniejące rozpoznawacze gestów za pomocą NSGestureRecognizerRepresentable, zbudować menu za pomocą NSHostingMenu i dołączyć sceny SwiftUI z delegata aplikacji za pomocą NSHostingSceneRepresentation; wszystkie z nich pojawiają się w wydaniach z 2026 roku lub wcześniej.816171819
Pełny klaster Apple Ecosystem: substrat SwiftUI (konstruktory wyników, typy nieprzezroczyste, drzewo widoków oparte na typach wartościowych), który wyjaśnia, dlaczego leniwy stos rozwija struktury widoków w inny zbiór podwidoków; powierzchnia SwiftUI w iOS 27 (zmiana kolejności, dokumenty, paski narzędzi, błędy), obok której stoi ta historia o wydajności i interoperacyjności; wnętrze @Observable, które teraz steruje automatycznym przerysowaniem również w AppKit i UIKit; oraz wzorce Liquid Glass, których międzyframeworkową implementację sesja o interoperacyjności przypisuje współdzielonemu SwiftUI. Centrum stanowi seria Apple Ecosystem. Szerszy kontekst iOS z agentami AI znajduje się w przewodniku iOS Agent Development.
Bibliografia
-
Apple, WWDC26 session 321, “Dive into lazy stacks and scrolling with SwiftUI.” developer.apple.com/videos/play/wwdc2026/321. Obejmuje układ leniwego stosu i szacowanie wysokości, szacowane przesunięcie zawartości, rozwijanie struktur widoków w podwidoki, pułapkę dynamicznej liczby podwidoków, wstępne pobieranie przez terminy klatek oraz konfigurację w
onAppearkontra inicjalizator. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, WWDC26 session 321, “Dive into lazy stacks and scrolling with SwiftUI”. Sesja przedstawia
onScrollTargetVisibilityChange(modyfikator, którego domknięcie uruchamia się, gdy zmienia się zbiór widocznych celów przewijania) jako stabilną alternatywę względnej widoczności dla bezwzględnych odczytów przesunięcia zawartości. ↩↩↩↩↩ -
Apple, WWDC26 session 322, “Compose advanced graphics effects with SwiftUI.” developer.apple.com/videos/play/wwdc2026/322. Ujmuje efekty jako komponowalny potok; obejmuje rozmycie, trzy punkty wejścia dla shaderów, technikę deformacji dziedziny z
NoiseTexture, bezstanowe shadery sterowane czasem oraz dołączanie przez wytyczną wyrównania. ↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
colorEffect(_:isEnabled:). Zwraca nowy widok, który stosuje shader przekształcający kolor każdego piksela na podstawie jego pozycji i pierwotnego koloru. ↩↩↩↩ -
Apple Developer Documentation:
distortionEffect(_:maxSampleOffset:isEnabled:). Stosuje shader, który odwzorowuje pozycję każdego piksela na pozycję źródłową, z której ma nastąpić próbkowanie, do efektów geometrycznych. ↩↩↩↩ -
Apple Developer Documentation:
layerEffect(_:maxSampleOffset:isEnabled:). Stosuje shader jako efekt warstwy z dostępem do całej warstwy widoku, pozwalając każdemu pikselowi wyjściowemu próbkować wiele pikseli wejściowych. ↩↩↩↩ -
Apple Developer Documentation:
TimelineView. Widok, który aktualizuje swoją zawartość zgodnie z harmonogramem; na harmonogramie animacji dostarcza znacznik czasu dla każdej klatki, który sesja 322 podaje do swojego shadera. ↩↩↩↩ -
Apple, WWDC26 session 272, “Use SwiftUI with AppKit and UIKit.” developer.apple.com/videos/play/wwdc2026/272. Obejmuje automatyczne przerysowanie
@Observablew AppKit/UIKit, wsteczne wdrożenie obserwacji za pośrednictwem Info.plist,Canvas,NSHostingView,NSGestureRecognizerRepresentable,NSHostingMenuorazNSHostingSceneRepresentation. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
NSGestureRecognizerRepresentable. Protokół, który opakowujeNSGestureRecognizerdo użycia jako gest SwiftUI, implementowany za pomocąmakeNSGestureRecognizerorazhandleNSGestureRecognizerAction. ↩ -
Apple Developer Documentation:
MenuBarExtra. Scena, która renderuje element paska menu; sesja 272 dołącza ją do istniejącej aplikacji AppKit poprzezNSHostingSceneRepresentation. ↩ -
Apple Developer Documentation:
scrollTransition(_:axis:transition:). Stosuje przejście w miarę przewijania widoku w widoku przewijania; sesja 321 ostrzega, że transformacja wpychająca widok do widocznego prostokąta może rozsynchronizować leniwy stos. ↩ -
Apple Developer Documentation:
onChange(of:initial:_:). Uruchamia akcję, gdy wartość się zmienia; sesja 322 używa jej do ponownego wyśrodkowania bieżącej linii transkrypcji. ↩ -
Apple Developer Documentation:
alignmentGuide(_:computeValue:). Ustawia wytyczną wyrównania widoku tak, by system układu pozycjonował go semantycznie; sesja 322 nadpisuje dolną wytyczną, by przyczepić górną krawędź podwidoku do dolnej krawędzi jego kontenera. ↩ -
Apple Developer Documentation:
Observable. Makro, które sprawia, że zmienne właściwości klasy uczestniczą w systemie Observation, śledzonym przez AppKit i UIKit dla automatycznego przerysowania w wydaniach z 2026 roku. ↩↩ -
Apple Developer Documentation:
Canvas. Widok rysujący w trybie natychmiastowym, którego domknięcie otrzymujeGraphicsContext; sesja 272 używa go do przerysowania okrągłego selektora kolorów i odnotowujewithCGContextdo ponownego wykorzystania kodu Core Graphics. ↩ -
Apple Developer Documentation:
NSHostingView. PodklasaNSView, która hostuje hierarchię widoków SwiftUI wewnątrz drzewa widoków AppKit. ↩↩ -
Apple Developer Documentation:
NSViewRepresentable. Opakowanie, które pozwala widokowiNSViewuczestniczyć w hierarchii widoków SwiftUI; sesja 272 wymienia je obokNSGestureRecognizerRepresentablejako część rodziny protokołów reprezentujących. ↩↩ -
Apple Developer Documentation:
NSHostingMenu. PodklasaNSMenu, która renderuje widok SwiftUI jako zawartość menu, dodawana do menu głównego jako podmenu elementuNSMenuItem. ↩↩ -
Apple Developer Documentation:
keyboardShortcut(_:modifiers:). Przypisuje skrót klawiaturowy do akcji kontrolki; sesja 272 dodaje jeden do przycisku menu, tak by urządzenia wejściowe, które nie potrafią wykonać force-click, nadal docierały do funkcji. ↩↩ -
Apple, WWDC 2026 UI Frameworks group lab, session 8002. Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania; nie opublikowano oficjalnej transkrypcji. Zespół wyjaśnił, że przeniesienie kodu z
bodydo właściwości obliczanej to wyłącznie zmiana dla czytelności, a granica niezależnego unieważniania pojawia się, gdy wyodrębni się osobny typ widoku. ↩ -
Apple, WWDC 2026 UI Frameworks group lab, session 8003. Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania; nie opublikowano oficjalnej transkrypcji. Zespół zauważył, że każda zmiana środowiska unieważnia wszystkie widoki odczytujące tę wartość, i doradził trzymanie szybko zmieniających się wartości (przykładem z panelu był bieżący czas) poza środowiskiem. ↩
-
Apple, WWDC26 session 8006, “SwiftUI Group Lab.” developer.apple.com/videos/play/wwdc2026/8006. Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026; Apple nie publikuje oficjalnych napisów do laboratoriów. Źródło dla częściowej oceny grafu w pozostałym czasie klatki (oraz ostrzeżenia o zmianie rozmiaru w
onAppear, z ustalaniem rozmiaru wykonywanym w inicjalizatorze), układu interoperacyjnego „kanapka lub ciasto” w postaci UIKit-z-góry-w-dół kontra SwiftUI-z-dołu-w-górę oraz przykładu z pozycją przewijania w środowisku, który unieważnia każdego czytelnika przy każdej klatce. ↩↩↩↩↩ -
Apple, WWDC 2026 SwiftUI Group Lab (session 2). Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026 (sesja 2); Apple nie publikuje oficjalnych napisów do laboratoriów. Źródło dla mechanizmu
onGeometryChange, w którym transformacja decyduje o akcji (zwróć zgrubną wartość, by uruchamiać się na progach), oraz wskazówki, by kosztownyGeometryReaderograniczyć do tła. Kształt dwóch domknięć jest udokumentowany przyonGeometryChange(for:of:action:): domknięcie transformująceof:wyprowadza wartośćEquatablez proxy geometrii, a domknięcieaction:uruchamia się tylko wtedy, gdy ta wartość się zmieni. ↩↩ -
Apple, WWDC 2026 SwiftUI Group Lab (session 2). Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026 (sesja 2); Apple nie publikuje oficjalnych napisów do laboratoriów. Źródło dla zastąpienia większości pracy
onChangewłaściwością dynamiczną, która dostarcza zbuforowaną wartość synchronicznie. ProtokółDynamicPropertyfirmy Apple definiuje metodęupdate(), którą SwiftUI wywołuje bezpośrednio przed wyrenderowaniembodywidoku, tak by właściwość przechowywała swoją najnowszą wartość. ↩↩ -
Apple, WWDC 2026 SwiftUI Group Lab (session 2). Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026 (sesja 2); Apple nie publikuje oficjalnych napisów do laboratoriów. Źródło dla odcinania (culling) widoków, które rysują poza zgłoszonymi granicami swojego układu wewnątrz
ScrollView(awaria stojąca za przepełniającymi się niestandardowymi listami rozwijanymi i nakładkami) oraz uwagi, że to samo zagrożenie dotyczy zawartościoverlaywykraczającej poza swoją kotwicę. ↩↩ -
Apple, WWDC 2026 SwiftUI Group Lab (session 2). Sparafrazowane z lokalnie sporządzonej transkrypcji nagrania laboratorium grupowego SwiftUI z WWDC 2026 (sesja 2); Apple nie publikuje oficjalnych napisów do laboratoriów. Źródło dla braku czystego, czysto-SwiftUI rozwiązania pełnoekranowej nakładki ponad arkuszami, zejścia do nowego
UIWindowpoprzez cykl życia oparty na scenach w UIKit, zasady „ostatnie okno wygrywa / jedno źródło prawdy” oraz preferencji dla przywracania stosu nawigacji zamiast zakłócającej pokrywy. ↩↩