لإدراج اقتراح دون تطبيق قيمة الحقل، أبقِ الإكمال منفصلًا عن الإرسال. في macOS 15 أو أحدث، توفر SwiftUI اقتراحات نصية أصلية؛ استخدم submitScope() لمنع معالجات الإرسال في العناصر الحاوية، وزرًا صريحًا لتنفيذ الإجراء النهائي.
يبني هذا الدليل منتقيًا صغيرًا للتسميات داخل تطبيق Mac الخاص بك. يؤدي إدخال Bl إلى عرض تسميات اصطناعية مثل Blue Tile. يؤدي اختيار اقتراح إلى تحديث المسودة القابلة للتحرير؛ ويؤدي النقر على Apply Label إلى نسخ تلك المسودة إلى القيمة المطبقة. هذا التمييز مهم: منع إجراء في التطبيق واستهلاك أمر مفتاح Return متطلبان مختلفان.
اختر واجهة API التي تناسب التفاعل
يعرض مُعدِّل Apple textInputSuggestions(_:) خيارات لحقل نصي. يحتاج كل اقتراح قابل للتنفيذ إلى textInputCompletion(_:)، الذي يوفّر نص الاستبدال. واجهتا API موثقتان لـ macOS 15 والإصدارات الأحدث. قد يظهر اقتراح بلا مُعدِّل إكمال، لكنه لا يستجيب للنقرات. Apple: textInputSuggestions(_:)، Apple: textInputCompletion(_:)
بالنسبة إلى حقل تسمية قصير، يكون استبدال المسودة بأكملها مناسبًا. توثّق Apple هذا الاستبدال الكامل للحقل في وثائق الاقتراحات. لا تفترض أن واجهة API هذه توفر محرر رموز يعتمد موضع المؤشر أو معاينة لاحقة مضمنة؛ فذلك يتطلب تصميمًا أكثر تحديدًا للتحرير. Apple: textInputSuggestions(_:)
| متطلبك | نقطة بداية مناسبة |
|---|---|
| عرض تسميات كاملة على macOS 15 أو أحدث | اقتراحات النص الأصلية في SwiftUI |
| منع الحقل من استدعاء معالج إرسال في عنصر حاوٍ | submitScope() على الحقل |
| استخدام تخطيط مخصص للاقتراحات أو دعم هدف نشر أقدم | أزرار اقتراحات يديرها التطبيق وتحدّث ربط المسودة |
| جعل Return يقبل عنصرًا محددًا ويستهلك ذلك الأمر | عنصر AppKit مملوك للتطبيق مع معالجة للأوامر |
| معاينة لاحقة أو استبدال الكلمة عند موضع الإدراج | إكمال نص AppKit ونطاق إكمال صريح |
الصفّان الأخيران خياران معماريان مستنتجان من واجهات API الموثقة للأوامر والإكمال. وهما مفيدان عندما يتطلب التفاعل تحكمًا أكبر مما توفره سلسلة الاستبدال وحدّ الإرسال.
أنشئ منتقي تسميات كاملًا باستخدام SwiftUI
أنشئ مشروع تطبيق macOS في Xcode باستخدام SwiftUI، مع SDK يتضمن واجهات API الخاصة بـ macOS 15. عيّن هدف النشر إلى macOS 15 أو أحدث. استبدل تعريفَي التطبيق وعرض المحتوى اللذين أنشأهما المشروع بالملف المصدري الواحد التالي، مع الإبقاء على تعريف تطبيق واحد فقط باستخدام @main في الهدف.
يفصل المثال بين draft وappliedLabel. قاعدة المطابقة المحلية فيه بسيطة عمدًا: مطابقة البادئة مع مراعاة حالة الأحرف، وعدم عرض اقتراحات للنص الفارغ، وعدم عرض اقتراح عند التطابق التام. توثّق Swift أن hasPrefix(_:) يقارن بادئة السلسلة؛ أما قواعد المطابقة الأوسع فتُترك لاختيار التطبيق. Swift: Strings and Characters
import SwiftUI
@main
struct LabelCompletionApp: App {
var body: some Scene {
WindowGroup {
LabelCompletionView()
}
}
}
struct LabelCompletionView: View {
private let labels = [
"Blue Tile",
"Blue Tray",
"Green Tile",
"Round Tray"
]
@State private var draft = ""
@State private var appliedLabel = ""
@State private var applyCount = 0
@FocusState private var fieldIsFocused: Bool
private var matches: [String] {
guard !draft.isEmpty else { return [] }
return labels.filter {
$0.hasPrefix(draft) && $0 != draft
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a label")
.font(.headline)
TextField("Type Bl to see suggestions", text: $draft)
.textFieldStyle(.roundedBorder)
.focused($fieldIsFocused)
.textInputSuggestions {
ForEach(matches, id: \.self) { label in
Text(label)
.textInputCompletion(label)
}
}
.submitScope()
Button("Apply Label") {
appliedLabel = draft
applyCount += 1
}
.disabled(draft.isEmpty)
Text("Draft: \(draft)")
Text("Applied: \(appliedLabel)")
Text("Apply count: \(applyCount)")
}
.padding(24)
.frame(width: 420)
.onAppear {
fieldIsFocused = true
}
}
}
هذا مثال تطبيق كامل يتضمن تسمياته النموذجية وحالته. وقد جرت مراجعته بالاستناد إلى وثائق واجهات API التي جرى استرجاعها، لكن لم يُترجم أو يُشغّل هنا لعدم توفر سلسلة أدوات Swift وبيئة تشغيل واجهة macOS.
لماذا لا يؤدي اختيار اقتراح إلى تطبيق التسمية
يوفّر كل إكمال سلسلة نصية لحقل النص، الذي يشير ربطه إلى draft. الجزء الوحيد من الشيفرة الذي يعيّن appliedLabel أو يزيد applyCount هو إجراء زر Apply Label. لذلك لا يوجد في هذا المثال مسار ينقل تحديث المسودة إلى ذلك الإجراء النهائي.
يحتوي الحقل أيضًا على submitScope(). توثّق Apple هذا المُعدِّل بأنه يمنع وصول مُشغّلات الإرسال إلى مُعدِّلات الإرسال الأعلى في التسلسل الهرمي للعروض. وهذا مهم عند تضمين المكوّن لاحقًا في نموذج أكبر يحتوي على onSubmit في عنصر حاوٍ. Apple: submitScope(_:)
يأتي التركيز الأولي من @FocusState. توثّق Apple أن تعيين القيمة المرتبطة به برمجيًا ينقل التركيز إلى العرض المرتبط. التركيز هنا تسهيل للتحرير؛ ولا يتحكم في تطبيق التسمية. Apple: FocusState
ما يسمح به المثال عمدًا
توفر قائمة التسميات اقتراحات بدلًا من حصر الإدخال بالقيم الصالحة. يظل زر Apply Label مفعّلًا عند كتابة Square Tile. إذا كانت واجهتك تسمح بالتسميات المدرجة فقط، فغيّر قاعدة التحقق صراحةً، مثلًا بتفعيل الزر فقط عندما تطابق المسودة أحد عناصر الفهرس. لا تستبدل بصمت مسودة غير مطابقة بأول اقتراح.
يستبدل المثال التسمية بأكملها أيضًا. إذا احتوت المسودة على عدة كلمات، فينبغي فهم اختيار الإكمال على أنه اختيار القيمة النهائية للحقل. أما محرر الرموز الذي يجب أن يحافظ على النص المحيط، فيحتاج إلى استبدال يراعي التحديد.
افهم المقصود بعبارة «دون إرسال»
تذكر وثائق SwiftUI الخاصة بـ onSubmit(of:_:) أن حقل النص يطلق إجراء الإرسال عندما يضغط المستخدم على Return. يمكن إرفاق إجراءات الإرسال بعرض فردي أو بتسلسل هرمي أكبر. افحص الموضعين عندما يبدو أن اختيار الإكمال التلقائي يطلق إجراءً غير متوقع في التطبيق. Apple: onSubmit(of:_:)
ينشئ submitScope() حدًا يمنع معالجات العناصر الحاوية من الاستجابة. لكنه لا يوثّق ضمانًا بعدم وقوع حدث إرسال داخلي، أو استمرار التحرير، أو استهلاك كل أمر من أوامر لوحة المفاتيح. وبالمثل، توثّق مُعدِّلات الإكمال لدى Apple استبدال النص، لا تعطيلًا عامًا لسلوك مفتاح Return. ضع هذه الحدود في الحسبان عند اختيار التفاعل. Apple: submitScope(_:)، Apple: textInputCompletion(_:)
في مكوّن موجود، افحص هذه المسارات كلًا على حدة:
- أزل أي إجراء نهائي للتطبيق من
onSubmitالخاص بالحقل إذا كان ينبغي ألا يؤدي Return مطلقًا إلى تطبيق قيمته. - ضع
submitScope()على الحقل لمنع إجراء الإرسال في عنصر حاوٍ من الاستجابة. - افحص اختصارات لوحة المفاتيح للأزرار على حدة، وخصوصًا أي إجراء افتراضي صريح.
- افحص أي مراقب للتغييرات أو مُعيِّن للنموذج ينفذ الإجراء النهائي كلما تغيرت المسودة.
إغلاق onSubmit محلي فارغ ليس البديل الموثق لحد الإرسال. توفر Apple تحديدًا submitScope() لإيقاف انتشار الحدث. إذا كان ينبغي للتطبيق الإرسال عند الضغط على Return فقط عندما لا يكون هناك إكمال نشط، فسيحتاج هذا السلوك الشرطي للوحة المفاتيح إلى حالة إضافية ومعالجة للأوامر. Apple: onSubmit(of:_:)
استخدم AppKit عندما ينبغي قبول الاقتراح وإيقاف Return
بالنسبة إلى NSTextField يملكه التطبيق، توفر AppKit واجهة control(_:textView:doCommandBy:). وسيطة عرض النص فيها هي محرر الحقل الخاص بعنصر التحكم. يمكن للمفوّض معالجة أمر مرتبط وإرجاع true؛ أما إرجاع false فيترك الأمر دون معالجة من ذلك المفوّض. وهذه هي نقطة التوسعة الموثقة لتخصيص الأوامر في حقل نصي. Apple: control(_:textView:doCommandBy:)
يمكن لوحدة تحكم مخصصة للإكمال استخدام نقطة التوسعة هذه بالتسلسل التالي:
- تتبّع ما إذا كانت قائمة الاقتراحات مفتوحة، وأي عنصر محدد فيها.
- عندما يصل أمر Return مع وجود عنصر محدد صالح، أدرج ذلك العنصر في المسودة.
- أغلق قائمة الاقتراحات وزامن نموذج المسودة في التطبيق.
- أرجع
trueلأن وحدة التحكم عالجت الأمر. - أرجع
falseللأوامر التي لا تعالجها وحدة التحكم.
هذا التسلسل تصميم للتنفيذ مستنتج من عقد المفوّض، وليس سياسة إكمال تلقائي مضمنة. حدّد على نحو منفصل ما الذي ينبغي أن يفعله Return عندما تكون القائمة مغلقة. وإذا كنت تملك NSTextView مستقلًا بدلًا من ذلك، فنقطة الربط المناظرة هي textView(_:doCommandBy:)؛ وتذكر Apple أن إرجاع true يمنع عرض النص من تنفيذ ذلك الأمر بنفسه. Apple: textView(_:doCommandBy:)
أما لإكمال النص نفسه، فتدعم NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) الاستبدال وتغييرات التحديد. عند استخدام isFinal: false، تحدد اللاحقة المقترحة؛ وعند استخدام isFinal: true، تطوي التحديد بعد الكلمة المكتملة. تصف هذه القيمة حالة الإكمال، لا إرسال التطبيق. وتحدد وسيطة movement كيفية حدوث التنقل في الإكمال أو اختياره. Apple: insertCompletion(_:forPartialWordRange:movement:isFinal:)
حافظ على نطاق النص الصحيح
تحدد rangeForUserCompletion في AppKit الكلمة الجزئية من بدايتها حتى موضع الإدراج. استخدم نطاقًا يلائم الاستبدال المقصود، وأعِد حسابه بعد التعديلات أو تحريك موضع المؤشر. لا تُعِد استخدام نطاق محفوظ سابقًا بلا تحقق بعد تغيّر النص. Apple: rangeForUserCompletion
عند التحويل بين نطاقات النص، لا تستبدل طول NSString بعدد محارف Swift. توضح وثائق Swift أن String تحسب عناقيد المحارف الموسعة، بينما يحسب طول NSString وحدات شيفرة UTF-16. وقد تختلف القيمتان. يتجنب استبدال الربط للحقل بأكمله في مثال SwiftUI هذا التحويل بين النطاقات تمامًا. Swift: Strings and Characters)
تحقق من الإدراج والتطبيق كلٌّ على حدة
استخدم المسودة الظاهرة والقيمة المطبقة والعداد للتحقق من النتيجة على كل إصدار مدعوم من macOS. ما يلي فحوصات متوقعة للمثال، وليست نتائج اختبارات مُبلّغًا عنها.
| الإجراء | النتيجة المتوقعة التي يمكن ملاحظتها |
|---|---|
اكتب Bl |
تصبح المسودة Bl؛ وتُعرض التسميات المطابقة |
انقر على Blue Tile |
تصبح المسودة Blue Tile؛ وتظل القيمة المطبقة والعداد دون تغيير |
| اضغط على Return أثناء التحرير | لا يطبق التطبيق المسودة |
انقر على Apply Label |
تُنسخ المسودة إلى القيمة المطبقة؛ ويزداد العداد مرة واحدة |
| حرّر المسودة بعد ذلك | تظل القيمة المطبقة السابقة ظاهرة |
| امسح المسودة | تصبح الاقتراحات فارغة ويتعطل Apply Label |
اكتب bl |
لا توجد مطابقات وفق قاعدة المثال التي تراعي حالة الأحرف |
جرّب أيضًا تحديد اقتراح بلوحة المفاتيح في قائمة الاقتراحات الأصلية، وكرّر الفحوصات بعد تضمين الحقل في العرض الأب الفعلي. تشير Apple إلى أن أحداثًا مثل تحريك نافذة macOS قد تؤدي إلى إغلاق واجهة الاقتراحات الأصلية؛ لذا ينبغي أن تظل المسودة قابلة للاستخدام دون تلك القائمة. Apple: textInputSuggestions(_:)
إذا كان هدف النشر لديك أقدم من macOS 15، فوفّر عرضًا أصليًا محكومًا بتوافر واجهة API مع بديل، أو استخدم أزرار اقتراحات يديرها التطبيق في جميع الحالات. يمكن لزر البديل تعيين تسميته مباشرةً إلى المسودة وترك الإجراء النهائي لزر Apply Label. يوفّر ذلك الفصل نفسه بين المسودة والتطبيق، بينما تظل معالجة التنقل بلوحة المفاتيح والعرض من مسؤولية مكوّنك.
في مهمة منتقي التسميات هذه، الحد الفاصل واضح ويمكن ملاحظته: قبول إكمال يغيّر المسودة القابلة للتحرير، ولا يغيّر التسمية المطبقة إلا الإجراء النهائي الصريح. استخدم اقتراحات SwiftUI الأصلية ونطاق الإرسال لتحقيق هذا الفصل؛ وانتقل إلى معالجة أوامر AppKit عندما يكون استهلاك Return نفسه جزءًا من المتطلب.
المصادر
- textInputSuggestions(_:) — Apple
- textInputCompletion(_:) — Apple
- submitScope(_:) — Apple
- onSubmit(of:_:) — Apple
- FocusState — Apple
- control(_:textView:doCommandBy:) — Apple
- textView(_:doCommandBy:) — Apple
- insertCompletion(_:forPartialWordRange:movement:isFinal:) — Apple
- rangeForUserCompletion — Apple
- Strings and Characters — Swift.org