TableView Index out of range issue - swift

My view controller opens a directory, counts the file types therein and stores the results in a dictionary [String:Int] of filetypes and count. I have a TableView that displays this.
The first time I open a directory the ViewController correctly displays the information in the FileTypeTableview. If I try to open another directory, execution gets to the line:
let dialogButton = dialog.runModal()
Then immediately jumps to a line in my TableView function and throws an index out of range here:
val = types[row]
Types is a [String] and shows 0 elements and row is an Int and shows 0.
I'm confused by the fact this happens at the dialog.runModal() function call.
Below are the functions that get the directory and display the TableViews.
I'm very new to Swift and MacOS programing and I'd appreciate any insights.
#IBAction func getFolder(_ sender: Any) {
let dialog = NSOpenPanel()
dialog.canChooseDirectories = true
dialog.canChooseFiles = false
allFiles = []
fileTypesDict.removeAll()
let dialogButton = dialog.runModal()
if let theURL = dialog.url {
theDirectory = theURL.path
allFilesAsPaths = getAllFiles2(atDirectoryPath: theURL.path)
allFilesAsPaths.sort()
}
fileCountLabel.stringValue = String(allFilesAsPaths.count)
mainTableView2.reloadData()
fileTypeTableView.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
var numberOfRows : Int = 0
if tableView == mainTableView2 {
print("number of rows for mainTableView2")
numberOfRows = allFilesAsPaths.count
} else if tableView == fileTypeTableView {
print("number of rows for fileTypeTableView")
numberOfRows = fileTypesDict.count
} else if tableView == exifTableView {
numberOfRows = exifData.count
print("exifData.count = \(exifData.count)")
print("number of rows for exifTableview \(numberOfRows)")
}
return numberOfRows
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
var val : String = ""
if tableView == mainTableView2 {
val = allFilesAsPaths[row]
}
if tableView == exifTableView {
if tableColumn?.identifier.rawValue == "tag" {
let tagArray : [String] = Array(exifData.keys)
val = tagArray[row]
} else if tableColumn?.identifier.rawValue == "value" {
let valueArray : [String] = Array(exifData.values)
val = valueArray[row]
}
}
if tableView == fileTypeTableView {
let types : [String] = Array(fileTypesDict.keys)
let counts : [Int] = Array(fileTypesDict.values)
if tableColumn?.identifier.rawValue == "type" {
val = types[row]
} else {
val = String(counts[row])
}
}
return val
}

I think you should try to remove
allFiles = []
fileTypesDict.removeAll()
And simply set all your arrays at once after dialog validation. Because it looks like presenting the dialog triggers an update of the table, but at this time, your objects are not sync because you do some work before, and after.
BTW, it would be clearer if you do separate table classes than dealing with if tableView == ... {}. You take the risk that your code gradually become inextricable. I've been there already ;)

Related

How do I filter entire object in searchBar

In my Xcode project I have a tableView with a list of data. I have implemented a searchBar to filter user input. I would like it to filter my entire object var contactArray = [ExternalAppContactsBook]() but for some reason I'm only allowed to filter its properties, like contactArray.firstName. Here's what it looks like in searchbar function:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text?.count == 0 {
isFiltered = false
}else {
print("HELLO")
isFiltered = true
filteredName = contactArray.filter({$0.firstName.lowercased().prefix(searchText.count) == searchText})
}
tableViewOutlet.reloadData()
}
So, instead of $0.firstName I would like to have $0.prefix but the compiler says that there is no such property.
With $0.firstName I can only search first names on the list which is pretty limiting... I'm going about this the right way or what should I do?
Edit
ExternalAppContactsBook look like this:
class ExternalAppContactsBook {
var firstName = ""
var lastName = ""
var phoneNumber = ""
var company = ""
}
I have rewritten my code, but I run into the same problem. This is what I've got now in searchBar textDidChange:
filteredName = contactArray.filter({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.firstName.contains(text)
})
This works, but it only works when I search on the first name of the people on the list. I want to be able to search their last names as well.
So I tried adding another one:
filteredLastName = contactArray.filter({ object -> Bool in
guard let text = searchBar.text else {return false}
return object.lastName.contains(text)
})
Then in cellForRowAt_
if isFiltered {
contactName = filteredName[indexPath.row].firstName
contactLastName = filteredLastName[indexPath.row].lastName
...}
But this obviously doesn't work and provide a problem with how many rows that should be returned in numberOfRowsInSection
You could simply use a condition with OR (||) when filtering
let filteredName = contactArray.filter {$0.firstName.starts(with: searchStr) || $0.lastName.starts(with: searchStr)}
You could also add this as a function to your class
func isMatch(_ searchString: String) -> Bool {
return firstName.starts(with: searchString) || lastName.starts(with: searchString)
}
And then filter
let filteredName = contactArray.filter {$0.isMatch(searchStr)}

Updating table view cells swift

BACKGROUND
I have a simple bill splitting app that allows the users to assign a meal item to multiple people or users. When the meal is assigned to multiple people, the price is divided accordingly. The meal (which contains an item name and a price are the rows and the users are the sections.
When I delete a row, I want to delete the row, and update or alter certain other values (I basically want to reassign the price to one less person when an item is deleted from a user). My data model is a multidimensional array. Here it is:
struct UserModel {
var name: String
var itemModels = [ItemModel]()
init(name: String, itemModels: [ItemModel]? = nil) {
self.name = name
if let itemModels = itemModels {
self.itemModels = itemModels
}
}
}
struct ItemModel {
var itemName: String
var price: Double
init(itemName: String, price: Double) {
self.itemName = itemName
self.price = price
}
}
class Data {
static var userModels = [UserModel]()
static var itemModels = [ItemModel]()
}
For example, in trailingSwipeActionsConfigurationForRowAt
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (contextualAction, view, actionPerformed: #escaping (Bool) -> ()) in
let user = Data.userModels[indexPath.section].name
let item = Data.userModels[indexPath.section].itemModels[indexPath.row].itemName
let price = Data.userModels[indexPath.section].itemModels[indexPath.row].price
ItemModelFunctions.removeFromUser(from: indexPath.section, remove: indexPath.row)
var duplicate = Data.userModels.filter({$0.itemModels.contains(where: {$0.itemName == item})}).filter({$0.name != user})
for i in 0..<duplicate.count {
???
}
tableView.reloadData()
actionPerformed(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
The variable var duplicate returns an array of the other users who have the same item at the indexPath.row, but not the user(indexPath.section) who has the item. I know it sounds really confusing, but I can provide more code or print statements if needed.
Also in the for loop, I want to do something like this:
for i in 0..<duplicate.count {
let oldPrice = duplicate[i].price
let newPrice = oldPrice * duplicate.count
duplicate[i].price = newPrice
}
But I can't access the price. I believe need an indexPath.section and indexPath.row.
If you made it this far, thank you for taking the time. I feel like I need a nested loop, but I'm not sure how exactly to implement that. If there are any other easier ways to achieve this I'm open to any suggestions.
Thank you so much!
EDIT:
The marked answer worked! In case anyone else was having a similar issue this is what my final code looks like in the trailingSwipeActionsConfigurationForRowAt:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (contextualAction, view, actionPerformed: #escaping (Bool) -> ()) in
let item = Data.userModels[indexPath.section].itemModels[indexPath.row]
ItemModelFunctions.removeFromUser(from: indexPath.section, remove: indexPath.row)
let duplicate = Data.userModels.filter({$0.itemModels.contains(item)})
for i in 0..<Data.userModels.count {
if let idx = Data.userModels[i].itemModels.firstIndex(of: item) {
let oldPrice = Data.userModels[i].itemModels[idx].price
let newPrice = oldPrice * Double(duplicate.count+1)
Data.userModels[i].itemModels[idx].price = newPrice / Double(duplicate.count)
}
}
tableView.reloadData()
actionPerformed(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
Perhaps you can try this. Best of luck, please comment if it doesn't work. I am new to Swift :)
let actualItem = Data.userModels[indexPath.section].itemModels[indexPath.row]
for i in 0..<duplicate.count {
if let idx = duplicate[i].itemModels.firstIndex(of: actualItem) {
let oldPrice = duplicate[i].itemModels[idx].price
duplicate[i].itemModels[idx].price = oldPrice * duplicate.count
}
}

First TableView section title doesn't scroll Swift 4

I'm building the SalesViewControllerfor my app and it consists of a TableView showing all items found in a date range.
Itemis child of Order and it has category, date, itemId, itemName, priceattributes all String.
I finally succeed in displaying the result of itemFetchResultController properly divided in sections as I had wrong sortDescriptor. In configuring itemFetchResultController I want use category property from fetched Item entities to be the section displayed title in populated TableView. My goal is, dough I'm not sure it would be possible or how to achieve it, to only have 1 row per itemName in its section but know ho many of it have been found in fetch and use it to display sold value. It's the first time I use sections and it's all a bit confusing to me. I'm trying to follow Apple documentation sample code gives me a couple of errors in tableView's data source methods as you can see by commented out code. All other posts I found here on stack overflow are very old and in objective c so I don't find answers to my doubts.
So far TableViewgets populated correctly , but first section title doesn't move when scrolling.
Any Idea of what's causing this ?
As always many thanks.
Here is the code I'm using for itemFetchResultController :
lazy var itemFetchedResultController: NSFetchedResultsController<Item> = {
// first sortDescriptor filters the date range: possibly change date from String to dates in both function and CoreData and use "(date >= %#) AND (date <= %#)" instead of "BEGINSWITH" in predicate
let itemFetchRequest = NSFetchRequest<Item>(entityName: "Item")
itemFetchRequest.sortDescriptors = [NSSortDescriptor(key: "category", ascending: true)]
itemFetchRequest.predicate = NSPredicate(format: "order.user.name == %#", UserDetails.fullName!)
itemFetchRequest.predicate = NSPredicate(format: "date BEGINSWITH %#", dateToFetch)
let itemFetchedResultController = NSFetchedResultsController(fetchRequest: itemFetchRequest, managedObjectContext: context, sectionNameKeyPath: "category", cacheName: nil)
return itemFetchedResultController
}()
TableView data source:
func numberOfSections(in tableView: UITableView) -> Int {
// apple doc : trhrows an error : Initializer for conditional binding must have Optional type, not 'NSFetchedResultsController<Item>'
// if let frc = itemFetchedResultController {
// return frc.sections!.count
// }
// return 0
return itemFetchedResultController.sections!.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let sections = self.itemFetchedResultController.sections else {
print(" Error :no sections in fetchedResultController" )
return 0
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "statisticsCell", for: indexPath) as! StatisticsTableViewCell
cell.idInfoLabel.text = itemFetchedResultController.object(at: indexPath).itemId!
cell.nameInfoLabel.text = itemFetchedResultController.object(at: indexPath).itemName!
let item = itemFetchedResultController.object(at: indexPath).itemName!
let productRequest: NSFetchRequest<Product> = Product.fetchRequest()
productRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
productRequest.predicate = NSPredicate(format: "name == %#", item)
productRequest.fetchLimit = 1
do {
let fetch = try context.fetch(productRequest)
cell.productImageView.image = UIImage(data: (fetch[0].productImage! as Data))
cell.minimumStockInfoLabel.text = fetch[0].minimumStock
cell.soldQuantityInfoLabel.text = fetch[0].soldQuantity
} catch {
print("Error in fetching product for cell: \(error)")
}
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// guard let sections = self.itemFetchedResultController.sections else {
// print(" Error : no sections in itemsFetchedResultController " )
// return "empty"
// }
// let sectionInfo = sections[section]
// return sectionInfo.name
guard let sectionInfo = itemFetchedResultController.sections?[section] else {
return nil
}
return sectionInfo.name
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
// if let sectionIndexTitles: FetchedResultController.sectionIndexTitles = self.itemFetchedResultController.sectionIndexTitles {
// print(" Error : no sections in itemsFetchedResultController " )
// return [""]
// }
return itemFetchedResultController.sectionIndexTitles
}
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
// apple doc : trhrows an error : Initializer for conditional binding must have Optional type, not 'Int'
// guard let result = itemFetchedResultController.section(forSectionIndexTitle: title, at: index) else {
// fatalError("Unable to locate section for \(title) at index: \(index)")
// }
// return result
let result = itemFetchedResultController.section(forSectionIndexTitle: title, at: index)
return result
}
Set your UITableView style to grouped and your all section will scroll along with cells

Using type select on only one column with NSTableView in Swift

I'm working in XCode 9.3, Swift developing an application for MacOS.
I've created a table that currently has two columns. I want to use type select that only acts on one column so that when the user types the first few letters, it only selects within the first column and not in the 2nd column.
The Apple documentation has an example in Objective-C here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/RowSelection/RowSelection.html#//apple_ref/doc/uid/10000026i-CH6-SW1
but I can't seem to translate that into Swift. I've included the table code below. Any help at all would be greatly appreciated.
extension ViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return directoryItems?.count ?? 0
}
// Sorting.
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
guard let sortDescriptor = tableView.sortDescriptors.first else {
return
}
if let order = Directory.FileOrder(rawValue: sortDescriptor.key!) {
sortOrder = order
sortAscending = sortDescriptor.ascending
reloadFileList()
}
}
}
extension ViewController: NSTableViewDelegate {
fileprivate enum CellIdentifiers {
static let NameCell = "NameCellID"
static let TimeCell = "TimeCellID"
static let SizeCell = "SizeCellID"
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
var image: NSImage?
var text: String = ""
var cellIdentifier: String = ""
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .long
guard let item = directoryItems?[row] else {
return nil
}
if tableColumn == tableView.tableColumns[0] {
image = item.icon
text = item.name
cellIdentifier = CellIdentifiers.NameCell
} else if tableColumn == tableView.tableColumns[1] {
let asset = AVAsset(url: item.url as URL)
let audioTime = asset.duration
text = audioTime.durationText
cellIdentifier = CellIdentifiers.TimeCell
} else if tableColumn == tableView.tableColumns[2] {
text = item.isFolder ? "--" : sizeFormatter.string(fromByteCount: item.size)
cellIdentifier = CellIdentifiers.SizeCell
}
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView {
cell.textField?.stringValue = text
cell.imageView?.image = image ?? nil
return cell
}
return nil
}
}
Your problem is twofold:
First, you need to set an Identifier for each of your table columns. You can do this by selecting the column in Interface Builder, going to the Identity Inspector, and setting it to some string value:
Once you've done that, create some static properties to refer to these identifiers (this isn't strictly necessary, since you could of course just use rawValue to do a plain string comparison the Objective-C way, but the new Swift 4 identifiers are type-safe, and thus preferred). Do that like this:
extension ViewController: NSTableViewDelegate {
private struct TableColumns {
static let foo = NSUserInterfaceItemIdentifier("Foo")
static let bar = NSUserInterfaceItemIdentifier("Bar")
}
...
}
Now, you can use these identifiers to refer to your columns, using tableColumn(withIdentifier:) (or column(withIdentifier:) if you just want the column number). I recommend doing this everywhere you refer to them, including in your tableView(:viewFor:row:) method, since the way you're doing it now with tableView.tableColumns[0] and so forth depends on the order of the columns, and if the user reorders them, it may cause unexpected behavior. Using the identifier will make sure you're always looking at the column you think you're looking at.
Anyway, once you've got your identifiers set up, you can address the second problem: You're using the wrong delegate method. Instead of tableView(:shouldTypeSelectFor:searchString:), which is meant for catching these things at the event level (i.e., the user just pressed a key. Should I invoke the type select system at all?), use tableView(:typeSelectStringFor:row:). This method lets you return the string given to the type selection mechanism for each row/column combination in your table. For the ones you want type-select to ignore, just return nil; otherwise, return the string that you'd expect to have to type to select that particular row. So, something like:
func tableView(_ tableView: NSTableView, typeSelectStringFor tableColumn: NSTableColumn?, row: Int) -> String? {
if tableColumn?.identifier == TableColumns.foo {
return directoryItems?[row].name
} else {
return nil
}
}

UITableView with Segmented Controller not displaying data immediately - Swift

I have a Tab Bar Controller, and one of the two views from it is a TableViewController. The TableViewController has a SegmentedControl bar on the top of the table to switch between two datasets pulled from Firebase.
When I run the app, the table doesn't show any data straight away, only when I switch to the other segment of the SegmentControl bar. But, when I switch back, the data for the first segments loads.
I put in a breakpoint to see what was happening, and it was skipping over the code I wrote to pull the data from Firebase, so the arrays were empty upon initial loading, hence the lack of data. Yet, when I switch segments to the other option, the data appears.
I'm also trying to sort the data within the arrays, and this doesn't do anything at all because it runs when the arrays come back empty, so there is nothing to sort.
My code is:
class LeaderboardViewController: UITableViewController
{
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet var leaderboardTable: UITableView!
var countyRef = Database.database().reference().child("countyleaderboard")
var schoolRef = Database.database().reference().child("schoolleaderboard")
var refHandle: UInt!
var countyList = [County]()
var schoolList = [School]()
override func viewDidLoad()
{
super.viewDidLoad()
fetchData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
var sectionCount = 0
switch(segmentedControl.selectedSegmentIndex)
{
case 0:
sectionCount = countyList.count
break
case 1:
sectionCount = schoolList.count
break
default:
break
}
return sectionCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let boardCell = tableView.dequeueReusableCell(withIdentifier: "boardCell", for: indexPath)
switch(segmentedControl.selectedSegmentIndex)
{
case 0:
boardCell.textLabel!.text = "" + countyList[indexPath.row].name! + ": \(countyList[indexPath.row].score!)"
break
case 1:
boardCell.textLabel!.text = "" + schoolList[indexPath.row].name! + ": \(schoolList[indexPath.row].score!)"
break
default:
break
}
return boardCell
}
func fetchData()
{
schoolRef.observe(.childAdded, with:
{ snapshot in
if let dictionary = snapshot.value as? [String: AnyObject]
{
let school = School()
school.name = dictionary["name"] as! String
school.score = dictionary["score"] as! Float
self.schoolList.append(school)
}
}, withCancel: nil)
self.schoolList.sort(by: { $0.score > $1.score })
countyRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children
{
let snap = child as! DataSnapshot
let county = County()
county.name = snap.key as String
county.score = snap.value as! Float
self.countyList.append(county)
}
})
self.countyList.sort(by: { $0.score > $1.score })
DispatchQueue.main.async
{
self.leaderboardTable.reloadData()
}
}
#IBAction func segmentedControlChanged(_ sender: Any)
{
DispatchQueue.main.async
{
self.leaderboardTable.reloadData()
}
}
}
My questions are:
Why does the data not load straight away?
Where are the arrays coming from if they are not being populated with data on the first run? And why are they not being sorted if that code is directly below the code that populates them?
The dispatch block should be after the for loop and before the }).
It should be within the observe/observeSingleEvent. Also your sort code needs to be inside the same block. Like below:
countyRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children
{
let snap = child as! DataSnapshot
let county = County()
county.name = snap.key as String
county.score = snap.value as! Float
self.countyList.append(county)
}
self.countyList.sort(by: { $0.score > $1.score })
DispatchQueue.main.async
{
self.leaderboardTable.reloadData()
}
})