एक-पंक्ति वाली संपादन योग्य पंक्तियों वाले Mac ऐप में, Return को onSubmit से संभालें, मौजूदा पंक्ति के ठीक नीचे नया मॉडल जोड़ें और FocusState का उपयोग करके उसके फ़ील्ड पर फ़ोकस करें। हर पंक्ति को एक स्थिर ID दें, ताकि जोड़ने पर टेक्स्ट, पंक्ति और फ़ोकस के बीच का संबंध बना रहे।
इस ट्यूटोरियल में Item A जैसी कृत्रिम प्रविष्टियों वाली एक छोटी, स्थानीय सूची संपादक बनाई गई है। Return दबाने पर संपादित किए जा रहे फ़ील्ड के नीचे एक खाली पंक्ति जुड़ती है, मौजूदा टेक्स्ट बना रहता है और कीबोर्ड फ़ोकस नए फ़ील्ड पर चला जाता है। यह टेक्स्ट को कर्सर की स्थिति पर विभाजित नहीं करता।
इस व्यवहार के लिए कौन-सा टेक्स्ट कंट्रोल सही है?
जब हर पंक्ति में एक छोटी प्रविष्टि हो, तो एक-पंक्ति वाला TextField इस्तेमाल करें। Apple स्पष्ट रूप से बताता है कि उपयोगकर्ता हार्डवेयर या सॉफ़्टवेयर Return कुंजी दबाता है, तो TextField अपना onSubmit ऐक्शन ट्रिगर करता है। Apple यह भी बताता है कि FocusState को कोई मान देने से संबंधित व्यू पर फ़ोकस चला जाता है। ये दोनों API macOS 12 और उसके बाद के संस्करणों में उपलब्ध हैं। Apple: onSubmit(of:_:), Apple: FocusState।
उपयोगकर्ता जिस चीज़ को संपादित कर रहा है, उसके अनुसार कंट्रोल चुनें:
| संपादन का काम | शुरुआत यहाँ से करें | Return का व्यवहार लागू करें |
|---|---|---|
| हर पंक्ति में एक छोटी प्रविष्टि | एक-पंक्ति वाला TextField |
सबमिट करें, नीचे जोड़ें और नई पंक्ति पर फ़ोकस करें |
| हर पंक्ति के भीतर एक पैराग्राफ़ | TextEditor या AppKit टेक्स्ट व्यू |
तय करें कि पंक्ति जोड़ना, नई पंक्ति जोड़ने से कैसे अलग है |
| कस्टम टेक्स्ट कमांड वाला दस्तावेज़ | SwiftUI में एम्बेड किया गया NSTextView |
संबंधित टेक्स्ट कमांड संभालें और मॉडल में बदलाव का तालमेल करें |
Apple, TextEditor को बहुपंक्ति और स्क्रॉल करने योग्य संपादक बताता है। उसका दस्तावेज़ TextField के लिए बताए गए Return दबाने पर सबमिट होने वाले अनुबंध की पुष्टि नहीं करता। इसलिए उदाहरण को TextEditor में बदलने पर कमांड हैंडलिंग पर फिर से विचार करना होगा; केवल वही मॉडिफ़ायर लगाने से दस्तावेज़ित समकक्ष व्यवहार नहीं मिलता। Apple: TextEditor, Apple: onSubmit(of:_:)।
पंक्ति की पहचान को उसकी स्थिति से अलग रखें
तीन मान अलग-अलग काम करते हैं: पंक्ति का ID प्रविष्टि की पहचान करता है, उसका ऐरे इंडेक्स बताता है कि कहाँ जोड़ना है, और फ़ोकस किया गया ID उस फ़ील्ड की पहचान करता है जिसे कीबोर्ड इनपुट मिलना चाहिए।
Apple का ForEach पहचाने जा सकने वाले डेटा से व्यू बनाता है। ऐसा Identifiable मॉडल इस्तेमाल करें जिसका ID पंक्ति बनाते समय संग्रहित हो। इस डिज़ाइन में वही ID पंक्ति का फ़ोकस मान भी बनता है। इससे ऊपर कोई दूसरी प्रविष्टि जुड़ने पर भी प्रविष्टि की पहचान बनी रहती है। Apple: ForEach।
संपादन योग्य टेक्स्ट को ID न बनाएँ: दो खाली पंक्तियों की पहचान एक जैसी होगी और संपादन से वह पहचान बदल जाएगी। इस जोड़ने वाले काम के लिए ऐरे की स्थितियाँ भी खराब पहचान हैं, क्योंकि स्थितियाँ खिसकती हैं। ये चुने गए डेटा मॉडल के परिणाम हैं, SwiftUI की अतिरिक्त आवश्यकताएँ नहीं।
हर फ़ील्ड का फ़ोकस मान अलग होना चाहिए। Apple चेतावनी देता है कि एक ही फ़ोकस मान को कई व्यू से जोड़ने पर फ़ोकस बाइंडिंग अस्पष्ट हो जाती है। हर पंक्ति के लिए संग्रहित UUID फ़ील्डों को अलग पहचान देने का सीधा तरीका है। Apple: FocusState।
macOS का पूरा उदाहरण बनाएँ
Xcode में SwiftUI का उपयोग करके macOS App प्रोजेक्ट बनाएँ और उसका डिप्लॉयमेंट टारगेट macOS 12 या उसके बाद का संस्करण रखें। जेनरेट किए गए ऐप एंट्री पॉइंट और कंटेंट व्यू को नीचे दी गई एक फ़ाइल से बदलें। मूल ऐप घोषणा हटा दें, ताकि टारगेट में केवल एक @main एंट्री पॉइंट रहे।
इस उदाहरण में स्क्रॉल व्यू के भीतर एक साधारण VStack है। यह नई पंक्ति को लंबित फ़ोकस लक्ष्य के रूप में दर्ज करता है, फिर उस फ़ील्ड के दिखाई देने पर फ़ोकस का अनुरोध करता है। यह Apple के फ़ोकस और अपीयरेंस API पर आधारित एक कार्यान्वयन रणनीति है; Apple यह दस्तावेज़ित नहीं करता कि अपीयरेंस कॉलबैक हमेशा फ़ोकस के लिए तैयार होने की गारंटी देते हैं। onAppear का सटीक समय व्यू पर निर्भर करता है। Apple: onAppear(perform:)।
import SwiftUI
import Foundation
import Dispatch
private struct EditableRow: Identifiable {
let id = UUID()
var text: String
}
@main
struct RowEntryDemoApp: App {
var body: some Scene {
WindowGroup {
RowEditor()
}
}
}
private struct RowEditor: View {
@State private var rows = [
EditableRow(text: "Item A"),
EditableRow(text: "Item B")
]
@State private var pendingFocusID: UUID?
@FocusState private var focusedRowID: UUID?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Local row editor")
.font(.headline)
Text("Press Return in a field to insert a row below it.")
.font(.subheadline)
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 10) {
ForEach($rows) { $row in
TextField("New item", text: $row.text)
.textFieldStyle(.roundedBorder)
.focused($focusedRowID, equals: row.id)
.onSubmit {
insertRow(after: row.id)
}
.id(row.id)
.onAppear {
focusIfPending(row.id) {
proxy.scrollTo(row.id, anchor: .center)
}
}
}
}
.padding(4)
}
}
}
.padding(20)
.frame(minWidth: 420, minHeight: 280)
}
private func insertRow(after id: UUID) {
guard pendingFocusID == nil,
let index = rows.firstIndex(where: { $0.id == id })
else { return }
let newRow = EditableRow(text: "")
pendingFocusID = newRow.id
rows.insert(newRow, at: index + 1)
}
private func focusIfPending(
_ id: UUID,
scroll: @escaping () -> Void
) {
guard pendingFocusID == id else { return }
DispatchQueue.main.async {
guard pendingFocusID == id,
rows.contains(where: { $0.id == id })
else { return }
scroll()
focusedRowID = id
pendingFocusID = nil
}
}
}
मॉडल में सारा नमूना डेटा है और फ़ाइल में संदर्भित हर टाइप और फ़ंक्शन परिभाषित है। शोध परिवेश में Swift कंपाइलर और macOS रनटाइम उपलब्ध नहीं थे, इसलिए इस प्रोग्राम की जाँच तो की गई, लेकिन इसे यहाँ कंपाइल या चलाया नहीं गया।
जोड़ने और फ़ोकस करने का तालमेल कैसे काम करता है
फ़ील्ड की स्ट्रिंग बाइंडिंग, उपयोगकर्ता के संपादन के साथ संबंधित पंक्ति को अपडेट करती है। इसलिए सबमिट हैंडलर मौजूदा प्रविष्टि को बदले या साफ़ किए बिना नई पंक्ति जोड़ सकता है। Apple बताता है कि स्ट्रिंग-बाउंड टेक्स्ट फ़ील्ड लगातार अपडेट होते हैं। Apple: TextField।
इन्सर्शन हैंडलर सबमिट करने वाली पंक्ति को ID से ढूँढ़ता है और index + 1 पर नई पंक्ति जोड़ता है। बीच की पंक्ति के लिए इससे नई प्रविष्टि अगली मौजूदा प्रविष्टि से पहले आती है। आख़िरी पंक्ति के लिए यह स्थान ऐरे के endIndex के बराबर होता है, जो जोड़ने की वैध स्थिति है और तत्व को अंत में जोड़ता है। Apple: insert(_:at:)।
नई फ़ील्ड का अपीयरेंस कॉलबैक जाँचता है कि उसका ID लंबित अनुरोध से मेल खाता है या नहीं। डिस्पैच किया गया ब्लॉक स्क्रॉल करने और फ़ोकस देने से पहले फिर जाँचता है। इन जाँचों से कोई असंबंधित अपीयरेंस कॉलबैक अनुरोध को पूरा नहीं कर सकता। फ़ोकस अनुरोध लंबित होने पर उदाहरण अतिरिक्त सबमिशन को भी अनदेखा करता है।
अपीयरेंस कॉलबैक के भीतर से काम को आगे के लिए टालने हेतु अतिरिक्त डिस्पैच किया गया है; यह रेंडरिंग पूरी होने की दस्तावेज़ित गारंटी नहीं है। इस शेड्यूलिंग विकल्प को अपने समर्थित macOS संस्करणों और वास्तविक कंटेनर पदानुक्रम पर जाँचें।
स्क्रॉल करना और फ़ोकस देना अलग-अलग काम हैं। scrollTo व्यू आइडेंटिफ़ायर से किसी चाइल्ड को ढूँढ़ता है और उसकी ओर स्क्रॉल करता है; focusedRowID को मान देना कीबोर्ड फ़ोकस का अनुरोध करता है। स्पष्ट .id(row.id) स्क्रॉल करने का लक्ष्य देता है। Apple: scrollTo(_:anchor:)।
नतीजा जाँचें और आम समस्याओं का पता लगाएँ
नमूना प्रविष्टियों के साथ यह स्वीकृति क्रम अपनाएँ:
Item Aपर क्लिक करें, थोड़ा टेक्स्ट लिखें और एक बारReturnदबाएँ।- पुष्टि करें कि मूल दो प्रविष्टियों के बीच एक खाली फ़ील्ड दिखा है।
- तुरंत
Item Cलिखें और पुष्टि करें कि वह नई फ़ील्ड में आया है। - आख़िरी फ़ील्ड में
Returnदबाएँ और पुष्टि करें कि अंत में नई पंक्ति जुड़ी है। - खाली फ़ील्ड में
Returnदबाएँ और पुष्टि करें कि एक और खाली पंक्ति जोड़ी जा सकती है। - स्क्रॉल करना ज़रूरी होने तक आगे बढ़ें, फिर पुष्टि करें कि जोड़ी गई फ़ील्ड दिखाई देती है और उसमें टाइप किया जा सकता है।
ये वे अपेक्षित नतीजे हैं जिन्हें स्थानीय रूप से जाँचना चाहिए। उदाहरण खाली प्रविष्टियों की अनुमति देता है और कर्सर बीच में होने पर भी मौजूदा पूरी स्ट्रिंग को जस का तस रखता है। अगर आपके काम में उस स्ट्रिंग को दो प्रविष्टियों में बाँटना ज़रूरी है, तो चयन और टेक्स्ट रेंज संभालना भी आवश्यक होगा।
अगर पंक्ति दिखती है, लेकिन टाइप करने पर पुरानी फ़ील्ड में ही टेक्स्ट आता है, तो फ़ोकस किए गए ID, नई फ़ील्ड के अपीयरेंस कॉलबैक और विंडो सक्रिय है या नहीं, यह जाँचें। यह भी जाँचें कि कोई दूसरा कॉलबैक फ़ोकस रीसेट तो नहीं कर रहा। व्यू की गणना करते समय पंक्ति ID फिर से बनाने से बचें।
अगर दो बार पंक्ति जुड़ती है, तो व्यू पदानुक्रम में ऊपर के सबमिट हैंडलर देखें। Apple किसी कंट्रोल और उसे समाहित करने वाले व्यू, दोनों पर onSubmit की अनुमति देता है और बताता है कि submitScope से सबमिशन को ऊपर के हैंडलरों तक पहुँचने से रोका जा सकता है। जोड़ने की कार्रवाई केवल एक हैंडलर के ज़िम्मे रखें। Apple: onSubmit(of:_:)।
सबमिट लेबल बदलने से नेविगेशन या पंक्ति जोड़ने की सुविधा लागू नहीं होती। Apple, submitLabel को सबमिशन का अर्थपूर्ण लेबल सेट करने के रूप में परिभाषित करता है। मॉडल में बदलाव और फ़ोकस असाइन करने के लिए अब भी अलग कोड चाहिए। Apple: submitLabel(_:)।
बहुत बड़े संग्रह के लिए पहले से सभी व्यू बनाने वाले स्टैक पर फिर से विचार करें। Apple बताता है कि List और LazyVStack जैसे कंटेनर ForEach के तत्वों को लेज़िली क्वेरी करते हैं। इसलिए ऑफ़स्क्रीन फ़ील्ड का अपीयरेंस कॉलबैक फ़ोकस अनुरोध पूरा करने से पहले उसे व्यू में लाना पड़ सकता है। उस क्रम को जाँचे बिना कंटेनर न बदलें। Apple: ForEach।
AppKit और TextKit का उपयोग कब करना चाहिए?
छोटी, स्वतंत्र प्रविष्टियों के लिए SwiftUI का उदाहरण इस्तेमाल करें। जब पंक्तियों में बहुपंक्ति टेक्स्ट हो या टेक्स्ट कमांड के व्यवहार पर सटीक नियंत्रण चाहिए, तो अपना NSTextView, NSViewRepresentable के ज़रिए एम्बेड करना एक समर्थित विकल्प है। Apple, representable को SwiftUI के भीतर AppKit व्यू बनाने और अपडेट करने का पुल बताता है, जिसमें कोऑर्डिनेटर डेलिगेट इंटरैक्शन को आगे भेजता है। Apple: NSViewRepresentable।
इस विकल्प के लिए वही पंक्ति-ID मॉडल रखें और यह क्रम लागू करें:
- टेक्स्ट व्यू बनाएँ और उसके डेलिगेट के रूप में कोऑर्डिनेटर असाइन करें।
- टेक्स्ट में हुए बदलाव संबंधित पंक्ति के मॉडल तक पहुँचाएँ।
textView(_:doCommandBy:)लागू करें और संपादक के चुने हुए व्यवहार के अनुसारinsertNewline(_:)कमांड पहचानें।- कमांड को पंक्ति जोड़ने के रूप में संभालते समय मौजूदा टेक्स्ट बनाए रखें, नई पंक्ति जोड़ें और कमांड को संभालने के लिए
trueलौटाएँ। जिन कमांड को डेलिगेट संभालता नहीं है, उनके लिएfalseलौटाएँ। - नया टेक्स्ट व्यू अपने कंटेनिंग विंडो से जुड़ने के बाद, उस विंडो के
makeFirstResponder(_:)मेथड से फ़ोकस का अनुरोध करें।
Apple न्यूलाइन कमांड और डेलिगेट के Boolean अनुबंध, दोनों का दस्तावेज़ देता है: true का अर्थ है कि डेलिगेट ने कमांड संभाल ली; false टेक्स्ट व्यू को कमांड आज़माने देता है। इससे ऐप के अपने संपादक के लिए कमांड संभालने का स्पष्ट रास्ता मिलता है। Apple: insertNewline(_:), Apple: textView(_:doCommandBy:)।
कस्टम कमांड हैंडलिंग जोड़ते समय सक्रिय टेक्स्ट कंपोज़िशन को बनाए रखें। AppKit, चिह्नित टेक्स्ट बताने के लिए hasMarkedText() उपलब्ध कराता है; डेलिगेट को कमांड टेक्स्ट सिस्टम पर छोड़नी चाहिए या नहीं, यह तय करते समय इस स्थिति का उपयोग करें। नतीजे वाले संपादक में कंपोज़िशन जाँचें और यह न मानें कि Return से जुड़ी हर क्रिया को पंक्ति जोड़नी चाहिए। Apple: hasMarkedText()।
फ़ोकस के लिए Apple स्पष्ट निर्देश देता है कि टेक्स्ट व्यू के becomeFirstResponder() को सीधे कॉल करने के बजाय कंटेनिंग विंडो का makeFirstResponder(_:) इस्तेमाल करें। TextKit लेआउट को ऐक्सेस करने का संस्करण-विशिष्ट परिणाम भी है: macOS 12 और उसके बाद के संस्करणों में, टेक्स्ट व्यू के layoutManager को स्पष्ट रूप से ऐक्सेस करने पर वह NSLayoutManager का उपयोग करने वाले संगतता मोड में चला जाता है। पंक्ति जोड़ने के लिए इस ऐक्सेस की ज़रूरत नहीं है। Apple: NSTextView।
एक-पंक्ति वाले काम के लिए, पूरे SwiftUI उदाहरण से शुरुआत करें और अपने Mac ऐप में पंक्ति जोड़ने, उसे दिखाने और उसमें टाइप करने का क्रम जाँचें। स्थिर पहचान, जोड़ने की स्पष्ट स्थिति और लक्षित फ़ोकस अनुरोध बुनियादी व्यवहार देते हैं; जब संपादन के काम के लिए कमांड और चयन कंट्रोल चाहिए हों, तब AppKit टेक्स्ट व्यू पर जाएँ।
स्रोत
- onSubmit(of:_:) — Apple
- FocusState — Apple
- TextField — Apple
- TextEditor — Apple
- ForEach — Apple
- onAppear(perform:) — Apple
- insert(_:at:) — Apple
- scrollTo(_:anchor:) — Apple
- submitLabel(_:) — Apple
- NSViewRepresentable — Apple
- insertNewline(_:) — Apple
- textView(_:doCommandBy:) — Apple
- hasMarkedText() — Apple
- NSTextView — Apple