Gunakan NSTokenField milik AppKit di dalam SwiftUI, dengan mengembalikan objek khusus untuk label yang dikenali dan string untuk teks biasa. Pilih gaya tampilan untuk setiap objek agar label yang dikenal muncul sebagai token, sementara teks bebas tetap polos.

Panduan ini membuat editor label ringkas di aplikasi Mac Anda sendiri. Kosakata buatannya berisi Cedar dan Slate; pengguna juga dapat memasukkan frasa seperti draft notes. Editor melaporkan isinya saat pengeditan berakhir. Kolom ini ditujukan untuk entri campuran, dengan koma sebagai pemisah—bukan editor kalimat yang mempertahankan setiap karakter tanda baca.

Pilih kontrol yang sesuai dengan tugas pengeditan

Apple menjelaskan NSTokenField sebagai kolom teks yang mengubah teks yang dimasukkan menjadi token yang berbeda secara visual. Delegatenya dapat menyediakan objek yang direpresentasikan, string tampilan, string yang dapat diedit, dan gaya terpisah untuk setiap objek. Kait tersebut menyediakan titik awal yang didukung untuk antarmuka ini. NSTokenField, NSTokenFieldDelegate.

Buat satu pembedaan sebelum menerapkannya: menerima entri sembarang dan mengedit dokumen sembarang adalah dua tugas berbeda. Kolom label yang dipisahkan koma dapat menggunakan koma sebagai pemisah. Editor templat mungkin perlu mempertahankan koma, spasi, dan teks pada posisi persis di sekitar objek sebaris.

Tugas pengeditan pengguna Titik awal yang sesuai
Memasukkan label yang dikenali bersama entri teks sembarang NSTokenField dengan gaya per objek
Memasukkan satu string biasa SwiftUI TextField
Menyusun teks berkelanjutan dengan objek sebaris dan tanda baca yang posisinya harus persis NSTextView dengan teks beratribut dan lampiran

SwiftUI mendokumentasikan TextField sebagai antarmuka yang dapat diedit dan terikat pada suatu nilai. NSViewRepresentable menyediakan jembatan terdokumentasi untuk menempatkan tampilan AppKit ke dalam SwiftUI. Untuk pengeditan dokumen yang lebih kaya, NSTextView mendukung teks kaya, lampiran, pemilihan, pengelolaan masukan, dan teks yang ditandai. Kemampuan ini mendukung pilihan di atas; tabel tersebut merupakan rekomendasi implementasi yang diturunkan dari kemampuan itu. TextField, NSViewRepresentable, NSTextView.

Pisahkan identitas token dari teks biasa

Gunakan model berurutan dengan dua kasus: token yang dikenali dan teks literal. Dengan demikian, pembedaan yang akan hilang jika semuanya diratakan menjadi satu string tampilan tetap dipertahankan. Array Swift mempertahankan urutan elemen; panduan kolom token Apple menyebutkan bahwa objectValue berisi array objek yang direpresentasikan, termasuk string jika string menjadi nilai yang direpresentasikan. Array, Getting and Setting Token-Field Values.

Untuk contoh ini, pengenalan sengaja dibuat persis dan peka huruf besar-kecil. Cedar menjadi token; cedar tetap menjadi teks. Konversi ini tidak memangkas spasi, mengubah huruf menjadi kecil, menolak, atau menghapus duplikasi entri biasa. Kebijakan ini membuat contoh mudah diprediksi tanpa diam-diam mengubah apa yang dimasukkan seseorang.

Tiga berkas berikut membentuk satu aplikasi contoh yang lengkap. Buat proyek Aplikasi macOS di Xcode menggunakan SwiftUI dan SDK macOS terkini. Ganti titik masuk aplikasi yang dibuat secara otomatis dengan berkas ketiga, lalu tambahkan dua berkas pertama ke target yang sama. Kode ini menggunakan kasus gaya token yang saat ini didokumentasikan, termasuk .none dan .rounded. Contoh lama mungkin menggunakan penulisan berbeda; periksa SDK yang Anda gunakan untuk kompilasi. NSTokenField.TokenStyle.

Definisikan model dan objek yang direpresentasikan

Letakkan kode ini di Entry.swift. LabelToken yang tidak dapat diubah adalah objek yang diterima AppKit untuk label yang dikenali. Entry adalah model nilai yang dikembalikan ke antarmuka SwiftUI.

import Foundation

enum Entry: Equatable {
    case token(String)
    case text(String)

    var summary: String {
        switch self {
        case .token(let value):
            return "Token: \(String(reflecting: value))"
        case .text(let value):
            return "Text: \(String(reflecting: value))"
        }
    }
}

final class LabelToken: NSObject {
    let label: String

    init(_ label: String) {
        self.label = label
        super.init()
    }
}

enum FieldReadError: Error {
    case unexpectedValue
    case unexpectedObject
}

Hubungkan kolom native ke SwiftUI

Letakkan kode ini di MixedEntryField.swift. Representable menginisialisasi kontrol sekali dan menjaga callback tetap mutakhir selama pembaruan SwiftUI. Koordinatornya menerapkan delegat AppKit. Ini mengikuti siklus hidup pembuatan, pembaruan, dan koordinator yang didokumentasikan Apple. SwiftUI mengendalikan tata letak tampilan yang direpresentasikan, jadi pembungkus ini tidak menetapkan framenya. NSViewRepresentable.

import AppKit
import SwiftUI

@MainActor
struct MixedEntryField: NSViewRepresentable {
    let initialEntries: [Entry]
    var onCommit: (Result<[Entry], FieldReadError>) -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(onCommit: onCommit)
    }

    func makeNSView(context: Context) -> NSTokenField {
        let field = NSTokenField()
        field.delegate = context.coordinator
        field.isEditable = true
        field.isSelectable = true
        field.placeholderString = "Cedar, Slate, or free text"
        field.tokenizingCharacterSet = CharacterSet(charactersIn: ",")
        field.setAccessibilityLabel("Labels and free text")
        field.objectValue = initialEntries.map { entry -> Any in
            switch entry {
            case .token(let label):
                return LabelToken(label)
            case .text(let text):
                return text
            }
        }
        return field
    }

    func updateNSView(_ field: NSTokenField, context: Context) {
        context.coordinator.onCommit = onCommit
    }

    final class Coordinator: NSObject, NSTokenFieldDelegate {
        var onCommit: (Result<[Entry], FieldReadError>) -> Void
        private let knownLabels: Set<String> = ["Cedar", "Slate"]

        init(onCommit: @escaping
             (Result<[Entry], FieldReadError>) -> Void) {
            self.onCommit = onCommit
            super.init()
        }

        func tokenField(
            _ tokenField: NSTokenField,
            representedObjectForEditing editingString: String
        ) -> Any? {
            if knownLabels.contains(editingString) {
                return LabelToken(editingString)
            }
            return editingString
        }

        func tokenField(
            _ tokenField: NSTokenField,
            styleForRepresentedObject object: Any
        ) -> NSTokenField.TokenStyle {
            object is LabelToken ? .rounded : .none
        }

        private func label(for object: Any) -> String? {
            if let token = object as? LabelToken {
                return token.label
            }
            return object as? String
        }

        func tokenField(
            _ tokenField: NSTokenField,
            displayStringForRepresentedObject object: Any
        ) -> String? {
            label(for: object)
        }

        func tokenField(
            _ tokenField: NSTokenField,
            editingStringForRepresentedObject object: Any
        ) -> String? {
            label(for: object)
        }

        func controlTextDidEndEditing(_ notification: Notification) {
            guard let field = notification.object as? NSTokenField else {
                return
            }
            onCommit(readEntries(from: field))
        }

        private func readEntries(
            from field: NSTokenField
        ) -> Result<[Entry], FieldReadError> {
            guard let value = field.objectValue else {
                return .success([])
            }
            guard let objects = value as? [Any] else {
                return .failure(.unexpectedValue)
            }

            var entries: [Entry] = []
            for object in objects {
                if let token = object as? LabelToken {
                    entries.append(.token(token.label))
                } else if let text = object as? String {
                    entries.append(.text(text))
                } else {
                    return .failure(.unexpectedObject)
                }
            }
            return .success(entries)
        }
    }
}

Metode konversi mengembalikan string pengeditan asli untuk entri yang tidak dikenali. Apple secara eksplisit mendokumentasikan pengembalian string pengeditan jika tidak ada objek khusus yang direpresentasikan. Delegat gaya menggantikan gaya yang berlaku untuk seluruh kolom bagi setiap objek; label yang dikenali menggunakan .rounded, sedangkan string menggunakan .none. Panduan arsitektur Apple yang diarsipkan juga menjelaskan penggantian token terenkapsulasi dengan token teks biasa melalui kait delegat ini. representedObjectForEditing, styleForRepresentedObject, How Token Fields Work.

Kedua metode konversi string itu penting. Metode tampilan menyediakan label yang mudah dibaca untuk objek yang direpresentasikan. Metode pengeditan menyediakan bentuk tekstual yang dapat diedit; Apple mendokumentasikan pengembalian nil di sana jika token tidak boleh diedit. Contoh ini justru mengembalikan labelnya. displayStringForRepresentedObject, editingStringForRepresentedObject.

Tambahkan antarmuka kecil untuk memeriksa nilai yang sudah disimpan

Letakkan kode ini di MixedTokenDemoApp.swift. Pastikan hanya ada satu deklarasi aplikasi @main di target. Kolom teks kedua menyediakan tujuan eksplisit untuk memindahkan fokus papan ketik keluar dari kolom campuran.

import SwiftUI

@main
struct MixedTokenDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@MainActor
struct ContentView: View {
    @State private var focusDestination = ""
    @State private var output = "Finish editing to inspect the entries."

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Labels and free text")
                .font(.headline)

            MixedEntryField(
                initialEntries: [.token("Cedar")]
            ) { result in
                switch result {
                case .success(let entries):
                    output = entries.isEmpty
                        ? "No entries."
                        : entries.map(\.summary).joined(separator: "\n")
                case .failure:
                    output = "Could not read all entries. The field was kept."
                }
            }
            .frame(height: 28)

            Text("Use commas between entries; then move to the next field.")
                .font(.caption)

            TextField("Move focus here to finish editing",
                      text: $focusDestination)

            Text(output)
                .font(.system(.body, design: .monospaced))
                .textSelection(.enabled)
        }
        .padding(20)
        .frame(minWidth: 540, minHeight: 250)
    }
}

Simpan perubahan tanpa mengganggu pengeditan aktif

Contoh ini menyediakan cuplikan nilai yang sudah disimpan, alih-alih binding yang berubah pada setiap penekanan tombol. Apple mendefinisikan controlTextDidEndEditing sebagai notifikasi bahwa pengeditan telah selesai dan perubahan sudah disimpan. Membaca objek yang direpresentasikan pada titik tersebut memberi antarmuka di sekitarnya waktu yang jelas untuk menggunakan hasilnya. controlTextDidEndEditing.

Hindari menetapkan objectValue tanpa syarat dari updateNSView. Apple mendokumentasikan bahwa pengaturan properti ini saat sel sedang diedit akan membatalkan pengeditan sebelum menetapkan nilai baru. Membacanya selama pengeditan juga memicu validasi. Tindakan ini berdampak, bukan sinkronisasi pasif. objectValue.

Karena itu, pembungkus ini memperlakukan initialEntries sebagai nilai awal. Mengubah argumen tersebut setelah pembuatan tidak akan memuat ulang kolom. Jika kelak aplikasi Anda memerlukan penggantian dari luar, rancang operasi penggantian eksplisit dengan kebijakan untuk pengeditan yang tertunda. Selesaikan dan terima pengeditan saat ini, atau buang secara sengaja, sebelum mengganti nilai yang direpresentasikan.

Dekoder ini juga menghindari compactMap: objek yang direpresentasikan tetapi tidak dikenal akan menghasilkan kegagalan, alih-alih hilang dari hasil. Antarmuka melaporkan kegagalan itu dan membiarkan kolom tetap utuh. Ini penting saat contoh diperluas dengan jenis objek tambahan.

Verifikasi perilaku entri campuran pada versi macOS target Anda

Kode dalam naskah ini diperiksa berdasarkan deklarasi API yang diambil; kode tidak dikompilasi atau dijalankan di sini karena lingkungan yang tersedia adalah Linux tanpa toolchain Swift. Bangun dan uji aplikasi di Mac sebelum memasukkannya ke antarmuka yang lebih besar.

Gunakan urutan diagnosis ini untuk membedakan masalah pengenalan, gaya, dan penyimpanan:

  1. Pastikan objek awal Cedar tampil sebagai token.
  2. Tambahkan Slate dan ketik koma. Pastikan label yang dikenali tampil sebagai token.
  3. Tambahkan draft notes, lalu pindahkan fokus ke kolom kedua. Pastikan frasa itu tetap polos dan muncul sebagai teks pada keluaran yang sudah disimpan.
  4. Masukkan cedar dengan huruf kecil. Pastikan teks tersebut tetap menjadi teks sesuai kebijakan pencocokan persis dalam contoh.
  5. Ubah label yang dikenali menjadi frasa yang tidak dikenal, selesaikan pengeditan, lalu periksa jenis hasilnya.
  6. Hapus semua entri dan selesaikan pengeditan. Pastikan keluaran menyatakan bahwa kolom kosong.
  7. Coba pemilihan, penghapusan, pembatalan, dan komposisi teks dengan metode masukan yang hendak Anda dukung. Pastikan pembaruan SwiftUI yang tidak terkait tidak memulai ulang pengeditan.

Dalam contoh ini, koma adalah sintaks: koma memisahkan entri dan tidak dipertahankan sebagai konten literal. Spasi tidak diatur sebagai pemisah. Jangan sembarangan memperluas himpunan pembatas jika frasa harus dapat memuat karakter tambahan tersebut. Himpunan pembatas dikendalikan melalui tokenizingCharacterSet. tokenizingCharacterSet.

Jangan jadikan penekanan Return berulang sebagai satu-satunya pengujian penyimpanan. Uraian Apple yang diarsipkan menjelaskan bahwa Return dapat mengubah string saat ini menjadi token atau mengirimkan aksi kontrol. Memindahkan fokus ke kontrol lain yang dapat diedit membuat batas akhir pengeditan dalam contoh ini lebih mudah diperiksa. Arsip tersebut memberi latar belakang perilaku; dokumentasi terkini menyediakan deklarasi Swift yang digunakan di atas. How Token Fields Work.

Kapan sebaiknya Anda menggunakan TextKit?

Pilih jalur sistem teks yang lebih kaya jika hasil yang dibutuhkan adalah prosa berkelanjutan yang berisi token sebaris di posisi sembarang. Misalnya, mempertahankan tanda baca persis dalam Arrange [Cedar], then add a note. memerlukan kontrak pengeditan yang berbeda dari penggunaan koma sebagai pemisah entri.

Fondasi yang didukung adalah NSTextView dengan teks beratribut dan objek NSTextAttachment. Apple mendokumentasikan lampiran sebagai nilai atribut dalam string beratribut, dengan sel lampiran di macOS untuk menggambar dan interaksi tetikus. Kemampuan lampiran berbasis tampilan juga didokumentasikan untuk macOS 12 dan versi lebih baru. API ini menyediakan komponen dasar; API tersebut tidak menyediakan model token, kebijakan penyisipan, atau format ekspor lengkap untuk aplikasi Anda. NSTextView, NSTextAttachment.

Pertahankan batas TextKit jika memilih jalur tersebut: Apple memperingatkan bahwa mengakses layoutManager secara eksplisit pada NSTextView atau kontainer teks di macOS 12 dan versi lebih baru akan mengalihkannya ke mode kompatibilitas yang menggunakan NSLayoutManager. Tentukan sistem tata letak teks yang digunakan implementasi Anda sebelum menggabungkan contoh. NSTextView.

Untuk kolom ringkas yang berisi label yang dikenali dan entri teks bebas, mulailah dengan kolom token native beserta konversi delegatnya. Pertahankan teks biasa, tetapkan pemisah secara eksplisit, dan baca objek yang sudah disimpan. Beralihlah ke editor dokumen ketika struktur teks sebaris yang persis menjadi bagian dari tugas.

Sumber