見出し画像

未経験でも開発できる!iPhoneアプリのApple Watch用アプリの開発

🎄この記事はNAVITIME JAPAN Advent Calendar 2023の9日目の記事です。


こんにちは、KTANです。
ナビタイムジャパンでiOS/Androidアプリ開発を担当しています。

今月、iOS版『乗換NAVITIME』のApple Watchアプリがリリースされました。このアプリの開発者として、先月まで自分が開発を行っていました。
iOS版『乗換NAVITIME』には元々Apple Watch連携はなく、自分もApple Watchアプリ開発の経験はなかったため、完全に新規の機能となり、全てがゼロからの開発となりました。

この記事は、そこで得られた知見についてまとめています。
自分と同じようにApple Watch開発が未経験であり、iOSアプリにApple Watchアプリの新規導入を検討している方々に向けて書きました。参考になりましたら幸いです。


開発したアプリの機能

今回開発したApple Watchアプリは、iPhoneアプリで最後に検索したルートをApple Watchで確認できるというものです。
これによって、移動中など気軽にiPhoneを見られない状態でルートを再度確認する際に、iPhoneを起動せずにApple Watchで見ることができるようになりました。

また、ルートの中の停車駅と停車時刻や、その時刻に基づいた自分の場所も緑の矢印として表示される(有料機能)ため、常に自分が検索したルート通りに移動できているかどうかも、確認できるようになっています。

ルート表示
停車駅と現在地の表示

また、Apple Watchアプリを起動せずにApple Watchの文字盤上で常に情報を表示できるコンプリケーション(iPhoneアプリにおけるウィジェットのようなもの)にも対応しており、より手軽にルートが確認できるようになっています。

コンプリケーション

Apple Watch でできること

未経験でApple Watchアプリを開発しようと思った際に一番気になることは、そもそもApple Watchでは何ができて何ができないのか、だと思います。これが分からないと、作りたい機能が実現できるかどうか分からず、Apple Watchアプリの導入の検討ができません。
そして、これを理解するにはApple WatchとiPhoneの間のアプリ連携の仕組みを知る必要があります。

Apple WatchとiPhoneの間の連携の仕組み

Apple WatchとiPhoneそれぞれのアプリの間で連携するには、文字列の送受信によって行います。送信を行うメソッド(sendMessage)と受信を行うデリゲート(WCSessionDelegate)がそれぞれ用意されており、Apple Watchアプリ、iPhoneアプリどちらでも利用できます(公式ドキュメントはこちら)。
これらを利用することで、Apple Watchアプリ側から自由にiPhoneアプリ側の処理を呼び出すことができます。

例えば、ルート検索結果をApple Watchアプリで表示する場合、下記のようなイメージとなります。

連携のイメージ

つまり、ロジック部分は完全にiPhone側に任せることができ、Apple WatchはViewの表示のみ行うように役割分担することができます。また、このメッセージのやり取りはアプリがタスク終了されている場合でも関係なく受信できるため、iPhoneとApple Watchが接続状態である限り、いつでも可能となります。
よって、処理を移管することでiPhoneで実現可能な処理はApple Watchでも可能ということが分かります。

注意点

この方法は、すでにiPhoneアプリで実装している機能を、新規でApple Watchアプリにも少ないコストで実現したいという時に有効であって、ベストプラクティスではないという点に注意して下さい。
このアプリ間のメッセージ送信には当然時間もかかりますし、iPhoneとApple Watchの接続が切れていたり、何らかの理由で送信エラーになってしまった場合は、その機能が使えなくなってしまう、といったケースの考慮が常に必要となります。
ユーザーにどんな状態でも遅延なく情報を提供したい場合は、Apple Watchアプリ側に処理を任せたほうが良いという場合もあります。

Apple Watch開発における最近の変更点

Apple Watchアプリの開発事情はXcodeやWatch OSのバージョン更新によって常にアップデートされており、Apple Watch関連の記事を検索しても、その内容が古く参考にならないといったことが多々あったため、最近の変更点(※ 2023年12月9日時点)についてもいくつか明記しておきます。

WatchKit AppとWatchKit Extentionが1つに

以前のApple Watchアプリの開発では、画面のViewの構成を行うWatchKit Appと、内部ロジックを担当するWatchKit ExtentionでTARGETが分かれていました。そのため、証明書なども別々に用意する必要があり、更新が非常に手間でした。
Xcode 14からは、SwiftUIが導入され、1つのTARGETでViewの構成とロジックの両方を定義することができるようになり、必要な証明書も1つだけとなりました。

コンプリケーションをWidgetKitで開発可能に

以前はコンプリケーションにClockKitを利用していましたが、WatchOS9からは、iPhoneアプリのウィジェット開発と同じWidgetKitが使えるようになりました。これにより、iPhoneアプリで開発していたウィジェットをApple Watchのコンプリケーションとしてそのまま使い回したり、ウィジェットの開発経験がある人なら少ない学習コストでApple Watchのコンプリケーションを開発できるようになっています。
もちろん、iPhoneとApple Watchでは画面サイズが大きく異なるため、ウィジェットと違ってコンプリケーションではテキスト1行やアイコン画像しか表示できないといった制限がある場合もありますが、少なくともロジックに関しては完全に流用可能となっています。

開発時のつまずきポイント

最後に、自分が今回のApple Watchアプリを開発してきた中で困ってしまったことをいくつか紹介したいと思います。

iOS用のコードはwatchOS用のコードでは使えない

iOS用にビルドされたソースコードをwatchOS用にビルドされたソースコードでimportして使うことはできません。
聞けば当たり前のように思えますが、意外とこの考慮が必要になるパターンは多く、初見では気づきにくいです。

例えば、iPhoneアプリ側で定義されている共通処理や定義をApple Watchアプリ側でも使いたいというケースです。日時の表示のフォーマット処理やAPI通信のレスポンスオブジェクトの定義が、Apple Watchアプリ側の表示でも必要となることは少なくないと思います。

こういった場合、Apple Watchアプリ側のソースコード内で、iOS用のTARGETをimportすることになると思いますが、そのままではビルドエラーとなってしまいます。

解決方法としては、下記の手順で、watchOSのFrameworkのTARGETを新規に作成し、Apple Watchアプリ側でも使いたいソースコードのTarget Membershipに追加してからimportすることで、エラーにならずビルドが通るようになります。

  1. Add a targetでwatch OS -> Framework & library -> Frameworkを選択してwatchOS用のTARGETを新規作成(名前は仮にWatchCommonとする)

TARGET新規作成

2.Apple Watchアプリで参照したいソースコードを開き、右側のメニューを開いてTarget Membershipの中のWatchCommonにチェックを入れる

Target Membershipの追加

3.Apple Watchアプリのソースコードでimport WatchCommonを追加

importの追加

これにより、一つのソースコードをiOSとwatchOSで共通化することができます。

検証にはApple WatchのUDIDも必要

開発中のiOSアプリを検証端末で動かすには、Ad Hocプロビジョニングプロファイルを作成し、その中に検証端末のUDIDを登録する必要があるのは周知の事実かと思います。iPhoneだけでなくApple Watchにもこの対応が必要となります。
ですので、事前にテスターからApple WatchのUDIDを聞いておき、それを登録したAd Hocプロビジョニングプロファイルを作成しましょう。

また、watch OS9以降の場合、デベロッパモードもOS16以降のiPhoneと同じようにあるため、テスターにApple Watchのデベロッパモードを有効にしてもらうことも忘れずに伝えましょう。このモードはストアなどを経由せずにローカルでインストールしたアプリを実行する権限をApple Watchに付与するものになります(有効にする方法については、Apple公式ドキュメント「Enabling Developer Mode on a device」をご参考下さい)。

TARGETの設定

Apple WatchアプリのバージョンはiPhoneアプリのバージョンと同じにする必要があります。
また、PRODUCT_NAMEはiPhoneアプリとは異なるもので、かつ日本語を使わず英語の名前をつける必要があります。
これらを間違えてもビルドエラーになりませんが、Ad Hocの検証アプリをインストールする際に、「このAppは、整合性を確認できなかったためインストールできません。」というエラーが表示されます。そのため、証明書に問題があるように見えてしまって原因に気づきにくく、注意する必要があります。

UserDefaultsの情報はiPhoneとApple Watchで共有できない

アプリ固有の情報をローカルに保存する際に、UserDefaultsを使うことは多いと思います。しかしこのDBはあくまで端末に保存しているため、異なる端末であるApple WatchとiPhoneでは同じキーであっても別々に保存されます。(AppleWatchとiPhoneのアプリに同じApp Groupsを設定して、そのIDを指定して参照しようとしても同じです)。

そのため、UserDefaultsをiPhoneとApple Watchで共有したい場合は、片方で変更があった場合に、その変更をメッセージとして送信して、もう片方のUserDefaultsに再保存する必要があります(ただし、この方法でもApple Watch とiPhoneが常に接続されているとは限らないため注意が必要です)。
また、逆に言えば同じ端末であれば共有することができるため、コンプリケーションとApple Watchアプリの間でデータ共有にUserDefaultsを使うことは可能です。

Viewの描画に時間がかかる

先のApple Watch開発における最近の変更点で挙げた通り、Apple Watchの開発ではSwift UIを使います。
Swift UIは従来よりもシンプルにViewを書けるためコード量が大幅に減るというメリットがありますが、構成を上手く考えないと再描画によってパフォーマンスが低下するデメリットもあります。

当然Apple WatchはiPhoneよりも性能が高くはないため、このパフォーマンスの影響をより大きく受けます。そのため、一度にたくさんのViewを表示しようとすると、描画に多くの時間がかかってしまい、パフォーマンスが悪化します。

今回自分が開発したApple Watchアプリでは、停車駅で表示する駅が多い場合に、表示まで10秒以上もかかってしまうといった事象が発生しました。
そのため、これを解決するためにLazyHStackLazyVStackを利用しています。LazyHStack・LazyVStackは通常のHStack・VStackと異なり、画面内に入る分だけ描画して、画面外のViewの描画は遅延させるというものです。

ScrollView {
    VStack {
        ... 
    }
}

↓ そのまま置き換えるだけでOK

ScrollView {
    LazyVStack {
        ...
    }
}

これによって一度にたくさんのViewを表示する画面でも、実際に描画するのは画面内のViewに限られるため、パフォーマンスが大幅に改善し、停車駅も素早く表示できるようになりました。
ただし、LazyHStack・LazyStackは何も考えずにViewを配置すると、スクロールがカクついてしまって逆にパフォーマンスが悪化するという事象が発生するため、注意が必要です。これを回避するためには下記の点に注意して下さい。

LazyHStack・LazyStackの直下にForEachやSubViewを配置する

×NGパターン
ScrollView {
    LazyVStack {
        VStack {
            ForEach {
                ...
            }
        }
        ZStack {
            SubView(...)
        }
    }
}

●OKパターン
ScrollView {
    LazyVStack {
        ForEach {
            ...
        }
        SubView(...)
    }
}

上記のコードのように、必ずLazyHStack・LazyVStackの直下の階層にForEachやSubViewを配置するようにしましょう。間にVStackなどが挟まってしまうと、スクロールがカクついてしまうことがあります。

ForEachの中にForEachを置かない

×NGパターン
ScrollView {
    LazyVStack {
        ForEach {
            ...
            ForEach {
                ...
            }
        }
    }
}

●OKパターン
ScrollView {
    LazyVStack {
        ForEach {
            ...
        }
        ForEach {
            ...
        }
    }
}

上記のコードのようにForEachを入れ子で配置してしまうと、スクロール時に2回以上ループが走ることで無駄にViewが生成されてしまい、意図しないViewが表示されることがあります。
LazyHStack・LazyVStackでForEachを使う場合は、ループを2重で回す必要がないように、ForEachに渡すリストの形を修正しましょう。

SubViewに持たせるデータは描画に必要な最低限のものにする

×NGパターン
ScrollView {
    LazyVStack {
        SubView(list)
    }
}
struct SubView: View {
   let list: List<Data>
   ...
}

●OKパターン
ScrollView {
    LazyVStack {
        ForEach(0..<list.count, id: \.self) { index in
            SubView(list[index].name)
        }
    }
}
struct SubView: View {
   let name: String
   ...
}

上記のコードのように、SubViewにリストを持たせるのではなく、表示に必要なStringだけを持たせるようにしましょう。SubViewが大きなデータを持っている場合も、スクロールがカクついてしまうことがあります。

まとめ

今回は、自分がApple Watchの開発した経験で得られた知見をまとめました。
結論としては、Apple WatchはiPhoneアプリの処理を流用できるため、iPhoneアプリの機能なら簡単に実装でき、SwiftUIWidgetKitなど、iPhoneアプリでの開発経験がそのままApple Watchの開発に活かせることが多いです。
ですので、既にSwiftUIでiPhoneアプリを実装していたり、WidgetKitでウィジェット開発をした経験があるエンジニアが、iPhoneアプリと同じ機能をApple Watchでも実現したいという場合は、Apple Watch開発が未経験だったとしても、経験者に近い形で開発できるかと思います。

是非、この記事を読んでいただいたiOS開発者の方にも導入を検討していただければと思います。

また、冒頭でご紹介した通り、ルート結果や停車駅がいつでも確認できるApple Watch連携アプリが追加されたiOS版『乗換NAVITIME』アプリがリリースされました。
Apple Watchをお持ちの方はお出かけの際に、ぜひ利用してみてください!