100 Days of Swift – Day 77

Reflections on Day 76

Another good day under the belt because for every day that passes I am fine-tuning the how I want to learn this subject. I am adjusting the rhythm, the pace, the way I want to do that.

I have perhaps been too hard on Paul as of late and, normally, when one shouts at someone else, he is actually shouting at himself in the mirror. I still do not agree on many things but it is not my place here to agree or disagree on how other people do things.

Even if I personally purchased the Hacking with Swift book (something that you should absolutely do by going here) I am not paying for a service and so I should not be the one who influences the teaching directions of someone else. Actually, the greatest teaching of these days of crisis is that I should just mind my own business all the time.

If I do not like someone’s teachings, fine, just move on and choose another resource. And let’s be clear once and for all, I really like how Paul explains what he explains, I just do not like how many things he leaves out.

Yesterday I was watching the Swift on Sunday about the macOS menu bar fantastic project and Paul talked about what will happen when these 100 days are over and what should happen next. He also suggested that, after this book one could continue with Pro Swift and, then, with Swift Design Patterns. I will for sure take a pause from Paul’s books because I have a few other courses I want to finish first and, also, because I have an active Ray Wenderlich subscription which is just going to waste without being used.

The greatest thing of yesterday was that I decided to stop every single time I did not understand something and delved into Apple’s Documentation to try and understand more. I do not want to say I always understood more but, at least, I got a clearer picture in my mind.

Without further ado, let’s move to some learning for today.

Hacking with Swift – Learning Project 21

Today we are learning about local notifications, which is the beginning of a very deep subject. As we are reminded, just because we can show a notification it doesn’t mean we should. If I look at my Screen Time settings, I have an average of 173 screen activation a day (which may also not sound so much), but this means that I either check for something or have a notification about 17 times per hour (assuming 10 hours of active life a day!).

Setting up

This project will allow us to send reminders to the user’s lock screen to show informations when the app is not running. To get set up and running we just need to create an empty Single View iOS app.

Scheduling notifications: UNUserNotificationCenter and UNNotificationRequest

After watching the video I can say I really enjoyed it, it was clear, not too fast, and quite easy to follow. As I pioneered yesterday, I want to recap all of what was done and get deep into every single thing that I still do not know. This will make this article longer, sure but, what else do you have to do on Easter Sunday rather than reading my articles?!

Storyboard setup

Very easily, just embed the view controller in a navigation controller. Then move back to ViewController.swift

Buttons

In viewDidLoad create two bar button items, both with a plain style and a target of self. The title will be “Register” for the first and “Schedule” for the second, while the action will be #selector(registerLocal) for the first and #selector(scheduleLocal) for the second. Before Xcode starts complaining please create two methods: @objc func registerLocal and @objc func scheduleLocal.

Set up User Notifications

First import the UserNotifications framework, which contains all the methods we need. If you want to read more about this framework you can go to the Apple Developer dedicated page. This framework should be used in one of the following cases:

  • Define the types of notifications that your app supports.
  • Define any custom actions associated with your notification types.
  • Schedule local notifications for delivery.
  • Process already delivered notifications.
  • Respond to user-selected actions.

The Documentation is very thorough on this and I didn’t have time to check every single article but the two more relevant articles for this app are these: Asking Permission to Use Notifications and Scheduling a Notification Locally from Your App.

Request User Permission

In the registerLocal method we want to ask the user for permissions to show notifications on his screen. To do so we need to instantiate the shared UNUserNotificationCenter object which is defined as the central object for managing notification-related activities for your app or app extension. It is a subclass of NSObject so a pretty high level stuff I would say. The related Documentation article has everything needed to set up all kinds of notifications. This is done calling the .current() method on the user Notification Center object.

After this we call the requestAuthorization method on the center. This method requests authorisation to interact with the user when local and remote notifications are delivered to the user’s device. Apple recommends to always call this method before scheduling any local notification and before registering with the Apple Push Notification service. They also add that this method should usually get called at launch time when configuring the app’s notification support. In this case we are hooking it up to a button to make things much simpler.

This method accepts two parameters, an array of UNAuthorizationOptions objects and a closure marked @escaping which takes itself two parameters, a Boolean and an optional Error. @escaping “simply” means that this closure is stored for later reference and not destroyed after use, which sounds clever for a semi-permanent setting a notifications are.

For the array of options we will write [.alert, .badge, .sound] so that we support all the possible options and, for the closure, we use trailing closure syntax and then write (granted, error) in followed by a cheerful print statement if granted is true and a desperate one if otherwise!

Schedule the notification

Inside the scheduleLocal method we will once more invoke the user Notification Center object and call the .removeAllPendingNotificationRequests from it. This is good if we need to show only the last notification and remove all the others we may have received before.

We then need to configure what content to show, when to show it and to set up the request.

The first part is done by creating an instance of UNMutableNotificationContent and setting up the .title, .body to something sensible. We then set the .categoryIdentifier to “alarm” in this case but the Documentation explained that this should be connected to one’s own app’s categories … This seems to be only if one wants to support custom action buttons for the notifications, which may be a bit too far ahead still. The same can be said to the .userInfo dictionary. For the .sound property we will just use .default (there are not so many base alternatives).

For the second part we meet a new data type which is DateComponents which is defined as a date or time specified in terms of units (such as year, month, day, hour, and minute) to be evaluated in a calendar system and time zone. We then set the .hour property to 10 and the .minute one to 30, before creating a UNCalendarNotificationTrigger(dateMatching:repeats:) with this date component and a true repeats value. This will make the notification repeat every day at 10.30am. In order to be able to test this we will just comment out all the date components part and replace it with a UNTimeIntervalNotificationTrigger object with a timeInterval parameter of 5 and a false repeat value.

Finally we will request the notification to be displayed using a UNNotificationRequest object which is a combination of the first two parts: for identifier we will set UUID().uuidString, for content our content object and for trigger our trigger object. Last, we will call center.add(request).

UUID

Before closing this part I want to revise this UUID data type because I do not remember what it is.

According to the Documentation it is defined as a universally unique value that can be used to identify types, interfaces, and other items. Calling the .uuidString on such an object returns a string created from the UUID.

Acting on responses

Most of my open questions before have been answered in this second part of the project. In the scheduleLocal method we had set the categoryIdentifier property to “Alarm”, which allows us the create a registerCategories method that will handle the user’s interaction with the notification itself.

Inside this method we will once more create our UNUserNotificationCenter.current() object and then make the view controller be its delegate. To avoid any error we need to make our view controller conform to the UNUserNotificationCenterDelegate protocol. This protocol manages the interface for handling incoming notifications and notification-related actions.

We then create a UNNotificationAction object (which is the task to be performed in response to a delivered notification) with an internal identifier of “show”, a title of “Tell me more…” and the .foreground option. By reading the Declaring your actionable notification types article we learn that to support actionable notifications we must

  • Declare one or more notification categories at launch time from our iOS app.
  • Assign appropriate actions to our notification categories.
  • Handle all actions that we register.
  • Assign category identifiers to notification payloads when generating notifications.

In our case this action we just created is very similar in intent to what is done when we create a UIAlertAction so we can, in our brain, group those two things together. The UNNotificationCategory, instead is more the equivalent to the alert controller we used then as it groups the different actions all together.

Here we create an UNNotificationCategory object with the identifier “alarm”, which is the same we used in content.categoryIdentifier; for the actions parameter we will use an array containing the show action (it is an array because we could potentially have many buttons), an an empty array for the next two parameters, which in this case we do not need.

Finally we set our category to be a notification category for our notification center calling center.setNotificationCategories([category]).

Did the system receive my call?

Now we need to know whether the system received a response or not. This is performed using the userNotificationCenter(_:didReceive:withCompletionHandler:) method. As you recall, we set the content.userInfo to be a dictionary containing a key called “customData” and a corresponding value of “fizzbuzz”. Inside this method we need to pull out that data and store it into a constant that we will call userInfo using the response.notification.request.content.userInfo call.

Once that’s done we can check if the “customData” key has a string value inside and, if so, print it to the console. At this point we are in the user’s hands so we have to switch over the possibilities the user will have (which are handled by the .actionIdentifier property.

By default, the system provides two of them: UNNotificationDefaultActionIdentifier, that is when the user swiped to unlock and UNNotificationDismissActionIdentifier, for when the user tapped on the “Dismiss” button. In the middle between these two buttons we can add our one by passing the string “show” as one of the cases. Upon tapping it the notification will become bigger on the screen and we will see the “Tell me more…” button, which upon tap will bring us inside the app.

After all this we need to call completionHandler().


And that’s it, the project should be over like that.

You can find the code for the project in its current state in the master branch of this GitHub repository.

Wrap up

This was a surprisingly straightforward project but it allows us to enter a wonderfully deep and complex territory: notifications! We should always care about privacy and about the user’s right to remain in control of what they want to do. As mentioned, we are just scratching the surface of this new playground and, if we would like to dive further into it, we should go on and check Paul’s book called Advanced iOS: Volume One, which you should definitely check out.

Review for Project 21

Here is a recap of what was learned during this project:

  1. Notifications are part of the UserNotifications framework. This is also available on macOS, tvOS, and watchOS.
  2. Users can launch our app from a notification but this isn’t required; some notifications work better with a silent action.
  3. DateComponents stores values like hours, minutes, days, and months. We usually just set the components we care about, for example we might set the month but not the year.
  4. Notifications appear on the user’s lock screen. If they are currently using their device, notifications also appear as a banner. We did not test this because we always locked the screen but it works.
  5. We can change the delegate of the default UNUserNotificationCenter. This lets us control who gets told when notifications are triggered.
  6. UUID stands for “Universally Unique Identifier” and they are a long and random string of letters and numbers.
  7. If a function receives a closure parameter and doesn’t use it immediately, it must be marked @escaping. Think about it like this: the closure escapes the method rather than being used inside there then thrown away. We looked at it before. Closures get destroyed after they are used, as normal functions do, but marking them @escaping allows them to survive for later use. Under the hood it is all much more complicated than this but by now we do not need to know anything more about it.
  8. Notification categories let us attach buttons to our notifications. Each button can have its own action attached to it for custom behaviour.
  9. Notifications can be attached to a user’s location. This approach is called “geofencing” and lets us send messages as the user moves about.
  10. We can attach a custom data dictionary to our notifications. This lets us provide context that gets sent back when the notification action is triggered.
  11. Notifications can include alerts, sounds, and badges. There are extra settings, but these are the most common.
  12. We must request the user’s permission to show notifications. We can only start showing notifications when permission is granted.

As you may have noticed, my review chapter for today has been much shorter and that is thanks to all the researches I have done before. I cannot say that I have mastered the subject after one day but, at least, I feel fine with it and not overwhelmed.

Now on with the challenges: from what I read they look hard, but I will see, one by one.

Hacking with Swift: Challenge 21

Challenge 1: update the code in didReceive so that it shows different instances of UIAlertController depending on which action identifier was passed in.

At first I had not understood the text in the question but then, with the code in front of me, it looked quite straightforward. Inside each switch case we need to create a different alert controller with at least another action. At this point there is no need to show the code, you can check the repository at the “Challenge21-1” branch as I want to keep things separated.

Challenge 2: for a harder challenge, add a second UNNotificationAction to the alarm category of project 21. Give it the title “Remind me later”, and make it call scheduleLocal() so that the same alert is shown in 24 hours.

For the purpose of these challenges, a time interval notification with 86400 seconds is good enough—that’s roughly how many seconds there are in a day, excluding summer time changes and leap seconds—.

Well … this challenge was indeed harder but the solution I found is completely idiot. I first tried it and thought it couldn’t be such an idiot solution but then I also checked someone else’s code and, boy, it was my same idea … so … fine, here it is.

Let’s go in the order this challenge is worded: inside registerCategories I have added a new notification action in this form:

let remind = UNNotificationAction(identifier: "remind", title: "Remind me later...", options: .foreground)

Then, inside the category constant below I just added , remind to the array of actions. So far so good.

Then, in the didReceive method added a new case to the action identifier’s switch with a scheduleLocal call and… here I hit the first issue. As the scheduleLocal call has a single trigger (and I really believe it can have only one of them at a time), this would be called again and I could not set it to 24 hours later.

At first I tried to add an extra parameter to the scheduleLocal so that it could be handled separately but, to my greatest surprise, the UIBarButtonItem initialiser doesn’t support a #selector method with a parameter, where other times I had been able to do so (now I can’t recall when from atop my head but it was in this course).

I then thought to use a default parameter of 5 so that calling the scheduleLocal in the button’s initialiser would have worked without complaints but then, somehow, the notification didn’t get called… mystery…

I then resorted to changing the scheduleLocal method to scheduleLocal(after timeInterval: TimeInterval) (and accordingly modifying the trigger’s timeInterval parameter to get passed the timeInterval one from the function) and then create a new @objc method called scheduleTapped() where scheduleLocal(after: 5) would be called.

Then I would update the selector in the bar button item initialiser to #selector(scheduleTapped) and the case "remind" to scheduleLocal(after: 86400).

I have tested this with shorter time spans and it works, just I really hope there is a better solution than this as, at the very least, this looks pretty inelegant.

Challenge 3: …and for an even harder challenge, update project 2 so that it reminds players to come back and play every day.

This means scheduling a week of notifications ahead of time, each of which launch the app. When the app is finally launched, make sure you call removeAllPendingNotificationRequests() to clear any un-shown alerts, then make new alerts for future days.

What can I say? This was hard but more on a mechanical level, not on a conceptual one. I kind of new from the beginning what I had to do and the main thing was not even the how but … in which order? But this proved a lot of fun and I have installed the app on my device so that, for at least the next three days, I will possibly get a reminder at 9.30am to play the game. If this doesn’t happen I will just go back and revise the code but, hopefully, I will not need to.

So, let’s break this down into as clear a guide as possible: first I added an import UserNotifications at the top of the file and preemptively made the class conform to UNUserNotificationCenterDelegate.

Then, at the bottom of the class I added a // MARK: - Notifications to be able to quickly find this section at a later stage if needed and created a registerLocal method which, sincerely, was just the same as the one we used in the other app. I then called it within viewDidLoad, just after the call to super.viewDidLoad. I tested it and it worked, phew 😅, one thing less.

Then it was a mess because I either had to write seven schedule methods (one per day) or find a shortcut because all of them had to be equal apart from the weekday they would be called in. After starting to write the new schedule method I realised I needed to use heavy refactoring to optimise the code as much as possible. I could explain everything bit by bit but I guess seeing the code will be much clearer.

At the top of the line I have a method that will call the schedule code every day of the week, with the quirk that the loop has to run from 1 to 7 because this is how days work, at least in the Gregorian calendar.

func scheduleWeek() {
    for weekday in 1 ... 7 {
        schedule(for: weekday)
    }
} 

Then comes the schedule(for:) method which is pretty similar to the scheduleLocal one we had earlier but with a very heavy optimisation inside.

func schedule(for weekday: Int) {
    registerCategories()
    
    let center = UNUserNotificationCenter.current()
    let content = createContent() // I compressed this code which had to be the same every time
    let dateComponents = setDate(at: 9, and: 30, on: weekday) // same as above but with time of the day hardcoded and weekday dependent from the weekday parameter
    let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
    
    center.add(request)
}

The rest is the createContent() which, to work, has to return a UNMutableNotificationContent like this:

func createContent() -> UNMutableNotificationContent {
	let content = UNMutableNotificationContent()
	content.title = "It's flag time!"
	content.body = "Your favourite flags collection is missing you!"
	content.categoryIdentifier = "alarm"
	content.userInfo = ["customData": "It's game time!"]
	content.sound = .default
        
	return content
}

… and the setDate which becomes this nice boy:

func setDate(at hour: Int?, and minutes: Int?, on weekday: Int?) -> DateComponents {
    var dateComponents = DateComponents()
    dateComponents.hour = hour
    dateComponents.minute = minutes
    dateComponents.weekday = weekday
    
    return dateComponents
}

The final part is the same stuff we say before, but I wanted to expand on the concept of snoozing the notification but I didn’t want the approach I took in the previous challenge because, as I said, it really looked idiotic to me and not Swifty at all.

In the didReceive method I inserted three cases: the UNNotificationDefaultActionIdentifier, correspondent to the user just swiping to open the app after the notification triggered, would simply .removeAllPendingNotificationRequests() (still I would like to understand how all this works and why, but never mind). The second case, “show”, would remove the notification requests, then add an alert controller with a “Welcome back!” title and a message of “Your flags have missed you! Ready to play?” Upon which the user would have the only option to tap the “Yes, let’s play!”. I could have added a “No thank you!” action to make the app crash but … well … who is that evil? The final and most interesting case would be the “remind” one, for which I have to credit Simon Ljungberg and his solution.

I created an extension to UNNotification with a method called snoozeNotification(for:minutes:seconds:) inside. This would recreate the same content as before but, most importantly, it would unearth the identifier from self.request.identifier (brilliant!) and check that there would be an old trigger. I also met the debugPrint() function for the first time, here!

Then the old trigger’s date components would be extracted and modified with the method’s parameters and the rest is kind of history! Here is the extension for your pleasure:

extension UNNotification {
    func snoozeNotification(for hours: Int, minutes: Int, seconds: Int) {
        let center = UNUserNotificationCenter.current()
        let content = UNMutableNotificationContent()
        
        content.title = "It's flag time!"
        content.body = "We are waiting for you!"
        content.userInfo = ["customData": "It's game time!"]
        content.sound = .default
        
        let identifier = self.request.identifier
        guard let oldTrigger = self.request.trigger as? UNCalendarNotificationTrigger else {
            debugPrint("Cannot reschedule notification without calendar trigger.")
            
            return
        }
        
        var components = oldTrigger.dateComponents
        components.hour = (components.hour ?? 0) + hours
        components.minute = (components.minute ?? 0) + minutes
        
        let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
        let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
        center.add(request) { (error) in
            if let error = error {
                debugPrint("Rescheduling failed", error.localizedDescription)
            } else {
                debugPrint("Rescheduling succeeded")
            }
        }
    }
}

Inside didResponse all I had to do was to call:

response.notification.snoozeNotification(for: 0, minutes: 10, seconds: 0)

Much more elegant that any other solution I had tried before.

And that’s it!

I really hope this will work tomorrow morning when 9.30 arrives, and all the days after that! If anyone knows of a way to test this in some clever way, feel free to get in touch!

This was a great day, I think I coded for … about 5 hours?! I will be away almost all day tomorrow and then three days after that in which I will not have my Mac with me so, no coding until Thursday evening or, more probably, Friday, but this extra work today was very much worth the effort!

You can find the code for this last challenge here, in branch “Challenge21-3”.


Please don’t forget to drop a hello and a thank you to him for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page.

If you would like to give a look at his Advanced iOS books, you can check this link for Volume 1 and this one for Volume 2. He has about 20 great books on Swift, all of which you can check about here.

The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create your website with WordPress.com
Get started
%d bloggers like this: