In the previous part, we were able to set up our Pusher Beams application and also create our API backend with Laravel. We also added push notification support to the backend using the pusher-beams package.
In this part, we will continue where we left off. We will be creating the iOS application using Swift and then integrate push notifications to the application so we can receive notifications when they are sent.
In order to follow along in this tutorial you need to have the following:
In Xcode, create a new class LaunchViewController
and paste the contents of the file below into it:
1import UIKit
2
3 class LaunchViewController: UIViewController {
4 @IBOutlet weak var loginButton: UIButton!
5 @IBOutlet weak var signupButton: UIButton!
6
7 override func viewDidLoad() {
8 super.viewDidLoad()
9
10 loginButton.isHidden = true
11 signupButton.isHidden = true
12
13 loginButton.addTarget(self, action: #selector(loginButtonWasPressed), for: .touchUpInside)
14 signupButton.addTarget(self, action: #selector(signupButtonWasPressed), for: .touchUpInside)
15 }
16
17 override func viewDidAppear(_ animated: Bool) {
18 super.viewDidAppear(animated)
19
20 guard AuthService.shared.loggedIn() == false else {
21 SettingsService.shared.loadFromApi()
22 return performSegue(withIdentifier: "Main", sender: self)
23 }
24
25 loginButton.isHidden = false
26 signupButton.isHidden = false
27 }
28
29 private func loginButtonWasPressed() {
30 performSegue(withIdentifier: "Login", sender: self)
31 }
32
33 private func signupButtonWasPressed() {
34 performSegue(withIdentifier: "Signup", sender: self)
35 }
36 }
Set the controller as the custom class for the related storyboard scene.
Above we have two @IBOutlet
buttons for login and signup. In the viewDidLoad
method we hide the buttons and create a target callback for them when they are pressed. In the viewDidAppear
method we check if the user is logged in and present the timeline if so. If the user is not logged in we unhide the authentication buttons.
We also have the loginButtonWasPressed
and signupButtonWasPressed
methods. These methods present the login and signup controllers.
Next, create a SignupViewController
class and paste the following code into the file:
1import UIKit
2 import NotificationBannerSwift
3
4 class SignupViewController: UIViewController {
5 @IBOutlet weak var nameTextField: UITextField!
6 @IBOutlet weak var emailTextField: UITextField!
7 @IBOutlet weak var passwordTextfield: UITextField!
8 @IBOutlet weak var signupButton: UIBarButtonItem!
9
10 override func viewDidLoad() {
11 super.viewDidLoad()
12
13 activateSignupButtonIfNecessary()
14
15 nameTextField.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
16 emailTextField.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
17 passwordTextfield.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
18 }
19
20 @IBAction func closeButtonWasPressed(_ sender: Any? = nil) {
21 dismiss(animated: true, completion: nil)
22 }
23
24 @IBAction func signupButtonWasPressed(_ sender: Any) {
25 guard let credentials = textFields(), signupButton.isEnabled else {
26 return
27 }
28
29 ApiService.shared.signup(credentials: credentials) { token, error in
30 guard let token = token, error == nil else {
31 return StatusBarNotificationBanner(title: "Signup failed. Try again.", style: .danger).show()
32 }
33
34 AuthService.shared.saveToken(token).then {
35 self.closeButtonWasPressed()
36 }
37 }
38 }
39
40 func textFields() -> AuthService.SignupCredentials? {
41 if let name = nameTextField.text, let email = emailTextField.text, let pass = passwordTextfield.text {
42 return (name, email, pass)
43 }
44
45 return nil
46 }
47
48 func activateSignupButtonIfNecessary() {
49 if let field = textFields() {
50 signupButton.isEnabled = !field.name.isEmpty && !field.email.isEmpty && !field.password.isEmpty
51 }
52 }
53
54 @objc func textFieldChanged(_ sender: UITextField) {
55 activateSignupButtonIfNecessary()
56 }
57 }
Set the controller as the custom class for the signup storyboard scene.
Above we have three @IBOutlet
's for our signup text fields and one @IBOutlet
for our signup button. In the viewDidLoad
method we add a callback for our text fields to be triggered when the text is changed. We also call the activateSignupButtonIfNecessary
method, which activates the signup button if all the field’s contents are valid.
We have two @IBAction
functions. The first for when the close button is pressed and the other for when the signup button is pressed. When the Sign up button is pressed, the signupButtonWasPressed
method is called, which uses the ApiService
to create an account for the user and log the user in. If the signup fails we use the NotificationBanner package to display an error.
We also have other helper methods. The textFields
method returns a tuple of the text fields contents and the textFieldChanged
method is fired every time a text field’s content is modified.
Next, create a LoginViewController
class and paste the following code into the file:
1import UIKit
2 import NotificationBannerSwift
3
4 class LoginViewController: UIViewController {
5 @IBOutlet weak var emailTextField: UITextField!
6 @IBOutlet weak var passwordTextField: UITextField!
7 @IBOutlet weak var loginButton: UIBarButtonItem!
8
9 override func viewDidLoad() {
10 super.viewDidLoad()
11
12 activateLoginButtonIfNecessary()
13
14 emailTextField.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
15 passwordTextField.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
16 }
17
18 @IBAction func closeButtonWasPressed(_ sender: Any? = nil) {
19 dismiss(animated: true, completion: nil)
20 }
21
22 @IBAction func loginButtonWasPressed(_ sender: Any) {
23 guard let credentials = textFields(), loginButton.isEnabled else {
24 return
25 }
26
27 ApiService.shared.login(credentials: credentials) { token, error in
28 guard let token = token, error == nil else {
29 return StatusBarNotificationBanner(title: "Login failed, try again.", style: .danger).show()
30 }
31
32 AuthService.shared.saveToken(token).then {
33 self.closeButtonWasPressed()
34 }
35 }
36 }
37
38 func textFields() -> AuthService.LoginCredentials? {
39 if let email = emailTextField.text, let password = passwordTextField.text {
40 return (email, password)
41 }
42
43 return nil
44 }
45
46 func activateLoginButtonIfNecessary() {
47 if let field = textFields() {
48 loginButton.isEnabled = !field.email.isEmpty && !field.password.isEmpty
49 }
50 }
51
52 @objc func textFieldChanged(_ sender: UITextField) {
53 activateLoginButtonIfNecessary()
54 }
55 }
Set the controller as the custom class for the login storyboard scene.
The controller above functions very similarly to the SignupViewController
. When the loginButtonWasPressed
method is called it uses the ApiService
to log the user in and save the token.
Next, we need to create the settings controller. This will be where the settings can be managed. Create a SettingsTableViewController
and paste the following code into the file:
1import UIKit
2
3 class SettingsTableViewController: UITableViewController {
4 let settings = {
5 return SettingsService.shared.settings
6 }()
7
8 private func shouldCheckCell(at index: IndexPath, with setting: String) -> Bool {
9 let status = Setting.Notification.Comments(rawValue: setting)
10
11 return (status == .off && index.row == 0) ||
12 (status == .following && index.row == 1) ||
13 (status == .everyone && index.row == 2)
14 }
15
16 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
17 let cell = super.tableView(tableView, cellForRowAt: indexPath)
18 cell.accessoryType = .none
19
20 if let setting = settings["notification_comments"], shouldCheckCell(at: indexPath, with: setting) {
21 cell.accessoryType = .checkmark
22 }
23
24 return cell
25 }
26
27 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
28 let rowsCount = self.tableView.numberOfRows(inSection: indexPath.section)
29
30 for i in 0..<rowsCount {
31 let rowIndexPath = IndexPath(row: i, section: indexPath.section)
32
33 if let cell = self.tableView.cellForRow(at: rowIndexPath) {
34 cell.accessoryType = indexPath.row == i ? .checkmark : .none
35 }
36 }
37
38 let setting = indexPath.row == 0 ? "Off" : (indexPath.row == 1 ? "Following" : "Everyone")
39
40 if let status = Setting.Notification.Comments(rawValue: setting) {
41 SettingsService.shared.updateCommentsNotificationSetting(status)
42 }
43 }
44 }
Set the controller as the custom class for the settings storyboard scene.
In the SettingsTableViewController
, we load the settings from the SettingsService
class, which we will create later. We then define a shouldCheckCell
method, which will determine if the cell row should be checked by checking the users setting.
As seen from the storyboard scene, there are three possible settings for the comments notification section: ‘Off’, ‘From people I follow’ and ‘From everyone’. The settings controller attempts to update the setting locally and remotely using the SettingsService
when the setting is changed.
Next, create the SearchTableViewController
and paste the following code into it:
1import UIKit
2 import NotificationBannerSwift
3
4 class SearchTableViewController: UITableViewController {
5
6 var users: Users = []
7
8 override func viewDidLoad() {
9 super.viewDidLoad()
10
11 ApiService.shared.fetchUsers { users in
12 guard let users = users else {
13 return StatusBarNotificationBanner(title: "Unable to fetch users.", style: .danger).show()
14 }
15
16 self.users = users
17 self.tableView.reloadData()
18 }
19 }
20
21 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
22 return self.users.count
23 }
24
25 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
26 let user = self.users[indexPath.row]
27 let cell = tableView.dequeueReusableCell(withIdentifier: "User", for: indexPath) as! UserListTableViewCell
28
29 cell.delegate = self
30 cell.indexPath = indexPath
31 cell.textLabel?.text = user.name
32
33 if let following = user.follows {
34 cell.setFollowStatus(following)
35 }
36
37 return cell
38 }
39
40 }
41
42 extension SearchTableViewController: UserListCellFollowButtonDelegate {
43
44 func followButtonTapped(at indexPath: IndexPath) {
45 let user = self.users[indexPath.row]
46 let userFollows = user.follows ?? false
47
48 ApiService.shared.toggleFollowStatus(forUserId: user.id, following: userFollows) { successful in
49 guard let successful = successful, successful else { return }
50
51 self.users[indexPath.row].follows = !userFollows
52 self.tableView.reloadData()
53 }
54 }
55
56 }
Set the controller as the custom class for the search storyboard scene.
Though we have named the class SearchTableViewController
we are actually not going to be doing any searches. We are going to have a make-believe search result, which will display the list of users on the service with a Follow/Unfollow button to make it easy to follow or unfollow a user.
In the viewDidLoad
method we call the fetchUsers
method on the ApiService
class and then we load the users to the users
property, which is then used as the table’s data. In the class extension, we implement the UserListCellFollowButtonDelegate
protocol, which makes it easy for us to know when the Follow/Unfollow button is tapped. We use the delegation pattern to make this possible.
Next, create the TimelineTableViewController
class and paste the following code into it:
1import UIKit
2 import Alamofire
3 import NotificationBannerSwift
4 import PushNotifications
5
6 class TimelineTableViewController: UITableViewController {
7 var photos: Photos = []
8 var selectedPhoto: Photo?
9 let picker = UIImagePickerController()
10
11 override func viewDidLoad() {
12 super.viewDidLoad()
13 self.reloadButtonWasPressed()
14 self.picker.delegate = self
15 }
16
17 @IBAction func userButtonWasPressed(_ sender: Any) {
18 AuthService.shared.logout()
19 dismiss(animated: true, completion: nil)
20 }
21
22 @IBAction func reloadButtonWasPressed(_ sender: Any? = nil) {
23 ApiService.shared.fetchPosts { photos in
24 if let photos = photos {
25 self.photos = photos
26 self.tableView.reloadData()
27 }
28 }
29 }
30
31 @IBAction func addButtonWasPressed(_ sender: Any) {
32 picker.sourceType = .photoLibrary
33 picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary)!
34 picker.modalPresentationStyle = .popover
35 picker.popoverPresentationController?.barButtonItem = nil
36 present(picker, animated: true, completion: nil)
37 }
38
39 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
40 if let vc = segue.destination as? CommentsTableViewController, let photo = selectedPhoto {
41 selectedPhoto = nil
42 vc.photoId = photo.id
43 vc.comments = photo.comments
44 }
45 }
46 }
Set the controller as the custom class for the timeline storyboard scene.
In the controller above we have the photos
property, which is an array of all the photos on the service, the selectedPhoto
, which will temporarily hold the selected photo object, and the picker
property, which we will use for the image picker when trying to upload images to the service.
In the viewDidLoad
method, we load the posts by calling the reloadButtonWasPressed
method, then we set the class as the picker.delegate
. We have the @IBAction
method addButtonWasPressed
, which launches the iOS image picker.
The prepare
method is called automatically when the controller is navigating to the comments controller. So in here, we set the comments to the comments controller so we have something to display immediately. We also set the photoId
to the comments controller.
Next, in the same class, paste the following at the bottom:
1extension TimelineTableViewController {
2
3 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
4 return photos.count
5 }
6
7 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
8 let photo = photos[indexPath.row]
9 let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as! PhotoListTableViewCell
10
11 cell.delegate = self
12 cell.indexPath = indexPath
13 cell.nameLabel.text = photo.user.name
14 cell.photo.image = UIImage(named: "loading")
15
16 Alamofire.request(photo.image).responseData { response in
17 if response.error == nil, let data = response.data {
18 cell.photo.image = UIImage(data: data)
19 }
20 }
21
22 return cell
23 }
24
25 }
26
27 extension TimelineTableViewController: PhotoListCellDelegate {
28
29 func commentButtonWasTapped(at indexPath: IndexPath) {
30 self.selectedPhoto = photos[indexPath.row]
31 self.performSegue(withIdentifier: "Comments", sender: self)
32 }
33
34 }
In the code above, we have two extensions for the TimelineTableViewController
. The first extension defines how we want to present the photos to the table view. The second extension is an implementation of the PhotoListCellDelegate
, which is another implementation of the delegation pattern. The method defined here, commentButtonWasTapped
, will be triggered when the Comment button is pressed on a photo cell.
In the same file add the last class extension at the bottom of the file:
1extension TimelineTableViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
2
3 @objc func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
4 if let selected = info["UIImagePickerControllerOriginalImage"] as? UIImage {
5 guard let image = UIImageJPEGRepresentation(selected, 0) else {
6 return
7 }
8
9 let uploadPhotoHandler: (() -> Void)? = {
10 var caption: UITextField?
11
12 let alert = UIAlertController(title: "Add Caption", message: nil, preferredStyle: .alert)
13 alert.addTextField(configurationHandler: { textfield in caption = textfield })
14 alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
15 alert.addAction(UIAlertAction(title: "Save", style: .default, handler: { action in
16 var filename = "upload.jpg"
17 let caption = caption?.text ?? "No caption"
18
19 if let url = info[UIImagePickerControllerImageURL] as? NSURL, let name = url.lastPathComponent {
20 filename = name
21 }
22
23 ApiService.shared.uploadImage(image, caption: caption, name: filename) { photo, error in
24 guard let photo = photo, error == nil else {
25 return StatusBarNotificationBanner(title: "Failed to upload image", style: .danger).show()
26 }
27
28 try? PushNotifications.shared.subscribe(interest: "photo_\(photo.id)-comment_following")
29 try? PushNotifications.shared.subscribe(interest: "photo_\(photo.id)-comment_everyone")
30
31 self.photos.insert(photo, at: 0)
32 self.tableView.reloadData()
33
34 StatusBarNotificationBanner(title: "Uploaded successfully", style: .success).show()
35 }
36 }))
37
38 self.present(alert, animated: true, completion: nil)
39 }
40
41 self.dismiss(animated: true, completion: uploadPhotoHandler)
42 }
43 }
44
45 }
In the extension above, we implement the UIImagePickerControllerDelegate
, which let’s us handle image selection from the UIImagePickerController
. When an image is selected, the method above will be called.
We handle it by getting the selected image, displaying an alert controller with a text field so we can get a caption for the image and then we send the image and the caption to the API using the ApiService
.
When the upload is complete, we add the newly added photo to the table and then we subscribe the user to the Pusher Beam Interest so they can receive push notifications when comments are made to the photo.
Also above we subscribed to two interests. The first is photo_\(id)-comment_following
and the second one is photo_\(id)-comment_everyone
. We do this so that we can segment notifications depending on the users setting. On the server, when a comment is added, if the photo owner sets the comment notification setting to following then the push notification will be published to the photo_\(id)-comment_following
interest.
Next, create the CommentsTableViewController
class and paste the following code into it:
1import UIKit
2 import NotificationBannerSwift
3
4 class CommentsTableViewController: UITableViewController {
5 var photoId: Int = 0
6 var commentField: UITextField?
7 var comments: PhotoComments = []
8
9 override func viewDidLoad() {
10 super.viewDidLoad()
11
12 navigationItem.title = "Comments"
13 navigationController?.navigationBar.prefersLargeTitles = false
14 navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(addCommentButtonWasTapped))
15
16 if photoId != 0 {
17 ApiService.shared.fetchComments(forPhoto: photoId) { comments in
18 guard let comments = comments else { return }
19
20 self.comments = comments
21 self.tableView.reloadData()
22 }
23 }
24 }
25
26 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
27 return comments.count
28 }
29
30 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
31 let cell = tableView.dequeueReusableCell(withIdentifier: "Comment", for: indexPath) as! CommentsListTableViewCell
32 let comment = comments[indexPath.row]
33
34 cell.username?.text = comment.user.name
35 cell.comment?.text = comment.comment
36
37 return cell
38 }
39
40 @objc func addCommentButtonWasTapped() {
41 let alertCtrl = UIAlertController(title: "Add Comment", message: nil, preferredStyle: .alert)
42 alertCtrl.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
43 alertCtrl.addTextField { textField in self.commentField = textField }
44 alertCtrl.addAction(UIAlertAction(title: "Add Comment", style: .default) { _ in
45 guard let comment = self.commentField?.text else { return }
46
47 ApiService.shared.leaveComment(forId: self.photoId, comment: comment) { newComment in
48 guard let comment = newComment else {
49 return StatusBarNotificationBanner(title: "Failed to post comment", style: .danger).show()
50 }
51
52 self.comments.insert(comment, at: 0)
53 self.tableView.reloadData()
54 }
55 })
56
57 self.present(alertCtrl, animated: true, completion: nil)
58 }
59 }
Set the controller as the custom class for the timeline storyboard scene.
In the CommentsTableViewController
above we have the comments
property, which holds all the comments for the photo, the photoId
property, which holds the ID of the photo whose comments are being loaded and the commentField
property, which is the text field that holds new comments.
In the viewDidLoad
method we set up the controller title and add an ‘Add’ button to the right of the navigation bar. Next, we call the fetchComments
method in the ApiService
to load comments for the photo.
We have the addCommentButtonWasTapped
method in the controller, which is activated when the ‘Add’ button on the navigation bar is pressed. This brings up an alert controller with a text field where we can get the comment text and then send the comment to the API using the ApiService
.
Since we have created the controllers, let’s create some custom view classes that we need for the cells we used in the controllers earlier.
The first custom cell we will create will be the PhotoListTableViewCell
class. Create the class and paste the following code into the file:
1import UIKit
2
3 protocol PhotoListCellDelegate {
4 func commentButtonWasTapped(at indexPath: IndexPath)
5 }
6
7 class PhotoListTableViewCell: UITableViewCell {
8 @IBOutlet weak var nameLabel: UILabel!
9 @IBOutlet weak var photo: UIImageView!
10 @IBOutlet weak var commentButton: UIButton!
11
12 var indexPath: IndexPath?
13 var delegate: PhotoListCellDelegate?
14
15 override func awakeFromNib() {
16 super.awakeFromNib()
17 self.selectionStyle = .none
18
19 commentButton.addTarget(self, action: #selector(commentButtonWasTapped), for: .touchUpInside)
20 }
21
22 @objc func commentButtonWasTapped() {
23 if let indexPath = indexPath, let delegate = delegate {
24 delegate.commentButtonWasTapped(at: indexPath)
25 }
26 }
27 }
Set this class as the custom class for the cell in the timeline scene of the storyboard.
In the class above we have a few @IBOutlet
's for the name, photo and comment button. We have a commentButtonWasTapped
method that fires the commentWasTapped
method on a delegate of the cell.
The next cell we want to create is the CommentsListTableViewCell
. Create the class and paste the following code into the file:
1import UIKit 2 3 class CommentsListTableViewCell: UITableViewCell { 4 @IBOutlet weak var username: UILabel! 5 @IBOutlet weak var comment: UILabel! 6 }
Set this class as the custom class for the cell in the comments scene of the storyboard.
The next cell we want to create is the UsersListTableViewCell
. Create the class and paste the following code into the file:
1import UIKit
2
3 protocol UserListCellFollowButtonDelegate {
4 func followButtonTapped(at index:IndexPath)
5 }
6
7 class UserListTableViewCell: UITableViewCell {
8 var indexPath: IndexPath?
9 var delegate: UserListCellFollowButtonDelegate?
10
11 @IBOutlet weak var followButton: UIButton!
12
13 override func awakeFromNib() {
14 super.awakeFromNib()
15 self.selectionStyle = .none
16
17 self.setFollowStatus(false)
18 self.followButton.layer.cornerRadius = 5
19 self.followButton.setTitleColor(UIColor.white, for: .normal)
20 self.followButton.addTarget(self, action: #selector(followButtonTapped(_:)), for: .touchUpInside)
21 }
22
23 func setFollowStatus(_ following: Bool) {
24 self.followButton.backgroundColor = following ? UIColor.red : UIColor.blue
25 self.followButton.setTitle(following ? "Unfollow" : "Follow", for: .normal)
26 }
27
28 @objc private func followButtonTapped(_ sender: UIButton) {
29 if let delegate = delegate, let indexPath = indexPath {
30 delegate.followButtonTapped(at: indexPath)
31 }
32 }
33 }
Set this class as the custom class for the cell in the search scene in the storyboard.
In the class above we have a custom cell to display a user’s name and a follow button. We have a setFollowStatus
method that toggles the state of the follow button and we have a followButtonTapped
method that calls the followButtonTapped
method on a delegate of the cell.
That’s all for custom cell classes. Let’s move on to creating other classes and setting up push notification.
We still need to create one last file. Create an AppConstants
file and paste the following code into the file:
1import Foundation
2
3 struct AppConstants {
4 static let API_URL = "http://127.0.0.1:8000"
5 static let API_CLIENT_ID = "API_CLIENT_ID"
6 static let API_CLIENT_SECRET = "API_CLIENT_SECRET"
7 static let PUSHER_INSTANCE_ID = "PUSHER_INSTANCE_ID
8 }
In the struct above we have some constants that we will be using throughout the application. These will be used to store application credentials and will be unchanged throughout the lifetime of the application.
💡 Replace the key values with the actual values gotten from your Passport installation and from your Pusher dashboard.
Next, open the AppDelegate
class and replace the contents with the following:
1import UIKit
2 import PushNotifications
3
4 @UIApplicationMain
5 class AppDelegate: UIResponder, UIApplicationDelegate {
6
7 var window: UIWindow?
8
9 let pushNotifications = PushNotifications.shared
10
11 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
12 self.pushNotifications.start(instanceId: AppConstants.PUSHER_INSTANCE_ID)
13 self.pushNotifications.registerForRemoteNotifications()
14
15 return true
16 }
17
18 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
19 self.pushNotifications.registerDeviceToken(deviceToken)
20 }
21 }
In the class above, we use the Pusher Beams Swift SDK to register the device for push notifications.
That’s all for our application’s code.
Now that we have completed the logic for the application, let’s enable push notifications on the application in Xcode.
In the project navigator, select your project, and click on the Capabilities tab. Enable Push Notifications by turning the switch ON.
This will create an entitlements file in the root of your project. With that, you have provisioned your application to fully receive push notifications.
Let’s take it one step further and add rich notifications. We will want to be able to see the photo commented on in the notification received as this can increase engagement.
In Xcode go to ‘File’ > ‘New’ > ‘Target’ and select ‘Notification Service Extension’. Enter the name of the extension and then click proceed. Make sure the extension is added and embedded to the Gram project. We will call our extension Notification.
When the target has been created you will see a new Notification
group (it may be different depending on what you chose to call your extension) with two files in them. Open the NotificationService
class and replace the didReceive
method with the method below:
1override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
2 self.contentHandler = contentHandler
3 bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
4
5 func failEarly() {
6 contentHandler(request.content)
7 }
8
9 guard
10 let content = (request.content.mutableCopy() as? UNMutableNotificationContent),
11 let apnsData = content.userInfo["data"] as? [String: Any],
12 let photoURL = apnsData["attachment-url"] as? String,
13 let attachmentURL = URL(string: photoURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!),
14 let imageData = try? NSData(contentsOf: attachmentURL, options: NSData.ReadingOptions()),
15 let attachment = UNNotificationAttachment.create(imageFileIdentifier: "image.png", data: imageData, options: nil)
16 else {
17 return failEarly()
18 }
19
20 content.attachments = [attachment]
21 contentHandler(content.copy() as! UNNotificationContent)
22 }
Above we are simply getting the notifications payload and then extracting the data including the attachment-url
, which is the photo URL. We then create an attachment for the notification and add it to the notification’s content. That’s all we need to do to add the image as an attachment.
⚠️ Your image URL has to be a secure URL with HTTPS or iOS will not load the image. You can override this setting in your
info.plist
file but it is strongly recommended that you don’t.
Next, create a new file in the Notification extension called UNNotificationAttachment.swift
and paste the following into the file:
1import Foundation
2 import UserNotifications
3
4 extension UNNotificationAttachment {
5
6 static func create(imageFileIdentifier: String, data: NSData, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
7 let fileManager = FileManager.default
8 let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
9 let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)
10
11 do {
12 try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil)
13 let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier)
14 try data.write(to: fileURL!, options: [])
15 let imageAttachment = try UNNotificationAttachment(identifier: imageFileIdentifier, url: fileURL!, options: options)
16 return imageAttachment
17 } catch let error {
18 print("error \(error)")
19 }
20
21 return nil
22 }
23 }
The code above is a class extension for the UNNotificationAttachment
class. The extension contains the create
method that allows us to create a temporary image to store the image attachment that was sent as a push notification.
Now you can build your application using Xcode. Make sure the Laravel application is running or the app won’t be able to fetch the data.
If you are going to be testing the app’s backend using a local server, then there is one last thing we need to do. Open the info.plist
file and add an entry to the plist
file to allow connection to our local server:
That’s it now. We can run our application. However, remember that to demo the push notifications, you will need an actual iOS device as simulators cannot receive push notifications.
Here is a screen recording of the application in action:
In this article, we have seen how you can use Pusher Beams to send push notifications from a Laravel backend and a Swift iOS client application. When creating social networks it is essential that the push notifications we send are relevant and not spammy and Pusher Beams can help with this.
The source code to the application is on GitHub.