Created
August 2, 2017 10:46
-
-
Save mspvirajpatel/d50d51482f47e5926b3243085d2aad1c to your computer and use it in GitHub Desktop.
local notification schedule
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import UIKit | |
/// The maximum allowed notifications to be scheduled at a time by iOS. | |
///- important: Do not change this value. Changing this value to be over | |
/// 64 will cause some notifications to be discarded by iOS. | |
let MAX_ALLOWED_NOTIFICATIONS = 64 | |
///- author: Ahmed Abdul Badie | |
class ABNScheduler { | |
/// The maximum number of allowed UILocalNotification to be scheduled. Four slots | |
/// are reserved if you would like to schedule notifications without them being queued. | |
///> Feel free to change this value. | |
///- attention: iOS by default allows a maximum of 64 notifications to be scheduled | |
/// at a time. | |
///- seealso: `MAX_ALLOWED_NOTIFICATIONS` | |
static let maximumScheduledNotifications = 60 | |
/// The key of the notification's identifier. | |
fileprivate let identifierKey = "ABNIdentifier" | |
/// The key of the notification's identifier. | |
static let identifierKey = "ABNIdentifier" | |
///- parameters: | |
/// - alertBody: Alert body of the notification. | |
/// - fireDate: The date in which the notification will be fired at. | |
///- returns: Notification's identifier if it was successfully scheduled, nil otherwise. | |
/// To get an ABNotificaiton instance of this notification, use this identifier with | |
/// `ABNScheduler.notificationWithIdentifier(_:)`. | |
class func schedule(alertBody: String, fireDate: Date) -> String? { | |
let notification = ABNotification(alertBody: alertBody) | |
if let identifier = notification.schedule(fireDate: fireDate) { | |
return identifier | |
} | |
return nil | |
} | |
///Cancels the specified notification. | |
///- paramater: Notification to cancel. | |
class func cancel(_ notification: ABNotification) { | |
notification.cancel() | |
} | |
///Cancels all scheduled UILocalNotification and clears the ABNQueue. | |
class func cancelAllNotifications() { | |
UIApplication.shared.cancelAllLocalNotifications() | |
ABNQueue.queue.clear() | |
let _ = saveQueue() | |
print("All notifications have been cancelled") | |
} | |
///- returns: ABNotification of the farthest UILocalNotification (last to be fired). | |
class func farthestLocalNotification() -> ABNotification? { | |
if let localNotification = UIApplication.shared.scheduledLocalNotifications?.last { | |
return notificationWithUILocalNotification(localNotification) | |
} | |
return nil | |
} | |
///- returns: Count of scheduled UILocalNotification by iOS. | |
class func scheduledCount() -> Int { | |
return (UIApplication.shared.scheduledLocalNotifications?.count)! | |
} | |
///- returns: Count of queued ABNotification. | |
class func queuedCount() -> Int { | |
return ABNQueue.queue.count() | |
} | |
///- returns: Count of scheduled UILocalNotification and queued ABNotification. | |
class func count() -> Int { | |
return scheduledCount() + queuedCount() | |
} | |
///Schedules the maximum possible number of ABNotification from the ABNQueue | |
class func scheduleNotificationsFromQueue() { | |
for _ in 0..<(min(maximumScheduledNotifications, MAX_ALLOWED_NOTIFICATIONS) - scheduledCount()) where ABNQueue.queue.count() > 0 { | |
let notification = ABNQueue.queue.pop() | |
let _ = notification.schedule(fireDate: notification.fireDate!) | |
} | |
} | |
///Creates an ABNotification from a UILocalNotification or from the ABNQueue. | |
///- parameter identifier: Identifier of the required notification. | |
///- returns: ABNotification if found, nil otherwise. | |
class func notificationWithIdentifier(_ identifier: String) -> ABNotification? { | |
let notifs = UIApplication.shared.scheduledLocalNotifications | |
let queue = ABNQueue.queue.notificationsQueue() | |
if notifs?.count == 0 && queue.count == 0 { | |
return nil | |
} | |
for note in notifs! { | |
let id = note.userInfo![ABNScheduler.identifierKey] as! String | |
if id == identifier { | |
return notificationWithUILocalNotification(note) | |
} | |
} | |
if let note = ABNQueue.queue.notificationWithIdentifier(identifier) { | |
return note | |
} | |
return nil | |
} | |
///Instantiates an ABNotification from a UILocalNotification. | |
///- parameter localNotification: The UILocalNotification to instantiate an ABNotification from. | |
///- returns: The instantiated ABNotification from the UILocalNotification. | |
class func notificationWithUILocalNotification(_ localNotification: UILocalNotification) -> ABNotification { | |
return ABNotification.notificationWithUILocalNotification(localNotification) | |
} | |
///Reschedules all notifications by copying them into a temporary array, | |
///cancelling them, and scheduling them again. | |
class func rescheduleNotifications() { | |
let notificationsCount = count() | |
var notificationsArray = [ABNotification?](repeating: nil, count: notificationsCount) | |
let scheduledNotifications = UIApplication.shared.scheduledLocalNotifications | |
var i = 0 | |
for note in scheduledNotifications! { | |
let notif = notificationWithIdentifier(note.userInfo?[identifierKey] as! String) | |
notificationsArray[i] = notif | |
notif?.cancel() | |
i += 1 | |
} | |
let queuedNotifications = ABNQueue.queue.notificationsQueue() | |
for note in queuedNotifications { | |
notificationsArray[i] = note | |
i += 1 | |
} | |
cancelAllNotifications() | |
for note in notificationsArray { | |
let _ = note?.schedule(fireDate: (note?.fireDate)!) | |
} | |
} | |
///Retrieves the total scheduled notifications (by iOS and in the notification queue) and returns them as ABNotification array. | |
///- returns: ABNotification array of scheduled notifications if any, nil otherwise. | |
class func scheduledNotifications() -> [ABNotification]? { | |
if let localNotifications = UIApplication.shared.scheduledLocalNotifications { | |
var notifications = [ABNotification]() | |
for localNotification in localNotifications { | |
notifications.append(ABNotification.notificationWithUILocalNotification(localNotification)) | |
} | |
return notifications | |
} | |
return nil | |
} | |
///- returns: The notifications queue. | |
class func notificationsQueue() -> [ABNotification] { | |
return ABNQueue.queue.notificationsQueue() | |
} | |
///Persists the notifications queue to the disk | |
///> Call this method whenever you need to save changes done to the queue and/or before terminating the app. | |
class func saveQueue() -> Bool { | |
return ABNQueue.queue.save() | |
} | |
//MARK: Testing | |
/// Use this method for development and testing. | |
///> Prints all scheduled and queued notifications. | |
///> You can freely modifiy it without worrying about affecting any functionality. | |
class func listScheduledNotifications() { | |
let notifs = UIApplication.shared.scheduledLocalNotifications | |
let notificationQueue = ABNQueue.queue.notificationsQueue() | |
if notifs?.count == 0 && notificationQueue.count == 0 { | |
print("There are no scheduled notifications") | |
return | |
} | |
print("SCHEDULED") | |
var i = 1 | |
for note in notifs! { | |
let id = note.userInfo![identifierKey] as! String | |
print("\(i) Alert body: \(note.alertBody!) - Fire date: \(note.fireDate!) - Repeats: \(ABNotification.calendarUnitToRepeats(calendarUnit: note.repeatInterval)) - Identifier: \(id)") | |
i += 1 | |
} | |
print("QUEUED") | |
for note in notificationQueue { | |
print("\(i) Alert body: \(note.alertBody) - Fire date: \(note.fireDate!) - Repeats: \(note.repeatInterval) - Identifier: \(note.identifier)") | |
i += 1 | |
} | |
print("") | |
} | |
/// Used only for testing. | |
/// - important: This method is only used to test that loading the queue is successful. Do not use it in production. The `ABNQueue().load()` method is called automatically when initially accessing the notification queue. | |
/// - returns: Array of notifications read from disk. | |
class func loadQueue() -> [ABNotification]? { | |
return ABNQueue().load() | |
} | |
} | |
//MARK:- | |
///- author: Ahmed Abdul Badie | |
private class ABNQueue : NSObject { | |
fileprivate var notifQueue = [ABNotification]() | |
static let queue = ABNQueue() | |
let ArchiveURL = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("notifications.abnqueue") | |
override init() { | |
super.init() | |
if let notificationQueue = self.load() { | |
notifQueue = notificationQueue | |
} | |
} | |
///- paramater notification ABNotification to push. | |
fileprivate func push(_ notification: ABNotification) { | |
notifQueue.insert(notification, at: findInsertionPoint(notification)) | |
} | |
/// Finds the position at which the new ABNotification is inserted in the queue. | |
/// - parameter notification: ABNotification to insert. | |
/// - returns: Index to insert the ABNotification at. | |
/// - seealso: [swift-algorithm-club](https://github.com/hollance/swift-algorithm-club/tree/master/Ordered%20Array) | |
fileprivate func findInsertionPoint(_ notification: ABNotification) -> Int { | |
let range = 0..<notifQueue.count | |
var rangeLowerBound = range.lowerBound | |
var rangeUpperBound = range.upperBound | |
while rangeLowerBound < rangeUpperBound { | |
let midIndex = rangeLowerBound + (rangeUpperBound - rangeLowerBound) / 2 | |
if notifQueue[midIndex] == notification { | |
return midIndex | |
} else if notifQueue[midIndex] < notification { | |
rangeLowerBound = midIndex + 1 | |
} else { | |
rangeUpperBound = midIndex | |
} | |
} | |
return rangeLowerBound | |
} | |
///Removes and returns the head of the queue. | |
///- returns: The head of the queue. | |
fileprivate func pop() -> ABNotification { | |
return notifQueue.removeFirst() | |
} | |
///- returns: The head of the queue. | |
fileprivate func peek() -> ABNotification? { | |
return notifQueue.last | |
} | |
///Clears the queue. | |
fileprivate func clear() { | |
notifQueue.removeAll() | |
} | |
///Called when an ABNotification is cancelled. | |
///- parameter index: Index of ABNotification to remove. | |
fileprivate func removeAtIndex(_ index: Int) { | |
notifQueue.remove(at: index) | |
} | |
///- returns: Count of ABNotification in the queue. | |
fileprivate func count() -> Int { | |
return notifQueue.count | |
} | |
///- returns: The notifications queue. | |
fileprivate func notificationsQueue() -> [ABNotification] { | |
let queue = notifQueue | |
return queue | |
} | |
///- parameter identifier: Identifier of the notification to return. | |
///- returns: ABNotification if found, nil otherwise. | |
fileprivate func notificationWithIdentifier(_ identifier: String) -> ABNotification? { | |
for note in notifQueue { | |
if note.identifier == identifier { | |
return note | |
} | |
} | |
return nil | |
} | |
// MARK: NSCoding | |
///Save queue on disk. | |
///- returns: The success of the saving operation. | |
fileprivate func save() -> Bool { | |
return NSKeyedArchiver.archiveRootObject(self.notifQueue, toFile: ArchiveURL.path) | |
} | |
///Load queue from disk. | |
///Called first when instantiating the ABNQueue singleton. | |
///You do not need to manually call this method and therefore do not declare it as public. | |
fileprivate func load() -> [ABNotification]? { | |
return NSKeyedUnarchiver.unarchiveObject(withFile: ArchiveURL.path) as? [ABNotification] | |
} | |
} | |
//MARK:- | |
///- author: Ahmed Abdul Badie | |
enum Repeats: String { | |
case None, Hourly, Daily, Weekly, Monthly, Yearly | |
} | |
///A wrapper class around UILocalNotification. | |
///- author: Ahmed Abdul Badie | |
open class ABNotification : NSObject, NSCoding, Comparable { | |
fileprivate var localNotification: UILocalNotification | |
var alertBody: String | |
var alertAction: String? | |
var soundName: String? | |
var repeatInterval = Repeats.None | |
var userInfo: Dictionary<String, AnyObject> | |
fileprivate(set) var identifier: String | |
fileprivate var scheduled = false | |
var fireDate: Date? { | |
return localNotification.fireDate | |
} | |
init(alertBody: String) { | |
self.alertBody = alertBody | |
self.localNotification = UILocalNotification() | |
self.identifier = UUID().uuidString | |
self.userInfo = Dictionary<String, AnyObject>() | |
super.init() | |
} | |
init(alertBody: String, identifier: String) { | |
self.alertBody = alertBody | |
self.localNotification = UILocalNotification() | |
self.identifier = identifier | |
self.userInfo = Dictionary<String, AnyObject>() | |
super.init() | |
} | |
///Used to instantiate an ABNotification when loaded from disk. | |
fileprivate init(notification: UILocalNotification, alertBody: String, alertAction: String?, soundName: String?, identifier: String, repeats: Repeats, userInfo: Dictionary<String, AnyObject>, scheduled: Bool) { | |
self.alertBody = alertBody | |
self.alertAction = alertAction | |
self.soundName = soundName; | |
self.localNotification = notification | |
self.identifier = identifier | |
self.userInfo = userInfo | |
self.repeatInterval = repeats | |
self.scheduled = scheduled | |
super.init() | |
} | |
///Schedules the notification. | |
///> Checks to see if there is enough room for the notification to be scheduled. Otherwise, the notification is queued. | |
///- parameter fireDate: The date in which the notification will be fired at. | |
///- returns: The identifier of the notification. Use this identifier to retrieve the notification using `ABNQueue.notificationWithIdentifier` and `ABNScheduler.notificationWithIdentifier` methods. | |
func schedule(fireDate date: Date) -> String? { | |
if self.scheduled { | |
return nil | |
} else { | |
self.localNotification = UILocalNotification() | |
self.localNotification.alertBody = self.alertBody | |
self.localNotification.alertAction = self.alertAction | |
self.localNotification.fireDate = date.removeSeconds() | |
self.localNotification.soundName = (self.soundName == nil) ? UILocalNotificationDefaultSoundName : self.soundName; | |
self.userInfo[ABNScheduler.identifierKey] = self.identifier as AnyObject? | |
self.localNotification.userInfo = self.userInfo | |
self.soundName = self.localNotification.soundName | |
if repeatInterval != .None { | |
switch repeatInterval { | |
case .Hourly: self.localNotification.repeatInterval = NSCalendar.Unit.hour | |
case .Daily: self.localNotification.repeatInterval = NSCalendar.Unit.day | |
case .Weekly: self.localNotification.repeatInterval = NSCalendar.Unit.weekOfYear | |
case .Monthly: self.localNotification.repeatInterval = NSCalendar.Unit.month | |
case .Yearly: self.localNotification.repeatInterval = NSCalendar.Unit.year | |
default: break | |
} | |
} | |
let count = ABNScheduler.scheduledCount() | |
if count >= min(ABNScheduler.maximumScheduledNotifications, MAX_ALLOWED_NOTIFICATIONS) { | |
if let farthestNotification = ABNScheduler.farthestLocalNotification() { | |
if farthestNotification > self { | |
farthestNotification.cancel() | |
ABNQueue.queue.push(farthestNotification) | |
self.scheduled = true | |
UIApplication.shared.scheduleLocalNotification(self.localNotification) | |
} else { | |
ABNQueue.queue.push(self) | |
} | |
} | |
return self.identifier | |
} | |
self.scheduled = true | |
UIApplication.shared.scheduleLocalNotification(self.localNotification) | |
return self.identifier | |
} | |
} | |
///Reschedules the notification. | |
///- parameter fireDate: The date in which the notification will be fired at. | |
func reschedule(fireDate date: Date) { | |
cancel() | |
let _ = schedule(fireDate: date) | |
} | |
///Cancels the notification if scheduled or queued. | |
func cancel() { | |
UIApplication.shared.cancelLocalNotification(self.localNotification) | |
let queue = ABNQueue.queue.notificationsQueue() | |
var i = 0 | |
for note in queue { | |
if self.identifier == note.identifier { | |
ABNQueue.queue.removeAtIndex(i) | |
break | |
} | |
i += 1 | |
} | |
scheduled = false | |
} | |
///Snoozes the notification for a number of minutes. | |
///- parameter minutes: Minutes to snooze the notification for. | |
func snoozeForMinutes(_ minutes: Int) { | |
reschedule(fireDate: self.localNotification.fireDate!.nextMinutes(minutes)) | |
} | |
///Snoozes the notification for a number of hours. | |
///- parameter minutes: Hours to snooze the notification for. | |
func snoozeForHours(_ hours: Int) { | |
reschedule(fireDate: self.localNotification.fireDate!.nextHours(hours)) | |
} | |
///Snoozes the notification for a number of days. | |
///- parameter minutes: Days to snooze the notification for. | |
func snoozeForDays(_ days: Int) { | |
reschedule(fireDate: self.localNotification.fireDate!.nextDays(days)) | |
} | |
///- returns: The state of the notification. | |
func isScheduled() -> Bool { | |
return self.scheduled | |
} | |
///Used by ABNotificationX classes to convert NSCalendarUnit enum to Repeats enum. | |
///- parameter calendarUnit: NSCalendarUnit to convert. | |
///- returns: Repeats type that is equivalent to the passed NSCalendarUnit. | |
fileprivate class func calendarUnitToRepeats(calendarUnit cUnit: NSCalendar.Unit) -> Repeats { | |
switch cUnit { | |
case NSCalendar.Unit.hour: return .Hourly | |
case NSCalendar.Unit.day: return .Daily | |
case NSCalendar.Unit.weekOfYear: return .Weekly | |
case NSCalendar.Unit.month: return .Monthly | |
case NSCalendar.Unit.year: return .Yearly | |
default: return Repeats.None | |
} | |
} | |
///Instantiates an ABNotification from a UILocalNotification. | |
///- parameter localNotification: The UILocalNotification to instantiate an ABNotification from. | |
///- returns: The instantiated ABNotification from the UILocalNotification. | |
fileprivate class func notificationWithUILocalNotification(_ localNotification: UILocalNotification) -> ABNotification { | |
let alertBody = localNotification.alertBody! | |
let alertAction = localNotification.alertAction | |
let soundName = localNotification.soundName | |
let identifier = localNotification.userInfo![ABNScheduler.identifierKey] as! String | |
let repeats = self.calendarUnitToRepeats(calendarUnit: localNotification.repeatInterval) | |
let userInfo = localNotification.userInfo! | |
return ABNotification(notification: localNotification, alertBody: alertBody, alertAction: alertAction, soundName: soundName, identifier: identifier, repeats: repeats, userInfo: userInfo as! Dictionary<String, AnyObject>, scheduled: true) | |
} | |
// MARK: NSCoding | |
@objc convenience public required init?(coder aDecoder: NSCoder) { | |
guard let localNotification = aDecoder.decodeObject(forKey: "ABNNotification") as? UILocalNotification else { | |
return nil | |
} | |
guard let alertBody = aDecoder.decodeObject(forKey: "ABNAlertBody") as? String else { | |
return nil | |
} | |
guard let identifier = aDecoder.decodeObject(forKey: "ABNIdentifier") as? String else { | |
return nil | |
} | |
guard let userInfo = aDecoder.decodeObject(forKey: "ABNUserInfo") as? Dictionary<String, AnyObject> else { | |
return nil | |
} | |
guard let repeats = aDecoder.decodeObject(forKey: "ABNRepeats") as? String else { | |
return nil | |
} | |
guard let soundName = aDecoder.decodeObject(forKey: "ABNSoundName") as? String else { | |
return nil | |
} | |
let alertAction = aDecoder.decodeObject(forKey: "ABNAlertAction") as? String | |
self.init(notification: localNotification, alertBody: alertBody, alertAction: alertAction, soundName: soundName, identifier: identifier, repeats: Repeats(rawValue: repeats)!, userInfo: userInfo, scheduled: false) | |
} | |
@objc open func encode(with aCoder: NSCoder) { | |
aCoder.encode(localNotification, forKey: "ABNNotification") | |
aCoder.encode(alertBody, forKey: "ABNAlertBody") | |
aCoder.encode(alertAction, forKey: "ABNAlertAction") | |
aCoder.encode(soundName, forKey: "ABNSoundName") | |
aCoder.encode(identifier, forKey: "ABNIdentifier") | |
aCoder.encode(repeatInterval.rawValue, forKey: "ABNRepeats") | |
aCoder.encode(userInfo, forKey: "ABNUserInfo") | |
} | |
} | |
public func <(lhs: ABNotification, rhs: ABNotification) -> Bool { | |
return lhs.localNotification.fireDate?.compare(rhs.localNotification.fireDate!) == ComparisonResult.orderedAscending | |
} | |
public func ==(lhs: ABNotification, rhs: ABNotification) -> Bool { | |
return lhs.identifier == rhs.identifier | |
} | |
//MARK:- Extensions | |
//MARK: NSDate | |
extension Date { | |
/** | |
Add a number of minutes to a date. | |
> This method can add and subtract minutes. | |
- parameter minutes: The number of minutes to add. | |
- returns: The date after the minutes addition. | |
*/ | |
func nextMinutes(_ minutes: Int) -> Date { | |
let calendar = Calendar.current | |
var components = DateComponents() | |
components.minute = minutes | |
return (calendar as NSCalendar).date(byAdding: components, to: self, options: NSCalendar.Options(rawValue: 0))! | |
} | |
/** | |
Add a number of hours to a date. | |
> This method can add and subtract hours. | |
- parameter hours: The number of hours to add. | |
- returns: The date after the hours addition. | |
*/ | |
func nextHours(_ hours: Int) -> Date { | |
return self.nextMinutes(hours * 60) | |
} | |
/** | |
Add a number of days to a date. | |
>This method can add and subtract days. | |
- parameter days: The number of days to add. | |
- returns: The date after the days addition. | |
*/ | |
func nextDays(_ days: Int) -> Date { | |
return nextMinutes(days * 60 * 24) | |
} | |
func removeSeconds() -> Date { | |
let calendar = Calendar.current | |
let components = (calendar as NSCalendar).components([.year, .month, .day, .hour, .minute], from: self) | |
return calendar.date(from: components)! | |
} | |
/** | |
Creates a date object with the given time and offset. The offset is used to align the time with the GMT. | |
- parameter time: The required time of the form HHMM. | |
- parameter offset: The offset in minutes. | |
- returns: Date with the specified time and offset. | |
*/ | |
static func dateWithTime(_ time: Int, offset: Int) -> Date { | |
let calendar = Calendar.current | |
var components = (calendar as NSCalendar).components([.year, .month, .day, .hour, .minute], from: Date()) | |
components.minute = (time % 100) + offset % 60 | |
components.hour = (time / 100) + (offset / 60) | |
var date = calendar.date(from: components)! | |
if date < Date() { | |
date = date.nextMinutes(60*24) | |
} | |
return date | |
} | |
} | |
//MARK: Int | |
extension Int { | |
var date: Date { | |
return Date.dateWithTime(self, offset: 0) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment