Adobe Tech Blog

News, updates, and thoughts related to Adobe, developers, and technology.

Follow publication

Adobe Experience Platform Mobile SDK for Apple watchOS

--

Apple Watch with the Adobe logo.

The purpose of this blog is to detail my experience at Adobe as a software engineering intern integrating the Adobe Experience Platform Mobile SDKs and extensions with Apple watchOS.

Before we get started, let me give a brief intro. My name is Mark Frazier and I am part of the 2022 Adobe Digital Academy. My background and education are in film and photography production, but over the last couple of years have been pursuing a career in software development. You can find more about the Adobe Digital Academy here.

Context and background

If you aren’t familiar with the Adobe Experience Platform, simply put, it’s a system for building and managing complete solutions that drive customer experience. The whole Experience Platform ecosystem revolves around collecting, centralizing, and standardizing data to create 360-degree real-time customer profiles. These customer profiles and standardized data are then accessible by other Adobe Experience Cloud applications such as,
Adobe Analytics, Adobe Audience Manager, Adobe Campaign, etc. It is the centralized foundation on which other Adobe applications, services, and third-party solutions can act on with speed, efficiency, and relative ease by combining analytical and operational capabilities.

I worked with the Experience Platform Mobile SDK team, whose main goal is to allow users to quickly and easily integrate their applications(s) with Adobe Experience Cloud solutions and services. The SDK is flexible and modular comprised of the Mobile Core and various other SDK extensions that allow for additional optionable capabilities depending on the user’s needs. For example, AEP Edge Network, AEP Consent, AEP Lifecycle, AEP Identity, etc.

AEP diagram

(Check out the Experience League to learn more)

The Adobe Experience Platform Mobile SDK can be implemented across a number of platforms: iOS, Android, React Native, Flutter, etc; however, we have recently had a few customers ask about implementing our SDKs with Apple watchOS. Currently, our AEP SDKs have not been approved or integrated with Apple watchOS. This was the focus of my internship project.

Project scope and main goals

  • Design and build an Apple watchOS sample application using SwiftUI to provide Experience Cloud Customers with a working example of how the mobile SDKs and extensions are implemented.
  • Integrate Adobe Experience Cloud SDK and Extensions with watchOS.
  • Identify any differences in SDK implementation/execution between the mobile SDK and its performance with watchOS.
  • Enable push-notification functionality from Assurance.
  • Document and make necessary changes to source code to reach functionality.

Challenges

  • Apple WatchOS applications, in some cases, are not recognized by AEP Core and other extensions.
  • AEP SDKs platform and targetEnvironment need to be modified to allow for watchOS implementations.
  • AEP SDK requires use of frameworks that WatchOS doesn’t have access to.
  • Class differences between iOS and watchOS.
  • Unavailability of the webkit framework and WKWebView, which allows user to incorporate web content seamlessly into app’s UI.
  • Default WebViewSocket connection not compatible with watchOS.
  • Implementing push notifications with watchOS requires an additional header field (apns-push-type) in the APNS request.

AEP Sample WatchOS App

To begin integrating the AEP Mobile SDKs with watchOS, I first needed a standalone watchOS sample application. Adobe has already developed a similar application to provide users with an example of how to implement the SDKs with iOS using both Objective-C and Swift implementations, so I designed the watchOS app similar to the existing sample application.

(adobe/aepsdk-sample-app-ios)

My goal for the AEP sample watchOS application was to keep the UI clean and minimal where every button and all functionality is self-explanatory.

On the app’s main landing page, all the views correspond to the following available AEP Extensions:

  • AEP Core: contains the main functionality of the SDK, including Experience Cloud Identity services, data event hub, Rules Engine, reusable networking, and disk access routines.
  • AEP Edge Network: sends data to Platform.
  • AEP Consent: manages user data collection preferences.
  • AEP EdgeIdentity: enables identity management from your mobile app to the Edge Network.
  • AEP Assurance: used for debugging and validating SDK implementation.
  • AEP Messaging: collects user push tokens and manages interaction measurements with platform services.

My plan was to have the main menu view that listed all the SDK views and when selecting one of these views further options would be displayed to implement the specific SDK capabilities.

For example, when viewing the Core page, users have the option to change privacy status, dispatch custom events, set advertising identifiers, or retrieve the Experience Cloud Id. Here is an example of the Core view code and UI where Mobile Core functionality is implemented.

I continued this process to create individual views for all the extensions.
After building the UI that can implement the full functionality of the SDKs, it was time to install and register the extensions.

Adding WatchOS as a supported platform

First, a few changes needed to be made to the AEP extensions to allow for watchOS implementation. The AEP Swift iOS SDKs are open source and are available here: (Current AEP SDK Versions) — also, feel free to read more about our move to Swift and open source.

Within the AEPCore, AEPServices, AEPIdentity, AEPSignal, and AEPLifecycle extensions, which represent the foundation of the Adobe Experience Platform SDK, the available supported platforms needed to be updated to include watchOS. Depending on how you plan to install the SDKs, either the Package.swift or .podspec files need to include watchOS as an available platform.

AEPCore.podspec

...
s.platform = :ios, :watchos, :tvos
s.platforms = { :ios => "10.0", :watchos => "3.0", :tvos => "10.0" }
s.watchos.deployment_target = '3.0'
...

aepsdk-core-ios-main/Package

import PackageDescriptionlet package = Package(
name: "AEPCore",
platforms: [.iOS(.v10), .tvOS(.v10), .watchOS(.v3)],
products: [
.library(name: "AEPCore", targets: ["AEPCore"]),
.library(name: "AEPIdentity", targets: ["AEPIdentity"]),
.library(name: "AEPLifecycle", targets: ["AEPLifecycle"]),
.library(name: "AEPServices", targets: ["AEPServices"]),
.library(name: "AEPSignal", targets: ["AEPSignal"])
...

Using watchOS-specific libraries and classes

Further changes to the SDK involved using the WatchKit framework for watchOS instead of the UIKit which is normally used for iOS mobile devices. For example, within AEPServices > Sources > ApplicationSystemInfoService.swift, in order to get watchOS system information we must use WKInterfaceDevice instead of UIDevice.

AEPServices/Sources/ApplicationSystemInfoService.swift

...
// Get WatchOS system information
let model = WKInterfaceDevice.current().model
let osVersion = WKInterfaceDevice.current().systemVersion.replacingOccurrences(of: ".", with: "_")
...

I won’t go into too much detail on how to install the SDKs with this application as the process is nearly the same for mobile iOS, however, I will detail the changes needed specifically for watchOS. If you are curious about the installation process, please see the documentation. Or if you prefer a tutorial example, please see the Experience League.

If you aren’t familiar with iOS development, the AppDelegate.swift file is the entry point for the mobile app. With a standalone watchOS application, the ExtensionDelegate.swift is the entry point. This is where we import and register our extensions.

ExtensionDelegate.swift

import Foundation
import WatchKit
import UserNotifications
import AEPCore
import AEPEdge
import AEPIdentity
import AEPEdgeIdentity
import AEPMessaging
import AEPLifecycle
import AEPUserProfile
import AEPSignal
import AEPServices
import AEPEdgeConsent
import AEPAssurance
import AEPOptimize
import UIKit
import CoreData
///// Entry point of the watch app.
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
let extensions = [Edge.self, Lifecycle.self, UserProfile.self, Consent.self, AEPIdentity.Identity.self,
AEPEdgeIdentity.Identity.self, Assurance.self, UserProfile.self, Signal.self, Messaging.self]MobileCore.setLogLevel(.trace)
MobileCore.registerExtensions(extensions, {
MobileCore.configureWith(appId: "*************************************")
MobileCore.updateConfigurationWith(configDict: ["messaging.useSandbox": true])
MobileCore.lifecycleStart(additionalContextData: ["contextDataKey": "contextDataVal"])
setupRemoteNotifications()
}

The ExtensionDelegate is also where we ask the user for permission to receive remote notifications, and if granted, take the additional steps to share the PushIdentifier with AEP in order to later implement our push notifications.

ExtensionDelegate.swift

// MARK:  - Notification Methods
extension ExtensionDelegate: UNUserNotificationCenterDelegate {
func setupRemoteNotifications() {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (granted,
error) in} }}if granted {
print("Permission Granted")
DispatchQueue.main.async {
WKExtension.shared().registerForRemoteNotifications()
}
func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
// Convert token to string
let deviceTokenString = deviceToken.map { data in String(format: "%02.2hhx", data) }.joined()
print("Device Token: \(deviceTokenString)")
print("did Register for Remote Notifications called!!")
// Send push token to experience platformMobileCore.setPushIdentifier(deviceToken)
}

The following methods handle the notifications arriving while the app is in variable states.

ExtensionDelegate.swift

// MARK: - Handle Push Notification Interactions
// Receiving Notifications
// Delegate method to handle a notification that arrived while the app was running in the foreground.
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {let userInfo = notification.request.content.userInfo
if let apsPayload = userInfo as? [String: Any] {
NotificationCenter.default.post(name: Notification.Name("T"), object: self, userInfo: apsPayload)print(apsPayload)
}
completionHandler([.banner, .badge, .sound])
}
//Receive Notifications while app is in backgroundfunc didReceiveRemoteNotification(_ userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler:
@escaping (WKBackgroundFetchResult) -> Void) {
if let apsPayload = userInfo as? [String: Any] {
NotificationCenter.default.post(name: Notification.Name("TEST NAME"), object: self, userInfo:
apsPayload)
}
completionHandler(WKBackgroundFetchResult.newData)
}
// Handling the Selection of Custom Actions
// Delegate method to process the user's response to a delivered notification.
func userNotificationCenter(_: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// Perform the task associated with the action.
switch response.actionIdentifier {
case "ACCEPT_ACTION":
Messaging.handleNotificationResponse(response, applicationOpened: true, customActionId:
"ACCEPT_ACTION")
case "DECLINE_ACTION":
Messaging.handleNotificationResponse(response, applicationOpened: false, customActionId:
"DECLINE_ACTION")// Handle other actions...
default:
Messaging.handleNotificationResponse(response, applicationOpened: true, customActionId: nil)
}
completionHandler()
}
}

Now that the extensions have been installed and registered, it was time to go about implementing each of the views and extension capabilities. For more in-depth details and outcomes of implementing the SDKs with the watchOS application, please see the following document.

AEP Assurance

Integrating AEP Assurance with watchOS required a bit more work than the other SDKs. To give a bit more background, Adobe Experience Platform Assurance provides real-time inspection, proofing, simulation, and validation of collected events from the Mobile SDKs to the Adobe Edge Network. This tool allows the user to view all raw data collected in the application that has been forwarded to the Edge network. To learn more about AEP Assurance, please see the documentation here. To learn more about the installation and setup process of Assurance, please see the documentation here.

Assurance was designed to easily integrate with any iOS mobile application using a deeplink/base URL in order to directly and quickly connect to an Assurance session. Assurance essentially creates a WKWebView where the user is presented with a modal asking for a 4-digit PIN to securely connect to Assurance. A WebViewSocket is then called to establish this connection with the provided parameters.

With watchOS development, the WKWebView is not an object this platform has access to, so unfortunately we have to create these views ourselves. I started by creating an Assurance view that confirms the assuranceSessionUrl as well as a pin code view to allow users to input their connection PIN.

After the user inputs their PIN code, Assurance.startSession() is called passing the sessionUrl and pin code to the SDK. The SDK then saves both the URL and pin code in a shared state and then using the NativeSocket, establishes a connection with Assurance. The following shows the code for startSession within the Assurance SDK where previously socketUrl and pinCode were parameters retrieved from the WebView.

AEPAssurance/Source/AssuranceSession.swift

/// Called this method to start an Assurance session.
/// If the session was already connected, It will resume the connection.
/// Otherwise PinCode screen is presented for establishing a new connection.
func startSession() {
// assuranceExtension.shareState()
canProcessSDKEvents = true
if socket.socketState == .open || socket.socketState == .connecting {Log.debug(label: AssuranceConstants.LOG_TAG, "There is already an ongoing Assurance session.
Ignoring to start new session.")
return
}
//if there is a socket URL already connected in the previous session, reuse it.
if let socketURL = assuranceExtension.connectedWebSocketURL {
session.”)
// self.statusUI.display()
guard let url = URL(string: socketURL) else {
Log.warning(label: AssuranceConstants.LOG_TAG, "Invalid socket url. Ignoring to start newreturn }socket.connect(withUrl: url)
return }
// if there were no previous connected URL then start a new sessionbeginNewSession()
}
/// Called when a valid assurance deep link url is received from the startSession API
/// Calling this method will attempt to display the pinCode screen for session authentication
///
/// Thread : Listener thread from EventHub
func beginNewSession() {
guard let pincode = assuranceExtension.pincode else {
Log.debug(label: AssuranceConstants.LOG_TAG, "Start Session API called with no Pincode")
return
}
guard let orgID = getURLEncodedOrgID() else {
Log.debug(label: AssuranceConstants.LOG_TAG, "Start Session API called with no OrgId")
return
}guard let sessionId = assuranceExtension.sessionId else {
Log.debug(label: AssuranceConstants.LOG_TAG, "Start Session API called with no SessionId")
return
}let socketURL = String(format: AssuranceConstants.BASE_SOCKET_URL,
assuranceExtension.environment.urlFormat,
sessionId,
pincode,
orgID,
assuranceExtension.clientID)
guard let url = URL(string: socketURL) else {
Log.warning(label: AssuranceConstants.LOG_TAG, "Invalid socket url. Ignoring to start new session.")
return
}Log.debug(label: AssuranceConstants.LOG_TAG, "Attempting to make a socket connection with URL : \(url)")socket.connect(withUrl: url)// Save socketURL in State
assuranceExtension.connectedWebSocketURL = socketURL
if socket.socketState == .unknown {
socket.disconnect()
Log.debug(label: AssuranceConstants.LOG_TAG, "Disconnecting Socket cause it's in an unknown state.")
return
}
}

After entering the correct pin code and session URL, we can now successfully connect to Adobe Experience Platform Assurance with watchOS.

Assurance session now connects; however, there was an issue where the session would only remain connected for exactly one minute. What I found was that when we set up registerCallbacks() in the nativeSocket, we call .receive which accepts a completion handler, but if we don’t call .receive again after receiving the first message it automatically unregisters from receiving further messages. To solve this issue, we need to re-registering the .receive callback after every message.

AEPAssurance/Source/Assurance.swift

// MARK: - Private methodsprivate func registerCallbacks() {
socketTask?.receive {[weak self] result in
switch result {
case .success(let response):
switch response {
case .string(let message):
self?.didReceiveMessage(message)
case .data(let data):
self?.didReceiveBinaryData(data)
@unknown default:
Log.debug(label: AssuranceConstants.LOG_TAG, "Unknown format data received from socket.
Ignoring incoming event")
}
case .failure(let error):
self?.didReceiveError(error)return }
self?.registerCallbacks()
}
}

Success! We now stay connected to Assurance! For full details on changes made to the Assurance SDK to integrate watchOS, please see the pull request here.

Conclusion

I hope you found this article insightful. If you’re curious and want to learn more I recommend visiting the Adobe Experience Platform Mobile SDK documentation or visiting the Adobe Experience League. Finally, here is the repository for the AEP SDK Sample App for watchOS in its entirety.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response