メモ帳アプリに iCloud 同期を入れる — 全言語ローカライズの罠
| 開発記録 | メモ帳
タグ: #iOS #iCloud #ローカライズ
「iPhone で書いたメモを iPad でも見たい」。いちばん多かったリクエストです。iCloud 同期そのものより、3 状態のインジケータと、全 16 言語の文言・キーワード更新のほうがしんどかったです。写真削除のクラッシュも一緒に踏みました。
同期は技術より UX
「iPhone で書いたメモを iPad でも見たい」。いちばん多いリクエストでした。
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 行で価値提案
- 主要機能を箇条書き
- 利用シーン
- アプリ理念
このテンプレートに沿って全言語を整え、新機能には【New】マーカーを付けました。
教訓
- 同期は機能より UI 表示が肝心
- ローカライズは全言語まとめて手をつける。抜けが必ず出る
- App Store キーワードはぎりぎりまで詰める
機能を実装する時間と、それを「全世界の言語で正しく伝える」時間は、感覚として 1:1 くらいです。作り終わってからが、たぶん本番です。