【参加レポ #linedevday 】LINEのiOS対応、新しい技術チャレンジ 〜LINEの Apple Watch アプリ開発とSwift導入による開発チームの変化〜@LINE DEVELOPER_DAY 2015
4/28に開催されたLINEのエンジニアチームの様々な経験を、未解決の課題も含めて共有する技術カンファレンス、
に参加してきたのでその時のまとめ。
AppleWatch対応とswift対応回です。
他のセッションは以下からどうぞ
LINEの Apple Watch アプリ開発
クリストファー・ロジャース様
- AppleWatchについて
- 通知のUIを実装できるようになったので画像やスタンプを見せれるようになった
- GlanceはNotification centerのウィゼットに近い感じ
- storyboardや画像はwatchのほうでつかわれるのでBundleの方に格納します
ハマったこと
通知のアクション
- アプリのアイコンが左上にあるが、それをおせばiPhoneで開く。
- 通知センターでは通知内容によって動作を変えられるが、ウォッチでは動作を変えることはできない
- 開く動作を変えることはできない
- ボタンを使うにはNotificationActionというAPIを使うが、iPhoneにも表示される
- どういうボタンを置くかは検討が必要
- iPhonetとウォッチでどっちもアクションを実装しないといけない
返信画面
- WatchKitはすべてのviewを予め作っておく必要がある。
- しかし絵文字などは画像が多すぎて描画にすごい時間がかかってしまう
- 解決策1:画像の読み込みを描画するたびに読み込むと4秒くらいかかってしまうので、画像ファイルを予めWatchのストレージにコピーしておく
- 画像ファイルが最初から用意されてればWatchKitAppにいれておいて、ImageViewのsetImageViewで名前を指定して表示する
- 頻繁に使用される画像なら、デバイスのイメージキャッシュに追加するのをおすすめ
- すべて表示すると時間がかかるので、分割して表示して非同期で読み込むようにする
親アプリとのデータ共有・同期について
- 新着メッセージを取得するのを最新のサーバからとってくる必要がある
- サーバからメッセージをとるようにすると、通信の無駄遣い、レスポンスが遅くなったりする
- 無駄をなくして処理を最適化するには、リクエストは複数同時に送る
- 取得したメッセージは必ず保存
- リクエストは前のメッセージを送り終わってから送る
- 方法1:バックグラウンドで更新する
- iOSの都合のいい時に起こすpullとpushでアプリを開く方式の2つある
- content-available: 1をおくるともしかしたらpush通知で動く かも しれない(起動する保証はない)ので他に対策を取らないといけない
- 方法2:Extensionで更新する
- extenstionバイナリで更新するときはメッセージ取得APIを複数同時利用できないのでプロセス間の排他制御が必要になる
- 方法3:openParentApplicationで更新する
- 親アプリにメッセージを送る
- プロパティリストに対応したdictionaryをやりとりできる
- 注意点
- 親アプリを起動してからリクエストをうけとることになるで起動処理を最適化しないといけない
- openParentApplicationにはタイムアウトが実装されているらしく、親アプリのレスポンスが遅いとタイムアウトするがエラーで帰ってこないので、呼び出し側でタイムアウトタイマーを実装しないと認識できない
- 新着メッセージの取得は実際に使った方法はopenParentApplication
- サーバからデータを取る処理は実装が簡単に出来たので親アプリに取得は任せた
- スタンプ画像のDLはextenstionで行っていたり、通知での画像DLはopenParentApplicationを使ったりしている
- 今後はopenParentApplicationに頼らずextensionだけでやれるようにしたい
- データ共有はいくつか方法を検討している
- 同期する際のファイルロック方法はファイルロックを使ったりしている
- NSFileCoodinatorとNSFilePresenterというのがあるが、親アプリがDeadlockを起こす致命的なバグがあるので治るまで使わない方がいい
- データが更新されたかどうかを知るにはプロセス間の通知を使う
- 通知方法1:CFNotificationを使ったDarwinNotificationがある
- userInfoを送らないものになる
- 通知方法2:CFMessagePortというAPIを使う
- CSデータをリクエストレスポントでソートできる。
- 親アプリと共通の名前を持ったメッセージポートを使ってメッセージのやり取りをする
- メッセージポートの前にAppGroup名をつければAppGroup間で共有できる。
- AppGroup名つけないとポート作成に失敗する
- WFNotificationCenterがおすすめ
- kqueueやCGD Change Notificationを使用すればFileCoodinatorNotifivationを受け取れるので、データを最新の状態に保てる
- しかし、寝ている親アプリのプロセスをおこすことができない
まとめ
- 通知アクションの追加はiPhoneアプリの影響を考えて追加
- 頻繁に使う画像はAppleWatchストレージにコピーする
- 親アプリとデータ共有する方法で、openParentAppllicationとapp groupなどは適切に使う
Swift導入による開発チームの変化
イシカワヨウスケ様
- 大規模なアプリ+複雑なイベント処理+Obj-c
- しかし想定外の実行時エラーに悩む
- なぜかアプリクラッシュ
- なぜか画面にnull
- なぜか画面真っ白・・・
- アプリの規模とプログラマーの数に比例して想定外の実行時エラーなどは増えていく
- -> それをSwiftが解決しようとしている
- swift使うことで、分担のしやすさ、デバッグしやすさ、コードレビューしやすいなどができた
Objective-Cの場合
- APIClientで受け取った値からItemというオブジェクトを作る以下のコードにはnilの可能性がある
1 2 3 4 5 6 7 8 |
@interface Item : NSObject @property (nonatomic, readonly) NSString *identifier; // maybe nil? @property (nonatomic, readonly) NSString *name; // maybe nil? - (instancetype)initWithDictionary:(NSDictionary *)dictionary; // maybe nil? @end |
上記のコードを使った場合NSArrayの追加や、NSDictionaryの追加でクラッシュしたりするかも
1 2 3 4 5 |
NSMutableArray *items = [NSMutableArray new]; for (NSDictionary *dictionary in items) { Item *item = [[Item alloc] initWithDictionary: dictionary]; [items addObject:item]; // <- crash!! } |
ViewControllerがnilを扱っているかどうかは実行時まで発覚できない
Swiftの場合
1 2 3 4 5 6 7 8 |
class Item: NSObject { let identifier: String let name: String init?(dictionary: Dictionary) { ... } } |
- swiftの型はnilを許容しません!!
- initに?をつければidとnameにnilじゃないというのを満たさなければnilを返す
- つまり、初期化に失敗するnilを返すようになる
- obj-cでは3箇所nilの可能性があったがswiftでは1箇所になる
ApiClientで発生したエラーがViewControllerにわたらなくなる
- エラーの範囲が限定されると分業しやすくデバッグしやすくなる
- 関係する開発者の数も減らせる
- コツとしてはコンパイル時に保証される範囲を広げるのと、想定外のエラーの影響範囲を狭める
- swiftでもプロパティに!つけると実行時にエラーになり、objective-cの時と一緒になってしまいます
- swiftでは設計の良し悪しがインターフェースに出やすくなる
- あたりもつけやすくなるのでコードレビューもしやすい
- まとめ
- 分担しやすい
- デバッグしやすい
- コードレビュー簡単
- 安全コードが書かれやすい
所感
- watchの表示は工夫しないと結構大変そう
- swiftを導入するとクラッシュ率も減るかもしれない!
- 大先輩いわく、今まで!を使うとクラッシュの可能性があるので出来る限り使わないようにしていたが、クラスのプロパティのletに関してだけなら!を使ってもいいかもと言っていた。なぜならinitのところにクラッシュする問題を特定できるため。initで頑張れ!とのこと。