Damit SwiftUI-Text in einer NSTableView-Zelle umbricht, geben Sie der Hosting-Ansicht eine bestimmte Breite, verbinden Sie ihre Höhe über Auto Layout mit der Zelle, und aktivieren Sie automatische Zeilenhöhen. Diese Anleitung erstellt eine kleine Mac-Beschriftungsliste mit dieser Anordnung.
Die Aufgabe ist spezifisch: synthetische Beschriftungen unterschiedlicher Länge in Ihrer eigenen App anzeigen, mit Zeilen, die hoch genug sind, um ihren Inhalt bei sich ändernder verfügbarer Breite anzuzeigen. Das Beispiel zielt auf macOS 13 oder höher und verwendet eine view-basierte Tabelle.
Wie teilen SwiftUI und NSTableView die Dimensionierung?
Drei Ebenen sind beteiligt. Die Tabelle weist dem Zellenrechteck den Platz zu. Auto Layout verbindet die Zelle mit ihrem eingebetteten NSHostingView. SwiftUI bestimmt, wie die Beschriftung in den vorgeschlagenen Raum passt. Apple beschreibt NSHostingView als die Brücke, die SwiftUI-Inhaltsgrößenpräferenzen während des Layouts an AppKit meldet. NSHostingView
Für umbrechenden Text muss die Breite festgelegt sein, bevor die Höhe sinnvoll wird. Eine Beschriftung mag in einer breiten Spalte eine Zeile benötigen und in einer schmalen mehrere. Das Setzen des ursprünglichen Frames einer Hosting-Ansicht etabliert kein dauerhaftes Tabellenzellen-Layout: Apple dokumentiert, dass die Tabelle den Frame der von ihrem Zellen-Delegaten zurückgegebenen Ansicht festlegt. tableView(_:viewFor:row:)
Die folgende Implementierung folgt daher einer Dimensionierungsanordnung: Die Tabelle stellt den horizontalen Platz bereit, der Host wird durch horizontale Einränder an die Zellenränder angebunden, und die SwiftUI-Beschriftung behält ihre ideale vertikale Größe bei, während sie die vorgeschlagene Breite akzeptiert. Die Tabelle verwendet Auto Layout, um Zeilenhöhen zu berechnen, wenn usesAutomaticRowHeights aktiviert ist. usesAutomaticRowHeights
Behandeln Sie intrinsicContentSize und fittingSize nicht als austauschbare Messungen. Apple definiert fittingSize als die kleinste Größe, die die Constraints der Ansicht und ihrer Unteransichten erfüllt. Folglich beantwortet das Abfragen ohne die beabsichtigten Breiten-Constraints nicht die Frage, wie hoch die Zelle bei der aktuellen Spaltenbreite sein sollte. fittingSize
Erstellen Sie eine vollständige Beschriftungstabelle für macOS 13 und höher
Erstellen Sie ein macOS-App-Projekt in Xcode mit SwiftUI-Oberfläche und einem Deployment Target von macOS 13 oder höher. Ersetzen Sie die generierte App-Deklaration durch die erste untenstehende Datei und fügen Sie die anderen beiden Swift-Dateien demselben App-Target hinzu. Zusammen bilden diese drei Dateien ein vollständiges Programm; behalten Sie nur eine @main-Deklaration.
Die macOS-13-Anforderung ergibt sich aus der expliziten Verwendung von NSHostingView.sizingOptions. Die Hosting-Ansicht selbst ist seit macOS 10.15 verfügbar, und automatische Tabellenzeilenhöhen seit macOS 10.13. Ein älteres Deployment Target erfordert eine Verfügbarkeitsprüfung um sizingOptions oder eine Implementierungsversion, die diese Anpassung weglässt. NSHostingView, sizingOptions, usesAutomaticRowHeights
App und synthetische Beispieldaten erstellen
Die erste Datei, LabelSizingApp.swift, stellt drei Beschriftungen und einen Button bereit, der die letzte Beschriftung ändert. Dies gibt Ihnen sowohl eine initiale Layout-Prüfung als auch eine Inhaltsaktualisierungs-Prüfung.
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)
}
}
}
Hosting-Ansicht mit der Zelle verbinden
In HostedLabelCell.swift akzeptiert der SwiftUI-Inhalt die verfügbare Breite und fordert seine ideale Höhe an. Der achsenspezifische fixedSize-Modifikator ist wichtig: Die Fixierung der horizontalen Achse würde den Text auffordern, seine ideale Breite beizubehalten, und kann ihn die Grenzen des Parents überschreiten lassen. 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)
}
}
Die vier Rand-Constraints erstellen die Verbindung zwischen Host und Zelle. Das Deaktivieren der Autoresizing-Mask-Übersetzung für den eingebetteten Host lässt diese expliziten Constraints regieren, wie Apple für Ansichten empfiehlt, deren Constraints dynamische Anpassung erfordern. Die Tabelle bleibt für die Positionierung der äußeren Zelle verantwortlich. translatesAutoresizingMaskIntoConstraints
Hier behält sizingOptions die intrinsische Inhaltsdimensionierung bei, ohne die Mindest- und Höchstgrößen-Constraints des Hosts anzufordern. Apple dokumentiert diese als separat auswählbare Verhaltensweisen; das Setzen der Eigenschaft auf eine leere Optionsmenge erzeugt keine dieser inhaltsabgeleiteten Constraints. Das Beibehalten der intrinsischen Inhaltsdimensionierung ist eine Implementierungsentscheidung für diese inhaltsgesteuerte Zelle, keine universelle Einstellung für jede Hosting-Ansicht. sizingOptions
Automatische Zeilenhöhen aktivieren und wiederverwendete Zellen konfigurieren
Fügen Sie LabelTable.swift hinzu, um den Scrollview, die Tabelle und den Delegaten zu erstellen. Das Programm nutzt bewusst automatische Dimensionierung ohne Implementierung eines manuellen Zeilenhöhen-Delegaten.
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
}
}
}
Der Delegat versucht die Wiederverwendung bevor er eine Zelle erstellt, und weist dann jedes Mal die aktuelle Beschriftung zu. Dies folgt Apples empfohlener Zellen-View-Konstruktionssequenz. makeView(withIdentifier:owner:) kann eine wiederverwendete Ansicht zurückgeben oder nil, wenn keine passende Ansicht verfügbar ist. tableView(_:viewFor:row:), makeView(withIdentifier:owner:)
Das Beispiel lädt nur neu, wenn sich das Eingabe-Array ändert. Ein vollständiger Neuladen ist für drei Zeilen leicht nachvollziehbar, aber Apple dokumentiert, dass es sichtbare Zeilen- und Zellen-Views verwirft und neu erlangt. In einer größeren Anwendung wählen Sie eine engere Aktualisierungsstrategie, wenn angebracht. Halten Sie AppKit-View-Konstruktion und -Aktualisierungen auf dem Main Actor; Swift dokumentiert MainActor als Verwendung eines Executors, der der Haupt-Dispatch-Warteschlange entspricht. reloadData(), MainActor
Wie können Sie Clipping oder übermäßige Zeilenhöhe diagnostizieren?
Beginnen Sie mit der Breite, prüfen Sie dann die Höhenweitergabe, dann prüfen Sie Aktualisierungen. Diese Reihenfolge folgt den Abhängigkeiten im Layout und hilft, mehrere unabhängige Einstellungen nicht gleichzeitig zu ändern.
| Symptom | Erste Prüfung | Nützliche Korrektur |
|---|---|---|
| Langer Text läuft horizontal über die Zelle hinaus | Ist horizontale ideale Dimensionierung aktiviert? | Verwenden Sie fixedSize(horizontal: false, vertical: true) für diese umbrechende Beschriftung. |
| Text bricht um, aber die letzten Zeilen werden abgeschnitten | Ist der Host an beide vertikalen Zellenränder angebunden? | Stellen Sie die oberen und unteren Constraints wieder her und entfernen Sie conflicktierende feste Höhen. |
| Jede Zeile behält dieselbe Höhe | Ist automatische Dimensionierung aktiviert, und gibt es eine manuelle Höhen-Implementierung? | Prüfen Sie die Tabellenkonfiguration und verwenden Sie eine bewusste Dimensionierungsstrategie. |
| Kurzer Text erscheint in unnötig hoher Zelle | Enthält der Inhalt eine feste Höhe oder zusätzlichen vertikalen Raum? | Prüfen Sie die SwiftUI-Hierarchie und ihr Padding, bevor Sie die Tabelle anpassen. |
| Eine geänderte Beschriftung zeigt alten Inhalt | Wird die Zelle nach Modelländerungen erneut konfiguriert? | Aktualisieren Sie den angezeigten Root View oder laden Sie den betroffenen Inhalt neu. |
Verwenden Sie ein Breitenbudget, um die Prüfung konkret zu machen. Wenn die Zelle beispielhaft 320 Punkte breit ist, lassen die zwei 12-Punkte-horizontalen Einränder des Beispiels 296 Punkte für den Host. Sein vertikales Padding trägt insgesamt 16 Punkte bei. Die verbleibende erforderliche Höhe hängt vom tatsächlichen Textlayout ab; diese Zahlen sagen nicht voraus, wie viele Zeilen eine bestimmte Beschriftung belegen wird.
Der expandierende SwiftUI-Frame etabliert eine führende Ausrichtung im verfügbaren horizontalen Raum. Er weist keine unbegrenzte physische Breite zu: Apples Frame-Dokumentation erklärt, dass der Frame an Vorschlägen seines Parents teilnimmt und die angegebenen Größen-Constraints anwendet. frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)
Führen Sie das Beispiel mit der langen Beschriftung sowohl bei breiter als auch bei schmaler Fenstergröße aus, und wechseln Sie dann zurück zur kurzen Beschriftung. Prüfen Sie, dass der gesamte Text sichtbar bleibt und die Zeile wieder schrumpfen kann. Dies sind Akzeptanzprüfungen, die Sie auf Ihren unterstützten macOS-Versionen durchführen sollten. Der Code wurde anhand der abgerufenen API-Dokumentation geprüft; er wurde hier nicht kompiliert oder ausgeführt, da diese Umgebung keinen Swift-Compiler oder macOS-Laufzeitumgebung hat.
Was ändert sich, wenn Sie Zeilenhöhen manuell berechnen?
Manuelle Dimensionierung ist ein separater Implementierungspfad. Wenn Sie ihn benötigen, berechnen Sie Höhen für den beabsichtigten Inhaltsbreite außerhalb des Höhen-Delegaten und lassen den Delegaten das vorbereitete Ergebnis zurückgeben. Ein nützlicher Cache-Schlüssel umfasst das Element, die verfügbare Breite und alle Präsentationsparameter, die das Layout ändern, wie Schriftart oder Padding.
Apple verlangt, dass eine manuell zurückgegebene Zeilenhöhe größer als null ist und den Intercell-Spacing ausschließt. Die Dokumentation verbietet auch, Zellen- oder Zeilen-Views innerhalb von tableView(_:heightOfRow:) zu erhalten, und warnt vor Geometrieabfragen, die rekursives Tabellenlayout auslösen können. Das Messen einer sichtbaren Zelle aus diesem Callback ist daher eine schlechte Grundlage für eine manuelle Implementierung. tableView(_:heightOfRow:)
Wenn sich vorberechnete Höhen ändern, benachrichtigen Sie die Tabelle mit noteHeightOfRows(withIndexesChanged:). Apple dokumentiert, dass dies die Tabelle sofort mit den Delegatenhöhen neu kachelt, wenn diese Delegatenmethode implementiert ist. Spaltengrößenänderungen müssen auch breite-abhängige Messungen invalidieren; columnDidResizeNotification ist die dokumentierte Benachrichtigung für dieses Ereignis. Dies sind notwendige Überlegungen, wenn Sie den automatischen Ansatz durch einen manuellen Cache ersetzen. noteHeightOfRows(withIndexesChanged:), columnDidResizeNotification
Sollte TextKit eine SwiftUI-Beschriftung messen?
TextKit ist nützlich, wenn Ihre App tatsächlich Text durch einen TextKit-Container layoutet. Seine dokumentierte Messung hat einen engeren Geltungsbereich als eine gesamte SwiftUI-Zelle: NSLayoutManager.usedRect(for:) meldet die Grenzen der bereits im angegebenen Container layouteten Glyphen und erzeugt selbst keine Glyphen oder führt kein Layout durch. Rufen Sie ensureLayout(for:) auf, bevor Sie diese Messung lesen, wenn das Layout noch nicht abgeschlossen wurde. usedRect(for:), ensureLayout(for:)
Berücksichtigen Sie auch die Container-Konfiguration. Apple dokumentiert ein standardmäßiges lineFragmentPadding von 5 Punkten an jedem Ende eines Zeilenfragments, was die verfügbare Layoutbreite beeinflusst. Es wird auch angegeben, dass diese Eigenschaft nicht dazu gedacht ist, Text-Ränder auszudrücken. lineFragmentPadding
Die praktische Schlussfolgerung ist, dass eine separat konfigurierte TextKit-Messung allein die Höhe dieser SwiftUI-Hierarchie nicht festlegen kann. Die Zelle umfasst auch SwiftUI-Layoutverhalten und Padding. Für diese Beschriftungstabelle behalten Sie den Dimensionierungspfad über die Hosting-Ansicht und Auto Layout bei, und verifizieren Sie Umbruch, Inhaltsänderungen und Schrumpfen bei den Breiten, die Ihre App unterstützt.
Quellen
- 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