My problem: I have a ABUnknownPersonViewController which needs to get an image from an online database. I have implemented an NSSession to download the image. The thread adds the image to the ABRecordRef then adds it to the ABUnknownPersonViewController. When the controller is pushed on the stack, it doesn't show the image...therfore sadness ensues.
NSString *imageUrl = dict[#"mug"];
__block NSURL *url = [NSURL URLWithString:imageUrl];
__block NSData *urlData = nil;
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session =
[NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
NSURLSessionDownloadTask *getImageTask =
[session downloadTaskWithURL:[NSURL URLWithString:imageUrl]
completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error) {
urlData = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
newPerson = controller.displayedPerson;
ABPersonSetImageData(newPerson, (__bridge CFDataRef)(urlData), &anError);
controller.displayedPerson = newPerson;
if(urlData != nil) {
NSLog(#"I got here");
[self viewWillAppear:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:#"com.razeware.imagegrabber.imageupdated" object:(__bridge id)(newPerson)];
}
});
}];
[getImageTask resume];
**Oddly enough, if I choose "Create New Contact" the image appears (So, the thread is working?). If I click cancel on iPhone then the image appears on the ABUnknownPersonViewController. So, it seems like the controller just needs to be refreshed. How?
I've tried [[self view] setNeedsDisplay]; //ain't working'
Help please!
OK - This may not be the best practice. However, since you already have [self viewWillAppear:YES]; you can try calling [self viewWillDisappear:NO]; before viewWillAppear. This is likely to refresh the content.
Sourced from here.
I had a similar problem recently and got this from somewhere else. Try adding a dispatch sync after the async to the main queue like this:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
// your image fetch code here
});
});
I found a solution:
ABRecordRef cannot be updated in a thread. It must be updated on the main thread.
Related
I have a class for get xml and parse it.
+ (instancetype)sharedRssNewsLoader
{
static BGMRssNewsLoader *sharedRssNewsLoader = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedRssNewsLoader = [[self alloc] initPrivate];
});
return sharedRssNewsLoader;
}
- (instancetype)initPrivate
{
self = [super init];
//did the superclass's designated initializer succeed?
if (self)
{
self.rssNewsItems = [[NSMutableArray alloc] init];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config
delegate:nil
delegateQueue:nil];
//RSS call
[self fetchFeed];
}
return self;
}
- (void)fetchFeed
{
NSString *requestString = #"http://xxx...";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
self.rssXmlString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
DDLogVerbose(#"rssXmlString: %#", self.rssXmlString);
dispatch_sync(dispatch_get_main_queue(), ^{
[self parseXML:self.rssXmlString];
});}
];
[dataTask resume];
}
//parse XML
- (void)parseXML:(NSString *)source
{
NSError *error = nil;
DDXMLDocument *theDocument = [[DDXMLDocument alloc] initWithXMLString:source
options:0
error:&error];
NSArray *xmlItems = [theDocument nodesForXPath:#"//item"
error:&error];
for(DDXMLElement *itemElement in xmlItems)
{
NSString *itemTitleValue = [[[itemElement elementsForName:#"title"] lastObject] stringValue];
NSString *itemDescriptionValue = [[[itemElement elementsForName:#"title"] lastObject] stringValue];
BGMRssNewsEntry *rssEntry = [[BGMRssNewsEntry alloc] initRssNewsEntryWithTitle:itemTitleValue
rssText:itemDescriptionValue
rssUrl:nil
rssDate:nil
isRssRead:NO];
[self.rssNewsItems addObject:rssEntry];
}
}
//if a programmer calls [[BGMItemsStore alloc] init], let him know the error of his ways
- (instancetype) init
{
#throw [NSException exceptionWithName:#"Singleton"
reason:#"You are wrong! Use +[BBGMRssNewsLoader sharedRssNewsLoader]"
userInfo:nil];
return nil;
}
I would like to show result in the table. So in the other class I do:
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self)
{
//custom initialization
//initialization sharedRssNewsLoader with rssItems array and RSS call
[BGMRssNewsLoader sharedRssNewsLoader];
}
return self;
}
But my table is absolutely clear. I know that I need to switch to main_queue when I am working with NSURLSessionDataTask, I did it. I think that I mess with thread :(
That BGMRssNewsLoader is asynchronously loading the data and in parseXML you are adding entries to the self.rssNewsItems object. But I don't see where it tells the table to reload itself when this asynchronous request done. If your table view's data source tries to immediately retrieve the rssNewsItems array, it will likely be empty until the asynchronous request is complete.
Generally, when you're done loading the data, you'd call [self.tableView reloadData] (either by supplying the tableview as a parameter during the instantiation of the BGMRssNewsLoader, or by having it post a notification that the table view controller is observing, or by supplying a completion block parameter in which you'd perform reloadData).
I'd also suggest temporarily adding a breakpoint or log statement where you add objects to the rssNewsItems array, just to make sure that that the XML feed was successfully retrieved and parsed.
For example, I might retire fetch and define a method like so:
- (void)fetchFeedWithCompletionBlock:(void (^)(NSArray *items, NSError *error))completion
{
NSString *requestString = #"http://xxx...";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
self.rssXmlString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
DDLogVerbose(#"rssXmlString: %#", self.rssXmlString);
dispatch_sync(dispatch_get_main_queue(), ^{
[self parseXML:self.rssXmlString];
if (completion) {
completion(self.rssNewsItems, nil);
}
});
}];
[dataTask resume];
}
I would, by the way, not have the initPrivate method initiate the fetch. You want the caller to be able to supply whatever completion block it wants.
I'd then have the tableview's controller do something like:
[[BGMRssNewsLoader sharedRssNewsLoader] fetchFeedWithCompletionBlock:^(NSArray *items, NSError *error) {
// use the items array and/or error
[self.tableView reloadData];
}];
Clearly, the fetchFeedWithCompletionBlock should actually detect errors and call the completion block with the appropriate NSError (so the table view can decide how it wants to present the error ... the singleton shouldn't be doing anything with the UI itself), but hopefully this illustrates the idea of a network object that provides a completion block to allow the caller to specify what it wants to do when the asynchronous request is complete.
I'm new to ios7 background fetch. Here is how I'm doing it.
This is what is going on in appDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *url = [[NSURL alloc] initWithString:#"http://sample.com/api/home/lastnews"];
OldJson = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
}
Note that here I'm storing the whole JSON file in a string so i can use to check for new content.
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
//HomepageView is the view controller that is supposed to be updated
HomepageView *homepage = [[HomepageView alloc] init];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURL *url = [[NSURL alloc] initWithString:#"http://sample.com/api/home/lastnews"];
NewJson = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
completionHandler(UIBackgroundFetchResultFailed);
return;
}
if (![NewJson isEqualToString:OldJson])
{
completionHandler(UIBackgroundFetchResultNewData);
NSLog(#"New Data : %#",NewJson);
}
else
{
completionHandler(UIBackgroundFetchResultNoData);
NSLog(#"Old Json : %#",OldJson);
}
}];
// Start the task
[task resume];
}
Now to test the whole process, i simply run the application. Then i send it to background and then when i have new content in the JSON file, i use the Simulate Background Fetch operation from the Debug menu. Then i open the application but nothing has changed.
First store your newJson in NSUserDefaults as seen below
if (![NewJson isEqualToString:OldJson])
{
completionHandler(UIBackgroundFetchResultNewData);
NSLog(#"New Data : %#",NewJson);
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:NewJson forKey:#"newJson"];
}
Second in your View Controller add this in the ViewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(refresh:) name: UIApplicationWillEnterForegroundNotification object:nil];
Add this to your viewWillAppear
-(void)viewWillAppear:(BOOL)animated{
[self loadData];
}
Create the loadData
- (void)loadData {
// contains information the ViewController makes use of
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
textView.text = [defaults objectForKey:#"newJson"];
}
Finally add your action
-(IBAction)refresh:(id) sender {
NSLog(#"I ran");
[self loadData];
}
I tested this and it works.
Three problems:
There's no guarantee that JSON data comes in UTF-8 format. Use [NSData dataWithContensOfURL:...]
Never, ever access a URL from the main thread. If the server doesn't respond, your app will be hanging.
Really bad: In your background fetch handler, you actually load the URL synchronously. And then you create a task to load it again. So you load it actually twice.
I'm currently working on an app that uses a basic login page to check if the user has access to the app. I've made the login part with help of this tutorial which uses this frame work to use username and password from a simple web script.
I'm hoping that someone else maybe have worked with it or can help me with my issue. I wan't to show an activity indicator, I'm using MBProgressHUD as an activity indicator.
So I've experimented with it but can't get it to show the activity indicator when the app is connecting to the URL. I've done some bad connection simulating with the login process but the activity indicator won't show up when the app is connecting to the URL. It only shows on errors and the only thing that shows any kind of loading on success is that the login buttons pressed state is "active" (blue highlighted) until the loading is done.
So here's my code that runs when the user has typed in username and password and clicks on the login button:
// Login button
- (IBAction)loginBtnClicked:(id)sender
{
// Show the activity indicator
[HUD showUIBlockingIndicatorWithText:#"Loggar in..."];
#try {
if([[userNameTxtField text] isEqualToString:#""] || [[passwordTxtField text] isEqualToString:#""] ) {
// No username or password entered
[self alertStatus:#"Du måste ange användarnamn och lösenord" :#"Något gick fel!"];
// Hide activity indicator
[HUD hideUIBlockingIndicator];
} else {
NSString *post =[[NSString alloc] initWithFormat:#"username=%#&password=%#",[userNameTxtField text],[passwordTxtField text]];
NSLog(#"PostData: %#",post);
NSURL *url=[NSURL URLWithString:#"http://www.nebulon.se/json/sendus/jsonlogin.php"];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSString *postLength = [NSString stringWithFormat:#"%d", [postData length]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:url];
[request setHTTPMethod:#"POST"];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:postData];
//[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:[url host]];
NSError *error = [[NSError alloc] init];
NSHTTPURLResponse *response = nil;
NSData *urlData=[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(#"Response code: %d", [response statusCode]);
if ([response statusCode] >=200 && [response statusCode] <300){
NSString *responseData = [[NSString alloc]initWithData:urlData encoding:NSUTF8StringEncoding];
NSLog(#"Response ==> %#", responseData);
SBJsonParser *jsonParser = [SBJsonParser new];
NSDictionary *jsonData = (NSDictionary *) [jsonParser objectWithString:responseData error:nil];
NSLog(#"%#",jsonData);
NSInteger success = [(NSNumber *) [jsonData objectForKey:#"success"] integerValue];
NSLog(#"%d",success);
if(success == 1){
// Login success, grant user access to app
NSLog(#"Login SUCCESS");
[self loginSuccess];
// Hide activity indicator
[HUD hideUIBlockingIndicator];
// Store username
NSString *userName = [userNameTxtField text];
NSUserDefaults *UserDefaults = [NSUserDefaults standardUserDefaults];
[UserDefaults setObject:userName forKey:#"userName"];
[UserDefaults synchronize];
[self dismissViewControllerAnimated:NO completion:nil];
} else {
// Login error
NSString *error_msg = (NSString *) [jsonData objectForKey:#"error_message"];
[self alertStatus:error_msg :#"Inloggningen misslyckades"];
[self loginFailed];
// Hide activity indicator
[HUD hideUIBlockingIndicator];
}
} else {
// Login error
if (error) NSLog(#"Error: %#", error);
[self alertStatus:#"Ingen nätverksuppkoppling hittades." :#"Ett fel har inträffat!"];
[self loginFailed];
// Hide activity indicator
[HUD hideUIBlockingIndicator];
}
}
}
#catch (NSException * e) {
// Login error
NSLog(#"Exception: %#", e);
[self alertStatus:#"Inloggningen misslyckades." :#"Ett fel har inträffat!"];
[self loginFailed];
// Hide activity indicator
[HUD hideUIBlockingIndicator];
}
}
I believe the issue is due to the use of sendSynchronousRequest:returningResponse:error:
This method will be blocking the main/UI thread so the HUD never actually gets a chance to show until the method has returned, at which point the code continues and the HUD is hidden.
I think you should be looking at using an asynchronous request. and implementing the NSURLConnection delegate methods.
EDIT: Added code sample.
Assuming you're targeting iOS 5 and higher you can use the following code snippet which takes advantage of blocks with sendAsynchronousRequest:queue:completionHandler: and GCD.
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request
queue:backgroundQueue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// NOTE: This block is called on the background queue.
// Use GCD to get back onto the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// This block will process the response and data on the main thread
});
}];
It really is very little work to port your existing code to use this mechanism. If you don't know how blocks work you should read the documentation as they are a very powerful language feature and are being used in an increasing amount of Apple and third-party frameworks.
I would also recommend staying AWAY from third-party networking libraries for now until you understand the nuances that can cause issues such as this.
So I am trying to lazyload a user pictures for a custom UITableView (BubbleTableView)
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
NSLog(#"Success");
UIBubbleTableViewCell *cell = (UIBubbleTableViewCell *)[self.bubbleTableView cellForRowAtIndexPath:indexPath];
NSBubbleData *data = [[NSBubbleData alloc] init];
data = [cell data];
data.avatar = [UIImage imageNamed:#"send.png"];
[self.profileImages setObject:image forKey:[self.commUsers objectAtIndex:x]];
//Success
[cell setData:data];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
//Failed
NSLog(#"ERROR: %#", response);
}];
[operation start];
I am trying to change the Avatar to another image and I am not using the image pulled from the net rather just an image I have stored locally (for purposes of narrowing image update problem).
So if I put the
UIBubbleTableViewCell *cell = (UIBubbleTableViewCell *)[self.bubbleTableView cellForRowAtIndexPath:indexPath];
NSBubbleData *data = [[NSBubbleData alloc] init];
data = [cell data];
data.avatar = [UIImage imageNamed:#"send.png"];
[self.profileImages setObject:image forKey:[self.commUsers objectAtIndex:x]];
//Success
[cell setData:data];
Inside the AFImageRequestOperation Block, the image doesn't update. However, if I put the exact same code outside the Block, it updates the image. I feel like I am missing something on how Blocks work. How do I fix this?
Thanks!
Try to run the UI code in the block on the main thread:
if ([NSThread isMainThread]) {
// We're currently executing on the main thread.
// We can execute the block directly.
createBubbleTableViewCell();
} else {
//non-blocking call to main thread
dispatch_sync(dispatch_get_main_queue(), createBubbleTableViewCell);
}
The checking if you are on the main thread is just for case - preventing deadlock. Also you can use dispatch_async for blocking call.
The UI code should be run always on the main thread.
I want to click a button to start downloading image and to update my UIImageView to new image once it is updated. The problem with my code is that it only downloads stuff, and not updates. It only updates if I click it again.
I want it to update the image some time in future, when the image is downloaded. How do I do that?
Edit: I have found the wrong code, changing it a bit helped and it all works.
Here comes another question - how do I simplify this code without turning it a mess? It looks excessive.
- (IBAction)getImage
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSURL *imageURL = [NSURL URLWithString:#"http://example.com/1.jpg"];
__block NSData *imageData;
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_sync(dispatch_get_main_queue(), ^{
self.image = [UIImage imageWithData:imageData];
});
});
});
self.imageView.image = self.image;
}
You are setting the imageView before the image is done downloading, you need to move the logic into the block. Also there is no reason for you to do an extra dispatch_sync inside of your dispatch_async.
- (IBAction)getImage
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSURL *imageURL = [NSURL URLWithString:#"http://example.com/1.jpg"];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
//This is your completion handler
dispatch_sync(dispatch_get_main_queue(), ^{
//If self.image is atomic (not declared with nonatomic)
// you could have set it directly above
self.image = [UIImage imageWithData:imageData];
//This needs to be set here now that the image is downloaded
// and you are back on the main thread
self.imageView.image = self.image;
});
});
//Any code placed outside of the block will likely
// be executed before the block finishes.
}
Check out https://github.com/rs/SDWebImage
I use it to download images in the background with progress notification. It can be added simply to your project using Cocoapods (http://cocoapods.org).
There are several other async image loaders available on Cocoapods and GitHub if that doesn't work for you.
This is what I've been using although it doesn't provide any progress which I think is often useful. This is simple through.
- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, NSData *image))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ( !error )
{
completionBlock(YES,data);
NSLog(#"downloaded FULL size %lu",(unsigned long)data.length);
} else{
completionBlock(NO,nil);
}
}];
}