How do I select column and row from a NSTableView? - swift

I am writing a NSTableView based view. And I try to use:
let selectedRowNumber = tableViewMesos.selectedRow and
let selectedColumnNumber = tableViewMesos.selectedColumn to identify the textField that is in the cell View that I want to save into my array:
taulaDeConsumsMesos[selectedColumnNumber].filesConsum![selectedRowNumber] = sender.doubleValue
But a problem happens here, because the return value of selectedColumnNumber is -1 which is the value of non-selected Column for any row selected of any column.
But the selectedRowNumberreturns the index of the row properly.
So the program crashes when finishing the edition of a textfield in a cell. Because the selectedColumnNumber is -1 out of the limits of the array. See the code below to identify where this happens exactly.
Could you help me? I lost too many hours on this..
I'm new on OSX programing I hope you could help me.
The app layout
Here is the completed code:
class BalancGlobalViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate{
#IBOutlet weak var tableViewMesos: NSTableView!
let mesos = ColumnaDeConsumsMes() // contains an array of doubles called .filesConsum that represent the rows that I want to show
let consum = ColumnaDeConsumsMes()
var taulaDeConsumsMesos = [ColumnaDeConsumsMes]() // array of arrays that represent the columns with rows in it that will be the tableView data
func setUpMesos(){ // init table columns
mesos.filesConsum = [1, 2, 3, 4, 5, 6, 7, 8 , 9, 10, 11, 12, nil]
consum.filesConsum = [100, 100, 100, nil, nil, nil, nil, nil , nil, nil, nil, nil, nil]
taulaDeConsumsMesos.append(mesos)
taulaDeConsumsMesos.append(consum)
}
func setUpGradient(){
...not important here
}
override func viewDidLoad() {
super.viewDidLoad()
setUpMesos()
tableViewMesos.setDelegate(self)
tableViewMesos.setDataSource(self)
setUpGradient()
}
override func viewDidLayout() {
super.viewDidLayout()
setUpGradient()
}
#IBAction func alEditarUnTextFieldDeConsum(sender: NSTextField) { // this is the IBAction of the TextField of the Consum Column (see the image)
let selectedRowNumber = tableViewMesos.selectedRow
let selectedColumnNumber = tableViewMesos.selectedColumn
print("SelectedRowNumber! = \(selectedRowNumber)")
print("SelectedColumnNumber! = \(selectedColumnNumber)")
// the selectedColumnNumber return always -1 except when clicking the header of the column, then return 0 or 1 depending on the column. But it should return 0 even when the user edit a row of the index 0 column !!!
let numero = tableViewMesos.numberOfColumns
print("numero de columnes: \(numero)") // returns 2
if selectedRowNumber != -1 { //-1 is returned when no row is selected in the TableView
taulaDeConsumsMesos[selectedColumnNumber].filesConsum![selectedRowNumber] = sender.doubleValue //try to save here the value edited by the user on my array of arrays of doubles
//here the program crashes! selectedColumnNumber = -1
print("Sender! = \(sender.doubleValue)") // the sender returns the information correctly
//print("SelectedRowNumber! = \(selectedRowNumber)") // this works too!
reloadTaula()
}
}
func tableViewSelectionDidChange(notification: NSNotification) {
print("columna: \(tableViewMesos.selectedColumn)")
print("row: \(tableViewMesos.selectedRow)")
print("tag: \(tableViewMesos.selectedTag())")
}
// the tableViewMesos.selectedColumn return always -1 except when clicking the header of the colum, then return 0 or 1 depending on the column. But i should return 0 even when I edit a row of the index 0 column !!!
func reloadTaula() {
tableViewMesos.reloadData()
}
// MARK: - Datasource
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
if (tableView.identifier == "TaulaMesos") {
return 13
}
}
// MARK: - Delegate
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
var valorCela: Double
var cellIdentifier:String=""
if tableColumn == tableView.tableColumns[0] {
if taulaDeConsumsMesos[0].filesConsum![row] == nil {
return nil
}else {
valorCela = taulaDeConsumsMesos[0].filesConsum![row]!
cellIdentifier = "MesCellID"
}
} else {
if taulaDeConsumsMesos[1].filesConsum![row] == nil {
return nil
}else {
valorCela = taulaDeConsumsMesos[1].filesConsum![row]!
cellIdentifier = "ConsumCellID"
}
}
if let cell = tableView.makeViewWithIdentifier(cellIdentifier, owner: nil ) as? NSTableCellView {
cell.textField?.doubleValue = valorCela
return cell
}
return nil
}
}

Table views do not support selecting individual cells. Both styles of table view support selecting rows. Old-style NSCell-based table views support selecting columns, but modern view-based table views no longer support selecting columns. But it would never have been the case that both selectedRow and selectedColumn were valid at the same time.
From the 10.7 AppKit release notes, where view-based table views were introduced:
Note that the View Based TableView does not support column selection.
To get the row and column of the cell whose text field sent the action, you should use tableViewMesos.rowForView(sender) and tableViewMesos.columnForView(sender). Note, especially, that there's nothing guaranteeing that the text field that's sending the action method is in a selected row. So, don't think of it as selected. It's just the one that's been manipulated by the user.

Related

Search Bar crashing app when inputting characters

I have a UITableView that is populating locations and a Search Bar set as the header of that UITableView.
Whenever certain characters are entered, or a certain amount of characters are entered, the app crashes, giving me no error code.
Sometimes the app crashes after inputting one character, maybe 2 characters, maybe 3, or maybe 4. There seems to be no apparent reason behind the crashing.
The search function properly searches and populates the filtered results, but for no apparent reason, crashes if a seemingly arbitrary amount of characters are inputted.
I have tried using the exception breakpoint tool already, and it is providing me with no new information. I think it has something to do with if there are no search results.
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Locations..."
navigationItem.hidesSearchBarWhenScrolling = false
searchController.hidesNavigationBarDuringPresentation = false
locationTableView.tableHeaderView = searchController.searchBar
searchController.searchBar.sizeToFit()
searchController.searchBar.showsCancelButton = false
searchController.searchBar.barTintColor = UIColor.white
filteredData = locationList
// Sets this view controller as presenting view controller for the search interface
definesPresentationContext = true
locationList = createArray()
// Reload the table
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String) {
filteredData = locationList.filter({( locationName : Location) -> Bool in
return locationName.locationName.lowercased().contains(searchText.lowercased())
})
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
func locationTableView(_ locationTableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return filteredData.count
}
return locationList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let locationCell = locationTableView.dequeueReusableCell(withIdentifier: "locationCell", for: indexPath) as! locationCell
let location: Location
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
locationCell.setLocation(location: location)
return locationCell
}
The expected result is that the UITableView should populate with filtered results. Instead, it populates them and crashes if too many characters are inputted (usually 1-4 characters).
EDIT 1: I have found through debugging the error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
appears on Line 2 on this block of code:
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
EDIT 2: This is the tutorial I used.
https://www.raywenderlich.com/472-uisearchcontroller-tutorial-getting-started
Seems like you are expecting the tableView to provide YOU with the number of sections... it is supposed to be driven by your own datasource.
Since you are not providing a numberOfSections in your data source I'm assuming it is 1. If all of your rows are in 1 section, all of the nifty reloading you are doing could be greatly simplified.
I suggest you read up on UITableView dataSource protocol at https://developer.apple.com/documentation/uikit/uitableviewdatasource
Reviewing the tutorial you are reading, it seems it is using a reloadData() which forces the tableView to ignore previous number of rows and reload its content with a new number of rows. And based on your findings so far, I would assume that is part of the root cause, with the tableview wrongly assuming a pre-determined number of rows and attempting to retrieve cells that are no longer within range.

Let only select specific cells with shouldSelectItemAt indexPath function

I want that only four specific cells can be selected at one time. When a button is pressed, I want that the selectable cells are 4 indexPath.row lower.
Example: In the beginning, indexPath.row 44-47 is selectable. If the button is pressed I want, that the indexPath.row 40-43 is selectable and so on.
I thought about making an array with the indexPath and If the button is pressed, the numbers in the array are 4 numbers lower.
Than I don't know, how to add this to the shouldSelectItemAt indexPath function.
How can I realize this?
You can use an IndexSet.
var allowedSelectionRow: IndexSet
allowedSelectionRow.insert(integersIn: 44...47) //Initial allowed selection rows
In collectionView(_:shouldSelectItemAt:)
return allowedSelectionRow.contains(indexPath.row) //or indexPath.item
Whenever you need:
allowedSelectionRow.remove(integersIn: 44...47) //Remove indices from 44 to 47
allowedSelectionRow.insert(integersIn: 40...43) //Add indices from 40 to 43
Advantage from an Array: Like a set, there is unicity of the values (no duplicates). Contains only integers, and you can add in "range" which can be useful (not add all the indices, but a range).
After comments, if you have only 4 rows allowed and consecutive, you can have that method:
func updateAllowedSectionSet(lowerBound: Int) {
let newRange = lowerBound...(lowerBound+3)
allowedSectionRow.removeAll() //Call remove(integersIn:) in case for instance that you want always the 1 row to be selectable for instance
allowedSectionRow.insert(integersIn: newRange)
}
For the first one, you just need to do:
updateAllowedSectionSet(lowerBound: 44) instead of allowedSelectionRow.insert(integersIn: 44...47)
Let's consider that the items form a String array, and you are keeping track of the selected indices as a Range.
var selectedRange: Range<Int>? {
didSet {
collectionView.reloadData()
}
}
var items: [String] = [] {
didSet {
// To make sure that the selected indices are reset everytime this array is modified,
// so as to make sure that nothing else breaks
if items.count >= 4 {
// Select the last 4 items by default
selectedRange = (items.count - 4)..<items.count
} else if !items.isEmpty {
selectedRange = 0..<items.count
} else {
selectedRange = nil
}
}
}
Then, when you are pressing the button to decrement the range, you can use this logic to handle the same:
func decrementRange() {
if var startIndex = selectedRange?.startIndex,
var endIndex = selectedRange?.endIndex {
startIndex = max((startIndex - 4), 0)
endIndex = min(max((startIndex + 4), (endIndex - 4)), items.count)
selectedRange = startIndex..<endIndex
}
}
Then, you can identify whether the the selection is being done on the active range using:
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if let selectedRange = selectedRange {
return selectedRange.contains(indexPath.item)
}
return false
}
Note: I would advice you to verify whether this covers all the corner cases before trying it out for production code.

Swift - Characters in text field are not visible

In my application I have several custom designed games to practice spelling and grammar. The whole process was working just fine until iOS 11 released.
An overview of how my application works:
the game receives a random word and picture from our server and automatically creates a stack of text fields (in a horizontal stack view)
the amount of text fields is similar to the amount of letters of the received word (e.g. the word "throw" would create five text fields next to each other)
the view is linked to a custom keyboard that is displayed until all questions are answered correctly
the text fields reset themselves if the user's input is not similar to the expected answer
The problem since iOS 11
Only the first two questions don't show the user's input in their respective text fields (the first letter always seem to work). The console also confirms that the input and the comparison of the letters does indeed work, but the text fields stay empty all the time. The game even resets perfectly and goes to the next question if the word has been answered correctly. The weird thing is that question 3, 4 and 5 do not have this problem.
In the example below you can see what is going on. The first question ("plant") only shows the first letter. The second question ("clap") does the same thing. The third question ("bite") does not have this problem. My custom cursor works just fine and automatically jumps to the next box.
Swift Code (functionality only)
let TEXT_FIELD_TAG = 1000
let CURSOR_INDEX = 0
var game: GameDictionary? {
didSet {
self.m_Client.GetMediaImage(game?.Image) { (image) in
self.imageView.image = image
}
self.selectedIndex = 0
self.answerChars = (game?.Content?.map { String($0).lowercased() })!
self.views = (0..<self.answerChars.count).map { _ in UITextField() }
var index: NSInteger = TEXT_FIELD_TAG
for textField in self.views {
textField.backgroundColor = .white
textField.textColor = Constants.MAIN_THEME_COLOR
textField.font = UIFont(name: Constants.GAME_FONT_TYPE_1, size: scaledFontSize(fontSize: 36))
textField.textAlignment = .center
textField.delegate = self
textField.layer.cornerRadius = 10
textField.autocapitalizationType = .none
textField.tag = index
textField.isUserInteractionEnabled = true
// Create the cursor object and animate the blinking
let cursor = UIView(frame: CGRect(x: scaledViewSize(viewSize: 10), y: scaledViewSize(viewSize: 10), width: 2, height: scaledViewSize(viewSize: 30)))
cursor.backgroundColor = Constants.MAIN_THEME_COLOR
cursor.isHidden = true
UIView.animate(withDuration: 1, delay: 0, options: .repeat, animations: {() -> Void in
cursor.alpha = 0
}, completion: {(_ animated: Bool) -> Void in
cursor.alpha = 1
}
)
textField.addSubview(cursor)
self.container.addArrangedSubview(textField)
index+=1
}
}
}
// MARK: Question handlers
override func prepareForReuse() {
super.prepareForReuse()
inputArray.removeAll()
answerChars.removeAll()
self.selectedIndex = 0
for view in self.views {
view.removeFromSuperview()
}
self.views.removeAll()
}
override func onFocus() {
self.resetCursorTextField()
keyboardLauncher.onKeyPressed = { character in
self.views[self.selectedIndex].text = character
self.textChanged(sender: self.views[self.selectedIndex])
}
keyboardLauncher.showKeyboard()
keyboardLauncher.enableKeyboard()
}
func resetCursorTextField() {
for (index, textfield) in self.views.enumerated() {
textfield.subviews[CURSOR_INDEX].isHidden = (index == 0) ? false : true
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return true }
let newLength = text.count + string.count - range.length
return newLength <= 1
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return false
}
func resetInputViews() {
inputArray.removeAll()
self.selectedIndex = 0
self.resetCursorTextField()
for tag in TEXT_FIELD_TAG...(TEXT_FIELD_TAG + (self.answerChars.count) - 1) {
let textField = self.container.viewWithTag(tag) as! UITextField!
textField?.text = ""
if (tag == TEXT_FIELD_TAG) {
textField?.becomeFirstResponder()
}
}
}
func textChanged(sender: UITextField) {
if (sender.text?.count)! > 0 {
inputArray.append((sender.text?.lowercased())!)
if self.answerChars[self.selectedIndex] != inputArray.last! {
self.delegate?.updateAnswer(inputArray.joined(), false)
self.resetInputViews()
} else {
self.selectedIndex+=1
let nextField = sender.superview?.viewWithTag(sender.tag + 1) as! UITextField!
nextField?.becomeFirstResponder()
sender.subviews[CURSOR_INDEX].isHidden = true
nextField?.subviews[CURSOR_INDEX].isHidden = false
if self.answerChars.count == inputArray.count {
playCorrectAnswer()
isCompleted = true
keyboardLauncher.disableKeyboard()
self.textField?.isEnabled = false
self.delegate?.updateAnswer(inputArray.joined(), true)
self.delegate?.nextQuestion(self)
}
}
}
}
CellForItemAtIndexpath
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == self.m_Collection?.count {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: gameScoreOverviewCell, for: indexPath) as! GameScoreOverviewCell
cell.closeViewButton.addTarget(self, action: #selector(closeView(sender:)), for: .touchUpInside)
cell.rulesButton.addTarget(self, action: #selector(handleAlertScoreOverview(sender:)), for: .touchUpInside)
return cell
}
let game = self.m_Collection![indexPath.item]
if game.StarryGrade == StarryGrade.Grade4 && game.Level == Level.Level1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: fourthGradeCellLevel1, for: indexPath) as! GameCellFourthGradeLevel1
cell.cancelButton.addTarget(self, action: #selector(closeViewWithAlert(sender:)), for: .touchUpInside)
cell.questionLabel.text = "Question \(indexPath.item + 1) of \(self.m_Collection!.count)"
cell.game = game
cell.delegate = self
keyBoardIsVisible = true
return cell
}
}
This may not be a programmatic issue, but an interface issue. Try the following:
Ensure that you are compiling the app for Debug
Recreate the scenario in the left image below, where you have typed some letters and they are hidden:
Click this button in the Debug Navigator:
Select View UI Hierarchy.
In the deconstruction of your UI in Xcode, click on a text field that contains a hidden character.
You should now check a few things:
in the Object Inspector in the right panel, check to make sure that the Text Field's Title attribute is the character you typed (if not, the Text Field isn't capturing the keyboard input)
also in the Object Inspector, make sure that the View Visibility attribute is set to Not Hidden
in the Size Inspector (also in the right panel), check to make sure that the Text Field is big enough to show the character
I've had a similar issue before, and these steps allowed me to determine the cause of the problem. I hope this helps.

How to filter table view cells with a UISegmentedControl in Swift?

I've already searched this before asking the question but I didn't find what I need.
I'm building this app where the user puts a task (not going to the app store, just for me and some friends), and the task has a category. For example: school, home, friends, etc. When the user is going to add a new task, there are 2 text fields, the description text field and the category text field. I'm using a UIPickerView so the user picks a category, then, after creating the new task, it will add the category to an array I've created called "categories".
I want to put an UISegmentedControl on top of the table view with the sections:
All - School - Home - Friends
If all is selected, it will show all the cells with no filtering. If not, it will show the cell(s) with the corresponding categories.
I've read that I need to create table view sections to each category, but this would change my code a lot, and I don't even have an idea of how to work with multiple table view sections, I've tried once but it kept repeating the cells of one section in the second.
So how can I filter the cells per category?
Can I just put for example this? :
if //code to check in which section the picker is here {
if let schoolCell = cell.categories[indexPath.row] == "School" {
schoolCell.hidden = true
}
}
Please help me!!!
EDIT:
I have this code by now:
if filterSegmentedControl.selectedSegmentIndex == 1 {
if categories[indexPath.row] == "School" {
}
}
I just don't know where to go from here. How do I recognize and hide the cells?
It seems to me that you may want to take a simpler approach first and get something working. Set up your ViewController and add a tableView and two(2) arrays for your table data. One would be for home and the other for work. Yes, I know this is simple but if you get it working, then you can build on it.
Add a variable to track which data you are displaying.
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var tableView: UITableView!
// You would set this to 0, 1 or 2 for home, work and all.
var dataFilter = 0
// Data for work tasks
var tableDataWork : [String] = ["Proposal", "Send mail", "Fix printer", "Send payroll", "Pay rent"]
// Data for home tasks
var tableDataHome : [String] = ["Car payment", "Mow lawn", "Carpet clean"]
Add these functions for the segmented control.
#IBAction func segmentedControlAction(sender: AnyObject) {
switch segmentedControl.selectedSegmentIndex {
case 0:
print("Home")
dataFilter = 0
case 1:
print("Work")
dataFilter = 1
case 2:
print("All")
dataFilter = 2
default:
print("All")
dataFilter = 2
}
reload()
}
func reload() {
dispatch_async( dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("task-cell")
var title: String?
switch dataFilter {
case 0:
title = tableDataHome[indexPath.row]
case 1:
title = tableDataWork[indexPath.row]
case 2:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row - tableDataWork.count]
}
default:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row + tableDataWork.count]
}
}
cell?.textLabel?.text = title
if cell != nil {
return cell!
}
return UITableViewCell()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If
switch dataFilter {
case 0: return tableDataHome.count
case 1: return tableDataWork.count
default: return tableDataHome.count + tableDataWork.count
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1;
}
You can find the entire project here: https://github.com/ryantxr/segmented-control-app
It depends on your tableview.
If you use NSFetchedResultsController then you need to modify your fetch request. If you use an array directly, just use the filter function in Swift, passing in the condition, e.g. filteredArray = array.filter{$0.isAudioFile} Then, after setting your datasource array to the filtered one, call reloadData on your tableview.
You will need to keep a reference to the full array, and use the filtered one as your datasource in cellForRow...

Print the NSTableView's row number of the row clicked by the user

I have a NSTableView with one column. I would like to print the row number of the row that the user has clicked on. I am not sure where I should start with this. Is there a method for this?
You can use the selectedRowIndexes property from the tableView in the tableViewSelectionDidChange method in your NSTableView delegate.
In this example, the tableView allows multiple selection.
Swift 3
func tableViewSelectionDidChange(_ notification: Notification) {
if let myTable = notification.object as? NSTableView {
// we create an [Int] array from the index set
let selected = myTable.selectedRowIndexes.map { Int($0) }
print(selected)
}
}
Swift 2
func tableViewSelectionDidChange(notification: NSNotification) {
var mySelectedRows = [Int]()
let myTableViewFromNotification = notification.object as! NSTableView
let indexes = myTableViewFromNotification.selectedRowIndexes
// we iterate over the indexes using `.indexGreaterThanIndex`
var index = indexes.firstIndex
while index != NSNotFound {
mySelectedRows.append(index)
index = indexes.indexGreaterThanIndex(index)
}
print(mySelectedRows)
}
Use -selectedRowIndexes
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSTableView_Class/#//apple_ref/occ/instp/NSTableView/selectedRowIndexes
Then you can use those indexes to grab the data from your dataSource
(typically an array)