Om du vill matcha sidokontroller med en SwiftUI-förhandsvisning med bildförhållande beräknar du den anpassade förhandsvisningens storlek utifrån det tillgängliga fönsterutrymmet och ger sedan kontrollerna samma höjd. Det här macOS-exemplet bevarar proportionerna i en syntetisk förhandsvisning och gör att överflödiga kontroller kan rullas.
Uppgiften är en skrivbordsredigerare som kan ändra storlek, med en förhandsvisning till vänster och en kontrollpanel med fast bredd till höger. Här betyder ”förhandsvisning” innehåll i den app som körs. Målet är att justera de två panelernas övre och nedre layoutkanter när fönstrets storlek ändras.
Vilken höjd ska kontrollerna följa?
Börja med att definiera rektangeln du vill matcha. En förhandsvisning som anpassas efter bildförhållandet kan ta upp mindre utrymme än den omgivande behållaren. Om kontrollerna följer behållarens höjd kan de sträcka sig nedanför den synliga förhandsvisningen, även när båda vyerna har placerats ut korrekt.
Apple definierar bildförhållandet som bredd delat med höjd. För innehåll som kan ändra storlek bevarar aspectRatio(_:contentMode:) med .fit förhållandet samtidigt som innehållet anpassas till det föreslagna utrymmet. Dokumentationens ellipsexempel visar varför den anpassade formen och en omslutande kvadratisk ram kan ha olika mått. Apples dokumentation om bildförhållande.
Den här implementationen gör den anpassade rektangeln explicit. Den reserverar en fast bredd för kontrollerna och ett mellanrum mellan panelerna och anpassar sedan förhandsvisningen till återstående bredd och tillgänglig höjd. Båda panelerna får den beräknade höjden på förhandsvisningen.
Det här är en layoutprincip för exemplet: förhandsvisningen bestämmer kontrollernas visningshöjd. En lång lista med kontroller rullas inuti den visningsytan i stället för att göra förhandsvisningen högre. Apple beskriver ScrollView som en vy som visar en flyttbar del av det underliggande innehållet i ett rullningsbart område. Apples dokumentation om rullningsvyer.
Beräkna den anpassade rektangeln en gång
Låt W och H vara de tillgängliga innehållsmåtten efter yttre utfyllnad, C kontrollernas bredd, G mellanrummet och R förhandsvisningens bredd–höjdförhållande. Beräkningen är:
remainingWidth = max(0, W - C - G)
previewWidth = min(remainingWidth, H * R)
previewHeight = previewWidth / R
controlsHeight = previewHeight
Den första begränsningen hindrar förhandsvisningen från att ta upp kontrollernas horisontella utrymme. Den andra hindrar ett brett fönster från att ge en förhandsvisning som är högre än det tillgängliga området. Genom att beräkna höjden utifrån den valda bredden bevaras bildförhållandet i båda fallen.
Anta till exempel att innehållsområdet före utfyllnad är 960 gånger 600 punkter. Med 16 punkters utfyllnad vid varje kant, en kontrollpanel på 220 punkter, ett mellanrum på 16 punkter och en förhandsvisning med bildförhållandet 16:9 är det tillgängliga området 928 gånger 568 punkter. Den återstående bredden för förhandsvisningen är 692 punkter, vilket ger en höjd på 389,25 punkter. Kontrollernas visningsyta blir därför 220 gånger 389,25 punkter.
Det här är exempelvärden, inte uppmätta resultat från en app. De visar också varför det skulle ge felaktig justering att matcha kontrollerna mot hela den tillgängliga höjden på 568 punkter.
Använd CGFloat för layoutmåtten och ett positivt, ändligt förhållande. I Swift gör let ratio: CGFloat = 16.0 / 9.0 den avsedda flyttalsberäkningen explicit. Om appen senare ska tillåta konfigurerbara förhållanden bör du validera dem före divisionen.
Bygg det fullständiga macOS-exemplet
Skapa en macOS-app med SwiftUI i Xcode och ange macOS 13 eller senare som distributionsmål. Ersätt de genererade app- och innehållsvydeklarationerna med den här enda filen och se till att målet innehåller exakt en @main-deklaration. Exemplet kräver inga resurser eller externa paket.
Kravet på distributionsversion beror på fönsterstorleks-API:t som används här. Scenen använder .contentMinSize, som Apple beskriver som att fönstrets minsta storlek härleds från innehållet utan att någon maximal storlek anges. Apples dokumentation om fönsterstorlek, Apples dokumentation om minsta innehållsstorlek.
import SwiftUI
@main
struct MatchedPreviewApp: App {
var body: some Scene {
WindowGroup {
PreviewEditor()
.frame(minWidth: 640, minHeight: 360)
}
.windowResizability(.contentMinSize)
}
}
struct PreviewEditor: View {
@State private var intensity: Double = 0.5
@State private var showsLabel = true
private let ratio: CGFloat = 16.0 / 9.0
private let controlsWidth: CGFloat = 220
private let gap: CGFloat = 16
var body: some View {
GeometryReader { geometry in
let availableHeight = max(0, geometry.size.height)
let remainingWidth = max(
0,
geometry.size.width - controlsWidth - gap
)
let previewWidth = min(
remainingWidth,
availableHeight * ratio
)
let previewHeight = previewWidth / ratio
HStack(alignment: .top, spacing: gap) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2 + intensity * 0.7))
.aspectRatio(ratio, contentMode: .fit)
.overlay {
Text("Sample composition")
.font(.headline)
.opacity(showsLabel ? 1 : 0)
}
.frame(
width: previewWidth,
height: previewHeight
)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Preview controls")
.font(.headline)
Toggle("Show label", isOn: $showsLabel)
VStack(alignment: .leading, spacing: 8) {
Text("Color intensity")
Slider(value: $intensity, in: 0...1)
}
ForEach(1...8, id: \.self) { index in
Text("Sample setting \(index)")
.font(.caption)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(
width: controlsWidth,
height: previewHeight
)
.background(Color.gray.opacity(0.08))
}
.frame(
width: geometry.size.width,
height: geometry.size.height,
alignment: .center
)
}
.padding(16)
}
}
GeometryReader tillhandahåller storleken på området som tilldelats redigeraren. Apple beskriver den som en vy som definierar innehåll utifrån sin egen storlek och sitt koordinatsystem, med en flexibel föredragen storlek. När utfyllnaden placeras utanför läsaren använder beräkningen det inre innehållsområdet. Apples dokumentation om geometrieläsare.
De två fasta ramarna anger den gemensamma höjden. Apple beskriver frame(width:height:alignment:) som att skapa en osynlig ram med angivna mått. Kontrollernas bakgrund läggs till efter ramen, så den täcker kontrollernas visningsyta, även utrymme nedanför en kort lista. Apples dokumentation om fasta ramar.
Den sista ramen centrerar hela raden i läsaren. Den förstorar inte någon av panelerna. Därför kan ett högt fönster ha oanvänt utrymme ovanför och nedanför raden, samtidigt som kontrollerna fortfarande matchar förhandsvisningen.
Bevara tillstånd och livscykelbeteende när storleken ändras
Håll användarredigerbara värden åtskilda från beräknad geometri. I exemplet är intensity och showsLabel tillstånd; förhandsvisningens bredd och höjd är lokala värden som härleds från den aktuella tillgängliga storleken.
Apple beskriver tillstånd som lagring som hanteras av SwiftUI och vars ändringar uppdaterar beroende delar av vyhierarkin. Därför kan reglagets och växelknappens värden ligga kvar i redigeraren som äger dem. Apples dokumentation om tillstånd.
Layoutberäkningen innehåller inga tilldelningar till tillstånd, anropspunkter för när vyn visas, asynkront arbete eller återkoppling från mätningar. När storleken ändras uppdateras måtten som används av samma vystruktur. Etiketten finns kvar och ändrar opacitet när växelknappen ändras.
När du anpassar detta till en befintlig redigerare bör du behålla dess modellägande och bindningstyper. Tillämpa storleksprincipen runt förhandsvisningen och kontrollerna; flytta inte skapandet av modellen till geometriuträkningen. Exemplet behöver bara de tillgängliga måtten och det valda förhållandet för sin aritmetik.
Kontrollera storleksändring, överflöd och modifierarordning
Använd följande mått för att kontrollera beräkningen. De beskriver innehållsområdet före exemplets yttre utfyllnad, inte fönstrets yttre ram. De förväntade värdena är avrundade till två decimaler.
| Innehållsområde | Förhandsvisningens storlek | Kontrollernas visningsyta |
|---|---|---|
| 960 × 600 punkter | 692 × 389,25 | 220 × 389,25 |
| 640 × 360 punkter | 372 × 209,25 | 220 × 209,25 |
| 1200 × 400 punkter | 654,22 × 368 | 220 × 368 |
| 700 × 900 punkter | 432 × 243 | 220 × 243 |
Aritmetiken kontrollerades med en liten Python-beräkning, inklusive påståenden om att den anpassade förhandsvisningen håller sig inom den tillgängliga höjden och att båda panelerna plus mellanrummet ryms horisontellt. Swift och Xcode var inte tillgängliga i forskningsmiljön, så det fullständiga SwiftUI-programmet granskades men kompilerades eller kördes inte.
Kör appen i Xcode och ändra storleken till breda, låga och höga former. Kontrollera att panelernas kanter förblir justerade, att kontrollerna rullas när innehållet överskrider visningsytan och att reglaget och växelknappen behåller sina värden när storleken ändras.
Om justeringen fortfarande skiljer sig efter att du har anpassat exemplet bör du kontrollera följande:
- Extra utfyllnad i panelen: Utfyllnad som tillämpas utanför bara den ena panelens fasta ram ökar panelens totala utrymme. Placera kontrollernas utfyllnad inuti rullningsvyn, som i exemplet.
- En större omslutande vy runt förhandsvisningen: Om du lägger till en större ram runt förhandsvisningen bör du avgöra om kontrollerna ska matcha den ramen eller det anpassade innehållet. Beräkningen här matchar den anpassade rektangeln.
- Innehåll som spiller utanför ramen: En ram anger layoutmått men döljer inte automatiskt sådant som ritas utanför den. Apple beskriver
.clipped()för rektangulär beskärning; använd den när avsiktligt överflöd i förhandsvisningen ska döljas. Behåll kontrollerna rullningsbara så att alla inställningar går att nå. Apples dokumentation om beskärning. - För lite utrymme i värdvyn: Exemplet förutsätter sin minsta innehållsstorlek. Om det bäddas in i en mindre värdvy kan den fasta kontrollbredden lämna för lite användbar bredd åt förhandsvisningen. Välj en smalare panel, en vertikal placering eller en uttrycklig minimistorlek för värdvyn.
När bör du använda en anpassad layout i stället?
Den här metoden passar en redigerare som får ett begränsat fönsterområde och använder ett känt bildförhållande. Storleksprincipen är explicit och beror inte på mätning av förhandsvisningens interna innehåll.
Om din återanvändbara komponent måste bestämma sin föredragna storlek utifrån sina underordnade vyer kan du överväga en anpassad Layout. Apples dokumenterade kontrakt skiljer mellan att rapportera den sammansatta storleken via sizeThatFits(proposal:subviews:cache:) och att placera ut underordnade vyer via placeSubviews(in:proposal:subviews:cache:). Proxyobjekt för underordnade vyer tillhandahåller operationer för mätning och placering. Apples layoutdokumentation.
För den begränsade skrivbordsredigeraren som visas här räcker det med en gemensam anpassad rektangel: reservera horisontellt utrymme för kontrollerna, anpassa förhandsvisningen efter båda tillgängliga måtten och ge kontrollernas visningsyta samma höjd. Då får du en tydlig regel för storleksändring samtidigt som kontrollernas innehåll kan rullas.
Källor
- aspectRatio(_:contentMode:) — Apple Developer Documentation.
- ScrollView — Apple Developer Documentation.
- windowResizability(_:) — Apple Developer Documentation.
- contentMinSize — Apple Developer Documentation.
- GeometryReader — Apple Developer Documentation.
- frame(width:height:alignment:) — Apple Developer Documentation.
- State — Apple Developer Documentation.
- clipped(antialiased:) — Apple Developer Documentation.
- Layout — Apple Developer Documentation.