[Geliştirme Günlüğü Günü1] Captio'i o kadar çok seviyorum ki "o deneyimi" tekrar yapmak istedim

Bu sayfa Japonca aslından makine çevirisiyle hazırlanmıştır. Japonca aslını okuyun

Captio adlı uygulamayı biliyor musunuz?

Bir not yazın ve gönder düğmesine basın. E-posta adresinize bir not almak için ihtiyacınız olan tek şey bu. Başlattığınız anda klavye görüntülenecek ve yazarsanız gönderin ve gönderirseniz kaybolacaktır. Hiç gereksiz fonksiyon yoktur. Bu kadar büyük bir uygulamaydı.

Ama "hepsi bu kadar" inanılmaz derecede rahattı.

Banliyö treninde aklıma gelen fikirler, toplantı sırasında aniden aklıma gelen görevler ve yatmadan önce aklımdan geçen kelimeler - Captio hepsini hemen posta kutuma teslim etti. Not uygulamasını açma, klasör seçme ve başlık ekleme gibi bir güçlük yoktur. "Sıfır sürtünme" deneyimi, bir kez öğrendikten sonra bırakamayacağınız bir şeydi.

Captio hizmetini sonlandırdı.

Birkaç alternatif uygulama denedim, ancak hiçbiri bu deneyimden uzak değildi. Yavaş başlatma, kullanıcı arayüzünde gereksiz öğeler var ve iletimden sonra notlar kalıyor - önemsiz bir fark gibi görünebilir, ancak Captio tarafından yaratılan deneyimin özü bu tür "önemsiz şeylerin" birikimiydi.

Bu yüzden kendim yapmaya karar verdim. Captio'i tekrar deneyimleyin.

Bu, "SimpleMemo" Day1'in geliştirme günlüğüdür. Bu makalede, Captio deneyimini parçalarına ayıracağız ve modern bir teknoloji yığınında nasıl yeniden üretileceğine dair tüm teknik yargıları kaydedeceğiz.

1. Captio hakkında "ne" iyiydi - Deneyimi sökmeye çalıştım

Captio deneyimini sözlü olarak ifade etmek için onu 3'in iki unsuruna ayırmaya çalıştım.

Öğe 1: Anında Başlatmadan Girişe

Captio'i başlattığınızda klavye hemen görüntülenir ve metin girebilirsiniz. Ana ekran, açılış ekranından sonra görüntülenir ve oradan not oluşturma düğmesine basmak için bir prosedür yoktur. Başlangıç = Giriş. Buna "başlangıçtan metin girişine kadar süre" Metne-Süre diyoruz. Captio'in deneyimi 0.5 saniyeden azdı.

Öğe 2: Gönderdikten Sonra Anında Temizle (Gönderdikten Sonra Anında Temizle)

Gönder düğmesine bastığınızda ekran hemen temizlenecektir. Gönderiyorum..." İlerleme çubuğuna bakarken bekleyecek zamanım yok. Yazdığınız not kaybolduğu anda, "Ah, gönderebildim" güvenlik duygusuyla bir sonraki eyleme geçebilirsiniz. Buna "gönder düğmesine basmaktan kullanıcı arayüzünün temizlenmesine kadar olan süre" Send-to-Reset diyoruz. Amaç, beklediğinizi hissetmeden önce ekranı boşaltmaktır.

Öğe 3: Gereksiz Bir Şey Yok (Gereksiz Bir Şey Yok)

Klasör işlevi, etiket işlevi, zengin metin, markdown desteği - bunların hiçbiri Captio'de mevcut değildi. Yalnızca bir metin giriş alanı ve bir gönder düğmesi vardır. Bu düzgünlük bilişsel yükü sıfıra yaklaştırdı. Ne yapacağını düşünmek zorunda olmayan bir uygulama. Captio idi.

Bu 3 öğesi modern iOS geliştirmesinde yeniden üretilmiştir. "SimpleMemo"nin görevi budur.

2. Mimari: SwiftUI neden "değil" UIKit

2026'de iOS'in geliştirilmesinde, SwiftUI yerine UIKit'i seçmek sapkın görünebilir. Ancak, en öncelikli hedefimiz açıktı - Metin Zamanı 500ms veya daha az.

SwiftUI'nin `body` yeniden değerlendirmesi, `StateObject` başlatma`, `@Environment` çözümü - bu ek yükler normal uygulama geliştirmede göz ardı edilebilir, ancak "başlangıç girişi" elde etmek için göz ardı edilemezler. 1ms ile bile metin alanına hızlı bir şekilde odaklanmak istiyorum.

Orada benimsenen şey, Storyboard kullanmadan UIKit doğrudan yapımıdır.

SceneDelegate ile bir pencere oluşturun ve CompositeViewController'ı doğrudan rootViewController'a ayarlayın. Storyboard'un ayrıştırma işlemini bile atlayın.

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

ve ComposeViewController'ın `viewDidAppear` ile anında metin görünümüne odaklanın.

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

Yalnızca bu 2 koduyla, başlangıçtan klavye ekranına giden yol en kısa yoldur.

Hedef 500 ms veya altıdır. Daha sonra ekran kaydıyla yapılan ölçümde, 11/8/2026 tarihinde iPhone 16e üzerinde sıcak başlatmada, simgeye dokunmaktan klavyenin tamamen görünmesine kadar 0.4 saniye geçti. SwiftUI içinde `@FocusState` ve body yeniden değerlendirmesinin zamanlamasını hassas denetlemek zor olduğundan bu yolu hızlandırmanın güç olduğuna karar verdik. İki yaklaşımı aynı koşullarda ölçüp karşılaştırmış değiliz.

UIKit "eski" olabilir. Ancak, "hızlı". Bu 1 noktasında, UIKit hala en iyi seçimdir.

3. Tek dokunuşla "gönder ve kaybol"u gerçekleştirmek için tasarım

Captio'in "gönderildiğinde kaybolur" deneyimini yeniden üretmek için en önemli tasarım kararı vardır. iletim animasyonunu ve kullanıcı arayüzü temizlemesini ağ yanıtından tamamen ayırmaktır.

Yaygın uygulamaların iletim akışı aşağıdaki gibidir:

  1. gönder düğmesine basın
  2. Göster yükleme göstergesi
  3. Sunucudan yanıt bekliyor
  4. Başarılıysa kullanıcı arayüzünü temizle, başarısız olursa hatayı görüntüle

Bu akışta, ağ gecikmesi olduğu gibi kullanıcı arayüz gecikmesi haline gelir. Akışımız şöyle:

  1. gönder düğmesine basın
  2. hementemizle kullanıcı arayüzü (animasyon: 0.25 saniye)
  3. Arka planda ağ iletimi
  4. Başarılı → Outbox'den sessizce silin / Başarısız → Arka planda tekrar deneyin
// 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()
    }
}

Kullanıcılar için, gönder düğmesine bastığınız anda not "kaybolur" ve bir sonraki notu yazabilirsiniz. Ağın başarısı veya başarısızlığı, kullanıcının göremeyeceği bir yerde ele alınır.

"Ama ya iletim başarısız olursa?" - Bu sorunun cevabı bir sonraki bölümdeki Outbox mimarisidir.

4. Dökülmeyi önlemek için Outbox mimarisi

İletim kullanıcı arayüzünü ağ yanıtından ayırmak, "UI'nin temizlendiği, ancak e-posta iletiminin gerçekten başarısız olduğu" bir durumun ortaya çıkabileceği anlamına gelir. Bu kesinlikle kabul edilemez. Kullanıcının "gönderildiğini" düşündüğü not aslında gönderilmedi - bu, not uygulaması için ölümcül bir güven kaybıdır.

Orada benimsenen şey Outbox deseni.

Mesaj iletim akışı aşağıdaki gibidir:

  1. Gönder düğmesine dokunun: AES-GCM mesajını şifreleyin ve Outbox'e kaydedin (yerel depolama)
  2. UI clear: performSendAnimation ile ekranı anında temizleyin
  3. Arka plan iletimi: Outbox'den bir mesaj çıkarın ve Relay API'e gönderin
  4. Başarı: Outbox'den gelen mesajı silin
  5. Başarısızlık: İletimi beklerken bırakın ve daha sonra tekrar gönderin
  6. Çevrimdışı: NWPathMonitor ile bağlantıyı izleyin ve geri döndüğünüzde otomatik olarak yeniden gönderin

Önemli olan, ağı göndermeden önce Outbox'e kaydetmek. Sonuç olarak, uygulama aniden çökse veya terminal gücü kesilse bile, Outbox'e yazılan mesaj kalacaktır.

Outbox'de depolanan mesajlar, CryptoKit kullanılarak AES-GCM şifrelemesi ile korunur. Şifreleme anahtarı Anahtarlıkta saklanır ve uygulamanın sanal alanının dışından erişilemez.

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

Ayrıca, her şeyi kendi başına uyguladığı için harici kitaplığa bağımlılık sıfırdır. CryptoKit, NWPathMonitor, URLSession - hepsi orijinal Apple çerçevesiyle tamamlandı. Üçüncü taraf kütüphane sürüm kontrolü ve güvenlik denetiminden uzak olmak, küçük ekipler için büyük bir avantajdır.

5. Röle API: Posta kutusunda OAuth işbirliğine güvenmeden gönderme

Captio'in iletim mekanizması resmi olarak açıklanmamıştır. Geliştiricinin sonlandırma duyurusunun nedeni, artık uygulamanın kararlılığını ve güvenliğini korumak için zaman harcamaya devam edememesi ve iletim yönteminin bir neden olarak gösterilmemesidir. Bununla birlikte, genel olarak, harici e-posta API'e güvenerek gönderim, sağlayıcının gereksinimlerindeki ve erişim kısıtlamalarındaki değişikliklerden kolayca etkilenir.

Cloudflare Workers + Resend API yapılandırmasını seçtik.

Cloudflare Workers, dünyanın dört bir yanındaki kullanıcılara en yakın veri merkezinde kod yürüten bir uç bilgi işlem platformudur. Japonya'dan gelen talepler Japonya'nın kenarında işlenir ve Amerika Birleşik Devletleri'nden gelen talepler Amerika Birleşik Devletleri'nin kenarında işlenir. Bu, geleneksel sunucusuz (AWS Lambda, vb.) ile karşılaştırıldığında soğuk başlatma sorununu neredeyse ortadan kaldırır.

E-posta göndermek için Resend API'i kullanın. Resend, geliştiriciler için bir e-posta API'tür.

Posta kimlik doğrulama akışı

Kullanıcı hedef e-posta adresini ayarladığında, e-posta 6 haneli doğrulama koduyla doğrulanır. Bu, üçüncü taraf e-posta adreslerine yetkisiz gönderimi önler.

Çok katmanlı hız sınırı

Yetkisiz kullanımı önlemek için çok katmanlı oran kısıtlamaları uygulanır.

// 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 katmanına cihaz birimi, IP birimi ve genel birime göre hız limitleri uygulayarak, tek bir cihazda kaçak ve botnet saldırılarını engelleyebilirsiniz.

2026-09 güncellemesi: Cihaz başına sınır ve toplam 1 günlük sınır, 6/2026 tarihinde kaldırıldı. Güncel gönderim sınırı, IP adresi başına 1 saatlik kotadır.

Güç eşitliği garantisi

Ağ istikrarsızlığı nedeniyle yinelenen iletimi önlemek için her mesaja bir UUID atayın ve Relay API tarafında bir yinelenen kontrol gerçekleştirin. Aynı UUID mesajı yeniden gönderilirse, 2 süresinden sonra bir e-posta göndermeden başarılı bir yanıt gönderir. Bu, Outbox modelinin yeniden deneme mekanizmasıyla birleştirildiğinde kullanıcının aynı notu iki kez almasını önler.

2026-10 güncellemesi: Güncel gönderim sunucusu yinelenen istekleri kendisi denetlemez. İleti kimliği, Resend isteğine Idempotency-Key olarak eklenir. Resend aynı anahtarı 24 saat hatırlar ve aynı içerikli isteğe önceki yanıtı döndürür. Böylece aynı not 24 saat içinde tekrar gönderilmez. Ayrıntılar için Relay API tasarımına bakın.

6. Bugün yaptığım şey: Küçük rahatsızlık hissini tek tek silin

Day1'in uygulama çalışmalarının çoğu yıldız derecelendirme iletişim kutusunu gözden geçirmek için harcandı. App Store'in değerlendirmesi uygulamanın can damarıdır, ancak kullanıcı deneyimine zarar verecek şekilde değerlendirmeye çalışmayın.

Ekran koşullarının yeniden tasarlanması

Yıldız derecelendirme iletişim kutusunun görüntüleme koşulları şu şekilde ayarlanmıştır:

  • Her 100 görüntülenir: Yalnızca uygulamayı yeterince kullanan kullanıcılara gösterilir
  • Minimum 14 gün aralığı: En son görüntülendiğinden bu yana 14'den fazla geçti
  • Kapatıldıktan sonra 30 gün göstermeme: Kullanıcı kapatırsa 30 gün boyunca yeniden gösterilmez

Yıldız simgesinin dokunuş alanını değiştirin

Apple İnsan Arayüzü Yönergeleri, minimum dokunma hedef boyutunun 44x44pt olmasını önerir. Yıldız simgesinin dokunma alanı bu standardı karşılamıyordu, bu yüzden düzeltildi.

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

Yıldız simgesi bozulma düzeltme

UIStackView'ın `.fillEqually` dağıtımı nedeniyle yıldız simgesinin bozulması sorunu düzeltildi. `contentMode` `.scaleAspectFit` olarak ayarlandı ve StackView'un dağıtımı `.equalSpacing` olarak değiştirildi.

Değerlendirme sonrası akış

  • 5 yıldız: App Store'in inceleme ekranına geçiş (`SKStoreReviewController`)
  • 4 yıldız ve altı: Yalnızca kısa bir “Teşekkürler” bildirimi gösterilip işlem sonlandırılır. Olumsuz değerlendirmeler App Store sayfasına yönlendirilmez.

7. Geçmiş görüntüleme durumu tutarsızlığı çözüldü

İletim geçmişi ekranında bir hata fark ettim (Geçmiş). İletim tamamlanmış olmasına rağmen durumun "Gönderiliyor" olarak kaldığına dair bir mesaj vardı.

Nedenin araştırılması sonucunda durum güncelleme mantığıyla ilgili bir sorun olduğu tespit edildi. Önceki uygulamalarda, mesajın metin içeriği durumu güncellemek için bir anahtar olarak kullanıldı. Ancak, aynı içeriğe sahip bir notu birden fazla kez gönderirseniz, metin tabanlı bir arama doğru mesajı belirleyemez.

Düzeltme basit ve netti. Metin tabanlı arama Kimlik tabanlı durum güncellemesi olarak değiştirildi. Outbox kaydedilirken her mesaja bir UUID atanır, bu nedenle Geçmiş'in durumunu güncellemek için bu kimliği kullanın.

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

Bu düzeltmeyle, aynı içeriğe sahip bir notu kaç kez gönderirseniz gönderin, her durum artık doğru şekilde görüntülenir.

8. Gizlilikle ilgili panoid takıntısı

Özel bilgiler not uygulamasına yazılır. Şifreler, kişisel endişeler, iş fikirleri - kullanıcıların ne yazacağını tahmin etmek imkansızdır. Bu nedenle gizlilik koruması "yeterli" değil, ancak "aşırı" tam olarak doğru.

App switcher'da gizlilik katmanı

iOS uygulama değiştiricide uygulama ekranı küçük resim olarak görüntülenir. Uygulama arka plana geçtiğinde bir gizlilik katmanı görüntüler, böylece notun içeriği uygulama değiştirici ekranından görülemez.

2026-09 güncellemesi: Bu koruma güncel uygulamada etkin değildir. Görüntüleme kodu kaynakta vardır, ancak çağrılmamaktadır. Açık not, uygulama değiştiricinin görüntüsünde kalabilir. Ayrıntılar için “Gizlilik tasarımı” makalesine bakın.

Geçici URLOturumu

Normal URL Oturumu önbelleği, çerezleri ve kimlik doğrulama bilgilerini diskte depolar. `URLSessionConfiguration.ephemeral` kullanarak, bu bilgilerin diskte hiç kalmadığından emin oluruz. Bu, notun içeriğinin bir iletişim önbelleği olarak depoda kalmasını önlemek içindir.

Zararsız günlükler

Geliştirme aşamasındaki hata ayıklama günlüklerinin dikkatli olması gerekir. " "Gönderilen notun içeriğini günlüğüne çıkarma" gibi görünüşte zararsız süreç bir güvenlik açığı olabilir. Politikamız açıktır:

  • Bir DEBUG derlemesi olsa bile, günlüğüne hiç kaydedilmeyecektir
  • Hata günlüğü `localizedDescription` kullanmaz, yalnızca `type(of: error)` kaydedilir
  • Sebep: Çünkü `localizedDescription` kullanıcı girdisi içerebilir
// NetworkManager.swift
// BAD: エラーの詳細をログに出力
// Logger.error("Send failed: \(error.localizedDescription)")

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

Paranoyak görünebilir. Ancak, memo uygulamasına güvenmek ve kullanmak için bu derecede paranoyanın gerekli olduğunu düşünüyorum.

9. 10 dil desteği: Dünyanın dört bir yanındaki Captio kullanıcılarına teslim etmek istiyorum

Captio'in başta İngilizce konuşulan ülkelerde olmak üzere tüm dünyada kullanıcıları vardı. Eski kullanıcılara "SimpleMemo"i iletmek için ilk sürümden itibaren 10 dili desteklendi.

Desteklenen diller: Japonca, İngilizce, İspanyolca, Fransızca, Almanca, İtalyanca, Portekizce, Korece, Çince (basitleştirilmiş), Arapça.

2026-09 güncellemesi: Güncel App Store açıklamasında desteklenen 10 dil şunlardır: Japonca, İngilizce, İspanyolca, Fransızca, Almanca, Portekizce, Rusça, Basitleştirilmiş Çince, Geleneksel Çince ve Arapça. Yukarıdaki listede yer alan İtalyanca ve Korece, güncel açıklamada bulunmamaktadır.

Özellikle Arapça RTL (Sağdan Sola) desteği teknik bir zorluktu. Sadece metnin yönü değil, aynı zamanda tüm kullanıcı arayüzü düzeninin de yansıtılması gerekir. Otomatik Düzenin `öncü`/`izleyen` kısıtlamasını tutarlı bir şekilde kullanarak, bir RTL ortamında bile doğal bir düzen gerçekleştirir.

uygulama içi dil değiştirme

iOS 16'den sonra uygulama içi dil değiştirmeye ek olarak, kendi dil değiştirme mekanizmasını da uygular. Dil değiştirildiğinde, sorunsuz bir geçiş deneyimi sağlamak için ViewController `cross-dissolve` geçişi ile değiştirilir.

// 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. Bugünün öğrenmesi: Ürün bir "güvenlik duygusu" tasarımıdır

Day1'i bitirdikten sonra düşündüğüm şey, bu ürünün özünün bir "güvenlik duygusu" tasarımı olduğudur.

Teknik sayıları özetleyelim:

  • Mesaje için süre: 500ms veya daha az (hedef)
  • Send-to-Reset: Bekleme hissi vermeyen kısalık (hedef)
  • Mesaj dökülmesi: Outbox deseniyle önleyin
  • Şifreleme: AES-GCM (CryptoKit)
  • Harici kitaplık bağımlılığı: sıfır

Ancak, bu sayıların tümü "güvenlik duygusu" adı verilen tek bir hedefte özetlenmiştir.

Hızlı başlangıç → "İstediğiniz zaman hemen yazabilirsiniz" gönül rahatlığı.
Gönderdiğinizde Kaybolur → "İşlenmiş" güvenlik duygusu.
Dökülme karşı önlemleri → "Bulaşma olmadan kaybolmayacak" gönül rahatlığı.
Şifreleme → "Kimse göremez" gönül rahatlığı.

Captio yalnızca özellikleri iyi olduğu için sevilmedi. Güvenle kullanılabilen bir deneyim titizlikle geliştirilmişti. Sürdürmek istediğimiz anlayış bu.

11. Bir dahaki sefere (Day2) önizleme

Day2 aşağıdaki konularda çalışacaktır:

  • İletim animasyonunun sofistikeliği: Mevcut 0.25 saniyelik solmadan daha rahat bir animasyona
  • Geçmiş durum güncellemelerinin zamanlama optimizasyonu: Gerçek zamanlı güncellemelerin toplu güncellemelerle karşılaştırılması
  • Başarı bildirimi gerçekten gerekli mi? Gönderimin başarılı olduğuna ilişkin geri bildirimde, hiç bildirim göstermeme seçeneğini değerlendirme

Captio'in "hiçbir şey" bütünlüğünü hedefleyerek, Day2'e kadar devam edin.

Kaynaklar