How to open VC on local notification when app is closed - ios

Hy I am new to iOS but somehow I manged to complete some tasks. I am working on app that sets reminders for user. So for this I am using Local notifications (UNUserNotificationCenterDelegate).
Everything is working good and fine. I have written some code, I am receiving notification at scheduled time and I have handled following cases.
When app is in background
When app is in forground.
My app handles these both cases perfectly or you can say as I needed. but I am helpless in following case
when the App is removed from recent, or not even running in
background at all,and a that time if the scheduled notification pops up, and user taps the notification, It opens the splash view controller then opens my app main view controller, where as I need to go to same view controller from where user set the scheduled time for reminder.
I think I am quite clear in what I want and what is happening. Is there any changes to do that. I know it can be possible as Whats App and other apps are also doing this. Please help me in doing this. ...
Note:
I am using UserNotifications (Local notification) and Deployment target is 10.3
Update:
I saw this link has same need as mine but I do not know what the selected answer suggest as I am new to iOS so I am not sure what and how to do:

So, your problem is when the app is killed or inactive and then when user tap the notification the reminder screen will show up, right?
Here's the case:
Notification shows (inactive/killed) -> tap notification -> splash -> reminder screen.
You should save your data that you want to show in notification. iOS will save any notification data in remoteNotification.
So, when user opens the app from inactive, the first thing that will be called is launchOption in AppDelegate.
Here's the example:
if launchOptions != nil {
// get data from notificaioton when app is killed or incative
if let userInfo = launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] as? NSDictionary {
// Do what you want, you can set set the trigger to move it the screen as you want, for your case is to reminder screen
}
}

When your app launches via LocalNotification your UNUserNotificationCenterDelegate method userNotificationCenter didReceive response will be called.
So I would recommend you to present your notification on top of current view controller as like below approach.
//Add this extension in any of your class
extension UIApplication {
#objc class func topViewController(_ base: UIViewController?) -> UIViewController? {
var baseController = base
if baseController == nil{
baseController = UIApplication.shared.keyWindow?.rootViewController
}
if let nav = baseController as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = baseController as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
return baseController
}
}
//In your `userNotificationCenter didReceive response` method
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
if response.actionIdentifier == "YourIdentifier"{
let controllerToPresent = MyNotificationViewController(nibName: "MyNotificationViewController", bundle: nil)
controllerToPresent.notificationInfo = response.notification.request.content.userInfo
//If navigation flow needed, add controllerToPresent to nav controller
let navConroller = UINavigationController(rootViewController: controllerToPresent)
UIApplication.topViewController(nil)?.present(navConroller, animated: true, completion: nil)
//If navigation flow not needed, present directly
UIApplication.topViewController(nil)?.present(controllerToPresent, animated: true, completion: nil)
}
completionHandler()
}

Related

receive local notifications within own app / view (or how to register a UNUserNotificationCenterDelegate in SwiftUI )

I am redeveloping an android app for iOS with SwiftUI that contains a countdown feature. When the countdown finishes the user should be noticed about the end of the countdown. The Notification should be somewhat intrusive and work in different scenarios e.g. when the user is not actively using the phone, when the user is using my app and when the user is using another app. I decided to realize this using Local Notifications, which is the working approach for android. (If this approach is totally wrong, please tell me and what would be best practice)
However I am stuck receiving the notification when the user IS CURRENTLY using my app. The Notification is only being shown in message center (where all notifications queue) , but not actively popping up.
Heres my code so far:
The User is being asked for permission to use notifications in my CountdownOrTimerSheet struct (that is being called from a different View as actionSheet):
/**
asks for permission to show notifications, (only once) if user denied there is no information about this , it is just not grantedand the user then has to go to settings to allow notifications
if permission is granted it returns true
*/
func askForNotificationPermission(userGrantedPremission: #escaping (Bool)->())
{
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
userGrantedPremission(true)
} else if let error = error {
userGrantedPremission(false)
}
}
}
Only if the user allows permission for notification my TimerView struct is being called
askForNotificationPermission() { (success) -> () in
if success
{
// permission granted
...
// passing information about the countdown duration and others..
...
userConfirmedSelection = true // indicates to calling view onDismiss that user wishes to start a countdown
showSheetView = false // closes this actionSheet
}
else
{
// permission denied
showNotificationPermissionIsNeededButton = true
}
}
from the previous View
.sheet(isPresented: $showCountDownOrTimerSheet, onDismiss: {
// what to do when sheet was dismissed
if userConfirmedChange
{
// go to timer activity and pass startTimerInformation to activity
programmaticNavigationDestination = .timer
}
}) {
CountdownOrTimerSheet(startTimerInformation: Binding($startTimerInformation)!, showSheetView: $showCountDownOrTimerSheet, userConfirmedSelection: $userConfirmedChange)
}
...
NavigationLink("timer", destination:
TimerView(...),
tag: .timer, selection: $programmaticNavigationDestination)
.frame(width: 0, height: 0)
In my TimerView's init the notification is finally registered
self.endDate = Date().fromTimeMillis(timeMillis: timerServiceRelevantVars.endOfCountDownInMilliseconds_date)
// set a countdown Finished notification to the end of countdown
let calendar = Calendar.current
let notificationComponents = calendar.dateComponents([.hour, .minute, .second], from: endDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: notificationComponents, repeats: false)
let content = UNMutableNotificationContent()
content.title = "Countdown Finished"
content.subtitle = "the countdown finished"
content.sound = UNNotificationSound.defaultCritical
// choose a random identifier
let request2 = UNNotificationRequest(identifier: "endCountdown", content: content, trigger: trigger)
// add the notification request
UNUserNotificationCenter.current().add(request2)
{
(error) in
if let error = error
{
print("Uh oh! We had an error: \(error)")
}
}
As mentioned above the notification gets shown as expected when the user is everyWhere but my own app. TimerView however displays information about the countdown and is preferably the active view on the users device. Therefore I need to be able to receive the notification here, but also everywhere else in my app, because the user could also navigate somewhere else within my app. How can this be accomplished?
In this example a similar thing has been accomplished, unfortunately not written in swiftUI but in the previous common language. I do not understand how this was accomplished, or how to accomplish this.. I did not find anything on this on the internet.. I hope you can help me out.
With reference to the documentation:
Scheduling and Handling Local Notifications
On the section about Handling Notifications When Your App Is in the Foreground:
If a notification arrives while your app is in the foreground, you can
silence that notification or tell the system to continue to display
the notification interface. The system silences notifications for
foreground apps by default, delivering the notification’s data
directly to your app...
Acording to that, you must implement a delegate for UNUserNotificationCenter and call the completionHandler telling how you want the notification to be handled.
I suggest you something like this, where on AppDelegate you assign the delegate for UNUserNotificationCenter since documentation says it must be done before application finishes launching (please note documentation says the delegate should be set before the app finishes launching):
// AppDelegate.swift
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// Here we actually handle the notification
print("Notification received with identifier \(notification.request.identifier)")
// So we call the completionHandler telling that the notification should display a banner and play the notification sound - this will happen while the app is in foreground
completionHandler([.banner, .sound])
}
}
And you can tell SwiftUI to use this AppDelegate by using the UIApplicationDelegateAdaptor on your App scene:
#main
struct YourApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This approach is similar to Apple's Fruta: Building a Feature-Rich App with SwiftUI
https://developer.apple.com/documentation/swiftui/fruta_building_a_feature-rich_app_with_swiftui
Apple have used In-app purchases this way
This class holds all your code related to Notification.
class LocalNotificaitonCenter: NSObject, ObservableObject {
// .....
}
In your #main App struct, define LocalNotificaitonCenter as a #StateObject and pass it as an environmentObject to sub-views
#main
struct YourApp: App {
#Environment(\.scenePhase) private var scenePhase
#StateObject var localNotificaitonCenter = LocalNotificaitonCenter()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(localNotificaitonCenter)
}
}
}
It is just that!

swift notification messages manually for app

How to notify the message when manually changes from device notifications from setting
. notifications changes, what method will be invoke inside app, to know that notification was changes
override func awakeFromNib() {
super.awakeFromNib()
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { (settings) in
if(settings.authorizationStatus == .authorized)
{
print(" authorized")
DispatchQueue.main.async {
self.onOffSwitch.isOn = true
}
}
else
{
print(" not authorized")
DispatchQueue.main.async {
self.onOffSwitch.isOn = false
}
}
}
I got tableview inside SettingViewcontroller customize my cell where added the swift onOffSwift.
Based on the setting manually user changes the notifications from setting I wants my app sync accordingly.
SettingViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tbl_SettingView.reloadData()
}
I expected this will work for me? but not
How to handle when user changes there notifications from setting screen should reflect on mobile app screen.
Your app doesn't get an automatic notification when the settings are changed. But the user can make this change only in the Settings app, so you can check the settings when your app comes back to the foreground.
The reason your
override func viewWillAppear(_ animated: Bool) {
doesn't help is that viewWillAppear is not called when the app comes back to the foreground.
There might be better ways but the one I know is this:
Store Settings.authorizationStatus as value in a singleton class or as an entry in UserDefaults
Whenever the app becomes active compare the new value to the stored value. If the they are not the same than you know the settings were changed
I believe you want to get your updated settings when your app enters foreground and not when viewWillAppear called. Just add the observer to your viewDidLoad() (and don't forget to remove it once you're done with the controller):
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { _ in
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in
DispatchQueue.main.async {
// update your view with current settings.authorizationStatus
}
})
}

Launching app from Local Notification; VC instantiates, but does not appear

I would like to launch my app from a local notification that will appear when the home screen is locked or when the user is in another app based on similar discussions here and here I have the following code in my AppDelegate:
func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
switch response.actionIdentifier {
case "play":
var setAlarmVC = self.window?.rootViewController as? SettingAlarmViewController
if setAlarmVC == nil {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
setAlarmVC = storyboard.instantiateViewController(withIdentifier: "AlarmViewController") as? SettingAlarmViewController
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = setAlarmVC
self.window?.makeKeyAndVisible()
}
case "snooze":
print("I pressed pause")
default:
break
}
completionHandler()
}
Within my SettingAlarmViewController's viewDidLoad, I have set up some simple print-outs
override func viewDidLoad() {
super.viewDidLoad()
print("Setting Alarm View Controller was instantiated")
}
When I press play from the local notification while the app is in the background, I get the console print-out as expected:
Setting Alarm View Controller was instantiated
But the app does not actually launch and Setting Alarm View Controller does not appear. If I then click on the app, a fresh Setting Alarm View Controller is the first visible thing. I feel like I must be missing something obvious, but I cannot figure out what it is.
EDIT: Doing more testing. When the notification appears on the lock screen and the user presses "play" from the lock screen, the password / unlock screen does not appear, but the app still launches and I get the print-out " Setting Alarm View Controller was instantiated"
Well, a day of my life wasted on this, here is the problem so that others do not have the same issue: developer.apple.com/documentation/usernotifications/… you must ad [.foreground] to your Notification Action, as in let playAction = UNNotificationAction(identifier: "play", title: "play", options: [.foreground])

Selecting tableview cell from local notification when app closed

I am implementing local notifications in my iOS10 app. It is a simple master-detail app where users enter items (medicinal drugs) with expiry dates. Notifications are triggered when an item expires.
The app structure is a DrugTableViewController and a DetailViewController (which contains an array drugs of Drug objects) embedded in a navigation controller.
I am trying to get the appropriate row in the tableview selected when the user taps on a notification.
The code below is successful when the app is open or in background, but does not select the row when the app has been closed when the notification is received (though it still loads the tableview correctly).
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
// Pull out the buried userInfo dictionary
let userInfo = response.notification.request.content.userInfo
if let drugID = userInfo["drug"] as? String {
let navigationController = self.window?.rootViewController as! UINavigationController
let destinationViewController = navigationController.viewControllers[0] as! DrugTableViewController
navigationController.popToViewController(destinationViewController, animated: true)
var selectRow: Int? = nil
for drug in destinationViewController.drugs {
if drug.uniqueID == drugID {
selectRow = destinationViewController.drugs.index(of: drug)!
let path = IndexPath(row: selectRow!, section: 0)
destinationViewController.tableView.reloadData()
destinationViewController.tableView.selectRow(at: path, animated: true, scrollPosition: .top)
selectRow = nil
break
}
}
}
// Call completion handler
completionHandler()
}
I have also tried instantiating my detail view controller from the storyboard ID but this does not work.
Any help would be greatly appreciated!
This code may be invoked until table view has beed loaded. Firstly try to delay notification handling for a second:
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
//Your code here
})
and look, will it help or not. If will, think how you can check is tableview has been loaded or not (or just left the delay)

How trigger background process from Watch on iPhone (trigger: Watch)?

I'd like to add to my Watch app functionality which send to iPhone app a Local Notification (while iPhone app is on the background or iPhone is locked).
I know how to create Local Notification itself.
What Im asking for is way, how to trigger background process (which contains also Local Notification) on iPhone by (for example) tapping on button on Apple Watch.
WKInterfaceController.openParentApplication is the official way to communicate with the iPhone. Documentation.
You pass parameters in the userInfo dictionary and retrieve results via the reply block.
On the iPhone the request is handled by appDelegate's handleWatchKitExtensionRequest method. Documentation
Code in my InterfaceController.swift:
#IBAction func btn() {
sendMessageToParentApp("Button tapped")
}
// METHODS #2:
func sendMessageToParentApp (input:String) {
let dictionary = ["message":input]
WKInterfaceController.openParentApplication(dictionary, reply: { (replyDictionary, error) -> Void in
if let castedResponseDictionary = replyDictionary as? [String:String], responseMessage = castedResponseDictionary["message"] {
println(responseMessage)
self.lbl.setText(responseMessage)
}
})
}
Next i made new method in my AppDelegate.swift:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
if let infoDictionary = userInfo as? [String:String], message = infoDictionary["message"] {
let response = "iPhone has seen this message." // odešle se string obsahující message (tedy ten String)
let responseDictionary = ["message":response] // tohle zase vyrobí slovník "message":String
NSNotificationCenter.defaultCenter().postNotificationName(notificationWatch, object: nil)
reply(responseDictionary)
}
}
As you can see I use Notification to get iOS app know that button has been tapped. In ViewController.swift I have Notification Observer and function which is executed every time observer catch notification that user tapped on button on watch ("notificationWatch" is global variable with notification key). Hope this will help to anybody.

Resources