とことんDevOps | 日本仮想化技術のDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けする「とことんDevOps」では、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果、TipsなどDevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、コンテナ開発、IaCなど
読者登録と各種SNSのフォローもよろしくお願いいたします。

SwiftでiPhoneとApple Watch連携するアプリを作ってみた

Apple Watch SE2が2022/09/16(金)に発売されて早々に会社から支給してもらったので、遊んでみようと思っていたら、取り掛かり始めた頃には「今年もお世話になりました」という言葉が飛び交い、ブログにまとめて公開する頃には「明けましておめでとうございます」という言葉が飛び交っていました。

久々にネイティブアプリ開発でSwiftやXcodeを触ってみたくなったので、Apple Watch連携ができるまでをやってみました。

目次

完成後の動作イメージ

iPhone側

完成コード

実際に変更したコードのみを記載しています。他にもXcodeでプロジェクトを作成した際に複数のファイルが自動で生成されます。

実際のコード(クリックすると展開されます) gist.github.com

※コードに関するご指摘やフィードバックなどございましたら、GitHubの方でコメントお願いします。

詳細

インポート

import SwiftUI
import WatchConnectivity

必要なクラスをインポートします。

SwiftUIは、Xcodeでプロジェクトを作成した際に自動でインポートされています。

WatchConnectivityは、Apple Watchと連携したアプリを作りたい場合に、連携まわりをサポートしてくれるクラスになります。

ContentView.swiftの構成

import SwiftUI
import WatchConnectivity

struct ContentView: View { ... }

struct ContentView_Previews: PreviewProvider { ... }

class WatchConnector: NSObject, ObservableObject, WCSessionDelegate { ... }

全体構成としては、ContentViewContentView_Previewsの構造体とWatchConnectorのクラスで構成されています。

ContentViewContentView_Previewsは、Xcodeでプロジェクトを作成した際に自動で生成されるものです。

WatchConnectorは、Apple Watch連携をするときに必要になる処理を記載したクラスになります。

ContentViewとContentView_Previews

struct ContentView: View {
    @ObservedObject private var connector = WatchConnector()
    
    init() { ... }
    
    var body: some View { ... }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@ObservedObject private var connector = WatchConnector()は、WatchConnectorクラスで詳しく記載します。

ContentViewの構造体は、画面の見た目などに関する内容を実装する領域です。最終的にContentView_Previews内で使用します。ContentView_Previews内に記載されている内容が実際に画面に描画されます。ContentViewは、必要に応じて切り出されている構造体の1つと思えばよさそうです。

ContentViewの中には、init(){ ... }var body: some View { ... }があります。init()に関しては、見た目をそれっぽくするためにナビゲーションに関する処理をあれこれ書いていますが、今回は詳しく触れません。

var body: some View { ... }は、画面上で配置するテキストやボタンなどの定義をあれこれ書いている部分です。実際のアプリケーション開発では、多くのコードを書いていくことになるので、必要に応じてクラスや関数に分割されていくことになります。

struct ContentView: View {
    ...
    var body: some View {
        NavigationStack{ ... }
    }
    ...
}

ナビゲーションバーを使用しているのでNavigationStack内に処理を書いていますが、今回はそういうもんだという形で読み進めてもらえればと思います。

struct ContentView: View {
    @State private var count = 0
    @ObservedObject private var connector = WatchConnector()
    
    init() { ... }
    
    var body: some View {
        NavigationStack{
            VStack {
                VStack{ ... }
                .frame(maxHeight: .infinity)
                
                HStack{
                    Spacer()
                    ...
                }
                .padding()
            }
            .frame(maxHeight: .infinity)
            .navigationTitle("Sample App")
        }
    }
}

VStack・HStack・ZStack

画面UIを作っていく時に個人的にイメージしにくかったStack系の記述についてまとめました。

VStack { ... }は、垂直方向に要素を並べる。

HStack{ ... }は、水平方向に要素を並べる。

ZStack{ ... }は、重ねるように要素を並べる。※今回は使用なし

Spacer()は、余白のレイアウトを調整するために使用する。

Spacer | Apple Developer Documentation

WatchConnectorクラス

これからはApple Watchと連携していくために必要なクラスを作っていきます。

class WatchConnector: NSObject, ObservableObject, WCSessionDelegate {
    ...
    
    override init() { ... }
    
    func session(
        _ session: WCSession,
        activationDidCompleteWith activationState: WCSessionActivationState,
        error: Error?
    ) { ... }
    
    func sessionDidBecomeInactive( ... ) { ... }
    
    func sessionDidDeactivate( ... ) { ... }
    
    func session(
        _ session: WCSession,
       didReceiveMessage message: [String: Any]
    ) { ... }
    
    func send() { ... }
}

override init() { ... }は、イニシャライザと呼ばれるもので、初期化処理などを書いていく領域です。

func session( ... ) { ... }は、1つ目の方。セッションの有効化が完了したら実行されるメソッド。

func sessionDidBecomeInactive( ... ) { ... }は、セッションが現在の Apple Watch との通信を停止する場合に実行されるメソッド。

func sessionDidDeactivate( ... ) { ... }は、セッションが前のセッションからすべてのデータを配信し、Apple Watch との通信が終了したら実行されるメソッド。

func session( ... ) { ... }は、2つ目の方。メッセージが到着した場合に実行されるメソッド。

func send() { ... }は、Apple Watchに値を送信する際に呼び出されるメソッド。呼び出された際に、countに+1してからWCSession.default.sendMessage( ... )を呼び出して送信します。

WatchConnectorクラスの使用

...
import WatchConnectivity

struct ContentView: View {
    @ObservedObject private var connector = WatchConnector()
    
    init() { ... }
    
    var body: some View {
        NavigationStack{
            VStack {
                VStack{
                    ...
                    Text(String(self.connector.count))
                        .font(.largeTitle)
                        .foregroundColor(Color.gray)
                    
                    Text("\(self.connector.receivedMessage)")
                }
                .frame(maxHeight: .infinity)
                
                HStack{
                    ...
                    Button(action: {
                        self.connector.send()
                    }, label: { ... })
                    ...
                }
                .padding()
            }
            ...
        }
    }
}

struct ContentView_Previews: PreviewProvider { ... }

class WatchConnector: NSObject, ObservableObject, WCSessionDelegate { ... }

@ObservedObject private var connector = WatchConnector()は、さっき作ったクラスをインスタンス化して使用します。@ObservedObjectは、@ObservedObjectが付いたインスタンスのプロパティが変更された時にViewを更新する仕組みです。Swift5.1から導入された機能になります。

Text(String(self.connector.count))は、さっき作ったクラスでApple Watchとやり取りしている時に保持しているカウントをUI上に表示します。

Text("\(self.connector.receivedMessage)")は、受信した値を表示して通信上でどのような値を受け取ったのかを確認するために表示します。

Button(action: { ... }, label: { ... })は、ボタンのUI要素を配置します。

actionは、ボタンが押下された時の処理を記述します。今回は、self.connector.send()でApple Watchに現在の値を送信する処理を書いたメソッドを渡します。

labelは、ボタンの表示名などを指定します。今回は、プラス(+)のアイコンを表示します。

Apple Watch側

完成コード

実際に変更したコードのみを記載しています。他にもXcodeでプロジェクトを作成した際に複数のファイルが自動で生成されます。

実際のコード(クリックすると展開されます) gist.github.com

※コードに関するご指摘やフィードバックなどございましたら、GitHubの方でコメントお願いします。

詳細

基本的な構成はiPhone側と同じのため、スキップする部分が多いと思います。

インポート

import SwiftUI
import WatchConnectivity

iPhone側と同じものをインポートしています。

ContentView.swiftの構成

iPhone側で解説した知識で読み進められると思いますので、スキップします。

ContentViewとContentView_Previews

iPhone側で解説した知識で読み進められると思いますので、スキップします。

PhoneConnectorクラス

class PhoneConnector: NSObject, ObservableObject, WCSessionDelegate {
    @Published var receivedMessage = "PHONE : 未受信"
    @Published var count = 0
    
    override init() { ... }
    
    func session(
        _ session: WCSession,
        activationDidCompleteWith activationState: WCSessionActivationState,
        error: Error?
    ) { ... }
    
    func session(
        _ session: WCSession,
        didReceiveMessage message: [String : Any]
    ) { ... }
    
    func send() { ... }
}

基本的な構成は同じなのですが、iPhone側と違いsessionDidBecomeInactive(...) { ... }sessionDidDeactivate( ... ) { ... }のメソッドは不要になります。

func send() { ... }で使用しているif WCSession.default.isReachable { ... }は、WatchKit 拡張機能と iOS アプリが相互に通信できるかどうかの状態をBoolean形式で保持しています。

まとめ

本当はFlutterネタでやろうと思っていたのですが、ネイティブアプリの知識が薄い状態で作り始めるのはちょっと無謀すぎるかな?と思っていたところに、新しいApple Watchが発売されて業務端末として支給されたのをきっかけにネイティブアプリ開発に再入門することができました。

久々にやってみると忘れていることが多かったりしてかなり苦戦しました。 他にも前に比べてXcodeがどんどん進化していて浦島太郎状態でいちいち知らない機能を見つけたら感心していました。

普段はVS Codeを使って開発をすることが多いですが、その領域に特化したエディタの使いやすさはそれなりにあるので、理想をいうと用途に応じてエディタを使い分けられるといいのかもしれないですね。

新しいエディタを使うとショートカットなどを1から覚え直すことになるのでかなり苦行ではありますが

ということで、本年もどうぞよろしくお願い致します。