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

حدّد بدقة ما ينبغي أن يفعله الحاسوب

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

في هذا المثال، شروط الإدخال هي:

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

النتيجة هي مجموع مربعات الأعداد من 1 حتى عدد الطبقات المطلوب. الصيغة هي n * (n + 1) * (2 * n + 1) / 6. عند وجود 3 طبقة، يكون الحساب التوضيحي 1 + 4 + 9 = 14. أما عند 0، فالنتيجة 0. هذه أمثلة رياضية وليست نتائج تطبيق مقاسة.

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

لماذا يجب أن يلغي الإلغاء مسار التأخير

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

يستخدم التأخير try await Task.sleep(for: .milliseconds(300)). توثّق Apple أن هذا يعلّق المهمة دون حجب الخيط الأساسي، ويرمي CancellationError إذا أُلغيت قبل انتهاء التأخير. يتوفر الإصدار الذي يستقبل مدة على macOS 13 والإصدارات الأحدث. وثائق Task.sleep

يظل الإلغاء تعاونيًا: استدعاء cancel() لا يقاطع قسرًا شيفرة حساب عشوائية. يجب أن يعود مسار التأخير إذا رمى النوم خطأ، وينبغي أن يتحقق من الإلغاء مرة أخرى قبل الحساب. وثائق Task.cancel، وثائق Task.checkCancellation

من الأخطاء الشائعة استخدام try? للنوم ثم متابعة الحساب مباشرة. فهذا يتجاهل خطأ الإلغاء الذي رُمي ويسمح بتنفيذ العبارات التالية. هنا، يؤدي التقاط الخطأ إلى إنهاء المهمة قبل أن تنتج إجابة قديمة.

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

أنشئ مشروع تطبيق macOS في Xcode باستخدام واجهة SwiftUI ولغة Swift. استخدم سلسلة أدوات Swift 6 واضبط هدف النشر على macOS 14 أو إصدار أحدث. يستخدم هذا المثال الإصدار الحديث من onChange(of:initial:_:) الذي لا يستقبل معاملات، وتذكر Apple أنه متاح ابتداءً من macOS 14. وثائق SwiftUI onChange

استبدل ملف نقطة دخول التطبيق الذي أُنشئ تلقائيًا بالبرنامج الكامل أدناه. أزل ملف ContentView.swift الذي أُنشئ تلقائيًا من الهدف، أو استبدل محتواه بحيث لا يتبقى سوى تعريف واحد لـ ContentView. أبقِ تعريف تطبيق واحدًا فقط موسومًا بـ @main ضمن الهدف.

import SwiftUI
import Combine
import Foundation

@MainActor
final class TileCalculator: ObservableObject {
    @Published private(set) var result: Int?
    @Published private(set) var status = "Enter a layer count."

    private var pending: Task<Void, Never>?

    func schedule(_ text: String) {
        pending?.cancel()
        pending = nil
        result = nil

        let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !cleaned.isEmpty else {
            status = "Enter a layer count."
            return
        }
        guard let layers = Int(cleaned, radix: 10),
              (0...1000).contains(layers) else {
            status = "Use a whole number from 0 to 1000."
            return
        }

        status = "Waiting for typing to pause..."
        pending = Task { @MainActor [weak self] in
            do {
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
            } catch {
                return
            }

            guard let self else { return }
            let total = layers * (layers + 1) * (2 * layers + 1) / 6
            self.result = total
            self.status = "Calculated for \(layers) layers."
            self.pending = nil
        }
    }

    func stop() {
        pending?.cancel()
        pending = nil
        result = nil
        status = "Calculation paused."
    }
}

@MainActor
struct ContentView: View {
    @State private var input = "3"
    @StateObject private var calculator = TileCalculator()

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Square-layer tile counter")
                .font(.headline)
            TextField("Layers: 0 to 1000", text: $input)
                .textFieldStyle(.roundedBorder)
            Text(calculator.status)
            if let total = calculator.result {
                Text("Total tiles: \(total)")
                    .font(.title2)
            }
        }
        .padding(24)
        .frame(minWidth: 360, minHeight: 180)
        .onAppear {
            calculator.schedule(input)
        }
        .onChange(of: input) {
            calculator.schedule(input)
        }
        .onDisappear {
            calculator.stop()
        }
    }
}

@main
@MainActor
struct LocalTileCalculatorApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

يمتلك العرض الحاسوب باستخدام @StateObject. توثّق Apple أن هذا يحافظ على نسخة النموذج عبر التغييرات في مدخلات الحاوية، بينما يؤدي تغيير هوية العرض إلى إنشاء نسخة جديدة. وتحرّك خصائص النموذج المنشورة الحالة والإجابة المعروضتين. وثائق StateObject

وُسِم النموذج وإغلاق المهمة صراحةً بـ @MainActor. وتعرّف Apple منفّذ هذا الممثل بأنه مكافئ لطابور الإرسال الرئيسي. لذلك، تشترك الجدولة والإلغاء وإسناد النتيجة في هذا التنفيذ في الممثل نفسه. يعلّق النوم التنفيذ؛ ثم يُنفَّذ تعبير الحساب القصير على ذلك الممثل بعد الانتظار. وثائق MainActor

كيف تمنع قواعد الإدخال والنتيجة المعاينات القديمة

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

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

تزيل trimmingCharacters(in:) من Foundation الأحرف المطابقة من طرفَي السلسلة. واستخدام .whitespacesAndNewlines يعني أن إدخالًا مثل 3 يصبح 3؛ أما الأحرف الداخلية فتبقى كما هي. وثائق التشذيب في Foundation

يستخدم محلل الأعداد الصحيحة في Swift الأساس 10 افتراضيًا، ويقبل إشارة اختيارية في البداية، ويعيد nil للقيم غير الصالحة أو التي لا يمكن تمثيلها. لذلك يُقبل هنا +3 و003، بينما يُرفض 3.5 و1,000 و3 0. وتُرفض القيمة السالبة بعد تحليلها عبر فحص النطاق المنفصل. وثائق Int.init

كما يقيّد الحد الأعلى العمليات الحسابية. عند 1000 طبقة، يكون البسط 2,003,001,000 والإجمالي 333,833,500. وتبقى كل المدخلات المسموح بها ضمن هذه الحدود الوسيطة غير السالبة. ويتطلب رفع الحد التحقق من الضرب الوسيط إلى جانب النتيجة النهائية؛ فقسمة الناتج على ستة بعد ذلك لا تصلح تجاوزًا حدث بالفعل.

تلتقط كل مهمة مؤجلة قيمة layers التي جرى التحقق من صحتها لذلك التعديل. ولا تعيد قراءة الحقل بعد النوم. وبعد اجتياز المهمة فحص الإلغاء، لا توجد نقطة await أخرى قبل إسنادها النتيجة. ولا يمكن لاستدعاء جدولة آخر على الممثل الرئيسي أن يتداخل مع مقطع الحساب والإسناد المتزامن هذا. يشرح دليل لغة Swift عزل الممثلين ونقاط التعليق المحددة صراحةً. التزامن — لغة Swift البرمجية

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

تحقّق من السلوك باستخدام مدخلات اصطناعية

أنشئ التطبيق وشغّله، ثم اتبع تسلسل القبول هذا. تنبع النتائج المتوقعة من شروط البرنامج؛ أما أوقات الاستيقاظ الدقيقة فتعتمد على الجدولة.

الإجراء السلوك المتوقع
افتح النافذة مع الإدخال الأولي 3 اعرض حالة الانتظار، ثم Total tiles: 14.
استبدل الإدخال بـ 4 امسح الإجمالي السابق، ثم اعرض Total tiles: 30.
أدخل 1، ثم 10 قبل اكتمال التأخير الأول أعد بدء التأخير؛ وتنتج القيمة المستقرة Total tiles: 385.
أدخل 4، ثم امسحه قبل اكتمال التأخير اعرض Enter a layer count. دون إجمالي.
أدخل 3.5 أو 1001 اعرض توجيه النطاق فورًا دون إجمالي.
أدخل 0 اعرض Total tiles: 0 بعد التأخير.
أدخل 1000 اعرض Total tiles: 333833500 بعد التأخير.

لإجراء فحوص الإلغاء، أجرِ التعديل التالي قبل انتهاء التأخير بوقت كافٍ. عند الحد الفاصل، قد تكون نتيجة أقدم قد اكتملت بالفعل قبل معالجة التعديل التالي. ومع ذلك، سيمسح التعديل التالي النتيجة ويبدأ طلبًا جديدًا. فالتأخير ليس موعد تنفيذ دقيقًا؛ ويصف دليل لغة Swift النوم بأنه تعليق للتنفيذ مدة لا تقل عن الفترة المطلوبة. وثائق التزامن في Swift

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

اربط الإلغاء بعمر العرض

يستدعي معالج الاختفاء stop() صراحةً. توثّق Apple أن onDisappear يُنفَّذ بعد اختفاء العرض، ويعتمد توقيته الدقيق على نوع العرض. اعتبره نقطة تنظيف للعرض، لا موعدًا دقيقًا لحدث النافذة. ويجدول معالج الظهور الإدخال الحالي مجددًا كلما ظهر العرض. وثائق SwiftUI onDisappear

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

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

المصادر