Programmatic UINavigationController and segue away - ios

In my code I need to go through a navigation hierarchy, which is why I have a UIViewController in a UINavigationController.
If the user tabs on a cell and there is a next level, I create the same UIViewController again and push it on the UINavigationController. This works fine.
But when I reach the end of the hierarchy and try to performSegueWithIdentifier, to go to a different controller to see the details, the app crashes and says
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: 'Receiver (<myproject.MenuController: 0x7fb2e0ea9ef0>)
has no segue with identifier 'showDetail''
However I checked InterfaceBuilder, all fine.
Interestingly, If I don't push a next level of navigation and perform the segue to the new controller directly, all works well.
I also tried to push the new controller, like I'm doing with the navigation, but then it tries to access the new controllers delegates which strangely are nil and crashes.
Somebody know how to do this?
Complete code (inside MenuContoller)
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
//self.performSegueWithIdentifier("showDetail", sender: self)
let clickedCell = self.tableViewItems[indexPath.row]
print("Selected \(clickedCell.itemID)")
// Check if subview has children, then we push a deeper level on the navigation controller
if (menuItems.filter{$0.parentID == clickedCell.itemID}.count > 0) {
let subNavigationController = MenuController()
// Set the currentNavigationItemID for the new view controller
subNavigationController.currentNavigationItemID = clickedCell.itemID
subNavigationController.title = clickedCell.itemDisplayName
self.navigationController?.pushViewController(subNavigationController, animated: true)
} else {
// Load new Controller to show details
self.performSegueWithIdentifier("showDetail", sender: self)
// let subNavigationController = ProductListViewController()
// subNavigationController.currentCategoryID = clickedCell.itemID
// self.navigationController?.pushViewController(subNavigationController, animated: true)
}

The problem is that your MenuController has not been created from the Storyboard, so it has no segues attached to it.
You need to ask the Storyboard to create the MenuController, so instead of:
let subNavigationController = MenuController()
you need:
let subNavigationController = self.storyboard?.instantiateViewControllerWithIdentifier("menuControllerID")
For this to work, the storyboard ID must be set in the Identity Inspector to "menuControllerID" for MenuController.

Related

How to properly use didSelectRowAt in splitViewController on compact device in swift?

I'm using a splitViewController to display a master view and a detail view.
When I tap on a row, the detail view updates correctly.
Then, when I'm in portrait view, I collapse the splitview detail view, so that that master list items are shown as follows:
And when I tap on a row, I correctly move to the detail view, as shown:
The problem I'm having is that if I rotate the device in the detail view shown above, while I'm in the detail view, the rotation correctly goes back to the splitView, however, now when I select a row, the delegate method does not update the detail view. It only seems to work if I start out in the splitView and stay in that view, or if I start out in the collapsed view and stay in that. If I rotate, then the delegate method does not seem to work.
I found a prior post, which shows how to use the delegate method to update the detail view using objective C code, using the didSelectRow function. I tried to duplicate this code with the following swift code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let navigationVC = UINavigationController()
var detailVC = TestsDetailAdvertVC()
if let tests = controller.fetchedObjects, tests.count > 0 {
//if there is, keep track of the test which is selected
selectedTest = tests[indexPath.row]
if let isCollapsed = splitViewController?.isCollapsed {
if isCollapsed {
//solves problem of navigating to the detail view in compact view
// on the iPhone (compact) the split view controller is collapsed
// therefore we need to create the navigation controller and detail controller
detailVC = self.storyboard!.instantiateViewController(withIdentifier: "detailVC") as! TestsDetailAdvertVC
navigationVC.setViewControllers([detailVC], animated: false)
self.splitViewController?.showDetailViewController(detailVC, sender: self)
detailVC.testToEdit = selectedTest
} else {
// if the split view controller shows the detail view already there is no need to create the controllers
// so we just pass the correct test using the delegate
// if the test variable is set, then it calls the showDetail function
delegate?.testToEdit = selectedTest
}
}
}
}
I think that somehow when the one or the other method is used to update the detail view it works, but then when it switches back and forth, it stops working. I wonder if anyone has solved this issue using swift code who could point me to an example.
Note: After some additional searching, I realized that there are a few of delegate methods for the splitViewController, including:
func primaryViewControllerForExpandingSplitViewController:
and
func primaryViewControllerForCollapsingSplitViewController:
and
splitViewController:separateSecondaryViewControllerFromPrimaryViewController:
I've been fiddling around with these methods, but so far haven't been able to get them to work, and I haven't found any posts that show examples of how they are used.
Thanks.
I figured out how to make the detail view update properly, using an answer from a prior post at:
In UISplitViewController, can't make showDetailViewController:sender: push onto detail navigationController
my code to solve the problem is updated using swift code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var detail = UINavigationController()
var testVC = TestsDetailAdvertVC()
if let tests = controller.fetchedObjects, tests.count > 0 {
//if there is, keep track of the test which is selected
selectedTest = tests[indexPath.row]
if let isCollapsed = splitViewController?.isCollapsed {
if isCollapsed {
//in collapsed view, the correct detail view controller is not
//yet substantiated, so we need to substantiate it
testVC = self.storyboard?.instantiateViewController(withIdentifier: "detailVC") as! TestsDetailAdvertVC
detail.setViewControllers([testVC], animated: true)
testVC.testToEdit = selectedTest
} else {
//in expanded view, the correct view controller needs
//to be identified, using the appropriate view controller for
//the splitview controller
let vc = self.splitViewController?.viewControllers[1]
//which is a navigation controller
if vc is UINavigationController {
detail = vc as! UINavigationController
//which we then use to identify the correct detail view
testVC = detail.viewControllers.first as! TestsDetailAdvertVC
testVC.testToEdit = selectedTest
}
}
}
}
self.splitViewController?.showDetailViewController(detail, sender: self)
}
The key solution is that on the collapsed splitviewcontroller, the detail view has to be instantiated form the storyboard. However, on the expanded splitviewcontroller, the detail view has to come from the expanded navigation controller. Then when I rotate the correct detail view controller updates correctly.

How to test a segue attached in storyboard

I want to attach a Show segue to a property cell of a table view in storyboard. Now I want to test this binding is exists. i.e. I want to have a test that would fall if I delete the segue in the Interface Builder or changed the segue identifier.
From a user perspective, user taps on a cell of the table view and then this segue should be performed.
From testing perspective, I can swizzle the prepare(for:sender:) method the verify the performing of a segue, but I don't know how to trigger a "tap" programmatically.
I've tried tableView.selectRow(at:animated:scrollPosition), cell.setSelected(_:animated:) and those both didn't work.
How to programmatically trigger the segue added via InterfaceBuilder ?
Or is there any other way to test this segue binding?
Update for further clarification
I know how to trigger a segue programmatically -- thus that is not what being asked here.
The segue triggering work is done behind the scene by storyboard and there is no segue triggering code in the production code (there is only a overriden prepare(for:sender:)). And the app work as intended. The problem here is I need a test to guarantee this behind-the-scene triggering always exists, that is: if someday I got drunk and mis-edited the storyboard to connect the original segue to somewhere irrelevant, there would be a test to go red and kick me in the ass.
I was able to test a segue that is wired up to the view controller instead of the table view cell.
In the app, I perform the segue when the cell is tapped. This is roughly equivalent to what you get if you wire up the segue to the table view cell.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showNextView", sender: nil)
}
This allows the segue to be tested as follows:
func testSegue() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
// create the view controller that has the segue to be tested
let viewController = appDelegate.window?.rootViewController?.storyboard?.instantiateViewController(withIdentifier: "TableViewController") as! TableViewController
// assuming the view controller does a Show segue, put it in a navigation controller
let navigationController = UINavigationController(rootViewController: viewController)
// this is needed or test fails!
navigationController.view.layoutIfNeeded()
// replace the root view controller with the navigation controller
appDelegate.window!.rootViewController = navigationController
// finally, select the row! this fires the segue
viewController.tableView(viewController.tableView, didSelectRowAt: IndexPath(row: 0, section: 0))
// assert something about the result of the segue
XCTAssertTrue(navigationController.visibleViewController is SeguedViewController)
}
Just use this func :
[self performSegueWithIdentifier:#"YOURIDENTIFIER" sender:self];
It may help you test your segue.

Nil error assigning Delegate to PopoverPresentationController

I have a problem assigning a Delegate to a PopoverPresentationController using Swift 2.2 in Xcode 7.3
Reason of using a Delegate is I'm trying to trigger a function when pressing outside of a Popover view (class RedeemViewController) to go back to the main menu (class MenuViewController). This happens without a button. The function exists in MenuViewController, but it's not relevant what it does now, so I just included the Delegation part where the error occurs.
class MenuViewController: UIViewController, UIPopoverPresentationControllerDelegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (sender as! UIButton == btnRedeem) {
let rvc = segue.destinationViewController as! RedeemViewController
let nav = UINavigationController(rootViewController: rvc)
let popover = nav.popoverPresentationController
popover!.delegate = self
}
}
}
Pushing the button btnRedeem, connected to the Segue towards the Popover view, RedeemViewController, leads to
fatal error: unexpectedly found nil while unwrapping an Optional value
In Xcode, the debugger points to
popover!.delegate = self
The Segue exists and is named in the Storyboard. There are no warnings or obsolete references when right-clicking on the view.
Replacing the code with
popover?.delegate = self
leads to a more generic error where the debugger stops in AppDelegate:
libc++abi.dylib: terminating with uncaught exception of type NSException
I looked here but none of the possible causes applies. I have a generic AppDelegate.swift as described here.
Any help is appreciated! I mostly found examples with buttons but do not want to use a button to exit from the Popover since it's not needed.
Additional info on the Delegation: the function I need running on the background is
popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController)
to check when the Popover view (RedeemViewController) is exited and the function I want to trigger within it is one that reveals a new button.
Thank you for reading and maybe you can help me out!
Cheers,
Floris
let nav = UINavigationController(rootViewController: roc)
Here you are creating a brand new navigation controller with the root VC as the segue's destination controller. This isn't going to work - the destination controller is going to be set up with whatever container it needs, you don't create new view controllers in prepareForSegue.
You want to look at the popoverPresentationController of rvc, not of some navigation controller that isn't going to be added to the screen:
if (sender as! UIButton == btnRedeem) {
let rvc = segue.destinationViewController as! RedeemViewController
let popover = rvc.popoverPresentationController
popover!.delegate = self
}

Segue a ViewController over the top of SWRevealViewController side menu

I have implemented the SWRevealViewController into my app to give my app a nice side menu. My main (home) screen for the app is TableViewController.swift - this is also the only place you can access the side menu from. The side menu all works perfectly. Here is an image to show what it looks like:
The 5 options you can see in the menu, are created in a tableView I want them to all link to other view controllers. I have currently achieved this using the method they showed in the tutorial for this side menu, which is to create a segue of reveal view controller push controller. This all works.
The problem
When you press one of the options, It reveals the new view controller as the sw_front, therefore, I have lost access to the main (home) screen. For example, the VC linked to the first option 'Films I want to see' is just a plain green VC. The image below shows what happens after I have pressed that option and then tried to go back:
So you can see, the VC that appears at the front is no longer my main (home) screen.
I'd like to know is there away I can set this up so when you press an option in the side menu, it opens up the new VC over the top of the side menu, so when you close down that VC, it still shows the side menu open with the Main (home) screen in front.
I hope that makes sense. Please ask if you need to see any of my code.
I managed to find a solution!
I first created my 5 new view controllers (one for each option in the side menu)
I then linked each cell to it's view controller by creating a 'Show' segue. So my storyboard now looked like this:
I created a segue identifier for each segue.
Then in my sideMenu.swift file, I added the following:
Created variables to store the identifiers for the segues.
// Mark:- Sets the side menu segue identifiers
let smOption_One = "toFilmsIWantToSee"
let smOption_Two = "toSearchForAnyFilm"
let smOption_Three = "toMeetTheTeam"
let smOption_Four = "toContactUs"
let smOption_Five = "toTermsOfUse"
I then added the didSelectRowAtIndexPath method, and added performSegueWithIdentifier for each of my segues.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier(smOption_One, sender: indexPath)
self.performSegueWithIdentifier(smOption_Two, sender: indexPath)
self.performSegueWithIdentifier(smOption_Three, sender: indexPath)
self.performSegueWithIdentifier(smOption_Four, sender: indexPath)
self.performSegueWithIdentifier(smOption_Five, sender: indexPath)
}
And finally, added the prepareForSegue method. Inside this method I did a check for each identifier, and then segued to the destinationViewController.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == smOption_One {
segue.destinationViewController as! FilmsIWantToSeeViewController
} else if segue.identifier == smOption_Two {
segue.destinationViewController as! SearchForAnyFilmViewController
} else if segue.identifier == smOption_Three {
segue.destinationViewController as! MeetTheTeamViewController
} else if segue.identifier == smOption_Four {
segue.destinationViewController as! ContactUsViewController
} else if segue.identifier == smOption_Five {
segue.destinationViewController as! TermsOfUseViewController
}
}
This perfectly creates a segue to the correct view controller, depending on which option I press, and presents it over the top of the side menu, so when I dismiss that view controller, it still shows me the side menu, and therefore allows me to get back to my main screen.
Not sure if there is an easier way of doing this, but it certainly works how I needed it to. Written in Xcode 7.2 with Swift 2.

Performing Segue from - TableViewCell.xib to UIView - and - TableView.xib to UIViewController -

How can I perform segue from one .xib (TableCell.xib) to another .xib(UIViewController) on click on TableCell?
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print(results?[indexPath.row]) // prints successfully
...
// apply below chunks here...
}
First, I tried setting up a manual segue in Storyboard. Note that, TableView & TableViewCell are external xib but parent of TableView is in Storyboard. So, I tried control-dragging from parent-ViewController to the detailViewController (and identifier: showDetails)..However, doesn't work.
...
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.performSegueWithIdentifier("showDetails", sender: AnyObject?())
})
...
Obviously, I received this error (because TableView is in .xib whereas parent and detailsVC are in Storyboard and I ctrl+dragged from parentVC to detailsVC but TableView1 apparently doesn't recognise the segue in Main Storyboard):
TableView1: 0x7f97626a3280>) has no segue with identifier 'showDetails''
Then I tried creating a ViewController and tried adding var parentNavigationController : UINavigationController! at the top and referencing below chunk inside didSelectRowAtIndex
...
let newVC : UIViewController = CellDetailsVC()
newVC.title = "DetailsView"
print(newVC)
parentNavigationController!.pushViewController(newVC, animated: true)
However, I am receiving an error:
DetailsVC: 0x7ff7794ae550>
fatal error: unexpectedly found nil while unwrapping an Optional value
Finally, I also tried below chunk inside didSelectRowAtIndex, and got something close (as I can use dismissView to < Back); but not exactly what I want. Its problem is that the segue animation looks like the page is coming from bottom.
...
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("VideoDetailsVC") as UIViewController
self.presentViewController(vc, animated: true, completion: nil)
}
I am also receiving a warning (but not crash) for some reason I couldn't figure out:
Presenting view controllers on detached view controllers is discouraged
Which approach is the right one? Or is there a better way to adapt to achieve what I am trying to?
Firstly, what is the right way to perform segue from didSelectRowAtIndex (in TableView.xib class) to a new ViewController (either Storyboard or xib)?
Secondly, assume that there is an image thumbnail in the TableViewCell.xib. What is right way of performing segue to a new UIView on click on that view? (like full-screen image / dismiss-able)
Your second approach looks to be right one but of course you are doing something wrong there. You need to drag and drop Segue from Parent ViewController to Detail ViewController and give it a Segue identifier in Storyboard (check attached Image below). IN didSelectRowAtIndexPath you need to do below code:
self.performSegueWithIdentifier("showDetailViewController", sender: nil)
As I can see you already tried that but it looks like you missed some step either setting Segue Identifier or giving wrong identifier in performSegueWithIdentifier method. Also your Parent ViewController should be embedded in UINavigationController in case you are missing that thing :)
Also keep in mind you should perform navigation (here its Segue) from one ViewController to another ViewController at same level not from one View to another ViewController.

Resources