في تطبيق Mac يتضمن صفوفًا قابلة للتحرير من سطر واحد، عالج Return باستخدام onSubmit، وأدرج نموذجًا جديدًا أسفل الصف الحالي مباشرةً، وركّز على حقله باستخدام FocusState. امنح كل صف معرّفًا ثابتًا كي يحافظ الإدراج على الصلة بين النص والصف والتركيز.

ينشئ هذا البرنامج التعليمي محرر قوائم محليًا صغيرًا يتضمن عناصر اصطناعية مثل Item A. يؤدي الضغط على Return إلى إنشاء صف فارغ أسفل الحقل الجاري تحريره، مع الحفاظ على النص الحالي ونقل تركيز لوحة المفاتيح إلى الحقل الجديد. ولا يقسم النص عند موضع الإدراج.

ما عنصر التحكم النصي الذي يدعم هذا السلوك؟

استخدم TextField من سطر واحد عندما يمثل كل صف إدخالًا قصيرًا واحدًا. توثّق Apple صراحةً أن TextField يشغّل الإجراء onSubmit عندما يضغط المستخدم على مفتاح الإرجاع في لوحة مفاتيح فعلية أو افتراضية. كما توثّق Apple أن تعيين قيمة FocusState ينقل التركيز إلى العرض المرتبط بها. تتوفر واجهتا API في macOS 12 والإصدارات الأحدث. Apple: onSubmit(of:_:)، Apple: FocusState.

اختر عنصر التحكم وفقًا لما يحرره المستخدم:

مهمة التحرير نقطة البداية سلوك Return المطلوب تنفيذه
إدخال قصير واحد لكل صف TextField من سطر واحد إرسال، وإدراج صف أسفله، والتركيز على الصف الجديد
فقرة داخل كل صف TextEditor أو عرض نصي من AppKit تحديد كيفية اختلاف إدراج صف عن إدراج سطر جديد
مستند يتضمن أوامر نصية مخصصة NSTextView مضمّن في SwiftUI التعامل مع أمر النص المناسب وتنسيق تغييرات النموذج

تصف Apple TextEditor بأنه محرر متعدد الأسطر وقابل للتمرير. ولا يثبت توثيقها وجود عقد الإرسال عند الضغط على Return نفسه الموثّق لـ TextField. لذلك، يتطلب تحويل المثال إلى TextEditor إعادة النظر في معالجة الأوامر؛ إذ إن إرفاق المُعدِّل نفسه وحده ليس بديلًا مكافئًا موثّقًا. Apple: TextEditor، Apple: onSubmit(of:_:).

افصل هوية الصف عن موضعه

تخدم ثلاث قيم أغراضًا مختلفة: يحدد معرّف الصف العنصر، ويحدد فهرسه في المصفوفة موضع الإدراج، ويحدد المعرّف المركّز الحقل الذي ينبغي أن يستقبل إدخال لوحة المفاتيح.

تنشئ ForEach لدى Apple العروض انطلاقًا من بيانات لها معرّفات. استخدم نموذجًا يطبّق Identifiable ويخزّن معرّفًا عند إنشاء الصف. في هذا التصميم، يصبح المعرّف نفسه أيضًا قيمة التركيز للصف. وتتيح هذه الصلة للعنصر الاحتفاظ بهويته عند إدراج عنصر آخر قبله. Apple: ForEach.

تجنّب استخدام النص القابل للتحرير بوصفه المعرّف: فالصفّان الفارغان سيحملان الهوية نفسها، كما أن التحرير سيغيّر تلك الهوية. كذلك، لا تصلح مواضع المصفوفة هوياتٍ لهذه المهمة لأن المواضع تتغير. هذه نتائج مترتبة على نموذج البيانات المقترح، وليست متطلبات إضافية في SwiftUI.

يحتاج كل حقل إلى قيمة تركيز مميزة. تحذّر Apple من أن ربط قيمة التركيز نفسها بعدة عروض ينشئ ربطًا ملتبسًا للتركيز. يوفّر تخزين UUID لكل صف طريقة مباشرة للتمييز بين الحقول. Apple: FocusState.

أنشئ مثال macOS كاملًا

أنشئ مشروع تطبيق macOS في Xcode باستخدام SwiftUI، واضبط هدف النشر على macOS 12 أو إصدار أحدث. استبدل نقطة الدخول وعرض المحتوى اللذين أنشأهما المشروع بالملف الواحد التالي، واحذف تعريف التطبيق الأصلي كي لا يتضمن الهدف سوى نقطة دخول واحدة معلّمة بـ @main.

يستخدم المثال VStack عاديًا داخل عرض قابل للتمرير. ويسجل الصف الجديد بوصفه هدفًا مؤقتًا للتركيز، ثم يطلب التركيز عند ظهور هذا الحقل. هذه استراتيجية تنفيذ مستندة إلى واجهتي التركيز والظهور لدى Apple؛ لكن 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.

يعثر معالج الإدراج على الصف الذي أُرسل باستخدام معرّفه، ثم يدرج عند index + 1. في صف متوسط، يضع ذلك العنصر الجديد قبل العنصر الموجود التالي. أما في الصف الأخير، فيساوي الموضع endIndex الخاص بالمصفوفة، وهو موضع إدراج صالح يضيف العنصر إلى نهايتها. Apple: insert(_:at:).

يتحقق رد استدعاء ظهور الحقل الجديد مما إذا كان معرّفه يطابق الطلب المعلّق. ثم يتحقق المقطع المرسل مرة أخرى قبل التمرير وتعيين التركيز. تمنع هذه الفحوصات استهلاك الطلب بواسطة رد استدعاء ظهور غير ذي صلة. كما يتجاهل المثال عمليات الإرسال الإضافية ما دام طلب التركيز معلّقًا.

يؤجل الإرسال الإضافي العمل إلى ما بعد رد استدعاء الظهور؛ لكنه ليس حاجزًا موثقًا للرسم. اعتبر خيار الجدولة هذا أمرًا ينبغي التحقق منه على إصدارات macOS المدعومة لديك وفي تسلسل الحاويات الفعلي.

التمرير والتركيز عمليتان منفصلتان. يحدد scrollTo عنصرًا فرعيًا باستخدام معرّف عرضه، ثم يمرر نحوه؛ أما تعيين focusedRowID فيطلب تركيز لوحة المفاتيح. ويوفر .id(row.id) الصريح هدف التمرير. Apple: scrollTo(_:anchor:).

تحقق من النتيجة وشخّص المشكلات الشائعة

نفّذ تسلسل القبول هذا باستخدام عناصر العينة:

  1. انقر على Item A، واكتب نصًا إضافيًا، ثم اضغط على Return مرة واحدة.
  2. تأكد من ظهور حقل فارغ بين العنصرين الأصليين.
  3. اكتب Item C فورًا، وتأكد من ظهوره في الحقل الجديد.
  4. اضغط على Return في الحقل الأخير، وتأكد من ظهور صف جديد في النهاية.
  5. اضغط على Return في حقل فارغ، وتأكد من السماح بإضافة صف فارغ آخر.
  6. واصل حتى يصبح التمرير ضروريًا، ثم تأكد من ظهور الحقل المُدرج وإمكانية الكتابة فيه.

هذه نتائج متوقعة للتحقق منها محليًا. يسمح المثال عمدًا بإدخالات فارغة ويحافظ على السلسلة الحالية كاملة، حتى عندما يكون موضع الإدراج في وسطها. إذا كانت مهمتك تتطلب تقسيم تلك السلسلة إلى إدخالين، فستصبح معالجة التحديد ونطاق النص من المتطلبات الإضافية.

إذا ظهر صف لكن الكتابة ظلت في الحقل القديم، فتحقق من معرّف التركيز ورد استدعاء ظهور الحقل الجديد وما إذا كانت النافذة نشطة. وتحقق أيضًا مما إذا كان رد استدعاء آخر يعيد ضبط التركيز. تجنّب إعادة إنشاء معرّفات الصفوف أثناء حساب العرض.

إذا حدث الإدراج مرتين، فتحقق من معالجات الإرسال في مستويات أعلى من تسلسل العرض. تسمح Apple باستخدام onSubmit على عناصر التحكم الفردية والعروض الحاوية، وتوثّق submitScope بوصفه وسيلة لإيقاف انتشار الإرسال إلى المعالجات الأعلى. اجعل إجراء الإدراج مملوكًا لمعالج واحد. Apple: onSubmit(of:_:).

تغيير تسمية الإرسال لا ينفذ التنقل أو الإدراج. تعرّف Apple submitLabel على أنه تعيين تسمية الإرسال الدلالية. وتظل طفرة النموذج وتعيين التركيز بحاجة إلى شيفرتهما الخاصة. Apple: submitLabel(_:).

بالنسبة إلى مجموعة أكبر بكثير، أعد تقييم المكدس الذي ينشئ جميع العناصر فورًا. تشير Apple إلى أن حاويات مثل List وLazyVStack تستعلم عن عناصر ForEach عند الحاجة. لذلك قد يحتاج الحقل الخارج عن الشاشة إلى إظهاره قبل أن يتمكن رد استدعاء ظهوره من إكمال طلب التركيز. لا تستبدل الحاوية دون اختبار هذا التسلسل. Apple: ForEach.

متى ينبغي استخدام AppKit وTextKit بدلًا من ذلك؟

استخدم مثال SwiftUI للإدخالات القصيرة والمستقلة. عندما تتضمن الصفوف نصًا متعدد الأسطر أو تتطلب سلوكًا دقيقًا للأوامر النصية، فإن أحد البدائل المدعومة هو تضمين NSTextView خاص بك عبر NSViewRepresentable. توثّق Apple الواجهات القابلة للتمثيل بوصفها الجسر لإنشاء عروض AppKit وتحديثها داخل SwiftUI، مع مُنسّق يمرّر تفاعلات المفوّض. Apple: NSViewRepresentable.

لهذا البديل، احتفظ بنموذج معرّفات الصفوف نفسه ونفّذ التسلسل التالي:

  1. أنشئ عرض النص وعيّن مُنسّقًا بصفته المفوّض.
  2. مرّر تغييرات النص إلى نموذج الصف المقابل.
  3. نفّذ textView(_:doCommandBy:) وتعرّف على الأمر insertNewline(_:) وفقًا للسلوك الذي اخترته للمحرر.
  4. عند التعامل مع هذا الأمر بوصفه إدراجًا لصف، حافظ على النص الحالي، وأدرج الصف الجديد، وأعد true لاستهلاك الأمر. أعد false للأوامر التي لا يتعامل معها المفوّض.
  5. بعد إرفاق عرض النص الجديد بالنافذة الحاوية، اطلب التركيز عبر طريقة makeFirstResponder(_:) الخاصة بتلك النافذة.

توثّق Apple كلاً من أمر السطر الجديد وعقد المفوّض المنطقي: تعني true أن المفوّض تعامل مع الأمر، بينما تتيح false لعرض النص محاولة تنفيذه. يوفر هذا مسارًا صريحًا للتعامل مع الأوامر في محرر تملكه التطبيق. Apple: insertNewline(_:)، Apple: textView(_:doCommandBy:).

حافظ على تركيب النص النشط عند إضافة معالجة مخصصة للأوامر. يوفّر AppKit hasMarkedText() للإبلاغ عن النص المحدد بعلامة؛ استخدم هذه الحالة عند تحديد ما إذا كان ينبغي للمفوّض ترك الأمر لنظام النص. اختبر التركيب في المحرر الناتج بدلًا من افتراض أن كل تفاعل متعلق بـ Return ينبغي أن يدرج صفًا. Apple: hasMarkedText().

بالنسبة إلى التركيز، توجّه Apple صراحةً المتصلين إلى استخدام makeFirstResponder(_:) للنافذة الحاوية، بدلًا من استدعاء becomeFirstResponder() لعرض النص مباشرةً. كما أن الوصول إلى تخطيط TextKit له نتيجة مرتبطة بالإصدار: في macOS 12 والإصدارات الأحدث، يؤدي الوصول صراحةً إلى layoutManager لعرض النص إلى تحويله إلى وضع التوافق باستخدام NSLayoutManager. ولا يتطلب إدراج الصفوف ذلك الوصول. Apple: NSTextView.

لهذه المهمة ذات السطر الواحد، ابدأ بمثال SwiftUI الكامل وتحقق من تسلسل الإدراج والظهور والكتابة في تطبيق Mac. توفر الهوية الثابتة وموضع الإدراج الصريح وطلب التركيز المحدد السلوك الأساسي؛ وانتقل إلى عرض نص من AppKit عندما تتطلب مهمة التحرير أدواته للأوامر والتحديد.

المصادر