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.
Related
I want to have tabBar in all my ViewControllers. I have implemented the SWRevealViewController, which have two view controllers linked one is TabBarController and another is TableViewController I wants to have the same tabBar in all my ViewControllers that Segues from TableViewController.
I want to have tabBar in all my ViewControllers. I have implemented
the SWRevealViewController, which have two view controllers linked one
is TabBarController and another is TableViewController I wants to have
the same tabBar in all my ViewControllers that Segues from
TableViewController.
You can do some tweak like this, create the custom delegate and set delegate of TableViewController and TabBarController to SWRevealViewController. Now first time lets say you open the TableViewController and when tap on any cell then just invoke the delegate method which should execute inside SWRevealViewController class and then perform segue inside that class which should open the TabBarController and when click back button of TabBarController again invoke the delegate method which should execute inside SWRevealViewController class and perform segue to open the TableViewController
This shouldn't be too difficult. Your RevealViewController (the initial controller of your storyboard) already seems to be a subclass of SWRevealViewController. This is good, but it's not essentially needed for this scenario. What you do need is a UIViewController implementation for your side menu. The Storyboard alone won't do.
Also, I wouldn't use segues here to switch between the tabs because a segue has a destination view controller. So every time you perform a segue, you would create a new instance of the destination view controller. I would rather implement a UIViewController that handles the cell selection of the UITableView
Now let's say you create a UIViewController or UITableViewController as your menu (actually the RearViewController). Make sure to assign this class to your UITableViewController in your storyboard.
class MenuViewController: UITableViewController {
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let tabIndex = indexPath.row
let tabbarVC = (revealViewController().frontViewController as! UITabBarController)
tabbarVC.selectedIndex = tabIndex // this assumes the menu items are in the same order as the tabs. if not you need to adjust this
revealViewController().revealToggle(animated: true) // close the side menu after switching tabs
}
}
this should be all you need
I have a Navigation Controller with a UITableViewController.
When a user selects a cell on the TableViewController, it pushes to a new View Controller with a Table View inside. The user then selects a cell and the data gets passed back via an unwind segue.
The problem is I get this error when using a search bar before selecting the cell. Here is what the console says:
popToViewController:transition: called on <UINavigationController 0x7fc8ab856e00> while an existing transition or presentation is occurring; the navigation stack will not be updated.
Code from View Controller I'm unwinding from:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let cell = tableView.cellForRow(at: indexPath) as! MonsterSpriteCell
let monster = monsters[indexPath.row]
selectedMonster = monster
if isTeamBuilding {
// **ERROR OCCURS HERE**
performSegue(withIdentifier: "saveToTeamBuilderTableVC", sender: cell)
} else {
performSegue(withIdentifier: "showMonsterDetail", sender: self)
}
}
Here is the link the the project. The View Controller I'm unwinding from is Browse View Controller. The View Controller I'm unwinding to is TeamBuilderViewController
https://github.com/emanleet/monsterpedia
EDIT: I think it might be relevant to note that the segue unwinds to TeamBuilderTableViewController, which is a View Controller that is inside a container as a part of another view controller. Does anyone know if this might be why my unwind segue isn't working?
Two step thing, first dismiss the search controller presented view controller, then, do your thing.
yourSearchController.dismiss(animated: true, completion: {
self.performSegue(withIdentifier: "yourUnwindSegue", sender: self)
})
The SearchController is presenting an empty view controller
If you print the presentedViewController in didSelectRow.. when the search is active you will see a view controller.
that means you’re trying to perform segue from under the current presentation. You should dismiss the SearchController before performing any segues or presentations.
Also in this case you don’t need a SearchController since you’re only using the SearchBar for filtering.
Instead, put a SearchBar in the tableViewHeaderView and use its delegate to do the filtering and instead of checking if isActive to access the complete list vs. the filtered results, just put the whole array in the filter when the text is cleared and always access the filtered results.
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.
This seems to be a common problem, but none of the many solutions I've tried have seemed to work (or I'm not executing them properly).
I've got an image on FirstImageVC, I push a button to bring up a new view collection view controller with some custom images, the user selects one, and I want to send that image back to the FirstImageVC to overlay the original image, sort of like a sticker.
I just can't get it to execute a segue of any kind on selecting an image. Here's what I'm sort of working with in the second view controller. And it seems I may need to be adding something to the original VC, too, no?
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
prepareForSegue("backToFirstSegue", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "backToFirstSegue"{
let vc = (segue.destinationViewController as! FirstImageVC)
vc.Delegate = self //Include this line
vc.chosenGhostPhoto = bgGhostImage?.image
}
}
EDIT 1: Here's what I did to get the unwind to work, though it's not carrying back the image selected from the collection view, which needs to be called chosenGhostPhoto.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
performSegueWithIdentifier("unwindToFirstSegue", sender: self)
}
This is how you can do this:
Step 1: Add a delegate variable on your second view controller. Please name it delegate and not Delegate - owing to naming convention.
Step 2: Define a protocol and add functions that you want your second view controller delegate to perform. Say func selectedImage(image : UIImage)
Step 3: While pushing second view from first view controller, set your first view controller as delegate of second view controller. Something like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "secondSegue"{
let vc = (segue.destinationViewController as! SecondImageVC)
vc.delegate = self
}
}
Step 4: In your second view controller once image is selected call delegate of second view controller. Below function needs to be triggered on tap on the image on second view controller.
fun imageSelected {
self.delegate.selectedImage(bgGhostImage?.image)
}
Step 5: Implement selectedImage in your first view controller and use the passed image.
An unwind segue (sometimes called exit segue) can be used to navigate back through push, modal or popover segues (as if you popped the navigation item from the navigation bar, closed the popover or dismissed the modally presented view controller). On top of that you can actually unwind through not only one but a series of push/modal/popover segues, e.g. "go back" multiple steps in your navigation hierarchy with a single unwind action.
When you perform an unwind segue, you need to specify an action, which is an action method of the view controller you want to unwind.
For your reference this is quit similar question and might help in understanding unwind segue
I am using didDeselectRowAtIndexPath to navigate through different Storyboards like:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let story = ["News","Video","Twitter","Request Info","More"]
let vc = self.storyboard?.instantiateViewControllerWithIdentifier(story[indexPath.row]) as NewsFeedTableViewController
self.presentViewController(vc, animated: true, completion: nil)
}
However, the Navigation Bar does not show up when I use this function. But, when I use segue, the Navigation Bar shows up.
Now the Problem is:
Cells in a tableView cannot segue more than 1 storyboard
Need Navigation Bar to Scroll in the App (which seems to need Segue)
Any solutions through this?
You are using presentViewController, that's why you are not getting the NavigationBar, it presents the view modally. Instead of that pushViewController like:
self.navigationController?.pushViewController(vc, animated: true)
Probably in your storyboard the segue type will be push, that's why you are getting the Navigation Bar.
You might be interested in the following:
presentViewController:animated:completion:
ViewController Programming Guide for iOS
You are presenting the next view controller, not pushing it. (at least in case 1) So you have a modal link and no nav controller !
You can either use .pushViewController or simply performSegue and make sure your segue type is push. I would go for the performSegue and change the identifier name in each switchcase.
And you can still pass data in the prepareForSegue
Better to call performSegue in didSelectRow. And perform actions in perpareSegue Method .