Per una piccola riga di etichette in un’app per Mac, usa ViewThatFits per scegliere il prefisso completo più lungo che ci sta, includendo il conteggio degli elementi nascosti. Questo tutorial crea la riga senza troncare le etichette né aggiornare lo stato durante il layout.

L’obiettivo concreto è una finestra macOS ridimensionabile contenente sei etichette sintetiche. Mantieni il loro ordine, mostra solo etichette complete e visualizza un badge come 3 more per il suffisso omesso. La soluzione presuppone spazio orizzontale sufficiente per il solo badge con il conteggio e spazio verticale sufficiente per una riga.

Scegli alternative di riga complete

Apple descrive ViewThatFits come un elemento che seleziona il primo elemento figlio le cui dimensioni ideali rientrano nello spazio proposto sugli assi vincolati. È disponibile su macOS 13 e versioni successive. Qui usa .horizontal, perché il problema riguarda la larghezza della riga; l’interfaccia circostante deve fornire l’altezza della riga. ViewThatFits

Per sei etichette, fornisci sette alternative in quest’ordine: tutte e sei le etichette senza badge, cinque etichette più 1 more, quattro più 2 more e così via, fino alla sola voce 6 more. Ogni alternativa è una riga completa. Il badge partecipa alla verifica dello spazio disponibile, quindi l’aggiunta del conteggio non sposta inaspettatamente l’ultima etichetta visibile.

Questo implementa una politica basata sul prefisso: una prima etichetta lunga può far omettere tutte le etichette, anche se una successiva più corta ci starebbe. È intenzionale. Se invece il tuo caso d’uso consente di saltare singole etichette, definisci questa diversa politica di selezione prima di implementarla.

Applica fixedSize() a ogni riga candidata, in modo che le etichette e il badge mantengano le loro dimensioni ideali. Apple spiega che questo modificatore può far sì che una vista superi i limiti del genitore; da solo non garantisce che la vista ci stia. In questo caso, è il contenitore di selezione circostante a verificare lo spazio disponibile. fixedSize()

Crea l’esempio macOS completo

Crea un’app SwiftUI per macOS in Xcode e imposta come destinazione di distribuzione macOS 13 o una versione successiva. Sostituisci la dichiarazione dell’app e la vista dei contenuti generate con il seguente programma in un unico file. Nel target deve esserci esattamente una dichiarazione dell’app contrassegnata con @main.

L’esempio usa identificatori espliciti anziché il testo delle etichette come identità. La documentazione di Apple per ForEach richiede elementi identificabili oppure un percorso chiave esplicito; identificatori distinti consentono anche a due elementi di condividere lo stesso titolo visualizzato. ForEach

import SwiftUI

struct DemoItem: Identifiable {
    let id: Int
    let title: String
}

struct ItemChip: View {
    let title: String

    var body: some View {
        Text(title)
            .lineLimit(1)
            .padding(.horizontal, 10)
            .padding(.vertical, 6)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(Color.blue.opacity(0.12))
            )
    }
}

struct PrefixCandidate: View {
    let items: [DemoItem]
    let visibleCount: Int

    var body: some View {
        HStack(spacing: 8) {
            ForEach(items.prefix(visibleCount)) { item in
                ItemChip(title: item.title)
            }

            if visibleCount < items.count {
                Text("\(items.count - visibleCount) more")
                    .lineLimit(1)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.15))
                    )
            }
        }
        .fixedSize()
    }
}

struct WholeItemRow: View {
    let items: [DemoItem]

    var body: some View {
        if items.isEmpty {
            Text("No items")
                .fixedSize()
        } else {
            ViewThatFits(in: .horizontal) {
                ForEach(
                    Array((0...items.count).reversed()),
                    id: \.self
                ) { visibleCount in
                    PrefixCandidate(
                        items: items,
                        visibleCount: visibleCount
                    )
                }
            }
        }
    }
}

struct ContentView: View {
    let items: [DemoItem] = [
        DemoItem(id: 1, title: "Sketch"),
        DemoItem(id: 2, title: "Color samples"),
        DemoItem(id: 3, title: "Shapes"),
        DemoItem(id: 4, title: "Paper textures"),
        DemoItem(id: 5, title: "Layout notes"),
        DemoItem(id: 6, title: "Preview")
    ]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Sample labels")
                .font(.headline)

            WholeItemRow(items: items)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("Resize the window to change how many labels fit.")
                .foregroundStyle(.secondary)
        }
        .font(.system(size: 14))
        .padding(24)
        .frame(minWidth: 320, idealWidth: 680, minHeight: 160)
    }
}

@main
struct WholeItemsDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Gli identificatori candidati sono i possibili conteggi degli elementi visibili, in ordine decrescente dal numero totale di elementi a zero. All’interno di ogni candidato, gli identificatori degli elementi identificano le etichette. Ogni iterazione crea un PrefixCandidate, che contiene una riga orizzontale.

Il frame flessibile esterno consente alla riga di ricevere lo spazio orizzontale disponibile, mantenendo il contenuto selezionato allineato al bordo iniziale. Mantieni fixedSize() sul candidato all’interno di ViewThatFits. Applicarlo all’intero contenitore di selezione cambierebbe il modo in cui quel contenitore risponde alla proposta di larghezza del genitore.

Questo è un esempio di codice sorgente completo, ma qui non è stato compilato né eseguito perché non era disponibile una toolchain Swift. I calcoli illustrativi nella sezione successiva sono stati verificati separatamente; le larghezze effettive del testo devono essere verificate nell’app per Mac.

Includi il badge nel calcolo dello spazio disponibile

La regola di selezione utile è: scegli il numero massimo di elementi visibili per cui l’intero candidato ci sta. Il numero di elementi nascosti è sempre il totale degli elementi meno il numero di elementi visibili in quel candidato. Non serve sincronizzare una stima separata.

Considera una riga illustrativa con quattro elementi larghi 72, 96, 64 e 88 punti, misurati includendo la spaziatura interna degli elementi. Supponi una spaziatura di 8 punti e un badge da 60 punti per ogni conteggio non nullo di elementi nascosti. Sono valori inventati per il calcolo, non misurazioni dell’app di esempio.

Quando tutti e quattro gli elementi sono visibili, la larghezza richiesta è di 72 + 96 + 64 + 88 + 3 × 8 = 344 punti. Con tre elementi visibili, il badge sostituisce il suffisso omesso, quindi la larghezza diventa di 72 + 96 + 64 + 3 × 8 + 60 = 316 punti. Servono tre spazi, perché il candidato contiene tre elementi e un badge.

Larghezza disponibile Elementi visibili Elementi nascosti Larghezza del candidato selezionato
344 punti 4 0 344 punti
343 punti 3 1 316 punti
300 punti 2 2 244 punti
220 punti 1 3 140 punti
60 punti 0 4 60 punti

Nel programma, ogni candidato contiene il testo effettivo del badge. In questo modo gestisce le differenze tra le stringhe dei badge senza basarsi sull’ipotesi illustrativa di una larghezza costante. Le larghezze dei candidati non devono variare tutte della stessa quantità; il numero decrescente di elementi visibili esprime la preferenza di selezione.

Il candidato con il solo conteggio stabilisce la larghezza minima utilizzabile. Se non ci sta nemmeno quel badge, mostrare il conteggio completo nello spazio disponibile è impossibile secondo queste regole. Fornisci una larghezza minima sufficiente, consenti una disposizione diversa oppure sposta il conteggio altrove. Il ritaglio non è un risultato valido per mostrare gli elementi per intero.

L’esempio imposta una larghezza minima del contenuto e un carattere esplicito di dimensioni contenute per il breve badge sintetico. Se modifichi il testo, il carattere, la spaziatura interna o il layout circostante, verifica di nuovo il caso con il solo conteggio. La larghezza minima scelta per questo esempio non è una misura universale per i badge.

Separa la selezione del layout dal ciclo di vita delle viste

Il conteggio degli elementi nascosti fa parte degli input immutabili di ogni candidato. Il programma non scrive in un binding da un metodo di misurazione, non pianifica un aggiornamento asincrono del conteggio e non deduce la visibilità dai callback di apparizione. La scelta della riga determina anche il badge corrispondente.

Mantieni semplice il contenuto ripetuto dei candidati. Ripetere un’etichetta tra più alternative non stabilisce un’unica identità condivisa per quell’etichetta in tutte le alternative. Apple spiega che l’archiviazione dello stato è associata all’identità della vista e che cambiarne l’identità può sostituirne lo stato. Demystify SwiftUI

Se in seguito sostituisci le etichette con controlli modificabili, conserva i valori condivisi in un antenato comune o in un modello e passa gli input o i binding appropriati a ogni presentazione. Apple consiglia di collocare lo stato condiviso nell’antenato comune più vicino delle viste che ne hanno bisogno. Non fare affidamento sulla persistenza dello stato locale dei candidati quando cambia la presentazione. Gestione dello stato dell’interfaccia utente

Questa soluzione mostra il conteggio degli elementi nascosti nella riga selezionata. Non espone il conteggio selezionato a una vista sorella o al modello dell’applicazione. L’inizializzatore documentato da Apple per ViewThatFits accetta gli assi vincolati e il contenuto candidato; non fornisce un binding per il conteggio selezionato. Considera l’esportazione di quel valore un requisito di progettazione aggiuntivo. init(in:content:)

Quando serve un layout personalizzato, scegli un progetto diverso

Un Layout personalizzato è appropriato quando servono geometrie specializzate. Apple definisce le sue responsabilità fondamentali come la comunicazione delle dimensioni complessive e l’assegnazione delle posizioni alle viste secondarie tramite proxy di layout. I proxy espongono operazioni di misurazione e posizionamento, invece di consentire l’accesso diretto alle viste sottostanti. Layout

Non implementare l’omissione semplicemente interrompendo il posizionamento dopo l’ultimo elemento che ci sta. La documentazione di Apple per LayoutSubview indica di posizionare ogni vista secondaria una volta. Saltare il posizionamento non è un meccanismo documentato per nascondere una vista e un proxy di layout non fornisce un’operazione generica per riscrivere il contenuto e trasformare un’altra vista secondaria in una nuova etichetta con il conteggio. LayoutSubview

Se crei un layout personalizzato, mantieni coerenti la misurazione e il posizionamento, inclusi la spaziatura e la proposta di dimensioni usata. Apple richiede esplicitamente che questi calcoli siano coerenti. Questo vincolo vale anche quando il layout esamina diverse disposizioni possibili. placeSubviews(in:proposal:subviews:cache:)

L’approccio basato sui candidati è volutamente circoscritto. Per sei elementi descrive sette righe candidate e 21 occorrenze di elementi in quelle righe. In generale, il numero di occorrenze di elementi cresce come n × (n + 1) / 2. È un conteggio strutturale, non un benchmark delle prestazioni né un’affermazione secondo cui ogni candidato viene renderizzato a ogni aggiornamento. Usalo per una piccola raccolta di etichette; fai profilazione prima di estenderlo a una raccolta ampia o a contenuti costosi.

Verifica la riga nei casi limite

Avvia l’app e ridimensiona lentamente la finestra. Prima di adattare il componente, verifica queste condizioni:

  • Con una finestra larga, tutte le etichette sono visibili e non compare il badge 0 more.
  • Quando la finestra si restringe, le etichette scompaiono dalla fine senza essere tagliate a metà né sostituite da puntini di sospensione.
  • Il numero di elementi visibili più il conteggio degli elementi nascosti nel badge è sempre uguale a sei.
  • Con una prima etichetta eccezionalmente lunga, il candidato con il solo conteggio resta disponibile.
  • Con un array vuoto, la riga mostra No items.
  • Con titoli duplicati e identificatori distinti, entrambi gli elementi continuano a essere conteggiati.
  • Dopo aver cambiato i caratteri o il testo del badge, il candidato con il solo conteggio entra ancora nella larghezza minima supportata.

Per una riga piccola e ordinata, le alternative complete rendono facile esaminare la regola di selezione: ogni candidato include le sue etichette, la spaziatura e l’esatto conteggio degli elementi nascosti. Mantieni esplicita la condizione sulla larghezza minima e leggero il contenuto ripetuto: la riga si adatterà al ridimensionamento della finestra senza elementi parziali né un ciclo di aggiornamento separato del conteggio.

Fonti