I have a UITableView with four different custom UITableViewCells. I need to repeat each cell type as follows:
row 0 -> cell1
row 1 -> cell2
row 2 -> cell3
row 3 -> cell4
row 4 -> cell1
row 5 -> cell2
row 6 -> cell3
row 7 -> cell4
row 8 -> cell1
...
Any ideas how to achieve this?
To achieve this, just follow these steps (I assume you have made four prototype cells in the storyboard):
Open your storyboard, and select the first cell (be sure the UITableViewCell is selected, and not its content view)
On the right side of the storyboard, in the attributes inspector, type in a custom identifier to identify this cell in the box labelled reuse identifier (for this example, I'll name it cell1)
Repeat this for the other three cells (these subsequent cells will have the identifiers cell2, cell3, and cell4
Back in the UITableViewDataSource class, override the function tableView: cellForRowAtIndexPath.
In this function, get the indexPath.row's modulo (%) by 4. If this value is 0, then do tableView.dequeueReusableCellWithIdentifier("cell1") if it is 1, then replace it with cell2, and so on. In code, step 5 looks something like this -
switch indexPath.row % 4 {
case 0:
let cell = tableView.deqeueReusableCellWithIdentifier("cell1")
case 1:
let cell = tableView.deqeueReusableCellWithIdentifier("cell2")
case 2:
let cell = tableView.deqeueReusableCellWithIdentifier("cell3")
case 3:
let cell = tableView.deqeueReusableCellWithIdentifier("cell4")
}
return cell
Just put this code in the function mentioned in step 4, and you can build and run. Hope this works!
The cells are to be created in cellForRowAtIndexPath where the indexpath is passed as a parameter. With indexPath.row you can calculate what cell type needs to be used.
indexPath.row modulo 4 gives the cell number type.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellNumber = indexPath.row % 4
if cellNumber == 0 { // row 0, 4, 8, ...
// create cell1
}
else if cellNumber == 1 { // row 1, 5, 9, ...
// create cell2
}
else if cellNumber == 2 { // row 2, 6, 10, ...
// create cell3
}
else if cellNumber == 3 { // row 3, 7, 11, ...
// create cell4
}
}
Related
These are my structs:
struct Category{
var category_name = String()
var items = [Item]()
}
struct Item{
var rows = [Row]()
}
struct Row{
var size: Int
}
I have a menu object which is an array of Categories:
var menu = [Category]
I populate the menu, and have a structure like this:
-category1 // section 0, row 0
-item1 // section 0, row 1
-row1 // section 0, row 2
-row2 // section 0, row 3
-item2 // section 0, row 4
-item3 // section 0, row 5
-category2 // section 1, row 0
-item1 // section 1, row 1
-item2 // section 1, row 2
-row1 // section 1, row 3
-row2 // section 1, row 4
-row3 // section 1, row 5
-item3 // section 1, row 6
-row1 // section 1, row 7
I want to populate a UITableView with cells appropriate for the type of row in the structure, based on the index in the section.
So in the example above, section 0 row 0 = category1. So I should return a cell appropriate for a category heading. Section 1 row 5 = item2->row3, so I should return a subrow cell.
Row 0 will always equal a category cell, but for a given section index and row index, how can I determine the type of cell in the structure?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0{ // category cell type
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell_category") else {return UITableViewCell()}
cell.textLabel?.text = menu[indexPath.section].category_name
return cell
}else{// item cell type or subrow
// based on indexPath.section and indexPath.row,
// should we return an item cell or a subrow cell?
if ( ??? ){ // item cell type
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell_item") else {return UITableViewCell()}
return cell
}else{ // subrow cell type
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell_subrow") else {return UITableViewCell()}
return cell
}
}
So these are my expected values for the example above:
indexPath.section = 0
indexPath.row = 0
return cell type = cell_category
indexPath.section = 0
indexPath.row = 1
return cell type = cell_item
indexPath.section = 0
indexPath.row = 2
return cell type = cell_subrow
numberOfRowsInSection is returning the correct number of rows.
So how can I determine what type of cell to return?
I think I need to loop through the items and keep a counter, but I can't figure out a logical way to do so.
Off the top my head, this is what i have. Iterate through the array and identify what cell appears for each index path an store it. Let's say taking this example into account, we have an enum.
enum cellType {
case category
case item
case row
}
Now you build the array which has the cell type for each section.
var cells: [cellType] = []
for category in menu {
cells.append(.category)
if !category.items.isEmpty {
cells.append(.item)
for item in items {
if !category.items.rows.isEmpty {
for row in category.items.rows {
cells.append(.row)
}
}
}
}
}
Now use the cells array to find the type of cell to be dequeued.
This is a really ugly implementation, but it should work. Or atleast you'd know how to start.
Background
The story is come from the second example of the book IOS apprentice (Ed. 6 2016)
A UItableView with two section is created. In storyboard the following content had been designed: section 0 has one row fulfilled with one static cell, section 1 has two row full filled with two static cell.
What to achieve
When tap the last row of section 1 (ie, the dueDate row in picture A) a new cell with a UIDatePicker will be inserted into the tableView (please see picture B)
How does the author solve the problem
A UITableViewCell filled with a UIDatePicker is added in to the scene dock in storyBoard (please see picture C), the new tableCell will be added into the table when dueDate row is tapped
Table view with static cells does not have a data source and therefore does not use the function like “cellForRowAt”. In order to insert this cell into the table the following data source functions had been overridden
tableView(_:numberOfRowsInSection:)
tableView(_:cellForRowAt:)
tableView(_:heightForRowAt:)
tableView(_:indentationLevelForRowAt:)
Problem
the function tableView(_:indentationLevelForRowAt:) really confuse me a lot! And here is the full code
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
var newIndexPath = indexPath
if indexPath.section == 1 && indexPath.row == 2 {
newIndexPath = IndexPath(row: 0, section: indexPath.section)
}
return super.tableView(tableView, indentationLevelForRowAt: newIndexPath)
}
Question
What is the meaning of this function? What's the purpose of it? I've read the document, but I didn't get much information.
When the third row/datePicker cell is inserted (indexPath.section == 1 && indexPath.row == 2) why the function will indent the first row newIndexPath = IndexPath(row: 0, section: indexPath.section) ?
Implementing tableView(_:indentationLevelForRowAt:) allows you to display the table rows in a hierarchal / tree-view type format. As in:
Level 1
Sub-Level 1a
Sub-Level 1b
Level 2
Sub-Level 2a
Sub-sub-level 2aa
Sub-sub-level 2ab
etc...
Commonly, one might use:
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
if let myObj = myData[indexPath.row] as? MyObject {
return myObj.myIndentLevel
}
return 0
}
For the example you present, without reading the book...
It would appear the author decided that the new row (2) that contains the UIDatePicker should have the same indent level as the first row (0) in the section. If it's not the row 2, then return the default / current indentation level.
Although, the intent of
var newIndexPath = indexPath
if indexPath.section == 1 && indexPath.row == 2 {
newIndexPath = IndexPath(row: 0, section: indexPath.section)
}
return super.tableView(tableView, indentationLevelForRowAt: newIndexPath)
might seem a little more obvious / clear with
newIndexPath = IndexPath(row: 0, section: 1)
since it is already limiting this to section == 1.
In my Swift project I have a table controller view with a table view inside. The table view is divided into 4 section and every section has 4 rows. Currently, each row is formed by a label beside a text field.
My purpose is that only rows in the first section has label beside text field. On the contrary, I want the last 3 sections have only labels in their rows (and NOT text fields).
Please, help me.
That's the code I wrote to manage with this problem but it's not working:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("index ", indexPath.section);
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath)
cell.textLabel?.text = self.items[indexPath.section][indexPath.row];
if(indexPath.section == 0){
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell") as! TextInputTableViewCell
cell.configure("", placeholder: "name")
}
return cell
}
It's not working because you are using the same cell for all the rows. You need to define two different rows. You can do this by setting prototype cells to more than one row (two in your case).
Each cell must have its own reuse identifier and it must be unique within that table view.
Then in your tableView(cellForRowAtIndexPath:) you can ask:
if indexPath.section == 0 {
cell = tableView.dequeueReusableCellWithIdentifier("firstSectionCell", forIndexPath: indexPath)
//
} else {
cell = tableView.dequeueReusableCellWithIdentifier("otherSectionCell", forIndexPath: indexPath)
//
}
Also note that in Swift you do not need to use parenthesis in if-statement (nor for, while, etc). So I suggest you remove them as they are pointless.
It looks like your cell in if(indexPath.section == 0) block doesn't actually have scope outside that block so any properties set there won't get returned there. If you just want to remove the textField, but keep the label, You can just set the textField.hidden = true. Here is how I would go about it.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as! TextInputTableViewCell
cell.textLabel?.text = self.items[indexPath.section][indexPath.row];
if(indexPath.section == 0){
cell.textField.hidden = false //Assumes TextInputTableViewCell's textField property is called "textField"
cell.configure("", placeholder: "name")
} else {
cell.textField.hidden = true //Not in first section we will hide textField
}
return cell
}
Doing it this way you can use the same cell class for every cell in your tableView, but just hide what you want based on its section.
I have a UIViewController, and when a user clicks a button, I want to show a UITableView. I receive the data from the server.
The problem is that there is something weird happening. Sometimes, not all the cells are updated. In other words, in my prototype cell, there are two buttons, with default text, but when I load the data from server not all the buttons text are updated, instead when I scroll the table, they become updated. Also when I **scroll* the table, sometimes the buttons go back to the default text.
Here are my code: (really easy)
class CusinePreferencesTableView: NSObject, UITableViewDelegate, UITableViewDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentefiers.oneCusinePreferencesCell.rawValue, forIndexPath: indexPath) as! OneCusinePreferencesTableViewCell
var row = indexPath.row
print("row = \(row)")
let oneCusineDataLeft = Preferences2ViewController.cusines![row]
cell.leftButton.titleLabel?.text = oneCusineDataLeft
row = row + 1
if row < Preferences2ViewController.cusines!.count{
let oneCusineDataRight = Preferences2ViewController.cusines![row]
cell.rightButton.titleLabel?.text = oneCusineDataRight
}else {
//I should hide the right button
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let cusines = Preferences2ViewController.cusines {
if cusines.count % 2 == 0 {
return cusines.count/2
}else {
var count = cusines.count/2
count = count + 1
return count
}
}else {
return 0
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
}
And this is the table view (in case you want it)
If you see that you didn't get the description of my problem, I can make a video for you
Update 1
I already call the reloadData as you see here (where I get the data from the server)
func loadCusiens(){
let url = NSURL(string: ConstantData.getWebserviceFullAddress()+"preferences/cusines")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error ) in
if let error = error {
print("error = \(error)")
}
if let data = data {
do{
let jsonArray = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! NSArray
var results = [String]()
for oneJSON in jsonArray {
let name = oneJSON["name"] as! String
results.append(name)
}
dispatch_async(dispatch_get_main_queue(), {
Preferences2ViewController.cusines = results
self.cusineTableView.reloadData()
})
} catch{
print("This exception happened = \(error)")
}
}
})
task.resume()
}
Update
Now in the else part, i set the text of the right button to "sometext"
this is a video about the problem
http://www.mediafire.com/watch/k2113ovebdvj46d/IMG_0911.MOV
Update
The main problem is not cell reuse, nor the layout problem I explain below, but the use of:
cell.leftButton.titleLabel?.text = oneCusineDataLeft
to set the button text. You must use:
cell.leftButton.setTitle(oneCusineDataLeft, forState: .Normal)
instead. (In essence, a UIButton keeps track of the title text it should display when it is in different "states" - normal, selected, etc. Although the your code changes the text that is currently displayed, as soon as the button changes state, it sets the text back to the stored value. I assume the button state is reset when the cells are displayed. The setTitle method updates the stored value).
Original
Setting aside the cell reuse problem, I don't think your code will achieve quite what you want. As currently coded, the layout would be something like this:
Row 0: left: cuisine 0, right: cuisine 1
Row 1: left: cuisine 1, right: cuisine 2
Row 2: left: cuisine 2, right: cuisine 3
I'm guessing you actually want this:
Row 0: left: cuisine 0, right: cuisine 1
Row 1: left: cuisine 2, right: cuisine 3
Row 2: left: cuisine 4, right: cuisine 5
If that's the case, amend your code as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentefiers.oneCusinePreferencesCell.rawValue, forIndexPath: indexPath) as! OneCusinePreferencesTableViewCell
var row = indexPath.row
print("row = \(row)")
let oneCusineDataLeft = Preferences2ViewController.cusines![2*row]
cell.leftButton.titleLabel?.text = oneCusineDataLeft
if (2*row+1) < Preferences2ViewController.cusines!.count{
let oneCusineDataRight = Preferences2ViewController.cusines![2*row+1]
cell.rightButton.titleLabel?.text = oneCusineDataRight
}else {
//I should hide the right button
cell.rightButton.titleLabel?.text = ""
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let cusines = Preferences2ViewController.cusines {
if cusines.count % 2 == 0 {
return cusines.count/2
}else {
return (cusines.count+1)/2
}
}else {
return 0
}
}
I should also note that you would probably be better off using a UICollectionView rather than a UITableView, since the former will provide the multiple column flow much more easily and intuitively.
Looks like a reuse problem. Basically if you follow the examples in Apple and other places, it uses a dequeue function. What this means is that iOS will look for an already existing cell that is no longer being shown and give you that to draw the contents of a new cell. But of course if the old cell had a bunch of stuff set, you will see the old stuff. The solution is to always assign everything to the correct new value.
This can get you easily if your cells have variable information, for instance if there's a picture you set if a certain entry has it, but isn't set if it doesn't have it. What will happen then is when you get an entry with no picture your code might not set it the image to nil, and you'll see the picture of an old cell that was scrolled off.
Please let me explain the reuse of UITableViewCells works
Assume you have a very huge cells and you can see only 3 cells on you screen.
Assume now you see on your screen the following cells:
A
B
C
On the screen... and your datasource is: A B C C B A.
Now you are scrolling down for 1 cell exactly:
So the cell A is going from the Up (it was first cell) down and becoming your last visible cell
So now on the screen you will see
B
C
A // While you want to draw a cell `C` here according to the dataSource
What happens now in cellForRowAtIndexPath...
You are getting the same Cell A from the beginning.
Assume your cell A had only 1 button and a Cell C need to show 2 buttons
Because you have already draw the cell A you draw it with 1 button (lets assume you set the right button hidden property to YES).
So now you want to draw your cell C on the reused cell A
(Assume you set TAGS for the cell types to know what type of cell is popped up to you in the cellForRowAtIndexPath)
And now you can query the tag of the cell
if(myCell.tag == `A`/*(Assume you have an Enum for tags)*/)
{
//So here you will want to Unhide the Right button of the cell (which is currently still cell A
//After you have done with all needed adjustments you will want to set the appropriate tag for this cell to be ready for the next reuse -> so set it's tag to C
//Now next time this cell will get reused it will be drawn as cell `C` and it will think it is a cell `C`
}
indexPath.row is being incorrectly calculated on iPhone 6 but not on any other simulator as seen below. The iOS deployment target is set to 8.0. This calculation happens when you click on the 2nd tab in a segmented control from (and only from) the 4th tab.
This is where the app crashes:
func createCompletedCell(indexPath: NSIndexPath) -> CompletedCell {
var cell = tableView.dequeueReusableCellWithIdentifier(profileCompletionId) as! CompletedCell
println("indexPath.row: \(indexPath.row)")
println("indexPath.row-2: \(indexPath.row-2)")
var completion = switchState == 1 ? completionArr[indexPath.row-2] : favArr[indexPath.row-2] //crashes on this line
cell.setCompletedCell(completion)
return cell
}
Which was called from:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
var cell = tableView.dequeueReusableCellWithIdentifier("ProfileDetails", forIndexPath: indexPath) as! ProfileDetails
cell.selectionStyle = UITableViewCellSelectionStyle.None //No background color if cell is clicked
return cell
}
else if indexPath.row == 1 {
var cell = tableView.dequeueReusableCellWithIdentifier("SegControl", forIndexPath: indexPath) as! ProfileSegControl
cell.segControl.selectedSegmentIndex = switchState
cell.segControl.addTarget(self, action: Selector("changeState:"), forControlEvents: UIControlEvents.ValueChanged)
return cell
}
else {
if(switchState == 0 || switchState == 2){
return createBountyCell(indexPath)
}
else {
return createCompletedCell(indexPath)
}
}
}
When I print the indexPath.row variable on an iPhone 6 simulator when I click on the 4th segmented control tab and then the 2nd, I get the output:
(4th tab calcs)
indexPath.row: 2
indexPath.row-2: 0
indexPath.row: 3
indexPath.row-2: 1
(2nd tab calcs)
indexPath.row: 3
indexPath.row-2: 1
When I do the same but with iPhone 4s/5/5s/6plus
(4th tab calcs)
indexPath.row: 2
indexPath.row-2: 0
indexPath.row: 3
indexPath.row-2: 1
(2nd tab calcs)
indexPath.row: 2
indexPath.row-2: 0
The problem is, when I click on the 2nd tab, indexPath.row should be set to 2 but is instead set to 3.
Your logic is faulty. Consider an outline of your code. First you say this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 { // ...
}
else if indexPath.row == 1 { // ...
}
else {
if(switchState == 0 || switchState == 2){ // ...
}
else {
return createCompletedCell(indexPath)
}
}
}
Okay. So what index path is this when you call createCompletedCell? You don't know! You forgot to test that. Instead, you test something completely different - something about a switchState, which I have no basis for making any assumptions about. So indexPath row at this point can be any number except 0 or 1 (because if it were one of those, we would have returned by now).
So you should not be surprised if it is 2, because your logic permits of that possibility. Your mistake is assuming later on that it can't be 2, since obviously it can be.