Handle remote notification from within view controller - ios

I am trying to handle remote notifications from within a specific view controller. I've set my app up so a certain view controller is presented with a specific image in an ImageView when I push a remote notification, but I can't change the image without pushing the same controller again. I'm trying to change the ImageView's image from another remote notification once the view controller has been presented. The idea I had for achieving this, is creating a way to handle remote notifications from within the within the file of the view controller that has just been presented, so the remote notification can edit the ImageView's image. This is how I present the view controller in my AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let data = userInfo["showiMac"] as? [String: String], let iMacConfig = data["iMacConfig"] {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootTabBarController = self.window?.rootViewController as! UITabBarController
let firstNavigationController = storyboard.instantiateViewController(withIdentifier: "initialVC1") as! UINavigationController
rootTabBarController.viewControllers![0] = firstNavigationController
//the viewController to present
let viewController = storyboard.instantiateViewController(withIdentifier: "configureiMac") as! ConfigureiMacViewController
// present VC
firstNavigationController.pushViewController(viewController, animated: true, completion: {
viewController.image = UIImage(named: "iMac" + iMacConfig)
})
completionHandler(.noData)
}
}
And here is how image is declared in my view controller:
var image: UIImage!
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = image
}

In your app delegate, do:
weak var configureiMacViewController: ConfigureiMacViewController?
Create helper functions to keep your code clean and readable:
private func updateiMacImage(_ image: UIImage, in existingViewController: ConfigureiMacViewController) {
existingViewController.image = image
}
private func showiMacImage(_ image: UIImage) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootTabBarController = self.window?.rootViewController as! UITabBarController
let firstNavigationController = storyboard.instantiateViewController(withIdentifier: "initialVC1") as! UINavigationController
rootTabBarController.viewControllers![0] = firstNavigationController
//the viewController to present
let viewController = storyboard.instantiateViewController(withIdentifier: "configureiMac") as! ConfigureiMacViewController
configureiMacViewController = viewController
// present VC
firstNavigationController.pushViewController(viewController, animated: true, completion: {
viewController.image = UIImage(named: "iMac" + iMacConfig)
})
}
Then update:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let data = userInfo["showiMac"] as? [String: String], let iMacConfig = data["iMacConfig"] {
let image = UIImage(named: "iMac" + iMacConfig)
if let existingViewController = configureiMacViewController {
updateiMacImage(image, in: existingViewController)
} else {
showiMacImage(image)
}
completionHandler(.noData)
}
}
The weak var in your AppDelegate will store a reference to your view controller but the "weak" keyword will allow the view controller to be released from memory if it is dismissed, in which case the variable will be nil the next time you try to access it. If the view controller is still visible, then its value will still be defined and you will be able to re-use it to update the image.
You will most likely have to also update your ConfigureiMacViewController code to:
var image: UIImage! {
didSet {
imageView.image = image
}
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = image
}
This ensure that if the view controller was already loaded and displayed, but you update the image variable, that the image view is loaded with the most recent image.
Good luck!

Related

Swift Open specific view controller from button in today extension

I am trying to open specific view controller on button click in today extension, but can not show it. The main app is opening correctly on the main root controller using url scheme but not the specific view controller I set up in App Delegate.
I tried ALL tutorials and answers I could find on Google, for examble:
How to open Specific View controller on Widgets/ Today Extension click
but I can't get it to work.
I tried to figure out where the problem is and I think the method in app delegate do not call. I wrote some print() Methods but none of them appeared in console. I have configured the URL Scheme correctly like in the answers from the link above, so I don't know why the method in App Delegate don't execute.
Hope someone has a tip what else I can try.
Code from Button IBAction in TodayViewController:
#IBAction func addButtonOnePressed(_ sender: UIButton) {
let url = URL(string: "OpenURL://")!
self.extensionContext?.open(url, completionHandler: nil)
}
Code from App Delegate Method (I tried all I could find of them):
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool
{
if url.scheme == "OpenURL"
{
let storyboard = UIStoryboard(name: "Groups", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "Group") as! MasterViewController
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
return true
}
UPDATE:
I solved it myself. I have to do all the stuff in SceneDelegate and not in AppDelegate and take care of the Tabbar Controller and the navigation Controller.
New Code in SceneDelegate.swift:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
if url.scheme == "OpenURL" {
guard let rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController else {
return
}
let storyboard = UIStoryboard(name: "Groups", bundle: nil)
if let rootVC = storyboard.instantiateViewController(withIdentifier: "Group") as? MasterViewController,
let tabBarController = rootViewController as? UITabBarController,
let navController = tabBarController.selectedViewController as? UINavigationController {
navController.pushViewController(rootVC, animated: true)
}
}
}
}

Shortcut Items, Quick Actions 3D Touch swift

I am for some reason having some troubles figuring out the Shortcut Items in my app. I have followed all the tutorials and I believe that I have done everything right; however, it is acting really weird. The problem is that when you completely close out that app and do the 3D Touch Shortcut item on the home screen, it takes you to the correct part of the app as it should; however, when the app is open in the background (like you just click the home button) it doesn't take you to the correct part of the app, it just opens it. This is really odd and I am not sure what to do because I have followed all the instructions. Thank you so much!
Code:
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
if shortcutItem.type == "com.myapp.newMessage" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 2 //this takes me to the contacts controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: {
completionHandler(true)
})
} else if shortcutItem.type == "com.myapp.groups" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 1 //this takes me to the groups controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: {
completionHandler(true)
})
} else {
//more short cut items I will add later
}
}
NOTE: these are static Shortcut Items, and I configured them in the info.plist file, This function that was presented is the only place in the app besides the info.plist file that have anything about the Shortcut Items.
EDIT: This seems like an issue with the RootViewController, because I have tried multiple different ways of doing this, and it still gives me the same warning message
2017-11-22 22:36:46.473195-0800 QuickChat2.0[3055:1068642] Warning: Attempt to present on whose view is not in the window hierarchy!
I just tried to accomplish my goals this way through this post but it gives me the exact same results as before.
You should set a variable in your performActionForShortcutItem that tells you the shortcut type and move the code you have in there into applicationDidBecomeActive. Your could should look like this
var shortcut: String?
func applicationDidBecomeActive(application: UIApplication) {
if let shortcutItem = shortcut {
shortcut = nil
if shortcutItem == "com.myapp.newMessage" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 2 //this takes me to the contacts controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: nil)
} else if shortcutItem == "com.myapp.groups" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "RecentVC") as! UITabBarController
vc.selectedIndex = 1 //this takes me to the groups controller
let root = UIApplication.shared.keyWindow?.rootViewController
root?.present(vc, animated: false, completion: nil)
} else {
//more short cut items I will add later
}
}
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
shortcut = shortcutItem.type
completionHandler(true)
}
I think a cleaner method is to set your flags in the performActionFor method, then check for them once your VC loads. Using this notification will handle the shortcut every time the app becomes active, including launch.
override func viewDidLoad() {
super.viewDidLoad()
// Sends notification if active app is reentered to handle quick actions
NotificationCenter.default.addObserver(self, selector: #selector(handleQuickActions), name: UIApplication.didBecomeActiveNotification, object: nil)

Call a UIViewController with NavigationController programmatically in SWIFT 2.x

I want to call a UIViewController with NavigationController designed in Storyboard programmatically when I select a dynamic shortcut.
I designed my UIViewController and its NavigationController in Main.storyboard and I put a storyboard ID (I called it MyUICtrlStoryID)
In order to create my dynamic shortcut I wrote the following code in the AppDelegate:
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
let handledShortCutItem = shortcutItem.type // handleShortCutItem(shortcutItem)
if handledShortCutItem == "3DTouchShourtcutID"{
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
//FirstViewcontroller UI will be called as root UIView
let initialViewControlleripad : FirstViewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewControllerID") as! FirstViewController
initialViewControlleripad.go2MySecondUI = true //set this check variable in order to launch my second UI View from the first one.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = initialViewControlleripad
self.window?.makeKeyAndVisible()
}
}
in the viewWillAppear function of my FistViewController
if #available(iOS 9.0, *) {
let shortcutItem = UIApplicationShortcutItem(type: "3DTouchShortcutID",
localizedTitle: "This action",
localizedSubtitle: "action description",
icon: UIApplicationShortcutIcon(type: UIApplicationShortcutIconType.Add),
userInfo: nil)
UIApplication.sharedApplication().shortcutItems = [shortcutItem]
//check if FirstViewControler had to call the second UI
if go2MySecondUI == true {
self.go2MySecondUI = false
let newViewCtrl = self.storyboard?.instantiateViewControllerWithIdentifier("MyUICtrlStoryID") as? SecondViewController
// call second UI view
dispatch_async(dispatch_get_main_queue()) {
self.presentViewController(newViewCtrl!, animated: true, completion: nil)
}
// }
}
This code works fine: when I select, by 3d touch, my shortcut, my secondviewcontroller will be call and its UIView will be showed .. but without its navigation controller.
otherwise if I call my secondUIcontroller by a button designed in storyboard (with related segue callback) the secondUIView will be showed with navigation controller correctly..
What is wrong?
You need to embed your first view controller in a UINavigationController. So in the AppDelegate your code should be:
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
let handledShortCutItem = shortcutItem.type // handleShortCutItem(shortcutItem)
if handledShortCutItem == "3DTouchShourtcutID"{
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
//FirstViewcontroller UI will be called as root UIView
let initialViewControlleripad : FirstViewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewControllerID") as! FirstViewController
initialViewControlleripad.go2MySecondUI = true //set this check variable in order to launch my second UI View from the first one.
let navigationController = UINavigationController(rootViewController: initialViewControlleripad)
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
}
}
Then in viewWillAppear of the FirstViewController you need to push the second view controller rather than present. So your code should read:
if #available(iOS 9.0, *) {
let shortcutItem = UIApplicationShortcutItem(type: "3DTouchShortcutID",
localizedTitle: "This action",
localizedSubtitle: "action description",
icon: UIApplicationShortcutIcon(type: UIApplicationShortcutIconType.Add),
userInfo: nil)
UIApplication.sharedApplication().shortcutItems = [shortcutItem]
//check if FirstViewControler had to call the second UI
if go2MySecondUI == true {
self.go2MySecondUI = false
let newViewCtrl = self.storyboard?.instantiateViewControllerWithIdentifier("MyUICtrlStoryID") as? SecondViewController
// call second UI view
self.navigationController?.pushViewController(newViewCtrl, animated: true)
// }
}

3D touch quick actions preview view controller only one time

I have the following issue. When I run my app on my phone, 3d touch the icon and select the quick action it launches the app presenting the correct view controller but when I put the app in background and try to invoke the quick action it just opens the app where I have left it. So to make it work I have to kill my app every time.
Here is my code:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
if shortcutItem.type == "com.traning.Search" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let searchVC = sb.instantiateViewControllerWithIdentifier("searchVC") as! UINavigationController
let root = UIApplication.sharedApplication().keyWindow?.rootViewController
root?.presentViewController(searchVC, animated: false, completion: { () -> Void in
completionHandler(true)
})
}
}
Thanks in advance.
I'm guessing you're trying to present a view controller from a view controller that's not visible. You can use extensions like:
extension UIViewController {
func topMostViewController() -> UIViewController {
if self.presentedViewController == nil {
return self
}
if let navigation = self.presentedViewController as? UINavigationController {
return navigation.visibleViewController.topMostViewController()
}
if let tab = self.presentedViewController as? UITabBarController {
if let selectedTab = tab.selectedViewController {
return selectedTab.topMostViewController()
}
return tab.topMostViewController()
}
return self.presentedViewController!.topMostViewController()
}
}
extension UIApplication {
func topMostViewController() -> UIViewController? {
return self.keyWindow?.rootViewController?.topMostViewController()
}
}
You can place both of these in your app delegate.swift, above your app delegate class, to get the currently visible view controller. Then present the search view controller on that. For example:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
if shortcutItem.type == "com.traning.Search" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let searchVC = sb.instantiateViewControllerWithIdentifier("searchVC") as! UINavigationController
let topViewController = UIApplication.sharedApplication.topMostViewController()
topViewController.presentViewController(searchVC, animated: false, completion: { () -> Void in
completionHandler(true)
})
}
}

Issue Present View Controller with Custom URL Scheme

In Storyboard: NavigationController -> UITableViewController
appdelegate.swift
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("navViewController") as! UINavigationController
if let tableVC = navVC.topViewController as? UITableViewController {
tableVC.searchText = // URL query param
}
self.window?.rootViewController?.presentViewController(navVC, animated: false, completion: nil)
}
return true
}
The problem was when I enter with custom URL in Safari in second time
appUrl://search?q=...
The first time it is Ok, open correctly, but if I come back to Safari to open another URL I got
Warning: Attempt to present <UINavigationController: 0x7fb1fb090000> on <UINavigationController: 0x7fb1fa843400> whose view is not in the window hierarchy!
I think the problem is that the rootViewController tries to present two UIViewController which triggers the warning message.How about presenting the second UIViewController on the presented UIViewController.
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("navViewController") as! UINavigationController
if let tableVC = navVC.topViewController as? UITableViewController {
tableVC.searchText = // URL query param
}
self.window?.rootViewController?.presentedViewController?.presentViewController(navVC, animated: false, completion: nil)
}
return true
}
Try this
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("navViewController") as! UINavigationController
if let tableVC = navVC.topViewController as? UITableViewController
{
tableVC.searchText
}
if let presented = window?.rootViewController?.presentedViewController?.presentedViewController
{
presented.dismissViewControllerAnimated(false, completion: {
self.window?.rootViewController?.presentedViewController?.presentViewController(navVC, animated: false, completion: nil)
})
}
else
{
self.window?.rootViewController?.presentedViewController?.presentViewController(navVC, animated: false, completion: nil)
}
return true
}

Resources