Unit Testing UNUserNotificationCenterDelegate Methods - swift

I'd like to unit test some UNUserNotificationCenterDelegate methods, especially userNotificationCenter(_, willPresent:, withCompletionHandler:)
To do that, I'd have to create an instance of UNNotification, but that's not really possible because it only has an initWithCoder initializer. What to do?
This is an example of what I'd like to test:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler(.sound)
}

Thanks to this README and the private iOS headers here, I came up with this solution:
func testUserNotificationCenterDelegate() throws {
// Create the notification content
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Test"
notificationContent.userInfo = ["someKey": "someValue"]
// Create a notification request with the content
let notificationRequest = UNNotificationRequest(identifier: "test", content: notificationContent, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false))
// Use private method to create a UNNotification from the request
let selector = NSSelectorFromString("notificationWithRequest:date:")
let unmanaged = UNNotification.perform(selector, with: notificationRequest, with: Date())
let notification = unmanaged?.takeUnretainedValue() as! UNNotification
// Test the method
let callbackExpectation = XCTestExpectation(description: "Callback")
pushService.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) { (options) in
XCTAssertEqual(options, .sound)
callbackExpectation.fulfill()
}
wait(for: [callbackExpectation], timeout: 2.0)
}
In the code above, pushService is an instance of a class that conforms to the UNUserNotificationCenterDelegate protocol. Elsewhere in my code, I set that instance as the center's delegate: UNUserNotificationCenter.current().delegate = pushService.
Enjoy!

You can use class_createInstance(UNNotification.classForKeyedArchiver() ...)and cast the value as UNNotification
If you want to manipulate its content and date members you can subclass UNNotification and use this same formula «changing class name and cast» to create it, then you override those members- which are open- and return whatever you want

Related

Badge variable is missing in my FCM notification in Swift

I am receiving notifications in my app using FCM but i am unable to show badge count on my app icon since there seems to be missing variables in my notification. how can i add this?
Below is the error i am getting
[AnyHashable("icon"): logo.png, AnyHashable("user_id"): 3, AnyHashable("google.c.sender.id"): 938224596608, AnyHashable("aps"): {
alert = {
body = "You have new message! Check in the app.";
title = Tutil;
};
sound = default;
}, AnyHashable("google.c.a.e"): 1, AnyHashable("body"): You have new message! Check in the app., AnyHashable("user_photo"): images/3.png, AnyHashable("type"): message, AnyHashable("gcm.message_id"): 1604897386664267, AnyHashable("title"): Tutil, AnyHashable("user_name"): Mali, AnyHashable("lessons"): []]
Below is my code
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
print("%#", userInfo)
// Print message ID.
let content = notification.request.content
let badgeCount = content.badge as! Int //<<i am getting my error here as variables may not be available
incrementBadgeNumberBy(badgeNumberIncrement: badgeCount)
completionHandler([.alert,.badge,.sound])
}
You can get badge like this
if let aps = userInfo["aps"] as? Dictionary<String, Any>, let badge = aps["badge"] as? Int {
print(badge)
}

Trying to customize notifications in macOS with Swift

I am using macOS 10.5.6 and I am trying to display a custom notification. I am using UNNotificationAction to set up a drop down menu for the notification and UNNotificationCategory to save it. I can get the notification correctly. The title and body are displayed but the popup menu for the notification is displayed under a button labeled "Actions".
What I would like to happen is have the label "Actions" changed to a two button format the way that the Reminders app does. I have spent a couple of days searching this web site and several others trying to find the answer but all I have found is the method I am currently using to set up the notification with out the button format that I would like to display. I know that it can be done I just do not know which key words to use to get the answer I would appreciate any help I can get.
enter image description here
Sample notifications
A notification with an attachment:
A notification with an attachment, mouse is hovering over to make the action buttons visible (they're visible right away if there's no attachment).
Sample project
Delegate
AppDelegate is going to handle notifications in the following sample project. We have to make it conform to the UNUserNotificationCenterDelegate protocol.
import UserNotifications
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
...
}
We have to set the UNUserNotificationCenter.delegate to our AppDelegate in order to receive notifications. It must be done in the applicationDidFinishLaunching: method.
func applicationDidFinishLaunching(_ aNotification: Notification) {
setupNotificationCategories() // See below
UNUserNotificationCenter.current().delegate = self
// Other stuff
}
Authorization, capabilities, ... omitted for simplicity.
Constants
An example how to avoid hardcoded constant.
enum Note {
enum Action: String {
case acceptInvitation = "ACCEPT_INVITATION"
case declineInvitation = "DECLINE_INVITATION"
var title: String {
switch self {
case .acceptInvitation:
return "Accept"
case .declineInvitation:
return "Decline"
}
}
}
enum Category: String, CaseIterable {
case meetingInvitation = "MEETING_INVITATION"
var availableActions: [Action] {
switch self {
case .meetingInvitation:
return [.acceptInvitation, .declineInvitation]
}
}
}
enum UserInfo: String {
case meetingId = "MEETING_ID"
case userId = "USER_ID"
}
}
Setup categories
Make the notification center aware of our custom categories and actions. Call this function in the applicationDidFinishLaunching:.
func setupNotificationCategories() {
let categories: [UNNotificationCategory] = Note.Category.allCases
.map {
let actions = $0.availableActions
.map { UNNotificationAction(identifier: $0.rawValue, title: $0.title, options: [.foreground]) }
return UNNotificationCategory(identifier: $0.rawValue,
actions: actions,
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "",
options: .customDismissAction)
}
UNUserNotificationCenter.current().setNotificationCategories(Set(categories))
}
Create a notification content
Sample notification content with an attachment. If we fail to create an
attachment we will continue without it.
func sampleNotificationContent() -> UNNotificationContent {
let content = UNMutableNotificationContent()
content.title = "Hey Jim! Weekly Staff Meeting"
content.body = "Every Tuesday at 2pm"
content.userInfo = [
Note.UserInfo.meetingId.rawValue: "123",
Note.UserInfo.userId.rawValue: "456"
]
content.categoryIdentifier = Note.Category.meetingInvitation.rawValue
// https://developer.apple.com/documentation/usernotifications/unnotificationattachment/1649987-init
//
// The URL of the file you want to attach to the notification. The URL must be a file
// URL and the file must be readable by the current process. This parameter must not be nil.
//
// IOW We can't use image from the assets catalog. You have to add an image to your project
// as a resource outside of assets catalog.
if let url = Bundle.main.url(forResource: "jim#2x", withExtension: "png"),
let attachment = try? UNNotificationAttachment(identifier: "", url: url, options: nil) {
content.attachments = [attachment]
}
return content
}
Important: you can't use an image from the assets catalog, because you need an URL pointing to a file readable by the current process.
Trigger helper
Helper to create a trigger which will fire a notification in seconds seconds.
func triggerIn(seconds: Int) -> UNNotificationTrigger {
let currentSecond = Calendar.current.component(.second, from: Date())
var dateComponents = DateComponents()
dateComponents.calendar = Calendar.current
dateComponents.second = (currentSecond + seconds) % 60
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
}
Notification request
let content = sampleNotificationContent()
let trigger = triggerIn(seconds: 5)
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if error != nil {
print("Failed to add a notification request: \(String(describing: error))")
}
}
Handle notifications
Following functions are implemented in the sample project AppDelegate.
Background
This is called when your application is in the background (or even if your application is running, see Foreground below).
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler:
#escaping () -> Void) {
guard let action = Note.Action(rawValue: response.actionIdentifier) else {
print("Unknown response action: \(response.actionIdentifier)")
completionHandler()
return
}
let userInfo = response.notification.request.content.userInfo
guard let meetingId = userInfo[Note.UserInfo.meetingId.rawValue] as? String,
let userId = userInfo[Note.UserInfo.userId.rawValue] as? String else {
print("Missing or malformed user info: \(userInfo)")
completionHandler()
return
}
print("Notification response: \(action) meetingId: \(meetingId) userId: \(userId)")
completionHandler()
}
Foreground
This is called when the application is in the foreground. You can handle the notification silently or you can just show it (this is what the code below does).
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
#escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .badge, .sound])
}
iOS customization
There's another way how to customize the appearance of notifications, but this is not available on the macOS. You have to use attachments.

Call view controller function from delegate class

I'm relatively new to swift and I'm having issues trying to call a function in a view controller from a delegate I have defined. How can I call the function in my view controller from this delegate? This is a mixed project consisting of mostly Objective-C code with only one Swift controller. The function is inside of the Swift controller. Below is the delegate class:
class DelegateToHandle302:NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
//convert to https
let http = request.url!
var comps = URLComponents(url: http, resolvingAgainstBaseURL: false)!
comps.scheme = "https"
let httpsUrl = comps.url!
ViewControllerFunction(url: httpsUrl)
}
I get an error Use of unresolved identifier 'ViewControllerFunction'. I've tried creating an instance of the view controller but don't think that's the correct way to do it as this view controller also has an audio player (it also didn't work).
Here is where I call the delegate from a function inside the view controller:
let urlString = "https://urlthatredirects.com"
let config = URLSessionConfiguration.default
let url = URL(string: urlString)
//set delegate value equal to SessionDelegate to handle 302 redirect
let delegate = DelegateToHandle302()
//establish url session
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
//set task with url
let dataTask = session.dataTask(with: url!)
//init call
dataTask.resume()
I'm following part of an example on how to get the final URL from a redirection (https://gist.github.com/mgersty/b565ba4c9e9422637f15f52a5317f07e). My view controllers "header" is:
#objc class AudioPlayerController: UIViewController{........}
I hope I've provided enough info to allow anyone to assist me in figuring out what I'm doing wrong. The only thing I need to do is call that function and pass the redirection URL to it.
I'm a bit confused about what you're trying to do, and why you're trying to call a delegate method back into the VC rather than using the completion handler; but I think you've got your delegate pattern back-to-front. I'm assuming the idea is:
the view controller instiagtes the URL session
the url sessions passes of the result of the URLSession to the DelegateToHandle302 to process
DelegateToHandle302 then tries to run a method back in the view controller that launched it.
If this is the case you actually need the VC to be the delegate of the DelegateToHandle302 class, not the other way around.
So within your view controller
let handlerFor302 = DelegateToHandle302()
handlerFor302.delegate = self.
let session = URLSession(configuration: config, delegate: handlerFor302, delegateQueue: nil
//etc... as before
Create a protocol for the delegate to adopt, which defines the desired function
protocol URLProcessor {
func ViewControllerFunction(url: URL)
}
The adopt the protocol in your view controller and implement the method
extension MyViewController: URLProcessor {
func ViewControllerFunction(url: URL) { .... do whatever ...}
and then use the delegate with the protocol method in your DelegateToHandle302
class DelegateToHandle302:NSObject, URLSessionTaskDelegate {
weak var delegate: URLProcessor?
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
//Process the output
delegate?.ViewControllerFunction(url: httpsUrl)
}

Mock UNNotificationResponse & UNNotification (and other iOS platform classes with init() marked as unavailable)

I need to mock UNNotificationResponse and UNNotification so that I can test my implementation of:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Swift.Void)
However I can't usefully subclass these classes because init() is specifically marked as unavailable, resulting in compilation errors like this if I try:
/Path/to/PushClientTests.swift:38:5: Cannot override 'init' which has been marked unavailable
What alternate approaches can be taken here? I look into going down the Protocol Oriented Programming route, however since I do not control the API being called, I can't modify it to take the protocols I'd write.
To do it you do the following.
Get a real example of the object while debugging and save in file system using your simulator.
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: notification)
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/notification.mock"
fileManager.createFile(atPath: path, contents: encodedObject, attributes: nil)
Find the object in your Mac and add the file in the same target as the test class.
Now unarchive in your test.
let path = Bundle(for: type(of: self)).path(forResource: "notification", ofType: "mock")
let data = FileManager.default.contents(atPath: path ?? "")
let notification = NSKeyedUnarchiver.unarchiveObject(with: data ?? Data()) as? UNNotification
I've used the next extension to create UNNotificationResponse and UNNotification instances while implementing unit tests for push notifications on iOS:
extension UNNotificationResponse {
static func testNotificationResponse(with payloadFilename: String) -> UNNotificationResponse {
let parameters = parametersFromFile(payloadFilename) // 1
let request = notificationRequest(with: parameters) // 2
return UNNotificationResponse(coder: TestNotificationCoder(with: request))! // 3
}
}
Loads push notification payload from file
Creates UNNotificationRequest instance with specified parameters in userInfo
Creates UNNotificationResponse instance using NSCoder subclass
Here are the functions I've used above:
extension UNNotificationResponse {
private static func notificationRequest(with parameters: [AnyHashable: Any]) -> UNNotificationRequest {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Test Title"
notificationContent.body = "Test Body"
notificationContent.userInfo = parameters
let dateInfo = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date())
let trigger = UNCalendarNotificationTrigger(dateMatching: dateInfo, repeats: false)
let notificationRequest = UNNotificationRequest(identifier: "testIdentifier", content: notificationContent, trigger: trigger)
return notificationRequest
}
}
fileprivate class TestNotificationCoder: NSCoder {
private enum FieldKey: String {
case date, request, sourceIdentifier, intentIdentifiers, notification, actionIdentifier, originIdentifier, targetConnectionEndpoint, targetSceneIdentifier
}
private let testIdentifier = "testIdentifier"
private let request: UNNotificationRequest
override var allowsKeyedCoding: Bool { true }
init(with request: UNNotificationRequest) {
self.request = request
}
override func decodeObject(forKey key: String) -> Any? {
let fieldKey = FieldKey(rawValue: key)
switch fieldKey {
case .date:
return Date()
case .request:
return request
case .sourceIdentifier, .actionIdentifier, .originIdentifier:
return testIdentifier
case .notification:
return UNNotification(coder: self)
default:
return nil
}
}
}
Short answer: You can't!
Instead, decompose your implementation of
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Swift.Void)
and test the methods you call from there, instead.
Happy testing :)
It appears you can initialize UNNotificationContent objects. I've chosen to rework my push handling methods to take UNNotificationContent objects instead of UNNotificationResponse/UNNotification.
If the UNNotificationResponse value doesn't matter, and you just want to execute that method of your app delegate, you can accomplish this by creating a mock by subclassing NSKeyedArchiver like this:
class MockCoder: NSKeyedArchiver {
override func decodeObject(forKey key: String) -> Any { "" }
}
You can then call it like this:
let notificationMock = try XCTUnwrap(UNNotificationResponse(coder: MockCoder()))
appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: notificationMock) { }
Your app delegate's userNotificationCenter(_:didReceive:withCompletionHandler:) method will now have been called, allowing you to assert to your heart's content (assuming no assertions against the notification itself, at least).
You can use class_createInstance(UNNotification.classForKeyedArchiver() ...)and cast the value as UNNotification
If you want to manipulate its content and date members you can subclass UNNotification and use this same formula «changing class name and cast to your subclass» to create it, then you override those members- which are open- and return whatever you want

How to get url from webview whenever user move to other pages

How can I get a URL from webView whenever the URL is changed?
I want to change the button color when current URL is changed. So I need to check the current URL all the time.
And I also would like to get URL value as a String.
I tried the below code but it doesn't work at all.
NotificationCenter.default.addObserver(self, selector: #selector(urlChecker), name: NSNotification.Name.NSURLCredentialStorageChanged, object: webView)
you can use:
1. Solution
UIWebViewDelegate
https://developer.apple.com/reference/uikit/uiwebviewdelegate/1617945-webview
optional func webView(_ webView: UIWebView,
shouldStartLoadWith request: URLRequest,
navigationType: UIWebViewNavigationType) -> Bool
UIWebViewNavigationType:
https://developer.apple.com/reference/uikit/uiwebviewnavigationtype
don't forget to return true
case linkClicked
User tapped a link.
case formSubmitted
User submitted a form.
case backForward
User tapped the back or forward button.
case reload
User tapped the reload button.
case formResubmitted
User resubmitted a form.
case other
Some other action occurred.
2. Solution
Inject Javascript JavaScript MessageHandler
(credit to Vasily Bodnarchuk)
Solution is here: https://stackoverflow.com/a/40730365/1930509
Swift 3 example.
Description
The script is inserted into page which will displayed in WKWebView.
This script will return the page URL (but you can write another
JavaScript code). This means that the script event is generated on the
web page, but it will be handled in our function:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {...}
Full Code example
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView = WKWebView()
let getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"
let getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.addScript(script: WKUserScript.getUrlScript(scriptName: getUrlAtDocumentStartScript),
scriptHandlerName:getUrlAtDocumentStartScript, scriptMessageHandler:
self, injectionTime: .atDocumentStart)
config.addScript(script: WKUserScript.getUrlScript(scriptName: getUrlAtDocumentEndScript),
scriptHandlerName:getUrlAtDocumentEndScript, scriptMessageHandler:
self, injectionTime: .atDocumentEnd)
webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
webView.navigationDelegate = self
view.addSubview(webView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
webView.loadUrl(string: "http://apple.com")
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case getUrlAtDocumentStartScript:
print("start: \(message.body)")
case getUrlAtDocumentEndScript:
print("end: \(message.body)")
default:
break;
}
}
}
extension WKUserScript {
class func getUrlScript(scriptName: String) -> String {
return "webkit.messageHandlers.\(scriptName).postMessage(document.URL)"
}
}
extension WKWebView {
func loadUrl(string: String) {
if let url = URL(string: string) {
load(URLRequest(url: url))
}
}
}
extension WKWebViewConfiguration {
func addScript(script: String, scriptHandlerName:String, scriptMessageHandler: WKScriptMessageHandler,injectionTime:WKUserScriptInjectionTime) {
let userScript = WKUserScript(source: script, injectionTime: injectionTime, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
userContentController.add(scriptMessageHandler, name: scriptHandlerName)
}
}
Info.plist
add in your Info.plist transport security setting
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Result
Resources ##
Document Object Properties and Methods