SwiftUI के भीतर AppKit के NSTokenField का उपयोग करें; पहचाने गए लेबल के लिए कस्टम ऑब्जेक्ट और सामान्य टेक्स्ट के लिए स्ट्रिंग लौटाएँ। हर ऑब्जेक्ट के लिए डिस्प्ले स्टाइल चुनें, ताकि ज्ञात लेबल टोकन के रूप में दिखें और साधारण टेक्स्ट सादा रहे।

यह गाइड आपके अपने Mac ऐप में एक छोटा लेबल एडिटर बनाती है। इसकी काल्पनिक शब्दावली में Cedar और Slate हैं; लोग draft notes जैसे वाक्यांश भी दर्ज कर सकते हैं। एडिटिंग समाप्त होने पर एडिटर अपनी सामग्री बताता है। यह मिश्रित प्रविष्टियों के लिए फ़ील्ड है, जिसमें कॉमा विभाजक का काम करते हैं—यह ऐसा वाक्य एडिटर नहीं है जो विराम चिह्नों को जस का तस रखे।

एडिटिंग के काम के अनुसार कंट्रोल चुनें

Apple NSTokenField को ऐसे टेक्स्ट फ़ील्ड के रूप में बताता है जो दर्ज किए गए टेक्स्ट को अलग दिखने वाले टोकन में बदलता है। इसका डेलीगेट दर्शाए जाने वाले ऑब्जेक्ट, डिस्प्ले स्ट्रिंग, एडिट करने योग्य स्ट्रिंग और हर ऑब्जेक्ट के लिए अलग स्टाइल दे सकता है। ये हुक इस इंटरफ़ेस के लिए समर्थित शुरुआती आधार देते हैं। NSTokenField, NSTokenFieldDelegate।

इसे लागू करने से पहले एक अंतर स्पष्ट करें: मनमानी प्रविष्टियाँ स्वीकार करना और किसी मनमाने दस्तावेज़ को एडिट करना अलग-अलग काम हैं। कॉमा से अलग होने वाला लेबल फ़ील्ड कॉमा को विभाजक के रूप में इस्तेमाल कर सकता है। टेम्पलेट एडिटर को इनलाइन ऑब्जेक्ट के आसपास कॉमा, स्पेस और टेक्स्ट को ठीक उन्हीं स्थानों पर बनाए रखना पड़ सकता है।

पाठक का एडिटिंग कार्य उपयुक्त शुरुआती विकल्प
पहचाने गए लेबल के साथ मनमानी टेक्स्ट प्रविष्टियाँ दर्ज करना हर ऑब्जेक्ट के लिए स्टाइल वाला NSTokenField
एक सामान्य स्ट्रिंग दर्ज करना SwiftUI TextField
इनलाइन ऑब्जेक्ट और सटीक विराम चिह्नों के साथ लगातार टेक्स्ट लिखना एट्रिब्यूटेड टेक्स्ट और अटैचमेंट वाला NSTextView

SwiftUI TextField को किसी वैल्यू से बँधे एडिट करने योग्य इंटरफ़ेस के रूप में दस्तावेज़ित करता है। NSViewRepresentable SwiftUI में AppKit व्यू रखने के लिए दस्तावेज़ित ब्रिज देता है। अधिक समृद्ध दस्तावेज़ एडिटिंग के लिए NSTextView रिच टेक्स्ट, अटैचमेंट, चयन, इनपुट प्रबंधन और मार्क्ड टेक्स्ट का समर्थन करता है। ये क्षमताएँ ऊपर दिए विकल्पों का आधार हैं; तालिका उनसे निकली कार्यान्वयन संबंधी सिफ़ारिश है। TextField, NSViewRepresentable, NSTextView।

टोकन की पहचान को सामान्य टेक्स्ट से अलग रखें

दो स्थितियों वाला क्रमबद्ध मॉडल इस्तेमाल करें: पहचाना गया टोकन और शाब्दिक टेक्स्ट। इससे वह अंतर बना रहता है जो सब कुछ एक डिस्प्ले स्ट्रिंग में समेटने पर मिट जाता। Swift ऐरे तत्वों का क्रम बनाए रखते हैं; Apple की टोकन फ़ील्ड गाइड बताती है कि objectValue में दर्शाए गए ऑब्जेक्ट का ऐरे होता है, जिसमें स्ट्रिंग भी शामिल होती हैं, जब स्ट्रिंग ही दर्शाई गई वैल्यू हों। Array, टोकन फ़ील्ड की वैल्यू प्राप्त करना और सेट करना।

इस उदाहरण में पहचान जानबूझकर सटीक और केस-संवेदी है। Cedar टोकन बन जाता है; cedar टेक्स्ट बना रहता है। यह रूपांतरण सामान्य प्रविष्टियों को ट्रिम, लोअरकेस, अस्वीकार या डुप्लिकेट-मुक्त नहीं करता। यह नीति उपयोगकर्ता की प्रविष्टि को चुपचाप बदले बिना उदाहरण को अनुमानित रखती है।

नीचे दी गई तीन फ़ाइलें एक पूरा उदाहरण ऐप बनाती हैं। SwiftUI और मौजूदा macOS SDK का उपयोग करके Xcode में macOS App प्रोजेक्ट बनाएँ। जनरेट किए गए ऐप एंट्री पॉइंट को तीसरी फ़ाइल से बदलें और पहली दो फ़ाइलें उसी टार्गेट में जोड़ें। कोड मौजूदा दस्तावेज़ीकृत टोकन-स्टाइल केस, जिनमें .none और .rounded शामिल हैं, इस्तेमाल करता है। पुराने उदाहरणों में अलग स्पेलिंग हो सकती हैं; जिस SDK के साथ आप कंपाइल करते हैं, उसे जाँचें। NSTokenField.TokenStyle।

मॉडल और दर्शाया जाने वाला ऑब्जेक्ट परिभाषित करें

इसे Entry.swift में रखें। अपरिवर्तनीय LabelToken वह ऑब्जेक्ट है जो पहचाने गए लेबल के लिए AppKit को मिलता है। Entry वह वैल्यू मॉडल है जो 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
}

नेटिव फ़ील्ड को SwiftUI में ब्रिज करें

इसे MixedEntryField.swift में रखें। Representable कंट्रोल को एक बार शुरू करता है और SwiftUI अपडेट के दौरान कॉलबैक को मौजूदा रखता है। इसका कोऑर्डिनेटर AppKit डेलीगेट लागू करता है। यह Apple के दस्तावेज़ीकृत निर्माण, अपडेट और कोऑर्डिनेटर लाइफ़साइकल का पालन करता है। दर्शाए गए व्यू का लेआउट SwiftUI नियंत्रित करता है, इसलिए रैपर उसका फ़्रेम सेट नहीं करता। 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)
        }
    }
}

रूपांतरण विधि अपरिचित प्रविष्टि के लिए मूल एडिटिंग स्ट्रिंग लौटाती है। Apple स्पष्ट रूप से दस्तावेज़ित करता है कि कस्टम दर्शाया गया ऑब्जेक्ट न होने पर एडिटिंग स्ट्रिंग लौटाएँ। स्टाइल डेलीगेट हर ऑब्जेक्ट के लिए फ़ील्ड-व्यापी स्टाइल को ओवरराइड करता है; पहचाने गए लेबल .rounded और स्ट्रिंग .none इस्तेमाल करते हैं। Apple की पुरानी आर्किटेक्चर गाइड भी इस डेलीगेट हुक के ज़रिए एनकैप्सुलेटेड टोकन की जगह सादे टेक्स्ट वाले टोकन रखने का वर्णन करती है। representedObjectForEditing, styleForRepresentedObject, टोकन फ़ील्ड कैसे काम करते हैं।

दोनों स्ट्रिंग-रूपांतरण विधियाँ अहम हैं। डिस्प्ले विधि दर्शाए गए ऑब्जेक्ट के लिए पढ़ने योग्य लेबल देती है। एडिटिंग विधि उसका एडिट करने योग्य टेक्स्ट रूप देती है; Apple दस्तावेज़ित करता है कि जब टोकन एडिट करने योग्य न हो, तो वहाँ nil लौटाएँ। यह उदाहरण इसके बजाय लेबल लौटाता है। displayStringForRepresentedObject, editingStringForRepresentedObject।

कमिट की गई वैल्यू देखने के लिए छोटा इंटरफ़ेस जोड़ें

इसे MixedTokenDemoApp.swift में रखें। टार्गेट में केवल एक @main ऐप डिक्लेरेशन रखें। दूसरा टेक्स्ट फ़ील्ड मिश्रित फ़ील्ड से कीबोर्ड फ़ोकस हटाने के लिए स्पष्ट स्थान देता है।

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

सक्रिय एडिटिंग में बाधा डाले बिना बदलाव कमिट करें

यह उदाहरण हर कीस्ट्रोक पर बदलने वाले बाइंडिंग के बजाय कमिट किया गया स्नैपशॉट दिखाता है। Apple controlTextDidEndEditing को उस सूचना के रूप में परिभाषित करता है कि एडिटिंग पूरी हो गई है और बदलाव कमिट किए जा चुके हैं। उस बिंदु पर दर्शाए गए ऑब्जेक्ट पढ़ने से आसपास के इंटरफ़ेस को नतीजे का उपयोग करने के लिए स्पष्ट समय मिलता है। controlTextDidEndEditing।

updateNSView से objectValue को बिना शर्त असाइन करने से बचें। Apple दस्तावेज़ित करता है कि सेल के एडिट होने के दौरान यह प्रॉपर्टी सेट करने पर नई वैल्यू असाइन करने से पहले एडिटिंग रुक जाती है। एडिटिंग के दौरान इसे पढ़ने से वैलिडेशन भी चलता है। ये असर डालने वाली कार्रवाइयाँ हैं, निष्क्रिय सिंक्रोनाइज़ेशन नहीं। objectValue।

इसलिए रैपर initialEntries को शुरुआती सीड मानता है। निर्माण के बाद यह आर्ग्युमेंट बदलने से फ़ील्ड फिर से लोड नहीं होता। अगर आपके ऐप को बाद में बाहरी वैल्यू से बदलने की ज़रूरत हो, तो लंबित एडिट के लिए नीति के साथ स्पष्ट रिप्लेसमेंट ऑपरेशन बनाएँ। दर्शाई गई वैल्यू बदलने से पहले मौजूदा एडिट पूरा करके स्वीकार करें, या जानबूझकर उसे छोड़ दें।

डिकोडर compactMap से भी बचता है: अपरिचित दर्शाया गया ऑब्जेक्ट नतीजे से गायब होने के बजाय विफलता देता है। इंटरफ़ेस उस विफलता की सूचना देता है और फ़ील्ड को जस का तस छोड़ता है। उदाहरण को अतिरिक्त ऑब्जेक्ट प्रकारों के साथ बढ़ाते समय यह मायने रखता है।

अपने लक्षित macOS संस्करणों पर मिश्रित प्रविष्टियों का व्यवहार जाँचें

इस लेख की कोड को प्राप्त API डिक्लेरेशन के आधार पर जाँचा गया; यहाँ इसे कंपाइल या चलाया नहीं गया, क्योंकि उपलब्ध वातावरण Linux है और इसमें Swift टूलचेन नहीं है। ऐप को बड़े इंटरफ़ेस में शामिल करने से पहले Mac पर बिल्ड करें और चलाकर देखें।

पहचान, स्टाइलिंग और कमिट संबंधी समस्याओं को अलग-अलग समझने के लिए यह जाँच क्रम अपनाएँ:

  1. पुष्टि करें कि शुरुआती Cedar ऑब्जेक्ट टोकन जैसा दिखता है।
  2. Slate जोड़ें और कॉमा टाइप करें। पुष्टि करें कि पहचाने गए लेबल को टोकन जैसा रूप मिलता है।
  3. draft notes जोड़ें, फिर फ़ोकस दूसरे फ़ील्ड पर ले जाएँ। पुष्टि करें कि वाक्यांश सादा रहता है और कमिट किए गए आउटपुट में टेक्स्ट के रूप में दिखता है।
  4. लोअरकेस cedar दर्ज करें। पुष्टि करें कि उदाहरण की सटीक-मिलान नीति के तहत यह टेक्स्ट ही रहता है।
  5. किसी मौजूदा पहचाने गए लेबल को एडिट करके अपरिचित वाक्यांश बनाएँ, एडिटिंग पूरी करें और उसका परिणामी प्रकार जाँचें।
  6. सभी प्रविष्टियाँ हटाकर एडिटिंग पूरी करें। पुष्टि करें कि आउटपुट खाली फ़ील्ड बताता है।
  7. जिन इनपुट मेथड का समर्थन करना चाहते हैं, उनसे चयन, मिटाना, अनडू और टेक्स्ट कंपोज़िशन आज़माएँ। जाँचें कि असंबंधित SwiftUI अपडेट एडिटिंग को फिर से शुरू न करें।

इस उदाहरण में कॉमा सिंटैक्स है: यह प्रविष्टियों को अलग करता है और शाब्दिक सामग्री के रूप में सुरक्षित नहीं रहता। स्पेस को विभाजक के रूप में कॉन्फ़िगर नहीं किया गया है। अगर वाक्यांशों में जोड़े गए वर्ण होने चाहिए, तो विभाजकों की सूची को यूँ ही न बढ़ाएँ। विभाजकों की सूची tokenizingCharacterSet से नियंत्रित होती है। tokenizingCharacterSet।

कमिट की जाँच के लिए बार-बार Return दबाने को अपना एकमात्र तरीका न बनाएँ। Apple का पुराना विवरण बताता है कि Return मौजूदा स्ट्रिंग को टोकन बना सकता है या कंट्रोल की ऐक्शन भेज सकता है। फ़ोकस किसी दूसरे एडिट करने योग्य कंट्रोल पर ले जाने से इस उदाहरण की एडिटिंग-समाप्ति सीमा को जाँचना आसान होता है। पुराना दस्तावेज़ व्यवहार की पृष्ठभूमि देता है; मौजूदा दस्तावेज़ ऊपर इस्तेमाल की गई Swift डिक्लेरेशन देता है। टोकन फ़ील्ड कैसे काम करते हैं।

आपको TextKit कब इस्तेमाल करना चाहिए?

जब अपेक्षित नतीजा ऐसा लगातार गद्य हो जिसमें मनमानी जगहों पर इनलाइन टोकन हों, तब अधिक समृद्ध टेक्स्ट-सिस्टम वाला रास्ता चुनें। उदाहरण के लिए, Arrange [Cedar], then add a note. में विराम चिह्नों को ठीक-ठीक सुरक्षित रखने के लिए प्रविष्टियों के बीच कॉमा हटाने से अलग एडिटिंग अनुबंध चाहिए।

समर्थित आधार एट्रिब्यूटेड टेक्स्ट और NSTextAttachment ऑब्जेक्ट के साथ NSTextView है। Apple अटैचमेंट को एट्रिब्यूटेड स्ट्रिंग में एट्रिब्यूट वैल्यू के रूप में दस्तावेज़ित करता है; macOS पर ड्रॉइंग और माउस इंटरैक्शन के लिए अटैचमेंट सेल होता है। व्यू-आधारित अटैचमेंट क्षमताएँ macOS 12 और उसके बाद के संस्करणों के लिए भी दस्तावेज़ित हैं। ये API बिल्डिंग ब्लॉक देती हैं; ये आपके ऐप के लिए पूरा टोकन मॉडल, इंसर्शन नीति या एक्सपोर्ट फ़ॉर्मैट नहीं देतीं। NSTextView, NSTextAttachment।

अगर यह रास्ता चुनते हैं, तो TextKit की सीमा बनाए रखें: Apple चेतावनी देता है कि macOS 12 और उसके बाद NSTextView या टेक्स्ट कंटेनर के layoutManager को स्पष्ट रूप से ऐक्सेस करने पर वह NSLayoutManager इस्तेमाल करने वाले कम्पैटिबिलिटी मोड में चला जाता है। उदाहरणों को मिलाने से पहले तय करें कि आपका कार्यान्वयन कौन-सा टेक्स्ट लेआउट सिस्टम इस्तेमाल करेगा। NSTextView।

पहचाने गए लेबल और साधारण टेक्स्ट प्रविष्टियों वाले छोटे फ़ील्ड के लिए नेटिव टोकन फ़ील्ड और उसके डेलीगेट रूपांतरणों से शुरुआत करें। सामान्य टेक्स्ट जस का तस रखें, विभाजक स्पष्ट रूप से तय करें और कमिट किए गए दर्शाए गए ऑब्जेक्ट पढ़ें। जब सटीक इनलाइन टेक्स्ट संरचना काम का हिस्सा बन जाए, तब दस्तावेज़ एडिटर पर जाएँ।

स्रोत