[يوم يوميات التطوير1] أحب Captio كثيرا لدرجة أنني أردت أن أصنع "تلك التجربة" مرة أخرى

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

هل تعرف التطبيق المسمى Captio؟

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

لكن "هذا كل شيء" كان مريحا بشكل لا يصدق.

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

قام Captio بإنهاء الخدمة.

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

لذلك قررت أن أصنعها بنفسي. جرب Captio مرة أخرى.

هذا هو سجل تطوير "SimpleMemo" Day1. في هذه المقالة، سنقوم بتفكيك تجربة Captio وتسجيل جميع الأحكام الفنية حول كيفية إعادة إنتاجها في مجموعة من التقنيات الحديثة.

1. "ماذا" من Captio كان جيدا - حاولت تفكيك التجربة

من أجل التعبير عن تجربة Captio، حاولت تقسيمها إلى عنصرين من 3.

العنصر 1: التشغيل الفوري إلى الإدخال

Captio، يتم عرض لوحة المفاتيح على الفور وإدخال النص ممكن. يتم عرض الشاشة الرئيسية بعد شاشة البداية، ولا يوجد إجراء للضغط على زر إنشاء المذكرة من هناك. البداية = الإدخال. نسمي هذا "الوقت من بدء التشغيل إلى إدخال النص" Time-to-Text. كان Captio أقل من 0.5 ثانية في التجربة.

العنصر 2: يختفي فور الإرسال (مسح فوري بعد الإرسال)

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

العنصر 3: لا شيء غير ضروري (لا شيء غير ضروري)

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

يتم إعادة إنتاج عنصر 3 هذا في تطوير iOS الحديث. هذه هي مهمة "SimpleMemo".

2. الهندسة المعمارية: لماذا UIKit "بدلا من SwiftUI"

في تطوير iOS في 2026، قد يبدو من الهرطقة اختيار UIKit بدلا من SwiftUI. ومع ذلك، كان هدفنا ذو الأولوية القصوى واضحا - Time-to-Text 500ms أو أقل.

إعادة تقييم "الجسم" في SwiftUI، "تهيئة "StateObject""، حل "@Environment" - يمكن تجاهل هذه النفقات العامة في التطوير العادي للتطبيق، ولكن لا يمكن تجاهلها لتحقيق "مدخلات بدء التشغيل". أريد التركيز على حقل النص بسرعة حتى مع 1ms.

هناك، UIKit تم اعتماد البناء المباشر دون استخدام القصة المصورة.

قم بإنشاء نافذة باستخدام SceneDelegate وقم بتعيين CompositeViewController مباشرة على rootViewController. تخطي حتى عملية تحليل Storyboard.

// SceneDelegate.swift
func scene(_ scene: UIScene,
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = ComposeViewController()
    window.makeKeyAndVisible()
    self.window = window
}

ومع `viewDidAppear` في ComposeViewController، ركز على الفور على عرض النص.

// ComposeViewController.swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    textView.becomeFirstResponder()
}

يقصر رمز 2 هذا وحده المسار من بدء التشغيل إلى عرض لوحة المفاتيح.

الهدف هو 500ms أو أقل. في السجل الذي تم قياسه لاحقا عن طريق تسجيل الشاشة (2026 8 11، iPhone 16e، بداية دافئة)، كانت 0.4 ثانية من النقر على الرمز إلى شاشة لوحة المفاتيح. في SwiftUI، من الصعب التحكم في توقيت `@FocusState` وإعادة تقييم الجسم بالتفصيل، ومن الصعب تعبئة هذا المسار (لم يتم قياس الاثنين ومقارنتهما في ظل نفس الظروف).

UIKit قد يكون "قديما". ومع ذلك، "سريع". في نقطة 1 هذه، لا يزال UIKit هو الخيار الأفضل.

3. تصميم يدرك "يختفي عند إرساله" بنقرة واحدة

هناك أهم حكم تصميم لإعادة إنتاج تجربة Captio "يختفي عند إرسالها". إنه لفصل الرسوم المتحركة للإرسال ومسح واجهة المستخدم تماما عن استجابة الشبكة.

تدفق نقل التطبيقات الشائعة هو كما يلي:

  1. اضغط على زر الإرسال
  2. إظهار مؤشر التحميل
  3. في انتظار رد من الخادم
  4. امسح واجهة المستخدم إذا نجحت، فقم بعرض الخطأ إذا فشل

في هذا التدفق، سيكون تأخير الشبكة هو تأخير واجهة المستخدم كما هي. تدفقنا هكذا:

  1. اضغط على زر الإرسال
  2. على الفورمسح واجهة المستخدم (الرسوم المتحركة: 0.25 ثانية)
  3. نقل الشبكة في الخلفية
  4. عند النجاح: حذف بهدوء من Outbox؛ وعند الفشل: إعادة المحاولة في الخلفية
// ComposeViewController.swift
private func performSendAnimation() {
    UIView.animate(withDuration: 0.25,
                   delay: 0,
                   options: .curveEaseOut) {
        self.textView.alpha = 0
        self.subjectField.alpha = 0
    } completion: { _ in
        self.textView.text = ""
        self.subjectField.text = ""
        self.textView.alpha = 1
        self.subjectField.alpha = 1
        self.textView.becomeFirstResponder()
    }
}

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

"ولكن ماذا لو فشل الإرسال؟" - الإجابة على هذا السؤال هي بنية Outbox في القسم التالي.

4. بنية Outbox لمنع الانسكابات

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

ما تم اعتماده هناك هو نمط Outbox.

تدفق إرسال الرسائل هو كما يلي:

  1. إرسال زر النقر: تشفير الرسالة AES-GCM وحفظها في Outbox (التخزين المحلي)
  2. مسح واجهة المستخدم: مسح الشاشة فوراً باستخدام performSendAnimation
  3. الإرسال في الخلفية: قراءة الرسالة من Outbox وإرسالها إلى Relay API
  4. النجاح: حذف الرسالة من Outbox
  5. فشل: اتركه في انتظار الإرسال وأعد إرساله لاحقا
  6. دون اتصال: يراقب NWPathMonitor الاتصال ويعيد الإرسال تلقائياً عند عودته

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

الرسائل المخزنة في Outbox محمية بتشفير AES-GCM باستخدام CryptoKit. يتم تخزين مفتاح التشفير في سلسلة المفاتيح ولا يمكن الوصول إليه من خارج رمل التطبيق.

// OutboxManager.swift
func enqueue(message: OutboxMessage) throws {
    let key = try KeychainHelper.getOrCreateSymmetricKey()
    let sealedBox = try AES.GCM.seal(
        message.plainData,
        using: key
    )
    let encrypted = EncryptedOutboxEntry(
        id: message.id,
        sealedData: sealedBox.combined!,
        createdAt: Date(),
        retryCount: 0
    )
    try persistenceStore.save(encrypted)
}

أيضا، لأنه ينفذ كل شيء من تلقاء نفسه، الاعتماد على المكتبة الخارجية هو صفر. CryptoKit وNWPathMonitor وURLSession - اكتملت جميعها بإطار عمل Apple الأصلي. يعد التحرر من التحكم في إصدار مكتبة الجهات الخارجية والتدقيق الأمني ميزة كبيرة للفرق الصغيرة.

5 ترحيل API: الإرسال دون الاعتماد على تعاون OAuth في صندوق البريد

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

اخترنا التكوين Cloudflare العمال + Resend API.

Cloudflare Workers هي منصة حوسبة متطورة تنفذ التعليمات البرمجية في مركز البيانات الأقرب إلى المستخدمين في جميع أنحاء العالم. تتم معالجة الطلبات الواردة من اليابان على حافة اليابان، وتتم معالجة الطلبات الواردة من الولايات المتحدة على حافة الولايات المتحدة. هذا يلغي تقريبا مشكلة البدء البارد مقارنة بالخوادم التقليدية (AWS Lambda، وما إلى ذلك).

استخدم Resend API لإرسال رسائل البريد الإلكتروني. Resend هو بريد إلكتروني API للمطورين.

تدفق مصادقة البريد الإلكتروني

عندما يقوم المستخدم بتعيين عنوان البريد الإلكتروني الوجهة، يتم إجراء مصادقة البريد الإلكتروني باستخدام رمز التحقق المكون من أرقام 6. هذا يمنع الإرسال غير المصرح به إلى عناوين البريد الإلكتروني التابعة لجهات خارجية.

حد المعدل متعدد الطبقات

يتم تنفيذ قيود المعدل متعددة الطبقات لمنع الاستخدام غير المصرح به.

// Cloudflare Worker - Rate Limit Configuration
const RATE_LIMITS = {
  devicePerMinute: 30,    // 1デバイスあたり1分30回
  devicePerDay: 200,      // 1デバイスあたり1日200回
  ipPerHour: 120,         // 1IPあたり1時間120回
  globalPerDay: 300       // サービス全体で1日300回(調整可能)
};

من خلال تطبيق حد معدل في طبقة 3 حسب الجهاز وIP والعالمي، يمكنك حظر الهاربين وهجمات الروبوتات على جهاز واحد.

تحديث 2026-09: أُزيل الحد الخاص بكل جهاز والحد الإجمالي لكل 1 يوم في شهر 6 من عام 2026. الحد الحالي للإرسال هو حد لكل عنوان IP خلال 1 ساعة.

ضمان المساواة في القوة

لمنع الإرسال المكرر بسبب عدم استقرار الشبكة، يُمنح كل إرسال UUID، وتفحص Relay API التكرار. إذا أُعيد إرسال رسالة تحمل UUID نفسه، تُعاد استجابة نجاح من المحاولة 2 وما بعدها دون إرسال بريد جديد. بذلك، حتى مع آلية إعادة المحاولة في نمط Outbox، لا يتلقى المستخدم الملاحظة نفسها مرتين.

2026-10 Postscript: لا يتحقق خادم الإرسال الحالي من الازدواجية نفسها. قم بتمرير معرف الرسالة كمفتاح Idempotency للطلب إلى Resend، ويتذكر Resend نفس المفتاح لساعات 24، ويعيد نفس الاستجابة كما كان من قبل لنفس طلب المحتوى. لذلك، لن يتم إرسال نفس المذكرة مرارا وتكرارا خلال ساعات 24 (لمزيد من التفاصيل، Relay API design).

6. ما فعلته اليوم: محو الانزعاج الصغير واحدا تلو الآخر

تم إنفاق الكثير من أعمال تنفيذ Day1 في مراجعة مربع حوار تصنيف النجوم. تقييم App Store هو شريان الحياة للتطبيق، ولكن لا تسعى إلى التقييم بطريقة تضر بتجربة المستخدم.

إعادة تصميم ظروف العرض

تم تعيين شروط عرض مربع حوار تصنيف النجوم على النحو التالي:

  • معروض لكل 100: يتم عرضه فقط للمستخدمين الذين استخدموا التطبيق بالكامل
  • الحد الأدنى لفاصل أيام 14: لقد مر أكثر من 14 منذ آخر مرة تم عرضها
  • لن يتم عرض 30 لمدة يوم بعد الإغلاق: إذا أغلقه المستخدم، فلن يتم عرض 30 مرة أخرى لمدة يوم واحد

تعديل منطقة النقر في أيقونة النجمة

توصي إرشادات الواجهة البشرية Apple بالحد الأدنى لحجم اللمس المستهدف ل 44x44pt. لم تستوف منطقة النقر في أيقونة النجمة هذا المعيار، لذلك تم تصحيحها.

// StarRatingView.swift
private func createStarButton() -> UIButton {
    let button = UIButton(type: .custom)
    button.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
    ])
    return button
}

تصحيح تشويه أيقونة النجمة

UIStackView. تم تعيين `contentMode` على `.scaleAspectFit`، وتم تغيير توزيع StackView إلى `.equalSpacing`.

التدفق بعد التقييم

  • نجوم 5: الانتقال إلى شاشة مراجعة App Store (`SKStoreReviewController`)
  • نجوم 4 أو أقل: عرض نخب "شكرا لك" بهدوء وانتهى. لا تقود المراجعات السلبية إلى App Store.

7 تم حل عدم اتساق حالة العرض في "السجل"

لاحظت خطأ في شاشة سجل الإرسال (السجل). كانت هناك رسالة تفيد بأن الحالة ظلت "إرسال" على الرغم من اكتمال الإرسال.

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

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

// HistoryManager.swift
// Before: テキストベースの検索(バグの原因)
// func updateStatus(forText text: String, status: SendStatus)

// After: IDベースのステータス更新
func updateStatus(forMessageId id: UUID, status: SendStatus) {
    guard let index = entries.firstIndex(where: { $0.messageId == id }) else {
        return
    }
    entries[index].status = status
    persistEntries()
}

باستخدام هذا التصحيح، بغض النظر عن عدد المرات التي ترسل فيها ملاحظة بنفس المحتوى، يتم الآن عرض كل حالة بشكل صحيح.

8. التزام بانويد بالخصوصية

يتم كتابة المعلومات الخاصة في تطبيق المذكرة. كلمات المرور والمخاوف الشخصية والأفكار التجارية - من المستحيل التنبؤ بما سيكتبه المستخدمون. هذا هو السبب في أن حماية الخصوصية ليست "كافية" ولكن "المفرطة" مناسبة تماما.

تراكب الخصوصية في مبدل التطبيقات

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

تحديث 2026-09: هذه الحماية غير مفعّلة في التطبيق الحالي؛ شيفرة العرض ما زالت موجودة لكنها لا تُستدعى. قد تبقى الملاحظة المعروضة في صورة مبدّل التطبيقات. راجع مقالة «تصميم الخصوصية» للتفاصيل.

جلسة URL سريعة الزوال

تخزن جلسة عنوان URL العادية ذاكرة التخزين المؤقت وملفات تعريف الارتباط ومعلومات المصادقة على القرص. باستخدام `URLSessionConfiguration.ephemeral`، نتأكد من عدم بقاء هذه المعلومات على القرص على الإطلاق. هذا لمنع محتويات المذكرة من البقاء في التخزين كذاكرة تخزين للاتصالات.

سجلات غير ضارة

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

  • لن يتم تسجيل محتويات المذكرة في السجل على الإطلاق حتى لو كان بناء DEBUG
  • لا يستخدم سجل الأخطاء `localizedDescription` ويتم تسجيل `type(of: error)` فقط
  • السبب: لأن `localizedDescription` قد يحتوي على مدخلات المستخدم
// NetworkManager.swift
// BAD: エラーの詳細をログに出力
// Logger.error("Send failed: \(error.localizedDescription)")

// GOOD: エラーの型のみをログに出力
Logger.error("Send failed: \(type(of: error))")

قد يبدو بجنون العظمة. ومع ذلك، أعتقد أن هذه الدرجة من جنون العظمة ضرورية من أجل الثقة واستخدام تطبيق المذكرة.

9. دعم اللغة 10: أريد تسليمه لمستخدمي Captio في جميع أنحاء العالم

كان لدى Captio مستخدمين في جميع أنحاء العالم، وخاصة في البلدان الناطقة باللغة الإنجليزية. من أجل تقديم "SimpleMemo" إلى المستخدمين السابقين، تم دعم لغة 10 من الإصدار الأول.

اللغات المدعومة: اليابانية، الإنجليزية، الإسبانية، الفرنسية، الألمانية، الإيطالية، البرتغالية، الكورية، الصينية (المبسطة)، العربية.

تحديث 2026-09: يذكر الوصف الحالي في App Store دعم 10 لغات: اليابانية والإنجليزية والإسبانية والفرنسية والألمانية والبرتغالية والروسية والصينية المبسطة والصينية التقليدية والعربية. الإيطالية والكورية الواردتان في القائمة أعلاه غير مذكورتين في الوصف الحالي.

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

تبديل اللغة داخل التطبيق

بالإضافة إلى تبديل اللغة داخل التطبيق بعد iOS 16، يتم أيضا تنفيذ آلية تبديل لغة فريدة من نوعها. عندما يتم تبديل اللغة، يتم استبدال ViewController بانتقال "cross-dissolve" لتوفير تجربة تبديل سلسة.

// LanguageManager.swift
func applyLanguageChange() {
    guard let window = UIApplication.shared.connectedScenes
        .compactMap({ $0 as? UIWindowScene })
        .first?.windows.first else { return }

    let newVC = ComposeViewController()
    newVC.view.frame = window.bounds
    UIView.transition(with: window,
                      duration: 0.3,
                      options: .transitionCrossDissolve,
                      animations: {
        window.rootViewController = newVC
    })
}

10. تعلم اليوم: المنتج هو تصميم "الشعور بالأمان"

بعد الانتهاء من Day1، ما أعتقده هو أن جوهر هذا المنتج هو تصميم "الشعور بالأمان".

لخص الأرقام الفنية:

  • Time-to-Text: 500ms أو أقل (الهدف)
  • Send-to-Reset: قصر لا يشعر بالانتظار (الهدف)
  • فقدان الرسائل: يمنعه نمط Outbox
  • التشفير: AES-GCM (CryptoKit)
  • تبعية المكتبة الخارجية: صفر

ومع ذلك، يتم تلخيص كل هذه الأرقام في هدف واحد يسمى "الشعور بالأمان".

بدء التشغيل السريع → "يمكنك الكتابة على الفور في أي وقت" راحة البال.
يختفي عند إرساله → "معالجة" الشعور بالأمان.
التدابير المضادة للانسكاب → "لن تختفي بدون انتقال" راحة البال.
التشفير → "لا يمكن لأحد أن يرى" راحة البال.

Captio كان محبوبا ليس بسبب وظيفته الممتازة. " لأن تجربة "الاستخدام براحة البال" كانت مصقولة تماما. أريد أن أتولى ذلك.

11 في المرة القادمة (Day2) إشعار

Day2 يخطط للعمل على الموضوعات التالية:

  • تطور الرسوم المتحركة للإرسال: من 0.25 الثانية الحالية إلى الرسوم المتحركة الأكثر راحة
  • تحسين توقيت تحديثات حالة السجل: مقارنة التحديثات في الوقت الفعلي مقابل تحديثات الدفعات
  • هل تحتاج حقا إلى نخب ناجح؟ : النظر في خيار "عدم العرض" للحصول على ملاحظات حول الإرسال الناجح

تهدف إلى أناقة "لا شيء" من Captio، تابع إلى Day2.

المراجع