Apple Watch SE2が2022/09/16(金)に発売されて早々に会社から支給してもらったので、遊んでみようと思っていたら、取り掛かり始めた頃には「今年もお世話になりました」という言葉が飛び交い、ブログにまとめて公開する頃には「明けましておめでとうございます」という言葉が飛び交っていました。
久々にネイティブアプリ開発でSwiftやXcodeを触ってみたくなったので、Apple Watch連携ができるまでをやってみました。
目次
完成後の動作イメージ
動いた
— いっしー (@vtj_ismt7) 2022年12月27日
Apple Watch連携を完全に理解した pic.twitter.com/6635OqDZOZ
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 { ... }
全体構成としては、ContentView
とContentView_Previews
の構造体とWatchConnector
のクラスで構成されています。
ContentView
とContentView_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から覚え直すことになるのでかなり苦行ではありますが
ということで、本年もどうぞよろしくお願い致します。