···22import UIKit
3344let APP_GROUP = "group.app.bsky"
55+typealias ContentHandler = (UNNotificationContent) -> Void
66+77+// This extension allows us to do some processing of the received notification
88+// data before displaying the notification to the user. In our use case, there
99+// are a few particular things that we want to do:
1010+//
1111+// - Determine whether we should play a sound for the notification
1212+// - Download and display any images for the notification
1313+// - Update the badge count accordingly
1414+//
1515+// The extension may or may not create a new process to handle a notification.
1616+// It is also possible that multiple notifications will be processed by the
1717+// same instance of `NotificationService`, though these will happen in
1818+// parallel.
1919+//
2020+// Because multiple instances of `NotificationService` may exist, we should
2121+// be careful in accessing preferences that will be mutated _by the
2222+// extension itself_. For example, we should not worry about `playChatSound`
2323+// changing, since we never mutate that value within the extension itself.
2424+// However, since we mutate `badgeCount` frequently, we should ensure that
2525+// these updates always run sync with each other and that the have access
2626+// to the most recent values.
527628class NotificationService: UNNotificationServiceExtension {
77- var prefs = UserDefaults(suiteName: APP_GROUP)
2929+ private var contentHandler: ContentHandler?
3030+ private var bestAttempt: UNMutableNotificationContent?
831932 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
1010- guard let bestAttempt = createCopy(request.content),
3333+ self.contentHandler = contentHandler
3434+3535+ guard let bestAttempt = NSEUtil.createCopy(request.content),
1136 let reason = request.content.userInfo["reason"] as? String
1237 else {
1338 contentHandler(request.content)
1439 return
1540 }
16414242+ self.bestAttempt = bestAttempt
1743 if reason == "chat-message" {
1844 mutateWithChatMessage(bestAttempt)
1945 } else {
2046 mutateWithBadge(bestAttempt)
2147 }
22484949+ // Any image downloading (or other network tasks) should be handled at the end
5050+ // of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire
5151+ // gets called, we might not have all the needed mutations completed in time.
5252+2353 contentHandler(bestAttempt)
2454 }
25552656 override func serviceExtensionTimeWillExpire() {
2727- // If for some reason the alloted time expires, we don't actually want to display a notification
5757+ guard let contentHandler = self.contentHandler,
5858+ let bestAttempt = self.bestAttempt else {
5959+ return
6060+ }
6161+ contentHandler(bestAttempt)
2862 }
29633030- func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
3131- return content.mutableCopy() as? UNMutableNotificationContent
3232- }
6464+ // MARK: Mutations
33653466 func mutateWithBadge(_ content: UNMutableNotificationContent) {
3535- var count = prefs?.integer(forKey: "badgeCount") ?? 0
3636- count += 1
6767+ NSEUtil.shared.prefsQueue.sync {
6868+ var count = NSEUtil.shared.prefs?.integer(forKey: "badgeCount") ?? 0
6969+ count += 1
37703838- // Set the new badge number for the notification, then store that value for using later
3939- content.badge = NSNumber(value: count)
4040- prefs?.setValue(count, forKey: "badgeCount")
7171+ // Set the new badge number for the notification, then store that value for using later
7272+ content.badge = NSNumber(value: count)
7373+ NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount")
7474+ }
4175 }
42764377 func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
4444- if self.prefs?.bool(forKey: "playSoundChat") == true {
7878+ if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true {
4579 mutateWithDmSound(content)
4680 }
4781 }
···5488 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
5589 }
5690}
9191+9292+// NSEUtil's purpose is to create a shared instance of `UserDefaults` across
9393+// `NotificationService` instances. It also includes a queue so that we can process
9494+// updates to `UserDefaults` in parallel.
9595+9696+private class NSEUtil {
9797+ static let shared = NSEUtil()
9898+9999+ var prefs = UserDefaults(suiteName: APP_GROUP)
100100+ var prefsQueue = DispatchQueue(label: "NSEPrefsQueue")
101101+102102+ static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
103103+ return content.mutableCopy() as? UNMutableNotificationContent
104104+ }
105105+}