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

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)

Related

passing data from 2 view controllers without segue

I have a mainviewcontroller and a popup view controller which opens without a segue.
the popup viewcontroller recive data from Firebase, and then i need to append this data to an array in the mainviewcontroller.
How can i do that?
(i tried to create a property of the popupviewcontroller in the mainviewcontroller, but in crashes the app)
this is the code that opens the popup:
#IBAction func showPopUp(_ sender: Any) {
let popOverVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbPopUp") as! PopUpViewController
self.addChild(popOverVC)
popOverVC.view.frame = self.view.frame
self.view.addSubview(popOverVC.view)
popOverVC.didMove(toParent: self)
You need to connect the classes so that the popup class knows what to do with the data once it has been received. Here's a sample structure that works in a playground that you should be able to apply to your real classes.
class MainClass {
func showPopUp() {
let popOverVC = PopUpClass()
popOverVC.update = addToArray
popOverVC.receivedData()
}
func addToArray() {
print("Adding")
}
}
class PopUpClass {
var update: (()->())?
func receivedData() {
if let updateFunction = update {
updateFunction()
}
}
}
let main = MainClass()
main.showPopUp()
Or you could create a global variable so it can be accessed anywhere. ... It is better to pass data but I just thought it would be easier in this instance, so the variable can be accessed anywhere in your entire project.
if it is just a notification, you can show it in an alert, but if you don't want to use an alert my offer to present another view controller is not like this , try my code :
//if you have navigation controller ->
let vc = self.storyboard?.instantiateViewController(
withIdentifier: "storyboadrID") as! yourViewController
self.navigationController?.pushViewController(vc, animated: true)
//if you don't use navigation controller ->
let VC1 = self.storyboard!.instantiateViewController(withIdentifier: "storyboadrID") as! yourViewController
self.present(VC1, animated:true, completion: nil)
also if you want to pass any data or parameter to destination view controller you can put it like this before presenting your view controller :
VC1.textView.text = "test"

Swift Multiple Storyboard - How to access specific one

I had split a couple profiles into multiple storyboards to try to help with my freeze time because I heard to many storyboards can be the cause of why Xcode would just "freeze" for up to 10 mins sometimes causing a crash in my flow of work. So, I need to access each specific storyboard, yet don't know how. This current code accesses the "Main.Storyboard" file yet, I need to call other storyboard files. Here I am checking if a specific profile is signed in and pushing them to the profile. I had all the profiles in one storyboard but now I separated them. Just need to know how to push to other storyboards. Thanks
func checkIfBusinessLoggedIn() {
Auth.auth().addStateDidChangeListener({ (auth, user) in
if (user != nil) {
Database.database().reference().child("Businesses").child((user?.uid)!).observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() {
print("Business is Signed In")
let vc = self.storyboard?.instantiateViewController(withIdentifier: "Business Profile")
self.present(vc!, animated: true, completion: nil)
}
})
}
})
}
let storyboard = UIStoryboard(name: "your_storyboard_name", bundle: nil)
vc = storyboard.instantiateViewController(withIdentifier: "your_view_controller_name")
//to push that controller on the stack
self.navigationController?.pushViewController(vc, animated: true)
If your storyboard file is "Business.storyboard", "your_storyboard_name" name should be "Business".
If you want to access other storyboards you need to give that name.
self.storyboard will always point to your default storyboard. Instead do this :
let VC = UIStoryboard(name: "VCStoryBoardName", bundle: nil).instantiateViewController(withIdentifier: "InstructionScreenController") as! VC
Use below code for multipe storybord and create ENUM for storyboard name that is easy to specify storyboard name.
//MARK:- Enum_StoryBoard
enum enumStoryBoard:String {
case main = "Main"
case home = "HomeSB"
}
let storyBoard = UIStoryboard.init(name: enumStoryBoard. home.rawValue, bundle: nil)
let objLocationSearch = storyBoard.instantiateViewController(withIdentifier: "LocationSearch") as? LocationSearch
self.navigationController?.pushViewController(objLocationSearch!, animated: true)
Using enum and generic method to return the required object.
enum AppStoryboard : String {
case First, Second
var instance : UIStoryboard {
return UIStoryboard(name: self.rawValue, bundle: Bundle.main)
}
func instantiateVC<T : UIViewController>(viewControllerClass : T.Type) throws -> T {
let storyboardID = (viewControllerClass as UIViewController.Type).storyboardID
guard let viewObj = instance.instantiateViewController(withIdentifier: storyboardID) as? T else {
throw ExcpectedError.intantiationErro(msg:"ViewController with identifier \(storyboardID)")
}
return viewObj
}
}
use this code to instantiate your viewController
let second = try! AppStoryboard.Second.viewController(viewControllerClass: SecondViewController.self) self.present(second, animated: true, completion: nil)

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!

"Attempt to present while already presenting" still appearing after checks?

let appDelegate = UIKit.UIApplication.shared.delegate!
if let tabBarController = appDelegate.window??.rootViewController as? UITabBarController {
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let signInVC = storyboard.instantiateViewController(withIdentifier: "SignInVC") as! SignInVC
guard !signInVC.isBeingPresented else {
log.warning("Attempt to present sign in sheet when it is already showing")
return
}
signInVC.modalPresentationStyle = UIModalPresentationStyle.formSheet
tabBarController.present(signInVC, animated: true, completion: nil)
}
This code can be called multiple times despite signInVC being presented. I already added this check:
guard !signInVC.isBeingPresented else {
log.warning("Attempt to present sign in sheet when it is already showing")
return
}
but it doesn't seem to prevent this error:
Warning: Attempt to present <App.SignInVC: 0x101f2f280> on <UITabBarController: 0x101e05880> which is already presenting <App.SignInVC: 0x101f4e4c0>
Your guard isn't a valid check. The isBeingPresented is being called on a brand new view controller instance that hasn't yet been presented. So isBeingPresented will always be false. Besides that, that property can only be used from within a view controller's view[Will|Did]Appear method.
What you want to check is to see if the tabBarController has already presented another view controller or not.
And lastly, only create and setup the sign-in view controller if it should be presented.
let appDelegate = UIKit.UIApplication.shared.delegate!
if let tabBarController = appDelegate.window?.rootViewController as? UITabBarController {
if tabBarController.presentedViewController == nil {
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let signInVC = storyboard.instantiateViewController(withIdentifier: "SignInVC") as! SignInVC
signInVC.modalPresentationStyle = UIModalPresentationStyle.formSheet
tabBarController.present(signInVC, animated: true, completion: nil)
}
}

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

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.

Resources