Kommentar zum Dev Corner Artikel Live Activities in Mac & i Heft 1/2023

Ich habe ein Abo für Mac & i und bin immer besonders gespannt auf die Developer's Corner.

Kurzfassung

Dieses Mal habe ich den Artikel gelesen, um zu schauen, ob ich noch etwas zur Programmierung des iOS-16-Features "Live Activities" lernen kann. Dabei ist mir als iOS-Entwickler aufgefallen, daß die Passage, bei der es um Notifications geht, schlicht veraltet und falsch, und die, bei der es um Deep Links geht, veraltet und ein Sicherheitsrisiko ist.

Ich beginne mit der Stelle über Notifications und widme mich danach den Problemen mit den Deep Links.

Notifications

Zu Beginn führen sie als Motivation für den Einsatz von Live Activies an, daß diese aktualisiert werden können, während das mit Notifications angeblich nicht möglich wäre:

Apps geben oft Informationen an den Anwender weiter. Aber was ist, wenn dieser die App gerade nicht aktiv nutzt und diese sich im Hintergrund befindet? Bisher konnten Apps dann nur local (von einer App auf dem Gerät initiiert) oder remote (durch einen entfernten Server initiiert) Push Notifications senden, um den Anwender über Ereignisse zu informieren. Unpraktisch war und ist daran vor allem, dass neue Benachrichtigungen bislang nicht die vorherigen und gegebenenfalls veralteten ersetzen, sondern sich stattdessen stapeln.

Das war bis 2015 so. Die Aussagen sind jedoch seit iOS 10 bzw. seit 2016 und der Einführung des User Notifications Framework falsch. Erstmals wurde das in der WWDC 2016 - Session 707 Introduction to Notifications erklärt.

Schauen wir uns zuerst die lokalen Notfications an und weiter unten die Remote Notifications.

Lokale Benachrichtigungen (Local Notifications) löschen und aktualisieren

Lokale Notifications können sowohl gelöscht als auch aktualisiert werden, wie man in diesem Zitat aus dem Transcript des Videos lesen kann:

Now notification management is new with this framework and what this allows you to do is have access to notification that are pending delivery for your locally scheduled notifications, as well as access to delivered notification … Now you can also remove notifications that have already been sent to the user, as well as update and promote these notifications.

Code-Beispiel aus dem PDF zu "Introduction to Notifications", das zeigt, wie man lokale Notifications löscht:

// Pending Notification Removal
let gameStartIdentifier = "game1.start.identifier"
let gameStartRequest = UNNotificationRequest(identifier: gameStartIdentifier,
                                             content: content,
                                             trigger: startTrigger)
UNUserNotificationCenter.current().add(gameStartRequest) { (error) in // ... }
// Game was cancelled
UNUserNotificationCenter.current()
    .removePendingNotificationRequests(withIdentifiers: [gameStartIdentifier])
}

// Delivered Notification Removal
let gameScoreIdentifier = "game1.score.identifier"
let gameScoreRequest = UNNotificationRequest(identifier: gameScoreIdentifier,
                                             content: scoreContent,
                                             trigger: trigger)
UNUserNotificationCenter.current().add(gameScoreRequest) { (error) in // ... }
// Wrong game score was published
UNUserNotificationCenter.current()
    .removeDeliveredNotifications(withIdentifiers: [gameScoreIdentifier])

Code-Beispiel aus dem PDF zu "Introduction to Notifications", das zeigt, wie man lokale Notifications aktualisiert:

// Pending Notification Update
let gameStartIdentifier = "game1.start.identifier"
let gameStartRequest = UNNotificationRequest(identifier: gameStartIdentifier,
                                             content: content,
                                             trigger: startTrigger)
UNUserNotificationCenter.current().add(gameStartRequest) { (error) in // ... }
// Game start time was updated
let updatedGameStartRequest = UNNotificationRequest(identifier: gameStartIdentifier,
                                                    content: content,
                                                    trigger: newStartTrigger)
UNUserNotificationCenter.current().add(updatedGameStartRequest) { (error) in // ... }

// Delivered Notification Update
let gameScoreIdentifier = "game1.score.identifier"
let gameScoreRequest = UNNotificationRequest(identifier: gameScoreIdentifier,
                                             content: scoreContent,
                                             trigger: trigger)
UNUserNotificationCenter.current().add(gameScoreRequest) { (error) in // ... }
// Score game was updated
let updateGameScoreRequest = UNNotificationRequest(identifier: gameScoreIdentifier,
                                                   content: newScoreContent,
                                                   trigger: newTrigger)
UNUserNotificationCenter.current().add(updateGameScoreRequest) { (error) in // ... }

Push Server Benachrichtigungen (Remote Notifications) löschen und aktualisieren

Und auch Remote Notifications kann man sogar nach ihrer Zustellung löschen oder wahlweise aktualisieren:

And for remote notifications there's a new request header the APNS collapse id that you need to set on your payload. The system uses this request identifier to know which notification you're requesting to be removed or updated.

Um eine Remote Notification zu aktualisieren, muß der Push Server einfach nur eine weitere Push Notification mit dem korrigiertem Inhalt und derselben apns-collapse-id schicken:

Multiple notifications with the same collapse identifier are displayed to the user as a single notification.

Um eine Remote Notification zu löschen, muß der Push Server einfach nur eine weitere Push Notification schicken mit derselben apns-id oder derselben apns-collapse-id und content-available mit dem Wert 1. Mit dieser trickreichen Aktualisierung wird aus der normalen sichtbaren eine unsichtbare "silent" Remote Push Notification. Hier ist ein weiteres Beispiel mit einem kurzen Video, das ebenso vorgeht und zusätzlich noch lokal

UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [idToDelete])

aufruft.

Literatur zum Aktualisieren und Löschen von Notifications:

Deep Links

Für Deeplinks in die App verwenden sie ein Custom URL Scheme:

Der letzte Teil definiert den Absprung in die App […]. Hierbei kommt ein Deeplink zum Einsatz. Über Deeplinks kann man Funktionen in Apps direkt aufrufen und optional sogar Parameter übergeben. Die URL folgt dem Schema appscheme: / /methode /parameter / .... Dabei steht das Schema der URL für die App, und der Pfad kann Funktionen und Parameter enthalten. Über den Aufbau des Deeplinks und auf welche Funktionen sie damit von außen Zugriff gewährt, entscheidet jede App selbst. iOS kennt die angebotenen Schemata der installierten Apps und leitet Deeplink-Aufrufe an die entsprechende App weiter. Agenda-App hat das Schema de.agendaapp und verwendet als Pfad next, um den nächsten Tagesordnungspunkt zu beginnen. Link(destination: URL(string: "de.agendaapp://next") !) definiert diesen Link im Code. In der App bearbeitet die AgendaView diesen Aufruf:

.onOpenURL { url in
	guard url.scheme == "de.agendaapp" else { return }
	if url.host () == "next" { 
		agenda.startNextTopic()
	}
}

Apple rät seit der Vorstellung von Universal Links mit iOS 9, also seit 2015, von Custom URL Schemes ab und empfiehlt stattdessen Universal Links zu verwenden. Solche Universal Links haben viele Vorteile, mir geht es in diesem Zusammenhang jedoch speziell um den Sicherheitsgewinn, der einer der Hauptgründe für Apple war, Universal Links einzuführen.

Im Gegensatz zu Custom URL Schemes können Universal Links nicht von anderen Apps feindlich übernommen werden. Je nachdem in welcher Reihenfolge Apps installiert werden, betrachtet iOS die eine oder andere App als zuständig, falls beide dasselbe URL Schema für ihre Deeplinks verwenden.

Das Problem ist seit vielen Jahren bekannt als URL Scheme Hijacking.

David Thiel: iOS Application Security, S. 131

David Thiel (2016), iOS Application Security, S. 131

Warum der Artikel und das Demo-Projekt dennoch veraltete unsichere Technik verwenden, ist mir schleierhaft. Um so mehr als einer der beiden Autoren als "Experte für mobile Sicherheit sowie den Einsatz von iOS-Geräten im Unternehmensumfeld" beschrieben wird.

Eine mögliche Erklärung für solch einen Lapsus könnte sein, daß Universal Links einen Webserver erfordern, mit dem iOS für die App jeden Link zur Sicherheit abgleicht. Evtl. war das der Grund, den simplen, aber unsicheren Weg über Custom URL Schemes zu nehmen. Aber dann würde ich zumindest einen Hinweis erwarten, daß hier der Einfachheit halber eine nicht für die Praxis geeignete Abkürzung genommen wurde.

Literatur zu Custom URL Schemes und Universal Links:

Fazit

Wenn mir fachliche Fehler auffallen bei den Themen, mit denen ich mich selbst gut auskenne, dann frage ich mich, ob andere Themenbereiche im Artikel, mit denen ich mich weniger auskenne, vielleicht auch problematisch sind. Mac & i sollte gründlicher recherchieren, wenn sie nicht das Leservertrauen in ihre Artikel verlieren wollen.

Valid XHTML 1.0!

Besucherzähler


Latest Update: 07. May 2023 at 14:08h (german time)
Link: osx.realmacmark.de/blog/osx_blog_2023-05-a.php