···2import UIKit
34let APP_GROUP = "group.app.bsky"
000000000000000000000056class NotificationService: UNNotificationServiceExtension {
7- var prefs = UserDefaults(suiteName: APP_GROUP)
089 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
10- guard let bestAttempt = createCopy(request.content),
0011 let reason = request.content.userInfo["reason"] as? String
12 else {
13 contentHandler(request.content)
14 return
15 }
16017 if reason == "chat-message" {
18 mutateWithChatMessage(bestAttempt)
19 } else {
20 mutateWithBadge(bestAttempt)
21 }
22000023 contentHandler(bestAttempt)
24 }
2526 override func serviceExtensionTimeWillExpire() {
27- // If for some reason the alloted time expires, we don't actually want to display a notification
000028 }
2930- func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
31- return content.mutableCopy() as? UNMutableNotificationContent
32- }
3334 func mutateWithBadge(_ content: UNMutableNotificationContent) {
35- var count = prefs?.integer(forKey: "badgeCount") ?? 0
36- count += 1
03738- // Set the new badge number for the notification, then store that value for using later
39- content.badge = NSNumber(value: count)
40- prefs?.setValue(count, forKey: "badgeCount")
041 }
4243 func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
44- if self.prefs?.bool(forKey: "playSoundChat") == true {
45 mutateWithDmSound(content)
46 }
47 }
···54 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
55 }
56}
000000000000000
···2import UIKit
34let APP_GROUP = "group.app.bsky"
5+typealias ContentHandler = (UNNotificationContent) -> Void
6+7+// This extension allows us to do some processing of the received notification
8+// data before displaying the notification to the user. In our use case, there
9+// are a few particular things that we want to do:
10+//
11+// - Determine whether we should play a sound for the notification
12+// - Download and display any images for the notification
13+// - Update the badge count accordingly
14+//
15+// The extension may or may not create a new process to handle a notification.
16+// It is also possible that multiple notifications will be processed by the
17+// same instance of `NotificationService`, though these will happen in
18+// parallel.
19+//
20+// Because multiple instances of `NotificationService` may exist, we should
21+// be careful in accessing preferences that will be mutated _by the
22+// extension itself_. For example, we should not worry about `playChatSound`
23+// changing, since we never mutate that value within the extension itself.
24+// However, since we mutate `badgeCount` frequently, we should ensure that
25+// these updates always run sync with each other and that the have access
26+// to the most recent values.
2728class NotificationService: UNNotificationServiceExtension {
29+ private var contentHandler: ContentHandler?
30+ private var bestAttempt: UNMutableNotificationContent?
3132 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
33+ self.contentHandler = contentHandler
34+35+ guard let bestAttempt = NSEUtil.createCopy(request.content),
36 let reason = request.content.userInfo["reason"] as? String
37 else {
38 contentHandler(request.content)
39 return
40 }
4142+ self.bestAttempt = bestAttempt
43 if reason == "chat-message" {
44 mutateWithChatMessage(bestAttempt)
45 } else {
46 mutateWithBadge(bestAttempt)
47 }
4849+ // Any image downloading (or other network tasks) should be handled at the end
50+ // of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire
51+ // gets called, we might not have all the needed mutations completed in time.
52+53 contentHandler(bestAttempt)
54 }
5556 override func serviceExtensionTimeWillExpire() {
57+ guard let contentHandler = self.contentHandler,
58+ let bestAttempt = self.bestAttempt else {
59+ return
60+ }
61+ contentHandler(bestAttempt)
62 }
6364+ // MARK: Mutations
006566 func mutateWithBadge(_ content: UNMutableNotificationContent) {
67+ NSEUtil.shared.prefsQueue.sync {
68+ var count = NSEUtil.shared.prefs?.integer(forKey: "badgeCount") ?? 0
69+ count += 1
7071+ // Set the new badge number for the notification, then store that value for using later
72+ content.badge = NSNumber(value: count)
73+ NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount")
74+ }
75 }
7677 func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
78+ if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true {
79 mutateWithDmSound(content)
80 }
81 }
···88 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
89 }
90}
91+92+// NSEUtil's purpose is to create a shared instance of `UserDefaults` across
93+// `NotificationService` instances. It also includes a queue so that we can process
94+// updates to `UserDefaults` in parallel.
95+96+private class NSEUtil {
97+ static let shared = NSEUtil()
98+99+ var prefs = UserDefaults(suiteName: APP_GROUP)
100+ var prefsQueue = DispatchQueue(label: "NSEPrefsQueue")
101+102+ static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
103+ return content.mutableCopy() as? UNMutableNotificationContent
104+ }
105+}