Para una fila pequeña de etiquetas en una app para Mac, usa ViewThatFits para elegir el prefijo completo más largo que quepa, incluido el recuento de elementos ocultos. Este tutorial crea esa fila sin truncar etiquetas ni actualizar el estado durante el diseño.
La tarea concreta consiste en una ventana redimensionable de macOS que contiene seis etiquetas sintéticas. Conserva su orden, muestra solo etiquetas completas y presenta una insignia como 3 más para el sufijo omitido. La solución supone que hay suficiente espacio horizontal para la insignia que muestra solo el recuento y suficiente espacio vertical para una fila.
Elige alternativas de fila completas
Apple documenta ViewThatFits como un selector que elige el primer elemento secundario cuyo tamaño ideal cabe en el tamaño propuesto en los ejes restringidos. Está disponible en macOS 13 y versiones posteriores. Usa .horizontal aquí porque la tarea se centra en el ancho de la fila; la interfaz que la rodea debe proporcionar la altura de la fila. ViewThatFits
Para seis etiquetas, proporciona siete alternativas en este orden: las seis etiquetas sin insignia, cinco etiquetas más 1 más, cuatro más 2 más, y así sucesivamente, hasta terminar con solo 6 más. Cada alternativa es una fila completa. Su insignia participa en la decisión de ajuste, así que añadir el recuento no desplazará inesperadamente la última etiqueta visible.
Esto implementa una política de prefijo: una primera etiqueta larga puede hacer que se omitan todas las etiquetas, aunque quepa una etiqueta corta posterior. Es intencional. Si tu tarea permite omitir etiquetas individuales, define esa política de selección diferente antes de implementarla.
Aplica fixedSize() a cada fila candidata para que sus etiquetas y su insignia conserven sus dimensiones ideales. Apple explica que este modificador puede hacer que una vista exceda los límites de su elemento superior; por sí solo no garantiza que quepa. Aquí, el contenedor de selección que la envuelve determina si cabe. fixedSize()
Crea el ejemplo completo para macOS
Crea una app de SwiftUI para macOS en Xcode y establece como destino de implementación macOS 13 o posterior. Sustituye la declaración de la app generada y la vista de contenido por el siguiente programa de un solo archivo. Mantén exactamente una declaración de app marcada con @main en el destino.
El ejemplo usa identificadores explícitos en lugar del texto de las etiquetas como identidad. La documentación de ForEach de Apple requiere elementos identificables o una ruta de clave de identificador explícita; los identificadores distintos también permiten que dos elementos compartan el mismo título visible. 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()
}
}
}
Los identificadores candidatos son los posibles recuentos de elementos visibles, en orden descendente desde el total de elementos hasta cero. Dentro de cada candidato, los identificadores de los elementos identifican las etiquetas. Cada iteración crea un PrefixCandidate, que contiene una fila horizontal.
El marco flexible exterior permite que la fila reciba el espacio horizontal disponible y, a la vez, mantiene el contenido seleccionado alineado con el borde inicial. Mantén fixedSize() en el candidato dentro de ViewThatFits. Aplicarlo a todo el contenedor de selección cambiaría cómo responde ese contenedor a la propuesta de ancho de su elemento superior.
Este es un ejemplo de código fuente completo, pero aquí no se compiló ni se ejecutó porque no había un conjunto de herramientas de Swift disponible. El cálculo ilustrativo de la siguiente sección se comprobó por separado; los anchos reales del texto deben verificarse en la app para Mac.
Incluye la insignia al calcular qué cabe
La regla de decisión útil es seleccionar el mayor recuento de elementos visibles cuya fila completa quepa. El recuento de elementos ocultos siempre es el total de elementos menos el recuento de elementos visibles de ese candidato. No hay que sincronizar ninguna estimación aparte.
Considera una fila ilustrativa de cuatro elementos con anchos de 72, 96, 64 y 88 puntos, incluidas las inserciones de cada elemento. Supón un espaciado de 8 puntos y una insignia de 60 puntos para cualquier recuento de elementos ocultos distinto de cero. Son datos inventados para el cálculo, no medidas de la app de ejemplo.
Cuando los cuatro elementos están visibles, el ancho necesario es 72 + 96 + 64 + 88 + 3 × 8 = 344 puntos. Con tres elementos visibles, la insignia sustituye al sufijo omitido, por lo que el ancho pasa a ser 72 + 96 + 64 + 3 × 8 + 60 = 316 puntos. Se necesitan tres espacios porque el candidato contiene tres elementos y una insignia.
| Ancho disponible | Elementos visibles | Elementos ocultos | Ancho de la alternativa seleccionada |
|---|---|---|---|
| 344 puntos | 4 | 0 | 344 puntos |
| 343 puntos | 3 | 1 | 316 puntos |
| 300 puntos | 2 | 2 | 244 puntos |
| 220 puntos | 1 | 3 | 140 puntos |
| 60 puntos | 0 | 4 | 60 puntos |
En el programa, cada candidato contiene el texto real de su insignia. Por tanto, contempla las diferencias entre las cadenas de las insignias sin depender de la suposición de ancho constante de la ilustración. Los anchos de los candidatos no tienen por qué variar de manera uniforme; el orden descendente del recuento de elementos visibles expresa la preferencia de selección.
El candidato que solo muestra el recuento establece el ancho mínimo utilizable. Si ni siquiera cabe esa insignia, es imposible mostrar el recuento completo dentro del ancho disponible con estas reglas. Proporciona un ancho mínimo suficiente, permite una disposición distinta o coloca el recuento en otro sitio. No consideres que recortar el contenido da como resultado elementos completos.
El ejemplo proporciona un ancho mínimo para el contenido y una fuente explícita de tamaño moderado para su insignia sintética breve. Si cambias la redacción, la fuente, las inserciones o el diseño que la rodea, vuelve a verificar el caso en que solo se muestra el recuento. El ancho mínimo elegido para este ejemplo no es una medida universal para las insignias.
Mantén la selección del diseño separada del ciclo de vida de las vistas
El recuento de elementos ocultos forma parte de las entradas inmutables de cada candidato. El programa no escribe en un enlace desde un método de medición, no programa una actualización asíncrona del recuento ni deduce la visibilidad a partir de devoluciones de llamada de aparición. Al elegir la fila, también se elige la insignia correspondiente.
Mantén sencillo el contenido repetido de los candidatos. Repetir una etiqueta entre alternativas no establece una identidad compartida para esa etiqueta en todas ellas. Apple explica que el almacenamiento del estado está asociado a la identidad de la vista y que cambiar la identidad puede sustituir el estado. Demystify SwiftUI
Si más adelante sustituyes las etiquetas por controles editables, mantén sus valores compartidos en un antecesor común y pasa las entradas o los enlaces apropiados a cada presentación. Apple recomienda colocar el estado compartido en el antecesor común más cercano de las vistas que lo necesitan. No dependas de que el estado local de un candidato se conserve al cambiar de presentación. Managing user interface state
Esta solución muestra el recuento de elementos ocultos dentro de la fila seleccionada. No expone el recuento seleccionado a una vista hermana ni al modelo de la app. El inicializador ViewThatFits documentado por Apple acepta los ejes restringidos y el contenido candidato; no proporciona un enlace al recuento seleccionado. Considera que exportar ese valor es un requisito de diseño adicional. init(in:content:)
Cuándo un diseño personalizado requiere otro planteamiento
Un Layout personalizado es apropiado cuando necesitas una geometría especializada. Apple define sus responsabilidades principales como informar del tamaño compuesto y asignar las posiciones de las vistas secundarias mediante proxies de diseño. Los proxies exponen operaciones de medición y colocación, no acceso directo a las vistas subyacentes. Layout
No implementes la omisión simplemente dejando de colocar elementos después del último que quepa. La documentación de LayoutSubview de Apple indica que debes colocar cada vista secundaria una vez. Omitir la colocación no es un mecanismo documentado para ocultar elementos, y un proxy de diseño no proporciona una operación general para reescribir el contenido y convertir otra vista secundaria en una nueva etiqueta de recuento. LayoutSubview
Si creas un diseño personalizado, mantén la coherencia entre la medición y la colocación, incluidos el espaciado y la propuesta de tamaño utilizada. Apple exige explícitamente que esos cálculos sean coherentes. Ese requisito también se aplica cuando el diseño prueba varias disposiciones posibles. placeSubviews(in:proposal:subviews:cache:)
El planteamiento con candidatos tiene un alcance deliberadamente acotado. Para seis elementos, describe siete filas candidatas y 21 apariciones de elementos en esas filas. En general, el número de apariciones de elementos crece según n × (n + 1) / 2. Esto es un recuento estructural, no una prueba de rendimiento ni una afirmación de que todos los candidatos se dibujen en cada actualización. Úsalo para una colección pequeña de etiquetas; crea un perfil de rendimiento antes de ampliarlo a una colección grande o a contenido costoso.
Verifica la fila en los límites
Ejecuta la app y redimensiona lentamente la ventana. Comprueba estas condiciones antes de adaptar el componente:
- Con un ancho amplio, aparecen todas las etiquetas y no hay una insignia
0 más. - Al estrechar la ventana, las etiquetas desaparecen desde el final, sin quedar parcialmente visibles ni mostrar puntos suspensivos.
- El recuento de elementos visibles más el recuento de elementos ocultos de la insignia siempre suma seis.
- Incluso con una primera etiqueta excepcionalmente larga, el candidato que solo muestra el recuento sigue disponible.
- Con un arreglo vacío, la fila muestra
No items. - Con títulos duplicados e identificadores distintos, ambos elementos siguen incluidos en el recuento.
- Después de cambiar las fuentes o el texto de la insignia, el candidato que solo muestra el recuento sigue cabiendo en el ancho mínimo admitido.
Para una fila pequeña y ordenada, las alternativas completas hacen que la regla de ajuste sea fácil de comprobar: cada candidato incluye sus etiquetas, su espaciado y el recuento exacto de elementos ocultos. Mantén explícita la condición de ancho mínimo y ligero el contenido repetido, y la fila podrá adaptarse al redimensionamiento de la ventana sin elementos parciales ni un ciclo aparte para actualizar el recuento.
Fuentes
- ViewThatFits — Apple
- fixedSize() — Apple
- ForEach — Apple
- Demystify SwiftUI — Apple
- Managing user interface state — Apple
- init(in:content:) — Apple
- Layout — Apple
- LayoutSubview — Apple
- placeSubviews(in:proposal:subviews:cache:) — Apple