Update UITableView after background work done - objective-c

I'm new to iOS developing and have an silly problem
I have an project to parse RSS feed and it all works fine but I tried to fetch images in the background to improve the UI and stuck on how to update the UIImageView in my TableView cells?
Here the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CustomCell";
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.titleLabel.text = [[feeds objectAtIndex:indexPath.row] objectForKey:#"title"];
NSString* test = [[feeds objectAtIndex:indexPath.row] objectForKey:#"url"];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(doNow:)
object:test];
[queue addOperation:operation];
cell.myImages = theImagePropery;
return cell;
}
-(void)doNow:(NSString*)myData
{
NSString* url = myData;
url = [url stringByReplacingOccurrencesOfString:#" " withString:#""];
NSURL* imageURL = [NSURL URLWithString:url];
NSData* imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage* image = [[UIImage alloc]initWithData:imageData];
[self performSelectorOnMainThread:#selector(displayImage:) withObject:image waitUntilDone:NO];
}
Now in displayImage method I don't know what to do and thats my last try:
-(void)displayImage:(UIImage *)myImage{
[[self tableView] beginUpdates];
theImageProbery = [[UIImageView alloc] initWithImage:myImage];
[[self tableView] endUpdates];
What can I do?

Take a look at the LazyTableImages sample.
It's dated, and you can replace the IconDownloader class with NSURLSesssionDataTask, but it will give you the basic idea of how to structure your code.

Do not use the synchronous method dataWithContentsOfURL to request network-based URLs.This method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience. Use the NSURLSession methods ( if your are targeting iOS 7 and later).
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler
or use the NSURLConnection method (is you are targeting iOS 6 and later).
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
PS : The best is to use open source librairies that handle all this for you ( downloading the images in a background thread, caching,..).
AFNetworking+UIImageView

Related

Objective-C How to play a sound when a cell's button is tapped?

I have a button on each cell and while the button is tapped I want to play different audio file from array for each cell.
I don't know how to implement an array of sounds/audio files for my table view cells.
I have a button outlet and action on my UITableViewCell. But I am not sure how to initialise my AVaudioplayer in tableview.m specifically in:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell22";
Below I've inserted an image of the project I want to achieve.
image1
You can try like this:
Import AVFoundation.framework
Place this code in didSelectRowAtIndexPath method
NSString *soundPath = [[NSBundle mainBundle] pathForResource:#"your sound name" ofType:#"mp3"];
NSURL *soundUrl = [NSURL fileURLWithPath:soundPath];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundUrl error:nil];
[player play];
Note: Add your audio files to project and then take the names into one array. Then you can use your audio names based on indexPath.
If you want to play sound on button click which is in tableViewCell then add tag as indexPath to that button and add action to button like this in cellForRowAtIndexPath method
button.tag = indexPath.row;
[button addTarget:self action:#selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
and paste the same code in button action. but use sender.tag instead of indexPath.row
-(void)buttonPressed:(UIButton *)sender
{
NSString *soundName = [yourArray objectAtIndex:sender.tag];
NSString *soundPath = [[NSBundle mainBundle] pathForResource:soundName ofType:#"mp3"];
NSURL *soundUrl = [NSURL fileURLWithPath:soundPath];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundUrl error:nil]; [player play];
}
check now i updated code :its playing sound
1.create property in .h or .m file for ur AVAudioPlayer . like
#property (strong, nonatomic) AVAudioPlayer *audioPlayer;
declare <AVAudioPlayerDelegate> in .m file and
#synthesize audioPlayer;
3.implement this code in :
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath{
yourSoundArray=[NSArray arrayWithObjects:#"sss",#"sss",#"sss",#"sss",#"sss",#"sss", nil];//dont include formate type
NSString *audioPath = [[NSBundle mainBundle] pathForResource: [yourSoundArray objectAtIndex:indexPath.row] ofType:#"mp3"];
NSLog(#"%#",audioPath);
NSURL *audioURL = [NSURL fileURLWithPath:audioPath];
NSError *audioError = [[NSError alloc] init];
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:&audioError];
audioPlayer.delegate=self;
if (!audioError) {
NSLog(#"playing!");
audioPlayer play];
}
else {
NSLog(#"Error!");
}
4.
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
NSLog(#"%d",playing the audio);
}
happy coding its working please check :
Mate You need to use two things mostly
1st import AVFounfation.framwork
2nd Do code id "didSelectRowAtIndexPath"
for more details
Xcode 6 - Press button to play sound
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *selectedCell=[tableView cellForRowAtIndexPath:indexPath];
NSLog(#"%#",selectedCell.textLabel.text);
AVAudioPlayer *audioPlayer101;
if ([_detailPhrases isEqualToString:#"Basics"]){
NSArray *soundArray = [NSArray arrayWithObjects:#"cellTapSound", #"Second", nil];
NSString *audioPath = [[NSBundle mainBundle] pathForResource: [soundArray objectAtIndex:indexPath.row] ofType:#".mp3"];
NSURL *audioURL = [NSURL fileURLWithPath:audioPath];
NSError *audioError = [[NSError alloc] init];
audioPlayer101 = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:&audioError];
if (!audioError) {
[audioPlayer101 play];
NSLog(#"playing!");
}
else {
NSLog(#"Error!");
}
}
}

Reload table after request was finished

I have a table view that use result of fetching data with NSURLSession as a datasource. Here is my NSArray which is responsible about that table.
#property (strong, nonatomic) NSMutableArray *results;
And this is my delegate and datasource method
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_results count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
// Configure the cell...
WordResult *word = (WordResult *)[_results objectAtIndex:indexPath.row];
cell.textLabel.text = word.defid;
return cell;
}
In my viewDidLoad, I fetched request from Mashape and try to map the result into my custom class WordResult
Here is my fetch method
#pragma mark - GET Request
- (void)fetchDataFromMashape:(NSURL *)URL {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
[request setHTTPMethod:#"GET"];
[request setValue:API_KEY_MASHAPE forHTTPHeaderField:API_MASHAPE_HEADER_1];
[request setValue:API_ACCEPT forHTTPHeaderField:API_MASHAPE_HEADER_2];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
self.results = [self processResultData:jsonResult];
}];
[task resume];
}
- (NSMutableArray *)processResultData:(NSDictionary *)dict {
NSArray *list = [dict objectForKey:#"list"];
NSMutableArray *tempListOfWord = [[NSMutableArray alloc] init];
if (list) {
for (NSDictionary *item in list) {
WordResult *word = [[WordResult alloc] initWithDictionary:item];
[tempListOfWord addObject:word];
}
}
NSLog(#"Result array of word: %#", tempListOfWord);
return tempListOfWord;
}
My problem is, I dont know where to reload data after my result array was assigned by fetch method and dismissing my progress HUD that I showed on my viewDidLoad.
Here is my viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
[SVProgressHUD show];
[self fetchDataFromMashape:_finalURLrequest];
}
So where should I put [SVProgressHUD dismiss] and [self.tableView reloadData] after my request has been finished?
Any help would be appreciated.
Reload your table on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
A great tableView coding pattern is putting reload call in setters of model objects, then you won't miss any data change. For example setter for result in your case:
-(void)setResult:(NSMutableArray*)result{
_result = result;
[self.tableView reloadData]
[SVProgressHUD dismiss]
}
Try this:
You can put [SVProgress dismmis] and also reload your tableview [self.tableView reloadData] after self.results = [self processResultData:jsonResult];
In your viewdidload before calling fetchdata do
self.results=[[NSMutableArray alloc]init];
And then call your [self.tableView reloadData] within the completion block after assigning the array with data
And then call svprogress hud hide method outside the block after [task resume] in the fetch data function

create image programmatically in objective-c based on coreData info

I have a table in my ViewController, in this table I will get some information from webService and CoreData, this information comes in each rows, when I press in each row, I want to go to the detail view and create image,
I have card and in each card I have different stamp, when I'm going to detailView I want to create this images, I mean if my card has 2 stamp I want to create 2 image, if 3 stamp I want to create 3 image,
would you please help me in this implementation,
Thanks in advance!
Here is info that I got from web service:
createdAt = 1362412409;
id = 2;
stampNumber = 2;
},
{
createdAt = 1362069567;
id = 1;
stampNumber = 10;
}
Here is my Code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath
*)indexPath
{
static NSString *CellIdentifier = #"CardsCell";
CardCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil){
NSArray *objects = [[NSBundle mainBundle] loadNibNamed:#"CardCell" owner:nil
options:nil];
for (id currentObject in objects)
{
if([currentObject isKindOfClass:[UITableViewCell class]])
{
cell = (CardCell *) currentObject;
break;
}
}
}
card = self.cards[indexPath.row];
cell.stampId.text = [[NSString alloc] initWithFormat:#"%#",card.stampNumber];
cell.createdAt.text = [[NSString alloc] initWithFormat:#"%#",card.createdAt];
cell.CardId.text = [[NSString alloc] initWithFormat:#"Carte %d",(indexPath.row+1)];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:#"cardDetail" object:nil
userInfo:nil];
}
my question is how should I create this pictures programatically based on my stamp..
Edit:
I have this image:
UIImageView *stampIMG = [[UIImageView alloc] initWithFrame:self.view.frame];
stampIMG = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"1.jpg"]];
stampIMG.frame = CGRectMake(0, 0, 122, 122);
I just want to use this image
You should store a path to the image location not the NSData for the image. Then use stampId to get that image path and load in your image. If the image is on the server then download the image. You could store the image as NSData in CoreData but this blob could be large that's why it's typically better to just store the path to the image. So in CoreData you could have an object Stamp which has stampId and ImagePath. Then fetch the object by stampId and then get the imagePath out of the object.
I'd probably try something like this to get the image by stamp id. Substitute document directory vs main bundle if your images are downloaded.
NSFetchRequest * stampById = [[NSFetchRequest alloc] init];
[stampById setEntity:[NSEntityDescription entityForName:#"Card" inManagedObjectContext:self.managedObjectContextBKG]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"stampId == %#", stampId];
[stampById setPredicate:predicate];
NSError *error = nil;
NSArray * stampResults = [self.managedObjectContext executeFetchRequest:stampById error:&error];
if(nil != stampResults && [stampResults count]){
Stamp *stamp = [stampResults objectAtIndex:0];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: stamp.imagePath ofType:#"png"]];
}

GCD UITableView asynchronous load images, wrong cells are loaded until new image download

I have a UITableView with custom cells. I load images asynchronously using Grand Central Dispatch. Everything works fine, but when I scroll down, previously loaded images are shown till the new image is downloaded. Here is my code:
if (![[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingPathComponent:#"image.png"]])
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSString *url=[pat stringByAppendingPathComponent:#"comments.txt"];
NSString *u=[NSString stringWithContentsOfFile:url encoding:NSUTF8StringEncoding error:nil];
NSURL *imageURL=[NSURL URLWithString:u];
NSData *image=[NSData dataWithContentsOfURL:imageURL];
[image writeToFile:[pat stringByAppendingPathComponent:#"image.png"] atomically:YES];
dispatch_sync(dispatch_get_main_queue(), ^{
cell.imageView.image=[UIImage imageWithContentsOfFile:[pat stringByAppendingPathComponent:#"image.png"]];
[cell setNeedsLayout];
NSLog(#"Download");
});
});
}
else
{
NSLog(#"cache");
cell.imageView.image=[UIImage imageWithContentsOfFile:[pat stringByAppendingPathComponent:#"image.png"]];
}
Any suggestions appreciated.
P.S. I reuse the cells
Rather than capturing the cell you need to capture the index path, then get the cell back using:
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
That way, if the cell is now off screen you'll get nil back and the image won't be set on the wrong cell.
The other thing you need to add after your dispatch_async() is a cell.imageView.image=somePlaceholderImage.
E.g.:
if (![[NSFileManager defaultManager] fileExistsAtPath:[path stringByAppendingPathComponent:#"image.png"]])
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSString *url=[pat stringByAppendingPathComponent:#"comments.txt"];
NSString *u=[NSString stringWithContentsOfFile:url encoding:NSUTF8StringEncoding error:nil];
NSURL *imageURL=[NSURL URLWithString:u];
NSData *image=[NSData dataWithContentsOfURL:imageURL];
[image writeToFile:[pat stringByAppendingPathComponent:#"image.png"] atomically:YES];
dispatch_sync(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image=[UIImage imageWithContentsOfFile:[pat stringByAppendingPathComponent:#"image.png"]];
[cell setNeedsLayout];
NSLog(#"Download");
});
});
cell.imageView.image=[UIImage imageNamed:#"placeholder"];
}
else
{
NSLog(#"cache");
cell.imageView.image=[UIImage imageWithContentsOfFile:[pat stringByAppendingPathComponent:#"image.png"]];
}
In your - (void)tableView:(UITableView *)aTableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { You need to clear out the image, or reset it to your spinner. Since table view rows are reused, this is the behavior you will see.
Doesn't UITableViewCell define -(void) prepareForReuse for that purpose?
Override it and clear your imageView in there.

Accurate progress displayed with UIProgressView for ASIHTTPRequest in an ASINetworkQueue

Summary: I want to track the progress of file downloads with progress bars inside cells of a tableview.
I'm using ASIHTTPRequest in an ASINetworkQueue to handle the downloads.
It works, but the progress bars stay at 0%, and jump directly at 100% at the end of each download.
Details:
I set up my ASIHTTPRequest requests and ASINetworkQueue this way:
[Only an extract of my code]
- (void) startDownloadOfFiles:(NSArray *) filesArray {
for (FileToDownload *aFile in filesArray) {
ASIHTTPRequest *downloadAFileRequest = [ASIHTTPRequest requestWithURL:aFile.url];
UIProgressView *theProgressView = [[UIProgressView alloc] initWithFrame:CGRectMake(20.0f, 34.0f, 280.0f, 9.0f)];
[downloadAFileRequest setDownloadProgressDelegate:theProgressView];
[downloadAFileRequest setUserInfo:
[NSDictionary dictionaryWithObjectsAndKeys:aFile.fileName, #"fileName",
theProgressView, #"progressView", nil]];
[theProgressView release];
[downloadAFileRequest setDelegate:self];
[downloadAFileRequest setDidFinishSelector:#selector(requestForDownloadOfFileFinished:)];
[downloadAFileRequest setDidFailSelector:#selector(requestForDownloadOfFileFailed:)];
[downloadAFileRequest setShowAccurateProgress:YES];
if (! [self filesToDownloadQueue]) {
// Setting up the queue if needed
[self setFilesToDownloadQueue:[[[ASINetworkQueue alloc] init] autorelease]];
[self filesToDownloadQueue].delegate = self;
[[self filesToDownloadQueue] setMaxConcurrentOperationCount:2];
[[self filesToDownloadQueue] setShouldCancelAllRequestsOnFailure:NO];
[[self filesToDownloadQueue] setShowAccurateProgress:YES];
}
[[self filesToDownloadQueue] addOperation:downloadAFileRequest];
}
[[self filesToDownloadQueue] go];
}
Then, in a UITableViewController, I create cells, and add the name of the file and the UIProgressView using the objects stored in the userInfo dictionary of the request.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"fileDownloadCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"FileDownloadTableViewCell" owner:self options:nil];
cell = downloadFileCell;
self.downloadFileCell = nil;
}
NSDictionary *userInfo = [self.fileBeingDownloadedUserInfos objectAtIndex:indexPath.row];
[(UILabel *)[cell viewWithTag:11] setText:[NSString stringWithFormat:#"%d: %#", indexPath.row, [userInfo valueForKey:#"fileName"]]];
// Here, I'm removing the previous progress view, and adding it to the cell
[[cell viewWithTag:12] removeFromSuperview];
UIProgressView *theProgressView = [userInfo valueForKey:#"progressView"];
if (theProgressView) {
theProgressView.tag = 12;
[cell.contentView addSubview:theProgressView];
}
return cell;
}
The progress bar are all added, with the progress set to 0%.
Then, at end of download, they instantly jump to 100%.
Some of the download are very big (more than 40Mb).
I do not do anything tricky with threads.
Reading the forums of the ASIHTTPRequest, it seems I'm not alone, but I couldn't find a solution.
Am I missing something obvious? Is this a bug in ASI* ?
ASIHTTPRequest can only report progress if the server is sending Content-Length: headers, as otherwise it doesn't know how big the response will be. (ASINetworkQueue also sends HEAD requests at the start to try to figure out document sizes.)
Try collecting all the network traffic with charlesproxy or wireshark, see if these headers are present and/or what is happening with the HEAD requests.