Om du vill att en SwiftUI-rullningsvy ska anpassas efter kort innehåll mäter du dess inre innehåll och ställer in visningsytans höjd till det mindre av det måttet och en vald gräns. Längre innehåll kan sedan rullas inom gränsen. Den här genomgången visar hur du skapar det beteendet för en liten etikettpanel i din egen Mac-app.

Varför lämnar en kort rullningsvy tomt utrymme?

Börja med att skilja på två dimensioner: innehållets höjd och visningsytans höjd. Apple beskriver ScrollView som en vy som visar en del av sitt underliggande innehåll och justerar den synliga delen vid rullning. En kort grupp etiketter anger inte i sig vilken höjd du vill att visningsytan ska ha. Apples dokumentation om ScrollView förklarar skillnaden.

För en kompakt skrivbordspanel anger du en uttrycklig storleksregel: viewportHeight = min(contentHeight, maximumHeight). Mät innehållet vid den bredd det faktiskt får, inklusive eventuell utfyllnad som ska rullas tillsammans med det. Tillämpa resultatet på den yttre rullningsvyn. Swift-funktionen min(_:_:) returnerar det mindre värdet, medan SwiftUIs frame(width:height:alignment:) anger en fast höjd när höjdargumentet anges. Swift min(_:_:), SwiftUIs fasta ram.

Som exempel: anta att maxhöjden är 240 punkter. Innehåll som mäter 86 punkter ger en visningsyta på 86 punkter; innehåll som mäter 410 punkter ger en visningsyta på 240 punkter, och resten av innehållet går att nå genom att rulla. Det här är exempelvärden, inte mätningar från exemplet nedan.

Ändring Vad den styr Anger den önskad kompakta höjden?
Uppmätt innehållshöjd, begränsad och tillämpad på rullningsvyn Visningsytans höjd Ja, i den avgränsade layouten nedan
.scrollBounceBehavior(.basedOnSize) Studsbeteende i förhållande till innehållsstorleken Nej
.fixedSize(horizontal: false, vertical: true) på etiketterna Innehållets ideala vertikala storlek Den stöder mätningen; den yttre visningsytan behöver fortfarande en höjdregel

Apple beskriver .basedOnSize som en inställning för studsning och fixedSize(horizontal:vertical:) som ett sätt att låsa en vy till dess ideala storlek i valda dimensioner. Ingen av beskrivningarna anger en visningshöjd som baseras på innehållet. Beteende för rullningsstuds, Fast storlek per axel.

Bygg en kompakt panel med en maximal rullningshöjd

Använd en aktuell Xcode-installation vars macOS SDK har överlagringen av onGeometryChange(for:of:action:) med ett enda värde. Apple anger för närvarande att den överlagringen är tillgänglig från macOS 13.0. Det fullständiga exemplet nedan har macOS 13.3 eller senare som mål, eftersom det även använder scrollBounceBehavior(_:axes:), som kräver macOS 13.3. Callback för geometriförändring, Tillgänglighet för studsbeteendemodifieraren.

Skapa ett macOS-app-projekt med SwiftUI. Ersätt den genererade appdeklarationen och innehållsvyn med det här programmet i en enda fil och behåll endast en @main-deklaration i målet. Alla etiketter är syntetiska lokala strängar. Växlingsknappen Show more labels växlar mellan kort och långt innehåll.

import SwiftUI

@main
struct CompactLabelsApp: App {
    var body: some Scene {
        WindowGroup {
            CompactLabelsView()
        }
    }
}

struct CompactLabelsView: View {
    @State private var showMore = false
    @State private var viewportHeight: CGFloat = 240

    private var labels: [String] {
        if showMore {
            return (1...18).map { number in
                "Sample label \(number): a longer description that can wrap when the panel becomes narrower."
            }
        }
        return ["Sample label A", "Sample label B"]
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Toggle("Show more labels", isOn: $showMore)

            ScrollView(.vertical) {
                VStack(alignment: .leading, spacing: 8) {
                    if labels.isEmpty {
                        Text("No labels to display")
                    } else {
                        ForEach(labels.indices, id: \.self) { index in
                            Text(labels[index])
                        }
                    }
                }
                .font(.body)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(12)
                .onGeometryChange(for: CGFloat.self) { proxy in
                    min(proxy.size.height.rounded(.up), 240)
                } action: { newHeight in
                    viewportHeight = newHeight
                }
            }
            .frame(height: viewportHeight)
            .scrollBounceBehavior(.basedOnSize, axes: .vertical)
            .border(Color.secondary.opacity(0.4))

            Text("The label panel grows up to 240 points.")
                .font(.caption)
        }
        .padding(20)
        .frame(
            maxWidth: .infinity,
            maxHeight: .infinity,
            alignment: .topLeading
        )
        .frame(minWidth: 300, minHeight: 360)
    }
}

Programmet har granskats, men det har inte kompilerats eller körts här eftersom den här miljön saknar Swift-kompilator och macOS-körtid. Bygg det i Xcode och utför kontrollerna nedan innan du införlivar det i ett befintligt gränssnitt.

Varför mätningen ska göras inuti rullningsvyn

Geometricallbacken observerar den inre stacken med utfyllnad. Om du mäter den yttre rullningsvyn får du den visningsyta du redan har angett, vilket skapar en cirkulär storleksregel i stället för att ta reda på den höjd innehållet behöver.

Transformeringen returnerar endast den avrundade, begränsade höjden. Apple rekommenderar att geometrin omvandlas till det värde du behöver och att breda uppdateringar undviks vid frekventa geometriförändringar. Här påverkar ändringar i rullningspositionen inte beräkningen, och innehåll som är högre än maxgränsen ger fortfarande samma resultat på 240 punkter. Apples vägledning om geometriförändringar.

Avrundning uppåt undviker att medvetet ange en visningsyta som är en bråkdel kortare än det uppmätta innehållet. Det inledande värdet på 240 punkter ger en användbar visningsyta innan mätningen kommer; det är ett startvärde, inte en uppmätt innehållshöjd. Callbacken ersätter det sedan.

Varför bredden och modifierarnas ordning spelar roll

Låt den horisontella storleken vara flexibel. Exemplet använder .fixedSize(horizontal: false, vertical: true) så att etiketterna kan utnyttja den tillgängliga bredden samtidigt som deras ideala vertikala storlek behålls. Apple varnar för att en låst idealstorlek kan få en vy att överskrida förälderns gränser. Att låsa båda dimensionerna är därför inte ett lämpligt standardval för etiketter som radbryts i en smal panel. Apples dokumentation om fixedSize(horizontal:vertical:).

Utfyllnaden på 12 punkter läggs till före mätmodifieraren, så den ingår i den uppmätta innehållshöjden. Ramen läggs till efter den yttre höjdinställningen och markerar visningsytans gräns. Den här ordningen gör det enklare att se om oanvänt utrymme tillhör rullningspanelen eller fönstret runtomkring.

Kontrollera kort innehåll, radbrytning och minskad höjd

Använd den här sekvensen för att kontrollera beteendet i exemplet och igen efter att panelen placerats i appens faktiska behållare:

  1. Starta med Show more labels avstängd. Ramen ska omsluta de två etiketterna och deras utfyllnad, med bildtexten direkt nedanför.
  2. Aktivera Show more labels. Panelen ska växa till maxgränsen på 240 punkter. Rulla för att bekräfta att den sista etiketten fortfarande går att nå.
  3. Gör fönstret smalare och bredare. Radbrutna etiketter ska ändra höjd utan att de sista raderna försvinner.
  4. Stäng av växlingsknappen igen. Panelen ska krympa tillbaka till höjden för det korta innehållet.
  5. Ändra tillfälligt den korta arrayen till []. Det uttryckliga meddelandet No labels to display ska avgöra innehållshöjden.
  6. Upprepa med de textstilar och kontroller runtomkring som används i din riktiga panel.

Det tomma läget är ett medvetet gränssnittsval: exemplet behåller ett meddelande och utfyllnad när arrayen är tom. Om panelen i stället ska tas bort helt villkorar du visningen av hela rullningsvyn så att den utelämnas när det inte finns några etiketter. Det är ett separat produktbeslut från att mäta en panel som inte är tom.

Fönstret runt exemplet behåller sin minimihöjd. Koden anpassar etikettens visningsyta; den begär inte att Mac-fönstret krymper när etiketter tas bort. Eventuellt utrymme under bildtexten tillhör den yttre layouten, utanför den inramade rullningsvyn.

Felsök kvarvarande glapp eller avklippta etiketter

Om panelen förblir hög kontrollerar du först var mätmodifieraren sitter. Den ska observera innehållsstacken efter dess interna utfyllnad. Granska sedan föräldervyerna efter en uttrycklig höjd eller minimihöjd som reserverar utrymme runt panelen. Apple beskriver en angiven rambredd som en fast dimension, så en separat yttre ram kan behålla utrymme även när den inre visningsytan krymper. Storleksändring med SwiftUI-ramar.

Om själva innehållet förblir oväntat högt tar du bort vertikal expansion från det uppmätta delträdet. För det här mönstret med små etiketter bör du undvika en vertikal mellanrumsvy eller en innehållsram vars minimihöjd härleds från visningsytan. Sådana val gör innehållets höjd beroende av visningsytan och motverkar den oberoende mätning som storleksregeln behöver.

Om den nedre utfyllnaden klipps bort kontrollerar du att utfyllnaden ingår före mätningen. Om texten radbryts annorlunda efter integreringen granskar du den faktiska bredd som den nya föräldervyn erbjuder. En höjd som mätts vid en viss bredd ska inte behandlas som en permanent höjd vid en annan bredd.

Se till att maxgränsen på 240 punkter passar den omgivande layouten. Exemplet anger en minimistorlek på fönstret som räcker för kontrollerna, utfyllnaden, bildtexten och panelen vid maxhöjd. En lägre behållare behöver en mindre höjdbudget eller en annan omgivande layout; den här fasta maxgränsen beräknar inte automatiskt det utrymme som återstår under andra kontroller.

Använd metoden för en liten uppsättning etiketter som läggs ut direkt. Apple beskriver LazyVStack som en vy som skapar element först när de behövs för visning på skärmen. Utgå därför inte från att ett byte av den inre stacken mot en lazy-stack bevarar samma mätstrategi för hela innehållet. För en stor samling är en stabil visningsyta med lazy-innehåll oftast en lämpligare utformning. Apples dokumentation om LazyVStack.

När passar mätningar från AppKit och TextKit?

För den här etikettpanelen i SwiftUI mäter du den renderade SwiftUI-delen. En textmätning från AppKit skulle innebära ännu en layoutkonfiguration som måste stämma överens med de visade etiketterna.

Om appen redan använder en NSScrollView behåller du samma åtskillnad mellan dokument och visningsyta. Apple definierar documentView som vyn som rullas, medan contentSize är storleken på rullningsvyns innehållsvy. Trots namnet mäter contentSize inte hela dokumentet. AppKit documentView, AppKit contentSize.

För en befintlig TextKit-layout som bygger på NSLayoutManager rapporterar usedRect(for:) begränsningsrektangeln för glyfer som redan har lagts ut i en textbehållare. Apple säger uttryckligen att en fråga efter värdet inte skapar glyfer eller utför layout. När en fullständig behållarlayout behövs är ensureLayout(for:) den dokumenterade metoden för att framtvinga layouten först. TextKit usedRect(for:), TextKit ensureLayout(for:).

Även då motsvarar glyfens rektangel inte hela textrutans ram. Apple dokumenterar ytterligare utrymme runt textbehållaren via textContainerInset, samt utfyllnad för radfragment inuti den. Ta hänsyn till den faktiska textrutans konfiguration i stället för att kopiera en glyfhöjd direkt till en rullningsvy. Metoderna beskriver NSLayoutManager-vägen; de är inte en universell storlekslösning för alla textkomponenter. AppKit textContainerInset.

För en liten etikettpanel i SwiftUI är den praktiska lösningen att hålla bredden flexibel, mäta det inre innehållet med utfyllnad och begränsa den yttre visningsytans höjd. Kontrollera resultatet med både kort och långt innehåll och testa sedan panelen i den verkliga föräldralayouten, så att omgivande begränsningar stöder samma kompakta beteende.

Källor