Aby naprawić przesuwanie się zawartości SwiftUI poza ekran podczas zmiany rozmiaru okna na Macu, najpierw ustal, czy rysowanie wykracza poza granice układu, zawartość nie chce się zmniejszyć, czy osadzony widok AppKit przestaje dopasowywać się do swojego kontenera. Następnie napraw tę konkretną granicę.
Ten przewodnik jest przeznaczony dla programisty naprawiającego interfejs własnej aplikacji na Maca, której okno można zmieniać: chodzi o zachowanie czytelności etykiet, dostępności elementów sterujących i możliwości przewijania dłuższej zawartości. Poniższa sekwencja diagnostyczna wykorzystuje syntetyczne etykiety i rozróżnia układ SwiftUI, rozmiar okna oraz zawijanie tekstu w AppKit.
Ustal, która granica zawodzi podczas zmiany rozmiaru
Układ SwiftUI zaczyna się od propozycji rozmiaru przekazanej przez widok nadrzędny. Widok podrzędny wybiera rozmiar w odpowiedzi; propozycja nie jest poleceniem, by przyciąć wszystko do tego prostokąta. Apple opisuje propozycje rozmiaru minimalnego, maksymalnego i idealnego w ProposedViewSize. Wyjaśnia to, dlaczego zwężenie okna nie musi zwężać każdego widoku potomnego.
Zacznij od powtarzalnego testu: otwórz okno aplikacji, zwężaj je aż do pojawienia się pierwszego problemu, a następnie znów poszerz. Obserwuj pierwszy element, którego dotyczy problem, a nie końcowy układ. Czy etykieta wykracza poza swój kontener? Czy przesuwa się cały panel? Czy tekst zachowuje poprzednią szerokość zawijania?
Tymczasowo dodaj .border(Color.orange) do podejrzanego widoku podrzędnego, a .border(Color.blue) do otaczającej go ramki lub kontenera. Umieść modyfikatory na różnych poziomach, aby porównać ich prostokąty. W dokumentacji Apple dotyczącej modyfikatora offset pokazano tę technikę z obramowaniem, która pozwala odróżnić pierwotne położenie w układzie od przesuniętego rysunku.
| Co obserwujesz | Co sprawdzić w pierwszej kolejności | Przydatne kolejne działanie |
|---|---|---|
| Rysunek przesuwa się poza stabilny prostokąt | offset, position, obliczenia współrzędnych |
Usuwaj kolejno modyfikatory położenia |
| Etykieta pozostaje szeroka, gdy jej kontener się zwęża | Poziome fixedSize, jawne szerokości |
Pozwól etykiecie przyjąć ograniczoną szerokość |
| Elementy sterujące nie mieszczą się w poziomym wierszu | Łączna szerokość elementów i odstępy | Zastosuj układ pionowy |
| Dolna część zawartości znika przy zmniejszaniu wysokości | Brak obsługi nadmiarowej zawartości | Umożliw przewijanie obszaru zawartości |
| Panel SwiftUI osadzony w AppKit przesuwa się wewnątrz niego | Granice widoku hostującego i ograniczenia rozmiaru | Porównaj rozmiar hosta z wymaganiami zawartości |
| Widok tekstu AppKit zwęża się, ale wiersze pozostają szerokie | Dopasowywanie szerokości kontenera tekstu | Sprawdź konfigurację widoku tekstu i kontenera tekstu |
Są to hipotezy diagnostyczne wynikające z udokumentowanego działania, a nie gwarancja, że dany objaw ma tylko jedną przyczynę. Zmieniaj po kolei jeden podejrzany element i powtarzaj ten sam gest zmiany rozmiaru. Jeśli zastąpienie złożonego widoku przez Text("Sample panel") usuwa problem, stopniowo przywracaj jego kolejne warstwy.
Usuń reguły położenia i rozmiaru, które nie reagują na zmianę okna
Sprawdź przesunięcia i pozycjonowanie względem środka
offset przesuwa wyświetlaną zawartość, nie zmieniając pierwotnych wymiarów widoku. Dodatnie przesunięcie poziome może więc wypchnąć rysunek poza prawą krawędź, mimo że widok nadrzędny nadal rezerwuje pierwotne miejsce. Tymczasowo usuń przesunięcie; jeśli rysunek wróci do kontenera, zastąp tę korektę układem wyrażonym za pomocą stosów, wypełnienia lub wyrównania ramki. Zobacz opis działania modyfikatora offset w dokumentacji Apple.
position(x:y:) umieszcza środek widoku we współrzędnych układu współrzędnych jego widoku nadrzędnego. Uznanie tych współrzędnych za lewy górny róg wprowadza błąd zależny od rozmiaru widoku podrzędnego. Pozycja obliczona raz może też przestać pasować, gdy zmieni się rozmiar widoku nadrzędnego. W typowym panelu z etykietami i elementami sterującymi preferuj wyrównanie, które opisuje ich zamierzoną relację względem kontenera. Apple definiuje działanie oparte na środku w position(x:y:).
Na przykład .frame(maxWidth: .infinity, alignment: .leading) ustawia wyrównanie do początku w elastycznej ramce. Nie usuwa własnych wymagań dotyczących rozmiaru nadmiernie dużego widoku potomnego. Dokumentacja Apple dotycząca elastycznej ramki wyjaśnia, jak ramka ogranicza propozycje i pozycjonuje widok podrzędny.
Sprawdź stałe szerokości i idealny rozmiar tekstu
Sprawdź jawne ustawienia szerokości oraz poziome użycia fixedSize w gałęzi układu, w której występuje problem. Apple wyraźnie opisuje, że .fixedSize(horizontal: true, vertical: false) sprawia, iż tekst zachowuje idealną szerokość wymaganą dla całego ciągu znaków, co może wykraczać poza granice widoku nadrzędnego. W przypadku zawijanego tekstu głównego usuń to poziome wymaganie. Jeśli zachowanie pełnej wysokości zawiniętego tekstu jest właściwe, rozważ .fixedSize(horizontal: false, vertical: true). Zobacz fixedSize(horizontal:vertical:).
Zachowanie rozmiaru pionowego wymaga miejsca na tę wysokość. Akapit zwiększający wysokość w krótkim kontenerze bez przewijania może nadal stać się niedostępny. Jeśli zadanie wymaga dostępu do całego tekstu, użyj pionowego obszaru przewijania; obsługiwane osie przewijania Apple opisuje w ScrollView.
Ponownie sprawdź obliczenia oparte na geometrii
GeometryReader definiuje zawartość na podstawie własnego rozmiaru i układu współrzędnych oraz zwraca widokowi nadrzędnemu elastyczny preferowany rozmiar. Dodanie go wokół widoku zmienia strukturę układu, więc nie jest neutralnym kontenerem do pomiaru. Jeśli problem zaczął się po wprowadzeniu obliczeń geometrycznych, tymczasowo zastąp tę gałąź zwykłymi stosami i ramkami. Apple opisuje te właściwości w GeometryReader.
Jeśli układ proporcjonalny rzeczywiście wymaga geometrii, wyprowadź wymiary z aktualnego rozmiaru odpowiedniego kontenera. Sprawdź stan przechowujący szerokość pobraną przy pierwszym wyświetleniu. Sprawdź też, czy zmierzony wymiar służy do zmiany rozmiaru tego samego obszaru, który jest mierzony; usunięcie tej zależności to przydatny test izolacyjny.
Zbuduj kompaktowy układ z jawną obsługą nadmiarowej zawartości
Poniższy kompletny przykład pokazuje trzy niezależne decyzje: tekst jest zawijany, elementy sterujące zmieniają układ w razie potrzeby, a nadmiarową zawartość pionową można przewijać. Podane wymiary są przykładowymi założeniami projektowymi, a nie wartościami zalecanymi przez Apple.
Utwórz w Xcode aplikację SwiftUI na macOS, ustaw minimalną wersję docelową na macOS 13 lub nowszą i zastąp wygenerowane deklaracje aplikacji oraz widoku zawartości tym jednym plikiem. Pozostaw tylko jedną deklarację @main. Przykład korzysta z ViewThatFits i interfejsów API do zmiany rozmiaru okna dostępnych od macOS 13. Apple opisuje ich działanie w ViewThatFits oraz windowResizability(_:).
import SwiftUI
@main
struct ResizeExampleApp: App {
var body: some Scene {
WindowGroup {
ResizeExampleView()
.frame(minWidth: 280, minHeight: 220)
}
.windowResizability(.contentMinSize)
}
}
struct ResizeExampleView: View {
@State private var selection = "Sample A"
private let details = Array(
repeating: "Sample notes wrap as the panel becomes narrower.",
count: 12
).joined(separator: " ")
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Sample panel")
.font(.title2)
Text(details)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
ViewThatFits(in: .horizontal) {
HStack(spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
.fixedSize(horizontal: true, vertical: false)
VStack(alignment: .leading, spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
}
Text("Selected: \(selection)")
}
.padding(20)
.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .leading
)
}
}
}
ViewThatFits sprawdza alternatywy w zadeklarowanej kolejności i wybiera pierwszą, której idealny rozmiar mieści się na ograniczonych osiach. W tym przypadku dopasowanie poziome wybiera między wierszem przycisków a stosem pionowym. Wiersz celowo zachowuje swój idealny rozmiar poziomy, aby ściśnięty wiersz nie stał się preferowanym wynikiem. Obie alternatywy powinny zapewniać te same dostępne działania. Zobacz reguły wyboru Apple.
Zewnętrzna ramka pozwala kolumnie zawartości dopasować się do dostępnej szerokości, a pionowy widok przewijania zapewnia dostęp do zawartości, gdy zawijanie zwiększy wysokość. Taki układ zakłada, że poszczególne elementy sterujące mieszczą się przy wybranej minimalnej szerokości. Dłuższe etykiety lub większe czcionki mogą wymagać innej wartości minimalnej albo innego układu kompaktowego.
Sprawdzono spójność deklaracji i dostępność interfejsów API w tym przykładzie; nie został tu skompilowany, ponieważ kompilator Swift był niedostępny. Uruchom go w macOS, aby sprawdzić rzeczywiste działanie okna i tekstu.
Ustaw ograniczenia okna po naprawie układu zawartości
Przydatny minimalny rozmiar okna zapobiega zmniejszeniu interfejsu poniżej rozmiaru obsługiwanego przez jego układ. W przypadku .contentMinSize SwiftUI wyprowadza minimalny rozmiar okna z jego zawartości i nie nakłada maksymalnego rozmiaru. W przykładzie ramka główna dostarcza minimalną szerokość i wysokość. Zobacz contentMinSize.
Dobierz tę wartość minimalną, testując kompaktowy układ z najdłuższymi obsługiwanymi etykietami. Zwiększenie minimum aż do momentu, gdy niewłaściwie umieszczony widok stanie się widoczny, może ukryć pierwotny błąd układu. Najpierw usuń niezamierzone przesunięcia i nadmiernie duże widoki podrzędne, a następnie ustaw najmniejsze wymiary, przy których zamierzone zadanie pozostaje wykonalne.
Jeśli okno należy do AppKit i hostuje SwiftUI, osobno sprawdź granicę hostowania. W macOS 13 i nowszych NSHostingView.sizingOptions steruje ograniczeniami wyprowadzanymi z zawartości SwiftUI. Gdy widok hostujący jest widokiem contentView okna, aktualizuje także minimalny i maksymalny rozmiar jego zawartości. Apple opisuje ponadto, że zawartość jest wyśrodkowywana, gdy ramka hosta różni się od rozmiaru wymaganego przez jego zawartość SwiftUI. Sprawdź te zależności, zanim dodasz kompensujące przesunięcia. Zobacz NSHostingView sizingOptions.
Sprawdzaj AppKit i TextKit tylko wtedy, gdy używa ich problematyczny widok
Pozwól SwiftUI określać rozmiar widoku głównego reprezentowanego w AppKit
W przypadku NSViewRepresentable SwiftUI kontroluje frame i bounds reprezentowanego widoku AppKit. Apple wyraźnie ostrzega, że samodzielne ustawianie tych właściwości koliduje ze SwiftUI i prowadzi do niezdefiniowanego działania. Jeśli obsługa zmiany rozmiaru wielokrotnie przypisuje ramkę reprezentowanemu widokowi głównemu, usuń ten konflikt i określ rozmiar na granicy SwiftUI. Zobacz NSViewRepresentable.
Gdy potrzebny jest niestandardowy pomiar, macOS 13 i nowsze udostępniają sizeThatFits(_:nsView:context:). Podczas jednego przebiegu układu może otrzymać kilka propozycji; zwrócenie nil oznacza prośbę o domyślne określenie rozmiaru. Opieraj wynik na bieżącej propozycji i zawartości, a nie na poprzedniej szerokości okna. Nieokreślony wymiar w propozycji wymaga przemyślanej reguły pomiaru; nie należy automatycznie traktować go jako zera. Zobacz określanie rozmiaru widoku reprezentującego.
Opcjonalne wiązanie w języku Swift pozwala sprawdzić i pobrać proponowany wymiar przed jego użyciem. Przydaje się podczas implementowania reguły określania rozmiaru, która nie wymaga wymuszonego odpakowywania opcjonalnych szerokości ani wysokości. To działanie języka opisano w The Basics: Optional Binding.
Oddziel szerokość zawijania tekstu od szerokości widoku tekstu
Jeśli problematycznym widokiem podrzędnym jest NSTextView, porównaj jego bieżącą szerokość z szerokością kontenera tekstu. Apple opisuje, że NSTextContainer.widthTracksTextView domyślnie ma wartość false; ustawienie true sprawia, że kontener dostosowuje szerokość po zmianie szerokości widoku tekstu. To właściwe ustawienie, gdy wiersze mają się ponownie układać wraz ze zmianą szerokości widoku tekstu. Zobacz widthTracksTextView.
Sprawdź też isHorizontallyResizable. Decyduje ono, czy widok tekstu zmienia szerokość, by dopasować się do tekstu, co jest innym wymaganiem niż zawijanie tekstu wewnątrz przypisanej szerokości. W przypadku pionowo przewijanego obszaru z zawijanym tekstem skonfiguruj te dwie reguły spójnie i upewnij się, że układ nadrzędny rzeczywiście zmienia rozmiar widoku tekstu. Zobacz isHorizontallyResizable.
Zachowaj używany przez implementację mechanizm tekstowy. Apple opisuje, że odwołanie się do layoutManager w obiekcie NSTextView lub jego kontenerze tekstu może przełączyć go w tryb zgodności korzystający z NSLayoutManager. Nie dodawaj takiego odwołania pochopnie podczas diagnozowania widoku TextKit 2. Używaj interfejsów API odpowiednich dla istniejącego mechanizmu i docelowej wersji systemu. Zobacz NSTextView.
Sprawdź naprawę przy zmianach szerokości i wysokości
Wykonaj poniższe czynności w swojej aplikacji:
- Powoli zwężaj okno, przechodząc przez punkt, w którym pierwotnie pojawiał się problem, a następnie znów je poszerz.
- Niezależnie zmniejsz wysokość i upewnij się, że cała wymagana zawartość pozostaje dostępna.
- Powtórz test z długimi syntetycznymi etykietami i tekstem wielowierszowym.
- Uaktywnij każdy element sterujący w układzie szerokim i kompaktowym.
- W przypadku widoku tekstu AppKit upewnij się, że zawijanie reaguje na bieżącą szerokość przy jej zmniejszaniu i zwiększaniu.
Prawidłowa naprawa wiąże położenie z bieżącym kontenerem, zapewnia zawartości praktyczny układ kompaktowy i umożliwia dostęp do nadmiarowej zawartości. Gdy te warunki są spełnione przy wielokrotnej zmianie rozmiaru, usuń tymczasowe obramowania i pozostaw minimalny rozmiar okna uzasadniony przez interfejs.
Źródła
- ProposedViewSize — Apple
- offset(x:y:) — Apple
- position(x:y:) — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- ScrollView — Apple
- GeometryReader — Apple
- ViewThatFits — Apple
- windowResizability(_:) — Apple
- contentMinSize — Apple
- sizingOptions — Apple
- NSViewRepresentable — Apple
- sizeThatFits(_:nsView:context:) — Apple
- The Basics — Swift.org
- widthTracksTextView — Apple
- isHorizontallyResizable — Apple
- NSTextView — Apple