Skip to content
academia.sh

Lesson 19 / 19

Push Notifications

A measurement of how raising the badge locally on a notification from the server is repeat-sensitive and how an absolute badge value in the payload is order-sensitive, and the wrap-up table of the nineteen lessons of the course.

Published Updated

In this lesson

  • Separates that the delivery of a push notification is not guaranteed and that it can arrive twice
  • Measures that raising the badge locally is repeat-sensitive and carrying an absolute value in the payload is order-sensitive
  • Shows that filtering by notification identifier solves repetition but not order
  • Reads the nineteen lessons of the course in a single table by their sensitivities
Contents

In the previous lesson, two devices writing the same record took the order out of the app’s hands. In a push notification, the order and the count move one step further away: the server prepares the message, the platform’s push notification service carries it, and the app sees it only when it is delivered. This service does not guarantee delivery; a notification can be delayed, can never arrive, or can arrive twice. The question of the course is the same at this end too: if this operation is repeated or reordered, does the result change? This lesson is our last one; after the measurement, the nineteen lessons of the course are gathered in a single table.

// Replay.swift — classifies an operation's sensitivity to repetition and order
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "robust"
        case (true, false): return "repeat-sensitive"
        case (false, true): return "order-sensitive"
        case (true, true): return "doubly-sensitive"
        }
    }

    func report() -> String {
        print("\(label) | once=\(once) twice=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["robust", "repeat-sensitive", "order-sensitive", "doubly-sensitive"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}

once is the operation applied to the starting state one time, twice is two times; ab is a first and then b, and ba is the reverse order. The model below builds the state of the app that changes with a notification: a badge count, a device token, and the handled notification identifiers.

Permission and Registration Flow

The app takes two steps to be able to receive notifications. First it asks the user for permission, then it asks the platform for a device token. The token is the address of this app on this device and is passed to the server; the server sends the notification to this address. The fragment is SOURCE, because it needs a real permission screen and a network connection.

import UIKit
import UserNotifications

let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .badge, .sound])

if granted {
    await MainActor.run {
        UIApplication.shared.registerForRemoteNotifications()
    }
}

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let hex = deviceToken.map { String(format: "%02x", $0) }.joined()
    print(hex)
}

If the user refuses the permission, the registration call is not made and no notification arrives; after a refusal the permission can be turned on from settings, so the app reads the permission status again at every launch. The token is not fixed: the platform can renew it, so the app renews the registration call at every launch and reports the incoming value to the server again. The result of this repetition is one of the three operations this lesson measures.

The Model of the Notification

The model keeps three states. The badge is the number on the app’s icon. The token is the last saved device address. The handled set is the notification identifiers the app has processed before, and it fills only in the operation that filters by identifier. When printing, the handled set is not shown if it is empty.

// PushModel.swift — a pure Swift model of the badge, token, and handled notifications
struct PushState: Equatable, CustomStringConvertible {
    private(set) var badgeCount: Int
    private(set) var token: String
    private(set) var handled: Set<String>

    init(badgeCount: Int, token: String = "-", handled: Set<String> = []) {
        self.badgeCount = badgeCount
        self.token = token
        self.handled = handled
    }

    var description: String {
        let base = "badge=\(badgeCount) token=\(token)"
        return handled.isEmpty ? base : base + " handled=" + handled.sorted().joined(separator: ",")
    }

    func incrementing() -> PushState {
        PushState(badgeCount: badgeCount + 1, token: token, handled: handled)
    }

    func setting(badgeCount value: Int) -> PushState {
        PushState(badgeCount: value, token: token, handled: handled)
    }

    func registering(token value: String) -> PushState {
        PushState(badgeCount: badgeCount, token: value, handled: handled)
    }

    func incrementing(onceFor id: String) -> PushState {
        guard !handled.contains(id) else { return self }
        return PushState(badgeCount: badgeCount + 1, token: token, handled: handled.union([id]))
    }
}

func incrementingBadge() -> (PushState) -> PushState {
    { $0.incrementing() }
}

func settingBadge(_ value: Int) -> (PushState) -> PushState {
    { $0.setting(badgeCount: value) }
}

func registeringToken(_ value: String) -> (PushState) -> PushState {
    { $0.registering(token: value) }
}

func incrementingOnce(id: String) -> (PushState) -> PushState {
    { $0.incrementing(onceFor: id) }
}

PS21. The model builds each notification as an operation applied exactly once. How often the platform can deliver the same notification zero, one, or several times was not measured; what is measured is what happens if the delivery is twice.

PS22. The absolute badge value is computed on the server and carried in the payload; the model assumes the value is computed correctly. How the server finds this number was not measured.

PS23. The notification identifier is assumed to be unique and fixed. In a real service, how the identifier is produced and whether it is kept in a retry were not measured.

Measuring Three Operations

Three operations are built. The first is raising the badge locally at every delivery. The second is carrying the absolute badge value in the payload: the server writes the last count into the badge field inside aps, and the app uses it as it is. The third is registering the same token again. The badge starts at three.

// main.swift — badge and token operations
let start = PushState(badgeCount: 3)
let local = replay("badge-local-increment", from: start, incrementingBadge(), then: incrementingBadge()).report()
let absolute = replay("badge-absolute-value", from: start, settingBadge(4), then: settingBadge(7)).report()
let register = replay("token-reregister", from: start, registeringToken("t1"), then: registeringToken("t1")).report()

countSensitivities([local, absolute, register])
badge-local-increment | once=badge=4 token=- twice=badge=5 token=- ab=badge=5 token=- ba=badge=5 token=- -> repeat-sensitive
badge-absolute-value | once=badge=4 token=- twice=badge=4 token=- ab=badge=7 token=- ba=badge=4 token=- -> order-sensitive
token-reregister | once=badge=3 token=t1 twice=badge=3 token=t1 ab=badge=3 token=t1 ba=badge=3 token=t1 -> robust
robust: 1
repeat-sensitive: 1
order-sensitive: 1
doubly-sensitive: 0

The first line is the same as the badge-increment operation in the Network Client lesson: one delivery raises the badge to four, two deliveries to five. If the notification is delivered twice, the user sees an unread message that does not really exist. The operation is repeat-sensitive.

In the second line the payload carries the number, so the second delivery of the same notification does not change the result: once and twice are the same. But two different notifications carry different numbers, and the badge stays at the value of the one that comes later: ab is seven, ba is four. The absolute value solved repetition, not order; the operation is order-sensitive. It has the same structure as the last writer wins rule of the previous lesson.

In the third line, registering the same value again does not change the state; the order is meaningless too, because the two operations are the same. The operation is robust. This is why renewing the token is safe: the server acts as if it received the same address twice.

The count is one robust, one repeat-sensitive, one order-sensitive.

Delivery Is Not Guaranteed

The measurement builds the case where a notification is delivered twice as two applications of the operation. In reality, how many times and in what order the delivery will happen is in the platform’s hands. A notification may never arrive: the device is off, it cannot reach the network, the user has withdrawn the permission, or the platform has dropped the notification. When two notifications are sent, the second can arrive first. So a notification cannot be the app’s single source of truth.

Two conclusions follow. The first is that the information a notification carries must be readable again from the server when the app opens; a notification is a hint, not a record. The second is that the local change a notification makes must either be protected against repetition or not be made at all. Raising the badge locally requires the first way, and the absolute value is a form of the second way. This choice is a design decision and not a rule, and the rule does not make it for us.

The Bounding Measurement

The rule sees the inequality; it does not see how many times the delivery happened. We measure the same badge operation again, this time with a notification identifier: the app processes an identifier only once. We also try the case where two notifications carry the same absolute value.

// main.swift — filtering by identifier and equal absolute value
let start = PushState(badgeCount: 3)
let sameNotification = replay("identified-same-notification", from: start, incrementingOnce(id: "n1"), then: incrementingOnce(id: "n1")).report()
let twoNotifications = replay("identified-two-notifications", from: start, incrementingOnce(id: "n1"), then: incrementingOnce(id: "n2")).report()
let equalAbsolute = replay("absolute-equal-value", from: start, settingBadge(4), then: settingBadge(4)).report()

countSensitivities([sameNotification, twoNotifications, equalAbsolute])
identified-same-notification | once=badge=4 token=- handled=n1 twice=badge=4 token=- handled=n1 ab=badge=4 token=- handled=n1 ba=badge=4 token=- handled=n1 -> robust
identified-two-notifications | once=badge=4 token=- handled=n1 twice=badge=4 token=- handled=n1 ab=badge=5 token=- handled=n1,n2 ba=badge=5 token=- handled=n1,n2 -> robust
absolute-equal-value | once=badge=4 token=- twice=badge=4 token=- ab=badge=4 token=- ba=badge=4 token=- -> robust
robust: 3
repeat-sensitive: 0
order-sensitive: 0
doubly-sensitive: 0

All three operations came out robust. Filtering by identifier ignored the second delivery of the same notification; with two different notifications, the total of the increase is independent of the order, because the handled set is kept as a set and printed in sorted order. The equal absolute value also hid the difference in order: if two notifications carry the same number, nobody can see which one came first.

Here the rule fails to see two things. The first is whether the notification was really delivered twice: the measurement does not show whether the filtering works, it only shows that the result will stay the same if the delivery is twice. The second is whether the equal value is a coincidence or a design. The measured class is the result of a setup, not of the nature of the operation.

Choosing a Storage

The six lessons of the persistence topic support a choice: which storage the data goes into. This choice is not reduced to a number; sensitivity is not the only criterion of the choice. The shape of the data determines the criteria.

  • Small, single-valued, and non-secret settings go into the preference store.
  • Secret values such as passwords, tokens, and keys go into the keychain.
  • Documents, media, and caches that the app produces itself go into the file system; which directory is backed up is thought about separately.
  • Records that are queried, related, and numerous go into the local database or the object persistence framework.
  • A record that must be the same on several devices goes into cloud sync, and the conflict rule is written from the start.

In this list a notification is not a storage; it is an event that changes state. Every change a notification brings is written into one of the places above and inherits the class of that place.

Summary

  • The platform’s push notification service does not guarantee delivery; a notification can be delayed, can never arrive, or can arrive twice.
  • Raising the badge locally at every delivery is repeat-sensitive: one delivery raises the badge to four, two deliveries to five.
  • Carrying an absolute badge value in the payload solves repetition but not order: ab gives seven, ba gives four; the class is order-sensitive.
  • Registering the same token again is robust; filtering by identifier also makes the badge increase robust.
  • The real delivery count, the order, and the production of the identifier were not measured; they stayed as PS21–PS23.
  • This lesson’s sensitivity: repeat-sensitive.

Course Wrap-Up

The nineteen lessons of the course moved with the same question: if the operation is repeated or reordered, does the result change? The table gives, in a single row, the operation each lesson measured, the sensitivity written in the lesson’s Summary section, and the bounding measurement.

Lesson Operation Sensitivity Bounding measurement
The Network Client cache write, keyed send, retry, badge increment, preference write, journal append all four (1/2/2/1) starting state: robust from zero, repeat-sensitive from one
JSON Encoding and Decoding appending the decoded record to a list, writing by identifier, encode-decode order-sensitive equality of raw text and decoded value
Errors and Retries order without and with a key, spending a budget repeat-sensitive a lost response and a request that did not arrive cannot be told apart
Authentication last writer wins, larger stamp wins order-sensitive the second use of the refresh token is rejected
Image and Data Caching same key, two keys, write-read robust empty and full cache
Background Transfers enqueueing without checking, recording by identifier repeat-sensitive the system decides the order of completion and enqueueing
Asynchronous Functions reserving stock, reading stock, separate shelves order-sensitive remaining stock and the acceptance ledger
Tasks and Task Groups group set, arrival order, group total robust (in the set), order-sensitive (in the array) list and count view
Actors seat request, ticket sale, release order-sensitive (seat), repeat-sensitive (counter) suspension point not measured
Main Thread Isolation unversioned and versioned write, flag, counter order-sensitive (unversioned), robust (versioned) doubly sensitive if the number is given on the arrival of the response
Queue-Based Concurrency append to the end, assigning a value, unique insertion doubly sensitive (append), order-sensitive (assignment) sorted list and row count
Reactive Streams subscription, dropping repeats, accumulation, latest value repeat-sensitive (unshared), order-sensitive (different value), robust (shared) equal and different value pair
Simple Preference Storage two screens same key, separate key, launch counter order-sensitive three default values
The Keychain add, upsert repeat-sensitive robust if the status code is not part of the comparison
The File System append to the end, atomic write, creating a directory doubly sensitive robust when the file is absent, repeat-sensitive when present
Local Databases upsert, auto-increment key robust read in key order is robust, read in insertion order is order-sensitive
Object Persistence Frameworks object creation, unique attribute repeat-sensitive repeat-sensitive when the constraint is off
Cloud Sync writes of two devices, set union order-sensitive arrival order or stamp rule
Push Notifications raising the badge locally, absolute badge in the payload, re-registering the token repeat-sensitive filtering by identifier is robust; delivery count and order not measured

The eighteen lessons other than the opening one, this lesson included, are distributed as follows: robust 3, repeat-sensitive 6, order-sensitive 7, doubly sensitive 2. In the lessons with mixed classes, the class written first in the Summary was counted.

The core of the opening lesson is a separate count: six operations were distributed to the four classes as 1, 2, 2, and 1. The two distributions are not the same thing. The core counts six operations; the distribution above counts the sensitivities of the eighteen lessons other than the opening one.

Three observations come out of the table. The same operation falls into a different class depending on the starting state, the compared state, and the rule; no class is a fixed property of an operation. A measure that solves repetition, idempotence or filtering by identifier, does not solve order; a measure that solves order, a stamp or a version, must solve repetition from somewhere else. And a measurement is always inside a model: the platform’s real delivery, timing, and policy are recorded as not measured in the bounding measurement column.

The next course, M30/K06, deals with architecture, testing, and publishing: how an iOS app is divided into layers, how it is tested, and how it is taken to the store.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close