I have a single column, view-based NSTableView in which I want to place two different custom cell views, one alternating with the other in each row, like so:
Odd rows: OddRowNumberCellView.
Even rows: EvenRowNumberCellView.
There a solution for iOS from Natasha in her highly rated answer UITableview with more than One Custom Cells with Swift but it depends on the Dynamic Prototypes setting in the Attributes Inspector, a feature that is not available for MacOS.
Does anyone know how to do this on Mac please? (I'm on macOS Sierra 10.12.4.)
You do exactly the same but instead of Dynamic Protoypes you set an Identifier "OddRow" or "EvenRow" and then in your datasource implementation:
if indexPath.row % 2 == 0 {
let cellView: tableView.make(withIdentifier: "EvenRow")
//set the data here
return cellView
} else {
let cellView: tableView.make(withIdentifier: "OddRow")
//set the data here
return cellView
}
Related
I am writing an UI test case, in which I need to perform an action, and then on the current page, scroll the only UITableView to the bottom to check if specific text shows up inside the last cell in the UITableView.
Right now the only way I can think of is to scroll it using app.tables.cells.element(boundBy: 0).swipeUp(), but if there are too many cells, it doesn't scroll all the way to the bottom. And the number of cells in the UITableView is not always the same, I cannot swipe up more than once because there might be only one cell in the table.
One way you could go about this is by getting the last cell from the tableView. Then, run a while loop that scrolls and checks to see if the cell isHittable between each scroll. Once it's determined that isHittable == true, the element can then be asserted against.
https://developer.apple.com/documentation/xctest/xcuielement/1500561-ishittable
It would look something like this (Swift answer):
In your XCTestCase file, write a query to identify the table. Then, a subsequent query to identify the last cell.
let tableView = app.descendants(matching: .table).firstMatch
guard let lastCell = tableView.cells.allElementsBoundByIndex.last else { return }
Use a while loop to determine whether or not the cell isHittable/is on screen. Note: isHittable relies on the cell's userInteractionEnabled property being set to true
//Add in a count, so that the loop can escape if it's scrolled too many times
let MAX_SCROLLS = 10
var count = 0
while lastCell.isHittable == false && count < MAX_SCROLLS {
apps.swipeUp()
count += 1
}
Check the cell's text using the label property, and compare it against the expected text.
//If there is only one label within the cell
let textInLastCell = lastCell.descendants(matching: .staticText).firstMatch
XCTAssertTrue(textInLastCell.label == "Expected Text" && textInLastCell.isHittable)
Blaines answer lead me to dig a little bit more into this topic and I found a different solution that worked for me:
func testTheTest() {
let app = XCUIApplication()
app.launch()
// Opens a menu in my app which contains the table view
app.buttons["openMenu"].tap()
// Get a handle for the tableView
let listpagetableviewTable = app.tables["myTableView"]
// Get a handle for the not yet existing cell by its content text
let cell = listpagetableviewTable.staticTexts["This text is from the cell"]
// Swipe down until it is visible
while !cell.exists {
app.swipeUp()
}
// Interact with it when visible
cell.tap()
}
One thing I had to do for this in order to work is set isAccessibilityElement to true and also assign accessibilityLabel as a String to the table view so it can be queried by it within the test code.
This might not be best practice but for what I could see in my test it works very well. I don't know how it would work when the cell has no text, one might be able to reference the cell(which is not really directly referenced here) by an image view or something else. It's obviously missing the counter from Blaines answer but I left it out for simplicity reasons.
I've got a simple example of an app here which I slapped together, and what I'm getting is pretty much what I'm after.
The issue is that when the view loads up, in the NSViewController's viewDidLoad, I set the tableView's selected index to 0 i.e. the first item (which works).
What I do notice is that when this happens, the selected row comes up as grey in color (i.e. as if it's not an active window/view)… It only seems to high light in the normal blue color when I physically click on the row that's selected.
I can confirm that the row is selected and everything appears fine.
Any ideas?
To confirm, the code I use to select the row is:
override func viewDidAppear() {
self.tableView.selectRowIndexes(NSIndexSet(index: 0), byExtendingSelection: false)
}
Here is what's happening with the actual view itself:
ABOVE: The darker grey line is the "selection bar". This is what happens as soon as the view becomes active.
ABOVE: Once I click on that row (the one which was once dark grey), I get he desired high lighting.. i.e. Navy Blue.
The reason why the cell is grey is because the table view doesn't have focus / isn't the first responder.
There are 3 states for tableView cell selection color
no selection = clear row background
selection and focus = blue row background
selection and no focus = grey row background
This is probably because another view has focus. Simply selecting a cell doesn't shift focus to a tableView. You need to call NSWindow.makeFirstResponder() to change the focus.
func tableViewSelectionDidChange(notification: NSNotification) {
let tableView = notification.object as! NSTableView
if tableView.selectedRow != -1 {
self.window!.makeFirstResponder(self.tableView)
}
}
I've managed to find out what's going on. (I think) and it seems to work.
I had to:
Subclass NSTableRowView
Add a new NSView just below the actual cell view (row) in Interface Builder
Set the new Row View's class to 'myNSTableViewSubClass'
Set the row view's Identifier to: NSTableViewRowViewKey (this is very specific, and that literally is the key, if this isn't set, it won't work be regarded as the Table Row View.
in the subclass I had to override the emphasised: Bool to always return yes e.g.:
override var emphasized: Bool{
get{
return true
}
set{
//You need to have the "set" there as it's a mutable prop
//It doesn't have to do untying though
}
}
And voila..
The catch in my case was in 4 above.
I am using xCode 7.1. I would like to automate interaction with all cells from a table/collection view. I would expect it to be something like this:
for i in 0..<tableView.cells.count {
let cell = collectionView.cells.elementBoundByIndex(i)
cell.tap()
backBtn.tap()
}
However this snippet only queries current descendants of the table view, so it will loop through the first m (m < n) loaded cells out of total n cells from the data source.
What is the best way to loop through all cells available in data source? Obviously querying for .Cell descendants is not the right approach.
P.S.: I tried to perform swipe on table view after every tap on cell. However it swipes to far away (scrollByOffset is not available). And again, don't know how to extract total number of cells from data source.
Cheers,
Leonid
So problem here is that you cannot call tap() on a cell that is not visible. SoI wrote a extension on XCUIElement - XCUIElement+UITableViewCell
func makeCellVisibleInWindow(window: XCUIElement, inTableView tableView: XCUIElement) {
var windowMaxY: CGFloat = CGRectGetMaxY(window.frame)
while 1 {
if self.frame.origin.y < 0 {
tableView.swipeDown()
}
else {
if self.frame.origin.y > windowMaxY {
tableView.swipeUp()
}
else {
break
}
}
}
}
Now you can use this method to make you cell visible and than tap on it.
var window: XCUIElement = application.windows.elementBoundByIndex(0)
for i in 0..<tableView.cells.count {
let cell = collectionView.cells.elementBoundByIndex(i)
cell.makeCellVisibleInWindow(window, inTableView: tableView)
cell.tap()
backBtn.tap()
}
let cells = XCUIApplication().tables.cells
for cell in cells.allElementsBoundByIndex {
cell.tap()
cell.backButton.tap()
}
I face the same situation however from my trials, you can do tap() on a cell that is not visible.
However it is not reliable and it fails for an obscur reason.
It looks to me that this is because in some situation the next cell I wanted to scroll to while parsing my table was not loaded.
So here is the trick I used:
before parsing my tables I first tap in the last cell, in my case I type an editable UITextField as all other tap will cause triggering a segue.
This first tap() cause the scroll to the last cell and so the loads of data.
then I check my cells contents
let cells = app.tables.cells
/*
this is a trick,
enter in editing for last cell of the table view so that all the cells are loaded once
avoid the next trick to fail sometime because it can't find a textField
*/
app.tables.children(matching: .cell).element(boundBy: cells.count - 1).children(matching: .textField).element(boundBy: 0).tap()
app.typeText("\r") // exit editing
for cellIdx in 0..<cells.count {
/*
this is a trick
cell may be partially or not visible, so data not loaded in table view.
Taping in it is will make it visible and so do load the data (as well as doing a scroll to the cell)
Here taping in the editable text (the name) as taping elsewhere will cause a segue to the detail view
this is why we just tap return to canel name edidting
*/
app.tables.children(matching: .cell).element(boundBy: cellIdx).children(matching: .textField).element(boundBy: 0).tap()
app.typeText("\r")
// doing my checks
}
At least so far it worked for me, not sure this is 100% working, for instance on very long list.
Is there a way in Xcode 7 UI Testing to select the 3rd row in a UIPickerView?
I have tried various things like this to identify the rows in each picker but all the requests below return 0 found:
XCUIApplication().pickers.element.cells.count
XCUIApplication().pickers.element.staticTexts.count
Any ideas?
Updated: I am aware of adjustToPickerWheelValue method where you can select a particular value you already know, but I am trying to select the 3rd value (index = 4) when I don't know the values that exist in the picker.
You can use -adjustToPickerWheelValue: to select items on a UIPickerView.
When there is one UIPickerView on the screen you can select the element directly, like so.
let app = XCUIApplication()
app.launch()
app.pickerWheels.element.adjustToPickerWheelValue("Books")
If the picker has multiple wheels you will need to first select the wheel via it's accessibility identifier, then adjust it.
app.pickerWheels["Feet"].adjustToPickerWheelValue("5")
Here's a GitHub repo with a working example. And some more information in a blog post I wrote.
Workaround:
let pickerWheel = XCUIApplication().pickers.pickerWheels.element
let appWindowHeight = XCUIApplication().windows.elementBoundByIndex(0).frame.height
let pickerWheelCellHeight = GFloat(44)
func advancePickerWheelOneValue() -> Self {
let topScrollPoint = Double(pickerWheel.frame.midY/appWindowHeight)
let bottomScrollPoint = Double((pickerWheel.frame.midY + pickerWheelCellHeight)/appWindowHeight)
let topScreenPoint = XCUIApplication().windows.elementBoundByIndex(0).coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: topScrollPoint))
let bottomScreenPoint = XCUIApplication().windows.elementBoundByIndex(0).coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: bottomScrollPoint))
bottomScreenPoint.pressForDuration(0, thenDragToCoordinate: topScreenPoint)
return self
}
You may need to adjust the dx value depending on where the picker is.
.coordinateWithNormalizedOffset(CGVector(dx: 0.5,
But 0.5 works if the picker occupies the width of the screen.
With this method you an move the picker on value at a time. Call it multiple times and you get to the value you want.
Its not ideal but it can help if you want to check certain indexes. Hopefully you would have to use it on a picker with hundreds of values ;)
Example with Date picker (UIDatePicker)
XCUIApplication().datePickers.pickerWheels.elementBoundByIndex(0).adjustToPickerWheelValue("March")
XCUIApplication().datePickers.pickerWheels.elementBoundByIndex(1).adjustToPickerWheelValue("24")
XCUIApplication().datePickers.pickerWheels.elementBoundByIndex(2).adjustToPickerWheelValue("2000")
And here is example with Picker view (UIPickerView)
XCUIApplication().pickerWheels.element.adjustToPickerWheelValue("Male")
I'm having trouble accessing an optional string in swift from a table view cell. Each cell has a title label as well as a detail text label and the amount of cells at any given time in the table view is dependent on the user (the table view is used to display saved values). Since the strings saved as the detail text label's text in each cell are way too long to read in the table view, I wish to load them in another view that is segued to when a cell is pressed. This is my prepare for segue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "cellSegue"
{
let cell = tableView.cellForRowAtIndexPath(tableView.indexPathForSelectedRow()!)
var transfer : ExplanationView = segue.destinationViewController as ExplanationView
if let unwrapped = cell?.textLabel?.text!
{
transfer.infoText.text = unwrapped
}
}
}
When a segue is performed, I get a fatal crash, the offending line is "transfer.infoText.text = unwrapped" and Xcode tells me it found nil when unwrapping an optional. During my debuggingg efforts, I've tried "println(cell?.textLabel?.text!)" and Xcode prints "Optional(String)" where "String" is the actual string I'm trying to access so it seems like I'm on the right track, but obviously there's something I'm missing. I've also tried "cell?.textLabel?.text" but I get the same error. Any help is appreciated
You shouldn't ever extract data from a view. You should extract data from a model. The fact that you have a tableView with cells implies you have a model for the data that goes in those cells. It might be an array of strings or whatever. Instead of trying to extract the data from the cells, just get the data from the data model you used to populate the cells. Something like:
let selectedIndexPath = tableView.indexPathForSelectedRow()
let selectedData = data[selectedIndexPath.row]
Your problem is not unwrapped but instead transfer.infoText.text. Your IBOutlets are not set up at the time of the prepareForSegue. You need add a property (var) to your destination view controller to hold the unwrapped value. Then in viewDidLoad when the IBOutlets are set up, copy the string to your text field.