Para fazer o texto SwiftUI quebrar linhas dentro de uma célula de NSTableView, dê à view de hospedagem uma largura definida, conecte sua altura à célula com Auto Layout e habilite alturas automáticas de linha. Este passo a passo cria uma pequena lista de rótulos para Mac com essa configuração.
A tarefa é específica: exibir rótulos sintéticos de comprimentos diferentes no seu próprio app, com linhas altas o bastante para mostrar o conteúdo conforme a largura disponível muda. O exemplo tem como alvo o macOS 13 ou posterior e usa uma tabela baseada em views.
Como SwiftUI e NSTableView compartilham o dimensionamento?
Três camadas participam. A tabela aloca o retângulo da célula. O Auto Layout conecta a célula à NSHostingView incorporada. O SwiftUI determina como o rótulo se ajusta ao espaço proposto. A Apple descreve NSHostingView como a ponte que informa ao AppKit as preferências de tamanho do conteúdo SwiftUI durante o layout. NSHostingView
Para texto com quebra de linha, a largura precisa ser estabelecida antes que a altura faça sentido. Um rótulo pode precisar de uma linha em uma coluna larga e de várias em uma coluna estreita. Definir o frame inicial de uma view de hospedagem não estabelece um layout duradouro para a célula da tabela: a Apple documenta que a tabela define o frame da view retornada pelo delegate da célula. tableView(_:viewFor:row:)
Por isso, a implementação abaixo segue uma configuração de dimensionamento: a tabela fornece o espaço horizontal, a view de hospedagem é limitada às bordas da célula com recuos horizontais, e o rótulo SwiftUI preserva seu tamanho vertical ideal enquanto aceita a largura proposta. A tabela usa Auto Layout para calcular as alturas das linhas quando usesAutomaticRowHeights está habilitado. usesAutomaticRowHeights
Não trate intrinsicContentSize e fittingSize como medidas intercambiáveis. A Apple define fittingSize como o menor tamanho que satisfaz as restrições da view e de suas subviews. Portanto, consultá-lo sem as restrições de largura pretendidas não responde à pergunta sobre a altura que a célula deve ter na largura atual da coluna. fittingSize
Crie uma tabela completa de rótulos para macOS 13 e posterior
Crie um projeto de app para macOS no Xcode com a interface SwiftUI e um destino de implantação no macOS 13 ou posterior. Substitua a declaração de app gerada pelo primeiro arquivo abaixo e, em seguida, adicione os outros dois arquivos Swift ao mesmo destino do app. Juntos, esses três arquivos formam um programa completo; mantenha apenas uma declaração @main.
O requisito do macOS 13 vem do uso explícito de NSHostingView.sizingOptions. A própria view de hospedagem está disponível desde o macOS 10.15, e as alturas automáticas de linha da tabela estão disponíveis desde o macOS 10.13. Um destino de implantação mais antigo exige uma verificação de disponibilidade em torno de sizingOptions ou uma versão da implementação que omita essa personalização. NSHostingView, sizingOptions, usesAutomaticRowHeights
Crie o app e dados de exemplo sintéticos
O primeiro arquivo, LabelSizingApp.swift, fornece três rótulos e um botão que altera o último rótulo. Isso permite verificar tanto o layout inicial quanto a atualização do conteúdo.
import SwiftUI
@main
struct LabelSizingApp: App {
@State private var expanded = false
private var labels: [String] {
[
"Amber tile",
"Blue tile with a longer descriptive label that can wrap.",
expanded
? "Green tile with several descriptive words. This longer label makes it easier to inspect wrapping when the window becomes narrower."
: "Green tile"
]
}
var body: some Scene {
WindowGroup {
VStack(alignment: .leading, spacing: 12) {
Button("Toggle longer label") {
expanded.toggle()
}
LabelTable(labels: labels)
}
.padding()
.frame(minWidth: 300, minHeight: 240)
}
}
}
Conecte a view de hospedagem à célula
Em HostedLabelCell.swift, o conteúdo SwiftUI aceita a largura disponível e solicita sua altura ideal. O modificador fixedSize, específico por eixo, é importante: fixar o eixo horizontal pediria ao texto que preservasse sua largura ideal e poderia fazê-lo ultrapassar os limites do contêiner pai. fixedSize(horizontal:vertical:)
import AppKit
import SwiftUI
struct RowLabel: View {
let text: String
var body: some View {
Text(verbatim: text)
.font(.system(size: 14))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 8)
}
}
@MainActor
final class HostedLabelCell: NSTableCellView {
private let host = NSHostingView(
rootView: RowLabel(text: "")
)
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
host.translatesAutoresizingMaskIntoConstraints = false
host.sizingOptions = [.intrinsicContentSize]
addSubview(host)
NSLayoutConstraint.activate([
host.leadingAnchor.constraint(
equalTo: leadingAnchor, constant: 12
),
host.trailingAnchor.constraint(
equalTo: trailingAnchor, constant: -12
),
host.topAnchor.constraint(equalTo: topAnchor),
host.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("This sample creates cells programmatically.")
}
func configure(text: String) {
host.rootView = RowLabel(text: text)
}
}
As quatro restrições às bordas criam a conexão entre a view de hospedagem e a célula. Desabilitar a tradução da máscara de redimensionamento automático na view de hospedagem incorporada permite que essas restrições explícitas a controlem, como a Apple recomenda para views cujas restrições exigem ajustes dinâmicos. A tabela continua responsável por posicionar a célula externa. translatesAutoresizingMaskIntoConstraints
Aqui, sizingOptions mantém o dimensionamento pelo conteúdo intrínseco sem solicitar as restrições de tamanho mínimo e máximo da view de hospedagem. A Apple documenta esses comportamentos como opções selecionáveis separadamente; definir a propriedade como um conjunto vazio de opções não cria nenhuma dessas restrições derivadas do conteúdo. Manter o dimensionamento pelo conteúdo intrínseco é uma escolha de implementação para esta célula orientada pelo conteúdo, não uma configuração universal para toda view de hospedagem. sizingOptions
Habilite alturas automáticas de linha e configure células reutilizadas
Adicione LabelTable.swift para criar a view de rolagem, a tabela e o delegate. O programa usa deliberadamente o dimensionamento automático sem implementar um delegate manual para a altura das linhas.
import AppKit
import SwiftUI
@MainActor
struct LabelTable: NSViewRepresentable {
let labels: [String]
func makeCoordinator() -> Coordinator {
Coordinator(labels: labels)
}
func makeNSView(context: Context) -> NSScrollView {
let table = NSTableView()
let column = NSTableColumn(
identifier: Coordinator.cellID
)
column.title = "Label"
column.width = 360
column.minWidth = 120
column.resizingMask = .autoresizingMask
table.addTableColumn(column)
table.headerView = nil
table.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
table.intercellSpacing = NSSize(width: 0, height: 0)
table.usesAutomaticRowHeights = true
table.dataSource = context.coordinator
table.delegate = context.coordinator
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.documentView = table
return scrollView
}
func updateNSView(
_ scrollView: NSScrollView,
context: Context
) {
guard let table = scrollView.documentView as? NSTableView,
context.coordinator.labels != labels else {
return
}
context.coordinator.labels = labels
table.reloadData()
}
@MainActor
final class Coordinator: NSObject,
NSTableViewDataSource, NSTableViewDelegate {
static let cellID = NSUserInterfaceItemIdentifier("LabelCell")
var labels: [String]
init(labels: [String]) {
self.labels = labels
super.init()
}
func numberOfRows(in tableView: NSTableView) -> Int {
labels.count
}
func tableView(
_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?,
row: Int
) -> NSView? {
let cell = (
tableView.makeView(
withIdentifier: Self.cellID,
owner: self
) as? HostedLabelCell
) ?? HostedLabelCell(frame: .zero)
cell.identifier = Self.cellID
cell.configure(text: labels[row])
return cell
}
}
}
O delegate tenta reutilizar uma view antes de criar uma célula e, em seguida, atribui o rótulo atual a cada vez. Isso segue a sequência de construção de views de célula recomendada pela Apple. makeView(withIdentifier:owner:) pode retornar uma view reutilizada ou retornar nil quando não houver uma view correspondente disponível. tableView(_:viewFor:row:), makeView(withIdentifier:owner:)
O exemplo recarrega os dados apenas quando o array de entrada muda. Um recarregamento completo é fácil de acompanhar para três linhas, mas a Apple documenta que ele descarta e obtém novamente as views visíveis de linha e célula. Em um app maior, escolha uma estratégia de atualização mais específica quando apropriado. Mantenha a criação e as atualizações de views do AppKit no ator principal; a documentação do Swift descreve MainActor como usando um executor equivalente à fila de despacho principal. reloadData(), MainActor
Como diagnosticar recortes ou altura excessiva das linhas?
Comece pela largura, depois inspecione a propagação da altura e, por fim, as atualizações. Essa ordem segue as dependências do layout e ajuda a evitar alterações simultâneas em várias configurações sem relação entre si.
| Sintoma | Primeira verificação | Correção útil |
|---|---|---|
| O texto longo continua horizontalmente para além da célula | O dimensionamento horizontal ideal está habilitado? | Use fixedSize(horizontal: false, vertical: true) para este rótulo com quebra de linha. |
| O texto quebra linhas, mas as linhas finais ficam cortadas | A view de hospedagem está conectada às duas bordas verticais da célula? | Restaure as restrições superior e inferior e remova alturas fixas conflitantes. |
| Todas as linhas permanecem com a mesma altura | O dimensionamento automático está habilitado e há uma implementação manual de altura? | Verifique a configuração da tabela e use uma estratégia de dimensionamento deliberada. |
| O texto curto aparece em uma célula desnecessariamente alta | O conteúdo tem uma altura fixa ou espaço vertical extra? | Inspecione a hierarquia SwiftUI e seus recuos antes de ajustar a tabela. |
| Um rótulo alterado exibe conteúdo antigo | A célula é configurada novamente após a mudança do modelo? | Atualize a view raiz exibida ou recarregue o conteúdo afetado. |
Use um limite de largura para tornar a inspeção concreta. Se a célula tiver, por exemplo, 320 pontos de largura, os dois recuos horizontais de 12 pontos do exemplo deixam 296 pontos para a view de hospedagem. O recuo vertical acrescenta 16 pontos no total. A altura necessária restante depende do layout real do texto; esses números não preveem quantas linhas um rótulo específico ocupará.
O frame expansível do SwiftUI estabelece o alinhamento à esquerda dentro do espaço horizontal disponível. Ele não atribui largura física ilimitada: a documentação da Apple sobre frames explica que o frame participa das propostas do contêiner pai e aplica as restrições de tamanho especificadas. frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)
Execute o exemplo com o rótulo longo em uma janela larga e outra estreita e, depois, alterne de volta para o rótulo curto. Confira se todo o texto permanece visível e se a linha consegue voltar a encolher. Essas são verificações de aceitação a serem feitas nas versões do macOS compatíveis com seu app. O código foi examinado com base na documentação da API consultada; ele não foi compilado nem executado aqui porque este ambiente não tem compilador Swift nem runtime do macOS.
O que muda quando você calcula as alturas das linhas manualmente?
O dimensionamento manual é uma abordagem de implementação separada. Se precisar usá-la, calcule as alturas para a largura de conteúdo pretendida fora do delegate de altura e faça com que o delegate retorne o resultado preparado. Uma chave de cache útil inclui o item, a largura disponível e quaisquer entradas de apresentação que alterem o layout, como fonte ou recuo.
A Apple exige que a altura de linha retornada manualmente seja maior que zero e exclua o espaçamento entre células. A documentação também proíbe obter views de célula ou linha dentro de tableView(_:heightOfRow:) e alerta contra consultas geométricas que possam acionar um layout recursivo da tabela. Portanto, medir uma célula visível a partir desse callback é uma base inadequada para uma implementação manual. tableView(_:heightOfRow:)
Quando as alturas preparadas mudarem, notifique a tabela com noteHeightOfRows(withIndexesChanged:). A Apple documenta que isso reorganiza imediatamente a tabela usando as alturas do delegate, caso esse método do delegate esteja implementado. O redimensionamento de colunas também precisa invalidar as medidas que dependem da largura; columnDidResizeNotification é a notificação documentada para esse evento. Essas são considerações necessárias se você substituir a abordagem automática por um cache manual. noteHeightOfRows(withIndexesChanged:), columnDidResizeNotification
O TextKit deve medir um rótulo SwiftUI?
O TextKit é útil quando o app realmente organiza o texto por meio de um contêiner TextKit. A medida documentada tem um escopo mais restrito do que uma célula SwiftUI inteira: NSLayoutManager.usedRect(for:) informa os limites dos glifos já organizados no contêiner especificado e, por si só, não gera glifos nem executa o layout. Chame ensureLayout(for:) antes de consultar essa medida quando o layout ainda não tiver sido concluído. usedRect(for:), ensureLayout(for:)
Considere também a configuração do contêiner. A Apple documenta que o valor padrão de lineFragmentPadding é 5 pontos em cada extremidade de um fragmento de linha, o que afeta a largura disponível para o layout. Também afirma que essa propriedade não se destina a expressar margens de texto. lineFragmentPadding
A conclusão prática é que uma medida TextKit configurada separadamente não pode, por si só, estabelecer a altura dessa hierarquia SwiftUI. A célula também inclui o comportamento de layout e o recuo do SwiftUI. Para esta tabela de rótulos, mantenha o caminho de dimensionamento pela view de hospedagem e pelo Auto Layout e, depois, verifique a quebra de linha, as alterações de conteúdo e o encolhimento nas larguras compatíveis com seu app.
Fontes
- NSHostingView — Apple
- sizingOptions — Apple
- usesAutomaticRowHeights — Apple
- tableView(_:viewFor:row:) — Apple
- fittingSize — Apple
- fixedSize(horizontal:vertical:) — Apple
- translatesAutoresizingMaskIntoConstraints — Apple
- makeView(withIdentifier:owner:) — Apple
- reloadData() — Apple
- MainActor — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- tableView(_:heightOfRow:) — Apple
- noteHeightOfRows(withIndexesChanged:) — Apple
- columnDidResizeNotification — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- lineFragmentPadding — Apple