Per adattare una scroll view SwiftUI ai contenuti brevi, misura il contenuto interno e imposta l’altezza della viewport al minore tra questa misura e un limite scelto. I contenuti più lunghi possono così scorrere entro il limite. Questa guida realizza questo comportamento per un piccolo pannello di etichette nella tua app per Mac.

Perché una scroll view con pochi contenuti lascia spazio vuoto?

Per prima cosa, distingui due dimensioni: l’altezza del contenuto e quella della viewport che lo mostra. Apple descrive ScrollView come una vista che mostra una porzione del contenuto sottostante e ne regola la parte visibile durante lo scorrimento. Un breve gruppo di etichette non determina, da solo, l’altezza della viewport che preferisci. La documentazione Apple di ScrollView spiega questa distinzione.

Per un pannello compatto su desktop, definisci una regola esplicita per le dimensioni: viewportHeight = min(contentHeight, maximumHeight). Misura il contenuto alla larghezza che riceve effettivamente, includendo eventuali spaziature che devono scorrere insieme a esso. Applica il risultato alla scroll view esterna. min(_:_:) di Swift restituisce il valore minore, mentre frame(width:height:alignment:) di SwiftUI imposta un’altezza fissa quando viene fornito l’argomento height. Swift min(_:_:), frame fisso di SwiftUI.

A titolo d’esempio, supponiamo che il limite massimo sia 240 punti. Un contenuto alto 86 punti produce una viewport di 86 punti; un contenuto alto 410 punti produce una viewport di 240 punti, con il resto accessibile scorrendo. Sono valori esemplificativi, non misure tratte dall’esempio qui sotto.

Modifica Cosa controlla Imposta l’altezza compatta desiderata?
Altezza misurata del contenuto, limitata e applicata alla scroll view Altezza della viewport Sì, nel layout vincolato mostrato qui sotto
.scrollBounceBehavior(.basedOnSize) Comportamento del rimbalzo in relazione alle dimensioni del contenuto No
.fixedSize(horizontal: false, vertical: true) sulle etichette Dimensione verticale ideale del contenuto Supporta la misurazione; la viewport esterna necessita comunque di una regola per l’altezza

Apple documenta .basedOnSize come una configurazione del rimbalzo e fixedSize(horizontal:vertical:) come un modificatore che fissa una vista alla sua dimensione ideale lungo le dimensioni selezionate. Nessuna delle due descrizioni stabilisce un’altezza della viewport derivata dal contenuto. Comportamento del rimbalzo della scroll view, Dimensione fissa per asse.

Crea un pannello compatto con un’altezza di scorrimento massima

Usa un’installazione aggiornata di Xcode il cui SDK per macOS esponga l’overload a valore singolo di onGeometryChange(for:of:action:). Attualmente Apple documenta che questo overload è disponibile a partire da macOS 13.0. L’esempio completo qui sotto è destinato a macOS 13.3 o versioni successive, perché usa anche scrollBounceBehavior(_:axes:), disponibile a partire da macOS 13.3. Callback per le modifiche alla geometria, Disponibilità del modificatore di rimbalzo.

Crea un progetto per app macOS con SwiftUI. Sostituisci la dichiarazione dell’app e la vista del contenuto generate con questo programma in un unico file, lasciando un solo @main nel target. Tutte le etichette sono stringhe locali di esempio. L’interruttore Mostra altre etichette alterna tra contenuto breve e lungo.

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)
    }
}

Il programma è stato esaminato, ma non è stato compilato né eseguito qui, perché questo ambiente non dispone di un compilatore Swift o di un runtime macOS. Compilalo in Xcode ed esegui le verifiche qui sotto prima di integrarlo in un’interfaccia esistente.

Perché la misurazione deve avvenire all’interno della scroll view

Il callback della geometria osserva lo stack interno con spaziatura. Misurando la scroll view esterna otterresti l’altezza della viewport che hai già impostato, creando una regola di dimensionamento circolare anziché determinare l’altezza richiesta dal contenuto.

La trasformazione restituisce solo l’altezza arrotondata e limitata. Apple consiglia di trasformare la geometria nel valore necessario e di evitare aggiornamenti ampi per le frequenti variazioni geometriche. In questo caso, le modifiche alla posizione di scorrimento non influiscono sul calcolo, e un contenuto più alto del limite continua a produrre lo stesso risultato di 240 punti. Indicazioni di Apple sulle modifiche alla geometria.

Arrotondare per eccesso evita di assegnare intenzionalmente una viewport frazionalmente più bassa del contenuto misurato. Lo stato iniziale di 240 punti fornisce una viewport utilizzabile prima che arrivi la misurazione; è un valore iniziale provvisorio, non l’altezza misurata del contenuto. Il callback lo sostituisce in seguito.

Perché la larghezza e l’ordine dei modificatori sono importanti

Mantieni flessibile il dimensionamento orizzontale. L’esempio usa .fixedSize(horizontal: false, vertical: true) per consentire alle etichette di sfruttare la larghezza disponibile conservando al contempo la loro dimensione verticale ideale. Apple avverte che fissare una dimensione ideale può far superare a una vista i limiti del contenitore; fissare entrambe le dimensioni, quindi, non è una scelta predefinita adatta per etichette con testo a capo in un pannello stretto. Documentazione Apple di fixedSize(horizontal:vertical:).

La spaziatura di 12 punti precede il modificatore di misurazione, quindi contribuisce all’altezza misurata del contenuto. Il bordo viene aggiunto dopo l’assegnazione dell’altezza esterna e delimita la viewport. Questa disposizione rende più semplice vedere se lo spazio inutilizzato appartiene al pannello scorrevole o alla finestra circostante.

Verifica i contenuti brevi, il testo a capo e il ridimensionamento

Segui questa sequenza per verificare il comportamento nell’esempio e di nuovo dopo aver inserito il pannello nel contenitore effettivo della tua app:

  1. Avvia l’app con Mostra altre etichette disattivato. Il bordo dovrebbe circondare le due etichette e la relativa spaziatura, con la didascalia subito sotto.
  2. Attiva Mostra altre etichette. Il pannello dovrebbe espandersi fino al limite di 240 punti. Scorri per verificare che sia possibile raggiungere l’ultima etichetta.
  3. Restringi e allarga la finestra. Le etichette con testo a capo dovrebbero cambiare altezza senza perdere le righe finali.
  4. Disattiva di nuovo l’interruttore. Il pannello dovrebbe ridursi all’altezza del contenuto breve.
  5. Sostituisci temporaneamente l’array breve con []. Il messaggio esplicito Nessuna etichetta da mostrare dovrebbe determinare l’altezza del contenuto.
  6. Ripeti la verifica con gli stili di testo e i controlli circostanti usati dal pannello reale.

Lo stato vuoto è una scelta intenzionale dell’interfaccia: in questo esempio, quando l’array è vuoto, vengono mantenuti un messaggio e la spaziatura. Se desideri che il pannello scompaia del tutto, ometti condizionalmente l’intera scroll view quando non ci sono etichette. È una decisione di prodotto distinta dalla misurazione di un pannello non vuoto.

La finestra d’esempio circostante conserva la sua altezza minima. Il codice dimensiona la viewport delle etichette; non chiede alla finestra del Mac di ridursi quando le etichette vengono rimosse. Lo spazio sotto la didascalia appartiene al layout esterno, al di fuori della scroll view con bordo.

Individua gli spazi residui o le etichette tagliate

Se il pannello resta alto, controlla per prima cosa dove è collegato il modificatore di misurazione. Deve osservare lo stack del contenuto dopo la spaziatura interna. Poi verifica se le viste antenate hanno un’altezza o un’altezza minima esplicita che riserva spazio intorno al pannello. Apple documenta che un’altezza fornita a un frame è una dimensione fissa, quindi un frame esterno separato può mantenere dello spazio anche quando la viewport interna si riduce. Dimensionamento dei frame in SwiftUI.

Se il contenuto stesso rimane inaspettatamente alto, rimuovi l’espansione verticale dal sottoalbero misurato. Per questo schema con poche etichette, evita uno spacer verticale o un frame del contenuto la cui altezza minima dipenda dalla viewport. Queste scelte fanno dipendere l’altezza del contenuto dalla viewport, compromettendo la misurazione indipendente richiesta dalla regola di dimensionamento.

Se la spaziatura inferiore viene tagliata, verifica che sia inclusa prima della misurazione. Se il testo va a capo in modo diverso dopo l’integrazione, controlla la larghezza effettivamente offerta dal nuovo contenitore padre. Non considerare permanente, a una larghezza diversa, un’altezza misurata a una determinata larghezza.

Scegli un limite di 240 punti adatto all’interfaccia contenitore. L’esempio usa un’altezza minima della finestra sufficiente per i controlli, la spaziatura, la didascalia e il pannello alla sua altezza massima. Un contenitore più basso richiede un limite minore o un layout circostante diverso; questo limite fisso non calcola automaticamente lo spazio rimasto sotto gli altri controlli.

Usa questo approccio per un insieme modesto di etichette disposte tutte in anticipo. Apple descrive LazyVStack come uno stack che crea gli elementi solo quando servono per il rendering sullo schermo. Di conseguenza, non dare per scontato che sostituire lo stack interno con uno stack lazy mantenga la stessa strategia di misurazione dell’intero contenuto. Per una raccolta ampia, di solito è più adatto un viewport stabile con contenuto lazy. Documentazione Apple di LazyVStack.

Quando sono applicabili le misurazioni di AppKit e TextKit?

Per questo pannello di etichette SwiftUI, misura il sottoalbero SwiftUI renderizzato. Una misurazione del testo di AppKit introdurrebbe un’altra configurazione di layout che dovrebbe corrispondere alle etichette visualizzate.

Se la tua app usa già un NSScrollView, mantieni la stessa distinzione tra documento e viewport. Apple definisce documentView come la vista su cui si scorre, mentre contentSize è la dimensione della content view della scroll view. Nonostante il nome, contentSize non misura l’intero documento. documentView di AppKit, contentSize di AppKit.

Per un layout TextKit esistente basato su NSLayoutManager, usedRect(for:) restituisce il rettangolo di delimitazione dei glifi già disposti in un contenitore di testo; Apple specifica esplicitamente che interrogarlo non genera glifi né esegue il layout. Quando occorre il layout completo del contenitore, ensureLayout(for:) è il metodo documentato per forzare prima tale layout. usedRect(for:) di TextKit, ensureLayout(for:) di TextKit.

Anche in questo caso, il rettangolo dei glifi non corrisponde all’intero frame della vista di testo. Apple documenta lo spazio aggiuntivo intorno al contenitore di testo tramite textContainerInset, oltre alla spaziatura dei frammenti di riga al suo interno. Considera la configurazione effettiva della vista di testo anziché copiare direttamente l’altezza dei glifi nella viewport scorrevole. Questi metodi descrivono il percorso NSLayoutManager; non sono una ricetta di dimensionamento universale per ogni componente di testo. textContainerInset di AppKit.

Per un piccolo pannello di etichette SwiftUI, la soluzione pratica consiste nel mantenere flessibile la larghezza, misurare il contenuto interno con la spaziatura e impostare un limite all’altezza della viewport esterna. Verifica il risultato con contenuti brevi e lunghi, quindi provalo nel layout effettivo del contenitore, così da accertarti che i vincoli circostanti supportino lo stesso comportamento compatto.

Fonti