Skip to content

Commit

Permalink
Add iOS notification action buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
gdelataillade committed Jul 15, 2024
1 parent dde657e commit 97f64db
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 31 deletions.
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ SPEC CHECKSUMS:
alarm: 6c1f6a9688f94cd6bf8f104c67cc26e78c9d8d13
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_fgbg: 31c0d1140a131daea2d342121808f6aa0dcd879d
permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78

PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011

Expand Down
18 changes: 18 additions & 0 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
38B35C95769EE88AAB27208F /* [CP] Embed Pods Frameworks */,
C0B6B89F73C4E7451B5A6278 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
Expand Down Expand Up @@ -268,6 +269,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
C0B6B89F73C4E7451B5A6278 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
Expand Down
7 changes: 7 additions & 0 deletions example/lib/screens/edit_alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ class _ExampleAlarmEditScreenState extends State<ExampleAlarmEditScreen> {
notificationTitle: 'Alarm example',
notificationBody: 'Your alarm ($id) is ringing',
enableNotificationOnKill: Platform.isIOS,
notificationActionSettings: const NotificationActionSettings(
hasStopButton: true,
hasSnoozeButton: false,
stopButtonText: 'STOP !',
snoozeButtonText: 'SNOOZE !',
snoozeDurationInSeconds: 5,
),
);
return alarmSettings;
}
Expand Down
4 changes: 2 additions & 2 deletions example/lib/screens/shortcut_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class _ExampleAlarmHomeShortcutButtonState
bool showMenu = false;

Future<void> onPressButton(int delayInHours) async {
var dateTime = DateTime.now().add(Duration(hours: delayInHours));
var dateTime = DateTime.now().add(Duration(seconds: 3));
double? volume;

if (delayInHours != 0) {
Expand All @@ -42,7 +42,7 @@ class _ExampleAlarmHomeShortcutButtonState
enableNotificationOnKill: Platform.isIOS,
notificationActionSettings: const NotificationActionSettings(
hasStopButton: true,
hasSnoozeButton: true,
hasSnoozeButton: false,
stopButtonText: 'STOP !',
snoozeButtonText: 'SNOOZE !',
snoozeDurationInSeconds: 5,
Expand Down
20 changes: 6 additions & 14 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,10 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
Expand All @@ -195,10 +195,10 @@ packages:
dependency: transitive
description:
name: permission_handler_android
sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5"
sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54
url: "https://pub.dev"
source: hosted
version: "12.0.6"
version: "12.0.7"
permission_handler_apple:
dependency: transitive
description:
Expand Down Expand Up @@ -235,10 +235,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
Expand Down Expand Up @@ -388,14 +388,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
Expand Down
36 changes: 28 additions & 8 deletions ios/Classes/SwiftAlarmPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
#endif

private var registrar: FlutterPluginRegistrar!
static let sharedInstance = SwiftAlarmPlugin()
static let shared = SwiftAlarmPlugin()
static let backgroundTaskIdentifier: String = "com.gdelataillade.fetch"

public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com.gdelataillade/alarm", binaryMessenger: registrar.messenger())
let instance = SwiftAlarmPlugin()
let instance = SwiftAlarmPlugin.shared

instance.registrar = registrar
registrar.addMethodCallDelegate(instance, channel: channel)
Expand Down Expand Up @@ -62,6 +62,18 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
}
}

func stopAlarmFromNotification(id: Int) {
safeModifyResources {
self.stopAlarm(id: id, cancelNotif: true, result: { _ in })
}
}

func snoozeAlarmFromNotification(id: Int, snoozeDurationInSeconds: Int) {
safeModifyResources {
self.stopAlarm(id: id, cancelNotif: true, result: { _ in })
}
}

func safeModifyResources(_ modificationBlock: @escaping () -> Void) {
resourceAccessQueue.async {
modificationBlock()
Expand All @@ -77,11 +89,19 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
let loopAudio = args["loopAudio"] as? Bool,
let fadeDuration = args["fadeDuration"] as? Double,
let vibrationsEnabled = args["vibrate"] as? Bool,
let assetAudio = args["assetAudio"] as? String else {
result(FlutterError(code: "NATIVE_ERR", message: "[SwiftAlarmPlugin] Arguments are not in the expected format: \(call.arguments)", details: nil))
let assetAudio = args["assetAudio"] as? String,
let _ = args["volume"] as? Float,
let actionSettingsDict = args["notificationActionSettings"] as? [String: Any] else {
let argumentsDescription = "\(call.arguments ?? "nil")"
result(FlutterError(code: "NATIVE_ERR", message: "[SwiftAlarmPlugin] Arguments are not in the expected format: \(argumentsDescription)", details: nil))
return
}

// Since fromJson does not return an optional, directly use it without guard let
let actionSettings = NotificationActionSettings.fromJson(json: actionSettingsDict)

NSLog("SwiftAlarmPlugin: NotificationActionSettings: \(actionSettings)")

var volumeFloat: Float? = nil
if let volumeValue = args["volume"] as? Double {
volumeFloat = Float(volumeValue)
Expand All @@ -100,7 +120,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
let notificationTitle = args["notificationTitle"] as? String
let notificationBody = args["notificationBody"] as? String
if let title = notificationTitle, let body = notificationBody, delayInSeconds >= 1.0 {
NotificationManager.shared.scheduleNotification(id: String(id), delayInSeconds: Int(floor(delayInSeconds)), title: title, body: body) { error in
NotificationManager.shared.scheduleNotification(id: id, delayInSeconds: Int(floor(delayInSeconds)), title: title, body: body, actionSettings: actionSettings) { error in
if let error = error {
NSLog("[SwiftAlarmPlugin] Error scheduling notification: \(error.localizedDescription)")
}
Expand Down Expand Up @@ -272,7 +292,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {

private func stopAlarm(id: Int, cancelNotif: Bool, result: FlutterResult) {
if cancelNotif {
NotificationManager.shared.cancelNotification(id: String(id))
NotificationManager.shared.cancelNotification(id: id)
}

self.mixOtherAudios()
Expand Down Expand Up @@ -457,7 +477,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
if #available(iOS 13.0, *) {
BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in
self.scheduleAppRefresh()
sharedInstance.backgroundFetch()
shared.backgroundFetch()
task.setTaskCompleted(success: true)
}
} else {
Expand Down Expand Up @@ -489,4 +509,4 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
NSLog("SwiftAlarmPlugin: BGTaskScheduler not available for your version of iOS lower than 13.0")
}
}
}
}
19 changes: 19 additions & 0 deletions ios/Classes/models/NotificationActionSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

struct NotificationActionSettings {
var hasStopButton: Bool = false
var hasSnoozeButton: Bool = false
var stopButtonText: String = "Stop"
var snoozeButtonText: String = "Snooze"
var snoozeDurationInSeconds: Int = 9 * 60

static func fromJson(json: [String: Any]) -> NotificationActionSettings {
return NotificationActionSettings(
hasStopButton: json["hasStopButton"] as? Bool ?? false,
hasSnoozeButton: json["hasSnoozeButton"] as? Bool ?? false,
stopButtonText: json["stopButtonText"] as? String ?? "Stop",
snoozeButtonText: json["snoozeButtonText"] as? String ?? "Snooze",
snoozeDurationInSeconds: json["snoozeDurationInSeconds"] as? Int ?? 9 * 60
)
}
}
65 changes: 60 additions & 5 deletions ios/Classes/services/NotificationManager.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import Foundation
import UserNotifications

class NotificationManager {
class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
static let shared = NotificationManager()

private init() {} // Private initializer to ensure singleton usage
private override init() {
super.init()
}

private func setupNotificationActions(hasStopButton: Bool, hasSnoozeButton: Bool, stopButtonText: String, snoozeButtonText: String) {
var actions: [UNNotificationAction] = []

if hasStopButton {
let stopAction = UNNotificationAction(identifier: "STOP_ACTION", title: stopButtonText, options: [.destructive])
actions.append(stopAction)
}

if hasSnoozeButton {
let snoozeAction = UNNotificationAction(identifier: "SNOOZE_ACTION", title: snoozeButtonText, options: [])
actions.append(snoozeAction)
}

let category = UNNotificationCategory(identifier: "ALARM_CATEGORY", actions: actions, intentIdentifiers: [], options: [])

UNUserNotificationCenter.current().setNotificationCategories([category])
UNUserNotificationCenter.current().delegate = self
}

func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge], completionHandler: completion)
}

func scheduleNotification(id: String, delayInSeconds: Int, title: String, body: String, completion: @escaping (Error?) -> Void) {
func scheduleNotification(id: Int, delayInSeconds: Int, title: String, body: String, actionSettings: NotificationActionSettings, completion: @escaping (Error?) -> Void) {
requestAuthorization { granted, error in
guard granted, error == nil else {
completion(error)
return
}

self.setupNotificationActions(hasStopButton: actionSettings.hasStopButton, hasSnoozeButton: actionSettings.hasSnoozeButton, stopButtonText: actionSettings.stopButtonText, snoozeButtonText: actionSettings.snoozeButtonText)

let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = nil
content.categoryIdentifier = "ALARM_CATEGORY"
content.userInfo = ["id": id, "snoozeDurationInSeconds": actionSettings.snoozeDurationInSeconds] // Include the id as an Integer in userInfo

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(delayInSeconds), repeats: false)
let request = UNNotificationRequest(identifier: "alarm-\(id)", content: content, trigger: trigger)
Expand All @@ -29,7 +54,37 @@ class NotificationManager {
}
}

func cancelNotification(id: String) {
func cancelNotification(id: Int) {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["alarm-\(id)"])
}
}

func handleAction(withIdentifier identifier: String?, for notification: UNNotification) {
guard let identifier = identifier else { return }
guard let id = notification.request.content.userInfo["id"] as? Int else { return }

switch identifier {
case "STOP_ACTION":
NSLog("Stop action triggered for notification: \(notification.request.identifier)")
SwiftAlarmPlugin.shared.stopAlarmFromNotification(id: id)

case "SNOOZE_ACTION":
guard let snoozeDurationInSeconds = notification.request.content.userInfo["snoozeDurationInSeconds"] as? Int else { return }
NSLog("Snooze action triggered for notification: \(notification.request.identifier)")
SwiftAlarmPlugin.shared.snoozeAlarmFromNotification(id: id, snoozeDurationInSeconds: snoozeDurationInSeconds)

default:
break
}
}

// MARK: - UNUserNotificationCenterDelegate

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
handleAction(withIdentifier: response.actionIdentifier, for: response.notification)
completionHandler()
}

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .sound])
}
}
2 changes: 2 additions & 0 deletions lib/src/ios_alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class IOSAlarm {
AlarmStorage.getNotificationOnAppKillTitle(),
'notifDescriptionOnAppKill':
AlarmStorage.getNotificationOnAppKillBody(),
'notificationSettings':
settings.notificationActionSettings.toJson(),
},
) ??
false;
Expand Down

0 comments on commit 97f64db

Please sign in to comment.