स्वीकृत टेक्स्ट को पैरेंट व्यू में रखकर और अलग ड्राफ़्ट को शीट में संपादित करके Mac एडिटर बनाएँ। केवल Commit ड्राफ़्ट को वापस कॉपी करता है; Cancel शीट बंद कर देता है। यह ट्यूटोरियल आपके अपने ऐप में छोटे, सादे टेक्स्ट वाले नोट के लिए यही प्रवाह लागू करता है।

तय करें कि Commit और Cancel को क्या करना चाहिए

पाठक का काम सीधा है: मौजूदा नोट खोलें, उसकी कई पंक्तियाँ संशोधित करें, और या तो पूरा संशोधन स्वीकार करें या मूल नोट को जस का तस छोड़ दें। उदाहरण में कृत्रिम टेक्स्ट और एक ही संपादन सत्र का उपयोग किया गया है। स्वीकृत टेक्स्ट मेमोरी में रखा जाता है; फ़ाइल स्टोरेज जोड़ना ऐप्लिकेशन का अलग निर्णय है।

Apple का TextEditor कई पंक्तियों वाला, स्क्रॉल होने योग्य टेक्स्ट दिखाता है और संपादित किए जा रहे मान के लिए बाइंडिंग स्वीकार करता है। इसलिए स्वीकृत मान से बाइंडिंग करने पर संपादन उसी मान को तुरंत बदल सकते हैं। बदलावों को बाद में स्वीकार करने के लिए, एडिटर को ड्राफ़्ट से बाइंड करें। Apple का TextEditor दस्तावेज़ीकरण देखें।

डिज़ाइन में तीन हिस्से हैं: पैरेंट के स्वामित्व वाला स्वीकृत टेक्स्ट, संपादन शुरू होने पर बनाया गया स्नैपशॉट, और शीट के स्वामित्व वाली परिवर्तनशील ड्राफ़्ट स्थिति। Swift की String में value semantics होती है, इसलिए ड्राफ़्ट बदलने से स्वीकृत स्ट्रिंग नहीं बदलती। इसकी copy-on-write स्टोरेज एक कार्यान्वयन अनुकूलन है; स्वतंत्र मानों का व्यवहार वही रहता है। String देखें।

घटना ड्राफ़्ट का व्यवहार स्वीकृत टेक्स्ट
एडिटर खोलें मौजूदा स्वीकृत टेक्स्ट से शुरू होता है अपरिवर्तित
टेक्स्ट टाइप करें या हटाएँ शीट के भीतर बदलता है अपरिवर्तित
Commit चुनें पैरेंट को दिया जाता है, फिर शीट बंद हो जाती है ड्राफ़्ट से बदल जाता है
Cancel चुनें ड्राफ़्ट वापस दिए बिना शीट बंद हो जाती है अपरिवर्तित
एडिटर फिर खोलें नया सत्र शुरू होता है नया शुरुआती टेक्स्ट देता है

यह तालिका कार्यान्वयन का अनुबंध भी है। शीट बंद होना अपने आप में स्वीकृति नहीं है। इस अंतर को स्पष्ट रखने से rollback ऑपरेशन के बिना रद्द करना संभव होता है।

macOS का पूरा उदाहरण बनाएँ

Xcode में SwiftUI का उपयोग करके macOS ऐप प्रोजेक्ट बनाएँ और उसका deployment target macOS 12 या बाद का रखें। जनरेट की गई ऐप और content-view घोषणाओं को नीचे दी गई एकल स्रोत फ़ाइल से बदलें। पुरानी ऐप घोषणा हटा दें, ताकि टार्गेट में ठीक एक @main entry point हो। इस नमूने में नए attributed-text initializer के बजाय सादा टेक्स्ट संपादन है।

macOS 12 की न्यूनतम आवश्यकता शीट के DismissAction के कारण है; सादे टेक्स्ट वाला TextEditor macOS 11 से उपलब्ध है। Apple ने DismissAction और TextEditor में इन उपलब्धता सीमाओं का दस्तावेज़ दिया है।

import SwiftUI
import Foundation

@main
struct DraftEditorApp: App {
    var body: some Scene {
        WindowGroup {
            NoteView()
        }
    }
}

private struct EditSession: Identifiable {
    let id = UUID()
    let initialText: String
}

private struct NoteView: View {
    @State private var acceptedText = """
    Sample note
    Arrange the blue tiles beside the green tiles.
    """
    @State private var editSession: EditSession?

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Accepted note")
                .font(.headline)

            ScrollView {
                Text(acceptedText.isEmpty ? "(Empty note)" : acceptedText)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }

            Button("Edit Note") {
                editSession = EditSession(initialText: acceptedText)
            }
        }
        .padding(20)
        .frame(minWidth: 460, minHeight: 300)
        .sheet(item: $editSession) { session in
            DraftEditor(initialText: session.initialText) { newText in
                acceptedText = newText
            }
            .id(session.id)
        }
    }
}

private struct DraftEditor: View {
    @Environment(\.dismiss) private var dismiss
    @State private var draft: String

    private let initialText: String
    private let onCommit: (String) -> Void

    init(
        initialText: String,
        onCommit: @escaping (String) -> Void
    ) {
        self.initialText = initialText
        self.onCommit = onCommit
        _draft = State(initialValue: initialText)
    }

    private var hasChanges: Bool {
        draft != initialText
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Edit note")
                .font(.headline)

            TextEditor(text: $draft)
                .font(.body)
                .accessibilityLabel("Note draft")
                .frame(minHeight: 220)

            Text(hasChanges ? "Draft changed" : "No changes")
                .font(.caption)
                .foregroundColor(.secondary)

            HStack {
                Spacer()

                Button("Cancel") {
                    dismiss()
                }
                .keyboardShortcut(.cancelAction)

                Button("Commit") {
                    onCommit(draft)
                    dismiss()
                }
                .keyboardShortcut(.return, modifiers: .command)
            }
        }
        .padding(20)
        .frame(minWidth: 480, minHeight: 340)
    }
}

यह एक पूरा ऐप्लिकेशन उदाहरण है, जिसमें शुरुआती टेक्स्ट, प्रस्तुति स्थिति, एडिटर और entry point शामिल हैं। शोध परिवेश में Swift compiler और macOS SDK उपलब्ध नहीं थे, इसलिए नमूने को यहाँ compile या run करने के बजाय प्राप्त API दस्तावेज़ों के आधार पर जाँचा गया।

पैरेंट स्वीकृत मान का स्वामी क्यों है

पैरेंट acceptedText का स्वामी है, क्योंकि वही स्वीकृत परिणाम दिखाता है और अगले संपादन सत्र के लिए शुरुआती मान देता है। चाइल्ड draft का स्वामी है, क्योंकि इसे बदलने की ज़रूरत केवल संपादन इंटरफ़ेस को है। यह Apple के उस मार्गदर्शन के अनुरूप है जिसमें state को private रखने और view hierarchy के उचित स्तर पर रखने की सलाह दी गई है। State देखें।

पैरेंट एडिटर में $acceptedText पास नहीं करता। इसके बजाय, वह एक string value और एक closure पास करता है। संशोधन स्वीकार करने वाला एकमात्र assignment इसी closure में है: acceptedText = newText। इस कार्यान्वयन में न तो टाइपिंग और न ही रद्द करने से उस assignment तक पहुँचा जा सकता है।

हर बार खोलने पर सत्र का पहचानकर्ता क्यों मिलता है

वैकल्पिक EditSession प्रस्तुति स्थिति और शुरुआती टेक्स्ट, दोनों देता है। Apple के दस्तावेज़ के अनुसार, sheet(item:onDismiss:content:) तब सामग्री प्रस्तुत करता है जब उससे बाइंड किया गया item nil न हो, और उस item को content closure में पास करता है। sheet(item:onDismiss:content:) देखें।

Edit Note दबाने पर हर बार एक नया पहचानकर्ता बनता है। उस पहचानकर्ता को एडिटर पर लागू करने से इच्छित state lifetime स्पष्ट होती है: एक ड्राफ़्ट एक संपादन सत्र का होता है। initializer शुरुआती state value देता है; मौजूदा ड्राफ़्ट को पैरेंट में बाद में हुए बदलावों के साथ लगातार सिंक्रनाइज़ करने का यह तरीका नहीं है।

इस काम के लिए स्नैपशॉट वाला यह व्यवहार उपयोगी है। पैरेंट व्यू का कोई असंबंधित हिस्सा अपडेट होने पर आंशिक रूप से संपादित नोट अचानक रीसेट नहीं होना चाहिए। फिर से खोलने पर सबसे हाल में स्वीकृत टेक्स्ट से नया ड्राफ़्ट बनता है।

शीट बंद करने और कीबोर्ड के व्यवहार को सोच-समझकर तय करें

प्रस्तुत एडिटर के भीतर @Environment(\.dismiss) पढ़ें, जैसा कि उदाहरण में किया गया है। Apple बताता है कि यह action उस environment पर लागू होता है जिसमें इसे घोषित किया गया है। इसे प्रस्तुत करने वाले root view में पढ़ने पर, शीट के बजाय उस view की window लक्ष्य बन सकती है। DismissAction देखें।

Cancel बटन बस शीट बंद करता है। ड्राफ़्ट मिट सकता है, क्योंकि उसने स्वीकृत मान को कभी बदला ही नहीं। Commit बटन पहले स्वीकृति closure चलाता है और फिर शीट बंद करता है। स्वीकृति को onDismiss में न ले जाएँ: वह callback शीट बंद होने की सूचना देता है, जो रद्द करने पर भी होता है।

नमूना Apple के दस्तावेज़ में दिए गए मानक cancel shortcut .cancelAction का उपयोग करके Escape को Cancel से जोड़ता है। Command-Return को Commit से जोड़ने के लिए यह स्पष्ट key equivalent और modifier तय करता है। ये ऐप्लिकेशन के इंटरफ़ेस के नियंत्रण वाले shortcut हैं। cancelAction और keyboardShortcut(_:modifiers:) देखें।

कई पंक्तियों के संपादन के लिए साधारण Return उपलब्ध रहता है, क्योंकि उदाहरण commit बटन को .defaultAction नहीं देता। Apple .defaultAction को बिना modifiers वाले Return के रूप में परिभाषित करता है। Command-Return इस बहु-पंक्ति इंटरफ़ेस के लिए संपादकीय चुनाव है; यह दावा नहीं कि हर Mac एडिटर में इसका उपयोग होना चाहिए। defaultAction देखें।

संबंधित इंटरफ़ेस में shortcut अलग-अलग रखें। Apple के दस्तावेज़ के अनुसार, जब कई नियंत्रणों का shortcut एक ही हो, तो resolution के दौरान सबसे पहले मिलने वाला मेल खाता नियंत्रण इस्तेमाल किया जाता है। एडिटर में insertion point होने पर shortcut जाँचें और साथ ही दिख रहे बटन क्लिक करके भी देखें। keyboardShortcut(_:modifiers:) देखें।

छोटे परीक्षण क्रम से स्वीकृति की सीमा जाँचें

चल रहे Mac ऐप में ये जाँचें करें। ये उस व्यवहार को परखती हैं जो ड्राफ़्ट एडिटर को live एडिटर से अलग करता है, और इसमें आसानी से छूट जाने वाला empty-string मामला भी शामिल है।

  1. Edit Note खोलें, दूसरी पंक्ति को हरे टाइलों को एक कतार में सजाएँ। से बदलें, और Cancel क्लिक करें। स्वीकृत नोट में दूसरी पंक्ति मूल जैसी रहनी चाहिए।
  2. एडिटर फिर खोलें। उसमें मूल स्वीकृत टेक्स्ट होना चाहिए, रद्द किया गया संशोधन नहीं।
  3. वही संशोधन करें और Commit क्लिक करें। स्वीकृत नोट में अब संशोधित पंक्ति दिखनी चाहिए।
  4. एडिटर फिर खोलें, एक line break जोड़ें और Command-Return दबाएँ। पूरा बहु-पंक्ति ड्राफ़्ट स्वीकृत टेक्स्ट बन जाना चाहिए।
  5. एडिटर फिर खोलें, बदलाव करें और संपादन के दौरान Escape जाँचें। पुष्टि करें कि रद्द करने पर स्वीकृत टेक्स्ट अपरिवर्तित रहता है।
  6. एडिटर फिर खोलें, पूरा टेक्स्ट हटाएँ और Commit क्लिक करें। पैरेंट को (Empty note) दिखाना चाहिए। फिर खोलने पर एडिटर खाली होना चाहिए।

इस उदाहरण में खाली टेक्स्ट मान्य है। कार्यान्वयन whitespace को trim नहीं करता, line break नहीं हटाता, और खाली string के लिए स्वीकृति अक्षम नहीं करता। अगर आपके अपने note type में सामग्री आवश्यक है, तो दिखने वाला validation rule जोड़ें और उसे commit action के साथ-साथ बटन की enabled state पर भी लागू करें।

hasChanges तुलना केवल status label नियंत्रित करती है। बिना बदलाव के भी Commit को enabled रखने पर उसी मान को फिर से assign करना हानिरहित है। यदि बाद में आप बदलाव न होने पर इसे अक्षम करें, तो Cancel उपलब्ध रखें ताकि शीट से बाहर निकलने का स्पष्ट तरीका हमेशा रहे।

जानें कि SwiftUI कब पर्याप्त है और AppKit कब मदद करता है

इस सादे टेक्स्ट वाले काम के लिए TextEditor आवश्यक संपादन सतह पहले से देता है। ड्राफ़्ट के बदलावों को स्वीकृत टेक्स्ट से अलग रखने के लिए न custom text layout की ज़रूरत है, न AppKit bridge की। यह अलगाव ऐप्लिकेशन के state design का हिस्सा है।

मौजूदा SwiftUI में attributed string वाला TextEditor initializer भी उपलब्ध है, जो macOS 26 से मिलता है। इसलिए rich text के लिए SwiftUI को बदलना अपने आप में आवश्यक नहीं होता। इसके लिए ड्राफ़्ट का type बदलना और नई deployment requirement का ध्यान रखना ज़रूरी है। Apple इस एडिटर के लिए formatting attributes के conversion की सीमाएँ भी बताता है। init(text:selection:) देखें।

अगर आपको AppKit-विशिष्ट संपादन व्यवहार या सीधे text-layout नियंत्रण की ज़रूरत है, तो समर्थित integration path NSViewRepresentable के ज़रिए होस्ट किया गया NSTextView है। Apple representable methods का वर्णन करता है, जिनसे AppKit view बनाया, अपडेट और हटाया जाता है; साथ ही coordinator delegated बदलावों को वापस SwiftUI तक पहुँचाता है। NSViewRepresentable देखें।

उस bridge में भी स्वीकृति का वही अनुबंध बनाए रखें: AppKit संपादन के बदलाव ड्राफ़्ट अपडेट करें, और केवल स्पष्ट commit action स्वीकृत model अपडेट करे। कार्यान्वयन में सावधानी के तौर पर, हर SwiftUI update पर पूरे text view की सामग्री बदलने से बचें; वास्तव में आए बदलावों को एडिटर से जाने वाले बदलावों से अलग पहचानें।

TextKit की एक और सीमा है: NSTextView आधुनिक layout को textLayoutManager के ज़रिए और legacy layout को layoutManager के ज़रिए उपलब्ध कराता है। Apple के दस्तावेज़ के अनुसार, macOS 12 या बाद में दूसरे विकल्प तक पहुँचने पर text view NSLayoutManager का उपयोग करने वाले compatibility mode में चला जाता है। Layout सुविधाएँ जोड़ते समय engine सोच-समझकर चुनें। TextKit और NSTextView देखें।

अंत में, नमूने का commit operation synchronous है और केवल memory में काम करता है। यदि आप इसे दस्तावेज़ लिखने के लिए बढ़ाते हैं, तो तय करें कि स्वीकृति का मतलब model अपडेट करना है या उस write का पूरा होना। Write पूरा होने के बाद बंद करने वाले डिज़ाइन में, write विफल होने पर शीट खुली रखें और फिर प्रयास करने के लिए ड्राफ़्ट सुरक्षित रखें। यदि संपादन के दौरान वही नोट ऐप्लिकेशन के किसी अन्य इंटरफ़ेस से बदल सकता है, तो उसे बदलने से पहले revision check जोड़ें।

मुख्य डिज़ाइन छोटा ही रहता है: हर संपादन सत्र के लिए एक ड्राफ़्ट बनाएँ, एडिटर को उस ड्राफ़्ट में बदलाव करने दें, और स्वीकृति के लिए एक स्पष्ट रास्ता रखें। यह सीमा तय होने पर, संपादन सतह बढ़ने के साथ भी रद्द करने का व्यवहार अनुमानित रहता है।

स्रोत