How to refresh single section header's text in UITableView - ios

I'm using a table view to display a checklist. Some views group list items by type. For these I display the type name with the item count in parentheses (e.g. "Things to do today (58)").
I want to refresh that string to reflect the updated count when the user deletes or completes an item.
This can be accomplished easily by setting the header's textLabel property and calling setNeedsLayout(). Problem is, it changes the appearance of the text.
Before (all caps default section title font, e.g. "THINGS TO DO TODAY (58)"):
screenshot - before
After (no longer all caps, e.g. "Things to do today (58)"):
screenshot - after
Here's the code I'm using currently to refresh the header:
func refreshHeaderTitleForSection(_ section: Int) {
if let header = tableView.headerView(forSection: section) {
header.textLabel?.text = getHeaderTitleForSection(section)
header.setNeedsLayout()
} else {
return
}
}
I'm calling getHeaderTitleForSection() to get the string for titleForHeaderInSection when the table view loads, so it should be the same. Is there a way to tell it to keep the default formatting without defining a custom header view?

Presumably, your getHeaderTitleForSection method returns the non-uppercase string. UITableView automatically shows section headers in all uppercase regardless of what you return from titleForHeaderInSection.
The easiest solution is for you to force all uppercase when you update your header view.
header.textLabel?.text = getHeaderTitleForSection(section).uppercased()

Related

Swift combine different type into one array to display data for UITableView

I have tableview where I need to show few sections. You should imagine this table like a playlist of your songs. In the top first section I need to display a cell with button which will add more songs to the playlist and other sections of tableview are header titles of Music category (like pop, rock and etc). Each of these sections contains cells which are songs names.
I have an array of songs called like this: var songsGroups = [SongGroup]. Which is actually my datasource.
SongGroup contains few properties:
var categoryName: String
var songs: [Songs]
But the problem appears on the next level. I every time need to check indexPath.section and do like this:
if indexPath.section == 0 {
// this is a section for ADD NEW SONG BUTTON cell no need in header title as there is no data repression only static text on the cell.
} else {
var musicCategoryName = songsGroups[indexPath.seciton - 1]. categoryName
headerTitle.title = musicCategoryName
}
As you see my code became magical by adding this cool -1 magical number. Which I replay don't love at all.
As an idea for sure I can try to combine my ADD NEW SONG BUTTON section (by adding some additional object) with songsGroups array and create NSArray for this purposes. Like in Objective-C as you remember. So then my datasource array will looks like this:
some NSArray = ["empty data for first cell", songsGroups[0], songsGroups[1]... etc]
So then there is no need to check any sections we can trust our array to build everything and even if I will add more empty data cells there is no need for me to handle my code via if block and adding tons of magical numbers.
But the issue I see here that we don't use explicit types of array and it's upset.
So maybe you know more beautiful solutions how to resolve my issue.
You can introduce a helper enum:
enum Section {
case empty
case songCategory(categoryName: String, songs: [String])
}
Your data source would then look something like this:
let datasource: [Section] = [.empty, .songCategory(categoryName: "Category1", songs: ["Song 1", "Song2"])]
So now you can use pattern matching to fill the table view:
let section = datasource[indexPath.section]
if case let .songCategory(categoryName, songs) = section {
headerTitle.title = categoryName
} else {
// this is a section for ADD NEW SONG BUTTON cell no need in header title as there is no data repression only static text on the cell.
}
I am not sure if I understand you right. But is seems to me that you want to display
1) something that lets the user add a new song by tapping a button, and
2) a table of songs, sectioned into groups.
If this is the case, why don’t you put the add new song button in the table header view, and all your song groups and songs in a 2-dim array used as your dataSource?

Switch calls multiple cases one after another when checking table view tag

I have a collection view with three different cells. Each of the cells contains a table view. So, there are three table views. I've set tags for each of them (from 1 to 3).
Now, on my view controller (I set it as the table view's data source when I dequeue collection view's cells) I call table view's data source method for the number of rows. To distinguish table views I check each one's tag. Here is the code for that:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch tableView.tag {
case 1:
if unitItems1 != nil {
return unitItems1!.count
} else {
return 0
}
case 2:
if unitItems2 != nil {
return unitItems2!.count
} else {
return 4
}
case 3:
if unitItems3 != nil {
return unitItems3!.count
} else {
return 4
}
default:
return 0
}
}
The problem is, when the first cell of the collection view is shown (with the first table view) it works fine. But when I scroll to the second cell, BOTH case 2 and case 3 get executed. After that, the second table view shows data as expected, but when I scroll to the third one, the method doesn't get called.
I can't figure out why two case statements get called one after another, while everything works fine for the first cell. If you have any ideas why this happens (or maybe, you could suggested a better way of checking table view's), I would appreciate your help.
Actually, the solution is quite simple. The reason of the problem was that collectionView's data source method was dequeueing all the cells one after another, even when they weren't on the screen. Consequently, tableView's inside of each cell were getting set, too. So, their data source method was getting called, hence the problem.
UICollectionView has a property called isPrefetchingEnabled. According to the documentation it denotes whether cells and data prefetching is enabled.
The documentation says:
When true, the collection view requests cells in advance of when they will be displayed, spreading the rendering over multiple layout passes. When false, the cells are requested as they are needed for display, often with multiple cells being requested in the same render loop. Setting this property to false also disables data prefetching. The default value of this property is true.
So, to solve the problem, described in the question, I set it to false as soon as my collectionView gets set.

Simultaneously change display parameters on all table view cells

I am trying to implement a table view design where a user can click a button outside of a table view cell and the display mode of all the buttons should change. However this is not the 'selected' mode for a given cell (that will be yet a third state that becomes accessible via switching to this second state). What's the proper way to accomplish this?
I am using dequeueReusableCellWith so I don't want to simply cycle through every cell because some that are out of sight probably shouldn't be modified. I simply want any cell that is visible, or becomes visible, while the table view cell is in this second display mode to follow a second design rather than the first design.
The second design, for now, is being modified via a method I added to a subclass of UITableViewCell like so:
- (void) p_refreshDisplay {
if (self.editing) {
self.buttonToClearWidth.constant = 20;
self.buttonToClearLeadingWidth.constant = 20;
} else {
self.buttonToClearWidth.constant = 0;
self.buttonToClearLeadingWidth.constant = 0;
}
}
However, I'm not sure how to trigger this p_refreshDisplay for every visible (and to become visible) cell. It seems unwise to call this many times and refresh the table view. What would be the proper way to accomplish what I want to do?
You do what should be done for any table view change:
Update your data model or some flag as needed.
Either call reloadData on the table view or call reloadRowsAtIndexPaths:withRowAnimation: passing in indexPathsForVisibleRows as the list of rows to reload.
Implement cellForRowAtIndexPath to provide appropriate cells for the given data/flags.
It sounds like you should have a custom cell class that has one or more properties that can be set on the cell in cellForRowAtIndexPath so the cell can render itself properly based on the specified state.
You can achieve this by doing three things:
Establish some property that indicates the "mode" of the table, either a boolean or perhaps an enum if there are more than three states
Ensure that cellForRowAtIndexPath configures the cell appropriately based on the value of this property. This will ensure that newly displayed cells are configured correctly.
When the "mode" changes you can use the tableview's visibleCells property to update any currently visible cells:
for cell in tableview.visibleCells {
if let myCell = cell as? MyCustomCellClass {
myCell.setButtonStyle()
}
}

Swift 2.0, UITableView: cellForRowAtIndexPath returning nil for non-visible cells

Please don't mark this as a duplicate question because I have found no suitable answer for my query.
I have a table view with cells that contain text fields. I have a button at the bottom of the screen. The number of rows is greater than the screen can display, so some cells are not in view. I want that at any point when the button is pressed, all textfields be read and the text input be processed. I am unable to do so because apparently because of the reusability of cells, cells out of view do not exist at all and cellForRowAtIndexPath for those indexPaths gives a runtime error. I have read answers that say that this simply can't be done. But I find it hard to believe that there is no workaround to this. Please help. Thanks in advance!
This definitely can't shouldn't be done (accessing cells that are off screen, or implementing workarounds to allow it), for reasons of (at least) performance and memory usage.
Having said that there is, as you put it, a workaround. What you will need to do it change how you are storing the values in those text fields. Rather than iterating through cells and retrieving the text directly from the text fields you should store the text for each field in an collection.
Define a dictionary property on your table view controller / data source to hold the text for each row.
Act as the delegate of UITextField and assign as such in tableView:cellForRowAtIndexPath:
Implement textField:didEndEditing: (or whatever is appropriate for your use case) and store the text in the dictionary keyed against the index path relating to the cell that contains that text field.
Update the button action method to use this dictionary instead of iterating through cells.
Create a UITableViewCell subclass, add your tableViewCells a index property and introduce a delegate like:
protocol CustomCellDelegate {
func didEditTextField(test : String, atIndex : Int)
}
Create a delegate variable on your UITableViewCell subclass.
var delegate : CustomCellDelegate?
Implement the UITextViewDelegate in your tableViewCell and set the cell to be the delegate of your textfield.
Implement these two methods:
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField : UITextField) {
// call back with the delegate here
// delegate?.didEditTextField(textfield.text, atIndex: self.index)
}
So when the user ends editing the textField, your cell will call out with a delegate, sending the text and the index.
On your viewController, when you init your tableViewCell, set the delegate of the cell to the viewController, and set the index to indexPath.row .
Set up on your viewController a String array with as many items as many tableViewCells you got. Init the array with empty strings.
So you got the delegate on your viewController, and whenever it is called, you just insert the returned text to right index in the string array on your viewcontroller.
What do think, will this work?
If we can assume that cells that have NEVER been created has no text inputs, then create an Array or Set ourselves and clone the content/input texts there whenever user inputs to a cell's textfield.
Whenever, that button is clicked, you can iterate the array instead of cells to get all the inputs.
This is a bit hacky though..

Converting NSIndexPath to NSInteger in Swift

I have a UITableView with a segue that connects to a view controller. When the user taps on a cell, I would like the indexPath.row to be passed to that view controller so I can update the content in the view based on which cell has been tapped.
I am aware of the different methods of doing this, however I would like to use NSUserDefaults() for my particular use. The problem is that I don't know how to convert the indexPath.row into an integer to be saved into NSUserDefaults(). This is what I have tried:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
println(indexPath.row)
NSUserDefaults.standardUserDefaults().setObject(NSInteger(indexPath.row), forKey: "index")
self.performSegueWithIdentifier("event", sender: self)
}
Second View Controller:
override func viewDidAppear(animated: Bool) {
println(NSUserDefaults.standardUserDefaults().integerForKey("event"))
}
At the moment, when I try to print out the indexPath.row in the next view controller, it always just returns 0.
You aren't using the same key
NSUserDefaults.standardUserDefaults().setObject(NSInteger(indexPath.row), forKey: "index") // <--- INDEX
NSUserDefaults.standardUserDefaults().integerForKey("event")) // <-- EVENT
Also you :
Might want to save it / read it with the equivalent method setInteger:forKey: and integerForKey:
Don't have to convert the value (it's already an integer) ;
Make sure you don't paint yourself in a corner. An NSIndexPath contains two numbers, not one: A section and a row within that section. In trivial cases when a table view has only one section the section number will always be zero, but you will get more complicated cases.
If you want to store index paths, I would add a category to NSUserDefaults with one method that turns an NSIndexPath into a dictionary with keys section and row and writes that, and one that reads a dictionary and creates an NSIndexPath with the values for section and row.
That said, a row number is something horribly unstable. Your UI designers decide that two rows have to change their order, and all your preferences are broken. What I do is one enum that identifies the purpose of each row; that enum must never, ever, ever change its values, and that is stored in the preference. And then there is another enum that refers to items in the UI, and that can change freely.
Saving A Row(integer) Value :
NSUserDefaults.standardUserDefaults().setInteger(indexPath.row, forKey: "row")
NSUserDefaults.standardUserDefaults().synchronize()
Loading A Row(integer) Value:
row() -> Int
var row = NSUserDefaults.standardUserDefaults().integerForKey("row")

Resources