Swift. Two-way communication between an iOS app and watchOS app. PART 1
So, you have decided to create iOS application with its watch companion application. And, you need to exchange some data between two of them. For this scenario Apple provides simple WatchConnectivity framework, that specifically designed for this type of scenarios. This framework does most of the heavy lifting and your goal is simply to implement the business logic.
In this article consists of two parts:
- Part 1. Minimum you need to sync data.
- Part 2. One of the approaches to encapsulate complex sync data logic.
Lets get started. To isolate all logic in one place lets create a service. First of all we need to import WatchConnectivity framework. Also, we need to implement WCSessionDelegate delegate protocol that defines methods for receiving messages from WCSession.
import Foundation
import WatchConnectivity
class SyncService : NSObject, WCSessionDelegate { }
Initialize
Now, lets initialize our service. The WCSession is the core object responsible for whole communication between iOS and WatchKit. At first we create instance of WCSession, then we assign our service as WCSessionDelegate delegates handler, and lastly we initialize our session.
private var session: WCSession = .default
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
self.connect()
}
Connect
According to the WCSession documentation, before activating (connect) session, we need to make sure that current device can use WatchConnectivity. For that we need to call isSupported()
method, if it is successful we simply activate the session.
func connect() {
guard WCSession.isSupported() else {
print("WCSession is not supported")
return
}
session.activate()
}
Now, lets add minimum required handlers from WCSessionDelegate delegate protocol. We are going to reuse our service on both sides iOS and WatchOS. Because not all methods from WCSessionDelegate supported on WatchOS we need to use #if
preprocessing directive to exclude some code from WatchOS version.
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) { }
func sessionDidDeactivate(_ session: WCSession) { }
#endif
Where:
session
: delegate method called when session has finished activating. Here you can check is session was activated or not, and handle errors if any.sessionDidBecomeInactive
: delegate method is called when session will stop communicating with the current Apple Watch.sessionDidDeactivate
: delegate method is called when communication with the Apple Watch has ended.
Send data
To send data from one device to another we need to use WCSession sendMessage
method. Because, we are creating service, lets wrap it in our method that check if counterpart is available, before sending message.
func sendMessage(_ key: String, _ message: String, _ errorHandler: ((Error) -> Void)?) {
if session.isReachable {
session.sendMessage([key : message], replyHandler: nil) { (error) in
print(error.localizedDescription)
if let errorHandler = errorHandler {
errorHandler(error)
}
}
}
}
Where:
key
: the message key, unique name that is used to identify message on other side.message
: the data we want to sent to other side. To send complex objects, just convert them to JSON format.errorHandler
: the error handler in case if message was not send.
Receive data
The last thing we need to do is to handle received messages on the counterpart side. For that we also need to implement one more method from WCSessionDelegate delegate protocol.
var dataReceived: ((String, Any) -> Void)?
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
guard dataReceived != nil else {
print("Received data, but 'dataReceived' handler is not provided")
return
}
DispatchQueue.main.async {
if let dataReceived = dataReceived {
for pair in message {
dataReceived(pair.key, pair.value)
}
}
}
}
Where:
session
: current session object.message
: actual received message from counterpart app. Where the dictionary pairkey
is a message key, andvalue
is the message itself.dataReceived
: your data handler where you are going to handle the received messages.
Usage
Basically, that is the bearer minimum what you need to send/receive messages between iOS and Apple Watch. To test this service you need to start iOS and watch simulators all together.
var syncService = SyncService()
syncService.dataReceived = { (key, message) in
// handle message based on key
}
syncService.sendMessage("dataSyncKey", "Some data to sync", { error in })
Also see Source Code section for more practical usage.
Summary
Lets combine all together to get full picture.
import Foundation
import WatchConnectivity
class SyncService : NSObject, WCSessionDelegate {
private var session: WCSession = .default
var dataReceived: ((String, Any) -> Void)?
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
self.connect()
}
func connect(){
guard WCSession.isSupported() else {
print("WCSession is not supported")
return
}
session.activate()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) { }
func sessionDidDeactivate(_ session: WCSession) { }
#endif
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
guard dataReceived != nil else {
print("Received data, but 'dataReceived' handler is not provided")
return
}
DispatchQueue.main.async {
if let dataReceived = self.dataReceived {
for pair in message {
dataReceived(pair.key, pair.value)
}
}
}
}
func sendMessage(_ key: String, _ message: String, _ errorHandler: ((Error) -> Void)?) {
if session.isReachable {
session.sendMessage([key : message], replyHandler: nil) { (error) in
print(error.localizedDescription)
if let errorHandler = errorHandler {
errorHandler(error)
}
}
}
}
}
Source Code
The sample application for this article you can find here .