I have a pretty complicated table view setup and I resolved to use a block structure for creating and selecting the cells to simplify the future development and changes.
The structure I'm using looks like this:
var dataSource: [(
cells:[ (type: DetailSection, createCell: ((indexPath: NSIndexPath) -> UITableViewCell), selectCell: ((indexPath: NSIndexPath) -> ())?, value: Value?)],
sectionHeader: (Int -> UITableViewHeaderFooterView)?,
sectionFooter: (Int -> UITableViewHeaderFooterView)?
)] = []
I can then set up the table in a setup function and make my delegate methods fairly simple
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = dataSource[indexPath.section].cells[indexPath.row].createCell(indexPath:indexPath)
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource[section].cells.count
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return dataSource.count
}
I have made a similar setup before in another TVC
var otherVCDataSource: [[ (type: DetailSection, createCell: ((indexPath: NSIndexPath) -> UITableViewCell), selectCell: ((indexPath: NSIndexPath) -> ())?)]] = []
This solution has worked great.
The current dataSource with the sectionHead and footer however gives me a EXC_BAD_ACCESS every time I try to access the indexPath in one of the createCell blocks.
createCell: {
(indexPath) in
let cell:CompactExerciseCell = self.tableView.dequeueReusableCellWithIdentifier(self.compactExerciseCellName, forIndexPath:indexPath) as! CompactExerciseCell
cell.nameLabel.text = "\(indexPath.row)"
cell.layoutMargins = UIEdgeInsetsZero
return cell
}
The app always crashes on
self.tableView.dequeueReusableCellWithIdentifier(self.compactExerciseCellName, forIndexPath:indexPath)
What am I missing here? Why can't I access the indexPath in the new structure when it works fine in the old structure? What is different in the memory management between this tuple and the array?
UPDATE:
So I had a deadline to keep and I finally had to give up and rework the data structure.
My first attempt was to instead of sending the indexPath as a parameter send the row and section and rebuild an indexPath inside the block. This worked for everything inside the data structure but if I pushed another view controller on a cell click I got another extremely weird crash (some malloc error, which is strange as I use ARC) when dequeuing cells in the next VC.
I tried to dig around in this crash as well but there was no more time to spend on this so I had to move on to another solution.
Instead of this tuple-array [([],,)] I made two arrays; one for the cells and one for the headers and footers. This structure removed the problem of the indexPath crash but I still had the issue in the next VC that didn't stop crashing when dequeueing the cells.
The final solution, or workaround, was to access the cell creator and selector "safely" with this extension:
extension Array {
subscript (safe index: Int) -> Element? {
return indices ~= index ? self[index] : nil
}
}
basically the return statement in the tableView delegate functions then looks like this:
return dataSource[safe:indexPath.section]?[safe:indexPath.row]?.createCell?(indexPath: indexPath)
instead of
return dataSource[indexPath.section][indexPath.row].createCell?(indexPath: indexPath)
I can't see how it makes any difference to the next VC as the cell shouldn't even exist if there was an issue with executing nil or looking for non existing indexes in the data structure but this still solved the problem I was having with the dequeueing of cells in the next VC.
I still have no clue why the change of data structure and the safe extension for getting values from an array helps and if someone has any idea I would be happy to hear it but I can not at this time experiment more with the solution. My guess is that the safe access of the values reallocated the values somehow and stopped them from being released. Maybe the tuple kept the compiler from understanding that the values should be kept in memory or maybe I just have a ghost in my code somewhere. I hope one day I can go back and dig through it in more detail...
This is NOT an answer to the question but rather a workaround if someone ends up in this hole and has to get out:
First use this extension for array:
extension Array {
subscript (safe index: Int) -> Element? {
return indices ~= index ? self[index] : nil
}
}
And then in the table view delegate functions use the extension like this
let cell = dataSource[safe:indexPath.section]?[safe:indexPath.row]?.createCell?(indexPath: indexPath)
If this does not work remove the tuple from the data structure and you should have a working solution.
I wish you better luck with this issue than I had.
you have to register your tableview cell for particular cell idntifier in viewdidload.
eg.tableview.registerNib(UINib(nibName: "cell_nib_name", bundle: NSBundle.mainBundle()), forCellReuseIdentifier: "cell_identifier");
for deque cell
let cell:CompactExerciseCell = self.tableView.dequeueReusableCellWithIdentifier(self.compactExerciseCellName, forIndexPath:indexPath) as! CompactExerciseCell
like this.
Related
extension ActionSheetViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sheetActions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TableCellIds.ActionSheet.actionSheetTableCellIdentifier, for: indexPath) as! ActionsSheetCell
cell.actionCellLabel.text = "My cell content goes here"
return cell
}
}
Above code gives me 'Force Cast Violation: Force casts should be avoided. (force_cast)' error. How can I avoid it?
Some force-casts are unavoidable, especially when interacting with Objective C, which has a much more dynamic/loose type system.
In some cases like this, a force-cast would be self-explanatory. If it crashes, clearly you're either:
getting back nil (meaning there's no view with that reuse identifier),
or you're getting back the wrong type (meaning the cell exists, but you reconfigured its type).
In either case your app is critically mis-configured, and there's no much graceful recovery you can do besides fixing the bug in the first place.
For this particular context, I use a helper extension like this (it's for AppKit, but it's easy enough to adapt). It checks for the two conditions above, and renders more helpful error messages.
public extension NSTableView {
/// A helper function to help with type-casting the result of `makeView(wihtIdentifier:owner:)`
/// - Parameters:
/// - id: The `id` as you would pass to `makeView(wihtIdentifier:owner:)`
/// - owner: The `owner` as you would pass to `makeView(wihtIdentifier:owner:)`
/// - ofType: The type to which to cast the result of `makeView(wihtIdentifier:owner:)`
/// - Returns: The resulting view, casted to a `T`. It's not an optional, since that type error wouldn't really be recoverable
/// at runtime, anyway.
func makeView<T>(
withIdentifier id: NSUserInterfaceItemIdentifier,
owner: Any?,
ofType: T.Type
) -> T {
guard let view = self.makeView(withIdentifier: id, owner: owner) else {
fatalError("This \(type(of: self)) didn't have a column with identifier \"\(id.rawValue)\"")
}
guard let castedView = view as? T else {
fatalError("""
Found a view for identifier \"\(id.rawValue)\",
but it had type: \(type(of: view))
and not the expected type: \(T.self)
""")
}
return castedView
}
}
Honestly, after I got experienced enough with the NSTableView APIs, investigating these issues became second nature, and I don't find this extension as useful. Still, it could save some debugging and frustration for devs who are new the platform.
The force cast is actually correct in this situation.
The point here is that you really don't want to proceed if you can't do the cast, because you must return a real cell and if it's the wrong class, the app is broken and you have no cell, so crashing is fine.
But the linter doesn't realize that. The usual way to get this past the linter is to do a guard let with as?, along with a fatalError in the else. That has the same effect, and the linter will buy into it.
I really like the approach suggested by Alexander at https://stackoverflow.com/a/67222587/341994 - here's an iOS modification of it:
extension UITableView {
func dequeue<T:UITableViewCell>(withIdentifier id:String, for ip: IndexPath) -> T {
guard let cell = self.dequeueReusableCell(withIdentifier: id, for: ip) as? T else {
fatalError("could not cast cell")
}
return cell
}
}
So now you can say e.g.:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : MyTableViewCell = tableView.dequeue(withIdentifier: "cell", for: indexPath)
// ...
return cell
}
And everyone is happy including the linter. No forced unwraps anywhere and the cast is performed automatically thanks to the generic and the explicit type declaration.
As others have said, a force cast is appropriate in this case, because if it fails, it means you have a critical error in your source code.
To make SwiftLint accept the cast, you can surround the statement with comments as described in this issue in the SwiftLint repo:
// swiftlint:disable force_cast
let cell = tableView.dequeueReusableCell(withIdentifier: TableCellIds.ActionSheet.actionSheetTableCellIdentifier, for: indexPath) as! ActionsSheetCell
// swiftlint:enable force_cast
The right thing to do is: remove force_cast from swift lint’s configuration file. And be professional: only write force casts where you mean “unwrap or fatal error”. Having to “get around the linter” is a pointless waste of developer time.
I have been trying to change one of the cells in an NSTableView to a pull-down menu, but have been unsuccessful. I read the Apple developer documentation, but it doesn't give an example of how to use NSPopupButtonCell in a NSTableView. I searched forums, including here, and only found one somewhat relevant example, except that it was in objective-c, so it doesn't work for my swift app. Code for the table is here:
extension DeviceListViewController:NSTableViewDataSource, NSTableViewDelegate{
// get the number of rows for the table
func numberOfRows(in tableView: NSTableView) -> Int {
return homedevices.count
}
// use the data in the homedevices array to populate the table cells
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let result = tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
if tableColumn?.identifier == "ID" {
result.textField?.stringValue = homedevices[row].id
} else if tableColumn?.identifier == "Name" {
result.textField?.stringValue = homedevices[row].name
result.imageView?.image = homedevices[row].image
} else if tableColumn?.identifier == "Type" {
result.textField?.stringValue = homedevices[row].type
} else if tableColumn?.identifier == "Button" {
result.textField?.integerValue = homedevices[row].button
}
return result
}
// facilitates data sorting for the table columns
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
let dataArrayMutable = NSMutableArray(array: homedevices)
dataArrayMutable.sort(using: tableView.sortDescriptors)
homedevices = dataArrayMutable as! [HomeDevice]
tableView.reloadData()
}
}
I really just want to be able to allow pull-down selection to change the button assigned to a particular homedevice (a simple integer), instead of having to type a number into the textfield to edit this value. Unfortuantely, when I add the popupbuttoncell to my table in IB, all of the views for my table cells are removed. So I may need to create the table differently. But most of the things I have read about and tried have caused runtime errors or display an empty table.
EDIT:
Day 3:
Today I have been reading here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingViewTablesWithBindings/PopulatingView-TablesWithBindings.html
and many other places too, but I don't have rep to post any more links.
I have added a NSPopupButton in IB, but am not sure how to set the value. I tried result.objectValue = homedevices[row].button, but that does not work. I suppose that I need an array controller object. So then I tried creating an outlet for the object in my DeviceListViewController like #IBOutlet var buttonArrayController: NSArrayController! I guess that I now need to somehow find a way to connect the array controller to my homedevices array.
so I looked at example code here:
https://github.com/blishen/TableViewPopup
This is in objective-C, which is not a language I am using, but maybe if I keep looking at it at various times over the course of the week, I might figure out how to make a pull-down menu.
So I am continuing to work at this, with no solution currently.
This issue is solved, thanks to #vadian.
The button is inserted as NSPopUpButton object, rather than a NSPopUpButtonCell.
Then the cell gets its own custom class, which I called ButtonCellView as a subclass of NSTableCellView.
Then the created subclass can receive an outlet from the NSPopUpButton to the custom subclass. I can give this a selectedItem variable and create the menu here.
Then in the table view delegate, when making the table, I can just set the selectedItem of my ButtonCellView object to the value from my data array.
It works great!
I am trying to make an add friends list where the user selects multiple table view cells and a custom check appears for each selection. I originally used didSelectRowAtIndexPath, but this did not give me the results I am looking for since you can highlight multiple cells, but unless you unhighlight the original selected row you cannot select anymore. I then tried using didHighlighRowAtIndexPath, but this doesn't seem to work because now I am getting a nil value for my indexPath. Here is my code:
override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! AddedYouCell
let currentUser = PFUser.currentUser()?.username
let username = currentCell.Username.text
print(currentCell.Username.text)
let Friends = PFObject(className: "Friends");
Friends.setObject(username!, forKey: "To");
Friends.setObject(currentUser!, forKey: "From");
Friends.saveInBackgroundWithBlock { (success: Bool,error: NSError?) -> Void in
print("Friend has been added.");
currentCell.Added.image = UIImage(named: "checked.png")
}
}
How can I solve this? Thanks
I'm not going to write the code for you, but this should help you on your way:
To achieve your goal, you should separate the data from your views (cells).
Use an Array (i.e. friendList) to store your friend list and selected state of each of them, and use that Array to populate your tableView.
numberOfCellsForRow equals friendList.count
In didSelectRowAtIndexPath, use indexPath.row to change the state of your view (cell) and set the state for the same index in your Array
In cellForRowAtIndexpath, use indexPath.row to retrieve from the Array what the initial state of the cell should be.
I got a validation function that loop through my table view, the problem is that it return nil cell at some point.
for var section = 0; section < self.tableView.numberOfSections(); ++section {
for var row = 0; row < self.tableView.numberOfRowsInSection(section); ++row {
var indexPath = NSIndexPath(forRow: row, inSection: section)
if section > 0 {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCell
// cell is nil when self.tableView.numberOfRowsInSection(section) return 3 for row 1 and 2
// ... Other stuff
}
}
}
I'm not really sure what I'm doing wrong here, I try double checking the indexPath row and section and they are good, numberOfRowsInSection() return 3 but the row 1 and 2 return a nil cell... I can see my 3 cell in the UI too.
Anybody has an idea of what I'm doing wrong?
My function is called after some tableView.reloadData() and in viewDidLoad, is it possible that the tableview didn't finish reloading before my function is executed event though I didn't call it in a dispatch_async ??
In hope of an answer.
Thank in advance
--------------------------- Answer ------------------------
Additional explanation :
cellForRowAtIndexPath only return visible cell, validation should be done in data model. When the cell is constructed in
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
It should change itself according to the validation state.
As stated in the documentation, cellForRowAtIndexPath returns:
An object representing a cell of the table, or nil if the cell is not visible or indexPath is out of range.
Hence, unless your table is fully displayed, there are some off screen rows for which that method returns nil.
The reason why it returns nil for non visible cells is because they do not exist - the table reuses the same cells, to minimize memory usage - otherwise tables with a large number of rows would be impossible to manage.
So, to handle that error just do optional binding:
// Do your dataSource changes above
if let cell = tableView.cellForRow(at: indexPath) as? MyTableViewCell {
// yourCode
}
If the cell is visible your code got applied or otherwise, the desired Cell gets reloaded when getting in the visible part as dequeueReusableCell in the cellForRowAt method.
I too experienced the issue where cellForRowAtIndexPath was returning nil even though the cells were fully visible. In my case, I was calling the debug function (see below) in viewDidAppear() and I suspect the UITableView wasn't fully ready yet because part of the contents being printed were incomplete with nil cells.
This is how I got around it: in the viewController, I placed a button which would call the debug function:
public func printCellInfo() {
for (sectionindex, section) in sections.enumerated() {
for (rowIndex, _) in section.rows.enumerated() {
let cell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionindex))
let cellDescription = String(describing: cell.self)
let text = """
Section (\(sectionindex)) - Row (\(rowIndex)): \n
Cell: \(cellDescription)
Height:\(String(describing: cell?.bounds.height))\n
"""
print(text)
}
}
}
Please note that I'm using my own data structure: the data source is an array of sections, each of them containing an array of rows. You'll need to
adjust accordingly.
If my hypothesis is correct, you will be able to print the debug description of all visible cells. Please give it a try and let us know if it works.
I'm a beginner, clearly out of my league and I haven't been able to find an answer online.
I have a UITableViewController with a UITableViewshowing custom objects stored in one array. I don't show all the object of the array in one single section of said TableView: the TableView has multiple sections, each containing a filtered portion of my objects array (I filter the custom objects array checking that the object category property is equal to a category that I specified in a categories array).
This filtering and showing the single array in different sections is working fine (I understand that maybe it's not elegant, as I said I'm a beginner in coding and I absolutely needed to work with one single array, without creating other arrays corresponding to the filtered results), but to better understand my issue I think it's better that I show what I did, so here's the TableView part of my code:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return myCategoriesArray.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
for (var i = 0; i <= section; i++){
if section == i {
for eachCategory in myCategoriesArray {
return myObjectsArray!.filter() { $0.objectCategoryProperty == myCategoriesArray[i] }.count
}
}
}
// ...
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("objectCell", forIndexPath: indexPath) as UITableViewCell
for (var i = 0; i <= indexPath.section; i++){
if indexPath.section == i {
for eachCategory in myCategoriesArray {
cell.textLabel?.text = myObjectsArray!.filter() { $0.objectCategoryProperty == myCategoriesArray[i] }[indexPath.row].nameProperty
return cell
}
}
}
// ...
}
This works in the sense that I have the UITableViewController showing all my objects, but filtered in separated sections by category.
My issue is with the segue when I select a cell and show a detail view.
Here's my prepareForSegue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var nextVC = segue.destinationViewController as MyNextViewController
if let indexPath = tableView.indexPathForSelectedRow() {
let selected = myObjectsArray![indexPath.row]
nextVC.passedObject = selected
}
}
}
I'm sure that many of you already see my issue: the object that I pass to the next ViewController is selected in the custom objects array using as index [indexPath.row], but indexPath.row starts at 0 for each section, so when I select an object its index in the TableView is not equal to the index in the custom objects array, meaning that I pass the wrong object.
Now, I'm stuck because I don't see a way to pass the right (meaning, selected) object to the next View Controller while preserving the fact that I'm working with only a single array.
I was toying with the idea of adding an objectIDString property to every object and a single var currentlySelectedObjectIDString that is set every time a cell is selected and try to pass to the next View Controller the object with the objectIDString property matching the currentlySelectedObjectIDString, but it looks like a bad idea to my inexperienced eyes and I'm actually not sure how I could accomplish that even if I wanted to (maybe implementing didSelectRowAtIndexPath:, but I have not been able to make it work).
Any help would be really appreciated, I've been stuck on this for so long I begin to question a) my sanity b)every decision I made so far in the project (meaning, the single array for all objects that is filtered in sections), but I'm already so invested in it that I really would like not to have to start over.
Thank you,
Cesare
P.S. I hope my question is clear, english isn't my main language... sorry for any mistake!
I suggest you, to use a NSFetchedResultsController. This class have a property sectionNameKeyPath. In this property you could set your category and you won't need more iterate with a repetition in each numberOfSection and numberOfRows.
like this:
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: "event.startDate", cacheName: nil)
In my case i need filter data by event.startDate.
I don't know if you are using core data, but if you are using, this is the better way to do this.
I'll expose them for you!
In the first moment NSFetchedResultsController like complicated, but its very very useful. Don't be afraid.
I don't know exactly your model and data. In this case i'll show you my owner sample.
Please see my question in the following link:
Sectioning TableView and rows with Core Data Swift
In this link, see my question, and in the bottom i'll explain the complete solution with the others answer.
If this is not clear for you, please, talk with me :.)
I spent all day trying to figure out a way to solve my own question above and I think I've finally found a working-workaround.
My premise and disclaimer is that this is a pile of hacks, I post this only in case this might help someone in my situation in the future, but clearly the way to deal with this kind of situation is Core Data, as suggested by Weles' answer, not what I did.
Here's briefly what I've done to get my multi-component UITableView, in which all the data come from a single array of custom objects that is filtered by a different value in every component, to pass the selected object to the detail view when a cell is selected.
1) I added to all my customObjects an objectID : String computed property (current date + random number).
2) I added a var currentlySelectedObjectID : String? in my TableViewController.
3) I subclassed UITableViewCell, creating a CustomTableViewCell class that only adds to the normal class a var selectedCellID : String?, then I changed my cellForRowAtIndexPath to return a CustomTableViewCell instead of a UITableViewCell. Inside this method, before returning the cell, I also set the property selectedCellID of the cell equal to objectID of the current object. I also had to change the class of the cell in the Storyboard from UITableViewCell to CustomTableViewCell.
4) In the Storyboard I removed the segue from the cell to the detailViewController that was automatically created by Xcode and I set a custom StoryboardID to the detailViewController ("detailVC"),
5) Inside didSelectRowAtIndexPath of TableViewController I did all the work that before I was trying to do in prepareForSegue, but in a different way (not a segue, a self.navigationController?.pushViewController). Here's the code:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow();
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as CustomTableViewCell!;
self.currentlySelectedObjectID = currentCell.selectedCellID
// detailViewController instance
var detailVC = self.storyboard?.instantiateViewControllerWithIdentifier("detailVC") as MyDetailViewController
// I filter my objects array to "extract" the object with the objectID property equal to the currentlySelectedObjectID property (which is equal to the currentCell.selectedCellID, as set above). This array must have only 1 value. If so, I set the property passedCustomObject that I have in my detailViewController to the same object selected.
if (myObjectsArray!.filter() { $0.objectID == self.currentlySelectedObjectID }).count == 1 {
detailVC.passedCustomObject = (myObjectsArray!.filter() { $0.objectID == self.currentlySelectedObjectID })[0]
} else {
println("Error passing the object selected in the TableView to the DetailView")
}
// I push the detailViewController on top of the stack
self.navigationController?.pushViewController(detailVC, animated: true)
}
I think there are very good chance that a decent programmer (I am not one, but I hope to become one some day), seeing what I did, could faint.
Again, I don't think anyone should do this, if you're in my same situation go straight to Core Data: I spent a day on this, there's good chance that in three or four I could have had Core Data working.
But still, as hacked and inefficient as this is, it works... I tested multiple times. So, having spent so much time and having found no useful similar previous answers online, I thought to post mine.
Don't do this, I'm really afraid this is easily breakable! :)
I still look forward to other answers, to learn from my numerous mistakes!