UserNotifications schedules fewer notifications in Catalyst

34 Views Asked by At

I have a loop that schedules 64 notifications for a calendar app and it works well in iOS, but it seems that on Catalyst it schedules fewer than 64 notifications. The number varies, and to make things worse after a restart of the device it will schedule 64 until it starts to fail again. It usually schedules between 49 and 54. Things I can confirm:

  • there are 64 filteredEvents
  • the loop does run 64 times
  • the limit on Catalyst is actually 100, so it should work
  • notifications are authorised (if they weren't I'd get 0 scheduled)
  • there is no particular order to the ones that seem to fail
  • I can confirm that it's not just that pendingNotificationRequests().count is wrong, the notifications aren't getting delivered.
  • I suspected a concurrency issue, perhaps scheduling them too fast, and I tried a delay of 100ms between each call to center.add(request), but it made no difference
  • I used to not use the async version of center.add(request) and I hoped adding it would fix the issue, but it seems that was wrong too.
  • Setting the limit to 48 seems to work, but I don't know if that is still going to be the case on various devices, since I don't know what is causing this.
  • I tried replicating this with a simpler setup and a different project and I'm getting the same issue.

Any suggestions?

    import Foundation
import UserNotifications

class UserNotifications {
    
    static let center = UNUserNotificationCenter.current()
    static var isUpdatingNotifications = false
    
    static func requestAuthorisation() async {
        do { try await center.requestAuthorization(options: [.alert, .sound, .badge]) }
        catch { print("Error authorising notifications: \(error)") }
    }
    
    static func addScheduledNotification(date: Date,
                                        identifier: String,
                                        title: String?,
                                        body: String? = nil,
                                        sound: String? = nil,
                                        actions: UNNotificationCategory? = nil,
                                        interruptionLevel: UNNotificationInterruptionLevel = .active,
                                        userInfo: [AnyHashable : Any]? = nil) async throws {
        let trigger = UNCalendarNotificationTrigger(dateMatching: cal.dateComponents([.second, .minute, .hour, .day, .month, .year], from: date), repeats: false)
        let content = content(title: title, body: body, sound: sound, actions: actions)
        content.userInfo = userInfo ?? [:]
        content.interruptionLevel = interruptionLevel
        let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
        try await center.add(request)
    }

    static func updateNotifications(from: String) async {
        guard !isUpdatingNotifications else { return }
        isUpdatingNotifications = true

        // 1. We clear all delivered and pending notifications
        center.removeAllPendingNotificationRequests()

        // 2. EventKit has on one occasion not recognised the alarms for birthday events, returning null instead, so we will add an alert here temporarily.
        let events = DataStorage.events.map { event in
            if event.title.contains("Birthday") && !event.hasAlarms { event.addAlarm(EKAlarm(relativeOffset: -54000.0)) }
            return event
        }
        
        // 3. We filter all the relevant events and limit to 64. Please note that the limit on the mac is 100, but there's no need for so many.
        let filteredEvents = events.filter({ event in
            (event.alarms?.contains(where: { alarm in
                (alarm.absoluteDate?.timeIntervalSinceNow ?? event.startDate.addingTimeInterval(alarm.relativeOffset).timeIntervalSinceNow) > 0
            }) ?? false) && event.currentUserStatus != .declined && event.currentUserStatus != .pending
        }).sorted { $0.startDate < $1.startDate }.prefix(64)
        
        // 4. We then schedule 64 notifications
        var notificationsScheduled = 0
        let limit = 64
        
        outerLoop: for event in filteredEvents {
            guard let alarms = event.alarms?.excludingRepeatingAlarms() else { continue }
            for alarm in alarms {
                if notificationsScheduled >= limit { break outerLoop }
            
                let date = alarm.absoluteDate ?? event.startDate.addingTimeInterval(alarm.relativeOffset)
                guard date.isInTheFuture else { continue }
            
                var category: UNNotificationCategory? = nil
                if event.videoConferenceDetails != nil {
                    let action = UNNotificationAction(identifier: "JOIN_MEETING", title: "Join meeting", options: [.foreground])
                    category = UNNotificationCategory(identifier: "MEETING_INVITE_CATEGORY", actions: [action], intentIdentifiers: [], options: [])
                }
                
                do {
                    try await UserNotifications.addScheduledNotification(date: date,
                                                                        identifier: UUID().uuidString,
                                                                        title: event.title,
                                                                        body: event.body(forAlarm: alarm, alertDate: date),
                                                                        sound: "Notification5.mp3",
                                                                        actions: category,
                                                                        interruptionLevel: .timeSensitive,
                                                                        userInfo: ["title": event.title ?? "",
                                                                                    "startDate": event.startDate ?? Date(),
                                                                                    "meetingUrl": event.videoConferenceDetails?.0.absoluteString ?? ""])
                } catch {
                    print("Failed to add notification #\(notificationsScheduled)")
                }
                notificationsScheduled += 1
            }
        }
        
        print("\(await UserNotifications.center.pendingNotificationRequests().count) notifications scheduled")
        isUpdatingNotifications = false
    }
}

extension Array where Element == EKAlarm {
    func excludingRepeatingAlarms() -> [Element] {
        return self.reduce(into: [Element]()) { (result, alarm) in
            if !(result.contains(where: { $0.absoluteDate == alarm.absoluteDate && $0.relativeOffset == alarm.relativeOffset })) {
                result.append(alarm)
            }
        }
    }
}
0

There are 0 best solutions below