Gunakan NSTokenField daripada AppKit dalam SwiftUI, dengan memulangkan objek tersuai untuk label yang dikenal pasti dan rentetan untuk teks biasa. Pilih gaya paparan bagi setiap objek supaya label yang diketahui muncul sebagai token manakala teks bebas kekal sebagai teks biasa.

Panduan ini membina penyunting label ringkas dalam aplikasi Mac anda sendiri. Perbendaharaan kata rekaan ini mengandungi Cedar dan Slate; pengguna juga boleh memasukkan frasa seperti draft notes. Penyunting melaporkan kandungannya apabila penyuntingan berakhir. Ini ialah medan untuk entri bercampur, dengan koma sebagai pemisah—bukan penyunting ayat yang mengekalkan setiap aksara tanda baca.

Pilih kawalan yang sepadan dengan tugas penyuntingan

Apple menerangkan NSTokenField sebagai medan teks yang menukar teks yang dimasukkan menjadi token yang berbeza secara visual. Perwakilannya boleh membekalkan objek yang diwakili, rentetan paparan, rentetan boleh sunting, dan gaya berasingan bagi setiap objek. Cangkuk ini menyediakan titik permulaan yang disokong untuk antara muka ini. NSTokenField, NSTokenFieldDelegate.

Buat satu perbezaan sebelum melaksanakannya: menerima entri sewenang-wenangnya dan menyunting dokumen sewenang-wenangnya ialah dua tugas berbeza. Medan label yang dipisahkan koma boleh menggunakan koma sebagai pemisah. Penyunting templat mungkin perlu mengekalkan koma, ruang dan teks pada kedudukan tepat di sekeliling objek sebaris.

Tugas penyuntingan pengguna Titik permulaan yang sesuai
Memasukkan label yang dikenal pasti bersama entri teks sewenang-wenangnya NSTokenField dengan gaya bagi setiap objek
Memasukkan satu rentetan biasa SwiftUI TextField
Mengarang teks berterusan dengan objek sebaris dan tanda baca yang tepat NSTextView dengan teks beratribut dan lampiran

SwiftUI mendokumenkan TextField sebagai antara muka boleh sunting yang terikat pada suatu nilai. NSViewRepresentable menyediakan jambatan yang didokumenkan untuk meletakkan paparan AppKit ke dalam SwiftUI. Untuk penyuntingan dokumen yang lebih kaya, NSTextView menyokong teks berformat, lampiran, pemilihan, pengurusan input dan teks bertanda. Keupayaan ini menyokong pilihan di atas; jadual ini ialah saranan pelaksanaan yang dirumus daripada keupayaan tersebut. TextField, NSViewRepresentable, NSTextView.

Asingkan identiti token daripada teks biasa

Gunakan model tersusun dengan dua kes: token yang dikenal pasti dan teks literal. Ini mengekalkan perbezaan yang akan hilang jika semuanya diratakan menjadi satu rentetan paparan. Tatasusunan Swift mengekalkan tertib unsur; panduan medan token Apple menyatakan bahawa objectValue mengandungi tatasusunan objek yang diwakili, termasuk rentetan apabila rentetan menjadi nilai yang diwakili. Array, Getting and Setting Token-Field Values.

Untuk contoh ini, pengecaman sengaja dibuat secara tepat dan peka huruf besar-kecil. Cedar menjadi token; cedar kekal sebagai teks. Penukaran ini tidak memangkas, menukar kepada huruf kecil, menolak atau membuang pendua entri biasa. Dasar itu menjadikan contoh ini mudah dijangka tanpa mengubah secara senyap apa yang dimasukkan seseorang.

Tiga fail berikut membentuk satu aplikasi contoh yang lengkap. Cipta projek Aplikasi macOS dalam Xcode menggunakan SwiftUI dan SDK macOS semasa. Gantikan titik masuk aplikasi yang dijana dengan fail ketiga, dan tambahkan dua fail pertama pada sasaran yang sama. Kod ini menggunakan kes gaya token semasa yang didokumenkan, termasuk .none dan .rounded. Contoh lama mungkin menggunakan ejaan yang berbeza; semak SDK yang anda gunakan untuk kompilasi. NSTokenField.TokenStyle.

Takrifkan model dan objek yang diwakili

Letakkan kod ini dalam Entry.swift. LabelToken yang tidak boleh diubah ialah objek yang diterima AppKit bagi label yang dikenal pasti. Entry ialah model nilai yang dipulangkan kepada antara muka 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
}

Jambatankan medan natif ke dalam SwiftUI

Letakkan kod ini dalam MixedEntryField.swift. Representable ini memulakan kawalan sekali dan memastikan panggil balik sentiasa terkini semasa kemas kini SwiftUI. Penyelarasnya melaksanakan perwakilan AppKit. Ini mengikut kitar hayat penciptaan, pengemaskinian dan penyelaras yang didokumenkan oleh Apple. SwiftUI mengawal reka letak paparan yang diwakili, jadi pembalut ini tidak menetapkan bingkainya. 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)
        }
    }
}

Kaedah penukaran memulangkan rentetan penyuntingan asal bagi entri yang tidak dikenal pasti. Apple secara jelas mendokumenkan pemulangan rentetan penyuntingan apabila tiada objek tersuai yang diwakili. Perwakilan gaya mengatasi gaya seluruh medan bagi setiap objek; label yang dikenal pasti menggunakan .rounded, manakala rentetan menggunakan .none. Panduan seni bina Apple yang diarkibkan juga menerangkan penggantian token terkapsul dengan token teks biasa melalui cangkuk perwakilan ini. representedObjectForEditing, styleForRepresentedObject, How Token Fields Work.

Kedua-dua kaedah penukaran rentetan penting. Kaedah paparan membekalkan label yang boleh dibaca untuk objek yang diwakili. Kaedah penyuntingan membekalkan bentuk teks yang boleh disunting; Apple mendokumenkan pemulangan nil di situ apabila token tidak boleh disunting. Contoh ini sebaliknya memulangkan label. displayStringForRepresentedObject, editingStringForRepresentedObject.

Tambahkan antara muka ringkas untuk memeriksa nilai yang telah dikomit

Letakkan kod ini dalam MixedTokenDemoApp.swift. Pastikan hanya satu pengisytiharan aplikasi @main dalam sasaran. Medan teks kedua menyediakan destinasi yang jelas untuk mengalihkan fokus papan kekunci keluar daripada medan bercampur.

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

Komit perubahan tanpa mengganggu penyuntingan aktif

Contoh ini mendedahkan petikan nilai yang telah dikomit, bukannya pengikatan yang berubah pada setiap ketukan kekunci. Apple mentakrifkan controlTextDidEndEditing sebagai pemberitahuan bahawa penyuntingan telah selesai dan perubahan telah dikomit. Membaca objek yang diwakili pada titik itu memberi antara muka sekeliling masa yang jelas untuk menggunakan hasilnya. controlTextDidEndEditing.

Elakkan daripada menetapkan objectValue tanpa syarat daripada updateNSView. Apple mendokumenkan bahawa menetapkan sifat ini semasa sel sedang disunting akan membatalkan penyuntingan sebelum menetapkan nilai baharu. Membacanya semasa penyuntingan juga mencetuskan pengesahan. Ini ialah operasi yang mempunyai kesan penting, bukannya penyegerakan pasif. objectValue.

Oleh itu, pembalut ini menganggap initialEntries sebagai nilai awal. Mengubah argumen itu selepas penciptaan tidak akan memuat semula medan. Jika aplikasi anda kelak memerlukan penggantian luaran, reka bentuk operasi penggantian yang jelas dengan dasar untuk suntingan tertunda. Selesaikan dan terima suntingan semasa, atau sengaja buangkannya, sebelum menggantikan nilai yang diwakili.

Penyahkod juga mengelakkan compactMap: objek diwakili yang tidak dikenali akan menghasilkan kegagalan dan bukannya hilang daripada hasilnya. Antara muka melaporkan kegagalan itu dan membiarkan medan kekal seperti sedia ada. Hal ini penting apabila contoh ini dikembangkan dengan jenis objek tambahan.

Sahkan tingkah laku entri bercampur pada versi macOS sasaran anda

Kod manuskrip ini telah diperiksa berdasarkan pengisytiharan API yang diperoleh; kod ini tidak dikompil atau dijalankan di sini kerana persekitaran yang tersedia ialah Linux tanpa rangkaian alat Swift. Bina dan uji aplikasi pada Mac sebelum memasukkannya ke dalam antara muka yang lebih besar.

Gunakan urutan diagnostik ini untuk membezakan masalah pengecaman, penggayaan dan komit:

  1. Pastikan objek awal Cedar mempunyai rupa token.
  2. Tambahkan Slate dan taip koma. Pastikan label yang dikenal pasti mendapat rupa token.
  3. Tambahkan draft notes, kemudian alihkan fokus ke medan kedua. Pastikan frasa itu kekal biasa dan muncul sebagai teks dalam output yang telah dikomit.
  4. Masukkan cedar dalam huruf kecil. Pastikan ia kekal sebagai teks mengikut dasar padanan tepat contoh ini.
  5. Sunting label dikenal pasti yang sedia ada menjadi frasa yang tidak diketahui, tamatkan penyuntingan dan semak jenis hasilnya.
  6. Buang semua entri dan tamatkan penyuntingan. Pastikan output melaporkan medan kosong.
  7. Cuba pemilihan, pemadaman, buat asal dan penggubahan teks dengan kaedah input yang ingin anda sokong. Pastikan kemas kini SwiftUI yang tidak berkaitan tidak memulakan semula penyuntingan.

Koma ialah sintaks dalam contoh ini: koma memisahkan entri dan tidak dikekalkan sebagai kandungan literal. Ruang tidak dikonfigurasikan sebagai pemisah. Jangan tambah aksara pemisah dengan mudah jika frasa perlu mengandungi aksara tambahan itu. Set pemisah dikawal melalui tokenizingCharacterSet. tokenizingCharacterSet.

Jangan gunakan tekanan kekunci Return berulang sebagai satu-satunya ujian komit. Huraian Apple yang diarkibkan menjelaskan bahawa Return boleh menokenkan rentetan semasa atau menghantar tindakan kawalan. Mengalihkan fokus ke kawalan boleh sunting yang lain menjadikan sempadan tamat penyuntingan contoh ini lebih mudah diperiksa. Arkib tersebut memberikan latar tingkah laku; dokumentasi semasa membekalkan pengisytiharan Swift yang digunakan di atas. How Token Fields Work.

Bilakah anda patut menggunakan TextKit?

Pilih laluan sistem teks yang lebih kaya apabila hasil yang diperlukan ialah prosa berterusan dengan token sebaris pada kedudukan sewenang-wenangnya. Contohnya, mengekalkan tanda baca tepat dalam Arrange [Cedar], then add a note. memerlukan kontrak penyuntingan yang berbeza daripada menggunakan koma antara entri.

Asas yang disokong ialah NSTextView dengan teks beratribut dan objek NSTextAttachment. Apple mendokumenkan lampiran sebagai nilai atribut dalam rentetan beratribut, dengan sel lampiran pada macOS untuk lukisan dan interaksi tetikus. Keupayaan lampiran berasaskan paparan juga didokumenkan untuk macOS 12 dan versi lebih baharu. API ini menyediakan bahan asas; API ini tidak membekalkan model token, dasar penyisipan atau format eksport yang lengkap untuk aplikasi anda. NSTextView, NSTextAttachment.

Kekalkan sempadan TextKit apabila memilih laluan itu: Apple memberi amaran bahawa mengakses layoutManager pada NSTextView atau bekas teks secara jelas pada macOS 12 dan versi lebih baharu akan menukarnya kepada mod keserasian yang menggunakan NSLayoutManager. Tentukan sistem reka letak teks yang akan digunakan oleh pelaksanaan anda sebelum menggabungkan contoh. NSTextView.

Untuk medan ringkas yang mengandungi label dikenal pasti dan entri teks bebas, mulakan dengan medan token natif dan penukaran perwakilannya. Kekalkan teks biasa, nyatakan pemisah dengan jelas dan baca objek yang diwakili setelah dikomit. Beralih kepada penyunting dokumen apabila struktur teks sebaris yang tepat menjadi sebahagian daripada tugas.

Sumber