Wearables are becoming more and more popular nowadays wth Apple Watch being one of the most popular devices in that category. There are many interesting cases that watch could be used for. Especially when it comes to GPS tracking or health services. In this post I’ll be showing how easy it is to add WatchOS companion app to a project based on Ionic.
tldr; whole example is available here
Setup project
Let’s start with creating a simple blank Ionic project. App will have simple requirements:
- A text input
- A button that sends the input from text to WatchOS
- A subscription that listens to events from WatchOS to update the value
To get this working there needs to be connection between three areas: Ionic part <-> iOS native <-> WatchOS.
Let’s start by defining a plugin that will be responsible for communicating with the watch.
import { Injectable } from '@angular/core';
import { CallbackID, Capacitor, registerPlugin } from '@capacitor/core';
export enum WatchOsStatus {
OK = 0,
NotSupported = 1,
NotReachable = 2,
NotPaired = 3,
CommunicationProblem = 4,
ActivationFailure = 5,
WatchAppNotInstalled = 6,
}
export type SubscribeToValueCallback = (
message: { value: string } | null,
err?: any
) => void;
const _pluginName: string = 'WatchOSPlugin';
export interface WatchOSPlugin {
getState(): Promise<{
status: WatchOsStatus;
}>;
subscribe(callback: SubscribeToValueCallback): Promise<CallbackID>;
setValue(data: { value: string }): Promise<void>;
}
const WatchOSPlugin = registerPlugin<WatchOSPlugin>(_pluginName);
@Injectable({
providedIn: 'root',
})
export class WatchOSSevice {
async getState(): Promise<WatchOsStatus> {
if (!Capacitor.isPluginAvailable(_pluginName)) {
return WatchOsStatus.NotSupported;
}
const result = await WatchOSPlugin.getState();
return result ? result.status : WatchOsStatus.CommunicationProblem;
}
async subscribe(
callback: SubscribeToValueCallback
): Promise<CallbackID | null> {
if (Capacitor.isPluginAvailable(_pluginName)) {
return await WatchOSPlugin.subscribe(callback);
}
return null;
}
async setValue(data: { value: string }): Promise<void> {
if (Capacitor.isPluginAvailable(_pluginName)) {
await WatchOSPlugin.setValue(data);
}
}
}
Firstly the connection with the watch has to be verified. If it’s not paired or not actively connected, the user should know what could be the problem, therefore let’s have a getState method to start with. In real scenario you would probably want to use it as a subscription similar as the subscribe method.
For setting value we have setValue plugin call. For keeping an eye on updated value from the watch use Ionic callbacks in subscribe method.
Time to add some fancy UI:
<ion-app>
<ion-header>
Watch OS integration
</ion-header>
<ion-content>
<h2>Connection status: {{status}}</h2>
<ion-item>
<ion-label>Value</ion-label>
<ion-input type="text" [(ngModel)]="value"></ion-input>
</ion-item>
<ion-button (click)="sendValue()">Send value</ion-button>
<ion-button (click)="checkStatus()">Check status</ion-button>
</ion-content>
</ion-app>
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { WatchOSSevice, WatchOsStatus } from './watch-os.service';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent implements OnInit {
value: string = '';
status: WatchOsStatus | null = null;
constructor(
private watchOsService: WatchOSSevice,
private cd: ChangeDetectorRef
) {}
async ngOnInit() {
this.watchOsService.subscribe((msg) => {
this.value = msg?.value ?? this.value;
this.cd.detectChanges();
});
this.status = await this.watchOsService.getState();
}
async sendValue() {
await this.watchOsService.setValue({ value: this.value });
}
async checkStatus() {
this.status = await this.watchOsService.getState();
}
}
The app shows only a header and a number input. Value is bind to the value and can be send to its native part using sendValue. If a value is received from the watch, subscribe callback will handle it and update the value.
Create Watch target
Jump to Xcode to prepare watch target. Click File > New > Target select WatchOS tab and choose App. In the builder make sure to select Watch App for existing iOS App and pick App from a dropdown.
It should generate a project with a ContentView in SwiftUI. The view has only a TextField and a Button. When the button is tapped, the value is being send through WCSession to the app. On the other hand subscription to NotificationCenter should update it if any changes come from the mobile.
import SwiftUI
import Combine
import WatchConnectivity
struct ContentView: View {
@State private var value = "0"
var body: some View {
VStack {
TextField("Input", text: $value)
.padding()
Button("Send value to phone") {
WCSession.default.sendMessage(["value": self.value], replyHandler: nil)
}
.padding()
}
.onReceive(NotificationCenter.default.publisher(for: .onCapValueUpdated), perform: { val in
self.value = String(describing: val.object!)
})
}
}
NotificationCenter has to be used to listen to data received through WCSession which will be declared in few next steps. Notification names could be defined as an extension for easier use. You can make the file shared between both targets: App & Watch.
import Foundation
extension NSNotification.Name {
static let onCapValueUpdated = Notification.Name("onCapValueUpdated")
static let onWatchValueUpdated = Notification.Name("onWatchValueUpdated")
}
Connect watch to mobile
In order to make the connection between mobile and watch to work, the protocol WCSessionDelegate has to be implemented. Ideally set this up at the app launch. One way to do that is to use the WKApplicationDelegate. Declare a new ExtensionDelegate class in Watch target.
import WatchKit
import WatchConnectivity
import CoreData
class ExtensionDelegate: NSObject, WKApplicationDelegate, WCSessionDelegate {
override init() {
super.init()
assert(WCSession.isSupported(), "WatchConnectivity is required!")
WCSession.default.delegate = self
WCSession.default.activate()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
let value = message["value"] as? String
NotificationCenter.default.post(name: .onCapValueUpdated, object: value)
}
}
When the app is launched there’s an assertion to make sure WCSession is supported and then the session is activated. Protocol requires to implement method activationDidCompleteWith. It can be left empty for now. At this point more important is implementation of didReceiveMessage. That method will be called when the message is sent from the Mobile version.
WCSession also needs to know which delegate it should use. It is possible to implement it as a separate class, but in this example we can keep it as part of ExtensionDelegate.
didReceiveMessage has also overloaded function with replyHandler that could be used to instantly return response to the app version.
There are also other methods that could be implemented to for example to watch the status of connection or to finish processing any pending work.
Last thing to do is to tell the Watch app to use ExtensionDelegate.
import SwiftUI
@main
struct WatchApp_Watch_AppApp: App {
@WKApplicationDelegateAdaptor(ExtensionDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
That’s all for the watch part. Now let’s add a support in the app.
Connect mobile with watch
App version also requires activation of Watch Connectivity using implementation of WCSessionDelegate. Since Capacitor provides a Storyboard by default we simply add a ViewController to it that implements a WCSessionDelegate protocol.
Go to Main storyboard and add a new view controller.
Now implement WCSessionDelegate protocol in the MainViewController
import Foundation
import Capacitor
import WatchConnectivity
class MainViewController: CAPBridgeViewController, WCSessionDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if (WCSession.isSupported()) {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
let value = message["value"] as? String
NotificationCenter.default.post(name: .onWatchValueUpdated, object: value)
}
}
The implementation is pretty similar to WatchOS version, yet iOS version requires two methods to be implemented: sessionDidBecomeInactive and sessionDidDeactivate. If you have any processing ongoing that’s the place to do the last work before the disconnection could happen. You can also use this to update the status of connectivity and pass it into the app. Since iOS now supports multiple devices this could be also used to switch between them.
didReceiveMessage is similar to the method that we have in the watch version. We use the same pattern and pass the event along to NotificationCenter.
Capacitor connectivity plugin
Finally implement the plugin to support the communication from Ionic to Native.
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(WatchOSPlugin, "WatchOSPlugin",
CAP_PLUGIN_METHOD(getState, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setValue, CAPPluginReturnNone);
CAP_PLUGIN_METHOD(subscribe, CAPPluginReturnCallback);
)
import Foundation
import WatchConnectivity
import Capacitor
import Combine
class ResponseStatus {
let NotSupported = 1
let NotReachable = 2
let NotPaired = 3
let CommunicationProblem = 4
let ActivationFailure = 5
let WatchAppNotInstalled = 6
let OK = 0
}
@objc(WatchOSPlugin)
class WatchOSPlugin : CAPPlugin {
@objc(getState:)
func getState(_ call: CAPPluginCall) {
var result = ResponseStatus().CommunicationProblem
if (WCSession.isSupported()){
let state = WCSession.default.activationState
if state == WCSessionActivationState.activated {
WCSession.default.activate()
if (WCSession.default.isPaired)
{
if (WCSession.default.isWatchAppInstalled) {
if (!WCSession.default.isReachable) {
result = ResponseStatus().NotReachable
} else {
result = ResponseStatus().OK
}
} else {
result = ResponseStatus().WatchAppNotInstalled
}
} else {
result = ResponseStatus().NotPaired
}
} else {
result = ResponseStatus().ActivationFailure
}
} else {
result = ResponseStatus().NotSupported
}
call.resolve(["status": result])
}
@objc(setValue:)
func setValue(_ call: CAPPluginCall){
let value = call.getString("value")
WCSession.default.sendMessage(["value": value], replyHandler: nil)
}
@objc
func subscribe(_ call: CAPPluginCall) {
call.keepAlive = true
NotificationCenter.default.addObserver(forName: .onWatchValueUpdated, object: nil, queue: .main) { notification in
call.resolve(["value": notification.object ])
}
}
}
Let’s start from the end.
subscribe on each call adds a new observer to watch for notifications that will be send when the data comes from the watch. In real scenario make sure to kill the calls so you don’t end up with multiple active subscriptions.
setValue – this is very straightforward method simply passing along a message using WCSession to the Watch. That executes didReceiveMessage in the watch target.
getState – this may seem overcomplicated, however there are numerous things that could go wrong with the watch connectivity so it’s required to check the status in details. Before we activate the session we need to make sure it is supported. This method has to be called before setValue otherwise setValue won’t work as the session won’t be active.
That’s it. Now rebuild & try the app. When you set value in phone and tap send, the value should get updated in the watch version. Similar – the other way around.
Summary
In this post I demonstrated the process of integrating an iOS app based on Capacitor with an Apple Watch device. This integration involves some initial setup, such as implementing the necessary protocols, but overall, the process is remarkably smooth. Handling communication between the app and the Apple Watch is straightforward and resembles the functionality of typical publish-subscribe platforms.
Komentarze
Jedna odpowiedź do „Integrate Apple Watch with Ionic App”
[…] my previous post I showed how to connect the watchOS with a Capacitor based application. This time I want to show […]