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