Core graphics draw NSString from background thread - nsstring

I use this code to draw an NSString on a UILabel(which i have subclassed already) with Core Graphics from a queue but nothing shows on screen. I get no error.
I call the function like this from a queue.
[label1 createtext];
-(void)createtext
{
UIFont *font=[UIFont fontWithName:#"Times New Roman" size:15.0];
//Core Text (Create Attributed String)
NSMutableArray *Text=[globaltextarray Text];
CGSize size=[[Text objectAtIndex:0] sizeWithFont:font];
if (UIGraphicsBeginImageContextWithOptions != NULL)
UIGraphicsBeginImageContextWithOptions(size,NO,0.0);
else
// iOS is < 4.0
UIGraphicsBeginImageContext(size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
if (ctx != NULL)
NSLog(#"not null context");
UIColor *textColor = [UIColor blackColor];
CGColorRef color = textColor.CGColor;
CTFontRef font = CTFontCreateWithName((CFStringRef) #"TimesNewRomanPSMT", 15.0, NULL);
CTTextAlignment theAlignment = kCTLeftTextAlignment;
CFIndex theNumberOfSettings = 1;
CTParagraphStyleSetting theSettings[1] = {
{ kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment),
&theAlignment }
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(theSettings, theNumberOfSettings);
NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:(id)font, (NSString *)kCTFontAttributeName,color, (NSString *)kCTForegroundColorAttributeName,paragraphStyle, (NSString *) kCTParagraphStyleAttributeName,nil];
NSAttributedString *stringToDraw = [[NSAttributedString alloc] initWithString:[NSString stringWithCString:[[Text objectAtIndex:indexpath1.row] cStringUsingEncoding:NSISOLatin1StringEncoding] encoding:NSUTF8StringEncoding] attributes:attributesDict];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)stringToDraw);
//Prepare our view for drawing
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
CGContextTranslateCTM(ctx, 0, ([self bounds]).size.height );
CGContextScaleCTM(ctx, 1.0, -1.0);
//Create Frame
CGMutablePathRef path = CGPathCreateMutable();
CGRect rect = self.frame;
CGPathAddRect(path, NULL, rect);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
//Draw Frame
CTFrameDraw(frame, ctx);
UIGraphicsEndImageContext();
//Release all retained objects
CFRelease(framesetter);
CFRelease(path);
[stringToDraw release];
}
Any help appreciated!

You are creating a temporary graphics context, drawing into it, then discarding it. You never tell UIKit to put anything on the screen.
You need to also:
create an image from the content of that context, using UIGraphicsGetImageFromCurrentImageContext
on the main thread, put that image into a view, or draw the image into another view
For instance:
// In your class's declaration, define an image view to show your drawing.
// Make sure that this view is created (either as part of a storyboard/nib, or with explicit code)
#property (nonatomic) UIImageView *imageView;
- (void)createText
{
CGSize size = /* you decide */;
UIGraphicsBeginImageContext(size);
// draw into the graphics context
// after you're done:
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}

You are not drawing with the CoreGraphics framework, you are drawing with UIKit and that is not allowed in a background thread. You must only call UIKit methods from the main thread. For example, you are calling UIGraphicsBeginImageContext() but that is part of UIKit. You are unlikely to get any benefit from rendering widget code in a background thread anyway, unless you are doing some truly disconnected offline rendering (like creating a PDF or something) then you should just render your widgets in the main thread.

Related

UICollectionViewController keeps rearranging itself from asynchronous image load

In my application. I get a bunch of image URLs on the main thread like this:
- (void)viewDidLoad
{
[super viewDidLoad];
[self populateCollection];
}
- (void)populateCollection
{
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:[MTGJSONBridge JSONURLWithSetCode:#"LEA"]]
options:kNilOptions
error:nil];
NSLog(#"Json: %#", json);
NSArray *cards = json[#"cards"];
_URLs = [NSMutableArray arrayWithCapacity:cards.count];
for (NSDictionary *card in cards)
{
NSURL *imageURL = [MTGJSONBridge URLWithSetCode:#"LEA" cardName:card[#"imageName"]];
if (imageURL)
[_URLs addObject:imageURL];
}
}
This gets me about 300 URLs in 0.2 seconds. Then I try to load the images from each URL asynchronously like this:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = #"reuseIdentifier";
MTGCardCollectionCell *cell = (MTGCardCollectionCell *)[collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
NSURL *url = _URLs[indexPath.row];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// No explicit autorelease pool needed here.
// The code runs in background, not strangling
// the main run loop.
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_sync(dispatch_get_main_queue(), ^{
// This will be called on the main thread, so that
// you can update the UI, for example.
cell.imageView.image = image;
});
});
return cell;
}
I also have this:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _URLs.count;
}
The collection loads really quickly, and asynchronously too (I can scroll while things are loading, and I see new images pop up). The problem is that as I scroll up and down, even after all the images have loaded, the thing keeps rearranging itself in a random order: I'll be looking at one cell, and then it'll have its image switched with another for no apparent reason. Why is this happening?
I've discovered a very easy solution:
if (cell.imageView.image == nil)
{
NSURL *url = _URLs[indexPath.row];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// No explicit autorelease pool needed here.
// The code runs in background, not strangling
// the main run loop.
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_sync(dispatch_get_main_queue(), ^{
// This will be called on the main thread, so that
// you can update the UI, for example.
cell.imageView.image = image;
});
});
}
else
{
NSLog(#"Cell image isn't nil");
}
All I have to do is check if the cell isn't already loaded. Turns out the - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath method is called whenever a cell comes into view. Even if it was just in the view.

Saving an ALAsset to SQLite3 database - retrieve error

I'm trying to save a list of assets to upload in a sqllite3 db, but when i parse the database and set the assets to an array, then try to use the asset i get a SIGABRT error.
ALAsset *asset = (ALAsset *) assets[indexPath.row];
cell.textLabel.text = [NSString stringWithFormat:#"image%d: ready to upload.",indexPath.row];
cell.detailTextLabel.text = #"1.3MB to folder <server folder>";
[[cell imageView] setImage:[UIImage imageWithCGImage:[asset thumbnail]]];// SIGABRT ERROR
Im saving the ALAsset to the database as a string (TEXT) with UTF8formatting
NSMutableArray *tmpArray = [NSMutableArray alloc]init];
///get sql
[tmpArray addObject:someStringFromSQL];
///end sql loop
assets = [tmpArray mutableCopy];
in the code above I tried:
[[cell imageView] setImage:[UIImage imageWithCGImage:[(ALAsset *) asset thumbnail]]];// SIGABRT ERROR
and that didn't work.
This is the error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString thumbnail]: unrecognized selector sent to instance 0xc0a7800'
Any suggestions?
Also as a side question: Does anyone know how to get the file size (i.e. 1.3MB) from the asset?
BLOCK:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
//do stuff in cell
NSURL *aURL =[NSURL URLWithString:[assets objectAtIndex:indexPath.row]];
[assetsLibrary assetForURL:aURL resultBlock:^(ALAsset *asset){
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithCGImage:[asset thumbnail]];
});
[[NSNotificationCenter defaultCenter] postNotificationName:#"newAssetImageRetrieved" object:nil];
//in this notificaton I'm reloading the data; its putting the tableview in an infinite loop - but the images display...
}
failureBlock:^(NSError *error){
// error handling
NSLog(#"Can't get to assets: FAILED!");
}];
//cell.imageView.image = [UIImage imageWithCGImage:[asset thumbnail]];
cell.textLabel.text = [NSString stringWithFormat:#"image%d: ready to upload.",indexPath.row];
cell.detailTextLabel.text = [NSString stringWithFormat:#"1.3MB to folder %#", [destinations objectAtIndex:indexPath.row]];
//[[cell imageView] setImage:[UIImage imageWithCGImage:[asset thumbnail]]];
return cell;
}
There are a couple of issues with your code sample:
The image retrieval is happening asynchronously, so when you try to update the image, you want to make sure the cell is still visible (and not reused for another NSIndexPath).
In this case, the retrieval from the ALAssetsLibrary will probably be so fast that this isn't critical, but it's a good pattern to familiarize yourself with, because if you're ever retrieving images over the Internet, this issue becomes increasingly important.
Because cells are being reused, if you don't find the image immediately and have to update it asynchronously, make sure you reset the UIImageView before initiating the asynchronous process. Otherwise, you'll see a "flickering" of replacing old images with new ones.
You are using UITableViewCell for your cell. The problem with that is that it will layout the cell based upon the size of the image present by the time cellForRowAtIndexPath finishes.
There are two easy solutions to this. First, you could initialize the cell's imageView to be a placeholder image of the correct size. (I usually have an image called placeholder.png that is all white or all transparent that I add to my project, which is what I used below.) This will ensure that cell will be laid out properly, so that when you asynchronously set the image later, the cell will be laid out properly already.
Second, you could alternatively use a custom cell whose layout is fixed in advance, bypassing this annoyance with the standard UITableViewCell, whose layout is contingent upon the initial image used.
I'd suggest using a NSCache to hold the thumbnails images. That will save you from having to constantly re-retrieve the thumbnail images as you get them from your ALAssetsLibrary as you scroll back and forth. Unfortunately, iOS 7 broke some of the wonderful NSCache memory-pressure logic, so I'd suggest a cache that will respond to memory pressure and purge itself if necessary.
Anyway, putting that all together, you get something like:
#interface ViewController ()
#property (nonatomic, strong) NSMutableArray *assetGroups;
#property (nonatomic, strong) ALAssetsLibrary *library;
#property (nonatomic, strong) ThumbnailCache *imageCache;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.imageCache = [[ThumbnailCache alloc] init];
self.assetGroups = [NSMutableArray array];
self.library = [[ALAssetsLibrary alloc] init];
[self.library enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (!group) {
[self.tableView reloadData];
return;
}
CustomAssetGroup *assetGroup = [[CustomAssetGroup alloc] init];
assetGroup.name = [group valueForProperty:ALAssetsGroupPropertyName];
assetGroup.assetURLs = [NSMutableArray array];
[group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
[assetGroup.assetURLs addObject:[result valueForProperty:ALAssetPropertyAssetURL]];
}
}];
[self.assetGroups addObject:assetGroup];
} failureBlock:^(NSError *error) {
NSLog(#"%s: enumerateGroupsWithTypes error: %#", __PRETTY_FUNCTION__, error);
}];
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.assetGroups.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
CustomAssetGroup *group = self.assetGroups[section];
return [group.assetURLs count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
CustomAssetGroup *group = self.assetGroups[section];
return group.name;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// note, these following three lines are unnecessary if you use cell prototype in Interface Builder
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
CustomAssetGroup *group = self.assetGroups[indexPath.section];
NSURL *url = group.assetURLs[indexPath.row];
NSString *key = [url absoluteString];
UIImage *image = [self.imageCache objectForKey:key];
if (image) {
cell.imageView.image = image;
} else {
UIImage *placeholderImage = [UIImage imageNamed:#"placeholder.png"];
cell.imageView.image = placeholderImage; // initialize this to a placeholder image of the right size
[self.library assetForURL:url resultBlock:^(ALAsset *asset) {
UIImage *image = [UIImage imageWithCGImage:asset.thumbnail]; // note, use thumbnail, not fullResolutionImage or anything like that
[self.imageCache setObject:image forKey:key];
// see if the cell is still visible, and if so, update it
// note, do _not_ use `cell` when updating the cell image, but rather `updateCell` as shown below
UITableViewCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath]; // not to be confused with similarly named table view controller method ... this one checks to see if cell is still visible
if (updateCell) {
[UIView transitionWithView:updateCell.imageView duration:0.1 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
updateCell.imageView.image = image;
updateCell.textLabel.text = asset.defaultRepresentation.filename;
} completion:nil];
}
} failureBlock:^(NSError *error) {
NSLog(#"%s: assetForURL error: %#", __PRETTY_FUNCTION__, error);
}];
}
return cell;
}
#end
The above uses the following classes:
/** Thumbnail cache
*
* This cache optimizes retrieval of old thumbnails. This purges itself
* upon memory pressure and sets a default countLimit.
*/
#interface ThumbnailCache : NSCache
// nothing needed here
#end
#implementation ThumbnailCache
/** Initialize cell
*
* Add observer for UIApplicationDidReceiveMemoryWarningNotification, so it purges itself under memory pressure
*/
- (instancetype)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
self.countLimit = 50;
};
return self;
}
/** Dealloc
*
* Remove observer before removing cache
*/
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
#end
and
/** Custom AssetGroup object
*
* This is my model object for keeping track of the name of the group and list of asset URLs.
*/
#interface CustomAssetGroup : NSObject
#property (nonatomic, copy) NSString *name;
#property (nonatomic, strong) NSMutableArray *assetURLs;
#end
#implementation CustomAssetGroup
// nothing needed here
#end
You have to explore all the code base related to the save and retrieve functionality.
However, here are some good tips.
Save the ALAsset Url instead of saving the entire ALAsset as a string.
Retrieve the ALAsset Url from the database and convert it to NSUrlString.
Use ALAsset Library to load the image or thumbnail back.
Hope this will help you.

ios 7 view with transparent content overlaps previous view

Recently I updated my xcode project to work with iOS 7, but i faced a big problem. Because my whole application has only one background image (UIImageView added to key window) and all views are transparent, I face a problem when pushing UIViewController, because pushed view controller overlaps previous view (you can see it in the picture here: http://grab.by/qp0k). I can predict that this is because in iOS 7 push transition has been changed, because now it slides half a screen. Maybe anyone knows how to fix this issue?
This is how I set my key windows
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UIImageView *background = [[UIImageView alloc]initWithFrame:[[UIScreen mainScreen] bounds]];
background.image = [UIImage imageNamed:#"background.png"];
UINavigationController *navi = [[UINavigationController alloc]initWithRootViewController:self.viewController];
self.window.rootViewContro‌​ller = navi;
[self.window makeKeyAndVisible];
Afterwards when user clicks on "start workout" button I push my next view as always:
workoutView *w = [[workoutView alloc]initWithNibName:#"workoutView" bundle:nil];
[self.navigationController pushViewController:w animated:YES];
I did this.
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.view setAlpha:0];
}
Do not forget re set alpha when come back.
- (void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.view setAlpha:1];
}
I solved the problem by implementing the new UINavigationControllerDelegate Method animationControllerForOperation.
For example:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
PushTransition* transition = [PushTransition new];
[transition setNavigationControllerOperation: operation];
return transition;
}
PushTransition is a class that implements the UIViewControllerAnimatedTransitioning protocol and the two methods transitionDuration and animateTransition from that protocol. Additionally, i have added a property to pass the operation (tells me if it is a push or pop transition).
Just put the animation code for moving the views into the animateTransition as follows:
// the containerView is the superview during the animation process.
UIView *container = transitionContext.containerView;
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
CGFloat containerWidth = container.frame.size.width;
// Set the needed frames to animate.
CGRect toInitialFrame = [container frame];
CGRect fromDestinationFrame = fromView.frame;
if ([self navigationControllerOperation] == UINavigationControllerOperationPush)
{
toInitialFrame.origin.x = containerWidth;
toView.frame = toInitialFrame;
fromDestinationFrame.origin.x = -containerWidth;
}
else if ([self navigationControllerOperation] == UINavigationControllerOperationPop)
{
toInitialFrame.origin.x = -containerWidth;
toView.frame = toInitialFrame;
fromDestinationFrame.origin.x = containerWidth;
}
// Create a screenshot of the toView.
UIView *move = [toView snapshotViewAfterScreenUpdates:YES];
move.frame = toView.frame;
[container addSubview:move];
[UIView animateWithDuration:TRANSITION_DURATION delay:0
usingSpringWithDamping:1000 initialSpringVelocity:1
options:0 animations:^{
move.frame = container.frame;
fromView.frame = fromDestinationFrame;
}
completion:^(BOOL finished) {
if (![[container subviews] containsObject:toView])
{
[container addSubview:toView];
}
toView.frame = container.frame;
[fromView removeFromSuperview];
[move removeFromSuperview];
[transitionContext completeTransition: YES];
}];
described it and you can you are done. Additionally you can make any push or pop animation you like.
I fixed it by doing this when initialising the view:
self.view.clipsToBounds = YES;
You might want to look into a new iOS7 feature that allows you to define your own custom UIViewController transitions. Look in the docs for UIViewControllerTransitioningDelegate. Also, here's a link to an article about it: http://www.doubleencore.com/2013/09/ios-7-custom-transitions/
Ah, now I understand the issue. You were right, seems to be caused by the previous UIViewController not being hidden after the transition (because of the new transition effect).
There doesn't seem to be any SDK method to control this behavior. Short of redesigning the app to not requiring the background be static, you'll probably have to roll your own navigation. OSNavigationController is a complete reimplementation of UINavigationController that might help you out. If they haven't updated to the iOS 7 transition, you'll probably be good to go. If they have you can always use an older version.
I had the same problem. Try to load your background image in the init method. For me, it worked (sometimes):
For example:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
self.view.backgroundColor = [UIColor whiteColor];
[self.imageBack setImage:[UIImage imageNamed:#"mayBack.png"]];
}
return self;
}
However, you could see glimpses..
The best solution I found, beside implementing the new iOS7 transition protocol, is to implement a category, and use that category whenever you need it.
You can find the answer here
Setting the image to the background color solved the issue:
self.view.backgroundColor =
[UIColor colorWithPatternImage:[UIImage imageNamed:#"mainback.png"]];
Take a look at the UINavigationController category in that post (it solved me problem) :
https://stackoverflow.com/a/18882232/2826409
Props to #snoersnoer.
Here is the code in Swift 3.
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let pushTransition = SUPushTransition()
pushTransition.navigationControllerOperation = operation
return pushTransition
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// the containerView is the superview during the animation process.
let container = transitionContext.containerView
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toVC = transitionContext.viewController(forKey:UITransitionContextViewControllerKey.to);
if let from = fromVC,
let fromView = from.view,
let to = toVC,
let toView = to.view {
let containerWidth = container.frame.size.width
// Set the needed frames to animate.
var toInitialFrame = container.frame
var fromDestinationFrame = fromView.frame
if self.navigationControllerOperation == .push {
toInitialFrame.origin.x = containerWidth;
toView.frame = toInitialFrame;
fromDestinationFrame.origin.x = -containerWidth;
}
else if self.navigationControllerOperation == .pop {
toInitialFrame.origin.x = -containerWidth;
toView.frame = toInitialFrame;
fromDestinationFrame.origin.x = containerWidth;
}
// Create a screenshot of the toView.
if let move = toView.snapshotView(afterScreenUpdates: true) {
move.frame = toView.frame
container.addSubview(move)
UIView.animate(withDuration: Constants.MainPage.navControllerDuration, delay: 0.0, usingSpringWithDamping: 1000, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
move.frame = container.frame;
fromView.frame = fromDestinationFrame;
}, completion: { (finished) in
if finished {
if !container.subviews.contains(toView) {
container.addSubview(toView)
}
toView.frame = container.frame
fromView.removeFromSuperview()
move.removeFromSuperview()
transitionContext.completeTransition(true)
}
})
}
}
}
Cheers.

iOS table preview image release

I'm testing an RSS on my iPhone. It uses 0 nib files. I'll try to describe it as best as I can, and will post code if its required, but I bet its a common phenomena with a common solution. The issue is in a tableviewcontroller, and the solution probably needs to be implemented in the CellForRowAtIndexPath method. If I scroll down, preview images stay in their respective spots until the async queue loads the correct image for that cell. So if I have an image for array item 1, and I scroll down to array item 20, the image for array item 1 will still be there until my queue catches up and loads that image. How can I release the images from cells that I am not viewing? Thank you for your time.
Here is my CellForRow...
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CustomCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier];
}
ArticleItem *object = _objects[indexPath.row];
cell.primaryLabel.text = object.title;
cell.secondaryLabel.text = object.strippedDescription;
cell.primaryLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.primaryLabel.numberOfLines = 0;
cell.primaryLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.secondaryLabel.numberOfLines = 0;
//Async dispatch queue for image preview loading...
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *preview = nil;
if (object.iG = nil)
{
preview = [UIImage imageNamed:#"CellLogo.png"];
}
else
{
preview = [UIImage imageWithData:object.iG];
}
dispatch_sync(dispatch_get_main_queue(), ^{
[[cell myImageView] setImage:preview];
[cell setNeedsLayout];
});
});
return cell;
}
If you gather, I have an ArticleItem class which pulls the image URLS and turns them into data , and I have a CustomCell class which does what its called.
CustomCell.h
#interface CustomCell : UITableViewCell {
UIImageView *myImageView;
}
#property(nonatomic,strong)UIImageView *myImageView;
#end
=====================================================
CustomCell.m
- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithFrame:frame reuseIdentifier:reuseIdentifier]) {
// Initialization code
myImageView = [[UIImageView alloc]init];
[self.contentView addSubview:myImageView];
}
return self;
}
-(void)viewDidUnload {
myImageView = nil;
primaryLabel = nil;
secondaryLabel = nil;
}
Implement a subclass of UITableViewcell, make a property for imageview. As soon as it gets away from visibility, it will be released. Describing just the overview as you may yourself need to see the usage upon scrolling.

Cocos2d: Is it ok to have a pointer to a parent's class pointer?

Silly question. Cocos2d is build around the parent-child hierarchy. I was wondering if it is ok to have a parent class (e.g. GameScene) and initialize a child class (e.g. SpriteHandler) with a pointer to a member of the parent class (e.g. CCSpriteBatchNode*).
I am trying to do this to optimize the number of CCSpriteBatchNodes. Here is a code snippet of my main class (GameScene style).
#import <Foundation/Foundation.h>
#import "cocos2d.h"
enum ShooterSceneLayerTags {
HudLayerTag = 0,
};
#interface ShooterScene : CCLayer {
CCSpriteBatchNode* sharedSpriteBatchNode;
}
-(id) initWithSharedBatchNodeReference:( CCSpriteBatchNode*) sharedSpriteBatchNode;
+ (id) sceneWithId:(int)sceneId;
#end
#import "ShooterScene.h"
#import "MainMenuScene.h"
//Layers
#import "LevelSpritesLayer.h"
#import "HudLayer.h"
#interface ShooterScene (PrivateMethods)
-(void) addLayers:(int)sceneId;
-(void) loadGameArtFile;
-(BOOL) verifyAndHandlePause;
#end
#implementation ShooterScene
+ (id) sceneWithId:(int)sceneId
{
CCScene *scene = [CCScene node];
ShooterScene * shooterLayer = [[self alloc] initWithId:sceneId];
[scene addChild:shooterLayer];
return scene;
}
-(id) initWithId:(int)sceneId
{
if ((self = [super init]))
{
//Load game art before adding layers - This will initialize the batch node
[self loadGameArtFile:sceneId];
//Will add sprites to shared batch node for performance
[self addLayers:sceneId];
[self addChild:sharedSpriteBatchNode];
//Do other stuff..
[self scheduleUpdate];
}
return self;
}
-(void) addLayers:(int)sceneId
{
LevelSpritesLayer * levelData = [LevelSpritesLayer node];
[levelData initWithSharedBatchNodeReference:sharedSpriteBatchNode];
[self addChild:levelData];
switch (sceneId) {
case 1:
[levelData loadLevelOneSprites];
break;
case 2:
[levelData loadLevelTwoSprites];
break;
default:
break;
}
HudLayer * hud = [HudLayer node];
[hud setUpPauseMenu];
[self addChild:hud z:1 tag:HudLayerTag];
}
-(BOOL) verifyAndHandlePause
{
HudLayer * hud = [self getChildByTag:HudLayerTag];
if(hud.pauseRequested){
[[CCDirector sharedDirector] replaceScene:[MainMenuScene scene]];
return true;
}
else {
return false;
}
}
-(void) update:(ccTime)delta
{
if([self verifyAndHandlePause]==false)
{
//Continue with animation etc..
}
}
/**
This is tricky. Could have loaded this in LevelData but as I am expecting to use the same SpriteSheet for HudLayer as well then
I prefer to have the control here of this. Also, the same sheet could be used for more level, hence specific function is not bad
**/
-(void) loadGameArtFile:(int) sceneId
{
CCSpriteFrameCache* frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
[frameCache addSpriteFramesWithFile:#"game-art-hd.plist"];
sharedSpriteBatchNode = [CCSpriteBatchNode batchNodeWithFile:#"game-art-hd.png"];
}
//As dealloc is deprecated, I prefer to remove unused sprites and texture on cleanup
-(void) cleanup
{
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
}
And here is one of the loadLevelData methods, it uses the sharedSpriteBAtchNodeReference
-(void) loadLevelOneSprites
{
//Just a proof of concept example
testSprite = [CCSprite spriteWithSpriteFrameName:#"File0.png"];
testSprite.anchorPoint = CGPointMake(0.5f, 0.5f);
testSprite.position = CGPointMake(160.0f, 240.0f);
[sharedSpriteBatchNodeReference addChild:testSprite];
}
You can do that but if you are using ARC you should make your sharedSpriteBatchNode a weak pointer. If you don't you could end up with a circular reference.
What will happen with the circular reference is when the Director releases your game scene after it has finished running your game scene will still have your child retaining it and your game scene will still be retaining that child. This circle will float off and never be able to be released as it is abandoned.
What [Ben] said. It should not be a retaining or strong reference, the latter being the default in ARC for instance variables.
There is one way to ensure under ARC that you're retain-cycle-safe even if you use a strong reference. Override the cleanup method and nil the reference there (under MRC you should also call release here, if you retained the reference):
-(void) cleanup
{
sharedSpriteBatchNode = nil;
[super cleanup];
}
Doing this in dealloc won't work. As long as a child node has a strong reference to a parent, it will not be deallocated. So you need to do this in cleanup, and ensure that all method calls where you can set the cleanup flag has that parameter set to YES.
But there are other, and I think better, solutions than passing a parent node in the initializer. For example you could get the shared batch node via a common tag from the parent, and either do that every time you need it (wrap it into a small function) or store it in a weak (non-retaining) instance var:
// onEnter is typically called right after init (during addChild)
// parent is already set here
-(void) onEnter
{
[super onEnter];
CCSpriteBatchNode* sharedBatchNode = [parent getChildByTag:kSharedBatchNodeTag];
}
Or get the parent and cast it, assuming the sharedBatchNode to be a property of the parent class:
-(void) whereEver
{
ShooterScene* scene = (ShooterScene*)parent;
CCSpriteBatchNode* sharedBatchNode = scene.sharedSpriteBatchNode;
…
// you can also reduce the above to a single line:
CCSpriteBatchNode* batch = ((ShooterScene*)parent).sharedSpriteBatchNode;
}
Especially this latter solution is recommended. Even if you need to do that often it's fast. Casting is free, property access no more than a message send. Just be sure that the parent is in fact an object of the class you're casting it to.

Resources