Greetings, traveler!
Notifications are a popular tool for communicating with users. They can be local or remote. Remote notifications can be handled via external services like Firebase or Supabase, while local ones can be created and scheduled inside the app. Let’s take a closer look at how we can do this.
Notification Service
Imagine we have a Habit tracker. So there we have a model of a habit. This model has its ID, title, weekday array, and timestamps. So, users can schedule notifications on specific days at chosen timestamps.
struct Habit {
var id: UUID = .init()
var title: String = "Do 10 pull-ups"
var weekDays: [Locale.Weekday] = [.monday, .wednesday, .friday]
var timestamps: [Date] = [.now]
}
Now, we can create a Notification Service to schedule notifications for concrete habits. First, import the UserNotifications
framework. Then, make a notification scheduling function. There are several steps that we should achieve within this function scope.
- Create a
UNMutableNotificationContent
instance and set its title and subtitle. - Create an array of notification IDs to fill. We will use it to gain the ability to delete scheduled notifications.
- Schedule repeatable notifications for every chosen weekday at specific timestamps.
We will use UNCalendarNotificationTrigger
to trigger notifications. This class awaits date components to call the notification system at a specific time. We can compose these components using our timestamps array to specify hours and minutes and the weekday array to specify the weekdays. Since DateComponents
day is an optional Int
, we can create a dictionary, which will help us retrieve an offset value for a specific weekday.
This dictionary will be created by the array of all weekdays. To generate this array, we can map all Calendar
weekdaySymbols into an array of Locale.Weekday
. To make our code a bit cleaner, let’s create an extension. Note that Locale.Weekday
rawValue only has the first three letters of the day of the week name, so we have to use the prefix here. And since the offset starts from zero, we can add “1” to its value.
extension Locale.Weekday {
static var allCases: [Locale.Weekday] {
Calendar
.current
.weekdaySymbols
.compactMap { Locale.Weekday(rawValue: String($0.lowercased().prefix(3))) }
}
static var dayOfTheWeekDictionary: [Locale.Weekday: Int] {
Dictionary(uniqueKeysWithValues: allCases.enumerated().map { ($0.element, $0.offset + 1) })
}
}
// Example of Usage
let dictionary = Locale.Weekday.dayOfTheWeekDictionary // [.sunday: 1, .monday: 2, .tuesday: 3, .wednesday: 4, .thursday: 5, .friday: 6, .saturday: 7]
dictionary[.saturday] // 7
Now, we can use it with our NoticiationService.
struct NotificationService {
private func scheduleNotifications(for item: Habit) async throws -> [String] {
let content = UNMutableNotificationContent()
content.title = "My App notification"
content.subtitle = item.title
var notificationIDs: [String] = []
let calendar = Calendar.current
let weekdayOffsetDict = Locale.Weekday.dayOfTheWeekDictionary
for weekday in item.weekDays {
for date in item.timestamps {
let id = UUID().uuidString
let hour = calendar.component(.hour, from: date)
let min = calendar.component(.minute, from: date)
guard let day = weekdayOffsetDict[weekday] else { continue }
var components = DateComponents()
components.hour = hour
components.minute = min
components.weekday = day
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
notificationIDs.append(id)
try await UNUserNotificationCenter.current().add(request)
}
}
return notificationIDs
}
}
Save and Delete
We should give users the ability to delete scheduled notifications. First, we should save their IDs somewhere. In this article, we will use UserDafults
.
private enum Constants {
static let notificationsKey = "notificationsKey"
}
private func saveValueInUserDefaults(_ value: [String]?, id: String) {
var notificationDict = (UserDefaults.standard.object(forKey: Constants.notificationsKey) as? [String: [String]]) ?? [:]
notificationDict[id] = value
UserDefaults.standard.setValue(notificationDict, forKey: Constants.notificationsKey)
}
Now, we can retrieve them via the habit model ID.
private func getNotificationIDsFromUserDefaults(for id: String) -> [String] {
let notificationDict = (UserDefaults.standard.object(forKey: Constants.notificationsKey) as? [String: [String]]) ?? [:]
return notificationDict[id] ?? []
}
After that, we can easily delete them.
func deleteNotifications(for item: Habit) {
let ids = getNotificationIDsFromUserDefaults(for: item.id.uuidString)
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
saveValueInUserDefaults(nil, id: item.id.uuidString)
}
Create notifications
The last step is creating notifications using tools we made earlier.
extension NotificationService {
func createNotifications(for item: Habit) async {
do {
let ids = try await scheduleNotifications(for: item)
saveValueInUserDefaults(ids, id: item.id.uuidString)
} catch {
print(error)
}
}
}
Notifications limit
Local notifications are limited if you are using UILocalNotification
. Apple mentioned this detail in its documentation.
An app can have only a limited number of scheduled notifications; the system keeps the soonest-firing 64 notifications (with automatically rescheduled notifications counting as a single notification) and discards the rest.
We are using UNNotificationRequest
, and there is nothing about it in its documentation. However, this rule still exists, so you should keep that in mind. To make it more obvious, we can create a function to check the current notification count.
func createNotifications(for item: Habit) async {
guard let total = try? await notificationsCount() else {
return
}
if total >= 64 {
print(total)
return
}
do {
let ids = try await scheduleNotifications(for: item)
saveValueInUserDefaults(ids, id: item.id.uuidString)
} catch {
print(error)
}
}
private func notificationsCount() async throws -> Int {
let notificationCenter = UNUserNotificationCenter.current()
let notificationRequests = await notificationCenter.pendingNotificationRequests()
return notificationRequests.count
}
Thats it! And here is the final result.
import UserNotifications
struct Habit {
let id: UUID
let title: String
let weekDays: [String]
let timestamps: [Date]
}
struct NotificationService {
private enum Constants {
static let notificationsKey = "notificationsKey"
}
private func scheduleNotifications(for item: Habit) async throws -> [String] {
let content = UNMutableNotificationContent()
content.title = "My App notification"
content.subtitle = item.title
var notificationIDs: [String] = []
let calendar = Calendar.current
let weekdayOffsetDict = Locale.Weekday.dayOfTheWeekDictionary
for weekday in item.weekDays {
for date in item.timestamps {
let id = UUID().uuidString
let hour = calendar.component(.hour, from: date)
let min = calendar.component(.minute, from: date)
guard let day = weekdayOffsetDict[weekday] else { continue }
var components = DateComponents()
components.hour = hour
components.minute = min
components.weekday = day
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
notificationIDs.append(id)
try await UNUserNotificationCenter.current().add(request)
}
}
return notificationIDs
}
private func saveValueInUserDefaults(_ value: [String]?, id: String) {
var notificationDict = (UserDefaults.standard.object(forKey: Constants.notificationsKey) as? [String: [String]]) ?? [:]
notificationDict[id] = value
UserDefaults.standard.setValue(notificationDict, forKey: Constants.notificationsKey)
}
private func getNotificationIDsFromUserDefaults(for id: String) -> [String] {
let notificationDict = (UserDefaults.standard.object(forKey: Constants.notificationsKey) as? [String: [String]]) ?? [:]
return notificationDict[id] ?? []
}
private func notificationsCount() async throws -> Int {
let notificationCenter = UNUserNotificationCenter.current()
let notificationRequests = await notificationCenter.pendingNotificationRequests()
return notificationRequests.count
}
}
extension NotificationService {
func createNotifications(for item: Habit) async {
guard let total = try? await notificationsCount() else {
return
}
if total >= 64 {
print(total)
return
}
do {
let ids = try await scheduleNotifications(for: item)
saveValueInUserDefaults(ids, id: item.id.uuidString)
} catch {
print(error)
}
}
func deleteNotifications(for item: Habit) {
let ids = getNotificationIDsFromUserDefaults(for: item.id.uuidString)
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
saveValueInUserDefaults(nil, id: item.id.uuidString)
}
}
Conclusion
Local notifications can become a helpful tool that doesn’t rely on the network connection but has some imitations.
If you enjoyed this article, please feel free to follow me on my social media: