Error when re-presenting view controller - ios

I present a modal view controller in which the user can enter some information, then upon saving that information with this function...
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
DispatchQueue.main.async {
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
}
// Present reloaded view controller with new product added
let ac = UINavigationController()
let pController = ProductController()
productController = pController
ac.viewControllers = [pController]
present(ac, animated: true, completion: nil)
}
... I get an error in the viewWillAppear of ProductController (the controller that presented the modal view controller, and now am trying to get back to)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let companyToDisplay = self.navigationItem.title!
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Product")
fetchRequest.predicate = NSPredicate(format:"company.name == %#",companyToDisplay)
do {
products = try managedContext.fetch(fetchRequest)
print(products)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
The error is: unexpectedly found nil while unwrapping an optional, on the line let companyToDisplay = self.navigationItem.title!. How do I specify that the self.navigationItem.title that it's looking for (and missing) is the self.navigationItem.title of the controller that sent the modal view?
Thanks for any help, I've been trying to sort this problem out for days and can't figure it out.
EDIT: This is how I present the modal view AddProductController from my ProductController
func presentModalView() {
let nc = UINavigationController()
let addProductController = AddProductController()
nc.viewControllers = [addProductController]
self.modalTransitionStyle = UIModalTransitionStyle.coverVertical
self.modalPresentationStyle = .currentContext
self.present(nc, animated: true, completion: nil)
}
EDIT: Putting code inside dispatch block:
DispatchQueue.main.async {
self.productController?.save(name: newProductName, url: newProductUrl, image: newProductImage)
let pController = ProductController()
self.productController = pController
self.navigationController?.pushViewController(pController, animated: true)
}

The problem is that the block inside this call:
DispatchQueue.main.async {
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
}
actually executes after your handleSave() method returns. Looks like you're expecting it to execute sequentially with respect to the other code in that method.
DispathQueue.main.async adds a block to a queue of code to be executed at some point in the future -- it doesn't execute immediately.
To fix this, you need to put code inside the dispatch block which does whatever needs to happen next. This would be similar to what you have here:
// Present reloaded view controller with new product added
let ac = UINavigationController()
let pController = ProductController()
productController = pController
ac.viewControllers = [pController]
present(ac, animated: true, completion: nil)
But you probably want to clean up how/where you're creating and dismissing view controllers -- what you have here looks like it will unnecessarily pile up a bunch of view controllers.

Here is your problem, you used present(ac, animated: true, completion: nil) to get to this view, so this presentation is modal and there is no navigationController.
You have to use UINavigationController.pushViewController to present the view and this way you will get the navigationController.
Edit:
let nc = UINavigationController()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let addProductController = storyboard.instantiateViewController(withIdentifier: "addProductVC")
nc.viewControllers = [addProductController]
self.modalTransitionStyle = UIModalTransitionStyle.coverVertical
self.modalPresentationStyle = .currentContext
self.present(nc, animated: true, completion: nil)
Just don't forget to set the identifier for the addProductController in storyboard, change the storyboard and VC identifier to your ones.
Edit 2 :
let nc = UINavigationController()
let addProductController = ProductController()
addProductController.navigationItem.title = "Have a good one"
nc.viewControllers = [addProductController]
self.modalTransitionStyle = UIModalTransitionStyle.coverVertical
self.modalPresentationStyle = .currentContext
self.present(nc, animated: true, completion: nil)

Related

How to pass UIVIewController name as a parameter to particular function using swift?

In my scenario, I need to pass UIVIewController name and some more string values to particular function. I tried below code but not getting result.
Passing Parameters To Particular Function
self.accountoptionscall(vcName: UIViewController(), vcIdentifier: "profileviewcontroller", popUpVC: ProfileViewController.self)
func accountoptionscall<T: UIViewController>(vcName: UIViewController,vcIdentifier: String, popUpVC: T.self) {
let viewcontrollers = self.storyboard!.instantiateViewController(withIdentifier: vcIdentifier) as! vcName
let navController = UINavigationController(rootViewController: viewcontrollers)
self.present(navController, animated:true, completion: nil)
}
I use it in my app, I think it can help you.
extension UIViewController {
/// Load UIViewController type from UIStoryboard
class func loadFromStoryboard<T: UIViewController>() -> T {
let name = String(describing: T.self)
let storybord = UIStoryboard(name: name, bundle: nil)
if let viewController = storybord.instantiateInitialViewController() as? T {
return viewController
} else {
fatalError("Error: No initial view controller in \(name) storyboard!")
}
}
}
Here how you can use it:
func loadVC<T: UIViewController>(controller: T) {
let vc: T = T.loadFromStoryboard()
let navigationVC = UINavigationController(rootViewController: vc)
self.present(navController, animated:true, completion: nil)
// or if use in appDelegate you can do it: window?.rootViewController = navigationVC
}

iOS Swift3 check nil value for ViewController Object

let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for VC in viewControllers {
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
}
}
if bScreen == false {
let homeVC = HomeViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
I loop through navigation controller array to move to HomeViewController.above code is working fine.some times i am getting crash as “fatal error: unexpectedly found nil while unwrapping an Optional value”.I know the cause for this crash.Please help me how to check nil value for view controller object.any help will be appreciated.thanks in advance
-- Swift 3 --
for vc in (self.navigationController?.viewControllers)! {
if vc is HomeViewController {
_ = self.navigationController?.popToViewController(vc, animated: true)
}
}
Use this code. this is helpful for you.
let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for VC in viewControllers {
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
break;
}
}
if bScreen == false
{
let homeVC = HomeViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
let getCurrentVCIndex = self.navigationController?.viewControllers.indexOf({ (viewController) -> Bool in
if let _ = viewController as? HomeViewController {
return true
}
return false
})
if getCurrentVCIndex
{
let HomeVC = self.navigationController?.viewControllers[getCurrentVCIndex!] as! HomeViewController
self.navigationController?.popToViewController(HomeVC, animated: true)
}
else
{
// use push
}
or use like
if let HomeVC = self.navigationController?.viewControllers.filter({$0 is HomeViewController}).first
{
self.navigationController?.popToViewController(HomeVC!, animated: true)
}else
{
// use push
}
Never use directly ! until you are damn sure that it will not be nil. Replace your code as below. You can use if let or guard let to unwrap optionals.
if let viewControllers: [UIViewController] = self.navigationController?.viewControllers {
for VC in viewControllers {
if (VC.isKind(of: ViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
}
}
if bScreen == false
{
let homeVC = ViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
}
else {
// IF VC is nil
}
This is better to use to if let / guard for an optional value to avoid crashing.
if let viewControllers: [UIViewController] = self.navigationController.viewControllers{
for VC in viewControllers {
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
}
}
if bScreen == false
{
let homeVC = HomeViewController()
self.navigationController?.pushViewController(homeVC, animated: false)
}
}
Based on your code, In the loop, if the navigation stack contains the respective view controller will be popped to the respective page. But the thing is if the same view controller is present two times, will lead to execute the loop for the same time. This may cause crash. So Add a break after the poptoviewcontroller will avoid this issue. Please check the below code, will help you.
if (VC.isKind(of: HomeViewController.self)) {
bScreen = true
self.navigationController?.popToViewController(VC, animated: true)
break
}

How to presentViewController embedded in UITabBarController and UINavigationBarController

When using 3D Touch Shortcuts from the home screen I am trying to segue to different view controller.
The application is embedded within a UITabBarController and each tab root controller is a UINavigationController.
Here is how I attempted handling the shortcuts to load the view controller for each shortcut.
private func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) {
if let rootViewController = window?.rootViewController, let shortcutItemType = ShortcutItemType(shortcutItem: shortcutItem) {
let sb = UIStoryboard(name: "main", bundle: nil)
let helloVC = sb.instantiateViewControllerWithIdentifier("HelloVC") as! HelloViewController
let goodbyeVC = sb.instantiateViewControllerWithIdentifier("GoodbyeVC") as! GoodbyeViewController
switch shortcutItemType {
case .Hello:
rootViewController.presentViewController(helloVC, animated: true, completion: nil)
break
case .Goodbye:
rootViewController.presentViewController(goodbyeVC, animated: true, completion: nil)
break
}
}
}
With this code the shortcuts only open the application to the initial view controller and not to the helloVC and goodbyeVC controllers which are in different tabs.
I am presuming this is because the ViewControllers I am trying to load are embedded within a UINavigationController as well as embedded within the UITabBarController.
How can I presentViewController which is embedded within the UITabBarController and UINavigationController ?
UPDATE
I am unsure if the following works as I have not got a iPhone 6S with me atm. But I have changed the code to the following, hopefully this will load the selected tab index when the 3D Touch Action is performed. From there it should post a notification to the view controller to perform a segue.
private func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) {
if let rootViewController = window?.rootViewController, let shortcutItemType = ShortcutItemType(shortcutItem: shortcutItem) {
let tababarController = rootViewController as! UITabBarController
switch shortcutItemType {
case .Hello:
tababarController.selectedIndex = 1
NSNotificationCenter.defaultCenter().postNotificationName("performsegueHello", object: nil)
break
case .Goodbye:
tababarController.selectedIndex = 4
NSNotificationCenter.defaultCenter().postNotificationName("performsegueGoodbye", object: nil)
break
}
}
}
Please try the following code. I think you are not reaching the navigation controller of tab bar.you can do this by :
let insideNvc = tvc?.selectedViewController as? UINavigationController
Now, here is your navigation controller you can present or push anything on it.
private func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) {
let nvc = self.window?.rootViewController as? UINavigationController
let tvc = nvc?.topViewController as? TabBarController
let insideNvc = tvc?.selectedViewController as? UINavigationController
if let shortcutItemType = ShortcutItemType(shortcutItem: shortcutItem) {
let sb = UIStoryboard(name: "main", bundle: nil)
let helloVC = sb.instantiateViewControllerWithIdentifier("HelloVC") as! HelloViewController
let goodbyeVC = sb.instantiateViewControllerWithIdentifier("GoodbyeVC") as! GoodbyeViewController
switch shortcutItemType {
case .Hello:
insideNvc?.presentViewController(helloVC, animated: true, completion: nil)
break
case .Goodbye:
insideNvc?.presentViewController(goodbyeVC, animated: true, completion: nil)
break
}
}
}

Navigation and tab bar missing when presenting view controller

I am implementing home screen shortcuts using 3D Touch, and it's working well, however the way I currently have it means that when the shortcut takes the user to a specific view controller, the tab bar and navigation bar is missing.
This is my code:
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) {
let rootViewController = window!.rootViewController
switch shortcutType {
case .Favourites:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootController = storyboard.instantiateViewControllerWithIdentifier("favourites") as! FavouritesTableViewController
rootController.parkPassed = DataManager.sharedInstance.getParkByName(NSUserDefaults.standardUserDefaults().stringForKey("currentPark")!)
self.window?.rootViewController = rootController
self.window?.makeKeyAndVisible()
handled = true
}
return handled
}
Can anyone suggest what I need to change in the code?
This is the starboard layout (FavouritesTableViewController is indicated):
EDIT:
Here is my updated code:
#available(iOS 9.0, *)
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) {
switch shortcutType {
case .Favourites:
print("favourites")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootController = storyboard.instantiateViewControllerWithIdentifier("favourites") as! FavouritesViewController
rootController.parkPassed = DataManager.sharedInstance.getParkByName(NSUserDefaults.standardUserDefaults().stringForKey("currentPark")!)
let root = UIApplication.sharedApplication().delegate as! AppDelegate
if let navCont = root.window?.rootViewController?.navigationController {
navCont.presentViewController(rootController, animated: true, completion: nil)
} else {
root.window?.rootViewController?.presentViewController(rootController, animated: true, completion: nil)
}
root.window?.makeKeyAndVisible()
handled = true
}
}
return handled
}
Try this:
Get the delegate from app delegate:
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
GEt the controller you would like to to present from storyboard:
Controller *cont=//get the reference from storyboard either using storyboard ID
and then make your rootview present the other controller:
[appDelegate.window.rootViewController presentViewController:cont animated:YES completion:^{
DLog(#"presented your view ON TOP of the tab bar controller");
}];
Swift:
var appDelegate: AppDelegate = UIApplication.sharedApplication().delegate()
var cont: Controller = //get the reference form storyboard
appDelegate.window.rootViewController.presentViewController(cont, animated: true, completion: { DLog("presented your view ON TOP of the tab bar controller")
})
you can move the presenting stuff on main thread,if you like!!!!
So I got the solution I hope that will work for you.
First you have to set your Storyboard instance.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
after that you need to set where you want to start the navigation.
let mynVC = storyboard.instantiateViewControllerWithIdentifier("root") as! UINavigationController
right now you can set the viewcontroller that you want to appear
let playVC = storyboard.instantiateViewControllerWithIdentifier("playVC")
So and now you can start the workflow but note that you have to do this in a completion like this
self.window?.rootViewController?.presentViewController(rootVC, animated: true, completion: { () -> Void in
rootVC.pushViewController(playVC, animated: true)
})
So your rootViewController will present you "rootVC" and after that your playVC.
I hope It will help you guys:)
The problem is, you are setting the view controller(named:rootController) to be the window?.rootViewController, other than presenting the rootController by the window?.rootViewController. Just change
self.window?.rootViewController = rootController
to
self.window?.rootViewController.presentViewController(rootController, animated: true, completion: nil)
if you use it like this you will be replaced rootview to this viewcontroller. So this will be a new solo page and its normal to dont have navigationController or tabbarController. Because you have only this new page in view hierarchy.
If you want to present this from rootview, you may try
let root=UIApplication.sharedApplication().delegate as! AppDelegate
if let navCont=root.window?.rootViewController?.navigationController
{
navCont.presentViewController(viewControllerToPresent: UIViewController, animated: true, completion: nil)
}
else{
root.window?.rootViewController?.presentViewController(viewControllerToPresent: UIViewController, animated: true, completion: nil)
}

Swift – self.navigationController becomes nil after transition

I'm experiencing a very strange error in my app, between two views, self.navigationController is becoming nil
I have a few view controllers: MainViewController, SecondViewController PastSessionsViewController, JournalViewController. I use JournalViewController for two purposes, to save a new entry into CoreData or to edit an older one. The details aren't really relevant to this error.
The error occurs when I try to pop JournalViewController off the stack and return to MainViewController but only when JournalViewController is in "edit" mode, not when it's in "save a new entry mode"
Any idea 1) why this is happening and 2) how to correctly address it so I can return to PastSessionsViewController when coming back from JournalViewController in edit mode?
Here's some code to make things concrete.
In AppDelegate.swift (inside of didFinishLaunchingWithOptions):
navController = UINavigationController()
navController!.navigationBarHidden = true
var viewController = MainViewController()
navController!.pushViewController(viewController, animated: false)
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = navController
window?.makeKeyAndVisible()
In MainViewController:
func goToPastSessions(sender: UIButton) {
let pastSessionsVC = PastSessionsViewController()
self.navigationController?.pushViewController(pastSessionsVC, animated: true)
}
func goToWriteJournalEntry(sender: UIButton) {
let journalVC = JournalViewController(label: "Record your thoughts")
self.navigationController?.pushViewController(journalVC, animated: true)
}
In PastSessionsViewController:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let editJournalVC = JournalViewController(label: "Edit your thoughts")
let indexPath = tableView.indexPathForSelectedRow()
let location = indexPath?.row
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! EntryCell
if let objects = pastSessionsDataSource.coreDataReturn {
if let location = location {
editJournalVC.journalEntryCoreDataLocation = location
editJournalVC.editEntry = true
editJournalVC.journalEntryToEdit = objects[location].journalEntry
}
}
self.navigationController?.presentViewController(editJournalVC, animated: true, completion: nil)
}
And finally, in JournalViewController:
func doneJournalEntry(sender: UIButton) {
journalEntryTextArea?.resignFirstResponder()
var entry = journalEntryTextArea?.text
if let entry = entry {
let appDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
let managedObjectContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Session")
var error: NSError?
// new entry
if journalEntryText == "Record your thoughts" {
let result = managedObjectContext.executeFetchRequest(request, error: &error)
if let objects = result as? [Session] {
if let lastTime = objects.last {
lastTime.journalEntry = entry
}
}
} else {
// existing entry
let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sortDescriptor]
let result = managedObjectContext.executeFetchRequest(request, error: &error)
if let objects = result as? [Session] {
var location = journalEntryCoreDataLocation
var object = objects[location!]
object.journalEntry = entry
}
}
if !managedObjectContext.save(&error) {
println("save failed: \(error?.localizedDescription)")
}
}
// in "edit" mode, self.navigationController is `nil` and this fails
// in "record a new entry" mode, it's not nil and works fine
self.navigationController?.popViewControllerAnimated(true)
}
func cancelEntryOrEditAndReturn(sender: UIButton) {
self.journalEntryTextArea?.resignFirstResponder()
// in "edit" mode, self.navigationController is `nil` and this fails
// in "record a new entry" mode, it's not nil and works fine
self.navigationController?.popViewControllerAnimated(true)
}
Thanks for taking a look
You should push editJournalVC instead of presenting if you want it to be in same navigation stack. Because when you present controller its no longer in same navigation stack. Also if you are presenting it, you should dismiss it not pop. So if you want to present controller you should use
self.dismissViewControllerAnimated(true, completion: nil)
instead
self.navigationController?.popViewControllerAnimated(true)

Resources