NSView and loading subviews on the fly - objective-c

To get a sense of what I'm doing without posting pages of code... I have an NSOperation that I'm using to process files as they are added to a folder. In that NSOperation I'm using the NSNotificationCenter to send notifications to an NSView whenever a new job is started. The idea is, that I want to add a new subview to give me some information about the job that just started. The problem is I can't seem to get new subviews to draw. Here is what I have right now.
- (void)drawRect:(NSRect)dirtyRect
{
NSLog(#"Draw Count %i", [jobViewArray count]);
int i = 0;
while (i < [jobViewArray count]) {
[self addSubview:[jobViewArray objectAtIndex:i]];
}
}
and then further down:
-(void) newJobNotification: (NSNotification *) notification
{
if (!jobViewArray)
jobViewArray = [[NSMutableArray alloc] init];
++jobCount;
NSRect rect;
rect.size.width = 832;
rect.size.height = 120;
NSPoint point = { 0, ((jobCount * 120) - 120) };
rect.origin = point;
ProgressView *newJob = [[ProgressView alloc] initWithFrame:rect];
[jobViewArray addObject:newJob];
NSLog(#"Notice Count %i", [jobViewArray count]);
}
}
When I use my app to add a job, the notification is properly received by my NSView, the subview is properly added to the jobViewArray, but then when drawRect: gets called again my jobViewArray is empty. It's the first time I've tried to do something like this so I'm probably doing something completely wrong here... I guess that goes with out saying since it doesn't work huh?

You shouldn't be adding the subview to the view in drawRect:. When you receive the notification you should add the subviews there because the second time the notification comes around, you're going to add 2 subviews, then the next time 3 subviews and so one.
If you add the subview in the notification then you'll not need to mess around with the array.

Related

Image generator ( generateCGImagesAsynchronouslyForTimes method) doesn't give live results

Pretty new to cocoa development and really stuck with probably a fundamental problem.
So in short, my app UI looks like a simple window with a nsslider at the bottom. What I need is to generate N images and place them, onto N nsviews in my app window.
What it does so far:
I'm clicking on the slider (holding it) and dragging it. While I'm dragging it nothing happens to my views (pictures are not generated). When I release the slider the pictures got generated and my view get filled with them.
What I want:
- I need the views to be filled with pictures as I'm moving the slider.
I figured out the little check box on the NSSlider properties, which is continuous, and I'm using it, but my image generator still doesn't do anything until I release the slider.
Here is my code:
// slider move action
- (IBAction)sliderMove:(id)sender
{
[self generateProcess:[_slider floatValue];
}
// generation process
- (void) generateProcess:(Float64) startPoint
{
// create an array of times for frames to display
NSMutableArray *stops = [[NSMutableArray alloc] init];
for (int j = 0; j < _numOfFramesToDisplay; j++)
{
CMTime time = CMTimeMakeWithSeconds(startPoint, 60000);
[stops addObject:[NSValue valueWithCMTime:time]];
_currentPosition = initialTime; // set the current position to the last frame displayed
startPoint+=0.04; // the step between frames is 0.04sec
}
__block CMTime lastTime = CMTimeMake(-1, 1);
__block int count = 0;
[_imageGenerator generateCGImagesAsynchronouslyForTimes:stops
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
if (CMTimeCompare(actualTime, lastTime) != 0)
{
NSLog(#"new frame found");
lastTime = actualTime;
}
else
{
NSLog(#"skipping");
return;
}
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 500, 100, 100);
NSImageView *iView = [[NSImageView alloc] initWithFrame:rect];
[iView setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[iView setImage:myImage];
[self.windowForSheet.contentView addSubview: iView];
[_viewsToRemove addObject:iView];
}
if (result == AVAssetImageGeneratorFailed)
{
NSLog(#"Failed with error: %#", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled)
{
NSLog(#"Canceled");
}
count++;
}];
}
}
If you have any thoughts or ideas, please share with me, I will really appreciate it!
Thank you
In order to make your NSSlider continuous, open your window controller's XIB file in Interface Builder and click on the NSSlider. Then, open the Utilities area
select the Attributes Inspector
and check the "Continuous" checkbox
under the Control header. Once you've done this, your IBAction sliderMove: will be called as the slider is moved rather than once the mouse is released.
Note: Alternatively, with an
NSSlider *slider = //...
one can simply call
[slider setContinuous:YES];

NSImageView is not deallocated using ARC

Im pretty new to Cocoa development, and I probably do not clearly understand how ARC works.
My problem is that when I'm using NSImageView it is not getting deallocated as I want so the program is leaking memory.
__block CMTime lastTime = CMTimeMake(-1, 1);
__block int count = 0;
[_imageGenerator generateCGImagesAsynchronouslyForTimes:stops
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,
AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
if (CMTimeCompare(actualTime, lastTime) != 0)
{
NSLog(#"new frame found");
lastTime = actualTime;
}
else
{
NSLog(#"skipping");
return;
}
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 100, 100, 100);
// the problem is here!!! ImageView object gets allocated, but never released by the program even though I'm using ARC
NSImageView *imgV = [[NSImageView alloc] initWithFrame:rect];
[imgV setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[imgV setImage:myImage];
[self.window.contentView addSubview: imgV];
}
if (result == AVAssetImageGeneratorFailed)
{
NSLog(#"Failed with error: %#", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled)
{
NSLog(#"Canceled");
}
count++;
}];
Therefore, when I'm returning to this block again t generate new images and display them, everything works perfect except that my program memory use increases by the number of views got created.
If anyone can help me with this I would really appreciate it! Thank you!
Your problem is that you don't remove your subviews when you are generating new ones - make sure you remove your subviews before with something along those lines:
NSArray *viewsToRemove = [self.contentView subviews];
for (NSView *v in viewsToRemove) {
[v removeFromSuperview];
}
So your problem is not related to the usage of ARC actually. Each time you create a NSImageView and add it to contentView it is your responsability to remove them before adding a series of new ones. Note that adding those views to contentView will increment the ref count by one and removing them from the contentView will decrement the ref count by one leading to the memory usage for those views being freed by the system (because nothing else is retaining your views in btw).
Offending piece of code:
[self.window.contentView addSubview: imgV];
You've allocated an NSImageView. and keep adding it to the view. You never remove it, meaning the view is creating many references to different instances of the same object, all allocating their own piece of memory.
Solution: You'll need to keep track of the view, to make sure you can remove it later. Typically, I use class extensions.
For example:
#interface ClassName() {
NSImageView* m_imgV;
}
#end
....
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 100, 100, 100);
if (m_imgV) {
[m_imgV removeFromSuperView];
}
m_imgV = [[NSImageView alloc] initWithFrame:rect];
[m_imgV setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[m_imgV setImage:myImage];
[self.window.contentView addSubview:m_imgV];
I was fighting with this problem for the whole day and finally found the way. For some reason the program wanted me to add a whole function which looks like:
// remove all the view from the superview
// and clean up a garbage array
-(void) killAllViews
{
for (NSImageView *iv in _viewsToRemove)
{
[iv removeFromSuperview];
}
[_viewsToRemove removeAllObjects]; // clean out the array
}
where _viewsToRemove is an array of NSImageViews which I'm filling every time my block is generating new images and adds them to the view.
Still don't understand why just adding the pure code from inside my killAllViews method somewhere into program couldn't solve the problem. Right now I'm basically doing the same, but just calling this method.

How do I pass the user touch from one object to another object?

I am developing an application that allows the user at a certain point to drag and drop 10 images around. So there are 10 images, and if he/she drags one image onto another, these two are swapped.
A screenshot of how this looks like:
So when the user drags one photo I want it to reduce its opacity and give the user a draggable image on his finger which disappears again if he drops it outside of any image.
The way I have developed this is the following. I have set a UIPanGesture for these UIImageViews as:
for (UIImageView *imgView in editPhotosView.subviews) {
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(photoDragged:)];
[imgView addGestureRecognizer:panGesture];
imgView.userInteractionEnabled = YES;
[panGesture release];
}
Then my photoDragged: method:
- (void)photoDragged:(UIPanGestureRecognizer *)gesture {
UIView *view = gesture.view;
if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imgView = (UIImageView *)view;
if (gesture.state == UIGestureRecognizerStateBegan) {
imgView.alpha = 0.5;
UIImageView *newView = [[UIImageView alloc] initWithFrame:imgView.frame];
newView.image = imgView.image;
newView.tag = imgView.tag;
newView.backgroundColor = imgView.backgroundColor;
newView.gestureRecognizers = imgView.gestureRecognizers;
[editPhotosView addSubview:newView];
[newView release];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [gesture translationInView:view.superview];
[view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
[gesture setTranslation:CGPointZero inView:view.superview];
}
else { ... }
}
}
Thus as you see I add a new UIImageView with 0.5 opacity on the same spot as the original image when the user starts dragging it. So the user is dragging the original image around. But what I want to do is to copy the original image when the user drags it and create a "draggable" image and pass that to the user to drag around.
But to do that I have to pass the user touch on to the newly created "draggable" UIImageView. While it's actually set to the original image (the one the user touches when he starts dragging).
So my question is: How do I pass the user's touch to another element?.
I hope that makes sense. Thanks for your help!
Well, you can pass the UIPanGestureRecognizer object to another object by creating a method in your other object which takes the gesture recognizer as a parameter.
- (void)myMethod:(UIPanGestureRecognizer *)gesture
{
// Do stuff
}
And call from your current gesture recognizer using....
[myOtherObject myMethod:gesture];
Not entirely sure I'm understanding your question here fully. :-/
Maybe:
[otherObject sendActionsForControlEvents:UIControlEventTouchUpInside];
Or any other UIControlEvent
In the end I decided to indeed drag the original image and leave a copy at the original place.
I solved the issue with the gesture recognizers I was having by re-creating them and assigning them to the "copy", just like PragmaOnce suggested.

Slight pause in scrolling animation (iPad)

I am relatively new to programming on the iPad and I was trying to put together a simple program. Basically, it's a children's book and I need the functionality of a comic book style (or photo) viewer, where people swipe to change "pages" (or images).
Each image is 1024x768. Currently, they are stored as JPGs because of the very large file sizes PNGs seem to produce. For this story, there are 28 pages.
I took a look at the PageControl example, implementing a UIScrollView. On initialization, I create a big enough scrollview area. Then as the user scrolls, I load in the previous and next images. Again, just like the example only without implementing the page control at the bottom.
The problem I am running into is a very slight pause in the animation when I am flipping. Once the images are loaded or cached, this doesn't happen. Now, I know the photo application doesn't do this and I'm not sure what is causing it.
Here is my code for the scrollViewDidScroll method. I keep up with the page number and it will only call the loadPageIntoScrollView when a page has changed - I was thinking that the insane number of calls it was making was causing the slight pause in animation, but it turned out not to be the case.
- (void) scrollViewDidScroll: (UIScrollView *) sender
{
CGFloat pageWidth = scrollView.frame.size.width;
int localPage = floor( (scrollView.contentOffset.x - pageWidth / 2 ) / pageWidth ) + 1;
if( localPage != currentPage )
{
currentPage = localPage;
[self loadPageIntoScrollView:localPage - 1];
[self loadPageIntoScrollView:localPage];
[self loadPageIntoScrollView:localPage + 1];
}
} // scrollViewDidScroll
And here is my loadPageIntoScrollView method. I'm only creating a UIImageView and loading an image into that - I don't see how that could be much "leaner". But somehow it's causing the pause. Again, it's not a HUGE pause, just one of those things you notice and is enough to make the scrolling look like it has a very. very slight hiccup.
Thank you in advance for any help you could provide.
- (void) loadPageIntoScrollView: (int)page
{
if( page < 0 || page >= kNumberOfPages )
return;
UIImageView *controller = [pages objectAtIndex:page];
NSLog( #"checking pages" );
if( (NSNull *)controller == [NSNull null] )
{
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
NSString *pageName = [[NSString alloc] initWithFormat:#"%d.jpg", page];
controller = [[UIImageView alloc]initWithImage:[UIImage imageNamed:pageName]];
[controller setUserInteractionEnabled:YES];
[controller addGestureRecognizer:singleTap];
[pages replaceObjectAtIndex:page withObject:controller];
[controller release];
} // if controller == null
// add the page to the scrollview
if( controller.superview == nil )
{
NSLog(#"superview was nil, adding page %d", page );
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
controller.frame = frame;
[scrollView addSubview:controller];
} // if
} // loadPageIntoScrollView
Since you say after an image is loaded in it no longer lags, I'd suspect that it is disk access that is causing your lag, but you should run your app through instruments to try to rule out cpu-spikes as well as evaluate file system usage. You may try to pre-load images to the left and right of whatever image you are on so that the user doesn't perceive as much lag.
First off, you should be able to use PNG's just fine. I have build several apps that do exactly what you are doing here, you can fit 3 1024 x 768 PNGs in memory without running out (but you can't do much more). You should also use PNG's as they are the preferred format for iOS as they are optimized when the app is bundled together during build.
The slight lag is caused by loading the image, in this line:
controller = [[UIImageView alloc]initWithImage:[UIImage imageNamed:pageName]];
What I usually do is load the images in a separate thread, using something like this:
[self performSelectorInBackground:#selector(loadPageIntoScrollView:) withObject:[NSNumber numberWithInt:localPage]];
Note that you need to put your localPage integer into a NSNumber object to pass it along, so don't forget to change your loadPageIntoScrollView: method:
- (void) loadPageIntoScrollView: (NSNumber *)pageNumber
{
int page = [pageNumber intValue];
....

Objective C: Removing previous ui elements

I'm producing a "brick" every second. When you tap it, it goes away. My problem is, when a second, or more, appear on the screen, tapping the previous one eliminates the most recent one, and can't be removed from the screen at all. The code I have is:
- (NSTimer *)getTimer{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector: #selector(produceBricks) userInfo:nil repeats:YES];
return timer;
}
-(IBAction) tapBrick {
//remove last brick
[bricks[count] removeFromSuperview];
//add to score
count++;
NSString *scoreString = [NSString stringWithFormat:#"%d", count];
score.text = scoreString;
}
-(void) produceBricks {
//determine x y coordinates
int xPos, yPos;
xPos = arc4random() % 250;
yPos = arc4random() % 370;
//create brick
bricks[count] = [[UIButton alloc] initWithFrame:CGRectMake(xPos,yPos + 60,70,30)];
[bricks[count] setBackgroundColor:[UIColor blackColor]];
[bricks[count] addTarget:self action:#selector(tapBrick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:bricks[count]];
}
I know it has to do with the bricks[count] removeFromSuperview line being count is always increasing. How would I reference to the brick in the array that's being clicked instead of the current one?
If you add a sender argument, it'll automatically get set to the control which sent the action, so you can do something like the following:
-(IBAction) tapBrick:(id)sender {
[sender removeFromSuperview];
}
Make the method tapBrick:(UIButton *)brick and you'll get a reference to the brick that was tapped as the argument.
Also, I strongly advise against using a C array for this. It's just asking for memory management trouble. Better to use an NSMutableArray and make sure to follow the memory management rules. With the code you've posted, your brick objects will never be released, and instead they'll just keep eating memory until your app exhausts its supply.
Views have a tag property for a reason — So you can identify views of the same type in the same view controller. Consider setting a unique tag for each of these bricks, and then removing the particular one you want to.