Agar teks SwiftUI membungkus di dalam sel NSTableView, beri tampilan hosting lebar yang pasti, hubungkan tingginya ke sel dengan Auto Layout, dan aktifkan tinggi baris otomatis. Panduan ini membuat daftar label kecil untuk Mac dengan susunan tersebut.

Tugasnya spesifik: tampilkan label sintetis dengan panjang berbeda di aplikasi Anda sendiri, dengan baris yang cukup tinggi untuk menampilkan isinya saat lebar yang tersedia berubah. Contoh ini ditujukan untuk macOS 13 atau lebih baru dan menggunakan tabel berbasis tampilan.

Bagaimana SwiftUI dan NSTableView berbagi pengaturan ukuran?

Ada tiga lapisan yang berperan. Tabel mengalokasikan persegi panjang sel. Auto Layout menghubungkan sel ke NSHostingView yang disematkan di dalamnya. SwiftUI menentukan bagaimana label menyesuaikan diri dengan ruang yang diusulkan kepadanya. Apple menjelaskan NSHostingView sebagai jembatan yang melaporkan preferensi ukuran konten SwiftUI ke AppKit selama tata letak. NSHostingView

Untuk teks yang membungkus, lebar harus ditetapkan sebelum tinggi menjadi bermakna. Label mungkin perlu satu baris di kolom lebar dan beberapa baris di kolom sempit. Menetapkan bingkai awal tampilan hosting tidak membentuk tata letak sel tabel yang bertahan: Apple mendokumentasikan bahwa tabel menetapkan bingkai tampilan yang dikembalikan oleh delegasi selnya. tableView(_:viewFor:row:)

Karena itu, implementasi di bawah mengikuti satu susunan ukuran: tabel menyediakan ruang horizontal, tampilan hosting dibatasi ke tepi sel dengan inset horizontal, dan label SwiftUI mempertahankan ukuran vertikal idealnya sambil menerima lebar yang diusulkan. Tabel menggunakan Auto Layout untuk menghitung tinggi baris saat usesAutomaticRowHeights diaktifkan. usesAutomaticRowHeights

Jangan menganggap intrinsicContentSize dan fittingSize sebagai ukuran yang bisa saling menggantikan. Apple mendefinisikan fittingSize sebagai ukuran terkecil yang memenuhi batasan tampilan dan sub-tampilannya. Karena itu, memintanya tanpa batasan lebar yang dimaksud tidak menjawab seberapa tinggi sel seharusnya pada lebar kolom saat ini. fittingSize

Buat tabel label lengkap untuk macOS 13 dan lebih baru

Buat proyek App macOS di Xcode dengan antarmuka SwiftUI dan target deployment macOS 13 atau lebih baru. Ganti deklarasi app yang dibuat otomatis dengan berkas pertama di bawah, lalu tambahkan dua berkas Swift lainnya ke target app yang sama. Ketiga berkas ini membentuk satu program lengkap; gunakan hanya satu deklarasi @main.

Persyaratan macOS 13 berasal dari penggunaan eksplisit NSHostingView.sizingOptions. Tampilan hosting itu sendiri tersedia sejak macOS 10.15, dan tinggi baris tabel otomatis tersedia sejak macOS 10.13. Target deployment yang lebih lama memerlukan pemeriksaan ketersediaan untuk sizingOptions atau versi implementasi yang tidak menyertakan penyesuaian tersebut. NSHostingView, sizingOptions, usesAutomaticRowHeights

Buat app dan data contoh sintetis

Berkas pertama, LabelSizingApp.swift, menyediakan tiga label dan sebuah tombol yang mengubah label terakhir. Dengan begitu, Anda dapat memeriksa tata letak awal sekaligus pembaruan konten.

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)
        }
    }
}

Hubungkan tampilan hosting ke sel

Di HostedLabelCell.swift, konten SwiftUI menerima lebar yang tersedia dan meminta tinggi idealnya. Modifier fixedSize yang khusus untuk sumbu tertentu sangat penting: menetapkan sumbu horizontal akan meminta teks mempertahankan lebar idealnya dan dapat membuatnya melampaui batas induknya. 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)
    }
}

Keempat batasan tepi menghubungkan tampilan hosting ke sel. Menonaktifkan penerjemahan mask pengubahan ukuran otomatis pada tampilan hosting yang disematkan memungkinkan batasan eksplisit ini mengaturnya, sesuai rekomendasi Apple untuk tampilan yang batasannya memerlukan penyesuaian dinamis. Tabel tetap bertanggung jawab memosisikan sel bagian luar. translatesAutoresizingMaskIntoConstraints

Di sini, sizingOptions mempertahankan pengaturan ukuran konten intrinsik tanpa meminta batasan ukuran minimum dan maksimum dari tampilan hosting. Apple mendokumentasikan kedua perilaku ini sebagai opsi yang dapat dipilih secara terpisah; menetapkan properti ke set opsi kosong tidak membuat batasan apa pun yang diturunkan dari konten tersebut. Mempertahankan pengaturan ukuran konten intrinsik adalah pilihan implementasi untuk sel yang ukurannya mengikuti konten ini, bukan pengaturan universal untuk setiap tampilan hosting. sizingOptions

Aktifkan tinggi baris otomatis dan konfigurasi sel yang digunakan ulang

Tambahkan LabelTable.swift untuk membuat tampilan gulir, tabel, dan delegasi. Program ini sengaja menggunakan pengaturan ukuran otomatis tanpa menerapkan delegasi tinggi baris manual.

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
        }
    }
}

Delegasi mencoba menggunakan ulang tampilan sebelum membuat sel, lalu menetapkan label saat ini setiap kali. Ini mengikuti urutan pembuatan tampilan sel yang direkomendasikan Apple. makeView(withIdentifier:owner:) dapat mengembalikan tampilan yang digunakan ulang atau mengembalikan nil saat tidak ada tampilan yang cocok. tableView(_:viewFor:row:), makeView(withIdentifier:owner:)

Contoh ini hanya memuat ulang saat array input berubah. Memuat ulang seluruhnya mudah dipahami untuk tiga baris, tetapi Apple mendokumentasikan bahwa cara ini melepas lalu mengambil kembali tampilan baris dan sel yang terlihat. Dalam aplikasi yang lebih besar, pilih strategi pembaruan yang lebih terbatas jika sesuai. Lakukan konstruksi dan pembaruan tampilan AppKit di aktor utama; Swift menjelaskan MainActor sebagai aktor yang menggunakan executor setara dengan antrean utama. reloadData(), MainActor

Bagaimana mendiagnosis teks terpotong atau tinggi baris berlebihan?

Mulailah dengan lebar, lalu periksa perambatan tinggi, kemudian periksa pembaruan. Urutan ini mengikuti ketergantungan dalam tata letak dan membantu menghindari perubahan beberapa pengaturan yang tidak terkait sekaligus.

Gejala Pemeriksaan pertama Perbaikan yang berguna
Teks panjang memanjang secara horizontal melewati sel Apakah pengaturan ukuran ideal horizontal diaktifkan? Gunakan fixedSize(horizontal: false, vertical: true) untuk label yang membungkus ini.
Teks membungkus, tetapi baris terakhir terpotong Apakah tampilan hosting terhubung ke kedua tepi vertikal sel? Pulihkan batasan atas dan bawah serta hapus tinggi tetap yang bertentangan.
Semua baris tetap memiliki tinggi yang sama Apakah pengaturan ukuran otomatis diaktifkan, dan apakah ada implementasi tinggi manual? Periksa konfigurasi tabel dan gunakan satu strategi pengaturan ukuran yang jelas.
Teks pendek tampil di sel yang tingginya tidak perlu Apakah konten memiliki tinggi tetap atau ruang vertikal ekstra? Periksa hierarki SwiftUI dan padding-nya sebelum menyesuaikan tabel.
Label yang berubah menampilkan konten lama Apakah sel dikonfigurasi ulang setelah model berubah? Perbarui tampilan akar yang ditampilkan atau muat ulang konten terkait.

Gunakan anggaran lebar agar pemeriksaan lebih konkret. Jika lebar sel secara ilustratif 320 poin, dua inset horizontal 12 poin pada contoh ini menyisakan 296 poin untuk tampilan hosting. Padding vertikalnya berjumlah 16 poin. Tinggi yang dibutuhkan selebihnya bergantung pada tata letak teks sebenarnya; angka-angka tersebut tidak memprediksi berapa baris yang akan ditempati label tertentu.

Bingkai SwiftUI yang mengembang menetapkan perataan leading dalam ruang horizontal yang tersedia. Bingkai ini tidak memberikan lebar fisik tanpa batas: dokumentasi Apple tentang frame menjelaskan bahwa frame berpartisipasi dalam usulan dari induknya dan menerapkan batasan ukuran yang ditentukan. frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)

Jalankan contoh dengan label panjang pada ukuran jendela lebar dan sempit, lalu alihkan kembali ke label pendek. Pastikan semua teks tetap terlihat dan tinggi baris dapat menyusut kembali. Lakukan pemeriksaan penerimaan ini pada versi macOS yang Anda dukung. Kode ini telah ditinjau berdasarkan dokumentasi API yang diperoleh; kode tidak dikompilasi atau dijalankan di sini karena lingkungan ini tidak memiliki compiler Swift atau runtime macOS.

Apa yang berubah jika Anda menghitung tinggi baris secara manual?

Pengaturan ukuran manual adalah jalur implementasi yang terpisah. Jika Anda memerlukannya, hitung tinggi untuk lebar konten yang dimaksud di luar delegasi tinggi, lalu minta delegasi mengembalikan hasil yang telah disiapkan. Kunci cache yang berguna mencakup item, lebar yang tersedia, serta masukan tampilan yang mengubah tata letak, seperti font atau padding.

Apple mensyaratkan tinggi baris yang dikembalikan secara manual bernilai lebih dari nol dan tidak mencakup jarak antarsel. Dokumentasinya juga melarang pengambilan tampilan sel atau baris di dalam tableView(_:heightOfRow:) dan memperingatkan agar tidak melakukan kueri geometri yang dapat memicu tata letak tabel rekursif. Karena itu, mengukur sel yang terlihat dari callback tersebut merupakan dasar yang buruk untuk implementasi manual. tableView(_:heightOfRow:)

Saat tinggi yang disiapkan berubah, beri tahu tabel dengan noteHeightOfRows(withIndexesChanged:). Apple mendokumentasikan bahwa metode ini langsung menyusun ulang tata letak tabel menggunakan tinggi dari delegasi jika metode delegasi tersebut diterapkan. Perubahan ukuran kolom juga harus membatalkan pengukuran yang bergantung pada lebar; columnDidResizeNotification adalah notifikasi yang didokumentasikan untuk peristiwa itu. Hal-hal ini perlu diperhatikan jika Anda mengganti pendekatan otomatis dengan cache manual. noteHeightOfRows(withIndexesChanged:), columnDidResizeNotification

Apakah TextKit sebaiknya mengukur label SwiftUI?

TextKit berguna jika aplikasi Anda benar-benar menata teks melalui kontainer TextKit. Cakupan pengukurannya yang terdokumentasi lebih sempit daripada seluruh sel SwiftUI: NSLayoutManager.usedRect(for:) melaporkan batas glyph yang sudah ditata dalam kontainer yang ditentukan dan tidak dengan sendirinya menghasilkan glyph atau melakukan tata letak. Panggil ensureLayout(for:) sebelum membaca pengukuran tersebut jika tata letak belum selesai. usedRect(for:), ensureLayout(for:)

Perhatikan juga konfigurasi kontainernya. Apple mendokumentasikan nilai default lineFragmentPadding sebesar 5 poin di setiap ujung fragmen baris, yang memengaruhi lebar tata letak yang tersedia. Apple juga menyatakan bahwa properti ini tidak dimaksudkan untuk menyatakan margin teks. lineFragmentPadding

Kesimpulan praktisnya, pengukuran TextKit yang dikonfigurasi secara terpisah tidak dapat dengan sendirinya menetapkan tinggi hierarki SwiftUI ini. Sel ini juga mencakup perilaku tata letak dan padding SwiftUI. Untuk tabel label ini, pertahankan jalur pengaturan ukuran melalui tampilan hosting dan Auto Layout, lalu pastikan teks membungkus, konten berubah, dan sel menyusut pada lebar yang didukung aplikasi Anda.

Sumber