Custom UITableViewCell with Progress Bar download update
It's very bad practise to call method tableView:cellForRowAtIndexPath:
directly, becuase the cell may not exist at the same moment and it may be a cause of the bug you have.
Basically you should do this in another way: add an array of double:
double progressValues[30]; // 30 is the count of rows on your tableView, you can set the size dynamically
and use it like this:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:
(int64_t)totalBytesExpectedToWrite
{
dispatch_async(dispatch_get_main_queue(), ^{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
OFPTableCell *cell = (OFPTableCell*)[self tableView:self.tableViewCache cellForRowAtIndexPath:indexPath];
progressValues[indexPath.row] = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
[self.tableViewCache reloadData];
});
}
and in the dataSource method just add this string:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//... your code...
cell.progressView.progress = progressValues[indexPath.row];
// ...
return cell;
}
Correct way to show downloadable content in UITableView (with ProgressBar etc.)
It's important to realize that your progress bars will not be shown all the time (i.e. the user can scroll the table, and once offscreen that same cell can be reused at another index position for different content). So what you will need to do is have somewhere you can store the data about any active downloads, including the index position in the table, the total file size, and the number of bytes downloaded so far. Then, whenever your cell is drawn, you'll need to check whether the item for that cell is currently being downloaded and if so, show the bar with the appropriate percentage progress.
The easiest way to do this would be to add a property to your view controller to store this info. It can be an NSMutablerray
that will hold a collection of NSMutableDictionary
objects, each dictionary will contain the necessary info about an active download.
@property (nonatomic, strong) NSMutableArray *activeConnections;
First you'll initialize the array in viewDidLoad:
:
- (void)viewDidLoad
{
[super viewDidLoad];
//...
self.activeConnections = [[NSMutableArray alloc] init];
}
Whenever a button is pressed, you'll add an NSMutableDictionary object to your array with the info you'll need.
- (void)downloadFileWhenPressedButton:(UIButton*)sender
{
// ...
// then create dictionary
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:con forKey:@"connection"]; // save connection so we can reference later
[dict setObject:[NSNumber numberWithInt:[sender.tag]/10] forKey:@"row"]; // this is the row index from your table
[dict setObject:[NSNumber numberWithInt:999] forKey:@"totalFileSize"]; // dummy size, we will update when we know more
[dict setObject:[NSNumber numberWithInt:0] forKey:@"receivedBytes"];
[self.activeConnections addObject:dict];
}
Also we'll create two utility methods so we can find easily retrieve the connection info from our array, using either the connection object itself, or the row index position in the table.
- (NSDictionary*)getConnectionInfo:(NSURLConnection*)connection
{
for (NSDictionary *dict in self.activeConnections) {
if ([dict objectForKey:@"connection"] == connection) {
return dict;
}
}
return nil;
}
- (NSDictionary*)getConnectionInfoForRow:(int)row
{
for (NSDictionary *dict in self.activeConnections) {
if ([[dict objectForKey:@"row"] intValue] == row) {
return dict;
}
}
return nil;
}
When the connection is established, update your dictionary with the expected length
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// ...
NSDictionary *dict = [self getConnectionInfo:connection];
[dict setObject:[NSNumber numberWithInt:response.expectedContentLength] forKey:@"totalFileSize"];
}
As you receive data, you'll update the number of received bytes and tell your tableView to redraw the cell containing the progress bar.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// ...
NSDictionary *dict = [self getConnectionInfo:connection];
NSNumber bytes = [data length] + [[dict objectForKey:@"receivedBytes"] intValue];
[dict setObject:[NSNumber numberWithInt:response.expectedContentLength] forKey:@"receivedBytes"];
int row = [[dict objectForKey:@"row"] intValue];
NSIndexPath *indexPath = [NSIndexPathindexPathForRow:row inSection:0];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
When your connection is done downloading, you should remove the connection from your activeConnections array, and reload the table cell.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// ...
NSDictionary *dict = [self getConnectionInfo:connection];
[self.activeConnections removeObject:dict];
int row = [[dict objectForKey:@"row"] intValue];
NSIndexPath *indexPath = [NSIndexPathindexPathForRow:row inSection:0];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
Finally, in cellForRowAtIndexPath:
you'll need to draw the cell's progress bar based on the info in your activeConnections array.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// ...
// remove any previous buttons or progress bars from this cell
for (UIView *view in [cell.contentView subViews]) {
if ([view isKindOfClass:[UIProgressView class]] || [view isKindOfClass:[UIButton class]]) {
[view removeFromSuperView];
}
}
// look for active connecton for this cell
NSDictionary *dict = [self getConnectionInfoForRow:indexPath.row];
if (dict) {
// there is an active download for this cell, show a progress bar
UIProgressView *dlProgress = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
dlProgress.frame = CGRectMake(cell.frame.size.width-150, 17, 50, 9);
dlProgress.tag = indexPath.row*10+1;
dlProgress.progress = [[dict objectForKey:@"receivedBytes"] intValue] / [[dict objectForKey:@"totalFileSize"] intValue];
[cell.contentView addSubview:dlProgress];
} else {
// no active download, show the download button
UIButton *dl = [UIButton buttonWithType:UIButtonTypeCustom];
dl.tag = indexPath.row*10;
[dl setBackgroundImage:[UIImage imageNamed:@"downloadButton.png"] forState:UIControlStateNormal];
[dl setBackgroundImage:[UIImage imageNamed:@"downloadButtonH.png"] forState:UIControlStateHighlighted];
[dl setFrame:CGRectMake(230.0, (cell.frame.size.height-28)/2, 28, 28)];
[dl addTarget:self action:@selector(downloadFileWhenPressedButton:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:dl];
}
}
How to update progressview in uitableview cell by urlsession (download/upload file)
I'm curious how you did it in Objective-C with AFNetworking. At least conceptional, there shouldn't be a big difference to implementation in Swift with URLSession.
Imho your main problem about updating your progress is, that you share the progressView variable with a single UIView instance for all of your cells.
- you do not initialize a new progressView for each cell, but share one view for all cells
- because of 1,
cell.addSubview(progressView)
doesn't only add your progressView to that cell, it removes your progressView from the other cells as well, because a view can only have one parent view. - your progressView will have multiple UIProgressBars as subview. One for each time
tableView(_:cellForRowAt indexPath:)
is called - with
self.progressBar.progress = uploadProgress
you will always update the progressBar which was last initialized, because you don't have a reference to the other ones.
To get this to work in a clean way, I'd recommend you do some research to MVVM architecture.
- create a UITableViewCell subclass for your cell
- create a ViewModel class for that cell
- store a viewModel instance for each of your cells in your viewController (or better in a separate TableViewDataSource object)
- implement URLSessionDelegate protocol in your ViewModel and set the appropriate viewModel instance as delegate for your uploadTasks
For a quick and dirty fix:
Remove these lines:
var progressBar : UIProgressView = UIProgressView.init()
let progressView : UIView = UIView.init(frame: CGRect(x: 100, y: 10, width: 100, height: 20))
Add variable:
var uploadTasks: [URLSessionDataTask: IndexPath] = [:]
Add a helper function to calculate viewTag:
func viewTag(for indexPath: IndexPath) -> Int {
return indexPath.row + 1000
}
Change your tableView(_:cellForRowAt indexPath:)
to:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
let textName: UILabel = UILabel.init(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
textName.textColor=UIColor.black
textName.backgroundColor=UIColor.green
textName.text=dataArr[indexPath.row] as? String
print("\(dataArr[indexPath.row])");
textName.font=UIFont.systemFont(ofSize: 14)
cell.addSubview(textName)
let progressView = UIView(frame: CGRect(x: 100, y: 10, width: 100, height: 20))
progressView.backgroundColor = UIColor.red
let customKeys=["type","Facebook","Google","Twitter"];
let customsValues=["uploadFile","Mark","Lary","Goo"];
let customDatas=Dictionary(uniqueKeysWithValues: zip(customKeys,customsValues))
let progressBar = UIProgressView.init(frame: CGRect(x: 0, y: 5, width: 100, height: 20))
progressBar.tag = viewTag(for: indexPath)
progressView.addSubview(progressBar)
cell.addSubview(progressView)
uploadImage(data_dict: customDatas, indexPath: indexPath)
return cell
}
Change your uploadImage method to:
@objc func uploadImage(data_dict : Dictionary<String,String>, indexPath : IndexPath) {
print("click \(data_dict)")
let uploadImg = dataArr[indexPath.row] as! String
let image = UIImage(named: uploadImg)
...
let task = session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
...
})
uploadTasks[task] = indexPath
task.resume()
}
Change your urlSession delegate method to:
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
let uploadProgress:Float = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
print(uploadProgress)
guard let indexPath = uploadTasks[task] else { return }
let viewTag = viewTag(for: indexPath)
guard let progressBar = self.view.viewWithTag(viewTag) as? UIProgressView else { return }
DispatchQueue.main.async {
progressBar.progress = uploadProgress
}
}
Show download progress in UITableviewCell
Go through this tutorial for basic reference: https://www.raywenderlich.com/158106/urlsession-tutorial-getting-started
In section download progress you will see
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
// 1
guard let url = downloadTask.originalRequest?.url,
let download = downloadService.activeDownloads[url] else { return }
// 2
download.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
// 3
let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, countStyle: .file)
// 4
DispatchQueue.main.async {
if let trackCell = self.tableView.cellForRow(at: IndexPath(row: download.track.index,
section: 0)) as? TrackCell {
trackCell.updateDisplay(progress: download.progress, totalSize: totalSize)
}
}
}
How to show progress of download in a custom UITableViewCell with ASIHTTPRequest?
I suppose you could use a class of your own (or even the view controller) as progress delegate. In your view controller you could hold an array of references to all progress bars currently in use. You would create one progress bar per download and hold it in the array (thus the object would not be destroyed). Whenever a cell is being created you now have to check your array for the corresponding progress bar object instead of creating a new one. Whenever a download is done and the progress bar is no longer needed, you remove it from the array.
Beware of possible multithreading issues and if necessary (e.g. if your notifications could arrive in another thread than the main thread) use performSelectorOnMainThread:
to update the progress bar array and the progress bars within.
Cell UIProgressView not update after UITableView scrolls
I created a sample app a few years ago that demonstrates this exact technique. It is not perfect:
https://github.com/chefnobody/Progress
My guess is that you're not correctly modeling the progress of each download. It is important to keep the modeling of that data (and any async requests) separate from the rendering of the table view cells because iOS will set up and tear down each cell as it scrolls off screen. You may also have some threading issues going on, as well.
I would brush up on the UITableViewCell
life cycle events and ensure that you fully understand what's happening when a cell scrolls off screen.
Related Topics
Supportedinterfaceorientations Not Working
Programmatically Checking If a Passcode Lock Is Set
How to Access the Accelerometer from the Apple Watch
Changing Uipageviewcontroller's Page Programmatically Doesn't Update the Uipagecontrol
Custom Font Not Working in Wkwebview Swift
How to Properly Send an Image to Cloudkit as Ckasset
Error Using Swift - Instance Member Cannot Be Used on Type 'Viewcontroller'
Swiftui Foreach Index Out of Range Error When Removing Row
App Running on iPad While Its Iphone-Only
App Is Not Showing in the Share Menu of Shared Options in Shared Extension in iOS8
Downcast from Any to a Protocol
iOS Application Using Facebook -- Rejected
Parse JSON Response with Afnetworking
Add Uiimage on Top of Another Uiimage
Detecting When App Is Becoming Active from Lockscreen VS Other on iOS7
Apple Rejected App 10.6 Because Facebook Opens Safari to Login