3D Touch quick actions not working when called from didFinishLaunchingWithOptions - ios

I'm implementing 3D touch quick actions in my app and I have the following problem:
When the app is already running and so the quick action is initiated through perform Action For Shortcut Item, it works perfectly fine. However, when the app is killed and then launched through a quick action (so didFinishLaunchingWithOptions) it does not take me to the desired view controller, but rather to the home screen of the app.
Here is my code:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//... other stuff I'm doing
if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
shortcutItemToProcess = shortcutItem
}
return true
NOTE: I've read previous SO answers where they said that I need to return false in didFinishLaunchingWithOptions when the app was launched through a quick action, so that performAction won't get called. I need to always return true however in my didFinishLaunching method because of the other things I'm handling there. I tried however to return false just to see if that causes the problem and the app still behaved the same way.
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
shortcutItemToProcess = shortcutItem
}
Here is how I present the view controller:
func applicationDidBecomeActive(_ application: UIApplication) {
if let shortcutItem = shortcutItemToProcess {
if shortcutItem.type == "com.myName.MyApp.myQuickAction" {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myViewController = storyboard.instantiateViewController(withIdentifier: "myViewController") as! MyViewController
if let navVC = window?.rootViewController as! UINavigationController? {
navVC.pushViewController(myViewController, animated: true)
}
}
So this works fine when app is already running, but it lands me on the home page of my app when the app is killed. What am I doing wrong and how can I solve this?

Related

How to call a view controller via Quick Actions (3D touch) Swift

I'm trying to implement a 3D touch command where if the user presses "New scan", then the view controller ProcessedImageViewController is called. I have already set up the Info.plist to create the quick option, but I am having trouble actually calling the ProcessedImageViewController when "New scan" is pressed.
Here is my code from the AppDelegate.swift:
var launchedShortcutItem: UIApplicationShortcutItem?
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem{
launchedShortcutItem = shortcutItem
}
return true
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
completionHandler(handleShortcutItem(item: shortcutItem))
}
func applicationDidBecomeActive(_ application: UIApplication) {
guard let shortcutItem = launchedShortcutItem else { return }
//If there is any shortcutItem,that will be handled upon the app becomes active
_ = handleShortcutItem(item: shortcutItem)
//We make it nil after perfom/handle method call for that shortcutItem action
launchedShortcutItem = nil
}
func handleShortcutItem(item: UIApplicationShortcutItem) -> Bool {
var handled = false
// Verify that the provided shortcutItem's type is one handled by the application.
let mainStoryboard = UIStoryboard.init(name: "Main", bundle: Bundle.main)
var reqVC: UIViewController!
reqVC = mainStoryboard.instantiateViewController(withIdentifier: "ProcessedImageViewController") as! ProcessedImageViewController
handled = true
if let homeVC = self.window?.rootViewController as? UINavigationController {
homeVC.pushViewController(reqVC, animated: true)
} else {
return false
}
return handled
}
When I try to click on "New scan" in the Quick Actions menu, I only get taken to the Root View controller (not ProcessedImageViewController).

App crashes after push notification is tapped when app is killed

I am attempting to launch a specific viewController in my CustomTabBarController after receiving remote notifications. But somehow, the app always crashes when the app is killed.
Meaning to say, kill the app -> received push notifications -> tap the notifications -> app launches and crashed. This also happens when I tap the notification from the lockscreen.
I am able to execute when the app is in the background, but not when the app is killed. My code so far:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
...
if let remoteNotification = launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] as? NSDictionary {
guard let rootViewController = self.window?.rootViewController as? CustomTabBarController else {
return true
}
rootViewController.selectedIndex = 1
}
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if application.applicationState == .background || application.applicationState == .inactive {
guard let rootViewController = self.window?.rootViewController as? CustomTabBarController else {
return
}
rootViewController.selectedIndex = 1
}
}
I've followed this post to check the launchOptions, but it still crash. What can I try next?
Comment these 2 lines
// window = UIWindow(frame: UIScreen.main.bounds)
// window?.makeKeyAndVisible()
as overriding the window property destroys the initialization from storyboard (makes rootVC nil) and before return true window must have one

How can I delay splash launch screen programmatically in Swift Xcode iOS

I have put an image in imageView in LaunchStoreyboard. How can I delay the time of image programmatically?
Here is the Launch Screen Guideline from Apple.
Here is code for Launch Screen View controller:
import UIKit
class LaunshViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.delay(0.4)
}
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
}
As of today there is no predefine method from Apple to hold launch screen. Here are some Approaches which are not optimum but works
Approach #1
Create a Separate ViewController which has Launch logo & create a timer or perform some operation (Like Database/Loads some essential network call) depends on your app type this way you can ready with data before hand & hold the launch screen as well :)
Approach #2 Not Optimum
Use Sleep code which holds up the app for a while.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Thread.sleep(forTimeInterval: 3.0)
// Override point for customization after application launch.
return true
}
Would not recommending setting the entire application in a waiting state.
If the application needs to do more work before finishing the watchdog could kill the application for taking too long time to start up.
Instead you could do something like this to delay the launch screen.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()
window?.makeKeyAndVisible()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
}
return true
}
Swift 4.x
It is Not a good practice to put your application to sleep!
Booting your App should be as fast as possible, so the Launch screen delay is something you do not want to use.
But, instead of sleeping you can run a loop during which the receiver processes data from all attached input sources:
This will prolong the launch-screen's visibility time.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
RunLoop.current.run(until: NSDate(timeIntervalSinceNow:1) as Date)
return true
}
Swift 5.x, iOS 13.x.x
Modifying the following function in the AppDelegate class does not work in Swift 5.x/iOS 13.x.x.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
Instead, you will have to modify the scene function in SceneDelegate class as following. It will delay the LaunchSceen for 3 seconds.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window?.rootViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()
window?.makeKeyAndVisible()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
}
guard let _ = (scene as? UIWindowScene) else { return }
}
The window variable should already be there in SceneDelegate class like the following.
var window: UIWindow?
Definitely your app should not be put to sleep as it may be killed by the OS for being unresponsive for so long.
If you're using a static image for your launch screen, what works for me is to use the image in the LaunchScreen.storyboard, and then when your main controller launches, modally present a VC with the same image as the background in the ViewDidAppear of your main controller (with animated set to false).
You can then use your logic to know when to dismiss the launch screen (dismiss method in the VC with animated set to false).
The transition from the actual LaunchScreen to my VC presenting the same screen looks to me imperceptible.
PS: the ViewDidAppear method might be called more than once, in which case you need to use logic to not present the VC with the launch screen a second time.
Create a ViewController and use NSTimer to detect the delay time. and when the timer ends push the first UIViewcontroller.
In ViewDidLoad method..
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(fireMethod) userInfo:nil repeats:NO];
-(void)fireMethod
{
// push view controller here..
}
Put one line of code in AppDelegate Class;
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
Thread.sleep(forTimeInterval: 3.0)
return true
}
SwiftUI
For SwiftUI, you can put a very similar code to the accepted answer into ContentView.onAppearing:
struct ContentView: View {
var body: some View {
Text("Hello")
.onAppear {
Thread.sleep(forTimeInterval: 3.0)
}
}
}
Putting a thread to sleep is not a good idea.
I would suggest you go to SceneDelegate's "willConnectTo" function and paste this piece of code and you are good to go.
window?.rootViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()
window?.makeKeyAndVisible()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
}
guard let _ = (scene as? UIWindowScene) else { return }

iOS change ViewController if user is not authed with Facebook (Swift)

I have a tabbed Swift app (and new to app development) and I have the FB Core and Login kit.
If the user is logged in (FBSDKAccessToken.currentAccessToken() is not nil) then I'd like to display the view as normal. If they're not I'd like to display a login view with a button to authorize the app.
I know how to make the button, my question is what's the best place/way to interrupt the app and redirect to the login view. This should probably also do the check and redirect when coming back on to the app.
I saw this answer showing how you can interrupt and load a different view in func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?).
I suppose I should also check in func applicationDidBecomeActive(application: UIApplication)?
Facebook says to use the SDK as follows:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// showLoginIfUnauthed() ??
return FBSDKApplicationDelegate.sharedInstance()
.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Overload when coming back from authenticating
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
return FBSDKApplicationDelegate.sharedInstance()
.application(application, openURL: url, sourceApplication: sourceApplication, annotation: annotation)
}
func applicationDidBecomeActive(application: UIApplication) {
// showLoginIfUnauthed() ??
FBSDKAppEvents.activateApp()
}
I was thinking of calling a method in these three functions, is this the wrong way of going about this?:
func showLoginIfUnauthed() {
if !FBSDKAccessToken.currentAccessToken() {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var loginController: LoginViewController = mainStoryboard.instantiateViewControllerWithIdentifier("LoginController") as! LoginViewController
self.window?.rootViewController = loginController
self.window?.makeKeyAndVisible()
}
}
Thanks.

Launching with UIApplicationShortcutItem

I'm implementing some 3D touch quick actions for my iOS 9 app in swift, and I have a curious issue. When my app is in the background and I launch with the quick action, everything goes as planned. When my app is totally dead (i.e. I killed it from the multitasking menu), and I launch with the quick action, the app crashes. I'm having trouble debugging this as once I kill the app, the debug session in Xcode gets detached. Is there a way for me to connect to the app to debug like normal, or is there something in my code that would be causing it? Thanks in advance.
Code:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
var launchedFromShortCut = false
//Check for ShortCutItem
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem
{
launchedFromShortCut = true
self.handleShortCutItem(shortcutItem)
}
return !launchedFromShortCut
}
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void)
{
self.handleShortCutItem(shortcutItem)
}
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem)
{
//Get type string from shortcutItem
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type)
{
//Get root navigation viewcontroller and its first controller
let rootNavigationViewController = window!.rootViewController as? UINavigationController
if let rootViewController = rootNavigationViewController?.viewControllers.first as! LaunchViewController?
{
//Pop to root view controller so that approperiete segue can be performed
rootNavigationViewController?.popToRootViewControllerAnimated(false)
switch shortcutType
{
case .Compose:
rootViewController.shouldCompose()
break
}
}
}
}
Thanks!
In Xcode, open Product -> Schemes -> Edit Schemes
In your Run Scheme, change the Launch setting to 'Wait for executable to be launched'
Now, if you turn on debugging and run your app, Xcode will wait for you to launch your app from the home screen so you are able to test launching it using a 3D Touch Shortcut Item.
I finally got this working. Here's what my AppDelegate.swift file ended up as;
class AppDelegate: UIResponder, UIApplicationDelegate {
// Properties
var window: UIWindow?
var launchedShortcutItem: UIApplicationShortcutItem?
func applicationDidBecomeActive(application: UIApplication) {
guard let shortcut = launchedShortcutItem else { return }
handleShortcut(shortcut)
launchedShortcutItem = nil
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
var shouldPerformAdditionalDelegateHandling = true
// If a shortcut was launched, display its information and take the appropriate action
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
launchedShortcutItem = shortcutItem
// This will block "performActionForShortcutItem:completionHandler" from being called.
shouldPerformAdditionalDelegateHandling = false
}
return shouldPerformAdditionalDelegateHandling
}
func handleShortcut( shortcutItem:UIApplicationShortcutItem ) -> Bool {
// Construct an alert using the details of the shortcut used to open the application.
let alertController = UIAlertController(title: "Shortcut Handled", message: "\"\(shortcutItem.localizedTitle)\"", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
// Display an alert indicating the shortcut selected from the home screen.
window!.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
return handled
}
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
completionHandler(handleShortcut(shortcutItem))
}
Much of this was taken from Apple's sample code for UIApplicationShortcuts, and while I'm having my app launch an alert to prove that it is recognizing the proper shortcut was chosen, this could be adapted to your code to pop the view controller.
I think the func applicationDidBecomeActive was the critical part that I was missing, and removing the self.handleShortCut(shortcutItem) from didFinishLaunchingWithOptions (otherwise it was calling handleShortCut twice, it seemed).
For Swift 4.2
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
var isLaunchedFromQuickAction = false
if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
isLaunchedFromQuickAction = true
handleQuickAction(shortcutItem: shortcutItem)
}
return isLaunchedFromQuickAction
}
I tried all the above, and it didn't solve the problem
than I tried handling the shortcut after delay in handleShortcut method:
self.performSelector("action1", withObject: self, afterDelay: 0.5)
and added a method for every action, and it worked like a charm
Replace your didfinishlaunching method with this one.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
if let shortcutItem =
launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]
as? UIApplicationShortcutItem {
handleShortcut(shortcutItem)
return false
}
return true
}
XCode 11.6, Swift 5
We can attach a process at runtime. XCode will wait until the process is started and will get attached to it when an App is launched manually.
XCode -> Debug -> Attach to process by PID or Name -> ("Enter the name of an app in the pop-up")
Directions:
Make sure an application is freshly installed on a device or a simulator.
Kill the application.
In XCode attach the name of the process as mentioned above.
Open the application through the desired shortcut
P.S: If you are using SceneDelegate a shortcutItem can be found in
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
switch connectionOptions.shortcutItem?.localizedTitle {
case "Search":
break
case "DoSomething":
break
default:
break
}
}
Happy Debugging :)

Resources