はじめに
iPhone でとある記録を取るアプリを作っていて,
Apple Watch でも記録取れると楽でいいよね〜ということになり,
機能実現できるかを簡単なサンプルアプリを SwiftUI で作って確認してみました。
使う技術は Watch Connectivity を使います。
以前同じような内容で記事書いたのですが,こちらは iPhone と Watch 側が
それぞれアプリを起動してアクティブであることを前提にしていました。
ただ,実際に記録を取る際には急いでいたり,片手が塞がっていたりするため,
Watch 側で送信だけ行い iPhone 側のアプリを起動した際にデータを受信して
画面も更新する形の方が良さそうです。
今回は Apple Watch 側でバックグラウンドのデータ送信を行い,
iPhone 側でちゃんと受信して画面更新ができるかを確認します。
Watch Connectivity
Watch Connectivity とは,iOS アプリと
そのペアの watchOS アプリの間での双方向通信を可能にする機能です。
今回は,バックグラウンド送信を行うので
下記のメソッドの中で最適なものを選択します。
-
updateApplicationContext(_:)
-
[String : Any]
の辞書型のデータ送信 - 再送信時にデータを上書きするため最新データのみ送信される
-
-
transferUserInfo(_:)
-
[String : Any]
の辞書型のデータ送信 - キューにスタックされるため溜まった分のデータを送信
-
-
transferFile(_:metadata:)
- 画像またはファイルベースのドキュメントを送信する際に利用
今回は,辞書型で十分なデータでかつ,
リストのアニマルセルをタップした分だけ送信したいので
transferUserInfo(_:)
メソッドを利用して
Watch アプリからデータを送信します。
受信する iOS アプリ側は WCSessionDelegate
のデリゲートメソッドを利用します。
optional func session(_ session: WCSession,
didReceiveUserInfo userInfo: [String : Any] = [:])
実装イメージ
Watch 側のアニマルリストをタップすると・・・
選択されたアニマルの絵文字とテキストおよびタップ時の時刻を
iPhone 側にバックグラウンドで送信し,
iPhone のアプリ起動時に受信して画面更新するといったシンプルなものです。
今回は受け取ったデータをリスト表示するだけですが,
実際のユースケースでは Realm などのデータベースに保存したりといった感じになると思います。
両方のアプリを起動してアクティブ状態でもリアルタイムでうまくいくので
前回の記事の内容の上位互換だなぁと思いました。
開発環境
- Xcode 13 以上
- iOS 15 以上
- watchOS 8 以上
- ペアリングした iPhone と Apple Watch
NavigationView
の Large Title 無効の実装を変更すれば,
watchOS 7 & iOS 14 でも動作します。
Always test Watch Connectivity data transfers on paired devices. The Simulator app doesn’t support the transferUserInfo(_:) method.
レファレンス にある通り,シミュレータでは動作確認ができません。
なので実機が必要です。
サンプルコード
今回の実装は GitHub にプッシュしているので気になる方は確認お願いします。
動作確認をするのに実機が必要だったので,動画を撮ってます。
今回 GIF は載せないのでリポジトリの README を参照してみてください。
下準備
データまわり
アニマルリスト用の構造体は下記の通りです。
Data
型に変換するため Codable
にも準拠させておきます。
今回は静的なリストなため Array
で表示用のアニマルリストを用意します。
struct Animal: Hashable, Codable {
var name: String
var emoji: String
}
let animals: [Animal] = [
Animal(name: "ネコ", emoji: "🐱"),
Animal(name: "イヌ", emoji: "🐶"),
Animal(name: "ハムスター", emoji: "🐹"),
Animal(name: "ドラゴン", emoji: "🐲"),
Animal(name: "ユニコーン", emoji: "🦄")
]
今回送信するデータ用の構造体は下記です。
ボタンタップ時のアニマル情報と時刻を格納します。
こちらも同様に Codable
に準拠させます。
struct Record: Hashable, Codable {
var animal: Animal
var timeStamp: Date
}
Watch App 画面実装
Watch 側のアニマルリスト表示の実装は下記です。
List
を用います。Watch 専用の CarouselListStyle
😍を使ってます。
ボタンタップ時の処理をこの後実装していきます。
import SwiftUI
struct AnimalListView: View {
private let viewModel = AnimalListViewModel()
var body: some View {
List {
ForEach(animals, id: \.self) { animal in
Button {
// Send Animal
viewModel.transfer(animal: animal)
} label: {
HStack(spacing: 16.0) {
Text(animal.emoji)
.font(.title)
Text(animal.name)
}
.padding(.vertical, 20.0)
}
}
}
.listStyle(CarouselListStyle())
}
}
iOS App 画面実装
受信したデータを List
に展開する iPhone 側の実装は下記です。
受信してデータを更新する処理はこの後 ViewModel に実装していきます。
import SwiftUI
struct ReceiverView: View {
@StateObject var viewModel = ReceiverViewModel()
var body: some View {
List {
ForEach(viewModel.records, id: \.self) { record in
VStack(alignment: .leading) {
Text(record.animal.emoji + record.animal.name)
.font(.body)
.padding(.vertical, 4.0)
Text(record.timeStamp.toString())
.font(.footnote)
.foregroundColor(.gray)
}
}
}
}
}
Date 型を String 型に変換するコード
extension Date {
func toString() -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
formatter.locale = Locale(identifier: "ja-JP")
return formatter.string(from: self)
}
}
ViewModel 実装(Watch 側)
WCSession
を利用できるようにしておきます。
この後,データ送信用の実装を追加します。
import WatchConnectivity
final class AnimalListViewModel: NSObject {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
session.activate()
}
/// タップされたアニマル名と絵文字,時刻を iPhoneに送信
/// - Parameter animal: タップされたアニマル
func transfer(animal: Animal) {
// iPhone に送信する処理
}
}
extension AnimalListViewModel: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print(error.localizedDescription)
} else {
print("The session has completed activation.")
}
}
}
ViewModel 実装(iPhone 側)
WCSession
を利用できるようにしておきます。
この後,データを受信してリスト更新をできるように実装していきます。
records
に受信したデータを格納していく形になります。
よって受信するたびにリストが更新されるようになります。
import WatchConnectivity
final class ReceiverViewModel: NSObject, ObservableObject {
@Published var records: [Record] = []
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
session.activate()
}
}
extension ReceiverViewModel: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print(error.localizedDescription)
} else {
print("The session has completed activation.")
}
}
func sessionDidBecomeInactive(_ session: WCSession) { }
func sessionDidDeactivate(_ session: WCSession) { }
}
本実装
Watch 側の送信処理実装
transfer
関数内を実装します。
タップされたアニマルと時刻から Record
のデータを生成し,
Data
型に変換して辞書に突っ込みます。
先ほどの transferUserInfo(_:)
を利用して送信します。これだけです😮
import WatchConnectivity
final class AnimalListViewModel: NSObject {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
session.activate()
}
/// タップされたアニマル名と絵文字,時刻を iPhoneに送信
/// - Parameter animal: タップされたアニマル
func transfer(animal: Animal) {
let record = Record(animal: animal, timeStamp: Date())
guard let data = try? JSONEncoder().encode(record) else {
// Error handring if need
return
}
let userInfo: [String: Any] = ["record": data]
self.session.transferUserInfo(userInfo)
}
}
iPhone 側の受信・更新処理実装
受信側の処理を実装します。
WCSessionDelegate
のデリゲートメソッドを実装します。
受信時に呼ばれる関数で userInfo
のデータから
送信時と逆の手順で Record
型を取得します。
取得できたら records
に追加します。
追加時にデータバインディングによってリストが更新されます。
import WatchConnectivity
final class ReceiverViewModel: NSObject, ObservableObject {
@Published var records: [Record] = []
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
session.activate()
}
}
extension ReceiverViewModel: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
// Error handring if need
print(error.localizedDescription)
} else {
print("The session has completed activation.")
}
}
func sessionDidBecomeInactive(_ session: WCSession) { }
func sessionDidDeactivate(_ session: WCSession) { }
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
guard let data = userInfo["record"] as? Data,
let record = try? JSONDecoder().decode(Record.self, from: data) else {
// Error handring if need
return
}
self.records.append(record)
}
}
今回は SwiftUI 版でしたが,UIKit 版で TableView などに
反映させるにはメインスレッドで更新してやる必要あります。
バックグラウンドで処理されるため明確にメインスレッドで画面更新が必要ってことですね。
おわりに
今回は Apple Watch 側でバックグラウンドのデータ送信を行い,
iPhone 側でちゃんと受信して画面更新ができるかを確認しました。
Notification
の送受信と同じテイストで
とても少ないコードで実現できるのでとても良いですね。
iPhone-Apple Watch 間だけではないのですが,
異なるデバイス間で通信成功したら謎に嬉しくなりますね。
ご覧いただきありがとうございました。