I'm trying to create a simple demo where I click a button, and a custom notification appears, accompanied with a custom sound.
The behaviour that happens:
The notification appears properly on both emulator and connected device (iPhone SE)
The standard sound plays on emulator, but not on the device.
The custom sound does not play at all. On the emulator I get the default notification sound, while on the device I don't get any sound at all.
The sound file 'alert.caf' was added to the Assets.xcassets.
This is my class (access rights have been handled in the app launch callback):
import UIKit
import UserNotifications
class HomeViewController: UIViewController, UNUserNotificationCenterDelegate {
#IBOutlet weak var btnNotification: UIButton!
#IBAction func triggerNotification(_ sender: Any) {
/* let alert = UIAlertController(title: "Alert", message: "Message", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertAction.Style.default, handler: nil))
self.present(alert, animated: true, completion: nil) */
execNotification()
}
override func viewDidLoad() {
super.viewDidLoad()
UNUserNotificationCenter.current().delegate = self
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
#escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert,.sound])
}
private func execNotification() {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "UNUserNotification Sample"
notificationContent.body = "Sample test msg"
notificationContent.badge = 0
notificationContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "alert.caf"))
notificationContent.categoryIdentifier = "GENERAL"
let notificationTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.2, repeats: false)
let notificationRequest = UNNotificationRequest(identifier: "general", content: notificationContent, trigger: notificationTrigger)
// Add Request to User Notification Center
UNUserNotificationCenter.current().add(notificationRequest) { (error) in
if let error = error {
print("Unable to Add Notification Request (\(error), \(error.localizedDescription))")
}
}
}
}
Note: I checked all settings that may have cause the alert not to be played, but there really is no reason. The app settings are correct, there is no "Do not disturb" mode on.
I use Xcode 10.1
I also could not setup custom sound with only difference that i was developing app for macOS.
My setup:
macOS Big Sur 11.4
Xcode 12.5
Solution:
Custom sound was played only by adding 'wav' file to project ('caf' did not work) and then restarting mac.
Considering similarities between iOS and mac platforms i would suggest to add 'wav' file to the project + restart iPhone.
A few tips to note:
Make sure the sound file is located in the current executable’s main bundle or in the Library/Sounds directory of the current app container directory (source).
Make sure the sound file is less than 30 seconds. If the sound file is longer than 30 seconds, the system plays the default sound instead (source).
Make sure the sound file is added in your target's "Copy Bundle Resources" in Build Phases.
Select the sound file and make sure in its "Target Membership" your app's target is properly selected.
Related
I have a little app using SwiftUI and location triggered notifications.
I got started with this project as far as the location triggered notifications part is concerned.
It is already working quite well and I have notifications firing where I expect them, but there is one thing which I don't understand.
The function userNotificationCenter:didReceive:withCompletionHandler does not seem to fire at any time. This is the code for the function in case that may be useful, though it can only ring a bell for people who know the project.
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
if response.notification.request.identifier == "nyc_promenade_notification_id" {
let notificationData = response.notification.request.content.userInfo
let message = "You have reached \(notificationData["location"] ?? "your location!")"
let alertController = UIAlertController(title: "Welcome!",
message: message,
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alertController, animated: true)
}
completionHandler()
}
Here is some additional information.
To be precise, I first made the assumption the userNotificationCenter:didReceive:withCompletionHandler method was called at some point. Without knowing if it was to be called explicitly or in the background by some framework. But here is what I can now say:
No explicit call to the function appears in the source of my project, neither in the GitHub project which I used as a starting point.
When I want to use the debugger to catch a passing through the method, I get nothing.
I also tried to put some trace (leaving some logs in a DB) in case the function would be called while walking and entering a given area. But this also led to nothing.
As a result I am not even sure this method is called (or supposed to be called).
In the end, if it is not meant to be fired. I will still have to figure out how and where I can take some action, on entering (or leaving) a given area (i.e. circular region).
Finally, here is the function fired when I decide to set a location based notification.
func requestNotification(notificationInfo: LocationNotificationInfo) {
let notification = notificationContent(notificationInfo: notificationInfo),
destRegion = destinationRegion(notificationInfo: notificationInfo)
let trigger = UNLocationNotificationTrigger(region: destRegion, repeats: false),
request = UNNotificationRequest(identifier: notificationInfo.notificationId,
content: notification, trigger: trigger)
UNUserNotificationCenter.current().add(request) {
[weak self] (error) in
DispatchQueue.main.async {
self?.delegate?.notificationScheduled(error: error)
}
}
}
Having no suggestion from any expert on the matter, I decided to make "a big test".
For that I recompiled the app, commenting out the function:
/*func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
.......
}*/
To see what would happen.
The result is that the app kept working as usual. Thus showing the function was not really useful, maybe even useless, at least in the range of my testing.
I hope this will be useful to someone else facing the same question. If you know more about the issue, please comment.
I have an app with push notifications using PHP EasyAPNS notification working fine on Swift 3, iOS 10. But one thing I can't understand is why the badge on TabItem is works fine when I launch the app from the notification alert but not when I open the app direct from the app icon (with the red Badge)
So here is the code that I use on AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [String:Any])
{
print("Message details \(userInfo)")
if let aps = userInfo["aps"] as? NSDictionary
{
if let alertMessage = aps["alert"] as? String {
let rootViewController = self.window?.rootViewController as! UITabBarController!
let tabArray = rootViewController?.tabBar.items as NSArray!
let tabItem = tabArray?.object(at: 3) as! UITabBarItem
tabItem.badgeValue = "1"
let myAlert = UIAlertController(title: "Message", message: alertMessage, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil)
myAlert.addAction(okAction)
self.window?.rootViewController?.present(myAlert, animated: true, completion: nil)
}
}
}
So, when I click in the alert to open my app, the badge is fine like this:
But when I open the app using the icon itself, the badge is not showing up:
Anyone have any idea what I'm doing wrong?
Please let me know if I can improve the question!
You should be using the application(_:didReceiveRemoteNotification:fetchCompletionHandler:) method to handle notifications. As mentioned in the docs (found here), this method is called whether the app is in the foreground or background.
Also worth noting from the docs for application(_:didReceiveRemoteNotification:)
If the app is not running when a remote notification arrives, the
method launches the app and provides the appropriate information in
the launch options dictionary. The app does not call this method to
handle that remote notification.
Note, if the app was not running and the user taps on the icon, the app will call application(_:didFinishLaunchingWithOptions:). There will be appropriate launchOption key-value pairs if the app had a remote notification which needs handling.
I have a view that:
Creates an observer for UIApplicationDidBecomeActiveNotification with invokes a selector
Sequentially asks the user for permissions to: use the camera, location & receiving push notifications.
The view has three UIButtons with state depending on each permission state, which navigate the user to settings if permissions for anything were rejected
Tapping a button which represents a permission with rejected state navigates the user to settings
Once each alert hides, using the observer action, next alert is triggered and all button states are updated to reflect any changes
Once all permissions are granted it pushes next view with the rest of the signup/in flow.
The problem is: on some devices, when running the app from a clean state (app removed and reinstalled), permissions for location & notifications are set to rejected by default, as if the user was presented an alert that was rejected.
I couldn't pinpoint any rational issue behind this, except for leftover settings from some outdated build that don't get deleted when installing a new one. This view seems to be the only place that can possibly trigger these alerts.
Did anyone have a similar issue and can suggest anything?
I would suggest you to try to check for states of location services and notification services before asking user to use it. Since if user is going to disable these the moment you ask him for permission, he will need to go to the settings and enable it there. You should try to detect if user has disabled location/notification/camera.
For camera use:
func accessToCamera(granted: #escaping (() -> Void)) {
if UIImagePickerController.isSourceTypeAvailable(.camera) {
let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeAudio)
if status == .authorized {
granted()
} else if status == .denied {
self.cameraPermissionAlert()
} else if status == .notDetermined {
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { (accessAllowed) in
if accessAllowed {
granted()
} else {
self.cameraPermissionAlert()
}
})
} else if status == .restricted {
self.cameraPermissionAlert()
}
} else {
print("Camera not available on this device")
}
}
func cameraPermissionAlert() {
let alert = UIAlertController(title: "Access to camera not available", message: "Please enable access to camera in order to use this feature", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default, handler: { (action) in
if let url = URL(string: UIApplicationOpenSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
if let top = UIApplication.topViewController() { // This is extension to UIApplication that finds top view controller and displays it
top.present(alert, animated: true, completion: nil)
}
}
For remote notifications you can use something like this:
Determine on iPhone if user has enabled push notifications
And for location services:
Check if location services are enabled
In both of these cases you can detect if this is disabled or not by user and present user with alert controller that has open settings functionality.
To test local notifications, I wrote a test app with a single view controller.
In viewDidLoad, I set up the custom action, the notification category, and the userNotificationCenter delegate.
In viewDidAppear, I set the notification content, setup a trigger that fires after 5 sec, create the notification request, and add it to the notification center.
I expect the following:
Foreground mode:
When the app is launched, it should present after 5 sec the notification in foreground. Before, the delegate function „willPresent notification“ should be called.
Background mode:
If, however, the app is put into background by pressing the home button before the trigger fires, the notification should be presented in the home screen, and the delegate function „willPresent notification“ is not called.
After the notification has been presented, the user can tap the action button.
This should bring the app into foreground, and trigger the „didReceive response“ delegate function.
What happens is:
The action button in never shown, only title and body.
When I tap the body, the delegate function „didReceive response“ is triggered using the default action identifier.
The problem:
Why is the custom action button not shown?
Here is my code:
import UIKit
import UserNotifications
class ViewController: UIViewController, UNUserNotificationCenterDelegate {
let userNotificationCenter = UNUserNotificationCenter.current()
let categotyId = "categoryID"
let actionID = "actionID"
override func viewDidLoad() {
super.viewDidLoad()
userNotificationCenter.requestAuthorization(options: [.alert]) { (granted, error) in
if granted {
let okAction = UNNotificationAction(identifier: self.actionID,
title: "OK",
options: [])
let category = UNNotificationCategory(identifier: self.categotyId,
actions: [okAction],
intentIdentifiers: [],
options: [.customDismissAction])
self.userNotificationCenter.setNotificationCategories([category])
self.userNotificationCenter.delegate = self
} else {
print("local notifications not granted")
}
}
userNotificationCenter.removeAllPendingNotificationRequests()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let content = UNMutableNotificationContent()
content.title = NSString.localizedUserNotificationString(forKey: "Title", arguments: nil)
content.body = NSString.localizedUserNotificationString(forKey: "Body", arguments: nil)
content.categoryIdentifier = categotyId
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: (5), repeats: false)
let request = UNNotificationRequest.init(identifier: "requestID",
content: content,
trigger: trigger)
userNotificationCenter.add(request, withCompletionHandler: { (error) in
if let error = error {
print("Could not add notification request. Error: \(error)")
}
})
}
// MARK: - Notification Delegate
// Will be called while app is in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// Show alert to the user
print("App in foreground. Show alert.")
completionHandler([.alert])
}
// Should be called after the user tapped the action button
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
let request = response.notification.request
let requestID = request.identifier
switch response.actionIdentifier {
case actionID:
print("Custom OK action triggered in background")
case UNNotificationDefaultActionIdentifier:
print("Default action triggered in background")
default:
print("Unknown action triggered in background, action identifier: \(response.actionIdentifier)")
}
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [requestID])
completionHandler()
}
}
Sorry for my question, but maybe somebody else has the same problem:
I simply did not know that first, only title/body is displayed:
However, I was not aware of the thin grey bar below the body. If this bar is pulled down, the custom action button appears:
Update: As of iOS 10 beta 2, rich notifications are also available on pre-3D touch devices. Pull down on the regular notification to see it.
Make sure you are testing on a iPhone6s/iPhone6s plus simulator/device, it doesn't seem to work on pre-3D touch devices.
On a iPhone6 simulator, try to click and drag down on the stock notification you get and you should see your custom UI appear.
I have an app where I have implemented push notifications. I check with the user to allow remote notifications with:
let settings = UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(settings)
UIApplication.sharedApplication().registerForRemoteNotifications()
I also store the response in a DB with the device token.
I have also implemented a UISwitch in a settings page to enable/disable push notifications however I have only been able to enable/disable this in the DB column.
My problem is that if a user selected Don't Allow in the initial request, I cannot enable push notifications in the phone settings so even if I set the value to enable in the DB, the notification will never reach the phone as the phone settings are still set to disable.
Is there a way in Swift 2 to change the push notification in the phone setting from within the app instead of the user having to go in to the settings to change? Or is it completely redundant to have a UISwitch allow the user to toggle on/off push notifications?
There isn't any way you can change push notifications permission status from program. Also, prompt asking user to allow push notifications can not be shown again and again. You can refer this https://developer.apple.com/library/ios/technotes/tn2265/_index.html.
The first time a push-enabled app registers for push notifications, iOS asks the user if they wish to receive notifications for that app. Once the user has responded to this alert it is not presented again unless the device is restored or the app has been uninstalled for at least a day.
So using UISwitch to toggle permission status doesn't make any sense unless you use switch status to turn on/off remote notifications from your server.
Updated with swift 4 :
func switchChanged(sender: UISwitch!) {
print("Switch value is \(sender.isOn)")
if(sender.isOn){
print("on")
UIApplication.shared.registerForRemoteNotifications()
}
else{
print("Off")
UIApplication.shared.unregisterForRemoteNotifications()
}
}
if you are using FireBase to send push notifications to your devices , you can use topic subscription to enable push notification in subscribed devices and unsubscribe users from the topic when you don't want the user to receive push notification in those devices that have been unsubscribed.
to subscribe a user to a topic simply import Firebase, then use this method:
Messaging.messaging().subscribe(toTopic: "topicName")
and to unsubscribe a user use:
Messaging.messaging().unsubscribe(fromTopic: "topicName")
if settings.authorizationStatus != .authorized{
// Either denied or notDetermined
// Ask the user to enable it in settings.
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
(granted, error) in
// add your own
UNUserNotificationCenter.current().delegate = self
let alertController = UIAlertController(title: appName, message: "Please enable notifications in settings and try again.", preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
})
}
}
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(cancelAction)
alertController.addAction(settingsAction)
DispatchQueue.main.async {
(UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController?.present(alertController, animated: true, completion: nil)
}
}
}