بنية Outbox: مصممة لمنع الرسائل من الانسكاب

هذه الصفحة مترجمة آليًا من الأصل الياباني. اقرأ الأصل الياباني

المشكلة: لتجنب انسكاب الرسائل حتى لو قمت بمسحها على الفور

في SimpleMemo، يتم مسح واجهة المستخدم في اللحظة التي ينقر فيها المستخدم على زر الإرسال ويمكنه البدء على الفور في كتابة الملاحظة التالية. هذه هي تجربة المستخدم التي هي جذر نمط Captio.

ومع ذلك، فإن الشبكة الحقيقية لديها زمن انتقال يبلغ 300ms ~ 2 ثانية، وقد تفشل، وقد تكون غير متصلة بالإنترنت في المقام الأول. تنتظر العديد من التطبيقات استجابة الشبكة قبل مسح واجهة المستخدم. إنه آمن، لكن تجربة المستخدم سيئة. على العكس من ذلك، إذا قمت بمسحها على الفور، فهناك خطر فقدان الرسالة.

نمط Outbox يحل هذا المطلب المتضارب في نفس الوقت. احفظ الرسالة بشكل دائم في التخزين المشفر المحلي (Outbox) في لحظة النقر على الإرسال، ومسح واجهة المستخدم على الفور، وأرسل الشبكة في الخلفية. إذا نجح الإرسال، فسيتم حذفه من Outbox، وإذا فشل، فسيتم إعادة المحاولة تلقائيا. نتيجة لذلك، اترك الرسالة غير المرسلة على المحطة الطرفية وأعد إرسالها، ويقترب زمن انتقال التجربة من الصفر.

تدفق تصميم نمط Outbox

فيما يلي التدفق الكامل من النقر على زر الإرسال إلى إرسال الرسالة.

  1. إرسال الصنبور → أولا، قم بتشفير AES-GCM في Outbox واحفظه بشكل دائم
    في اللحظة التي يضغط فيها المستخدم على زر الإرسال، يتم تشفير الرسالة باستخدام AES-GCM 256bit وتخزينها بشكل دائم في التخزين المحلي للجهاز. لن يتم مسح واجهة المستخدم حتى تكتمل هذه الخطوة. بعبارة أخرى، سيكون تخزين Outbox أول كاسر أمواج لمنع الانسكاب.
  2. مسح واجهة المستخدم على الفور (يمكن للمستخدمين البدء في كتابة الملاحظات التالية)عندما ينجح حفظ
    Outbox، قم بمسح عرض النص وتشغيل الرسوم المتحركة للإرسال. من وجهة نظر المستخدم، إنه مرادف لكلمة "مرسل". أنا لا أنتظر أي رد على الشبكة. الهدف من الإرسال إلى إعادة الضبط هو أن تكون قصيرا دون الشعور بالانتظار (0.25 ثانية بما في ذلك الرسوم المتحركة).
  3. أرسل بريدا إلكترونيا عبر Relay API في الخلفيةبالتوازي مع مسح
    UI، يرسل SendManager طلب HTTP لترحيل API على عمال Cloudflare. هذه العملية غير متزامنة تماما ولا تمنع الخيط الرئيسي.
  4. إذا نجحت، احذف من Outbox
    Relay إذا تلقيت استجابة 200 من API، احذف الرسالة المقابلة بأمان من Outbox. هذا يكمل دورة حياة الرسالة.
  5. إذا فشل، فسيتم إعادة إرساله تلقائيا
    في حالة حدوث خطأ في الشبكة أو خطأ في الخادم، اتركه في انتظار الإرسال، عند إرجاع الخط، عند فتح التطبيق، معالجة الخلفية (BGTaskScheduler. يتم تحديد توقيت التنفيذ بواسطة iOS) وإعادة إرساله. هناك حد أعلى لعدد المرات لإعادة الإرسال تلقائيا.
  6. إذا كان غير متصل بالإنترنت، يكتشف NWPathMonitor استرداد الخط ويعيد إرساله تلقائياإذا كنت غير متصل بالإنترنت في وقت إرسال
    ، فإن NWPathMonitor من Network.framework يراقب حالة الخط، وعندما يكتشف استرداد Wi-Fi أو الخلوي، فإنه سيعيد إرسال الرسالة غير المرسلة تلقائيا في Outbox (كما هو الحال مع 5 أعلاه، هناك حد أعلى لعدد عمليات إعادة الإرسال التلقائية).

رمز إرسال المدير

وظيفة send() ل SendManager هي نقطة الدخول لنمط Outbox. أولا، احفظه في Outbox، وقم بالانتظار بدون اتصال بالشبكة، وأرسله على الفور إذا كان هناك اتصال.

// 保存完了と送信結果を分ける説明用の簡略API
@MainActor
func send(message: String, onSaved: () -> Void,
          completion: @escaping (SendResult) -> Void) {
    // まずOutboxに保存(クラッシュや圏外で失わないように)
    let outboxMessage: OutboxMessage
    do {
        outboxMessage = try OutboxManager.shared.add(body: message)
    } catch {
        DispatchQueue.main.async { completion(.failure(error)) }
        return
    }

    // 同期保存に成功した場合だけ、メインスレッドでUIへ通知する
    onSaved()

    // ネットワーク接続がない場合はキューに入れたことを通知
    guard NetworkMonitor.shared.isConnected else {
        BackgroundTaskManager.shared.scheduleRetryTask()
        DispatchQueue.main.async { completion(.queued) }
        return
    }

    // 送信処理を実行
    performSend(message: message, outboxId: outboxMessage.id, ...)
}

النقطة المهمة هي أن OutboxManager.shared.add(body:) يكتب إلى التخزين المستمر بشكل متزامن. عندما تنجح هذه المكالمة، يتم تخزين الرسالة بأمان على الجهاز. بغض النظر عن نتيجة نقل الشبكة، ستبقى الرسالة في Outbox.

تنفيذ تشفير AES-GCM

الرسائل المخزنة في Outbox هي ملاحظات خاصة من المستخدم. على الرغم من أنه تخزين محلي، إلا أنه لا يمكن تخزينه في نص عادي. باستخدام AES-GCM من Apple CryptoKit، لا يستخدم التنفيذ أي SDK خارجي للتشفير.

  • Apple CryptoKit - متاح ل iOS 13 والإصدارات الأحدث. لا تستخدم SDK خارجيا لمعالجة التشفير (Apple أصلي فقط).
  • مفتاح متماثل 256-bit - احفظ في سلسلة المفاتيح.
  • التشفير مع المصادقة - يضمن AES-GCM سرية وسلامة البيانات في نفس الوقت. يتم اكتشاف البيانات التي تم العبث بها في وقت فك التشفير.
enum OutboxEncryption {
    private static var encryptionKey: SymmetricKey {
        if let existingKey = KeychainHelper.load(key: "outbox_encryption_key") {
            return SymmetricKey(data: existingKey)
        }
        let newKey = SymmetricKey(size: .bits256)
        let keyData = newKey.withUnsafeBytes { Data($0) }
        KeychainHelper.save(key: "outbox_encryption_key", data: keyData)
        return newKey
    }

    static func encrypt(_ plainText: String) throws -> Data {
        guard let data = plainText.data(using: .utf8) else {
            throw OutboxError.encryptionFailed
        }
        let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
        guard let combined = sealedBox.combined else {
            throw OutboxError.encryptionFailed
        }
        return combined
    }
}

تلقائيا عند الحاجة لأول مرة ويتم تخزينه في سلسلة المفاتيح. إعداد الحفظ هو "هذا الجهاز فقط، اقرأ بعد إلغاء القفل الأول" (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)، والذي لا يمكن قراءته من التطبيقات الأخرى ولا ينتقل من النسخة الاحتياطية إلى محطة طرفية أخرى. إذا قمت بإلغاء قفله مرة واحدة بعد إعادة التشغيل، فيمكنك أيضا قراءته من معالجة الخلفية أثناء القفل. بالإضافة إلى ذلك، فإن مثال الكود أعلاه هو نسخة مبسطة في وقت كتابة المقالة، ولا ينشئ الرمز الحالي مفتاحا جديدا من خلال التعامل معه على أنه "بدون مفتاح" عندما تفشل سلسلة المفاتيح في القراءة.

معنى تكوين مسارات البيانات فقط باستخدام Apple حقيقي

جميع الأطر المستخدمة في بنية Outbox هي Apple الأصلية.

  • CryptoKit — تشفير AES-GCM. لا توجد مكتبة تشفير تابعة لجهة خارجية مطلوبة.
  • Network.framework - مراقبة حالة الخط باستخدام NWPathMonitor. مكتبة إمكانية الوصول غير مطلوبة.
  • مهام الخلفية — إعادة المحاولة في الخلفية باستخدام BGTaskScheduler.

حقيقة عدم وجود مكتبة خارجية في المسار الذي ينتظر الإرسال لها المعاني التالية.

  • لا يتأثر بالتحديثات الخارجية - لا تتوقف المعالجة المعلقة عن العمل بسبب تحديث مكتبات الجهات الخارجية أو إنهاء التوفير.
  • الأمان — لا يدخل رمز الطرف الثالث المعالجة (التشفير والحفظ والإرسال) الذي يمس نص المذكرة التي تنتظر إرسالها.

بالإضافة إلى ذلك، يستخدم التطبيق بأكمله أيضا SDK خارجيا (Google تسجيل الدخول، Firebase، AppsFlyer) (انظر تصميم يضع الخصوصية أولاً). يتعلق "Apple أصلي فقط" المذكور هنا بمسار البيانات التي تنتظر إرسالها.

فصل مسح واجهة المستخدم ونقل الشبكة

تكمن القيمة الحقيقية لنمط Outbox في الفصل الكامل لطبقة واجهة المستخدم وطبقة الشبكة. دعونا نلقي نظرة على معالج الصنبور الخاص بزر الإرسال.

@objc private func sendButtonTapped() {
    PerformanceLogger.shared.beginSendToReset()
    let message = textView.text ?? ""
    isSending = true
    sendBarButton.isEnabled = false

    SendManager.shared.send(message: message, onSaved: { [weak self] in
        // 端末への保存成功後にだけ消す。ネットワーク応答は待たない。
        self?.performSendAnimation { [weak self] in
            guard let self = self else { return }
            // アニメーション中に編集された入力は残す。
            if self.textView.text == message {
                self.clearTextView()
            }
            PerformanceLogger.shared.endSendToReset()
        }
    }) { [weak self] result in
        self?.isSending = false
        self?.updateSendButtonState()
        switch result {
        case .success:
            PerformanceLogger.shared.logEvent(name: "SendSuccess")
        case .failure(let error):
            self?.handleSendError(error)
        case .queued:
            self?.showQueuedFeedback()
        }
    }
}

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

الهدف من الإرسال إلى إعادة الضبط هو القصر الذي لا يشعر بالانتظار. 0.25 ثانية بما في ذلك الرسوم المتحركة. مباشرة بعد النقر على زر الإرسال، يمكن للمستخدم البدء في كتابة الملاحظة التالية. هذا هو UX نمط Captio.

الأسئلة الشائعة

س. ماذا يحدث للملاحظات المرسلة في وضع عدم الاتصال؟

يتم تشفيره وتخزينه في Outbox، ويعيد إرساله تلقائيا عند استعادة الخط (تم اكتشافه بواسطة NWPathMonitor)، عند فتح التطبيق، ومعالجة الخلفية. هناك حد أعلى لعدد المرات لإعادة الإرسال تلقائيا، وبالنسبة للملاحظات التي وصلت إلى الحد الأعلى، اضغط مع الاستمرار على "السجل" وحدد "إعادة الإرسال".

س. ألن تضيع الملاحظات حتى لو أغلقت التطبيق؟

Outbox هو تخزين دائم. حتى إذا أغلقت التطبيق أو أعدت تشغيل iPhone، فسيبقى. في المرة القادمة التي يتم فيها تشغيل التطبيق، أو ستحاول مهمة الخلفية تلقائيا.

س. لماذا اخترت AES-GCM؟

Apple CryptoKit مدعوم أصلا، ويمكن أيضا اكتشاف العبث باستخدام التشفير المصادق عليه. حقيقة أنه لا توجد حاجة إلى مكتبة خارجية لمعالجة التشفير هي أيضا ميزة.

س. ما هو عدد المحاولات في حالة فشل الإرسال؟

سيتم إرساله مرة أخرى تلقائيا. المشغل هو معالجة الخلفية عند إرجاع الخط، عند فتح التطبيق (BGTaskScheduler. يتم تحديد وقت التنفيذ بواسطة iOS). سيتم إرسال المذكرات التي لا يمكن إرسالها مرة أخرى دون حذفها من انتظار الإرسال، ولكن هناك حد لعدد مرات إعادة الإرسال التلقائي.

مقالات ذات صلة

المراجع