Lazy load improvement in UITableView Swift - ios

I'm trying to load the components using the default lazy load implementation of Apple: Basically I'm loading the components from my server in the cellForRowAtIndexPath method. Before the entire collectionView loads, I pre-load some of the components by calling them from the server. The thing is that if I pre-load all the elements, it takes a long time ti finish loading the elements.
The problem I'm facing is that, when I scroll down, it gets a little jumpy because it's waiting for the components to load. What chances can I do to improve the load?
Here is the code I'm using to load the elements as the user scrolls:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
do {
if expComponents[indexPath.row].items == nil {
let componentItemsResponse = try `await`(interactor!.lazyloadItems(with: expSection!.id!, componentId: expComponents[indexPath.row].id!))
expComponents[indexPath.row].items = componentItemsResponse.result
}
let cell: ComponentTableViewCell? = getCell(
forIndexPath: indexPath,
componentType: expComponents[indexPath.row].componentType!)
if let mCell = cell {
mCell.configure(withComponent: self.expComponents[indexPath.row], delegate: self)
return mCell
}
} catch {
print(error)
}
return UITableViewCell()
}

Related

After tableView scrolled data puts in cells in wrong order

in my View:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TransactionTableCell", for: indexPath) as! TransactionTableCell
let newItem = getTransactionsInSection(section: sectionHeader[indexPath.section])[indexPath.row]
cell.configure(item: newItem)
}
in my TransactionTableCell
func configure(item: TransactionModel) {
guard let withdrawalBonuses = item.withdrawalBonuses,
withdrawalBonuses < 0,
let accruedBonuses = item.accruedBonuses,
accruedBonuses > 0 else {
configureWithOneOperation(item)//shows one line of operation
return
}
//show 2 lines of operations
firstOperationAmountLabel.text = "+\(Int(accruedBonuses))"
secondOperationAmountLabel.text = "\(Int(withdrawalBonuses))"
}
When I scroll the cell , second operation line is appears in wrong cells where its shouldn't be, even If I reload my table , that also has this problem.
You should use prepareForReuse() method
Simply just clear data of your labels:
override func prepareForReuse() {
super.prepareForReuse()
firstOperationAmountLabel.text = nil
secondOperationAmountLabel.text = nil
}
There are few things to check here.
Make sure you reset all fields before configure a new cell.
If you have created a cell using xib or storyboard, make sure you haven't filled labels with static text.
Is your guard statements passing for every item?
Else block for guard configures cell with a single operation, Is it handling all ui elements in cell?

Swift 3 button.addTarget not working on all table cells

I have created an app to allow users to store various voice recordings against reviews. When I display this a table and the data is populated with the following code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = (indexPath as NSIndexPath).item
let cell = tableView.dequeueReusableCell(withIdentifier: "voiceRecordingCell", for: indexPath) as! VoiceRecordingTableViewCell
let voiceRecording = self.voiceRecordings[row] as! NSDictionary
let isoFormatter = DateFormatter()
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'.000Z'"
let createdAt = isoFormatter.date(from: voiceRecording["created_at"] as! String)
self.recordingIndexPaths.insert(indexPath, at: row)
cell.recording = voiceRecording
cell.date.text = getDateFormatter("dd-MM-y").string(from: createdAt!)
cell.time.text = getDateFormatter("HH:mm").string(from: createdAt!)
cell.length.text = voiceRecording["length"] as? String
cell.location.text = voiceRecording["location"] as? String
let audioPlayerController = self.storyboard?.instantiateViewController(withIdentifier: "AudioPlayerController") as! AudioPlayerController
audioPlayerController.voiceRecording = voiceRecording
cell.audioPlayer.addSubview(audioPlayerController.view)
self.addChildViewController(audioPlayerController)
audioPlayerController.didMove(toParentViewController: self)
cell.deleteRecordingButton.tag = row
cell.deleteRecordingButton.addTarget(self, action: #selector(deleteRecordingPressed(_:)), for: .touchUpInside)
return cell
}
The cells all appear to be rendering correctly however for the cells that are not initially rendered with the page, the ones I have to scroll down to view, when I click on the buttons either on the audio player controls or the deleteRecordingButton nothing happens, its as though the addTarget is not being set. The code to set the buttons is being called and doesn't create an error, its just not applying to those button.
The buttons that are initially displayed on the screen have the correct actions and all work perfectly so I know that works.
I'm really at a loss as to what is causing this. I did try searching google and stackoverflow but I've not found anything. Any assistance with this would be greatly received.
--------------- UPDATE -----------
I just tried putting some breakpoints in
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
That also only works get called on the top 2 cells or the top one if in landscape!
Since the cell gets reused all the time the reference get lost.
Try something like this:
class ButtonTableViewCell: UITableViewCell {
typealias TapClosure = () -> Void
var buttonTapped: TapClosure?
#IBAction func buttonTouchUpInside(sender: UIButton) {
buttonTapped?()
}
}
extension TestViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! ButtonTableViewCell
cell.buttonTapped = {
print("Button tapped")
}
return cell
}
}
And another tipp. Never init an DateFormatter in cellForRowAtIndexPath. Instead create it in viewDidLoad or in a static struct for reuse.
Finally I have figured this out...
This was happening because I was using a scrollview with to scroll up and down the table without using the tables native scroll functionality.
When using the tables scroll functionality the buttons are applied the actions as they are brought into the view. This is handled by the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { method. However when you do it the way I was it initially renders all the cells and the ones that are off the screen don't work!
Its a little bit annoying working in this way but at least I now know about it.

Why the order of the UITableView Cells changed when I changed the size of the cell and reload?

I got a tableView to display some information like following image.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if cellStates[indexPath.section] == .unfold {
cellStates[indexPath.section] = .fold
} else {
cellStates[indexPath.section] = .unfold
}
tableView.reloadData()
}
The cell would be expanded under fold state, like following image.
If you touch it again, it should be folded again, like image 1.
Then here comes the problem.
As what we can see in image 2, there are three cells out side the screen.
No1, No2, No5 outside. And No3, No4 Inside. No3 is the cell we just touched.
When I touch No3 cell again to fold it, the order changed to No3, No4, No1, No2, No5.
It seems the cells that inside screen have been lifted to the top.
I'm confused in this, what's really going on? How to avoid?
The height of the cell depends on the unchanged data source, so it will mess things up.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "WBCell") as! WBTableViewCell
cell.controller = self
let state = cellStates[indexPath.section]
if state == .initial {
let data = dataSource[index]
cell.initial(data: data)
cellStates[indexPath.section] = .fold
} else if state == .fold {
cell.switchTo(unFolded: false) // Update autolayout constrains via SnapKit
} else {
cell.switchTo(unFolded: true)
}
return cell
}

Reloading table causes flickering

I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.
Every time I make a network call, I also call self.tableView.reloadData() on the main thread. Since each cell has an image, you can see flickering - the cell images flash white.
I tried implementing this solution but I don't know where to put it in my code or how to. My code is Swift and that is Objective-C.
Any thoughts?
Update To Question 1
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(R.reuseIdentifier.searchCell.identifier, forIndexPath: indexPath) as! CustomTableViewCell
let book = booksArrayFromNetworkCall[indexPath.row]
// Set dynamic text
cell.titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.authorsLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
// Update title
cell.titleLabel.text = book.title
// Update authors
cell.authorsLabel.text = book.authors
/*
- Getting the CoverImage is done asynchronously to stop choppiness of tableview.
- I also added the Title and Author inside of this call, even though it is not
necessary because there was a problem if it was outside: the first time a user
presses Search, the call for the CoverImage was too slow and only the Title
and Author were displaying.
*/
Book.convertURLToImagesAsynchronouslyAndUpdateCells(book, cell: cell, task: task)
return cell
}
cellForRowAtIndexPath uses this method inside it:
class func convertURLToImagesAsynchronouslyAndUpdateCells(bookObject: Book, cell: CustomTableViewCell, var task: NSURLSessionDataTask?) {
guard let coverImageURLString = bookObject.coverImageURLString, url = NSURL(string: coverImageURLString) else {
return
}
// Asynchronous work being done here.
task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue(), {
// Update cover image with data
guard let data = data else {
return
}
// Create an image object from our data
let coverImage = UIImage(data: data)
cell.coverImageView.image = coverImage
})
})
task?.resume()
}
When I scroll to the bottom of the table, I detect if I reach the bottom with willDisplayCell. If it is the bottom, then I make the same network call again.
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == booksArrayFromNetworkCall.count {
// Make network calls when we scroll to the bottom of the table.
refreshItems(currentIndexCount)
}
}
This is the network call code. It is called for the first time when I press Enter on the search bar, then it is called everytime I reach the bottom of the cell as you can see in willDisplayCell.
func refreshItems(index: Int) {
// Make to network call to Google Books
GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in
guard let books = books else {
return
}
self.footerView.hidden = false
self.currentIndexCount += 10
self.booksArrayFromNetworkCall += books
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache.
I would recommend to use SDWebImage to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling convertURLToImagesAsynchronouslyAndUpdateCells, and you will see that it will not flash again.
I dont' program in Swift but I see it is as simple as cell.imageView.sd_setImageWithURL(myImageURL). And it's done!
Here's an example of infinite scroll using insertRowsAtIndexPaths(_:withRowAnimation:)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var dataSource = [String]()
var currentStartIndex = 0
// We use this to only fire one fetch request (not multiple) when we scroll to the bottom.
var isLoading = false
override func viewDidLoad() {
super.viewDidLoad()
// Load the first batch of items.
loadNextItems()
}
// Loads the next 20 items using the current start index to know from where to start the next fetch.
func loadNextItems() {
MyFakeDataSource().fetchItems(currentStartIndex, callback: { fetchedItems in
self.dataSource += fetchedItems // Append the fetched items to the existing items.
self.tableView.beginUpdates()
var indexPathsToInsert = [NSIndexPath]()
for i in self.currentStartIndex..<self.currentStartIndex + 20 {
indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .Bottom)
self.tableView.endUpdates()
self.isLoading = false
// The currentStartIndex must point to next index.
self.currentStartIndex = self.dataSource.count
})
}
// #MARK: - Table View Data Source Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = dataSource[indexPath.row]
return cell
}
// #MARK: - Table View Delegate Methods
func scrollViewDidScroll(scrollView: UIScrollView) {
if isLoading == false && scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
isLoading = true
loadNextItems()
}
}
}
MyFakeDataSource is irrelevant, it's could be your GoogleBooksClient.getBooksFromGoogleBooks, or whatever data source you're using.
Try to change table alpha value before and after calling [tableView reloadData] method..Like
dispatch_async(dispatch_get_main_queue()) {
self.aTable.alpha = 0.4f;
self.tableView.reloadData()
[self.aTable.alpha = 1.0f;
}
I have used same approach in UIWebView reloading..its worked for me.

How to make table view load all data at once and not change said data while scrolling?

I'm making a simple table view app to display and play all the iOS System sounds.
I have all of the sounds and ID's in a a dictionary(I now realize this was a bad way to do this) in the form of [ID(Int):Name(String)], the problem is that when I load my view it loads well, but if I scroll down the cells originally on top change. Same when scrolling from the bottom to the top.
For example, the view loads in and I can click and hear the various sounds from any of the cells I click on. Lets say the first cell is "SMS-Sound1" and the seconds is "SMS-Sound2". Now when I scroll down to where those cells are out of view and then scroll back to the the top they are named something different(Still from my data dictionary).
How would I fix this problem so that it loads the tableview and then the tableview data doesn't change?
Edit: I thought the problem could be in the fact that the for in loop was executed around 300,000 times but thats not the case, made an array of the IDS so it was only executed around 1000 times total and the problem persists
My Code:
Cell set up
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("soundCell", forIndexPath: indexPath)
let button = cell.viewWithTag(3) as! UILabel //UILabel in "SoundCell"
for i: Int in 999..<4100 {
//Lowest id sound is 1000, highest is 4095
if (sounds[i] != nil) && loadedSoundStrings.contains(sounds[i]!) == false {
button.text = sounds[i]
loadedSoundStrings.append(sounds[i]!)
cell.tag = i
break
}
}
return cell
}
Rows/sections
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sounds.count
}
Variables:
let sounds =
[ 1000:"new-mail.caf",
1001:"mail-sent.caf",
1002:"Voicemail.caf",
1003:"ReceivedMessage.caf",
1004:"SentMessag.caf",
1005:"alarm.caf",
1006:"low-power.caf",
1007:"sms-received1.caf",
1008:"sms-received2.caf",
1009:"sms-received3.caf",
1010:"sms-received4.caf",
1011:"-(SMSReceived_Vibrate)",
1012:"sms-received1.caf",
1013:"sms-received5.caf",
1014:"sms-received6.caf",
1015:"Voicemail.caf",
1016:"tweet_sent.caf",
1020:"Anticipate.caf",
1021:"Bloom.caf",
1022:"Calypso.caf",
1023:"Choo_Choo.caf",
1024:"Descent.caf",
1025:"Fanfare.caf",
1026:"Ladder.caf",
1027:"Minuet.caf",
1028:"News_Flash.caf",
1029:"Noir.caf",
1030:"Sherwood_Forest.caf",
1031:"Spell.caf",
1032:"Suspense.caf",
1033:"Telegraph.caf",
1034:"Tiptoes.caf",
1035:"Typewriters.caf",
1036:"Update.caf",
1050:"ussd.caf",
1051:"SIMToolkitCallDropped.caf",
1052:"SIMToolkitGeneralBeep.caf",
1053:"SIMToolkitNegativeACK.caf",
1054:"SIMToolkitPositiveACK.caf",
1055:"SIMToolkitSMS.caf",
1057:"Tink.caf",
1070:"ct-busy.caf",
1071:"ct-congestion.caf",
1072:"ct-path-ack.caf",
1073:"ct-error.caf",
1074:"ct-call-waiting.caf",
1075:"ct-keytone2.caf",
1100:"lock.caf",
1101:"unlock.caf",
1102:"-(FailedUnlock)",
1103:"Tink.caf",
1104:"Tock.caf",
1105:"Tock.caf",
1106:"beep-beep.caf",
1107:"RingerChanged.caf",
1108:"photoShutter.caf",
1109:"shake.caf",
1110:"jbl_begin.caf",
1111:"jbl_confirm.caf",
1112:"jbl_cancel.caf",
1113:"begin_record.caf",
1114:"end_record.caf",
1115:"jbl_ambiguous.caf",
1116:"jbl_no_match.caf",
1117:"begin_video_record.caf",
1118:"end_video_record.caf",
1150:"vc~invitation-accepted.caf",
1151:"vc~ringing.caf",
1152:"vc~ended.caf",
1153:"ct-call-waiting.caf",
1154:"vc~ringing.caf",
1200:"dtmf-0.caf",
1201:"dtmf-1.caf",
1202:"dtmf-2.caf",
1203:"dtmf-3.caf",
1204:"dtmf-4.caf",
1205:"dtmf-5.caf",
1206:"dtmf-6.caf",
1207:"dtmf-7.caf",
1208:"dtmf-8.caf",
1209:"dtmf-9.caf",
1210:"dtmf-star.caf",
1211:"dtmf-pound.caf",
1254:"long_low_short_high.caf",
1255:"short_double_high.caf",
1256:"short_low_high.caf",
1257:"short_double_low.caf",
1258:"short_double_low.caf",
1259:"middle_9_short_double_low.caf",
1300:"Voicemail.caf",
1301:"ReceivedMessage.caf",
1302:"new-mail.caf",
1303:"mail-sent.caf",
1304:"alarm.caf",
1305:"lock.caf",
1306:"Tock.caf",
1307:"sms-received1.caf",
1308:"sms-received2.caf",
1309:"sms-received3.caf",
1310:"sms-received4.caf",
1311:"-(SMSReceived_Vibrate)",
1312:"sms-received1.caf",
1313:"sms-received5.caf",
1314:"sms-received6.caf",
1315:"Voicemail.caf",
1320:"Anticipate.caf",
1321:"Bloom.caf",
1322:"Calypso.caf",
1323:"Choo_Choo.caf",
1324:"Descent.caf",
1325:"Fanfare.caf",
1326:"Ladder.caf",
1327:"Minuet.caf",
1328:"News_Flash.caf",
1329:"Noir.caf",
1330:"Sherwood_Forest.caf",
1331:"Spell.caf",
1332:"Suspense.caf",
1333:"Telegraph.caf",
1334:"Tiptoes.caf",
1335:"Typewriters.caf",
1336:"Update.caf",
1350:"-(RingerVibeChanged)",
1351:"-(SilentVibeChanged)",
4095:"-(Vibrate)"]
var loadedSoundStrings = [String]()
You are instantiating all of the sounds for every single row when you actually want to only instantiate the sound for the rows that are loaded. To fix your order issue change your
cellForRowAtIdexPath
to this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("soundCell", forIndexPath: indexPath)
let button = cell.viewWithTag(3) as! UILabel //UILabel in "SoundCell"
button.text = sounds[i]
cell.tag = indexPath.row
return cell
}
This gives you 1 sound per cell since you have NumberOfRowsInSection set to sounds.count Cell for row will be called for every sound you have.
If I understand your code correctly, you're going about it the wrong way. You have a dictionary of sounds that you load once. The cellForRowAtIndexPath function should be returning one tableViewCell with details for the one sound.
UITableView automatically discards cells that are off screen to conserve memory, and will reuse them for newly visible cells. That's why you call dequeueReusableCellWithIdentifier. Therefore you should just be doing:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("soundCell", forIndexPath: indexPath)
let button = cell.viewWithTag(3) as! UILabel //UILabel in "SoundCell"
//Lowest id sound is 1000, highest is 4095
let i = indexPath.row + 1000
button.text = sounds[i]
cell.tag = i
return cell
}
Since you are hardcoding the sound number range I have done the same.
A table view works best with an array, as an array has a defined order and you can quickly access a given element; a for loop in cellForRowAtIndexPath is seldom a good thing.
You have a couple of issues, however, as your sounds identifiers don't start from 0, you can't use the identifier as a direct index into the array, but also the identifiers aren't sequential, so you can't even use a simple offset (adding a constant value to the row number).
I think that the best solution is not to rely directly on intrinsic types as you are for your dictionary, but rather, create a struct for each sound and store an array of them. Something like this:
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDatasource
struct Sound {
var id:Int
var fileName:String
}
var sounds=[Sound]()
func loadSounds() {
let soundsDict =
[1000:"new-mail.caf",
1001:"mail-sent.caf",
1002:"Voicemail.caf",
1003:"ReceivedMessage.caf",
1004:"SentMessag.caf",
1005:"alarm.caf",
1006:"low-power.caf",
1007:"sms-received1.caf",
1008:"sms-received2.caf",
...
]
for (id,fileName) in soundsDict {
self.sounds.append(Sound(id: id, fileName: fileName))
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("soundCell", forIndexPath: indexPath)
let button = cell.viewWithTag(3) as! UILabel //UILabel in "SoundCell"
button.text=self.sounds[indexPath.row].fileName
return cell
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sounds.count
}
}

Resources