メモ帳アプリに iCloud 同期を入れる — 全言語ローカライズの罠

| 開発記録 | メモ帳

タグ: #iOS #iCloud #ローカライズ

メモを iPhone と iPad で同期したい、というのは最も多いリクエストでした。iCloud 同期そのものより、全言語の同期メッセージとキーワード更新のほうがしんどかった話です。

同期は技術より UX

iCloud 同期の技術的な実装は、NSUbiquitousKeyValueStore や CloudKit を使えば素直に書けます。本質的な難しさは「ユーザーが同期されているのか不安にならない UI」をどう作るかにあります。

同期インジケータの設計

メモ一覧の上部に、控えめな「☁️ 同期済み」「☁️ 同期中…」「☁️ 同期エラー」の 3 状態インジケータを置きました。エラー時はタップで詳細を表示します。

このインジケータの言葉ひとつとっても、対応する言語の数だけ翻訳が必要で、英語・日本語に始まり、ドイツ語、フランス語、イタリア語、スペイン語、韓国語、中国語(簡体・繁体)、ヒンディー語、オランダ語… と続きます。

写真削除時のクラッシュ

小さな設定値の同期には NSUbiquitousKeyValueStore を使っています。conflict は基本「last-write-wins」ですが、更新日時を一緒に格納し、外部更新通知が来たタイミングで自前で比較する形にしました。

final class MemoSettingsSync {
  private let store = NSUbiquitousKeyValueStore.default
  private let key = "memo.preferred_font_size"
  private let updatedAtKey = "memo.preferred_font_size.updatedAt"

  func save(fontSize: Double) {
    store.set(fontSize, forKey: key)
    store.set(Date().timeIntervalSince1970, forKey: updatedAtKey)
    store.synchronize()
  }

  @objc func onExternalChange(_ note: Notification) {
    let localTs = UserDefaults.standard.double(forKey: updatedAtKey)
    let cloudTs = store.double(forKey: updatedAtKey)
    guard cloudTs > localTs else { return } // 古いクラウド値は採用しない
    UserDefaults.standard.set(store.double(forKey: key), forKey: key)
    UserDefaults.standard.set(cloudTs, forKey: updatedAtKey)
  }
}

同期と並行して、メモに添付した写真の削除でクラッシュする報告が出ました。同期データ側の参照と、ローカル側の削除が競合する典型的なバグです。

対策として、写真削除は同期キューに乗せる前にローカル DB の整合性をチェックし、孤児参照を作らないようにしました。

ASO のキーワード更新

同期機能リリースに合わせて、App Store のキーワードも全言語で更新しました。「iCloud 同期」「クラウド」「クロスデバイス」など、検索される語を入れ込みます。

  • 各言語のキーワード欄は 100 文字
  • ロングテール(複数語の組み合わせ)を意識
  • 機械翻訳のままだと検索意図とずれることが多い

オランダ語のキーワードは 99/100 文字までギチギチに詰めました。1 文字でも惜しい。

アプリ説明文の刷新

同期機能の追加を機に、全 16 言語のアプリ説明文を「魅力的な構成」に書き換えました。

  1. 冒頭 1 行で価値提案
  2. 主要機能を箇条書き
  3. 利用シーン
  4. アプリ理念

このテンプレートに沿って全言語を整え、新機能には【New】マーカーを付けました。

教訓

  • 同期は機能より UI 表示が肝心
  • ローカライズは全言語まとめて手をつけないと、抜けが必ず出る
  • App Store キーワードはぎりぎりまで詰める

機能を実装する時間と、それを「全世界の言語で正しく伝える」時間は、感覚として 1:1 くらいです。