Paging Data in a UITableView - ios

I am currently trying to create a UITableView that loads data as a user scrolls through it.
Basically, I have a data source with a lot of records. Way more than is feasible to load all at once. So I am querying 50 records at a time.
The problem lies in the fact that the user will be able to jump to the middle of this list via letters at the right. For example, if they press 'M' I load 50 records starting at 'M' into the list. Scrolling down will navigate through the M's and eventually the N's, O's, etc.
Of course, appending data to the bottom of the list is common and I was able to do this easily. I am having trouble finding reference or if it is done at all of appending 50 records to the top of the list as a user scrolls up.
For example, if the user hits 'M'. they should be able to scroll up and start seeing L's. I can append data to the beginning of the list, but the problem lies in the continuation of the scrolling list.
As of now, I can not get it working without a jump of the list or a complete stop.
Can anyone point me to someone who has done this cleanly?

OK, so I figured out the answer to my own question. Sorry for the post but maybe it will help someone else because I failed to find an exact solution to my answer else where, at least when it came to Swift.
First I loaded 50 records that started with 'M'. The JSON data would return the row number of these records. I created Int variables 'begin' and 'end' which held on to the range of rows I had loaded into my array. I set the numberOfRowsInSection to the 'end' of my range. This number was significantly more than the amount of rows I had loaded in my array, so I had to finagle how rows were loaded based on that array. Simple code snippet:
var begin:Int?
var end:Int = 0
func onDataLoad() {
data = data + DataHandler.data!
begin = data[0].Row
end = begin! + 50
tableView.reloadData()
let count = tableView.indexPathsForVisibleRows!.count - 2
let indexPath = NSIndexPath(forRow: begin! + count, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: false)
loading = false
}
func onBackwardData(evt:Event) {
data = DataHandler.data! + data
begin = begin! - 50
tableView.reloadData()
loading = false
}
func onForwardData() {
end = end + 50
data = data + DataHandler.data!
tableView.reloadData()
loading = false
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return end
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("UserCell", forIndexPath: indexPath) as! PulseListTableViewCell
let cellrow = indexPath.row - begin!
if cellrow >= 0 {
cell.name.text = data[cellrow].Name
}
return cell
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
row = indexPath.row
let count = tableView.indexPathsForVisibleRows!.count + 5
if(indexPath.row >= end - 5 && !loading)
{
loading = true
DataHandler.getUsers(end)
}
else if(indexPath.row <= begin! + count && begin! > 0 && !loading)
{
loading = true
var offset = begin! - 50
if offset < 0 {
offset = 0
}
DataHandler.getUsers(offset)
}
}
This code is crude and has some hard coded things that would need to be adjusted to handle getting to the very beginning or end of the list but the idea is there. Sorry if this is a totally obvious solution/issue in the iOS world but I'm new and thought it might help.

Related

UITableView with Full Screen cells - pagination breaks after fetch more rows

My app uses a UITableView to implement a TikTok-style UX. Each cell is the height of the entire screen. Pagination is enabled, and this works fine for the first batch of 10 records I load. Each UITableViewCell is one "page". The user can "flip" through the pages by swiping, and initially each "page" fully flips as expected. However when I add additional rows by checking to see if the currently visible cell is the last one and then loading 10 more rows, the pagination goes haywire. Swiping results in a partially "flipped" cell -- parts of two cells are visible at the same time. I've tried various things but I'm not even sure what the problem is. The tableView seems to lose track of geometry.
Note: After the pagination goes haywire I can flip all the way back to the first cell. At that point the UITableView seems to regain composure and once again I'm able to flip correctly through all of the loaded rows, including the new ones.
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Pause the video if the cell is ended displaying
if let cell = cell as? HomeTableViewCell {
cell.pause()
}
if let indices = tableView.indexPathsForVisibleRows {
for index in indices {
if index.row >= self.data.count - 1 {
self.viewModel!.getPosts()
break
}
}
}
}
In order to create a "Tik Tok" style UX, I ended up using the Texture framework together with a cloud video provider (mux.com). Works fine now.
I was facing the same issue and as I couldn't find a solution anywhere else here's how I solved it without using Texture:
I used the UITableViewDataSourcePrefetching protocol to fetch the new data to be inserted
extension TikTokTableView: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.prefetchRows(at: indexPaths)
}
}
prefetchRows will execute the request if the visible cell is the last one, as in my case
func prefetchRows(at indexPaths: [IndexPath]) {
if indexPaths.contains(where: isLastCell) {
getPosts(type: typeOfPosts, offset: posts.count, lastPostId: lastPost)
}
}
private func isLastCell(for indexPath: IndexPath) -> Bool {
return indexPath.row == posts.count - 1
}
I have a weak var view delegate type TikTokTableViewDelegate in my view model to have access to a function insertItems implemented by my TikTokTableView. This function is used to inform the UITableView where to insert the incoming posts at
self.posts.append(contentsOf: response.posts)
let indexPathsToReload = self.calculateIndexPathToReload(from: response.posts)
self.view?.insertItems(at: indexPathsToReload)
private func calculateIndexPathToReload(from newPosts: [Post]) -> [IndexPath] {
let startIndex = posts.count - newPosts.count
let endIndex = startIndex + newPosts.count
print(startIndex, endIndex)
return (startIndex..<endIndex).map { IndexPath(row: $0, section: 0) }
}
and this is the insertItems function implemented in TikTokTableView and here is the key: If we try to insert those rows, the pagination of the table will fail and leave that weird offset, we have to store the indexPaths in a local property and insert them once the scroll animation has finished.
extension TikTokTableView: TikTokTableViewDelegate {
func insertItems(at indexPathsToReload: [IndexPath]) {
DispatchQueue.main.async {
// if we try to insert rows in the table, the scroll animation will be stopped and the cell will have a weird offset
// that's why we keep the indexPaths and insert them on scrollViewDidEndDecelerating(:)
self.indexPathsToReload = indexPathsToReload
}
}
}
Since UITableView is a subclass of UIScrollView, we have access to scrollViewDidEndDecelerating, this func is triggered at the end of a user's scroll and this is the time when we insert the new rows
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if !indexPathsToReload.isEmpty {
tableView.insertRows(at: indexPathsToReload, with: .none)
indexPathsToReload = []
}
}

Filter tableView rows doesn't reload tableView

My first iOS app works with simple custom cells, but enhancement to filter tableView rows is causing delays and frustration. Searched online for help on filter rows, read dataSource and delegate protocols in Apple Developer guides, no luck so far.
Using slider value to refresh table rows. Extracted data from line array (100 items) to linefilter array (20). Then want to refresh/reload the tableview.
Slider is declared with 0 and all line array items show up. moving the slider does not alter display. If slider is declared with say 1, then 20 filter items show.
Quite new to Apple/Xcode/Swift so have no Objective C knowledge.
Any answers will probably help me get there.
Jim L
Relevant selection of code :
#IBAction func moveSlider(sender: AnyObject) {
// Non-continuous ******
_ = false
// integer 0 to 5 ******
let slider = Int(lineSlider.value)
}
}
// Global Variable ******
var slider = 0
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if slider == 0 {
return self.line.count
} else {
return self.linefilter.count
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! myTableViewCell
if slider == 0 {
cell.myCellLabel.text = line[indexPath.row]
} else {
cell.myCellLabel.text = linefilter[indexPath.row]
}
cell.myImageView.image = UIImage(named: img[indexPath.row])
return cell
}
tableView.reloadata()
try to put
tableView.reloadData()
like this
let slider = Int(lineSlider.value)
tableView.reloadData()
}
in your moveSlider function

sorting UITableView into sections

I'm new to swift & Xcode (teaching myself via tutorials and stackoverflow). I've written some code that adds places to a list in a TableView and now I am trying to sort that list of places into sections.
Specifically, I have a ViewController where I input name, neighborhood and friend (all strings) and this adds a Place to the bottom of my TableView.
I want to group this list of places by neighborhood, and display all the places in the same neighborhood together in a section, using the neighborhood string as the section header.
I'm close, but I'm not indexing my sections correctly. indexPath.section I believe is what I'm missing..
So far I have this code in my TableViewController:
// MARK: Properties
var places = [Place]()
override func viewDidLoad() {
super.viewDidLoad()
// Use the edit button item provided by the table view controller.
navigationItem.leftBarButtonItem = editButtonItem()
// Load any saved places, otherwise load sample data.
if let savedPlaces = loadPlaces() {
places += savedPlaces
}
else {
// Load the sample data.
loadSamplePlaces()
}
// Sort by neighborhood
places.sortInPlace { (place1, place2) -> Bool in
return place1.neighborhood < place2.neighborhood
}
}
// MARK: Getting Count, Number and Name of Neighborhoods
func getCountForNeighborhood(neighborhood:String) -> Int {
return places.filter { (place) -> Bool in
return place.neighborhood == neighborhood
}.count
}
func getNumberOfNeighborhoods() -> Int {
var neighborhoodCount = 0
var neighborhoodPrev = "Not a real neighborhood"
for place in places {
if place.neighborhood != neighborhoodPrev {
neighborhoodCount += 1
}
neighborhoodPrev = place.neighborhood
}
return neighborhoodCount
}
func getNeighborhoodForSection(section:Int) -> String {
var previousNeighborhood:String = "Not a real neighborhood"
var currentIndex = -1
for place in places {
if(place.neighborhood != previousNeighborhood) {
currentIndex += 1
}
if(currentIndex == section){
return place.neighborhood
}
previousNeighborhood = place.neighborhood
}
return "Unknown"
}
func loadSamplePlaces() {
let place1 = Place(name: "Motorino", neighborhood: "East Village", friend: "Maggles")!
let place2 = Place(name: "Bar Primi", neighborhood: "Lower East Side", friend: "Em")!
let place3 = Place(name: "El Carino", neighborhood: "Williamsburg", friend: "Ruby")!
places += [place1, place2, place3]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
//alter this for To Try, Been To sections =2
return getNumberOfNeighborhoods()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
let neighborhood = getNeighborhoodForSection(section)
return getCountForNeighborhood(neighborhood)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "PlaceTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! PlaceTableViewCell
// Fetches the appropriate place for the data source layout.
let place = places[indexPath.row]
cell.placeLabel.text = place.name
cell.neighborhoodLabel.text = place.neighborhood
cell.friendLabel.text = place.friend
return cell
}
//Add section headers
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return getNeighborhoodForSection(section)
}
I'm fairly certain it's an issue with my cellForRowAtIndexPath method. I think I'm missing some code to properly index the data into sections...
Current build displays
You've made a mess with sections.
In method tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) you're taking data from the places array based on the row of the indexPath.
But you don't "talk" about sections at all. That's why you tell the data source delegate to take the data of those 2 place objects regardless of the section it's asking you.
You've made a data structure in a relational style like this:
neighbourhood1 - place1
neighbourhood1 - place2
neighbourhood2 - place3
neighbourhood2 - place4
neighbourhood3 - place5
neighbourhood3 - place6
And believe me, it's really hard to interface this data structure with the UITableView data source concept, cause it's really hard to determine the section index from the single data.
Try to arrange your data in a tree like this:
neighbourhood1:
- place1
- place2
neighbourhood2:
- place3
- place4
neighbourhood3:
- place5
- place6
In an array of arrays, and then it will be simple, cause the indexPath pair (section, row) will match the indexes of your places[section][row]:
neighbourhood1: (section 0)
- place1 (path 0.0)
- place2 (path 0.1)
neighbourhood2: (section 1)
- place3 (path 1.0)
- place4 (path 1.1)
neighbourhood3: (section 2)
- place5 (path 2.0)
- place6 (path 2.1)
I do concur with the other answer: this will be easier if your store your data in a two-dimensional array organized by neighborhood first, then place second, then access the data as:
let place = places[indexPath.section, indexPath.row];
However, to your specific questions and current code, I see two things here. First, your sample data has a single item per neighborhood, but your table is showing two rows per neighborhood. That indicates that your numberOfRowsInSection function is returning 2 when we expect it to be 1. Make sure your getNeighborhoodForSection is returning the right section. Then look at why getCountForNeighborhood isn't returning 1 for each section. (As an aside, you probably already know this, but getNeighborhoodForSection assumes your data is always sorted by neighborhood. If you insert a new neighborhood out of order, you'll run into problems).
Second, cellForRowAtIndexPath you always pull data by the indexPath.row number. When you enter this function for each neighborhood, the indexPath.row number will always restart at 0 for each section (neighborhood). And therefore you'll pull the 1st item out of places (Motorino) for the first row in every section. That's why you see the same list of places repeated in each section.
Once you have the right number of rows per neighborhood, In cellForRowAtIndexPath you need to:
Get the neighborhood for index.section using your getNeighborhoodForSection(index.section);
Search through places until you find that neighborhood and save the index of the starting point, which I'll call neighborHoodStartIndex
let place = places[neighborHoodStartIndex + indexPath.row]
For debugging purposes, I'd suggest making a different number of sample places in each neighborhood. It'll be a little easier to spot problem patterns when you expect different numbers of rows in each section.

UITableView Section is Repeating Data

This has been killing me for a few hours now. I have a UITableViewController that has multiple data sections. My data source is simply an Array.
The problem I'm running into is that each section is repeating data from the array starting from the first index instead of "slicing" it as I expect it should.
Simplified example:
let sections = ["Section A", "Section B"]
let counts = [3, 5]
let source = ["a","b",c","d","e","f","g","h"]
// Output in simulator:
# Section A
- a
- b
- c
# Section B
- a
- b
- c
- d
- e
- and so on...
I would expect that "Section B" would be the next 5 results starting at "d" and not restart from the first index.
The relevant code is pretty standard stuff:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sections.count // returns 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return counts[section] // returns correct data
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let data = source[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! MyTableViewCell
// some cell formatting, populate UILabels, etc
cell.testLabel.text = data["test"].string
return cell
}
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("Header") as! MyTableViewHeaderCell
headerCell.backgroundColor = UIColor.grayColor()
headerCell.testHeaderLabel.text = sections[section]
return headerCell
}
Initial searching of SO led me to believe it's a cell reuse issue but after overriding prepareForReuse in my cell class, I don't think thats it.
Expected Results
# Section A
- a
- b
- c
# Section B
- d
- e
- f
- g
- h
Like I said, I'm expecting that dividing the TableView data in to sections would keep a reference to the array pointer and continue where it left off instead of starting back at 0 for each section.
Any thoughts or suggestions?
indexPath.row always returns the row-number inside a section.
In your second section, you need to add the number of rows displayed in all sections before.
Change let data = source[indexPath.row] to something like this:
let data = source[indexPath.row+counts[0]]
If you add more sections, this will be a bit more complicated to calculate.
Other idea:
If it is possible, you could rearrange your array. You could make a two-dimensional array. The main array would include arrays with the data for each section.
To display it, you' need to use indexPath.section, too.
dataArray[indexPath.section][indexPath.row]
Using the idea of FelixSFD, but with a little logical modification, so you can work dynamically:
Change this:
let data = source[indexPath.row]
for this:
var countIndex = indexPath.row
for section in 0...indexPath.section {
countIndex += counts[section]
}
let data = source[countIndex]
Be careful with this approach because you may have some performance issues on large tableViews.
If you can rearrange your array:
change
let source = ["a","b",c","d","e","f","g","h"]
into
let source = [["a","b","c"],["d","e","f","g","h"]]
and change
let data = source[indexPath.row]
into
let data = source[indexPath.section][indexPath.row]
I had the same problem, but with more complex situation, and i needed more dynamically way of doing it. Sure i could rearrange my data, to use two-dimensional array, but i don't want to handle it later. So i did it like this.
I am pulling my data from firebase, so i never know, how many sections/arrays i will have.
Creating an array, to insert amount of items in array.
var counterTableView = [Int]()
Filling array with 0, without doing it, i was getting errors later. (Index out of range)
override func numberOfSections(in tableView: UITableView) -> Int {
for i in 0...Array(Set(self.sections)).count {
counterTableView.insert(0, at: i)
}
counterTableView.removeLast(counterTableView.count-Array(Set(self.sections)).count-1)
return Array(Set(self.sections)).count
}
Next step, is to fill the amount of items in one section in array
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
counterTableView[section+1] = counts[section] + counterTableView[section]}
Last step, showing the data in cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell.textLabel.text = source[indexPath.row+counterTableView[indexPath.section]]}

UITableView not displaying correct amount of rows

I've created a table view within a view controller and am trying to set the amount of rows it will have. The amount gets returned from a database and then passed on to the correct method (see below). The problem is that the amount of rows visible is not the same amount that gets returned from the database.
Within .viewDidLoad()
self.activeIDs.delegate = self
self.activeIDs.dataSource = self
self.activeIDs.rowHeight = 30
self.activeIDs.reloadData()
Methods that are supposed to "set up" the table view
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = activeIDs.dequeueReusableCellWithIdentifier("idCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel!.text = "Test"
return cell
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
var numberOfRows = 0 //Assigning default value
SQLHandler.getActiveIDAmount {
amountOfIDs in
numberOfRows = amountOfIDs.description.toInt()!
println(numberOfRows) //Displays correct (database) value
}
return numberOfRows //Returns correct value EDIT: wrong value.
}
Instead of getting the desired amount of rows (4) I always, despite the value which I get from the database, end up with 6? Screenshot of table view in action: http://gyazo.com/753f326177dc8cd6b1734f4d19681d71
What is the problem? What am I doing wrong?
The method you are calling SQLHandler is a completion handler, that means that swift will continue executing your code and just after (and return the numberOfRowns = 0) than after (when the the request finish) it will come back to the block:
SQLHandler.getActiveIDAmount {
amountOfIDs in
numberOfRows = amountOfIDs.description.toInt()!
println(numberOfRows) //Displays correct (database) value
//add the values returned to your dataset here
//call refresh table and dispatch in the main thread in case
//this block is running in a background thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
and print the number of rows.
What you need to do is to call the function SQLHandler.getActiveIDAmount somewhere else in your code and call table.reloadData() after the callback is finished.
I hope that helps you!

Resources