Thumbnail image

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.

image-20220511152556631

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.

Warning
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.

image

(source: betterprogramming.pub)

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:

image

Diagram by Chris Eidhof from objc.io

  • Use @State for simple properties that belong to a single view. They should usually be marked private.
  • 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.

(What’s the difference between @ObservedObject, @State, and @EnvironmentObject? - ww.hackingwithswift.com)

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 application
  • WatchConnectivityManager 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 local sessionDef variable and setting the WCSession delegate to the PhoneWatchConnectivityManager 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

image-20220523174301855

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.

demo-send-message-appwatch

Problems

Create Apple Watch Simulator paird iPhone

  1. Pair your phone simulator with your watch. Click Add Additional Simulators from the drop-down menu near the run and stop buttons in Xcode.

    image-20220524142519641

  2. Add check Paired Apple Watch image-20220524142719622

  3. 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 .

    image-20220524143126538

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

image-20220524143932597

You can check how many targets are shared files by click to Target Membership

image-20220524144507291

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.

Warning
You just only to remove current iphone paired watch and create again

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