Wowgineer Notes

WowTechエンジニアの徒然開発日記

Wowgineer Notes

〜新たな"Wow"を生み出す〜

iOS版WowTalkアプリへWidgetを追加した時の学びをまとめておく

明けましておめでとうございます。ワウテックアプリ開発チームの岡野です。

久しぶりの投稿となりましたが、今回は今度リリース予定のiOS11.9.1で利用可能になったwidget機能について、いくつかでた課題とその対応方法について後学のため整理しておきたいと思います。

先日Androidのwidget機能についてもまとめておりますので、そちらもご確認いただければと思います。

engineer.wowtech.co.jp

Widget機能について

Widget機能はiOS14から搭載された機能で、ホーム画面にアプリの特定の機能のショートカットを追加する機能です。お天気アプリやニュースアプリ、株などのWidgetを使ってる方も多いのではないでしょうか。僕自身もカレンダーアプリや天気アプリやバッテリー残量を表示するWidgetは日頃使っています。

WowTalkのWidget機能


iOS版WowTalkの11.9.1から、日頃連絡を取るメンバーのチャットルームへ遷移するためのWidgetを実装しました。

デフォルトで現在ログインしている自分のアカウントが選ばれた状態になってますが、編集メニューからメンバーリストにいる他のメンバーを選択することも可能です。Widgetは複数登録することもできるので、ホーム画面から簡単にメンバーに連絡をとることができるようになります。

また今回はシンプルにこの機能だけですが、今後は通知バッジを表示したり共有、タスク、日報など各種機能の情報を表示するなども検討できればと思います。

Widget機能実装で困ったこと

ここからは今回WowTalkでWidget機能を実装していく上で生じた課題と対応方法について共有したいと思います。

既存のクラスが使えない

問題

現在のWowTalkのクラス間の関係は複雑になっており、様々なクラスから参照されるユーティリティクラスが存在します。またこのクラスが逆に他のモデルクラスも参照することから内部的には不要な参照を含むクラス間の関係性が形成されています。

これは今後解消を要する課題であると同時に、今回Widgetを実装する上での大きな課題となりました。

例えば、WidgetExtensionのTargetでWowTalk内のメンバーを表すBuddyクラスを利用しようとしたとき、XcodeからBuddy.mのTargetMembershipにWidgetExtensionを追加する必要があります。

このときTargetMembershipに追加する必要のあるクラスはBuddyを取得するためのDatabase、およびBuddyが参照してるクラスも含む必要があります。しかしながらDatabaseやBuddyから参照してるユーティリティクラス(Utility.h)が今度は別の多くのクラスを参照している状況から、本来は不要なクラスを全部Target Membershipに追加する必要が出てきてしまいました。

「だったら全部追加すればいいだろう」と言う人もいるかもしれませんがその数が膨大だったため追加漏れがないかビルドの都度チェックしたり、ARC(自動でメモリを解放する仕組み)を利用していない古いObjective-Cファイルを追加したときに発生したコンパイルフラグの互換性エラーを解消する必要が出てきたりと、過剰な労力が発生したため既存のクラスの利用は避けることにしました。

複雑に絡み合うカルマ

対応

上記の理由からBuddyを利用することは諦め、新しくWidgetで表示するメンバー情報のみを管理するBuddyWrapperクラスを作りました。他のクラスとの参照関係を最低限にすることでWidgetExtensionがビルド時にCompileする必要があるクラスを低減しました。

また他の利点として、本来Buddyクラスで保持していたプロパティの中にはWidgetでは使わないものもあり、その分のコードを削除することでクラスとしてもスッキリすることができました。

また他にも部門情報を保存するGroupRoomInfoの代わりにDepartmentInfoWrapperクラスを、UserDefaultへの値の設定や取得をするクラスの代わりにGroupDefaultクラスを作成するなど、Widgetに必要な最低限の依存関係で構築されたクラスを作成・利用することでWidgetExtension内部のクラス関係をスッキリさせることができました。

クラス間の依存関係を必要最低限にする

Widgetの表示がグレーアウトする

問題

Widgetがグレーアウトして閲覧・編集ができない

Widgetの開発を進めている途中で、ビルドしたWidgetがグレーアウトする現象が発生するようになりました。この問題は自分で開発をしている時には気づかなかったのですが、テスターから指摘を受けWowTalkアプリケーションの再インストールを行ったり検証で使っていた端末以外の端末などで再現することがわかり、何が原因なのかがなかなかわからない問題の1つでした。

対応

特定のcommitから問題が起きていることはわかったので、一度問題が起きていない時点のcommitからブランチを作り、調査を進めたところ、Widgetに表示する文言の多言語対応(localize)を行った際にIntentdefinitionファイルをBaseではなくEnglishにしてしまっていたことが原因でした。

正しい状態。右側のLocalizationを見るとintentdefinitionの設定がBaseになっており、サポートする言語にチェックが入る。

間違った状態。Baseにチェックがない。

Base(もしくはBase .lproj)とは、StoryboardやXibのファイルを多言語対応させる際に生成されるもので、大昔の書き方ではStoryboard内のテキストを多言語対応する際はlocalizableの設定ファイル(上のプロジェクト画像左のproject ツリー上でオレンジのアイコンで表されるもの)ではなくStoryBoardやXibファイルごとに設定ファイルを定義する必要がありました。

Widget Extensionとは異なり、iOSプロジェクトではProject > info > Use Base Internationaliztionを有効にすることでStoryBoardやXibファイルと言語設定ファイルを切り離すことができるようになりましたが、intentdefinitionなどにはこの設定がないため、従来通りBaseにチェックを入れて多言語対応をする必要があります。(参考リンク参照)

Note

Xcode adds the Base and the development language to the localization table by default. Use the Base localization for resources that support string substitution at runtime, such as storyboard, XIB, and Siri intent definition files.

データの共有方法

問題

WowTalkアプリケーションではアプリ内で利用するユーザのデータはローカルのDBに保存されていますが、Widget ExtensionからはこのDBファイルに直接アクセスすることはできず、Widgetの設定画面で利用したいユーザの情報を本体アプリのターゲットから直接共有できませんでした。

対応

iOSのapp Groupsの仕組みを利用し、アプリケーションがバックグランドになるタイミングで、DBファイルをapp Groups用のディレクトリにコピーし、Widget ExtensionからコピーされたDBファイルを参照、データを取得することで情報の共有を行いました。

この手法の利用シーンは幅広く、NotificationServiceなどの Extensionでも本体アプリのデータを利用したい時などに利用できます。

またDB以外にもWidgetに表示するユーザのアイコンなども同様の仕組みを利用して本体と同期できるようにしています。

@implementation Database(AppGroup)
//アプリがバックグランドに切り替わる時に呼ばれる処理
+(void)copyDBFile {
    //copyされるDBファイルはユーザごとの切り分けれるようユーザ固有のIDにする
    NSString* wowtalkid = [[NSUserDefaults standardUserDefaults] objectForKey:@"wowtalk_id"];
    NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                                       NSUserDomainMask,
                                                                       YES);
    NSString *directory = [documentDirectories objectAtIndex:0];
    NSString * dbPath = [directory stringByAppendingPathComponent:[NSString stringWithFormat:@".%@",wowtalkid]];
    NSFileManager *manager = [NSFileManager defaultManager];
    if ([manager fileExistsAtPath:dbPath]) {
    //同一のAppGroupsに設定されたProject, Extensionで共有可能なディレクトリパスを取得
        NSURL *url = [manager containerURLForSecurityApplicationGroupIdentifier:@"//app groupsのIDを設定"];
        NSData *db = [manager contentsAtPath:dbPath];
        NSString *path = [url.path stringByAppendingPathComponent:[NSString stringWithFormat:@".%@", wowtalkid]];
        [db writeToFile:path atomically:YES];
        NSDictionary *attr = [manager attributesOfItemAtPath:url.path error:nil];
        
        NSLog(@"copied db size :%llu",attr.fileSize);
    }
}

@end

まとめ

いかがだったでしょうか。

1つ目の「既存クラスが使えない」という問題は当社特有の問題といえばそうですが、どこのサービスでも起こりうる問題だと思います。特に歴史が長いサービスなどで構成の見直しや技術負債の精算が行われていない状況では起きやすいのかなとも思いますので回避するテクニックの1つとして参考になれば幸いです。

また2つ目の「Widgetがグレーアウトする問題」というのも、「iOS14の初期で起きてる問題だからOSをアップデートしろ」や「端末を再起動してみろ」といった回答は見かけるのですが具体的な原因となりうるものについて言及した記事は少ないので同じ問題を抱えてる方の一助になれば幸いです。

今回のWidget開発を通じて改めてiOS開発の奥深さを感じましたが、今後はユーザの声を反映してWidgetをより便利にしていければと思います。

参考

Adding support for languages and regions
https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions

Internationalizing the User Interface https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/InternationalizingYourUserInterface/InternationalizingYourUserInterface.html#//apple_ref/doc/uid/10000171i-CH3-SW2

iOSアプリの多言語対応について
https://techblog.zozo.com/entry/ios-localization

WowTechは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain