The latest release of iOS, iOS 13.5, includes beta support for the Exposure Notification API that Apple defined jointly with Google to enable contact tracing apps. Apple also published a sample app to showcase best practices in contact-tracing apps.
Besides providing the actual framework implementation, iOS 13.5 includes the basic user-facing authorization mechanism that lets users opt in to or out of contact logging. No apps using the Exposure Notification API are available yet, which means that the Exposure Logging setting will be disabled by default. When you install a third-party app developed by some public health authority, users will be given the option to opt in or out.
To make things easier for third-parties wanting to build a contact tracing app, Apple has published a sample app providing a reference design. In addition to showing best practices when using the framework, the sample also includes code to simulate central server responses to implement diagnosis key sharing and exposure criteria management.
The Apple sample app checks on launch whether the user has enabled exposure logging, and asks them to enable it in the case that they did not. Contrary to what usually happens with other privileges managed by iOS, the Exposure Notification API provides a mechanism for the app to explicitly trigger the authorization mechanism through the ENManager
singleton:
static func enableExposureNotifications(from viewController: UIViewController) {
ExposureManager.shared.manager.setExposureNotificationEnabled(true) { error in
NotificationCenter.default.post(name: ExposureManager.authorizationStatusChangeNotification, object: nil)
if let error = error as? ENError, error.code == .notAuthorized {
viewController.show(RecommendExposureNotificationsSettingsViewController.make(), sender: nil)
} else if let error = error {
//...
}
}
}
Apple's sample goes as far as asking the user to enable the service twice, if they deny their permission on the first request. This is obviously not a requirement but hints at Apple considering this an accepted practice, which could mean such an aggressive strategy to gather user permission will not be considered cause for rejection in the eventual App Store review process.
The app stores all data it logs locally, including a flag telling whether the user on-boarded or not, data about any test they took, whether they shared it with the server, etc. Not requiring to store all user data on a central server is a key feature of the Exposure Notification protocol, since storing it locally preserves user pricacy.
Only when a user is positively diagnosed COVID-19, they may decide to share it with the central server. This requires the app to retrieve a list of diagnosis keys, which in turns requires the user to provide an explicit authorization each time, and send it to the server:
func getAndPostDiagnosisKeys(testResult: TestResult, completion: @escaping (Error?) -> Void) {
manager.getDiagnosisKeys { temporaryExposureKeys, error in
if let error = error {
completion(error)
} else {
// In this sample app, transmissionRiskLevel isn't set for any of the diagnosis keys. However, it is at this point that an app could
// use information accumulated in testResult to determine a transmissionRiskLevel for each diagnosis key.
Server.shared.postDiagnosisKeys(temporaryExposureKeys!) { error in
completion(error)
}
}
}
}
The sample app also uses a background task to periodically check exposure for users having no COVID-19 diagnosis. The background task identifier shall end in .exposure-notification
, which ensures it automatically receives more background time to complete its operation. Additionally, apps that own such tasks are launched more frequently when they are not running. The background task calls the app's detectExposures
method to check if the user got exposed and re-schedules itself:
BGTaskScheduler.shared.register(forTaskWithIdentifier: AppDelegate.backgroundTaskIdentifier, using: .main) { task in
// Notify the user if bluetooth is off
ExposureManager.shared.showBluetoothOffUserNotificationIfNeeded()
// Perform the exposure detection
let progress = ExposureManager.shared.detectExposures { success in
task.setTaskCompleted(success: success)
}
// Handle running out of time
task.expirationHandler = {
progress.cancel()
LocalStore.shared.exposureDetectionErrorLocalizedDescription = NSLocalizedString("BACKGROUND_TIMEOUT", comment: "Error")
}
// Schedule the next background task
self.scheduleBackgroundTaskIfNeeded()
}
As a final remark, the Exposure Notification framework provides a way to estimate risk each time a contact is detected. This takes into account when the interaction took place and how long it lasted based on the detected device proximity. The app can alter how risk is estimated by providing an ENExposureConfiguration
object, which will be usually sent by the server, and eventually call finish
to update the local store and complete the search. The ENExposureConfiguration
object supports parameters such as a minimum risk, transmission risk, contact duration, days since last exposure, and a few more.
The ENExposureConfiguration
object is passed to the ENManager
singleton detectExposures(configuration:diagnosisKeyURLs:completionHandler:)
method. For each detected exposure, the app can get additional information using getExposureInfo(summary:userExplanation:completionHandler:)
:
Server.shared.getExposureConfiguration { result in
switch result {
case let .success(configuration):
ExposureManager.shared.manager.detectExposures(configuration: configuration, diagnosisKeyURLs: localURLs) { summary, error in
if let error = error {
finish(.failure(error))
return
}
let userExplanation = NSLocalizedString("USER_NOTIFICATION_EXPLANATION", comment: "User notification")
ExposureManager.shared.manager.getExposureInfo(summary: summary!, userExplanation: userExplanation) { exposures, error in
if let error = error {
finish(.failure(error))
return
}
let newExposures = exposures!.map { exposure in
Exposure(date: exposure.date,
duration: exposure.duration,
totalRiskScore: exposure.totalRiskScore,
transmissionRiskLevel: exposure.transmissionRiskLevel)
}
finish(.success((newExposures, nextDiagnosisKeyFileIndex + localURLs.count)))
}
}
case let .failure(error):
finish(.failure(error))
}
}
The Exposure Notification API requires iOS 13.5 and Xcode 11.5.