I'm working on an iPhone game and currently the game uses many UIImageViews to show space ships, bullets and other things. The game probably has 30-40 different subviews in the view controller and I've decided that I should probably programmatically add subviews when they are needed rather than starting the game with them in the hidden state and making them unhidden when needed (for performance issues) so here are my questions:
(1) will there be a significant performance gain from adding subviews when needed?
(2) how do I release a subview when ARC is enabled? Im trying to add the views (based on type) into an NSMutableArray when its created to hold it, and removing it from the array when I'm done using it, does removing it from the array deallocate that memory?
heres the code (theres an NSTimer that runs the movement method every 0.03 seconds, and the get and set methods are on an "instance variable" NSMutableArray);
-(void)movement {
if (shootButton3.isHighlighted) {
NSMutableArray *array = [self getBulletArray];
[self setarray:array];
}
[self shootBullet: [self getArray]];
}
-(NSMutableArray *)getBulletArray {
UIImageView *bullet = [[UIImageView alloc] initWithFrame:CGRectMake(ship.center.x + 20, ship.center.y, 15, 3)];
bullet.image = [UIImage imageNamed:#"bullet2.png"];
bullet.hidden = NO;
[self.view addSubview:bullet];
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:bullet];
return array;
}
-(void)shootBullet: (NSMutableArray *)bulletArray {
for (int i = 0; i < [bulletArray count]; i++) {
UIImageView *bullet = [bulletArray firstObject];
bullet.center = CGPointMake(bullet.center.x + 4, bullet.center.y);
if (bullet.center.x > 500) {
bullet.hidden = YES;
[bulletArray removeObjectAtIndex:0];
}
}
}
-(void)setarray:(NSMutableArray *) array {
bulletArray = array;
}
-(NSMutableArray *)getArray {
return bulletArray;
}
To answer your two questions:
Question 1: Yes, there will be a performance increase for sure, regardless of how large the UIImageViews are.
Question 2: Simply set the imageView to nil: self.imageView = nil;
Related
Aster finding myself in the situation of wanting to disable a UIPickerView component and not finding any answers that uses the default functionality but tries to go around it by using labels or multiple UIPickerViews, I start poking around the UIPickerView structure in order to find a more direct approach to solve the problem by using the OS already present functionality.
So these is my answer, what I found by trial and error, and the way that I am using it:
The method:
- (void)dissableUIPickerViewComponent:(int)componentToDissable forPickerView:(UIPickerView *)pickerView{
NSArray *pickerViews = [[NSArray alloc] initWithArray:[pickerView subviews]];
UIView *mainSubview;
for (int count = 0; count < pickerViews.count; count++) {
UIView *temp = [pickerViews objectAtIndex:count];
if (temp.frame.size.height > 100) {
mainSubview = temp;
}
}
NSArray *componentSubview = [mainSubview subviews];
while ([[[[componentSubview objectAtIndex:componentToDissable] subviews] firstObject] superview].gestureRecognizers.count) {
[[[[[componentSubview objectAtIndex:componentToDissable]subviews]firstObject]superview] removeGestureRecognizer:[[[[[componentSubview objectAtIndex:componentToDissable]subviews]firstObject]superview].gestureRecognizers objectAtIndex:0]];
}
}
Best place to call:
- (void)viewDidAppear:(BOOL)animated{
[self dissableUIPickerViewComponent:0 myPickerView];
}
I hope these will help others that found themself in the same position that I did :)
I guess I'm missing something obvious, but I simply can't figure out how to obtain the label of a CCMenuItemFont.
Background
I'm building a simple hangman game for the iPad. For entering the next guess, I've added 26 buttons to the UI (one for each letter of the alphabet) and wired them all to the same event handler.
Now, inside the event handler, I'd like to obtain the label of the button to update the current guess, but CCMenuItemFont apparently doesn't respond to text or label.
Problem
So - what method can I use to obtain the label of a CCMenuItem?
Code
Code for creating the buttons:
-(void)addButtons {
NSArray* charArray = [NSArray arrayWithObjects:
#"A",#"B",#"C",#"D",#"E",#"F",#"G",#"H",#"I",#"J",#"K",
#"L",#"M",#"N",#"O",#"P",#"Q",#"R",
#"S",#"T",#"U",#"V",#"W",#"X",#"Y",#"Z", nil];
[CCMenuItemFont setFontName:#"Marker Felt"];
[CCMenuItemFont setFontSize:45];
NSMutableArray* buttonArray = [NSMutableArray array];
for (unsigned int i=0; i < [charArray count]; ++i) {
CCMenuItemLabel* buttonMenuItem = [CCMenuItemFont
itemWithString:(NSString*)[charArray objectAtIndex:i]
target:self selector:#selector(buttonTapped:)];
buttonMenuItem.color = ccBLACK;
buttonMenuItem.position = ccp(60 + (i/13)*40, 600 - (i%13)*40);
[buttonArray addObject:buttonMenuItem];
}
CCMenu *buttonMenu = [CCMenu menuWithArray:buttonArray];
buttonMenu.position = CGPointZero;
[self addChild:buttonMenu];
}
And the event handler:
- (void)buttonTapped:(id)sender {
// Get a reference to the button that was tapped
CCMenuItemFont *button = (CCMenuItemFont *)sender;
[_guess addObject:[button text]]; // this throws an exception because text is the wrong method
[self paintCurrentGuess];
}
You're adding to your menu a CCMenuItemLabel, not a CCMenuItemFont (which actually extends the first one). In both cases, you need to access to the inner label containing the text.-
CCMenuItemLabel *button = (CCMenuItemLabel *)sender;
NSString *label = button.label.string;
THIS QUESTION HAS BEEN RE-WRITTEN IN ORDER TO PROPERLY COMMUNICATE THE INTENTIONS OF THE AUTHOR
I have a MasterDetail App that will eventually become a way to track an amount of "players" through a fixed amount of "challenges" (60 to be exact). The challenges are the same for every player, while the status of each player on the individual challenges may vary. All of the data about the players are being stored on a Postgres Database and then fetched using the JSONModel in the LeftViewController. I am getting all of the data correctly from the DB and I have successfully put it into a NSMutableDictionary. The data in the Dictionary looks like this (this is one player):
player = (
{
id = 9;
name = "Arthur Dent";
currentChallenge = "Cooking";
timeStarted = "04 Jul 2013 1:08 PM";
challengesDone = (
{
challengeName = "Flying";
timeCompleted = "04 Jul 2013 12:08 PM";
}
);
nextChallenge = "Swimming";
freePass = (
Running,
"Climbing"
);
},
As you can see, the player has some challenges "done" ("done"), some he has doesn't need to do ("pass") on, one he is doing ("currentChallenge") and the one challenge that he will do next ("nextChallenge"). For each of these states that a player can be in, I want the "Challenges" that are in the RightViewController to be color coded so you can understand what is happening at a glance. I need the "challenges" to light up different colors, based on the status of the player, which is determined by the data that is in my NSMutableDictionary.
By creating arrays from the data in the NSDictionary I can get the colors to change by doing this:
if ([freePassArray containsObject:#"challenge three name"])
{
UIImage *image = [UIImage imageNamed:#"status_pass.png"]; //status_pass.png is my color
[challengeThreeImageView setImage:image];
}
But if I did it like this, I would have to write 240 if and else if statements to cover all possible statuses on all 60 challenges.
My arrays only contain the data of the player selected because I pass the data over like so:
*LeftViewController.m * didSelectRowAtIndexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
//Re-fetch the feed from the Postgres Database when a user selects an entry
[JSONHTTPClient getJSONFromURLWithString:#"http://myurl" completion:^(NSDictionary *json, JSONModelError *err) {
NSError* error = nil;
_feed = [[PostgresFeed alloc] initWithDictionary:json error:&error];
[[NSNotificationCenter defaultCenter] postNotificationName:#"myNotification" object:nil userInfo:[[json objectForKey:#"preclear"] objectAtIndex:indexPath.row]];
}];
Player *selectedPlayer = [_players objectAtIndex:indexPath.row];
if (_delegate)
{
[_delegate selectedPlayer:selectedPlayer];
}
}
So the images wont set for all of the players at once, but they also do not clear after another player is selected.
How do I get the colors to change without writing 240 if and else if statements? and how would I get the colors to clear when I select another player?
Assuming you have all the 50 images and you know which challenge corresponds to which image I would put the status image on top of the actual challenge image:
- (void)setChallengesDone:(NSArray*)doneAr next:(NSArray*)nextAr current:(NSArray*)currentAr andPass:(NSArray*)passAr
{
// remove additions from the previous selected player
UIView *prevAdditionsView = [self.view viewWithTag:122333];
while (prevAdditionsView != nil)
{
[prevAdditionsView removeFromSuperview];
prevAdditionsView = [self.view viewWithTag:122333];
}
for (int i=0; i<[doneAr count]; i++)
{
[self addChallengeImageAdditionWithImage:#"status_done" forChallenge:[doneAr objectAtIndex:i]];
}
for (int i=0; i<[nextAr count]; i++)
{
[self addChallengeImageAdditionWithImage:#"status_next" forChallenge:[nextAr objectAtIndex:i]];
}
for (int i=0; i<[currentAr count]; i++)
{
[self addChallengeImageAdditionWithImage:#"status_current" forChallenge:[currentAr objectAtIndex:i]];
}
for (int i=0; i<[passAr count]; i++)
{
[self addChallengeImageAdditionWithImage:#"status_free" forChallenge:[passAr objectAtIndex:i]];
}
}
- (void)addChallengeImageAdditionWithImage:(NSString*)image forChallenge:(NSString*)challengeTitle
{
UIImageView *challengeImage = [challenges objectForKey:challengeTitle];
CGRect frame = challengeImage.frame;
UIImageView *statusImg = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:image]] autorelease];
[statusImg setFrame:frame];
[statusImg setAlpha:0.5]; // optionally add some transparency
[statusImg setTag:122333];
[self.view addSubview:statusImg];
[self.view bringSubviewToFront:challengeImage]; // bring challenge image to front
}
The simplest way to map your challenge titles with the UIImageViews is to maintain a NSDictionary with the challenge titles as keys, and a references to their corresponding UIImageViews as values.
This is an excellent example of the model/view concept.
Each challenge is a model with all the info required (challenge name, status, timestamp and others).
Each of your 50 views is a "challenge view" that knows how to render any challenge.
The status data is in the model; the status image is in the view.
Example:
-(void) drawChallenge: (Challenge*) challenge
{
// Draw challenge name as text
switch ([challenge status])
{
case done: // draw "done" image
case next: // draw "next" image
}
}
The idea is that the challenge itself should not know about how it is rendered, and the view should not know about what the challenge contains. The view is responsible for drawing it.
This could be packed into a ChallengeView subclass of UIView. Then, create your 50 or so ChallengeViews, and call [currentView drawChallenge: currentChallenge].
Essentially, when a new challenge is added, you create a view for it, and give the view a pointer to the challenge. When the challenge view is rendered by iOS, it'll draw "its" challenge as you have described in the drawChallenge method.
If you need a primer on the Model-View-Controller pattern, this section of the Apple docs is pretty good. You can disregard the Controller role in this situation.
I have about 10 draggable objects in my game that need to be removed entirely from the game at certain points, no use of these objects is required until a new game is started...
currently i say self.imageView = nil
However, the more images i discard this way, the slower the game becomes. I believe it is because the images are not completely gone and they are all being put at (0, 0) even though they are out of the view.
How else can i get rid of these image views in order to increase my performance?
Here is how I add the images to my view:
#interface GameView : UIView
{
UIImage *ball;
}
#property UIImageView *redBall;
-(id)initWithBallImage:(UIImageView *)ball;
#implementation GameView
-(id)initWithBallImage:(UIImageView *)ball
{
self = [super init]
if (self)
{
_redBall = ball;
return self;
}
return nil;
}
-(void)spawnBallWithColor:(BallColor)ballColor intoArray:(NSMutableArray *)array atPoint:(CGPoint)point
{
switch (ballColor) {
case kRedBall:
ball = [UIImage imageNamed:#"redBall.png"];
self.redBall = [[UIImageView alloc] initWithImage:ball];
self.redBall.center = point;
[array addObject:self.redBall];
[self addSubview:self.redBall];
break;
}
//I use the above method in an initWithLevel: method...
Then to remove the object from the view...
[self.redBall removeFromSuperview];
[self.redBall removeFromSuperview];
You also need
self.redBall = nil;
And you need to remove it from that array you added it to.
Additionally
self.redBall = [[UIImageView alloc] initWithImage:ball];
Is leaking memory if you aren't using automatic reference counting.
I have an app the loaded many view controllers in a scroll view depending on the number of objects the user has in a tableview. So when I flip between the tableview and the scroll view, the number of view controllers in the scroll view changes according to how many objects the user has in the tableview.
I use the code in Apple's PageControl sample code to build the scroll view with many view controllers inside it, after some modification of course.
- (void)loadScrollViewWithPage:(int)page
{
if (page < 0) return;
if (page >= kNumberOfPages) return;
// replace the placeholder if necessary
MainViewController *countdownController = [viewControllers objectAtIndex:page];
if ((NSNull *)countdownController == [NSNull null])
{
id occasion = [eventsArray objectAtIndex:page];
countdownController = [[MainViewController alloc] initWithPageNumber:page];
[countdownController setOccasion:occasion];
[viewControllers replaceObjectAtIndex:page withObject:countdownController];
[countdownController release];
}
// add the controller's view to the scroll view
if (nil == countdownController.view.superview)
{
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
countdownController.view.frame = frame;
[scrollView addSubview:countdownController.view];
}
}
The problem is the number of living view controllers (MainViewController here) keeps increasing when I flip between the table view and the scroll view (according to Instruments) even though I didn't add any new objects which causes memory problems of course.
I tried so many things in viewWillDisappear of the scroll view like:
- (void) viewWillDisappear:(BOOL)animated
{
//test unloading all views
//Remove all subviews
[[scrollView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
//[[scrollView subviews] makeObjectsPerformSelector:#selector(release)];
//[viewControllers removeAllObjects];
for (unsigned m = 0; m < [viewControllers count]; m++)
{
//[[viewControllers objectAtIndex:m] makeObjectsPerformSelector:#selector(release)];
[viewControllers removeObjectAtIndex:m];
}
}
But it didn't work.
Here is a recording of how the app works youtube.com/watch?v=5W8v_smZSog
And this is the viewWillAppear method of the scroll view:
- (void)viewWillAppear:(BOOL)animated
{
eventsArray = [[NSMutableArray alloc] init];
kNumberOfPages = [self.dataModel occasionCount];
//update the eventsArray from the dataModel
//Fill in the events Array with occasions form the data model
for (unsigned r = 0; r < kNumberOfPages; r++)
{
Occasion* occasion = [self.dataModel occasionAtIndex:r];
[eventsArray insertObject:occasion atIndex:r];
}
// view controllers are created lazily
// in the meantime, load the array with placeholders which will be replaced on demand
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < kNumberOfPages; i++)
{
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
[controllers release];
// a page is the width of the scroll view
scrollView.pagingEnabled = YES;
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * kNumberOfPages, scrollView.frame.size.height);
scrollView.showsHorizontalScrollIndicator = NO;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.scrollsToTop = NO;
scrollView.delegate = self;
pageControl.numberOfPages = kNumberOfPages;
pageControl.currentPage = currentPage;
[self loadScrollViewWithPage:0];
[self loadScrollViewWithPage:1];
}
UPDATE: Video recording of Instruments http://www.youtube.com/watch?v=u1Rd2clvMQE&feature=youtube_gdata_player
And a screen shot showing the responsible caller:
Thank you.
This is for you if you don't want to use UIPageViewController (read my other answer).
The sample project is designed for a constant number of pages (kNumberOfPages). The scrollview content size and the size of the view controller array depends on the number of pages. The sample code set this up in awakeFromNib, which is called only once.
So in order to make this dynamic you could recreate the whole ContentController when the number of pages changes. You just need to add a property for the number of pages.
The other option would be to reset the scrollview and view controller array when the number of pages changes.
I'm assuming you have defined a property for the events:
#property(nonatomic,retain) NSArray* eventsArray;
You could then add a setter method like this:
-(void)setEventsArray:(NSArray *)eventsArray
{
if (eventsArray != _eventsArray) {
[_eventsArray release];
_eventsArray = [eventsArray retain];
NSUInteger eventCount = [eventsArray count];
//reset scrollview contentSize
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * eventCount, scrollView.frame.size.height);
// reset content offset to zero
scrollView.contentOffset = CGPointZero;
//remove all subviews
[[scrollView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
pageControl.numberOfPages = eventCount;
// reset viewcontroller array
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < eventCount; i++)
{
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
[controllers release];
[self loadScrollViewWithPage:0];
[self loadScrollViewWithPage:1];
}
}
You call this method from the table view controller at the time when the user switches to the scroll view.
Apple's PageControl sample code is 2 years old and you can consider it as deprecated because there is a new container view controller in iOS 5 that does all this: UIPageViewController.
You should really start using UIPageViewController, then you don't need that loadScrollViewWithPage method at all. It would be less code and more easy.
Take a look at the PhotoScroller sample code. It has been updated to take full advantage of UIPageViewController.
It doesn't look like you are implementing Apple's View Controller Containment pratices. It would make memory management that much easier and safer.
Plus, hoping that it might save you a lot of future headaches, there is already an open source project that does what you are describing (implementing a self-managing scrollview of an arbritary number of view controllers).
You might want to take a look at it: RHHorizontalSwipe.
The concept of a UIScrollView containing multiple UIViewController views sounds sketchy at best, that design does not sound good at all.
That being said, one potential issue could be this line:
if ((NSNull *)countdownController == [NSNull null])
You would be better off with something like this:
if (!countdownController || [countdownController isKindOfClass:[NSNull class]])
Also, you should call [super viewWillDisappear:animated] in your viewWillDisappear method.