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

يتناول هذا الدليل حقلاً رقمياً داخل تطبيق macOS تطوّره: إدخال Tile count اصطناعي، ومسحه، ثم إدخال رقم آخر. الهدف هو الاحتفاظ بقيمة فارغة ذات معنى مع السماح بالتحرير التالي.

لماذا أصبحت القيمة الرقمية الاختيارية nil؟

توفر Apple مُهيّئ SwiftUI مخصصاً للقيم الاختيارية: TextField(value:format:prompt:label:). والسلوك الموثّق له هو تعيين الارتباط إلى nil عندما يتعذر على نمط التنسيق تحليل الإدخال. لذا فإن فشل التحويل حالة متوقعة في واجهة API هذه. ولا تصف Apple هذه الحالة بأنها تعطل الحقل. يتطلب مُهيّئ نمط التنسيق الاختياري هذا macOS 12 أو إصداراً أحدث. مُهيّئ TextField ذي القيمة الاختيارية من Apple

تحقّق من المُهيّئ الذي يستخدمه تطبيقك فعلياً. فللمُهيّئ الأقدم formatter: سلوك مختلف موثّق عند الفشل: إذا فشل التحويل، فإنه يترك القيمة المرتبطة دون تغيير. لذا فإن استبدال formatter: بـ format: يغيّر هذا السلوك المتفق عليه؛ تعامل مع ذلك على أنه تغيير مقصود في السلوك، لا إصلاح عام. مُهيّئ TextField المعتمد على المنسّق من Apple

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

شخّص سبب الإخفاق قبل تغيير الحقل

اتبع التسلسل التالي في نافذة تطبيقك. فهو يميّز مشكلة التحويل من مشكلة التفاعل دون افتراض وجود عيب محدد في تطبيق macOS.

  1. تحقّق مما إذا كان النص يتغير فعلاً. انقر الحقل، وحدد محتواه، واكتب 24. إذا تغيرت الأحرف ولم يتغير النموذج الرقمي، فتحقّق من التحويل والتعامل مع تثبيت القيمة.
  2. تحقّق من شروط التفاعل. ابحث في الحقل والحاويات التي تضمّه عن شروط مثل .disabled(tileCount == nil). فهذا الشرط يمنع التعديل اللازم للتعافي من قيمة فارغة.
  3. تحقّق مما إذا كان الحقل لا يزال موجوداً. إذا كان العرض ينشئ المحرر داخل فرع if let فقط، فقد يؤدي تعيين القيمة إلى nil إلى إزالة المحرر. أبقِ المسودة والمحرر متاحين بمعزل عن الرقم الاختياري.
  4. تحقّق من التركيز بشكل منفصل. إذا كان الحقل يقبل النقرة ثم يعمل الإدخال، فأعِد التركيز عبر واجهة API العامة المناسبة.
  5. بسّط المثال. أزل مؤقتاً تحويلات الارتباط المخصصة ومزامنة النموذج. أعد إنتاج الانتقال من 12 إلى فارغ ثم إلى 24 في عرض صغير.

تعطيل عنصر سلف مهم: توثّق Apple أن .disabled(true) على الحاوية يتغلب على .disabled(false) على عنصر فرعي. أصلح شرط العنصر السلف بدلاً من تكرار تمكين الحقل وحده. توثيق Apple للدالة disabled(_:)

افحص أيضاً أدوات تعيين الارتباط المخصصة. فالأداة التي تعود مبكراً كلما كان الرقم الحالي nil لا تستطيع قبول رقم بديل. وبالمثل، فإن تحويل كل فشل في التحويل إلى 0 يفقد التمييز بين غياب القيمة والصفر الفعلي. هذه قرارات تتعلق بمنطق التطبيق؛ ولا يمكن لتغيير التركيز إصلاحها.

احتفظ بمسودة قابلة للتحرير وثبّت الرقم صراحةً

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

يستخدم المثال الكامل أدناه السياسة التالية: الإدخال الفارغ يعني nil؛ والعدد الصحيح الصالح ذو الأساس العشري يستبدل الرقم المقبول؛ أما الإدخال غير الصالح فيبقى ظاهراً ويترك الرقم المقبول دون تغيير. يفرغ زر Clear الحالتين ويطلب التركيز لكي يمكن إدخال رقم آخر.

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

import SwiftUI

@main
struct NumericEditingDemoApp: App {
    var body: some Scene {
        WindowGroup {
            NumericEditingView()
        }
    }
}

struct NumericEditingView: View {
    @State private var tileCount: Int? = 12
    @State private var draft = "12"
    @State private var message: String? = nil
    @FocusState private var fieldIsFocused: Bool

    private var acceptedDescription: String {
        if let value = tileCount {
            return String(value)
        }
        return "None"
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            TextField("Tile count", text: $draft)
                .textFieldStyle(.roundedBorder)
                .focused($fieldIsFocused)
                .onSubmit {
                    applyDraft()
                }

            HStack {
                Button("Apply") {
                    applyDraft()
                }

                Button("Clear") {
                    tileCount = nil
                    draft = ""
                    message = nil
                    fieldIsFocused = true
                }

                Button("Edit") {
                    fieldIsFocused = true
                }
            }

            Text("Accepted count: \(acceptedDescription)")

            if let message = message {
                Text(message)
                    .foregroundColor(.secondary)
            }
        }
        .padding(20)
        .frame(width: 380)
    }

    private func applyDraft() {
        if draft.isEmpty {
            tileCount = nil
            message = nil
            return
        }

        guard let parsed = Int(draft, radix: 10) else {
            message = "Enter a whole number without spaces, or clear the field."
            fieldIsFocused = true
            return
        }

        tileCount = parsed
        draft = String(parsed)
        message = nil
    }
}

يرتبط الحقل دائماً بـ draft، لذا فإن تعيين tileCount إلى nil لا يزيل حالة النص القابلة للتحرير. ولا يحدث التحويل إلا عندما ينقر الشخص Apply أو يضغط Return. بعد إرسال إدخال غير صالح، تبقى الرسالة حتى الإرسال التالي أو الضغط على Clear؛ وتظل المسودة متاحة للتصحيح.

يمنع guard التحويل غير الناجح من استبدال الرقم المقبول. وتوحّد عمليات الإرسال الناجحة طريقة كتابة الرقم عبر String(parsed): فمثلاً، يؤدي إدخال +024 إلى 24. هذه سياسة صريحة في المثال. أزل تعيين التوحيد هذا إذا كان الاحتفاظ بطريقة كتابة الرقم المُرسَل مفيداً.

اختر قواعد كتابة الأرقام بعناية

يقبل هذا المثال أعداداً صحيحة موقّعة وغير مجمّعة ذات أساس عشري. ويرفض المسافات والكسور العشرية وفواصل التجميع. توثّق Swift أن Int(_:radix:) يعيد nil عند الإدخال غير الصالح أو عندما تتعذر على نوع العدد الصحيح تمثيل القيمة؛ لذا فإن رقماً كبيراً جداً ملصقاً يتبع مسار التحقق أيضاً. مُهيّئ السلسلة النصية Int في Swift

للحقل الذي يتطلب تدويناً رقمياً محلياً، أبقِ على بنية المسودة والتثبيت نفسها، لكن استبدل التحويل بإستراتيجية تحليل مناسبة من Foundation. تعرض ParseableFormatStyle خاصية parseStrategy، وتنفّذ طريقة parse(_:) في إستراتيجية التحليل التحويل وقد تُطلق خطأً. عالج الإخفاق مع الاحتفاظ بالمسودة. اختر قواعد تنسيق وتحليل متوافقة، ثم اختبر اللغات المحلية المستهدفة. توثيق Apple لـ ParseableFormatStyle، توثيق Apple لـ ParseStrategy

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

استعد التركيز عبر SwiftUI أو AppKit

في SwiftUI، أرفق .focused($fieldIsFocused) بالمحرر وعيّن fieldIsFocused = true من إجراء محلي، كما يفعل المثال. توثّق Apple أن تعيين قيمة لحالة التركيز ينقل التركيز إلى العرض المرتبط بها. امنح الحقول المنفصلة ارتباطات تركيز مختلفة؛ فتعيين قيمة التركيز نفسها لعدة حقول يسبب التباساً. توثيق Apple لـ FocusState

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

بالنسبة إلى حقل AppKit يملكه تطبيقك، افحص NSTextField.isEditable. تعرّف Apple هذه الخاصية بأنها تتحكم في إمكانية تحرير المستخدم لقيمة الحقل. أعدها إلى true إذا جعلتها شيفرتك خطأً تساوي false، مع الحفاظ على أي قيود مقصودة في التطبيق. توثيق Apple لـ NSTextField.isEditable

عند نقل تركيز AppKit صراحةً، تحقّق من acceptsFirstResponder واستدعِ makeFirstResponder(_:) للنافذة المالكة. وتتطلب نتيجة الاستدعاء عناية: فقد تعني false أن المستجيب الحالي رفض التخلي عن التركيز، بينما قد تحدث true أيضاً عندما يرفض المستجيب المقترح ويصبح مستجيب النافذة هو الأول. تحقّق من حالة التحرير الناتجة. لتحديد الحقل بأكمله واستبداله، فإن selectText(_:) هي العملية الموثّقة؛ وليس لها تأثير قبل أن ينتمي الحقل إلى التسلسل الهرمي لعروض نافذة. توثيق Apple لـ makeFirstResponder(_:)، توثيق Apple لـ selectText(_:)

راعِ دورة حياة محرر الحقل

تعيد currentEditor() في AppKit محرر الحقل الحالي للعنصر، أو nil إذا لم يكن لديه محرر. تصف Apple هذا المحرر بأنه NSTextView مشترك بين العناصر في النافذة ويُنشأ عند الحاجة. لذلك، فإن وجود محرر nil خارج جلسة تحرير نشطة لا يثبت أن إدخال الأرقام معطّل. توثيق Apple لـ currentEditor()

على مستوى نظام النصوص، يتعامل محرر الحقل مع Tab وShift-Tab وReturn بوصفها إشارات لإنهاء التحرير وربما تغيير المستجيب الأول. لا تغيّر isFieldEditor لإصلاح التحويل الرقمي: فهذه الخاصية تتحكم في سلوك التحرير، لا في تحليل الرقم الاختياري. أبقِ فحوص AppKit وTextKit هذه محصورة في العناصر التي يملكها تطبيقك. توثيق Apple لـ NSTextView.isFieldEditor

تحقّق من نجاح المسح وإعادة الإدخال

نفّذ هذا التسلسل اليدوي على المثال الكامل في كل إصدار من macOS يدعمه تطبيقك. هذه هي النتائج المتوقعة استناداً إلى منطق المثال، وليست نتائج اختبارات أُجريت فعلياً.

الإجراء القيمة المقبولة المتوقعة سلوك التحرير المتوقع
التشغيل 12 تحتوي المسودة على 12
انقر Clear nil، وتُعرض على أنها None المسودة فارغة؛ ويُطلب التركيز
اكتب 24، ثم انقر Apply 24 تحتوي المسودة على 24
استبدل بـ -، ثم اضغط Return القيمة المقبولة السابقة تبقى المسودة -؛ وتظهر رسالة
استبدل بـ -8، ثم اضغط Return -8 تُمسح الرسالة
أدخل 0، ثم انقر Apply 0 يظل الصفر متميزاً عن غياب القيمة
أدخل 2.5، ثم انقر Apply القيمة المقبولة السابقة يبقى النص الكسري للتصحيح
أفرغ الحقل، ثم انقر Apply nil يظل الحقل متاحاً لتحرير آخر

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

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

المصادر