[Development Diary Day1] Saya sangat menyukai Captio sehingga saya ingin membuat "pengalaman itu" lagi

Halaman ini diterjemahkan secara otomatis dari bahasa Jepang. Baca versi asli berbahasa Jepang

Apakah Anda tahu aplikasi bernama Captio?

Tulis catatan dan tekan tombol kirim. Itu saja yang Anda butuhkan untuk menerima memo ke alamat email Anda. Saat Anda memulainya, keyboard akan ditampilkan, dan jika Anda menulis, kirimkan, dan jika Anda mengirimkannya, itu akan hilang. Tidak ada fungsi yang tidak perlu sama sekali. Itu hanya sebagian besar dari sebuah aplikasi.

Tapi "hanya itu saja" sangat nyaman.

Ide-ide yang saya dapatkan di kereta komuter, tugas-tugas yang tiba-tiba terlintas dalam pikiran selama rapat, dan kata-kata yang terlintas dalam pikiran saya sebelum tidur - Captio mengirimkan semuanya segera ke kotak surat saya. Tidak ada kerumitan untuk membuka aplikasi memo, memilih folder, dan menambahkan judul. Pengalaman "nol gesekan" adalah sesuatu yang tidak bisa Anda lepaskan begitu Anda mengetahuinya.

Captio telah menghentikan layanan.

Saya mencoba beberapa aplikasi alternatif, tetapi tidak ada yang jauh dari pengalaman itu. Startup lambat, ada elemen yang tidak perlu di UI, dan catatan tetap ada setelah transmisi - ini mungkin tampak seperti perbedaan sepele, tetapi inti dari pengalaman yang dibuat oleh Captio adalah akumulasi "hal-hal sepele" seperti itu.

Jadi saya memutuskan untuk membuatnya sendiri. Rasakan Captio lagi.

Ini adalah log pengembangan dari "SimpleMemo" Day1. Dalam artikel ini, kami akan membongkar pengalaman Captio dan mencatat semua penilaian teknis tentang cara mereproduksinya dalam tumpukan teknologi modern.

1 "Apa" dari Captio bagus - Saya mencoba membongkar pengalamannya

Untuk mengucapkan pengalaman Captio, saya mencoba memecahnya menjadi dua elemen 3.

Elemen 1: Peluncuran-ke-Input Instan

Saat Anda memulai Captio, keyboard akan segera ditampilkan dan Anda dapat memasukkan teks. Layar utama ditampilkan setelah layar splash, dan tidak ada prosedur untuk menekan tombol pembuatan memo dari sana. Mulai = Masukan. Kami menyebut ini "waktu dari startup ke input teks" Time-to-Text. Captio kurang dari 0.5 detik dalam pengalaman.

Elemen 2: Menghilang segera setelah mengirim (Instan Jelas Setelah Mengirim)

Saat Anda menekan tombol kirim, layar akan segera dihapus. " Mengirim..." Saya tidak punya waktu untuk menunggu sambil menatap bilah kemajuan. Saat catatan yang Anda tulis menghilang, Anda dapat melanjutkan ke tindakan berikutnya dengan rasa aman "Oh, saya bisa mengirimkannya". Kami menyebut ini "waktu dari menekan tombol kirim ke UI dihapus" Send-to-Reset. Tujuannya adalah untuk mengosongkan layar sebelum Anda merasa telah menunggu.

Elemen 3: Tidak Ada Yang Tidak Perlu (Tidak Ada Yang Tidak Perlu)

Fungsi folder, fungsi tag, teks kaya, dukungan penurunan harga - tidak ada yang tersedia di Captio. Hanya ada bidang input teks dan tombol kirim. Kerapian ini membawa beban kognitif mendekati nol. Aplikasi yang tidak perlu memikirkan apa yang harus dilakukan. Itu adalah Captio.

Elemen 3 ini direproduksi dalam pengembangan iOS modern. Itulah misi dari "SimpleMemo".

2. Arsitektur: Mengapa "UIKit" daripada SwiftUI

Dalam pengembangan iOS di 2026, mungkin tampak sesat untuk memilih UIKit daripada SwiftUI. Namun, tujuan prioritas utama kami jelas - Time-to-Text 500ms atau kurang.

Evaluasi ulang `tubuh` SwiftUI, inisialisasi `StateObject`, resolusi `@Environment` - biaya overhead ini dapat diabaikan dalam pengembangan aplikasi normal, tetapi tidak dapat diabaikan untuk mencapai "mulai-dan-masukan". Saya ingin fokus pada bidang teks dengan cepat bahkan dengan 1ms.

Apa yang diadopsi di sana adalah konstruksi langsung UIKit yang tidak menggunakan Storyboard.

Buat jendela dengan SceneDelegate dan atur CompositeViewController langsung ke rootViewController. Lewati bahkan proses penguraian 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
}

dan dengan `viewDidAppear` di ComposeViewController, langsung fokus pada tampilan teks.

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

Dengan kode 2 ini saja, jalur dari startup ke tampilan keyboard adalah yang terpendek.

Targetnya adalah 500ms atau kurang. Dalam catatan yang diukur kemudian dengan perekaman layar (2026 8 11, iPhone 16e, awal hangat), itu adalah 0.4 detik dari ketukan ikon ke tampilan keyboard. Di SwiftUI, sulit untuk mengontrol waktu `@FocusState` dan evaluasi ulang tubuh secara detail, dan sulit untuk mengemas jalur ini (keduanya tidak diukur dan dibandingkan dalam kondisi yang sama).

UIKit mungkin "tua". Namun, "cepat". Pada titik 1 ini, UIKit masih menjadi pilihan terbaik.

3. Desain untuk mewujudkan "kirim dan hilang" dengan satu ketukan

Ada penilaian desain yang paling penting untuk mereproduksi pengalaman "kirim dan hilang" Captio. Ini adalah untuk sepenuhnya memisahkan animasi transmisi dan pembersihan UI dari respons jaringan.

Alur transmisi aplikasi umum adalah sebagai berikut:

  1. Tekan tombol kirim
  2. Tampilkan indikator pemuatan
  3. Menunggu respons dari server
  4. Bersihkan UI jika berhasil, tampilkan kesalahan jika gagal

Dalam alur ini, penundaan jaringan menjadi penundaan UI sebagaimana adanya. Aliran kami seperti ini:

  1. Tekan tombol kirim
  2. segeramembersihkan UI (animasi: 0.25 detik)
  3. Transmisi jaringan di latar belakang
  4. Berhasil → Diam-diam hapus dari Outbox / Gagal → Coba lagi di latar belakang
// 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()
    }
}

Untuk pengguna, saat Anda menekan tombol kirim, catatan "menghilang" dan Anda dapat menulis catatan berikutnya. Keberhasilan atau kegagalan jaringan ditangani di tempat yang tidak terlihat oleh pengguna.

"Tetapi bagaimana jika transmisi gagal?" - Jawaban atas pertanyaan itu adalah arsitektur Outbox di bagian berikutnya.

4 Arsitektur Outbox untuk mencegah tumpahan

Memisahkan UI transmisi dari respons jaringan berarti bahwa situasi bahwa "UI telah dihapus, tetapi transmisi email sebenarnya gagal" dapat terjadi. Ini benar-benar tidak dapat diterima. Catatan yang menurut pengguna "terkirim" sebenarnya tidak terkirim - ini adalah kehilangan kepercayaan yang fatal untuk aplikasi memo.

Yang diadopsi di sana adalah pola Outbox.

Aliran transmisi pesan adalah sebagai berikut:

  1. Ketuk tombol kirim: Enkripsi pesan dengan AES-GCM dan simpan di Outbox (penyimpanan lokal)
  2. Hapus UI: Bersihkan layar secara instan dengan performSendAnimation
  3. Transmisi latar belakang: Ekstrak pesan dari Outbox dan kirimkan ke Relay API
  4. Sukses: Hapus pesan dari Outbox
  5. Kegagalan: Biarkan menunggu transmisi dan kirim ulang nanti
  6. Offline: Pantau koneksi dengan NWPathMonitor dan secara otomatis mengirim ulang saat dipulihkan

Yang penting adalah untuk menyimpannya ke Outbox sebelum mengirim jaringan. Akibatnya, bahkan jika aplikasi tiba-tiba mogok atau daya terminal mati, pesan yang ditulis ke Outbox akan tetap ada.

Pesan yang disimpan di Outbox dilindungi enkripsi AES-GCM menggunakan CryptoKit. Kunci enkripsi disimpan di Keychain dan tidak dapat diakses dari luar sandbox aplikasi.

// 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)
}

Juga, karena semuanya diimplementasikan oleh Anda sendiri, ketergantungan pada perpustakaan eksternal adalah nol. CryptoKit, NWPathMonitor, URLSession - semuanya diselesaikan dengan kerangka kerja Apple yang asli. Bebas dari kontrol versi perpustakaan pihak ketiga dan audit keamanan adalah keuntungan besar bagi tim kecil.

5. Relay API: Transmisi yang tidak bergantung pada tautan OAuth dari kotak surat

Mekanisme transmisi Captio belum diungkapkan secara resmi. Alasan pengumuman penghentian pengembang adalah karena tidak lagi dapat terus menghabiskan waktu untuk menjaga stabilitas dan keamanan aplikasi, dan metode transmisi tidak dikutip sebagai alasan. Namun, secara umum, pengiriman dengan mengandalkan email eksternal API mudah dipengaruhi oleh perubahan persyaratan penyedia dan pembatasan akses.

Kami memilih konfigurasi Pekerja Cloudflare + Resend API.

Cloudflare Workers adalah platform komputasi tepi yang mengeksekusi kode di pusat data terdekat dengan pengguna di seluruh dunia. Permintaan dari Jepang diproses di tepi Jepang, dan permintaan dari Amerika Serikat diproses di tepi Amerika Serikat. Ini hampir menghilangkan masalah awal dingin dibandingkan dengan serverless konvensional (AWS Lambda, dll.).

Gunakan Resend API untuk mengirim email. Resend adalah email API untuk pengembang.

Alur otentikasi email

Saat pengguna menetapkan alamat email tujuan, autentikasi email dilakukan dengan kode autentikasi digit 6. Ini mencegah pengiriman yang tidak sah ke alamat email pihak ketiga.

Batas tarif berlapis-lapis

Batas tarif berlapis-lapis diterapkan untuk mencegah penggunaan yang tidak sah.

// 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回(調整可能)
};

Dengan menerapkan batas tarif pada lapisan 3 pada perangkat, IP, dan global, Anda dapat memblokir pelarian dan serangan botnet pada satu perangkat.

2026-09 Postscript: Batas atas untuk setiap terminal dan batas atas untuk seluruh hari 1 telah dihapus di 2026 6. Batas transmisi maksimum saat ini adalah batas atas per 1 jam per alamat IP.

Jaminan kesetaraan daya

Untuk mencegah transmisi duplikat karena ketidakstabilan jaringan, tetapkan UUID ke setiap pesan dan lakukan pemeriksaan duplikat di sisi Relay API. Jika pesan UUID yang sama dikirim ulang, setelah waktu 2, itu akan mengembalikan respons yang berhasil tanpa mengirim email. Ini mencegah pengguna menerima memo yang sama dua kali ketika dikombinasikan dengan mekanisme coba lagi dari pola Outbox.

2026-10 Postscript: Server transmisi saat ini tidak memeriksa duplikasi itu sendiri. Berikan ID pesan sebagai Idempotency-Key dari permintaan ke Resend, dan Resend mengingat kunci yang sama untuk jam 24, dan mengembalikan respons yang sama seperti sebelumnya untuk permintaan konten yang sama. Oleh karena itu, memo yang sama tidak akan dikirim berulang kali selama jam 24 (untuk detailnya, Desain Relay API).

6. Apa yang saya lakukan hari ini: Hapus ketidaknyamanan kecil satu per satu

Sebagian besar pekerjaan implementasi Day1 dihabiskan untuk meninjau dialog peringkat bintang. Evaluasi App Store adalah garis hidup dari aplikasi, tetapi jangan mencari evaluasi dengan cara yang merusak pengalaman pengguna.

Desain ulang kondisi tampilan

Kondisi tampilan dialog peringkat bintang diatur sebagai berikut:

  • Ditampilkan untuk setiap 100: Ditampilkan hanya untuk pengguna yang telah sepenuhnya menggunakan aplikasi
  • Interval hari minimum 14: Lebih dari 14 telah berlalu sejak terakhir kali ditampilkan
  • 30 tidak akan ditampilkan selama sehari setelah penutupan: Jika pengguna menutupnya, 30 tidak akan ditampilkan lagi selama sehari

Mengubah area ketuk ikon bintang

Apple Panduan Antarmuka Manusia merekomendasikan minimum 44x44pt untuk ukuran target sentuh. Area ketukan ikon bintang tidak memenuhi standar ini, jadi itu diperbaiki.

// 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
}

Perbaiki distorsi ikon bintang

Memperbaiki masalah di mana ikon bintang terdistorsi karena distribusi `.fillEqually` dari UIStackView. `contentMode` diatur ke `.scaleAspectFit`, dan distribusi StackView diubah menjadi `.equalSpacing`.

Aliran setelah evaluasi

  • 5 stars: Transisi ke layar ulasan App Store (`SKStoreReviewController`)
  • 4 bintang atau kurang: Diam-diam menampilkan bersulang "Terima kasih" dan mengakhiri. Jangan membawa ulasan negatif ke App Store.

7. Ketidakkonsistenan status tampilan riwayat telah diselesaikan

Saya melihat sebuah bug pada layar riwayat transmisi (Riwayat). Ada pesan bahwa statusnya tetap "Mengirim" meskipun transmisi telah selesai.

Sebagai hasil dari penyelidikan penyebabnya, ditemukan bahwa ada masalah dengan logika pembaruan status. Dalam implementasi sebelumnya, konten teks pesan digunakan sebagai kunci untuk memperbarui status. Namun, jika Anda mengirim catatan dengan konten yang sama lebih dari satu kali, pencarian berbasis teks tidak dapat mengidentifikasi pesan yang benar.

Koreksinya sederhana dan jelas. Mengubah pencarian berbasis teks menjadi pembaruan status berbasis ID. Setiap pesan diberi UUID saat menyimpan Outbox, jadi gunakan ID ini untuk memperbarui status Riwayat.

// 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()
}

Dengan koreksi ini, tidak peduli berapa kali Anda mengirim catatan dengan konten yang sama, status masing-masing sekarang ditampilkan dengan benar.

8. Obsesi Panoid dengan privasi

Informasi pribadi ditulis ke aplikasi memo. Kata sandi, kekhawatiran pribadi, ide bisnis - tidak mungkin untuk memprediksi apa yang akan ditulis pengguna. Itulah mengapa perlindungan privasi tidak "cukup" tetapi "berlebihan" tepat.

Lapisan privasi di pengalih aplikasi

Di pengalih aplikasi iOS, layar aplikasi ditampilkan sebagai thumbnail. Menampilkan lapisan privasi saat aplikasi bermigrasi ke latar belakang sehingga konten catatan tidak dapat dilihat dari layar pengalih aplikasi.

2026-09 Postscript: Perlindungan ini tidak diaktifkan di aplikasi saat ini (pemrosesan tampilan tetap ada di sumber, tetapi tidak dipanggil). Memo yang ditampilkan mungkin tetap berada di gambar pengalih aplikasi. Untuk informasi lebih lanjut, silakan lihat artikel tentang "Desain Privasi".

Sesi URL sementara

Sesi URL Normal menyimpan cache, cookie, dan informasi otentikasi di disk. Dengan menggunakan `URLSessionConfiguration.ephemeral`, kami memastikan bahwa informasi ini tidak ada di disk sama sekali. Ini untuk mencegah isi memo tetap berada di penyimpanan sebagai cache komunikasi.

Log tidak berbahau

Hati-hati tentang log debugging dalam pengembangan. " Proses yang tampaknya tidak berbahaya untuk "mengeluarkan isi dari memo yang dikirim ke log" dapat menjadi lubang keamanan. Kebijakan kami jelas:

  • Isi memo tidak dicatat dalam log meskipun itu adalah build DEBUG
  • Log kesalahan tidak menggunakan `localizedDescription`, hanya `type(of: error)` yang dicatat
  • Alasan: Karena `localizedDescription` mungkin berisi masukan pengguna
// NetworkManager.swift
// BAD: エラーの詳細をログに出力
// Logger.error("Send failed: \(error.localizedDescription)")

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

Ini mungkin terlihat paranoid. Namun, saya pikir tingkat paranoia ini diperlukan untuk mempercayai dan menggunakan aplikasi memo.

9. Dukungan bahasa 10: Saya ingin mengirimkannya ke pengguna Captio di seluruh dunia

Captio memiliki pengguna di seluruh dunia, terutama di negara-negara berbahasa Inggris. Untuk menyampaikan "SimpleMemo" kepada pengguna sebelumnya, bahasa 10 didukung sejak rilis pertama.

Bahasa yang didukung: Jepang, Inggris, Spanyol, Prancis, Jerman, Italia, Portugis, Korea, Cina (Sederhana), Arab.

2026-09 Postscript: Dalam notasi App Store saat ini, bahasa yang didukung adalah bahasa Jepang, Inggris, Spanyol, Prancis, Jerman, Portugis, Rusia, Cina (sederhana), Cina (tradisional), dan bahasa Arab 10. Bahasa Italia dan Korea dalam daftar di atas tidak termasuk dalam notasi saat ini.

Terutama Dukungan RTL Arab (Kanan-ke-Kiri) adalah tantangan teknis. Tidak hanya arah teks, tetapi juga seluruh tata letak UI perlu dicerminkan. Dengan secara konsisten menggunakan kendala `leading`/`trailing` dari Auto Layout, itu mewujudkan tata letak alami bahkan di lingkungan RTL.

Pengalihan bahasa dalam aplikasi

Selain peralihan bahasa dalam aplikasi setelah iOS 16, mekanisme peralihan bahasa yang unik juga diterapkan. Ketika bahasa dialihkan, ViewController diganti dengan transisi `cross-dissolve` untuk memberikan pengalaman peralihan yang lancar.

// 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. Pembelajaran hari ini: Produk adalah desain "rasa aman"

Setelah menyelesaikan Day1, yang saya pikirkan adalah bahwa esensi dari produk ini adalah desain "rasa aman".

Mari kita rangkum angka-angka teknisnya:

  • Waktu-ke-Teks: 500ms atau kurang (target)
  • Send-to-Reset: Singkatan yang tidak terasa menunggu (tujuan)
  • Tumpahan pesan: Cegah dengan pola Outbox
  • Enkripsi: AES-GCM (CryptoKit)
  • Ketergantungan perpustakaan eksternal: Nol

Namun, semua angka ini dirangkum menjadi satu tujuan "rasa aman".

Mulai cepat → "Anda dapat menulis segera kapan saja" ketenangan pikiran.
Menghilang saat Anda mengirimkannya → "Diproses" rasa aman.
Penanggulangan tumpahan → "Itu tidak akan hilang tanpa transmisi" ketenangan pikiran.
Enkripsi → "Tidak ada yang bisa melihat" ketenangan pikiran.

Captio dicintai bukan karena fungsinya yang luar biasa. Karena pengalaman "penggunaan dengan ketenangan pikiran" benar-benar dipoles. Saya ingin mengambil alih itu.

11. Lain kali (Day2) pratinjau

Day2 berencana untuk mengerjakan topik-topik berikut:

  • Kecanggihan animasi transmisi: Dari 0.25 detik saat ini memudar menjadi animasi yang lebih nyaman
  • Optimasi waktu pembaruan status Riwayat: Perbandingan pembaruan waktu nyata vs pembaruan batch
  • Apakah Anda benar-benar membutuhkan roti panggang yang sukses? : Pertimbangan opsi "jangan tampilkan" untuk umpan balik transmisi yang berhasil

Bertujuan untuk keanggunan "tidak ada" dari Captio, lanjutkan ke Day2.

Referensi