Custom Uitableviewcell with Progress Bar Download Update

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.

  1. you do not initialize a new progressView for each cell, but share one view for all cells
  2. 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.
  3. your progressView will have multiple UIProgressBars as subview. One for each time tableView(_:cellForRowAt indexPath:) is called
  4. 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.

  1. create a UITableViewCell subclass for your cell
  2. create a ViewModel class for that cell
  3. store a viewModel instance for each of your cells in your viewController (or better in a separate TableViewDataSource object)
  4. 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



Leave a reply



Submit