How do you test UIStoryBoard segue is triggered by didSelectRow(at:) - ios

So I have a storyboard with a UITableView. There is a prototype cell with a show segue hooked up to another UIViewController
Example
The cell identifier is "CellOne"
The segues has no identifier
My class is the dataSource and delegate for the tableView.
class looks like this:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "CellOne", for: indexPath)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected the row")
}
}
Normally I would test it by swizzling prepare for segue to capture the destination ViewController and whatever else I need but calling tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) programmatically doesn't trigger the code in prepare for segue.
Is it possible to test that selecting Cell One triggers the storyboard segue without adding a segue identifier and calling it explicitly from prepareForSegue?

If your tableViewCell is the only thing that triggers a segue to the destination you can use is or as:
if let destination = segue.destination as? MyViewController,
let indexPath = tableView.indexPathForSelectedCell {
destination.detail = model[indexPath.row]
}
Otherwise if you need to disambiguate you can check the class of the sender with is or as

Updated:
Long story short there isn't a great way to do this. What makes this difficult is that while most controls we're used to testing have an action and a sender, a touch on a UITableViewCell is a different paradigm.
That said, having a segue identifier is basically a pre-requisite for any strategy.
One way is to get a reference to the cell and call performSegue(withIdentifier:,sender:):
class ViewControllerTests: XCTestCase {
func testClickingACell() {
let controller = UIStoryboard(name: "ViewController", bundle: nil).instantiateInitialViewController() as! ViewController
let cell = controller.tableView.dataSource?.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 0))
controller.performSegue(withIdentifier: "MySegue", sender: cell)
XCTAssertNotNil(controller.presentedViewController as? TheDestinationViewController)
}
}
Another (completely overkill) way would be to have a custom cell where you handle all of your own touch logic. This would be insane but it's a possibility and it would open up more options for testing. I'm not going to show this way because it would be an insane way to do it.
Another way is to use a different architecture that gets rid of UIKit and allows for testing just the performSegue logic. Example:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var myTableView: UITableView!
var navigator: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
navigator = self
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
navigator?.performSegue(withIdentifier: "MySegue", sender: nil)
}
}
This allows you to do something like this in your tests:
class MockNavigator: ViewController {
var performSegueCalled = false
var performSegueIdentifier: String?
override func performSegue(withIdentifier identifier: String, sender: Any?) {
performSegueCalled = true
performSegueIdentifier = identifier
}
}
func testExample() {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController
controller.loadViewIfNeeded()
// Need to keep a reference to be able to assert against it
let mockNavigator = MockNavigator()
controller.navigator = mockNavigator
controller.tableView(controller.myTableView, didSelectRowAt: IndexPath(row: 0, section: 0))
XCTAssertTrue(mockNavigator.performSegueCalled)
XCTAssertEqual(mockNavigator.performSegueIdentifier, "MySegue")
}
Another way to structure your code to avoid UIKit is to use something like a view model-coordinator pattern to create and test a viewModel. Basically you'd tell your coordinator that a cell was selected and the coordinator would update a view model with the desired segue identifier. This way you could test your coordinator object to and be mostly sure that you'll trigger the correct segue if the coordinator is hooked up. A simple manual test would tell you that.
In pseudocode:
struct ViewModel {
let labelText: String
let segueIdentifier: String
}
class Coordinator {
var data = [YourObject]()
var viewModel = ViewModel(labelText: "", segueIdentifier: "")
func selectedItem(at row: Int) {
let item = data[row]
// Do some logic to figure out which identifier you want
var segueIdentifer: String
if item == whatever {
segueIdentifier = "something"
}
viewModel = ViewModel(labelText: item.text, segueIdentifier: segueIdentifier)
}
}
Probably the best way is a combination of approaches. Use a coordinator with a view model that's tested on its own. Then have a test where you use UIKit to select a cell and make sure that a mocked implementation of that coordinator is used as expected. The smaller units you're testing at a time the easier it will be.

For just testing a segue you can do:
In your VC:
open let segueToSomewhere = "segueToSomewhere"
open var calledSegue: UIStoryboardSegue!
override open func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
calledSegue = segue
}
In your Tests:
func testYourSegue() {
//Given
let storyboard = UIStoryboard(name: "Your_storyboard", bundle: nil)
let yourVC = storyboard.instantiateViewController(withIdentifier: "YourVC") as? YourVC
//When
yourVC.performSegue(withIdentifier: previousVC.segueToSomewhere, sender: nil)
//Then
XCTAssertEqual(previousVC.calledSegue.identifier, yourVC.segueToSomewhere, "The selected segue should be \(previousVC.segueToSomewhere)")
}

Related

how to make sure that my for in loop enables me to perform only one segue?

I have a tableview inside a view controller, that is connected to 3 different view controller by a segue. Each one has an identifier.
In order to perform a segue to the corresponding view controller, I have created an array that contains the identifiers for each segue. like so :
var tableauSegues = ["versChapitre1", "versChapitre2", "versChapitre3"]
In order to manage the segue according to the index of the array, I chose to use a for in loop inside of an iB action, like this:
#IBAction func versHistoire(_ sender: UIButton) {
for i in tableauSegues {
performSegue(withIdentifier: i, sender: self)
}
}
the problem is that all 3 segues are performed. I only want to perform one segue at the time.
any idea ?
Obviously, you want to avoid looping, but instead want to just use tableauSegues[row]. But the trick is how to get the row.
Bottom line, you should let the cell handle the tapping of the cell, and have the cell tell the table view controller when its button was tapped. But the trick is that when it does that, it should supply a reference to itself when it does that, so that the view controller can determine which row had its button tapped.
Thus, remove your existing #IBAction directly between the button and the view controller, and instead, hook it up to an #IBAction in the cell’s base class.
// CustomCell.swift
import UIKit
protocol CustomCellDelegate: class {
func didTapButton(_ sender: Any, in cell: CustomCell)
}
class CustomCell: UITableViewCell {
#IBOutlet weak var customLabel: UILabel!
static let preferredReuseIdentifier = "CustomCell"
weak var delegate: CustomCellDelegate?
func configure(text: String, delegate: CustomCellDelegate) {
customLabel.text = text
self.delegate = delegate
}
#IBAction func didTapButton(_ sender: Any) {
delegate?.didTapButton(sender, in: self)
}
}
Note, when I configure that cell, I’m going to have the table view provide not only whatever values need to be displayed in the cell, but also indicate that it is the delegate for that cell:
// ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var tableauSegues = ["versChapitre1", "versChapitre2", "versChapitre3"]
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableauSegues.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.preferredReuseIdentifier, for: indexPath) as! CustomCell
cell.configure(text: "...", delegate: self)
return cell
}
}
extension ViewController: CustomCellDelegate {
func didTapButton(_ sender: Any, in cell: CustomCell) {
let row = tableView.indexPath(for: cell)!.row
let identifier = tableauSegues[row]
performSegue(withIdentifier: identifier, sender: self)
}
}

Variable returns nil value after changing its value from another swift file

I'm trying to change a value of a variable from another Swift file, but for some reason it does not work and it returns nil.
This is what I have tried:
class ShowIssues: UIViewController, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let si = self.storyboard?instantiateViewController(withIdentifier: "ShowIssueDetail") as! ShowIssueDetail
si.idSelected = indexPath.row //Here I change the value of the variable
performSegue(withIdentifier: "ShowIssueDetail", sender: self)
}
}
ShowIssueDetail.swift:
class ShowIssueDetail: UITableViewController {
var idSelected: Int! //This is the variable I want to change its value from the another swift file
override func viewDidLoad() {
print(idSelected) //Here it prints out nil instead of the selected row
}
}
I have also tried it in this way, but same issue:
class ShowIssues: UIViewController, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let si = ShowIssueDetail()
si.idSelected = indexPath.row //Here I change the value of the variable
performSegue(withIdentifier: "ShowIssueDetail", sender: self)
}
}
What am I doing wrong?
Thank you in advance!
Note: Both swift files are of different type, ShowIssues.swift is UIViewController and ShowIssueDetail is UITableViewController, I do not know if it does not work due to this.
If you have a segue set up in Storyboard, you shouldn't be instantiating the destination view controller from code, the Storyboard will create the view controller for you. By initialising another instance, you end up setting values on an instance that won't be presented to the user.
You need to override prepare(for:) and set the value there.
class ShowIssues: UIViewController, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "ShowIssueDetail", sender: indexPath.row)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowIssueDetail", let destinationVC = segue.destination as? ShowIssueDetail, let selectedId = sender as? Int {
destinationVC.idSelected = selectedId
}
}
}
performSegue will create a new instance of ShowIssueDetail.
Because of that you never set idSelected
It looks like you're talking about the variable idSelected in your ShowIssueDetail view controller.
The problem is that in both versions of your code you are creating a different instance of the view controller than the one you are segueing to, and setting a value in that throw-away view controller.
You need to use prepareForSegue to pass the variable to the view controller that you're segueing to.
//Add an instance variable to remember which item is selected.
var selected: Int?
class ShowIssues: UIViewController, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selected = self.storyboard?instantiateViewController(withIdentifier: "ShowIssueDetail") as! ShowIssueDetail
performSegue(withIdentifier: "ShowIssueDetail", sender: self)
}
}
func prepare(for: segue, sender: Any) {
if let dest = segue.destination as? ShowIssueDetail {
dest.idSelected = selected
}
}
Your approach is wrong here:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let si = self.storyboard?instantiateViewController(withIdentifier: "ShowIssueDetail") as! ShowIssueDetail
si.idSelected = indexPath.row //Here I change the value of the variable
performSegue(withIdentifier: "ShowIssueDetail", sender: self)
}
You are creating another instance of the VC and setting the value. That VC is not the actual one that will be shown.
You need to use the prepareForSegue method
var selectedRow: someTypeSameAsRow?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedRow = indexPath.row
performSegue(withIdentifier: "ShowIssueDetail", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowIssueDetail",
let vc = segue.destination as? ShowIssueDetail {
vc.idSelected = selectedRow
}
}
EDIT:
What you may need to do to resolve the error mentioned in the comments is wait until the detail view is loaded properly. The awakeFromNib function should work here:
// this goes in the view controller
override func awakeFromNib() {
super.awakeFromNib()
self.detailView.selected = selected
}
So, with this code you are waiting until the view of the VC and it's subviews are fully loaded and then setting the selected property of showDetailView to the same selected property that is on the VC.

How to change the label of a different view controller when selecting data from a tableviewcontrollerin Swift 3 / xCode 8?

I have a view controller which is used as a table view. From the table view I wish to select an item from the rows available and once selected I wish for that chosen thing to be the name of a label in a second view controller.
So
tableViewController - select an item from the list in the table
secondViewcontroller - label name is what is selected in the tableViewController
I have looked around and there is talk of using NSNotification but I can't seem to get it to work, in addition to using prepareForSegue.
My relevant code for the tableViewContrller is this:
//MARK Table View
//number of sections the table will have
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
//number of rows each section of the table will have
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return peripheralArray.count
}
//the way the data will be displayed in each row for the sections
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let BluetoothNaming = peripheralArray[indexPath.row].peripheral.name
cell.textLabel?.text = BluetoothNaming
return cell
}
//what happens when we select an item from the bluetooth list
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
stopScanning()
peripheral = peripheralArray[(indexPath as NSIndexPath).row].peripheral
print ("connecting to peripheral called \(peripheral)")
//store the name of the connected peripeheral
let connectedPeripheral = peripheral
manager?.connect(connectedPeripheral!, options: nil)
performSegue(withIdentifier: "segueBackwards", sender: nil)
}
My secondViewController does not have much on its just the label:
import UIKit
class HomepageViewController: UIViewController {
#IBOutlet var ConnectionLabel: UILabel!
func changeBlueoothLabel() {
self.ConnectionLabel.text = "aaaa"
}
}
What do I need to do so that when I select a row in the table that the label in the secondViewController changes its label to reflect it.
Furthermore, if I wanted to change it so that I have another label on the secondViewController and I wanted to change it from disconnected to connected, would there be much more work involved?
Many thanks
to pass data add a string variable in the HomepageViewController and implement prepareForSegue method to pass selected name from tableViewController to HomepageViewController
in HomepageViewController
var selectedName: String?
in tableViewController:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showHomePage" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let peripheral = peripheralArray[indexPath.row].peripheral.name
let homeController = segue.destination as! HomepageViewController
homeController.selectedName = peripheral
}
}
in HomepageViewController
import UIKit
class HomepageViewController: UIViewController {
#IBOutlet var ConnectionLabel: UILabel!
var selectedName: String?
override func viewDidLoad() {
super.viewDidLoad()
// Used the text from the First View Controller to set the label
if let name = selectedName {
ConnectionLabel.text = name
}
}
}

Why is this value not passing between my ViewControllers?

I'm coding a multi-step sign-up flow, but cannot get the cell labels to pass between each viewcontroller.
Here is the code from the first viewController:
class FirstVC: UIViewController, UITableViewDelegate {
#IBOutlet weak var tableview: UITableView!
var userGoalOptions = ["Lose Fat","Gain Muscle", "Be Awesome"]
var selectedGoal: String = ""
override func viewDidLoad() {
super.viewDidLoad()
self.title = "What Is Your Goal?"
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell1")! as UITableViewCell
cell.textLabel?.text = userGoalOptions[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userGoalOptions.count
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!
selectedGoal = currentCell.textLabel!.text!
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "secondSeque") {
let vc = segue.destinationViewController as! SecondVC
vc.userGoal = selectedGoal
}
}
The segue (secondSegue) is connected to the table view cell in Interface Builder
In my destination viewcontroller (vc) I have an empty userGoal variable, but when I try to print the contents it returns nothing.
I know variations of this question have been asked numerous times, but I can't seem to find (or maybe understand) what I'm messing up.
Assuming the segue is connected to the table view cell in Interface Builder the cell is passed as the sender parameter in prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "secondSeque") {
let selectedIndexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let vc = segue.destinationViewController as! SecondVC
vc.userGoal = userGoalOptions[selectedIndexPath.row]
}
In this case didSelectRowAtIndexPath is not needed and can be deleted.
Apart from that it is always the better way to retrieve the data from the model (userGoalOptions) than from the view (the table view cell) for example
Do
let indexPath = tableView.indexPathForSelectedRow
selectedGoal = userGoalOptions[indexPath.row]
Do not
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!
selectedGoal = currentCell.textLabel!.text!
The prepareForSegue Should not be part of the table view function, you have copied it into a place where it is never called. Move it out and place a break point on it to see that it is being called.

How do I send specific data based on which cell is clicked?

I have a tableview with a bunch of concerts, and when x cell is clicked, I want the artist of that concert to populate the new tableView. Below is the code for the first view controller (the view controller with all the concerts).
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView1: UITableView!
var arrayOfConcerts: [concert] = [concert]()
override func viewDidLoad()
{
super.viewDidLoad()
self.setUpConcerts()
self.tableView1.rowHeight = 145.0
}
func setUpConcerts()
{
var ACL = concert(imageName: "ACL2015.png")
let Landmark = concert(imageName: "Landmark.png")
let BostonCalling = concert(imageName: "BostonCalling.png")
let Lolla = concert(imageName: "Lolla.png")
arrayOfConcerts.append(ACL)
arrayOfConcerts.append(Landmark)
arrayOfConcerts.append(BostonCalling)
arrayOfConcerts.append(Lolla)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayOfConcerts.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.tableView1.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CustomCell
let concerts = arrayOfConcerts[indexPath.row]
cell.setCell(concerts.imageName)
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
performSegueWithIdentifier("concertartist", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
}
}
Below is the code for the Artist View Controller (the second
viewcontroller). This tableView should be populated with specific
artists.
How would I go about doing that?
class ArtistConcerts: UIViewController, UITableViewDataSource, UITableViewDelegate {
var arrayOfArtists: [artist] = [artist]()
#IBOutlet weak var tableViewArtists: UITableView!
override func viewDidLoad()
{
super.viewDidLoad()
self.setUpArtists()
}
func setUpArtists()
{
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayOfArtists.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableViewArtists.dequeueReusableCellWithIdentifier("", forIndexPath: indexPath) as! CustomCell
let artists = arrayOfArtists[indexPath.row]
cell.setCell(artists.imageNameArtist)
return cell
}
}
You need to do this in the prepareForSegue method.
let vc = segue!.destinationViewController as! ArtistConcerts
if let rowSelected = tableView.indexPathForSelectedRow()?.row {
vc.arrayOfArtists = artistsPerforming[0]//this is an array
}
then, set whatever data you need to actually populate the tableView in that new view controller. I am not 100% sure on what you are trying to do though. Does my answer make sense?
you can send data once the cell is pressed to another viewcontroller by using didSelectRowAtIndexPath or prepareForSegue or didSelectRowAtIndexPath + prepareForSegue
let's say you are using prepareForSegue, things you will need
the segue identifier name
cast the destinationController to the controller you want to send the data
the indexPath.row where the cell was selected
then set the variable or variables or data structure that should receive data
Create a segue when you pressed CTRL + Drag from your cell into another viewcontroller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if segue.identifier == "Name_Of_Segue"
{
let destination = segue.destinationViewController as! NAME_Of_ViewController
let indexPath = mytableview.indexPathForSelectedRow!
let data = arrayOfArtists[indexPath.row]
destination.artist = data
}
mytableview is an IBOutlet from your tableview
artist is a variable that is declared in your destination controller
destination.artist = data this line of code is passing data from the specific cell then send it to your destination cell

Resources