XCode 4.5.2; I'm downloading an image from a remote server like this :
- (void)viewDidLoad
{
[super viewDidLoad];
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(loadImage)
object:nil];
[queue addOperation:operation];
}
- (void)loadImage{
self.theobject = [RemoteQuery loadObjectWithImage:self.imageKey];
[self performSelectorOnMainThread:#selector(displayImage) withObject:nil waitUntilDone:YES];
}
-(void)displayImage{
UIImage *image = [UIImage imageWithData: self.theobject.imageData];
[self.imageView setImage:image];
}
This works fine on IOS simulator, but doesn't work on a device; it seems like displayImage is called before the data is loaded from [RemoteQuery loadImage]. What would be the best way to ensure that the image has loaded properly before showing it ?
Create a delegate protocol which will call back to the original object when the image download finishes. This NSOperation tutorial has more details on how to do this
Alternatively, use the NSOperation's completionBlock to perform the image display.
Related
I'm trying to disable the navigation bar button when I start the app and after I finish the process(fetching data), I enable it back but unfortunately it won't enable.
Please where would be my issue? While I'm putting enable to YES and when I debug it I can see that it enabling it to YES.
- (void)viewDidLoad {
UIImage *searchBtn = [UIImage imageNamed:#"search_icon.png"];
barButtonSearch = [[UIBarButtonItem alloc] initWithImage:[searchBtn imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] style:UIBarButtonItemStylePlain target:self action:#selector(searchButton)];
UIImage *menuBtn = [UIImage imageNamed:#"menu_icon.png"];
barButtonMenu = [[UIBarButtonItem alloc] initWithImage:[menuBtn imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] style:UIBarButtonItemStylePlain target:self action:#selector(menuButton)];
self.navigationItem.rightBarButtonItem = barButtonMenu;
self.navigationItem.leftBarButtonItem = barButtonSearch;
barButtonMenu.enabled = NO;
barButtonSearch.enabled = NO;
}
- (void)unhide{
if (!(barButtonSearch.enabled && barButtonMenu.enabled)) {
barButtonMenu.enabled = YES;
barButtonSearch.enabled = YES;
}
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
ViewController *theInstance = [[ViewController alloc] init];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
_dic = (NSDictionary *)responseObject;
[theInstance unhide];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Err");
}];
[operation start];
return YES;
}
}
Well there it is!
You are initializing your ViewController but that doesn't call your viewDidLoad: method. The viewDidLoad: method gets called when your ViewController is either Pushed or Presented! That is the time when the view gets loaded into the memory.
Therefore, the barButtons are never created and you are unable to see them.
So either make your network call inside the viewDidLoad: method of your ViewController
OR
Push the instance of your ViewController and then call the method unhide.
Edit
Since you are using Storyboards and not pushing any ViewController from AppDelegate, you need to use reference of your ViewController.
replace this in your - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions method
ViewController *theInstance = (ViewController *)[(UINavigationController*)self.window.rootViewController topViewController];
You are calling [theInstance unhide] from the completion block of the AFHTTPOperation - this will almost certainly be executed on a background queue.
All UI operations must be performed on the main queue.
You should use -
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
_dic = (NSDictionary *)responseObject;
dispatch_async(dispatch_get_main_queue(),^{
[theInstance unhide];
});
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Err");
}];
Update
Your main problem is that theInstance points to an instance of your view controller that isn't on the screen - It is just an instance you have allocated but not actually presented.
Assuming this view controller is the initial view controller loaded by your application you can get a reference to the correct instance using [UIApplication sharedApplication].keyWindow.rootViewController
Remove this method from the completion block of app delegate.
[theInstance unhide];
And add some delegate function which will be activated after the asynchronous call completes its task. And add that unhide method there (In your view controller may be).
Is there a best practice or library that helps cache processed images (i.e. images that have been created while the app is running) in iOS? I use SDWebImage for images that I download, but in various places in the app I blur or in other ways process some of these images. I would like to store the processed images in a cache so that I can access them easily rather than reprocess each time a user opens that image. What's the best way to do this?
Thanks!
The answer it seems is using NSCache. It's quite straightforward to do. I ended up with a subclass of NSCache to make sure memory warnings are handled.
Implementation of NATAutoPurgeCache (heavily based on other posts on StackOverflow)
#implementation NATAutoPurgeCache
+ (NATAutoPurgeCache *)sharedCache
{
static NATAutoPurgeCache *_sharedCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedCache = [[self alloc] init];
});
return _sharedCache;
}
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
#end
And using it when needed for an image: (in this case to store a blurred image)
UIImage* blurImage = [myCache objectForKey:#"blurred placeholder image"];
if (!blurImage)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage* blurImage = self.activityPic.image;
blurImage= [blurImage applyLightEffect];
dispatch_async(dispatch_get_main_queue(), ^{
self.activityPic.image = blurImage;
});
[myCache setObject:blurImage forKey:#"blurred placeholder image"];
});
}
else {
self.activityPic.image = blurImage;
}
I have a UITableView that loads thumbnails into cells aynchronously as follows:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:
^{
ThumbnailButtonView *thumbnailButtonView = [tableViewCell.contentView.subviews objectAtIndex:i];
UIImage *image = [self imageAtIndex:startingThumbnailIndex + i];
[self.thumbnailsCache setObject: image forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
UITableViewCell *tableViewCell = [self cellForRowAtIndexPath:indexPath];
if (tableViewCell)
{
[activityIndicatorView stopAnimating];
[self setThumbnailButtonView:thumbnailButtonView withImage:image];
}
}];
}];
[self.operationQueue addOperation:operation];
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
As per a technique I learned in a WWDC presentation, I store all of my operation queues in a NSCache called operationQueues so that later on I can cancel them if the cell scrolls off the screen (there are 3 thumbnails in a cell):
- (void) tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger startingThumbnailIndex = [indexPath row] * self.thumbnailsPerCell;
for (int i = 0; i < 3; i++)
{
NSNumber *key = [[NSNumber alloc] initWithInt:i + startingThumbnailIndex];
NSOperation *operation = [self.operationQueues objectForKey:key];
if (operation)
{
[operation cancel];
[self.operationQueues removeObjectForKey:key];
}
}
}
However, I notice if I repeatedly launch, load, then close my UITableView, I start recieving memory warnings, and then eventually the app crashes. When I remove this line:
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
The memory issues go away. Does anyone have any clue on why storing the operation queues in a cache or an array causes the app to crash?
Note: I learnt about NSCache and NSOperationQueue a couple of days ago so I might be wrong.
I don't think that this is a problem with NSOperationQueue, you are adding images to your thumbnailsCache but when the view scrolls off-screen they are still in memory. I am guessing that when the cells scroll back in, you re-create your images. This is probably clogs your memory.
Also, shouldn't you be caching your images instead of your operations?
EDIT
I did some detailed testing with NSCache by adding images and strings until my app crashed. It doesn't seem to be evicting any items so I wrote my custom cache, which seems to work:
#implementation MemoryManagedCache : NSCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(reduceMemoryFootprint) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)reduceMemoryFootprint
{
[self setCountLimit:self.countLimit/2];
}
#end
I'm new to IOS I need to implement NSThread in my program, but when it invoked it shows an SIGABRT error. my current code is given below
XMLParser.m
-(void)loadXML
{
categories =[[NSMutableArray alloc]init];
NSString *filepath =[[NSBundle mainBundle]pathForResource:#"cd_catalog" ofType:#"xml"];
NSData *data =[NSData dataWithContentsOfFile:filepath];
parser=[[NSXMLParser alloc]initWithData:data];
parser.delegate =self;
[parser parse];
}
ViewController.m
- (void)viewDidLoad
{
xmlParser =[[XMLParser alloc]init];
NSThread *myThread =[[NSThread alloc]initWithTarget:self selector:#selector(loadXML) object:nil];
[myThread start];
[super viewDidLoad];
}
please tell me what is wrong with my program
Use this code to solve your problem...
ViewController.m
- (void)viewDidLoad
{
NSThread *myThread =[[NSThread alloc]initWithTarget:self selector:#selector(doParsing) object:nil];
[myThread start];
[super viewDidLoad];
}
-(void)doParsing
{
xmlParser =[[XMLParser alloc]init];
[xmlParser loadXML];
}
Instead of creating a NSThread object you can start a thread using
//performSelectorInBackground:withObject: is NSObject's method
[self performSelectorInBackground:#selector(loadXML) withObject:nil];
I didn't find any buggy code but enable NSZombie and seee which object is causing this.
loadXML is not define on ViewController so your thread code should be changed to use an instance of XMLParser instead of self like so:
XMLParser *parser = [[XMLParser alloc] init];
NSThread *thread = [[NSThread alloc] initWithTarget:parser selector:#selector(loadXML) object:nil];
[thread start];
Since Apple introduced GCD, you may solve it without creating any NSThread instance.
dispatch_async(dispatch_get_global_object(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self loadXML];
});
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"app/site_pattern" usingBlock:^(RKObjectLoader* loader) {
[loader setObjectMapping:clientMappring];
loader.delegate = self;
shopLoader = loader;
}];
Above, I use the block function to load some data in my app, but when I pop this viewcontroller, I don't know when and how to cancel this request .
Any idea?
- (void)showSelectShop
{
SelectShopViewController * selectShopViewController = [[SelectShopViewController alloc] initWithNibName:#"SelectShopViewController" bundle:nil];
[self.navigationController pushViewController:selectShopViewController animated:YES];
}
More:
I try to cancel it in the viewDidUnload
- (void)viewDidUnload
{
[super viewDidUnload];
[shopLoader cancel];
}
But it didn't work. I still getting error.
I solved this by adding
- (void)viewWillDisappear:(BOOL)animated
{
[shopLoader cancel];
shopLoader.delegate = nil;
shopLoader = nil;
}
I still want to know if I don't want to cancel this request in viewWillDisappear, which function do those lines should be written in?