MFMessageComposeViewControllerDelegate not being called - ios

I am trying to implement a class that will present a MFMessageComposeViewController from the AppDelegate. The class declaration looks like this:
import UIKit
import MessageUI
class MyClass: NSObject, MFMessageComposeViewControllerDelegate {
func sendAMessage() {
// message view controller
let messageVC = MFMessageComposeViewController()
messageVC.body = "Oh hai!"
messageVC.recipients = ["8675309"]
// set the delegate
messageVC.messageComposeDelegate = self
// present the message view controller
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(messageVC, animated: true, completion: nil)
}
// delegate implementation
func messageComposeViewController(controller: MFMessageComposeViewController!, didFinishWithResult result: MessageComposeResult) {
switch result.value {
case MessageComposeResultCancelled.value:
controller.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
case MessageComposeResultFailed.value:
controller.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
case MessageComposeResultSent.value:
controller.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
default:
break
}
}
}
In my AppDelegate I am creating and calling an instance of MyClass after receiving a push notification like this:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// instance of class
let handler = MyClass()
// call method
handler.sendAMessage()
}
Everything works fine at first--the message view controller appears and is responsive with no errors, but whenever the send or cancel button is pressed, the message view controller does not dismiss, the screen becomes unresponsive, the delegate is not called, and I get a BAD_ACCESS error.
If I put the MFMessageComposeViewControllerDelegate in the AppDelegate and set the messageVC. messageVC.messageComposeDelegate = UIApplication.sharedApplication().delegate as! MFMessageComposeViewControllerDelegate, then everything works fine and the controller dismisses as expected.
Why is the MFMessageComposeViewControllerDelegate not called when it lives in the MyClass object? Thanks for reading and helping!

It's crashing because your handler object is getting released and deallocated right after the call to handler.sendMessage(), and then a delegate callback is attempted on that now-deallocated object when you try to send or hit cancel. The object is getting released and deallocated because nothing is holding a strong reference to it anymore at the end of application:didReceiveRemoteNotification:.
Since you are creating this object in your app delegate, I would suggest making a property in your app delegate to hold onto this object:
var handler: MyClass?
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// instance of class
handler = MyClass()
// call method
handler?.sendAMessage()
}

Related

Handling where the user goes when clicking on a notification?

I have several different types of notifications, all of which should take the user to a different view controller when the notification is clicked on.
How should this be handled (I'm using Swift 5, by the way)? From my research, I see that people tend to present a new view controller in the AppDelegate's didReceive function, but doing all the logic, for several different view controllers, all in the AppDelegate seems wrong. Is this really the right way of doing it?
Further, I'm using Firebase to send messages to the device from the backend. I have a separate class, FirebaseUtils, where I handle all logic for the data that's passed along. Would it be better to present the view controller from here? If so, how would I do that without the root view controller?
I typically set up something along these lines (untested):
Create a NotificationHandler protocol for things that may handle notifications
protocol NotificationHandler {
static func canHandle(notification: [AnyHashable : Any])
static func handle(notification: [AnyHashable : Any], completionHandler: #escaping (UIBackgroundFetchResult) -> Void)
}
Create a notificationHandlers variable in AppDelegate and populate it with things that may want to handle notifications.
let notificationHandlers = [SomeHandler.self, OtherHandler.self]
In didReceive, loop over the handlers, ask each one if it can handle the notification, and if it can, then tell it to do so.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
guard let handler = notificationHandlers.first(where:
{ $0.canHandle(notification: userInfo) }) {
else {
return
}
handler.handle(notification: userInfo, completionHandler: completionHandler)
}
This approach keeps the logic out of the AppDelegate, which is correct, and keeps other types from poking around inside the AppDelegate, which is also correct.
would something like this work for you?
struct NotificationPresenter {
func present(notification: [AnyHashable: Any], from viewController: UIViewController) {
let notificationViewController: UIViewController
// decide what type of view controller to show and set it up
viewController.present(notificationViewController, animated: true, completion: nil)
}
}
extension UIViewController {
static func topViewController(_ parentViewController: UIViewController? = nil) -> UIViewController {
guard let parentViewController = parentViewController else {
return topController(UIApplication.shared.keyWindow!.rootViewController!)
}
return parentViewController.presentedViewController ?? parentViewController
}
}
let notificationPresenter = NotificationPresenter()
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
notificationPresenter.present(userInfo, from: UIViewController.topViewController())
}

How to perform an action on a ViewController when pushing a notification and retrieve its data

I would like to know how to change the value of a UITextField on a ViewController whenever a notification arrives and the user taps on it. The notification contains the String that I will be putting on that UITextField.
This is how my app looks
I can currently retrieve the notification data on AppDelegate and decide which tab must be selected when the user taps on the notification. This is how I do it:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
// Print full message.
print(userInfo)
let fragmento = response.notification.request.content.userInfo["fragmento"] as? String //Keeps the notification String "Fragmento" on a local variable
if fragmento == "anuncios"{ // Condition to select tab
if let tabbarController = self.window!.rootViewController as? UITabBarController {
tabbarController.selectedViewController = tabbarController.viewControllers?[1]
}
} else if fragmento == "registro"{
if let tabbarController = self.window!.rootViewController as? UITabBarController {
tabbarController.selectedViewController = tabbarController.viewControllers?[0]
}
}
completionHandler()
}
What I would like to do know is to pass the data from the notification to that specific Tab bar ViewController and change the value of the UITextField based on that data and then perform an action when that TextField changes its value.
I hope I explained myself well, otherwise please ask me whatever questions you have. Thank you so much
The NotificationCenter is potentially your simplest solution. Define a custom string to use as a common name for a a NotificationCenter notification that will be used to pass the information from the AppDelegate to whoever is listening. You can attach the string as the notification object.
When you instantiate that label, either via a custom class or from your view controller, add your notification listener to the NotificationCenter and upon receiving the notification retrieve the object attached to the notification, double check its a string then if so, use it to update your label.
For example, in AppDelegate.swift:
static let conferenciaNotification = NSNotification.Name(rawValue: "conferenciaNotification")
...
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let updatedTextFromReceivedPushNotification = "Hello world"
NotificationCenter.default.post(name: AppDelegate.conferenciaNotification, object: updatedTextFromReceivedPushNotification)
}
In your view controller with the label:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: AppDelegate.conferenciaNotification, object: nil, queue: OperationQueue.main) { (conferenciaNotification) in
if let conferenciaText = conferenciaNotification.object as? String {
myTextLabel.text = conferenciaText
}
}
}
Please note you probably should keep a reference to the NSObjectProtocol returned from the NotificationCenter when you add the observer, so you can remove it when your view controller is deinit().

how to redirect user to various screen based on remote notification type

I am receiving remote notification's in my application. Following code is written in AppDelegate file which called when i receive notification.
get notification when app is in background
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("Notification Recieved")
UIApplication.shared.applicationIconBadgeNumber += 1
//process notification
for (key,val) in userInfo{
let nkey = key as! String
if nkey == "notificationType"{
let notificationType = val as! String
handleNotificationScreen(type: notificationType)
}
}
}
Above function get called when app is in background and user clicks on notification in that function userInfo is a payload which contains info of notification. In userInfo I am passing notificationType from server which contains various values like TASK,KRA etc
Now based on these notificationType values I want to launch different screens.
If notificationType is TASK Then launch Task Screen when user clicks on notification.
I have a TabBarController which have multiple viewControllers
viewControllers = [taskController,messageController,notificationConroller,userProfileNavigationController,empCorner,perfomranceVC]
Now what should I do to launch screen in my handleNotificationScreen(type: notificationType) function.
func handleNotificationScreen(type: String){
switch type {
case "KRA":
print("anc")
case "TASK":
print("task")
case "EMPMONTH":
print("empMonth")
default:
print("none")
}
}
Thank you guys for any help.
Get the reference of your TabBarController and inside handleNotificationScreen , if you want to get TabBarControllers inside in Appdelegate, Declare property optional like this.
var tabbarController: UITabBarController?
inside didFinishLaunchingWithOptions function call this
tabbarController = self.window?.rootViewController as? UITabBarController
Now can use tabbarController to call your specific controller in your desired function.
func handleNotificationScreen(type: String){
switch type {
case "KRA":
print("anc")
case "TASK":
print("task")
tabBarController?.selectedIndex = 0 //it will open taskbarcontroler
case "EMPMONTH":
print("empMonth")
default:
print("none")
}
}

Posting NSNotification when receiving Push crash

When I receive a push notification, I postNotificaionName via NSNotificationCenter (from the AppDelegate method below):
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
playSoundWithUserInfo(userInfo)
NSNotificationCenter.defaultCenter().postNotificationName(kNotifPushReceived, object: nil)
}
I am making one of my view-controllers an observer for this notification:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(retrieveUsersAndSetData(_:)), name: kNotifPushReceived, object: nil
}
func retrieveUsersAndSetData(completed : (() -> Void)?) {
Friendship.retrieveFriendshipsForUser(backendless.userService.currentUser, includeGroups: false) { (friendships, fault) -> Void in
guard let friendships = friendships else { return }
self.friendships = friendships
self.tableView.reloadData()
}
}
Most of the time I receive a push notification, I receive a crash in the appDelegate where postNotificationName() is called:
The crash reads:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x10070)
Now most answers I've read that seem similar to this problem suggest that an object who was made an observer was never properly released, however I do not believe that is the case here because this is currently the only view-controller in the app, and I am implementing the following from the view-controller who is the observer of the notification:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
All in all, I cannot figure out why this crash occurs most of the time when I receive the push notification in the app delegate and then post my NSNotification.
The handler method 'fetchCompletionHandler' expects by you to call the completionHandler at the end of your processing:
completionHandler(UIBackgroundFetchResult.NewData)
Please read the documentation of this method, there is more to consider then just that.

I get a warning saying that the completion handler was never called When I simulate a Background fetch

I followed all the steps in order to set up the background fetch but I'm suspecting that I made a mistake when writing the function performFetchWithCompletionHandlerin the AppDelegate.
Here is the warning that I get as soon as I simulate a background fetch
Warning: Application delegate received call to - application:
performFetchWithCompletionHandler:but the completion handler was never called.
Here's my code :
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
if let tabBarController = window?.rootViewController as? UITabBarController,
viewControllers = tabBarController.viewControllers as [UIViewController]! {
for viewController in viewControllers {
if let notificationViewController = viewController as? NotificationsViewController {
firstViewController.reloadData()
completionHandler(.NewData)
print("background fetch done")
}
}
}
}
How can I test if the background-fetchis working ?
If you don't enter the first if statement, the completion handler will never be called. Also, you could potentially not find the view controller you're looking for when you loop through the view controllers, which would mean the completion would never be called. Lastly, you should probably put a return after you call the completion handler.
func application(
application: UIApplication,
performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void
) {
guard let tabBarController = window?.rootViewController as? UITabBarController,
let viewControllers = tabBarController.viewControllers else {
completionHandler(.failed)
return
}
guard let notificationsViewController = viewControllers.first(where: { $0 is NotificationsViewController }) as? NotificationsViewController else {
completionHandler(.failed)
return
}
notificationViewController.reloadData()
completionHandler(.newData)
}

Resources