swift picker view row out of range if row greater the 15 - pickerview

I am working with a multiple picker view in iOS.
Everything works perfectly only if the selected rows in a component is lower than 16.
In few words, even if the component has more than 16 items it looks that row can not point over the 15th item.
When a component points over the 15th, the pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) sets the error "Fatal error: Index out of range"

Related

UIPickerView rendering incorrectly after iOS 14/Xcode 12.0.1 update

Anybody notice that the text in your application's UIPickerViews is rendered incorrectly, with the first character cut off? I'm seeing this in all UIPickerViews in my app, on multiple devices. You can see a few pixels of the first character in most cases.
I've tried deleting derived data, and the application from the phones, but no dice.
I'm not sure which update might have triggered the problem, but it just started in a project that has been stable for months. The code for the labels:
func pickerView(_ pickerView: UIPickerView,
viewForRow row: Int,
forComponent component: Int,
reusing view: UIView?) -> UIView
{
let pickerLabel = UILabel()
pickerLabel.text = "Rec.709"
pickerLabel.font = UIFont(name: "Ropa Sans", size: 18)
pickerLabel.textColor = UIColor.white
pickerLabel.textAlignment = NSTextAlignment.left
}
Did not change anything to my PickerViews, but I am experiencing exactly the same problem since updating to iOS 14. Seems like Apple changed something in the PickerView implementation.
My ViewForRow function is returning a horizontal UIStackView containing three labels. I was able to solve the problem temporarily by adding a 15 points offset constraint to the leading edge of the first label, and the trailing edge of the last:
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
var stackView: UIStackView
if view != nil {
stackView = view as! UIStackView
} else {
let leftLabel = UILabel()
let ctrLabel = UILabel()
let rightLabel = UILabel()
stackView = UIStackView(arrangedSubviews:[leftLabel, ctrLabel, rightLabel])
stackView.axis = .horizontal
stackView.distribution = .fill
// Temporary fix.
leftLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 15.0).isActive = true
rightLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -15.0).isActive = true
// Set text of labels here...
return stackView
}
I haven't been able to check yet, but I am afraid that this extra margin may now look weird on devices that are still running an older iOS version.
I found only the leftmost component's left-aligned string to be cut off (for example, I have a multipicker view with 3 components, all of which are left-aligned. Only the leftmost component is cutoff.
I found it easier to just modify the strings to pad them with a couple of extra leading spaces - I'm using a dictionary for the strings and so they are all in one place (and used only for display, so I don't have to worry about using values). Worked well in my case. If and when Apple fixes the issue, it will be easy to revert (if necessary).

NSTableView has unwanted space above first row

I have an NSTableView whose first row is pushed down 10 pt from the top. Headers are turned off, there are no group rows, cell spacing is 0, and the enclosing scroll view's content inset is 0.
UPDATED
Here's a sample project that demonstrates the issue.
Here's a grab of the view hierarchy debugger:
Here's the vertical constraints of the first row's NSTableRowView while paused in the view hierarchy debugger:
I tried implementing the delegate's tableView(_:didAdd:forRow:) and inspecting the constraints, first with constraintsAffectingLayout(for:):
[<NSLayoutConstraint:0x60000390bd40 'NSTableRowView_Encapsulated_Layout_Height' NSTableRowView:0x7fdb12e26eb0.height == 24 priority:500 (active)>]
Then printing all the row view's constraints:
- 0 : <NSLayoutConstraint:0x60000390bcf0 'NSTableRowView_Encapsulated_Layout_Width' NSTableRowView:0x7fdb12e26eb0.width == 329 priority:500 (active)>
- 1 : <NSLayoutConstraint:0x60000390bd40 'NSTableRowView_Encapsulated_Layout_Height' NSTableRowView:0x7fdb12e26eb0.height == 24 priority:500 (active)>
- 2 : <NSAutoresizingMaskLayoutConstraint:0x60000390bbb0 h=--& v=-&- InlineCell.minX == 16 (active, names: InlineCell:0x7fdb12e283a0, '|':NSTableRowView:0x7fdb12e26eb0 )>
- 3 : <NSAutoresizingMaskLayoutConstraint:0x60000390bc00 h=--& v=-&- InlineCell.width == 297 (active, names: InlineCell:0x7fdb12e283a0 )>
- 4 : <NSAutoresizingMaskLayoutConstraint:0x60000390bc50 h=--& v=-&- InlineCell.minY == 0 (active, names: InlineCell:0x7fdb12e283a0, '|':NSTableRowView:0x7fdb12e26eb0 )>
- 5 : <NSAutoresizingMaskLayoutConstraint:0x60000390bca0 h=--& v=-&- V:[InlineCell]-(0)-| (active, names: InlineCell:0x7fdb12e283a0, '|':NSTableRowView:0x7fdb12e26eb0 )>
The cell's minY constraint is set to 0, but the row is using an autoresizing mask. The table uses a simple diffable datasource:
NSTableViewDiffableDataSourceReference(tableView: table) { tableView, column, row, item in
guard let cell = tableView.makeView(withIdentifier: inlineCellIdentifier, owner: self) as? NSTableCellView else {
preconditionFailure("Failed to create results cell")
}
cell.textField?.textColor = self.themeAttributes.color
cell.textField?.font = self.themeAttributes.font
cell.textField?.stringValue = self.displayString(for: item)
return cell
}
The only delegate method implemented is tableView(_:heightOfRow:). The table aligns itself with the lines of a sibling text view so it gets it row height from there:
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let layoutManager = editor?.layoutManager else {
preconditionFailure("Missing layout manager in editor")
}
return height(of: row, in: layoutManager)
}
This seems like it's probably obvious, but I don't see why the table is forcing its rows to be offset like this. I've found plenty of questions on this forum asking how to insert a gap at the top of the table, but not how to remove one. Any advice?
It's not a bug, as stated in some comments, it's a new way how the NSTableView works in Big Sur (actually there's a bug, but elsewhere, see below). Free Pascal site contains nice overview of what's new.
New NSTableView properties
macOS Big Sur introduced new NSTableView properties:
style
effectiveStyle
style documentation:
The default value for this property is NSTableView.Style.automatic in macOS 11. Apps that link to previous macOS versions default to NSTableView.Style.fullWidth.
effectiveStyle documentation:
If the style property value is NSTableView.Style.automatic, then this property contains the resolved style.
.automatic documentation:
The system resolves the table view style in the following manner:
If the table view is in a sidebar split-view controller item, effectiveStyle resolves to NSTableView.Style.sourceList.
If the table’s scroll view has a border, effectiveStyle resolves to NSTableView.Style.fullWidth.
Otherwise effectiveStyle resolves to NSTableView.Style.inset. However, if the table needs extra space to fit its column cells, effectiveStyle resolves to NSTableView.Style.fullWidth.
Your table view has style set to .automatic in the Main.storyboard. Which means that the effective style resolves to .inset -> 10pt around content (based on the rules from the .automatic documentation).
Open the view debugger with a selected row. You can see the .inset style effect:
Use .fullWidth to remove 10pt insets.
You can also test the .automatic style behavior - try to add a border to the scroll view. Resolves to .fullWidth.
Bug & workaround
You probably tried to set the table view style to Full Width in the Interface Builder. It doesn't work. No matter what value you choose, it behaves like .automatic.
Add the following line to your code to workaround this issue:
tableView?.style = .fullWidth
Works as expected now:
Reported as FB8258910.

Controlling editing in NSTableView

I have subclassed NSTableView and NSTableColumn, and I want to control processus of edition of the cells of the table.
I have 2 states for my table class: edition and nonedition.
whatever the state of the table, when the user select a row, the row has to be overlighted.
if the table is in state edition, the controls corresponding to the selected row must be visible.
I use the function rowview to make this possible, but I have no success.
1 when I click on the text cell of a cell, the control appears always.
2 when the rowview function is called, I don't understand the value of the row parameter. Sometimes it is correct, sometimes it is incorrect.
is it someone who could hep me?
I want to make precision: to simplyfy, in my subclassed NSTableView, I write this code:
override func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView? {
let aRow = super.rowView (atRow: row, makeIfNecessary: makeIfNecessary)
if aRow != nil {
Swift.print("rowview is called for row (row)")
}
return aRow
}
When I click on row 0, I have the following result: "rowview is called for row 1"
Then when I click on row 1: "rowview is called for row 1 rowview is called for row 0 rowview is called for row 2"

get row number of dragged row in tableview

i have two table views where you can drag a row from table A to table B.
this works fine.
but i need to know, which row number will dragged from table A.
This is my code of table A and the problem, too:
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {
pboard.declareTypes([NSStringPboardType],owner: nil)
pboard.setString(persons[0].objectID.uriRepresentation().absoluteString, forType: NSStringPboardType)
return true
}
at the moment i always dragged the first element of "persons"
i need a solution where i can set a row number (or something like this) instead of the static "persons[0]".
is there an way for this?
Have you tried persons[rowindexes.first] ?

Xcode 7 ui automation - loop through a tableview/collectionview

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.

Resources