Apple Watch Serial P2 Homescreen Connect
Table of Contents
Abstract
Today, we will to work on the home screen. In this post, we will focus on communication between an iOS app with watchOS app. Here are some keys you will get through this topic:
- Understand Watch Connectivity.
- The fundamental component in SwiftUI
- Have knowledge about
WCSession
- Have basic knowledge about data flow in SwiftUI
This article is part of my Apple Watch by SwiftUI series.
Previously : Apple Watch Serial P1 Homescreen
Let’s do it.
Home Screen
Related knowledge
The magic of the Apple Watch experience comes from seamless interactions between the watch and your iOS apps. (source: raywenderlich)
Watch Connectivity
Two-Way Communication Using Watch Connectivity: The Watch Connectivity framework
provides convenient APIs to implement two-way communication between paired apps.
Relationship between Watch app, WatchKit extension and iOS app (source: developer.apple.com)
To transfer data between the iOS app and the watch app, we need to use WCSession: The object that initiates communication between a WatchKit extension and its companion iOS app.
Configuring and Activating the Session
Your iOS app and watchOS app must both create and configure an instance of this class at some point during their execution. When both session objects are active, the two processes can communicate immediately by sending messages back and forth. When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
Activation state changes when switching to another Apple Watch (source: developer.apple.com)
Figure above shows the sequence of events that happen when the user switches from one Apple Watch to another. When automatic switching is enabled, only one Apple Watch at a time actually communicates with the iOS app.
Communicating with the Counterpart App
-
updateApplicationContext(_:)
can be used to send data to the counter app when the counter app only cares about the latest state of that data. -
sendMessage(_:replyHandler:errorHandler:)
can be used when you need to transfer a dictionary of data immediately. -
sendMessageData(_:replyHandler:errorHandler:)
same as above, but for sending a Data object instead of a dictionary. -
transferUserInfo(_:)
is used to transfer a dictionary of data in the background. -
transferFile(_:metadata:)
is used to transfer files like images and optional dictionaries with file metadata.
transferUserInfo(:_)
method suits the most for our use case, but according to Apple documentation the system will not call the didReceiveUserInfo
delegate method on simulator.
Data flow in SwiftUI
State is inevitable in any modern app, but with SwiftUI it’s important to remember that all of our views are simply functions of their state – we don’t change the views directly, but instead manipulate the state and let that dictate the result.
SwiftUI
gives us several ways of storing state in our application, but they are subtly different and it’s important to understand how they are different in order to use the framework properly.
The challenge in making a SwiftUI app is technically all four of @State
, @StateObject
, @ObservedObject
and @EnvironmentObject
will superficially “work”. Your app will compile, and you may even get the behaviour you are after even when using the wrong property wrapper for the situation. But, if used incorrectly, you may find your view doesn’t update when your data updates. Here is a summary about four states and how to use this:
- Use
@State
for simple properties that belong to a single view. They should usually be markedprivate
. - Use
@ObservedObject
for complex properties that might belong to several views. Most times you’re using a reference type you should be using@ObservedObject
for it. - Use
@StateObject
once for each observable object you use, in whichever part of your code is responsible for creating it. - Use
@EnvironmentObject
for properties that were created elsewhere in the app, such as shared data.
Identifiable
A class of types whose instances hold the value of an entity with stable identity. - apple
The Identifable
protocol is for use with any type that needs to have a stable notion of identity. We will compare Equatable with Identifiable in the following articles.
Let’s code
Because, we are using swift ( iOS version >= 10) for iphone application and switUI (iOS version >=13) for watch app. We cannot create one class WCSession to share with two projects. So we need to create two instances class to manager WCSession for iPhone and watch.
PhoneWatchConnectivityManager
for Iphone applicationWatchConnectivityManager
for watch applicaiton
And, WatchConnectivityShareData
class to share data model between two projects
The iPhone Side
First, Let’s start with the iOS side of things. Below is a completely finished version of my PhoneWatchConnectivityManager.swift
. Underneath the code will be an explanation of everything in the file.
//
// WatchConnectivityManager.swift
// 12BayV2
//
// Created by Tuan Truong Quang on 5/13/22.
// Copyright © 2022 Truong Quang Tuan. All rights reserved.
//
import Foundation
import WatchConnectivity
class PhoneWatchConnectivityManager: NSObject {
static let shared = PhoneWatchConnectivityManager()
fileprivate var sessionDef:WCSession? = nil
private override init() {
super.init()
print("::PhoneWatchConnectivityManager init WCSession.default")
self.sessionDef = WCSession.default
self.sessionDef?.delegate = self
}
}
//MARK:// Base funcs
extension PhoneWatchConnectivityManager {
func activate() {
if WCSession.isSupported() {
if WCSession.isSupported() {
print("::PhoneWatchConnectivityManager WCSession.default.activate()")
self.sessionDef?.activate()
}
}
}
func send(_ message: String) {
print("::PhoneWatchConnectivityManager WCSession.default.reachable \(WCSession.default.isReachable)")
guard self.sessionDef?.activationState == .activated else {
return
}
#if os(iOS)
guard self.sessionDef?.isWatchAppInstalled ?? false else {
return
}
#else
guard self.sessionDef?.isCompanionAppInstalled ?? false else {
return
}
#endif
if self.sessionDef?.isReachable ?? false {
print("::PhoneWatchConnectivityManager wcSession sendMessage: \(message)")
self.sessionDef?.sendMessage([WatchConnectivityShareData.MESSAGE_KEY : message], replyHandler: nil) { error in
print("::PhoneWatchConnectivityManager Cannot send message: \(String(describing: error))")
}
} else {
print("::PhoneWatchConnectivityManager WCSession.default.isReachable false : \(WCSession.default.isReachable)")
}
}
}
//MARK:// WCSessionDelegate
extension PhoneWatchConnectivityManager: WCSessionDelegate {
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("::PhoneWatchConnectivityManager didReceiveMessage: \(message)")
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("::PhoneWatchConnectivityManager activationState: \(activationState.rawValue)")
switch activationState {
case .activated:
print("WCSession activated successfully")
case .inactive:
print("Unable to activate the WCSession. Error: \(error?.localizedDescription ?? "--")")
case .notActivated:
print("Unexpected .notActivated state received after trying to activate the WCSession")
@unknown default:
print("Unexpected state received after trying to activate the WCSession")
}
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
print("::PhoneWatchConnectivityManager sessionDidBecomeInactive ")
}
func sessionDidDeactivate(_ session: WCSession) {
print("::PhoneWatchConnectivityManager sessionDidDeactivate ")
session.activate()
}
#endif
}
init()
We will create localsessionDef
variable and setting theWCSession
delegate to thePhoneWatchConnectivityManager
in private init singleton class.func activate()
Configuring and Activating the Session.activationDidCompleteWith activationState (WCSessionDelegate)
: We get the current activation state of the session.sendMessage(_:replyHandler:errorHandler:)
We will using this function to sends a message immediately to the paired and active device and optionally handles a response.
Send message
Next, We will implement func activate()
at AppDelegate
class
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//Activate WatchConnect
PhoneWatchConnectivityManager.shared.activate()
self.window = UIWindow(frame: UIScreen.main.bounds)
StartViewCoordinator(window: self.window!).start()
return true
}
Now add a button in content view to call sendMessage
function:
PhoneWatchConnectivityManager.shared.send("Hello World!, New message from iPhone\n\(Date().dateString(withFormat: .DD_MM_YYYY_HH_MM))")
Finally, once you’re done with that, click to run project, if you see message bellow, your iOS app should be ready to go.
::PhoneWatchConnectivityManager init WCSession.default
::PhoneWatchConnectivityManager WCSession.default.activate()
::PhoneWatchConnectivityManager activationState: 2
::PhoneWatchConnectivityManager WCSession activated successfully
The WatchOS Side
Now, let’s get started on the watchOS side of things. You’ll find the complete version of my WatchConnectivityManager
below:
//
// WatchConnectivityService.swift
// watchOS WatchKit Extension
//
// Created by Tuan Truong Quang on 5/19/22.
// Copyright © 2022 Truong Quang Tuan. All rights reserved.
//
import os
import SwiftUI
import WatchConnectivity
struct WatchMessageItemModel : Identifiable {
var id: String = UUID().uuidString
var messsage:String = ""
}
class WatchConnectivityManager: NSObject, ObservableObject {
private let wcSession: WCSession
private let logger = Logger(subsystem: "WCExperimentsWatchApp", category: "WatchConnectivityService")
@Published var notificationMessage: WatchMessageItemModel? = nil
override init() {
self.wcSession = WCSession.default
super.init()
if WCSession.isSupported() {
wcSession.delegate = self
wcSession.activate()
}
}
}
extension WatchConnectivityManager: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("::WatchConnectivityService WCSession activationDidCompleteWith state: \(activationState.rawValue), error: \(error?.localizedDescription ?? "nil")")
}
func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) {
print("::WatchConnectivityService didFinish userInfoTransfer: \(userInfoTransfer.userInfo), error: \(error?.localizedDescription ?? "nil")")
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
print("::WatchConnectivityService didReceiveMessageData: messageData \(messageData)")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
print("::WatchConnectivityService didReceiveMessage: message \(message)")
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data, replyHandler: @escaping (Data) -> Void) {
print("::WatchConnectivityService didReceiveMessageData: messageData \(messageData)")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("::WatchConnectivityService didReceiveMessage: \(message)")
DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return }
let messageByString:String = message[WatchConnectivityShareData.MESSAGE_KEY] as? String ?? "Error"
strongSelf.notificationMessage = WatchMessageItemModel.init(messsage: messageByString)
}
}
}
Receiving messages
To receive the messages sent using sendMessage(_:replyHandler:errorHandler:)
method we need to implement the session(_:didReceiveMessage:)
method from WCSessionDelegate
protocol.
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("::WatchConnectivityService didReceiveMessage: \(message)")
DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return }
let messageByString:String = message[WatchConnectivityShareData.MESSAGE_KEY] as? String ?? "Error"
strongSelf.notificationMessage = WatchMessageItemModel.init(messsage: messageByString)
}
}
The didRecieveMessage
method passes through a dictionary of data. This dictionary is the same dictionary that the iOS app sent, so the string inside the brackets must be the same as the key in the dictionary in PhoneWatchConnectivityManager
.
Once we receive the message we can store it as a published variable in WatchConnectivityManager
on the main thread since we should always make sure we publish on the main thread. Our content view will then observe the changes to this variable and display a pop-up alert when the state changes.
Alert message
Now let’s add code to observe changes to the notification message in our content view (HomeView
). In the content view, we will add StateObject
variable for the WatchConnectivityManager
import SwiftUI
struct HomeView: View {
@StateObject var service = WatchConnectivityManager()
var body: some View { ... }
}
And then, we will bind the message item model WatchMessageItemModel
from WatchConnectivityManager
to the alert.
.alert(item: $service.notificationMessage, content: { info in
Alert(title: Text("WELL DONE\n\(info.messsage)"))
})
Here is the complete HomeView
code:
//
// HomeView.swift
// watchOS WatchKit Extension
//
// Created by Tuan Truong Quang on 5/5/22.
// Copyright © 2022 Truong Quang Tuan. All rights reserved.
//
import SwiftUI
struct HomeView: View {
@StateObject var service = WatchConnectivityManager()
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 10.0) {
Text("Support 24/7")
Button.init(role: .none, action: {
}, label: {
Text("1900 2642")
.bold()
})
.foregroundColor(.white)
.background(Color.init(hex: 0xE49E26))
.cornerRadius(30.0)
HStack.init(alignment: .center, spacing: 8.0) {
HomeItemView(item: .init(title: "All Bookings", icon: "calendar"))
HomeItemView(item: .init(title: "Notifications", icon: "notification"))
}
}.padding()
.alert(item: $service.notificationMessage, content: { info in
Alert(title: Text("WELL DONE\n\(info.messsage)"))
})
}
.navigationTitle("12Bay")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
.navigationBarHidden(false)
.navigationBarTitleDisplayMode(.inline)
}
}
struct HomeView_Previews: PreviewProvider {
static let devices:[String] = ["Apple Watch Series 6 - 44mm", "Apple Watch SE - 40mm"]
static var previews: some View {
ForEach(HomeView_Previews.devices, id: \.self) { devicesName in
HomeView()
.previewDevice(PreviewDevice(rawValue: devicesName))
.previewDisplayName(devicesName)
}
}
}
And that is it for the watchOS code!
Running the Apps
Now all you have to do is run the scheme that deploys the app to the Apple Watch. Once the Apple Watch app loads (it may take a few minutes the first time), click the app on the iPhone home screen. Now you should have both apps running.
Problems
Create Apple Watch Simulator paird iPhone
-
Pair your phone simulator with your watch. Click
Add Additional Simulators
from the drop-down menu near the run and stop buttons in Xcode. -
Add check
Paired Apple Watch
-
Launch the iPhone or watch application.
To run an app on the simulator, you simply select the scheme for your Phone or WatchKit application by choosing it in the righthand side of the scheme .
Share Swift classes across multiple targets
Simple way by click to Project
choose Build Phases
tab and click to +
add icon in Copy Bundle Resources
session
You can check how many targets are shared files by click to Target Membership
WCErrorDomain Code=7007
If you have a problem WCSession activated and isReachable
is true value. But you cannot send message to watch app. It took me a long time to solve the above problem. Finally, I found the solution.
Conclusion
Now, you have an app that runs both on the Apple watch and on the iPhone.
In the next post, I will go over establishing watch connectivity to send data between my iPhone and watch to update the UI and complete the home screen.
So, Happy coding ^^
Posts in this Series
- Apple Watch Serial Summary
- Apple Watch Serial My Notification
- Apple Watch Serial - My Order Detail Screen
- Apple Watch Serial - My Order Screen
- Apple Watch Serial P2 Homescreen Complete
- Apple Watch Serial P2 Homescreen Connect
- Apple Watch Serial P1 Homescreen
- Let's Idea, How to Design Travel App (12Bay) on WatchOS?
- Apple Watch Series