Will a local notification action execute if selected while an app is not running?

I use local notifications to notify my users when a session is ready to begin or end. My app performs several tasks when a session is started or ended. Currently, the user has to launch the app to manually start/end a session. I would like to offer notification actions to start/end a session without launching the app in the foreground.

When the app is running in the background and I select a notification action, the UNUserNotificationCenterDelegate’s didReceive method is called, I receive a UNNotificationResponse, and my app performs the tasks associated with starting/ending a session as expected.

However, if I force close my app from the App Switcher and then select a notification action when the notification comes in, it appears the didReceive delegate method is not being called. My app doesn’t run the tasks needed to start/end a session properly.

Apple’s docs on Actionable Notifications states:
Actionable notifications let the user respond to a delivered notification without launching the corresponding app. Other notifications display information in a notification interface, but the user’s only course of action is to launch the app. For an actionable notification, the system displays one or more buttons in addition to the notification interface. Tapping a button sends the selected action to the app, which then processes the action in the background.

Am I doing something wrong here or perhaps misunderstanding how notification actions work?

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    let injectionContainer = AppContainer()
    let dbMaintenanceManager = DBMaintenanceManager()
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        
        requestNotificationAuthorization()
        
        dbMaintenanceManager.performMaintenanceTasks()
        
        let appearance = UINavigationBarAppearance()
        appearance.backgroundColor = Color.primaryBlue
        appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
        
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().tintColor = .white
      
        let mainVC = injectionContainer.makeMainViewController()

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.overrideUserInterfaceStyle = .light
        window?.makeKeyAndVisible()
        window?.rootViewController = mainVC

        return true
    }
        
    func applicationWillEnterForeground(_ application: UIApplication) {
        dbMaintenanceManager.performMaintenanceTasks()
    }
    
    private func requestNotificationAuthorization() {
        let notificationCenter = UNUserNotificationCenter.current()
        
        notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if let error = error {
                print("Error requesting authorization: (error)")
                return
            }

            if granted {
                let startAction = UNNotificationAction(identifier: "START_ACTION",
                                                              title: "Start Session",
                                                       options: [])
                
                let endAction = UNNotificationAction(identifier: "END_ACTION",
                                                            title: "End Session",
                                                     options: [])
                
                let startCategory = UNNotificationCategory(identifier: "START_CATEGORY",
                                                                  actions: [startAction],
                                                                  intentIdentifiers: [],
                                                                  options: [])
                
                let endCategory = UNNotificationCategory(identifier: "END_CATEGORY",
                                                                  actions: [endAction],
                                                                  intentIdentifiers: [],
                                                                  options: [])
                
                let notificationCenter = UNUserNotificationCenter.current()
                notificationCenter.setNotificationCategories([startCategory, endCategory])
                notificationCenter.delegate = self
                print("Notification permission authorized")
            } else {
                print("Notification permission denied")
            }
        }
    }
}

// MARK: - UNUserNotificationCenterDelegate

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        NotificationCenter.default.post(name: .didReceivePushNotification, object: notification)
        completionHandler([.banner, .list, .sound])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        handleNotificationResponse(response)
        completionHandler()
    }
        
    private func handleNotificationResponse(_ response: UNNotificationResponse) {
        let userInfo = response.notification.request.content.userInfo
        guard let id = userInfo["sessionId"] as? String else {
            print("Error reading session id associated with notification")
            return
        }

        switch response.actionIdentifier {
        case "START_ACTION":
            NotificationCenter.default.post(name: .didStartSession, object: nil, userInfo: ["sessionId" : id])
        case "END_ACTION":
            NotificationCenter.default.post(name: .didEndSession, object: nil, userInfo: ["sessionId" : id])
        default:
            break
        }
    }
}

extension Notification.Name {
    static let didReceivePushNotification = Notification.Name("didReceivePushNotification")
    static let didStartSession = Notification.Name("didStartSession")
    static let didEndSession = Notification.Name("didEndSession")
}
class NotificationScheduler {
    
    enum NotificationType {
        case reminder
        case completion
    }
    
    let notificationCenter = UNUserNotificationCenter.current()
    
    func scheduleNotification(for session: Session, type: NotificationType) {
        switch session.type {
        case .now:
            scheduleNowNotification(session: session, type: type)
        case .later:
            scheduleLaterNotification(session: session, type: type)
        case .recurring:
            scheduleRecurringNotification(session: session, type: type)
        case .none:
            break
        }
    }
    
    private func scheduleNowNotification(session: Session, type: NotificationType) {
        if type == .completion { scheduleCompletionNotification(session: session, date: session.endTime) }
    }
    
    private func scheduleLaterNotification(session: Session, type: NotificationType) {
        if type == .reminder { scheduleReminderNotification(session: session, date: session.startTime) }
        if type == .completion { scheduleCompletionNotification(session: session, date: session.endTime) }
    }
    
    private func scheduleRecurringNotification(session: Session, type: NotificationType) {
        for day in session.recurringDays {
            if type == .reminder { scheduleReminderNotification(session: session, date: session.startTime, recurringDay: day) }
            if type == .completion { scheduleCompletionNotification(session: session, date: session.endTime, recurringDay: day) }
        }
    }
    
    private func scheduleReminderNotification(session: Session, date: Date, recurringDay: Weekday? = nil) {
        let content = UNMutableNotificationContent()
        content.title = "Reminder"
        content.body = "Your session (session.name) is starting."
        content.sound = .default
        content.categoryIdentifier = "START_CATEGORY"
        content.userInfo = ["sessionId" : session._id.stringValue]
        
        let trigger: UNNotificationTrigger
        if let recurringDay = recurringDay {
            trigger = createWeeklyTrigger(date: date, weekday: recurringDay)
        } else {
            trigger = createTrigger(date: date)
        }
        
        let request = UNNotificationRequest(identifier: "(session._id)-reminder", content: content, trigger: trigger)
        notificationCenter.add(request, withCompletionHandler: nil)
    }
    
    private func scheduleCompletionNotification(session: Session, date: Date, recurringDay: Weekday? = nil) {
        let content = UNMutableNotificationContent()
        content.title = "Session Complete"
        content.body = "Your session (session.name) is complete."
        content.sound = .default
        content.categoryIdentifier = "END_CATEGORY"
        content.userInfo = ["sessionId" : session._id.stringValue]
        
        let trigger: UNNotificationTrigger
        if let recurringDay = recurringDay {
            trigger = createWeeklyTrigger(date: date, weekday: recurringDay)
        } else {
            trigger = createTrigger(date: date)
        }
        
        let request = UNNotificationRequest(identifier: "(session._id)-completion", content: content, trigger: trigger)
        notificationCenter.add(request, withCompletionHandler: nil)
    }
    
    private func createTrigger(date: Date) -> UNCalendarNotificationTrigger {
        let calendar = Calendar.current
        let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
        return UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
    }
    
    private func createWeeklyTrigger(date: Date, weekday: Weekday) -> UNCalendarNotificationTrigger {
        var components = Calendar.current.dateComponents([.hour, .minute, .second], from: date)
        components.weekday = weekday.rawValue
        return UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
    }
    
    func updateNotification(for session: Session) {
        // Remove existing notifications for this session
        removeNotification(for: session._id)
        
        // Schedule new notifications
        if session.type == .now {
            scheduleNotification(for: session, type: .completion)
        } else {
            scheduleNotification(for: session, type: .reminder)
        }
        
    }
    
    func removeNotification(for sessionId: ObjectId) {
        notificationCenter.removePendingNotificationRequests(withIdentifiers: ["(sessionId)-reminder"])
    }
}```

4

The problem is that you’re looking for the user’s action response in the wrong place. If the app is killed, whether by the user or by the system, and the local notification alert appears and the user taps an action, that action arrives immediately, in the launchOptions dictionary of application(_:didFinishLaunchingWithOptions:). You have to grab it — and your code is not grabbing it.

(In a modern architecture app, which I presume your app is not since you seem to be creating the window in the app delegate, you would have a scene delegate, and the scene delegate’s scene(_:willConnectTo:options:) would be called with the notificationResponse in its options: parameter. Again, your job would be to grab it there.)

If your app delegate is the user notification center delegate, then the action can also arrive in the userNotificationCenter(_:didReceive:withCompletionHandler:) method. The trouble here, though, is that because of the way you’ve written your requestNotificationAuthorization, your app delegate is not assigned as the notification center delegate until after the launch has completed — and so it happens too late to “take the call”. That’s why, as you have observed, it isn’t called under those circumstances. The time to assign your app as the user notification center delegate would have been much earlier — right at the start of application(_:didFinishLaunchingWithOptions:), before anything else has a chance to happen.

However, that second point doesn’t really matter; I’m just explaining the phenomenon to you. Just pick up the action in the launchOptions dictionary directly and all will be well.

1

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật