SearchBar with UITableViewController contains wrong amount of cells - ios

I am setting up a UISearchController in my UITableViewController like this.
self.resultSearchController = UISearchController(searchResultsController: nil)
self.resultSearchController.searchResultsUpdater = self
self.resultSearchController.delegate = self
self.resultSearchController.dimsBackgroundDuringPresentation = false
self.resultSearchController.searchBar.sizeToFit()
self.tableView.tableHeaderView = self.resultSearchController.searchBar
In my willPresentSearchController I am counting the amount of cells and putting them into an array in order to make the filtered cells equal to the original ones.
{
print("Will Present")
cells.removeAll()
print(numberOfSectionsInTableView(table))
// Iterate over all the rows of a section
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++) {
if let cell:chatUebersichtCell = table.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: 0)) as? chatUebersichtCell{
cells.append(cell)
}
}
}
Now, counting the cells, I am able to get a number equal to the visible amount of cells I am having in my TVC, but not more (5 on iPhone 5).
Any ideas, how to change this?
Edit:
NumberOfSections = 1
NumberOfRows is equal to number of cells (10 in my example)
After Row nr.5 I cannot cast to my cell anymore

The reason why you are getting only 5 cells is because you are using the same view controller to present your results (the table view gets altered after the search). In order to achieve what you are looking for, you can do one of two things:
Keep a reference of the number of rows before the search happens and run your loop against this number.
OR
Use another table view controller for displaying your search results: self.resultSearchController = UISearchController(searchResultsController: newTableViewController)

Related

Iterate StackView, Swift

I have a custom cell composed by 3 StackView. Each one of them has a title, a description and an image, horizontally.
I have to fill this cell with an Array, it could be made of max 3 elements, but it could have 2 or 1.
So in my viewModel I'm treating this array like this ...
let firstItem = myArray[0]
let secondItem = myArray[1]
let thirdItem = myArray[2]
And I fill the field with firstItem.name firstItem.description ... For each one of them (not the best approach I guess)
Then I made some check if index exist, if it doesn't I delete the StackView, I set manually some constraints and I fit the cell to the content ( If I have 2 elements the cell is shorter, If I have 3 elements the cell is bigger).
This is a piece of code after I check that index 3 doesn't exist:
self.stackView.removeFromSuperview()
self.ownConstraints.constant = value (20 for example)
My question is, what is the best approach to achieve this? With cell I usually append item with a for cycle one by one, but I'm not familiar with this approach on StackView inside a Cell.
This is what I have done with cell (a series of cell with 1 name and 1 description):
for (element) in myArray {
self.cellArray.append( elementName , elementDescription )
}
// hide all items in stackView
stackView.arrangedSubviews.forEach({ $0.isHidden = true })
// add or update arrangedSubviews
for (index, element) in myArray.enumerated() {
if index >= stackView.arrangedSubviews.count - 1 {
stackView.addArrangedSubview(UILabel())
}
(stackView.arrangedSubviews[index] as? UILabel)?.text = element
stackView.arrangedSubviews[index].isHidden = false
}
I would hide all subviews in stackView and only show subviews if content is available in your viewModel.

How to maintain tableview scroll to stop reusing cell

I have multiple sections in tableview. I have multiple questions and multiple answers of each question. In multiple answers, I have one option and that is other (option). when I select the button of other, then it shows the text field for advice. Now i need to maintain the data of text field and that selected option's (Other) text when scrolling in tableview.I am using below code for all answer.
if (indexPath.section == 2)
{
let cellidentifier="cell3"
let cell=tableView.dequeueReusableCell(withIdentifier: cellidentifier,for:indexPath as IndexPath) as! TextfieldTableViewCell
let object_3:AnswerBaseClass = arrobject_answer[0][indexPath.row]
//print("arrobject is\(arrobject_answer[0][indexPath.row])")
if object_3.answer == "O"
{
// cell.lbl_answer.isHidden = true
cell.btn_selected.isHidden=true
//cell.lbl_answer_height.constant = 0
cell.Other_textfield.tag = 101
cell.Other_textfield.borderStyle = .line
cell.Other_textfield_top.constant = -30
cell.Height_2.constant = 30
}
else
{
cell.lbl_answer?.text = object_3.answer!
cell.Other_textfield_top.constant = 12
cell.Height_2.constant = 0
cell.lbl_answer.isHidden = false
cell.btn_selected.isHidden=false
if answer_main_data[0][indexPath.row] == true
{
cell.lbl_answer.tag = indexPath.row
cell.btn_selected.isSelected=true
if cell.lbl_answer.text == "Other"
{
for subview in cell.contentView.subviews
{
subview.removeFromSuperview()
}
if arrOtherTextfield_2.indices.contains(indexPath.row)
{
cell.addSubview(arrOtherTextfield_2[indexPath.row])
}
else
{
cell.Other_textfield.tag = 1100
cell.Other_textfield.borderStyle = .line
cell.Height_2.constant = 30
arrOtherTextfield_2.append(cell.Other_textfield)
}
}
else
{
cell.Height_2.constant = 0
}
}
else
{
cell.Other_textfield_top.constant = 12
cell.btn_selected.isSelected=false
cell.Height_2.constant = 0
}
}
cell.Other_textfield.borderStyle = .line
return cell
}
You will have to retain (store in some dictionary) the data entered in textfield, otherwise it will be lost when you scroll table and cell is reloaded. If you don't want to retain then you should use scroll view instead of table view. In scrollview it will not redraw UI even if you scroll up and down.
You need to separate UI and data. You embed data into your cell and when cells are reused, you lost data.
You can do 2 things:
Create a ViewModel class which contains data of cell: text, color, etc. Of course you need to update your ViewModel as you receive input You can google "MVVM pattern" for more information. Even if your cell is reused, your data is safe in ViewModel object.
You can keep your cells in an Array so that they won't be reused.

UITableView load more data when scrolling to top?

How could I load more data while scrolling to the top without losing the current offset oh the UITableView?
Here is what I am trying to achieve:
This is the whole set of data:
row 1
row 2
row 3
row 4
row 5
row 6
row 7
row 8*
row 9
row 10
row 11
row 12
row 13
row 14
row 15
Now, imagine that the user loaded the ones marked in bold and the offset is at row 8, if the user scroll up and reached row 7, I want to load and insert the rows from 1 to 5 without jumping from row 7. Keeping in mind that the user may be scrolling so when data reached the phone it is at row 6, so I can't jump it back to row 7, but keep the scroll smooth and natural (just how happen when you load more data while scrolling down, that the data is reloaded without the tableview jumping from between rows).
By the way, by offset I mean the contentOffset property of the UITableView.
Thanks for your help, I do really appreciate it!
When you are updating your data, you need to get the current offset of the tableView first. Then, you can add the height of the added data to the offset and set the tableView's offset like so:
func updateWithContentOffsset(data: [String]) {
guard let tableView = tableView else {
return
}
let currentOffset = tableView.contentOffset
let yOffset = CGFloat(data.count) * tableView.rowHeight // MAKE SURE YOU SET THE ROW HEIGHT OTHERWISE IT WILL BE ZERO!!!
let newOffset = CGPoint(x: currentOffset.x, y: currentOffset.y + yOffset)
tableView.reloadData()
tableView.setContentOffset(newOffset, animated: false)
}
You can also take a look at the gist that I created. Simply open up a new iOS Playground and copy-paste the gist.
The only thing you have to be aware of is that make sure you know your row height to add to the offset.
#Pratik Patel Bellow answer is best one.
Right down or call bellow function in the web-service response.
func updateWithScroll(data: [String]) {
guard let tableView = tableView else {
return
}
guard let currentVisibleIndexPaths = tableView.indexPathsForVisibleRows else {
return
}
var updatedVisibleIndexPaths = [IndexPath]()
for indexPath in currentVisibleIndexPaths {
let newIndexPath = IndexPath(row: indexPath.row + data.count, section: indexPath.section)
updatedVisibleIndexPaths.append(newIndexPath)
}
tableView.reloadData()
tableView.scrollToRow(at: updatedVisibleIndexPaths[0], at: .top, animated: false)
}
Now, imagine that the user loaded the ones marked in bold and the offset is at row 8, if the user scroll up and reached row 7, I want to load and insert the rows from 1 to 5 without jumping from row 7.
Loading data into the table as its needed is one of the things that UITableView does for you automatically. Don't try to insert rows into the table because they'll soon be needed as the user scrolls toward them -- the table view will request those rows from its data source as they're needed. You just need to make sure that your data source has the information it needs in order to fulfill the tables requests for cells as they arrive.
Things get a little more complicated if populating the rows requires making a network request for each row. Given your use of "load" in the question, I don't think that's what you're talking about, but in case that's your situation, here are some tips:
Request the data for as many rows as you reasonable can as early as you reasonably can. The amount of data displayed in a single row is typically small, so requesting a few hundred rows all at once shouldn't be a big deal.
If you don't have the data you need for a given row, make the necessary request, but return a cell immediately. The cell you return could use a spinner or other indication that the data is pending. When the request completes, you can tell the table to reload the appropriate row so that the proper content will display.

Odd behavior with UITableView in Swift

I have a custom cell with two labels and a image. I receive some data from internet in Json. Everything works fine; every cell fills with the correspondent data. I've added a new label that has to be filled just like the other ones. This is the data:
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell") as! friendCell
cell.friendPicture?.image = newImageUser
cell.friendName.text = "#\(theName[indexPath.row])"
if repliesNumber[indexPath.row] == "0"{
cell.repliesNumber.textColor = UIColor.redColor()
}
else{
cell.repliesNumber.textColor = UIColor(patternImage: UIImage(named:"backgroundPattern")!)
}
if AttachedImage[indexPath.row] != ""{
cell.backgroundColor = .orangeColor()
}
For testing I've made to be colored the first two cells. My issue is that the fist two cells get colored, but if I scroll down, every two, three, four cells (depending on the device -device height I guess-, another two cells get colored too.
It's odd/weird because the rest of labels work fine.
Where should I start looking?
If I print the json data I receive everything is okay
Here is a gif:
GIF
Basically my problem is that when I scroll down the data disappear but only from a label, the rest of labels and images doesn't.
It's not very clear from your code which are the cells that are "colored". Is it from the orangeColor() condition ?
Anyway, UITableViewCells in an UITableView are reused, which means you are given them in the exact same state you left them. This is why, if you don't reset your backgroundColor, they still are orange as you scroll.
So basically, whenever you do something in a certain condition in a cell, don't forget to restore its state when the condition is not met. In your case, this gives :
// cellForRowAtIndexPath {
if itemIsMultimedia() {
cell.Multimedia.text = "it's multimedia"
cell.Multimedia.hidden = false
}
else {
cell.Multimedia.text = ""
cell.Multimedia.hidden = true
}
here #aimak
if AttachedImage[indexPath.row] != ""{
cell.Multimedia.text = "It's multimedia"
}
else{
cell.Multimedia.hidden = true
}

Adding subviews to only one UICollectionViewCell on button tap

Each UICollectionViewCell has its own button hooked up to the following action:
#IBAction func dropDown(sender:UIButton){
var pt = sender.bounds.origin
var ptCoords : CGPoint = sender.convertPoint(pt, toView:sender.superview);
var ptCoords2 : CGPoint = sender.convertPoint( ptCoords, toView: collectionView!);
var cellIndex: NSIndexPath = self.collectionView!.indexPathForItemAtPoint(ptCoords2)!
//var i : NSInteger = cellIndex.row;
//var i2 : NSInteger = cellIndex.section;
var selectedCell = collectionView?.cellForItemAtIndexPath(cellIndex) as CollectionViewCell!
selectedCell.button.backgroundColor = UIColor.blackColor()
for (var i = 0; i < 3; i++){
var textView : UITextView! = UITextView(frame: CGRectMake(self.view.frame.size.width - self.view.frame.size.width/1.3, CGFloat(50 + (30*(i+1))), CGRectGetWidth(self.view.frame), CGFloat(25)))
textView.backgroundColor = UIColor.whiteColor()
selectedCell.contentView.addSubview(textView)
}
}
What I want to do is add 3 subviews to only the cell that's been tapped. The subviews are added successfully, but as soon as I scroll, cells that come into view & correspond to the previously set indexPath are loaded with 3 subviews. I figure this is due to the dequeueReusableCellWithReuseIdentifier method, but I can't figure out a way around it. I considered removing the subviews on scrollViewDidScroll, but ideally I would like to keep the views present on their parent cell until the button is tapped again.
EDIT:
Okay, I ditched the whole convertPoint approach and now get the cell index based on button tags:
var selectedCellIndex : NSIndexPath = NSIndexPath(forRow: cell.button.tag, inSection: 0)
var selectedCell = collectionView?.cellForItemAtIndexPath(selectedCellIndex) as CollectionViewCell!
Regardless, when I try to add subviews to only the cell at the selected index, the subviews are duplicated.
EDIT:
I've created a dictionary with key values to track the state of each cell like so:
var cellStates = [NSIndexPath: Bool]()
for(var i = 0; i < cellImages.count; i++){
cellStates[NSIndexPath(forRow: i, inSection: 0)] = false
}
which are set by cellStates[selectedCellIndex] = true within the dropDown function. Then in the cellForItemAtIndexPath function, I do the following check:
if(selectedIndex == indexPath && cellStates[indexPath] == true){
for (var i = 0; i < 3; i++){
var textView : UITextView! = UITextView(frame: CGRectMake(cell.frame.size.width - cell.frame.size.width/1.3, CGFloat(50 + (30 * (i+1))), CGRectGetWidth(cell.frame), CGFloat(25)))
textView.backgroundColor = UIColor.whiteColor()
cell.contentView.addSubview(textView)
println("display subviews")
println(indexPath)
}
} else {
println("do not display subviews")
println(indexPath)
}
return cell
where selectedIndex, the NSIndexPath of the active cell set via the dropDown function, is compared to the indexPath of the cell being created & the cellState is checked for true.
Still no luck - the subviews are still displayed on the recycled cell. I should mention that "display subviews" and "do not display subviews" are being logged correctly while scrolling, so the conditional statement is being evaluated successfully.
MY (...hack of a...) SOLUTION!
Probably breaking a bunch of best coding practices, but I assigned tags to all the created subviews, remove them at the beginning of the cellForItemAtIndexPath method, and create them again if the cellState condition returns true.
No problem. Basically, you need to store program state OUTSIDE your UI components in what is commonly called a "model". Not sure what your app is so I am going to make up an example. Assume you want to show a grid where each cell is initially green and they toggle to red when the user taps it. You would need to store the state (I.e., whether a cell has been tapped or not) in some two dimensional array, which is going to contain a Boolean for ALL cells, and not just the ones that are currently showing (assuming you have enough cells to make the grid scroll). When the user taps a cell you set the flag in corresponding array element. Then, when the iOS calls you back to provide a cell (in the dequeue method) you check the state in the array, apply the appropriate color to the UIView of the cell, then return it. That way, iOS can reuse the cell view objects for efficiency, while at the same time you apply your model state to corresponding cells dynamically. Let me know if this clear.
One of two things:
- Disallow pooling of cells.
- Maintain sufficient info in your mode to be able to draw cells depending on the model rather than on their location on screen. That is, store a bit in your model that determines whether or not to show the three views for each "logical" cell. Then, when asked to dequeue a cell, check its model and add/remove the backgrounds dynamically.

Resources