3D Touch Quick Action launches black screen. UiNavigationController->UITableViewController-> UIViewController - ios

The app that I am working on has a UITableViewController embedded in a UINavigationController. Tapping on cells in the UITableViewController presents other UIViewControllers. I am trying to implement 3D touch in an iOS app so that the user can directly access one of the UIViewControllers from the home screen. Everything works fine except that when I tap on the link on the home screen, I get a black screen (except for the navigation bar). Here is the relevant code from the AppDelegate:
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
guard ShortcutIdentifier(fullType: shortcutItem.type) != nil else { return false }
guard let shortCutType = shortcutItem.type as String? else { return false }
let storyboard = UIStoryboard(name: "Main", bundle: nil)
var vc = UIViewController()
switch (shortCutType) {
case ShortcutIdentifier.Tables.type:
vc = storyboard.instantiateViewController(withIdentifier: "TableVC") as! StatTableViewController
handled = true
break
case ShortcutIdentifier.ChiSquare.type:
vc = storyboard.instantiateViewController(withIdentifier: "Chisquare") as! CSViewController
handled = true
break
case ShortcutIdentifier.PowerContinuous.type:
vc = storyboard.instantiateViewController(withIdentifier: "PowerCont") as! PowerContViewController
handled = true
break
case ShortcutIdentifier.PowerDichotomous.type:
vc = storyboard.instantiateViewController(withIdentifier: "PowerDichot") as! PowerDichotViewController
handled = true
break
default:
break
}
let navVC = self.window?.rootViewController as! UINavigationController
navVC.pushViewController(vc, animated: true)
// navVC.show(vc, sender: self) // Same result
return handled
}
I'm reasonably sure that I'm getting to the correct UIViewController each time, but the screen is black. I can navigate back to the UITableViewController, where I can then segue back to the UIViewController and it works just fine. So it is clearly something in the presentation of the window that is messed up.
Thanks in advance for any and all advice.

The problem turned out to be in my info.plist file. I had mistakenly thought that $(PRODUCT_BUNDLE_IDENTIFIER) as a prefix for the UIApplicationShortcutItemType would be my bundle identifier. It wasn't, so replacing $(PRODUCT_BUNDLE_IDENTIFIER) with my explicit bundle identifier did the trick.
Thanks for the comments, which eventually led to my finding the answer.

Related

Problems starting ViewController from a widget click

I have a widget and want to open a particular ViewController when clicking on it. I've read all the documentation and questions on SO regarding the topic, and can't figure out why it isn't working. When clicking the widget, it always opens the default ViewController.
Here's the code for the WidgetView.
struct WidgetAdapter : View {
let entry: TimeLine.Entry
#Environment(\.widgetFamily) var family
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
SmallView(...).widgetURL(URL(string: "fcv://open_activity"))
case .systemMedium:
MediumView(...).widgetURL(URL(string: "fcv://open_activity"))
default:
LargeView(...).widgetURL(URL(string: "fcv://open_activity"))
}
}
}
Here the AppDelegate method for managing URLs.
func application(_ application: UIApplication, open
url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool{
if url.scheme == "fcv"{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "WidgetActivity") as! WidgetActivityController
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
return true
}
I also tried implementing the respective method for the SceneDelegate, I added the url scheme to the URL Types in project info, I added the LSApplicationQueriesSchemes item to the info.plist, used Link instead of .widgetURL... And it didn't work even once. I also think that the method in the AppDelegate is not being called, however, I checked for the cases were that can happen and they don't come to case.
Any help would be appreciated.
I solved the same problem by finding current rootViewController and pushing the view I need:
let rootViewController = window!.rootViewController as! UINavigationController
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)let profileViewController = mainStoryboard.instantiateViewController(withIdentifier: "MainVC") as! CombinedMainVC
rootViewController.pushViewController(profileViewController, animated: false)
It is always UINavigationController in my case, so I push a new VC, in your case you could present it.
I use the same method in AppDelegate, so there could be a problem with your if statement. Where do you set a scheme for a widget URL? Maybe you could just check URL string:
if url.absoluteString.prefix(3) == "fcv" { }

Opening ViewController In AppDelegate While Keeping Tabbar

In my Xcode project when a user taps on a notification I want to first send them to a certain item in my tabBar then I want to instantiate a view controller and send an object over to that view controller. I have code the that sends them to the tabBar I want, but I do not know how to instantiate them to the view controller while keeping the tabBar and navigation bar connected to the view controller. All the answers on this require you to change the root view controller and that makes me lose connection to my tabBar and navigation bar when the view controller is called.
A Real Life Example of this: User receives Instagram notification saying "John started following you" -> user taps on notification -> Instagram opens and shows notifications tab -> quickly send user to "John" profile and when the user presses the back button, it sends them back to the notification tab
Should know: The reason why I'm going to a certain tab first is to get that tab's navigation controller because the view controller I'm going to does not have one.
Here's my working code on sending the user to "notifications" tab (I added comments to act like the Instagram example for better understanding):
if let tabbarController = self.window!.rootViewController as? UITabBarController {
tabbarController.selectedViewController = tabbarController.viewControllers?[3] //goes to notifications tab
if type == "follow" { //someone started following current user
//send to user's profile and send the user's id so the app can find all the information of the user
}
}
First of all, you'll to insatiate a TabBarController:
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
And then insatiate all of the viewControllers of TabBarController. If your viewControllers is embedded in to the UINavigationController? If so, you'll to insatiate a Navigation Controller instead:
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
Also you should instantiate your desired ViewController too:
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
Make all of the NavigationControllers as viewControllers of TabBarController:
tabBarController.viewControllers = [first, second, third]
And check: It's about your choice.
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
Make tabBarController as a rootViewController:
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
Finally: It's your completed code:
func openViewController() {
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
tabBarController.viewControllers = [first, second, third]
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
}
If you want to present or push ViewController when the notification is tapped? Try something like that:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
openViewController()
completionHandler()
default:
break;
}
}
}
I can think of two ways to do that:
1) If that view controller is a UINavigationController you can simply push the profile from wherever you are:
if let tabNavigationController = tabbarController.viewControllers?[3] as? UINavigationController {
tabbarController.selectedViewController = tabNavigationController
let profileViewController = ProfileViewController(...)
// ... set up the profile by setting the user id or whatever you need to do ...
tabNavigationController.push(profileViewController, animated: true) // animated or not, your choice ;)
}
2) Alternatively, what I like to do is control such things directly from my view controller subclass (in this case, PostListViewController). I have this helper method in a swift file that I include in all of my projects:
extension UIViewController {
var containedViewController: UIViewController {
if let navController = self as? UINavigationController, let first = navController.viewControllers.first {
return first
}
return self
}
}
Then I would do this to push the new view controller:
if let tabViewController = tabbarController.selectedViewController {
tabbarController.selectedViewController = tabViewController
if let postListViewController = tabViewController.containedViewController as? PostListViewController {
postListViewController.goToProfile(for: user) // you need to get the user reference from somewhere first
}
}
In my last live project, I'm using the same approach like yours. So even though I doubt this method is the correct or ideal for handling a push notification from the AppDelegate (I still got a lot of stuff to learn in iOS 🙂), I'm still sharing it because it worked for me and well I believe the code is still readable and quite clean.
The key is to know the levels or stacks of your screens. The what are childViewControllers, the topMost screen, the one the is in the bottom, etc...
Then if you're now ready to push to a certain screen, you would need of course the navigationController of the current screen you're in.
For instance, this code block is from my project's AppDelegate:
func handleDeeplinkedJobId(_ jobIdInt: Int) {
// Check if user is in Auth or in Jobs
if let currentRootViewController = UIApplication.shared.keyWindow!.rootViewController,
let presentedViewController = currentRootViewController.presentedViewController {
if presentedViewController is BaseTabBarController {
if let baseTabBarController = presentedViewController as? BaseTabBarController,
let tabIndex = TabIndex(rawValue: baseTabBarController.selectedIndex) {
switch tabIndex {
case .jobsTab:
....
....
if let jobsTabNavCon = baseTabBarController.viewControllers?.first,
let firstScreen = jobsTabNavCon.childViewControllers.first,
let topMostScreen = jobsTabNavCon.childViewControllers.last {
...
...
So as you can see, I know the hierarchy of the screens, and by using this knowledge as well as some patience in checking if I'm in the right screen by using breakpoints and printobject (po), I get the correct reference. Lastly, in the code above, I have the topMostScreen reference, and I can use that screen's navigationController to push to a new screen if I want to.
Hope this helps!

How to reopen application after login

I'm an android developer. in android, when user login in application, I will re-open the MainActivity class ( controller ) to refresh some views.
in iOS applications, how to do this scenario ?
You can reopen you default/LandingViewController.
Suppose you have a View Controller with name LandingViewController
When you successfully logged in all you need is to re instantiate the LandingViewController
In AppDelegate class make a function with name
func userDidLoggedIn(){
let storyboard = UIStoryboard(name: "Main", bundle: nil)//Replace Main With your own storyboard name containing LandingViewController
let landingViewController = storyboard.instantiateViewController(withIdentifier: "LandingViewControllerIdentifier")//Pass your LandingViewController Identier that you have set in your storyboard file.
guard let subviews = self.window?.subviews else {
return
}
for view in subviews {
view.removeFromSuperview()
}
self.window?.rootViewController = landingViewController
}
Now Simply Call this Function where ever in the entire project like this In your case write below lines in the completion block of login request API.
let delegate = UIApplication.shared.delegate as! AppDelegate
delegate. userDidLoggedIn()
Once user login, you can change your rootviewcontroller like this:
var nav_VC: UIViewController?
func onSuccessfulLogin()
{
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
nav_VC = nil
if nav_VC == nil {
nav_VC = storyboard.instantiateViewController(withIdentifier: "home_nav")
}
self.window?.rootViewController = nav_VC
self.window?.makeKeyAndVisible()
}

performSegueWithIdentifier not working when being called from new instance of viewController in Swift

My mission
When app receive notification and user taps on the notification i want to redirect the user to the correct View. In my case, SingleApplicationViewController.
Current code
PushNotification.swift - A class with static functions to handle behaviors when receiving Push Notifications
The __getNavigationController returns a specific NavigationController based on a tab -and viewIndex from TabBarController.
internal static func __getNavigationController(tabIndex: Int, viewIndex: Int) -> UINavigationController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let window:UIWindow? = (UIApplication.sharedApplication().delegate?.window)!
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateViewControllerWithIdentifier("MainEntry")
window?.rootViewController = viewController
let rootViewController = appDelegate.window!.rootViewController as! UITabBarController
rootViewController.selectedIndex = tabIndex
let nav = rootViewController.viewControllers![viewIndex] as! UINavigationController
return nav
}
The applicationClicked is being called when user click on notification and that method calls on __getApplication to fetch the application from the db with the objectId received in the push notification and then instantiate a GroupTableViewController to perform segue to the SingleApplicationViewController.
(TabbarController -> Navigation Controller -> GroupTableViewController -> SingleApplicationViewController)
What is a bit strange is when I set tabIndex to 0 and viewIndex to 1. The GroupView however is on second tab (tab 1) and the view controller should be the first (0). But when I set them to the corresponding numbers, I receive nil and the application crashes.
I read that you will force the view controller to load when doing _ = groupTableViewController.view and which it actually does. When this is being called, the viewDidLoad -function is being called.
/************** APPLICATION ***************/
static func applicationClicked(objectId: String) {
__getApplication(objectId) { (application, error) in
if application != nil && error == nil {
let nav = __getNavigationController(0, viewIndex: 1)
let groupTableViewController = nav.viewControllers.first as! GroupsTableViewController
_ = groupTableViewController.view
groupTableViewController.performSegueWithIdentifier("GroupTableToApplicationToDetailApplication", sender: application!)
} else {
// Hanlde error
}
}
}
GroupTableViewController.prepareForSegue()
Here I create a new instance of the ApplicationTableViewController, which is a middle step before getting to SingleApplicationViewController
} else if segue.identifier == "GroupTableToApplicationToDetailApplication" {
let navC = segue.destinationViewController as! UINavigationController
let controller = navC.topViewController as! ApplicationViewController
controller.performSegueWithIdentifier("ApplicationsToSingleApplicationSegue", sender: sender as! Application)
}
So, what's not working?
Well, the prepareForSegue in GroupTableViewController is not being called. I use the same code structure on my TimeLineViewController, and almost the exact same code, when getting another Push Notification and it works perfectly. In that case I use tabIndex 0 and viewIndex 0 to get the proper NavigationController.
Please, any thoughts and/or suggestions is more than welcome!
There is change in following method..
internal static func __getNavigationController(tabIndex: Int) -> UINavigationController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let window:UIWindow? = (UIApplication.sharedApplication().delegate?.window)!
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateViewControllerWithIdentifier("MainEntry")
window?.rootViewController = viewController
let rootViewController = appDelegate.window!.rootViewController as! UITabBarController
rootViewController.selectedIndex = tabIndex
let nav = rootViewController.selectedViewController as! UINavigationController //This will return navigation controller..
//No need of viewIndex..
return nav
}
you have written
let nav = rootViewController.viewControllers![viewIndex] as! UINavigationController
change to rootViewController.selectedViewController give you UINavigationController.
Here you get navigavtion controller object..In your applicationClicked method nav object might be nil so it can not execute further performsegue code.
Check following method.
/************** APPLICATION ***************/
static func applicationClicked(objectId: String) {
__getApplication(objectId) { (application, error) in
if application != nil && error == nil {
let nav = __getNavigationController(0)//0 is your tab index..if you want 1 then replace it with 1
let groupTableViewController = nav.viewControllers.first as! GroupsTableViewController //Rootview controller of Nav Controller
groupTableViewController.performSegueWithIdentifier("GroupTableToApplicationToDetailApplication", sender: application!) //Perform seque from Root VC...
} else {
// Hanlde error
}
}
}

Using 3d Touch to Segue to VC - how to populate optional values?

I'm using this handleShortCutItem method inside my AppDelegate to switch on the shortcut type the user taps in 3D touch to direct to the correct VC. The problem I'm having is the VC has 2 optional values that I need to pass in otherwise obviously it breaks.
Normally this is done via a prepareForSegue method and assigning the values to the destination view controller. i.e. dvc.optionalValue = x.
(1) How can I pass in the value here in the App Delegate?
(2) the additional complexity is the VC I want to get to is embedded in a navigation controller so I need to say "present this UINavigationController and pass in these values to the VC embedded in it.
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
guard ShortCutIdentifier(fullType: shortcutItem.type) != nil else {return false}
guard let shortcutType = shortcutItem.type as String? else {return false}
switch (shortcutType) {
case ShortCutIdentifier.Second.type:
handled = true
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("NavClothingTableViewController") as! UINavigationController
self.window?.rootViewController?.presentViewController(navVC, animated: true, completion: nil)
break
default:
break
}
return handled
}
All that I needed to do was instantiate the embedded VC by calling .topViewController (to access the Navigation Controller's embedded VC), then I was able to pass values into the underlying VC:
let clothingTVC = navVC.topViewController as! ClothingTableViewController
clothingTVC.store = Store(name: "test", clothingSizes: ["":""])
clothingTVC.nameForShare = "Gary"
self.window?.rootViewController?.presentViewController(navVC, animated: true, completion: nil)

Resources