Show bounding box of UIImageView in UIView - objective-c

I have written a class extending UIImageView in order to allow me dynamically generate bricks on screen. The brick is a 20x10 PNG.
Here is my codes:
- (id) initBrick:(NSInteger *)str x:(float)ptX y:(float)ptY {
int brickIndex = arc4random() % 10 + 1;
NSString *filename = [NSString stringWithFormat:#"brick%d.png", brickIndex];
UIImage *brickImage = [UIImage imageNamed:filename];
CGRect imageRect = CGRectMake(0.0f, 0.0f, 20.0f, 10.0f);
[self initWithFrame:imageRect];
[self setImage:brickImage];
self.center = CGPointMake(ptX, ptY);
self.opaque = YES;
self.isDead = NO;
return self;
}
Then, I have a simple collision detection function in the same class:
- (BOOL)checkHit:(CGRect)frame {
if(CGRectIntersectsRect(self.frame, frame)) {
isDead = YES;
return YES;
} else {
return NO;
}
}
But the collision detection is not performed well.
The bounding box seems a bit lower than my image.
How to show the bounding box in order to allow me to check the collision?
If the code is unclear, I can supply more information.

You could set the background color to be sure the problem is not caused by the image. But if the image is simple opaque rectangle, it should be fine. I’d set a breakpoint in the checkHit method, see what self.frame gives and think for a while, it can’t be too hard.
And as for the checkHit method, you should either rename it to checkAndSetHit, or (better) do not set the dead flag there:
- (BOOL) checkHit: (CGRect) frame
{
return CGRectIntersectsRect(self.frame, frame);
}
The code would read even a tiny little bit better if you renamed it to hitsFrame or intersectsFrame, but that’s nitpicking.

Related

Disabling NSView fade animation for NSView `setHidden:`

I am working on a project that has the concept of draggable controls, everything is working fine except that NSView seems to employ a fade in/out animation when calling setHidden:.
I have been able to work around the problem by changing the line session.animatesToStartingPositionsOnCancelOrFail = YES; to NO and implementing the image snapback myself with a custom animated NSWindow subclass. it looks great, but I know there must be an easier way.
I have tried:
using NSAnimationContext grouping with duration of 0 around the setHidden: calls
setting the view animations dictionary using various keys (alpha, hidden, isHidden) on the control and superview
overriding animationForKey: for both the control and its superview
I am not using CALayers and have even tried explicitly setting wantsLayer: to NO.
Does anybody know how to either disable this animation, or have a simpler solution then my animated NSWindow?
here is my stripped down altered code with the bare minimum to see what I'm talking about.
#implementation NSControl (DragControl)
- (NSDraggingSession*)beginDraggingSessionWithDraggingCell:(NSActionCell <NSDraggingSource> *)cell event:(NSEvent*) theEvent
{
NSImage* image = [self imageForCell:cell];
NSDraggingItem* di = [[NSDraggingItem alloc] initWithPasteboardWriter:image];
NSRect dragFrame = [self frameForCell:cell];
dragFrame.size = image.size;
[di setDraggingFrame:dragFrame contents:image];
NSArray* items = [NSArray arrayWithObject:di];
[self setHidden:YES];
return [self beginDraggingSessionWithItems:items event:theEvent source:cell];
}
- (NSRect)frameForCell:(NSCell*)cell
{
// override in multi-cell cubclasses!
return self.bounds;
}
- (NSImage*)imageForCell:(NSCell*)cell
{
return [self imageForCell:cell highlighted:[cell isHighlighted]];
}
- (NSImage*)imageForCell:(NSCell*)cell highlighted:(BOOL) highlight
{
// override in multicell cubclasses to just get an image of the dragged cell.
// for any single cell control we can just make sure that cell is the controls cell
if (cell == self.cell || cell == nil) { // nil signifies entire control
// basically a bitmap of the control
// NOTE: the cell is irrelevant when dealing with a single cell control
BOOL isHighlighted = [cell isHighlighted];
[cell setHighlighted:highlight];
NSRect cellFrame = [self frameForCell:cell];
// We COULD just draw the cell, to an NSImage, but button cells draw their content
// in a special way that would complicate that implementation (ex text alignment).
// subclasses that have multiple cells may wish to override this to only draw the cell
NSBitmapImageRep* rep = [self bitmapImageRepForCachingDisplayInRect:cellFrame];
NSImage* image = [[NSImage alloc] initWithSize:rep.size];
[self cacheDisplayInRect:cellFrame toBitmapImageRep:rep];
[image addRepresentation:rep];
// reset the original cell state
[cell setHighlighted:isHighlighted];
return image;
}
// cell doesnt belong to this control!
return nil;
}
#pragma mark NSDraggingDestination
- (void)draggingEnded:(id < NSDraggingInfo >)sender
{
[self setHidden:NO];
}
#end
#implementation NSActionCell (DragCell)
- (void)setControlView:(NSView *)view
{
// this is a bit of a hack, but the easiest way to make the control dragging work.
// force the control to accept image drags.
// the control will forward us the drag destination events via our DragControl category
[view registerForDraggedTypes:[NSImage imagePasteboardTypes]];
[super setControlView:view];
}
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
BOOL result = NO;
NSPoint currentPoint = theEvent.locationInWindow;
BOOL done = NO;
BOOL trackContinously = [self startTrackingAt:currentPoint inView:controlView];
BOOL mouseIsUp = NO;
NSEvent *event = nil;
while (!done)
{
NSPoint lastPoint = currentPoint;
event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSLeftMouseDraggedMask)
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (event)
{
currentPoint = event.locationInWindow;
// Send continueTracking.../stopTracking...
if (trackContinously)
{
if (![self continueTracking:lastPoint
at:currentPoint
inView:controlView])
{
done = YES;
[self stopTracking:lastPoint
at:currentPoint
inView:controlView
mouseIsUp:mouseIsUp];
}
if (self.isContinuous)
{
[NSApp sendAction:self.action
to:self.target
from:controlView];
}
}
mouseIsUp = (event.type == NSLeftMouseUp);
done = done || mouseIsUp;
if (untilMouseUp)
{
result = mouseIsUp;
} else {
// Check if the mouse left our cell rect
result = NSPointInRect([controlView
convertPoint:currentPoint
fromView:nil], cellFrame);
if (!result)
done = YES;
}
if (done && result && ![self isContinuous])
[NSApp sendAction:self.action
to:self.target
from:controlView];
else {
done = YES;
result = YES;
// this bit-o-magic executes on either a drag event or immidiately following timer expiration
// this initiates the control drag event using NSDragging protocols
NSControl* cv = (NSControl*)self.controlView;
NSDraggingSession* session = [cv beginDraggingSessionWithDraggingCell:self
event:theEvent];
// Note that you will get an ugly flash effect when the image returns if this is set to yes
// you can work around it by setting NO and faking the release by animating an NSWindowSubclass with the image as the content
// create the window in the drag ended method for NSDragOperationNone
// there is [probably a better and easier way around this behavior by playing with view animation properties.
session.animatesToStartingPositionsOnCancelOrFail = YES;
}
}
}
return result;
}
#pragma mark - NSDraggingSource Methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationNone;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationPrivate;
break;
}
}
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
// now tell the control view the drag ended so it can do any cleanup it needs
// this is somewhat hackish
[self.controlView draggingEnded:nil];
}
#end
There must be a layer enabled somewhere in your view hierarchy, otherwise there wouldn't be a fade animation. Here is my way of disabling such animations:
#interface NoAnimationImageView : NSImageView
#end
#implementation NoAnimationImageView
+ (id)defaultAnimationForKey: (NSString *)key
{
return nil;
}
#end
The solution you already tried by setting the view animations dictionary should work. But not for the keys you mention but for the following. Use it somewhere before the animation is triggered the first time. If you have to do it on the window or view or both, I don't know.
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderIn];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Or also just remove the keys if they are there (might not be the case as they are implicit / default):
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations removeObjectForKey:NSAnimationTriggerOrderIn];
[animations removeObjectForKey:NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Ok. I figured out that the animation I'm seeing is not the control, the superview, nor the control's window. It appears that animatesToStartingPositionsOnCancelOrFail causes NSDraggingSession to create a window (observed with QuartzDebug) and put the drag image in it and it is this window that animates back to the origin and fades out before the setHidden: call is executed (i.e. before the drag operation is concluded).
Unfortunately, the window that it creates is not an NSWindow so creating a category on NSWindow doesn't disable the fade animation.
Secondly, there is no public way that I know of to get a handle on the window, so I can't attempt directly manipulating the window instance.
It looks like maybe my workaround is the best way to do this, after all its not far from what AppKit does for you anyway.
If anybody knows how to get a handle on this window, or what class it is I would be interested to know.

NSScrollView infinite / endless scroll | subview reuse

I'm searching for a way to implement something like reusable cells for UI/NSTableView but for NSScrollView. Basically I want the same like the WWDC 2011 video "Session 104 - Advanced Scroll View Techniques" but for Mac.
I have several problems realizing this. The first: NSScrollView doesn't have -layoutSubviews. I tried to use -adjustScroll instead but fail in setting a different contentOffset:
- (NSRect)adjustScroll:(NSRect)proposedVisibleRect {
if (proposedVisibleRect.origin.x > 600) {
// non of them work properly
// proposedVisibleRect.origin.x = 0;
// [self setBoundsOrigin:NSZeroPoint];
// [self setFrameOrigin:NSZeroPoint];
// [[parentScrollView contentView] scrollPoint:NSZeroPoint];
// [[parentScrollView contentView] setBoundsOrigin:NSZeroPoint];
}
return proposedVisibleRect;
}
The next thing I tried was to set a really huge content view with a width of millions of pixel (which actually works in comparison to iOS!) but now the question is, how to install a reuse-pool?
Is it better to move the subviews while scrolling to a new position or to remove all subviews and insert them again? and how and where should I do that?
As best I can tell, -adjustScroll: is not where you want to tap into the scrolling events because it doesn't get called universally. I think -reflectScrolledClipView: is probably a better hookup point.
I cooked up the following example that should hit the high points of one way to do a view-reusing scroll view. For simplicity, I set the dimensions of the scrollView's documentView to "huge", as you suggest, rather than trying to "fake up" the scrolling behavior to look infinite. Obviously drawing the constituent tile views for real is up to you. (In this example I created a dummy view that just fills itself with red with a blue outline to convince myself that everything was working.) It came out like this:
// For the header file
#interface SOReuseScrollView : NSScrollView
#end
// For the implementation file
#interface SOReuseScrollView () // Private
- (void)p_updateTiles;
#property (nonatomic, readonly, retain) NSMutableArray* p_reusableViews;
#end
// Just a small diagnosting view to convince myself that this works.
#interface SODiagnosticView : NSView
#end
#implementation SOReuseScrollView
#synthesize p_reusableViews = mReusableViews;
- (void)dealloc
{
[mReusableViews release];
[super dealloc];
}
- (NSMutableArray*)p_reusableViews
{
if (nil == mReusableViews)
{
mReusableViews = [[NSMutableArray alloc] init];
}
return mReusableViews;
}
- (void)reflectScrolledClipView:(NSClipView *)cView
{
[super reflectScrolledClipView: cView];
[self p_updateTiles];
}
- (void)p_updateTiles
{
// The size of a tile...
static const NSSize gGranuleSize = {250.0, 250.0};
NSMutableArray* reusableViews = self.p_reusableViews;
NSRect documentVisibleRect = self.documentVisibleRect;
// Determine the needed tiles for coverage
const CGFloat xMin = floor(NSMinX(documentVisibleRect) / gGranuleSize.width) * gGranuleSize.width;
const CGFloat xMax = xMin + (ceil((NSMaxX(documentVisibleRect) - xMin) / gGranuleSize.width) * gGranuleSize.width);
const CGFloat yMin = floor(NSMinY(documentVisibleRect) / gGranuleSize.height) * gGranuleSize.height;
const CGFloat yMax = ceil((NSMaxY(documentVisibleRect) - yMin) / gGranuleSize.height) * gGranuleSize.height;
// Figure out the tile frames we would need to get full coverage
NSMutableSet* neededTileFrames = [NSMutableSet set];
for (CGFloat x = xMin; x < xMax; x += gGranuleSize.width)
{
for (CGFloat y = yMin; y < yMax; y += gGranuleSize.height)
{
NSRect rect = NSMakeRect(x, y, gGranuleSize.width, gGranuleSize.height);
[neededTileFrames addObject: [NSValue valueWithRect: rect]];
}
}
// See if we already have subviews that cover these needed frames.
for (NSView* subview in [[[self.documentView subviews] copy] autorelease])
{
NSValue* frameRectVal = [NSValue valueWithRect: subview.frame];
// If we don't need this one any more...
if (![neededTileFrames containsObject: frameRectVal])
{
// Then recycle it...
[reusableViews addObject: subview];
[subview removeFromSuperview];
}
else
{
// Take this frame rect off the To-do list.
[neededTileFrames removeObject: frameRectVal];
}
}
// Add needed tiles from the to-do list
for (NSValue* neededFrame in neededTileFrames)
{
NSView* view = [[[reusableViews lastObject] retain] autorelease];
[reusableViews removeLastObject];
if (nil == view)
{
// Create one if we didnt find a reusable one.
view = [[[SODiagnosticView alloc] initWithFrame: NSZeroRect] autorelease];
NSLog(#"Created a view.");
}
else
{
NSLog(#"Reused a view.");
}
// Place it and install it.
view.frame = [neededFrame rectValue];
[view setNeedsDisplay: YES];
[self.documentView addSubview: view];
}
}
#end
#implementation SODiagnosticView
- (void)drawRect:(NSRect)dirtyRect
{
// Draw a red tile with a blue border.
[[NSColor blueColor] set];
NSRectFill(self.bounds);
[[NSColor redColor] setFill];
NSRectFill(NSInsetRect(self.bounds, 2,2));
}
#end
This worked pretty well as best I could tell. Again, drawing something meaningful in the reused views is where the real work is here.
Hope that helps.

Animation using array of images in sequence

I have an array of images which I want to animate by playing these images one after the other in a sequence. I want to repeat the whole loop several times. I am developing a game for iPad. Suggest to me a method to achieve this functionality in Objective-C with the Cocoa framework.
NSArray *animationArray = [NSArray arrayWithObjects:
[UIImage imageNamed:#"images.jpg"],
[UIImage imageNamed:#"images1.jpg"],
[UIImage imageNamed:#"images5.jpg"],
[UIImage imageNamed:#"index3.jpg"],
nil];
UIImageView *animationView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0,320, 460)];
animationView.backgroundColor = [UIColor purpleColor];
animationView.animationImages = animationArray;
animationView.animationDuration = 1.5;
animationView.animationRepeatCount = 0;
[animationView startAnimating];
[self.view addSubview:animationView];
[animationView release];
add your own images in the array.repeat Count 0 means infinite loop.You can give your own number also.
There are at least 3 ways to animate an array of images through a UIImageView. I'm adding 3 links to download sample code for the 3 possibilities.
The first one is the one that everyone knows. The other ones are less known.
- UIImageView.animationImages
Example Link
The problem of this one is that do not have Delegate to tell us in which moment the animation is finished. So, we can have problems if we want to display something after the animation.
In the same way, there is no possibility to kept the last image from the animation in the UIImageView automatically. If we combine both problems we can have a gap at the end of the animation if we want to kept the last frame on screen.
self.imageView.animationImages = self.imagesArray; // the array with the images
self.imageView.animationDuration = kAnimationDuration; // static const with your value
self.imageView.animationRepeatCount = 1;
[self.imageView startAnimating];
- CAKeyframeAnimation
Example Link
This way to animate works through CAAnimation. It have an easy delegate to use and we can know when the animation finish.
This is probably the best way to animate an array of images.
- (void)animateImages
{
CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
keyframeAnimation.values = self.imagesArray;
keyframeAnimation.repeatCount = 1.0f;
keyframeAnimation.duration = kAnimationDuration; // static const with your value
keyframeAnimation.delegate = self;
// keyframeAnimation.removedOnCompletion = YES;
keyframeAnimation.removedOnCompletion = NO;
keyframeAnimation.fillMode = kCAFillModeForwards;
CALayer *layer = self.animationImageView.layer;
[layer addAnimation:keyframeAnimation
forKey:#"girlAnimation"];
}
Delegate:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (flag)
{
// your code
}
}
- CADisplayLink
Example Link
A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.
This way to do it is really interesting and opens a lot of possibilities to manipulate what are we showing in screen.
DisplayLink getter:
- (CADisplayLink *)displayLink
{
if (!_displayLink)
{
_displayLink = [CADisplayLink displayLinkWithTarget:self
selector:#selector(linkProgress)];
}
return _displayLink;
}
Methods:
- (void)animateImages
{
self.displayLink.frameInterval = 5;
self.frameNumber = 0;
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes];
}
- (void)linkProgress
{
if (self.frameNumber > 16)
{
[self.displayLink invalidate];
self.displayLink = nil;
self.animationImageView.image = [UIImage imageNamed:#"lastImageName"];
self.imagesArray = nil;
return;
}
self.animationImageView.image = self.imagesArray[self.frameNumber++];
self.frameNumber++;
}
GENERAL PROBLEM:
Even though we have this 3 possibilities, if your animation is with a lot of big images, consider using a video instead. The usage of memory will decrease a lot.
A General problem you will face doing this is in the moment of the allocation of the images.
If you use [UIImage imageNamed:#"imageName"] you will have cahe problems.
From Apple:
This method looks in the system caches for an image object with the specified name and returns that object if it exists. If a matching image object is not already in the cache, this method locates and loads the image data from disk or asset catelog, and then returns the resulting object. You can not assume that this method is thread safe.
So, imageNamed: stores the image in a private Cache.
- The first problem is that you can not take control of the cache size.
- The second problem is that the cache did not get cleaned in time and if you are allocating a lot of images with imageNamed:, your app, probably, will crash.
SOLUTION:
Allocate images directly from Bundle:
NSString *imageName = [NSString stringWithFormat:#"imageName.png"];
NSString *path = [[NSBundle mainBundle] pathForResource:imageName
// Allocating images with imageWithContentsOfFile makes images to do not cache.
UIImage *image = [UIImage imageWithContentsOfFile:path];
Small problem:
Images in Images.xcassets get never allocated. So, move your images outside Images.xcassets to allocate directly from Bundle.
See the animationImages property of UIImageView. It’s hard to say if it fits your needs as you don’t give us details, but it’s a good start.
I have added a swift 3.0 extension for this
extension UIImageView {
func animate(images: [UIImage], index: Int = 0, completionHandler: (() -> Void)?) {
UIView.transition(with: self, duration: 0.5, options: .transitionCrossDissolve, animations: {
self.image = images[index]
}, completion: { value in
let idx = index == images.count-1 ? 0 : index+1
if idx == 0 {
completionHandler!()
} else {
self.animate(images: images, index: idx, completionHandler: completionHandler)
}
})
}
}
Best solution for me use CADisplayLink. UIImageView doesn't have completion block and you can't catch steps of animation. In my task i must changing background of view with image sequencer step by step. So CADisplayLink allows you handling steps and finishing animation. If we talk about usage of memory, i think best solution load images from bundle and delete array after finishing
ImageSequencer.h
typedef void (^Block)(void);
#protocol ImageSequencerDelegate;
#interface QSImageSequencer : UIImageView
#property (nonatomic, weak) id <ImageSequencerDelegate> delegate;
- (void)startAnimatingWithCompletionBlock:(Block)block;
#end
#protocol ImageSequencerDelegate <NSObject>
#optional
- (void)animationDidStart;
- (void)animationDidStop;
- (void)didChangeImage:(UIImage *)image;
#end
ImageSequencer.m
- (instancetype)init {
if (self = [super init]) {
_imagesArray = [NSMutableArray array];
self.image = [self.imagesArray firstObject];
}
return self;
}
#pragma mark - Animation
- (void)startAnimating {
[self startAnimatingWithCompletionBlock:nil];
}
- (void)startAnimatingWithCompletionBlock:(Block)block {
self.frameNumber = 0;
[self setSuccessBlock:block];
self.displayLink.frameInterval = 5;
if (self.delegate && [self.delegate respondsToSelector:#selector(animationDidStart)]) {
[self.delegate animationDidStart];
}
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes];
}
-(void)stopAnimating {
self.image = [self.imagesArray lastObject];
[self.displayLink invalidate];
[self setDisplayLink:nil];
Block block_ = [self successBlock];
if (block_) {
block_();
}
if (self.delegate && [self.delegate respondsToSelector:#selector(animationDidStop)]) {
[self.delegate animationDidStop];
}
[self.imagesArray removeAllObjects];
}
- (void)animationProgress {
if (self.frameNumber >= self.imagesArray.count) {
[self stopAnimating];
return;
}
if (self.delegate && [self.delegate respondsToSelector:#selector(didChangeImage:)]) {
[self.delegate didChangeImage:self.imagesArray[self.frameNumber]];
}
self.image = self.imagesArray[self.frameNumber];
self.frameNumber++;
}
#pragma mark - Getters / Setters
- (CADisplayLink *)displayLink {
if (!_displayLink){
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(animationProgress)];
}
return _displayLink;
}
- (NSMutableArray<UIImage *> *)imagesArray {
if (_imagesArray.count == 0) {
// get images from bundle and set to array
}
return _imagesArray;
}
#end
This is a simple and working code for animation./
-(void)move
{
[UIView animateWithDuration:5
delay:0.0
options: UIViewAnimationCurveEaseOut
animations:^{
[_imgbox setFrame:CGRectMake(100, 100, _imgbox.frame.size.width, _imgbox.frame.size.height)];
}
completion:^(BOOL finished){
NSLog(#"Done!");
}];
}

CGAffineTransformMakeScale Makes UIView Jump to Original Size before scale

I have a UIView that I set up to respond to pinch gestures and change its size, except, once you enlarge it and then try and pinch it again, it jumps to its original size (which just so happens to be 100x200). Here is the code:
#implementation ChartAxisView
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
// do gesture recognizers
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(onPinch:)];
[self addGestureRecognizer:pinch];
[pinch release];
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0].CGColor;
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);
}
- (void)onPinch: (UIPinchGestureRecognizer*) gesture {
self.transform = CGAffineTransformMakeScale(gesture.scale, gesture.scale);
}
- (void)dealloc {
[super dealloc];
}
#end
Any thoughts?
So there are two types of Scale (or, transform in general) functions: CGAffineTransformMakeScale and CGAffineTransformScale
The first one, CGAffineTransformMakeScale which you are using, always transforms with respect to the image's original size. And that is why you see the jump to its original size before the scaling happens.
The second one, CGAffineTransformScale, transforms from the image's current position. This is what you need. For this, it requires an additional 'transform' arg. The 'transform' arg in your case represents the enlarged image.
Read this very informative blog post about transformations.
- (void)onPinch: (UIPinchGestureRecognizer*) gesture {
if ([gesture state] == UIGestureRecognizerStateBegan)
{
curTransform = self.transform;
}
self.transform = CGAffineTransformScale(curTransform,gesture.scale, gesture.scale);
}
use CGAffineTransformScale instead of CGAffineTransformMakeScale
you will need a member -> CGAffineTransform curTransform;
;)
you can set transform with following code:
ivClone setTransform:CGAffineTransformMakeScale(scale, scale)];
and you can get current transform with following coee:
newV.transform.a //x scale
newV.transform.d // y scale

How do I manage the drawing of custom data (not photos) in an NSView that has an area dynamically larger than what is viewable?

UPDATE: Relative to the questions and answers below, it seems I have a misunderstanding of the NSView class in relation to the custom classes I'm trying to draw and the wrapping NSScrollView. In the end, what I'm trying to figure out is how do I manage the dynamic drawing of custom data (not photos) in an NSView that has an area larger than what is viewable?
I am not looking for a handout, but I am a novice to Cocoa and I thought I was doing best-practice based on Apple's docs, but it seems I've gotten the fundamentals wrong. Apple's documentation is incredibly detailed, technical, centered entirely around working with photos, and thus useless to me. The related code examples provided by Apple (e.g. Sketch) get the document size from the printer paper sizes in their typically oblique fashion, and that's not what I need. I've scoured the web for tutorials, examples and the like, but I'm not finding much of anything (and I promise to write one when I get this sorted out).
I'm porting this code from REALbasic where I have this completely working, even with Undo commands, but the paradigms to do so are entirely different. This just isn't "clicking" for me. I appreciate the help given, I'm still missing something here, anything else folks have to offer is appreciated.
Thanks
I have a subclassed NSView where I'm creating a piano-roll MIDI interface. I am trying to resolve a few problems:
Drawing artifacts during and after scrolling
Lines not spanning across the visible area during and after scrolling
While scrolling and sometimes on mouseDown, the horizontal scroller jumps to the right 1 (one) pixel, but I don't have scrollToPoint implemented anywhere yet.
Symptoms that relate to the above:
Implementing adjustScroll makes everything worse.
mouseDown corrects all of the problems except sometimes the 1-pixel jump to the right.
If I uncomment the NSLog command the beginning of drawRect nothing draws.
Apple's documentation mentions pixel-accurate drawing, but (of course) offers up no examples on how this can be achieved. I've been using the floor() function to try to get consistent values, but once I start tacking on scrollToPoint or any other complexity, things go haywire.
Please see the linked image as an example. The screenshot, if you can believe it, actually cleans up what I see on screen. There are double lines almost everywhere at half opacity as well. The same is applied to any objects I draw as well.
Graphics Artifacts and inconsistencies in a subclassed NSView generated after scrolling http://www.oatmealandcoffee.com/external/NSViewArtifacts.png
Here is the code. I hate giving up so much publicly, but I've searched everywhere for clues, and if the Internet is any indication I'm the only person with this problem, and I really just want to get this sorted out and move forward. There is a lot, and there is more to come, but these is the core stuff I really need to get right, and, frankly, I am at a loss on how to correct it.
- (void)drawRect:(NSRect)rect {
//NSLog(#"OCEditorView:drawRect: START");
[self setFrame:[[self EditorDocument] DocumentRect]];
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
// CLEAR BACKGROUND
[[[self EditorDocument] ColorWhiteKey] set];
NSRectFill(rect);
// BACKGROUND KEYS
int firstRowLine = 0; //NSMinY(rect); //<- adding the function results in bad spacing on scrolling
int currentRowLine = 0;
int lastRowLine = NSMaxY(rect);
//NSLog(#"lastRowLine:%d", lastRowLine);
float currentZoomY = [self ZoomY];
for (currentRowLine = firstRowLine; currentRowLine <= lastRowLine; currentRowLine += currentZoomY) {
int currentTone = floor(currentRowLine / [self ZoomY]);
BOOL isBlackKey = [[self MusicLib] IsBlackKey:currentTone];
//NSLog(#"%d, tone:%d, black:%d", [self MusicLib], currentTone, isBlackKey);
if (isBlackKey) {
[[[self EditorDocument] ColorBlackKey] set];
} else {
[[NSColor whiteColor] set];
}
NSBezierPath *rowLine = [NSBezierPath bezierPath];
NSPoint bottomLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
NSPoint bottomRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
NSPoint topRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine + [self ZoomY]);
NSPoint topLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine + [self ZoomY]);
[rowLine moveToPoint:bottomLeftPoint];
[rowLine lineToPoint:bottomRightPoint];
[rowLine lineToPoint:topRightPoint];
[rowLine lineToPoint:topLeftPoint];
[rowLine closePath];
[rowLine fill];
BOOL isOctave = [[self MusicLib] IsOctave:currentTone];
if (isOctave) {
[[[self EditorDocument] ColorXGrid] set];
NSBezierPath *octaveLine = [NSBezierPath bezierPath];
NSPoint leftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
NSPoint rightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
[octaveLine moveToPoint:leftPoint];
[octaveLine lineToPoint:rightPoint];
[octaveLine stroke];
}
}
// BACKGROUND MEASURES
//[[self EditorDocument].ColorYGrid setStroke];
int firstColumnLine = 0;
int currentColumnLine = 0;
int lastColumnLine = NSMaxX(rect);
int snapToValueInBeats = [[self EditorDocument] SnapToValue];
int snapToValueInPixels = floor(snapToValueInBeats * [self ZoomX]);
int measureUnitInBeats = floor([[self EditorDocument] TimeSignatureBeatsPerMeasure] * [[self EditorDocument] TimeSignatureBasicBeat]);
int measureUnitInPixels = floor(measureUnitInBeats * [self ZoomX]);
for (currentColumnLine = firstColumnLine; currentColumnLine <= lastColumnLine; currentColumnLine += snapToValueInPixels) {
//int currentBeat = floor(currentColumnLine / [self ZoomX]);
int isAMeasure = currentColumnLine % measureUnitInPixels;
int isAtSnap = currentColumnLine % snapToValueInPixels;
if ((isAMeasure == 0) || (isAtSnap == 0)) {
if (isAtSnap == 0) {
[[NSColor whiteColor] set];
}
if (isAMeasure == 0) {
[[[self EditorDocument] ColorXGrid] set];
}
NSBezierPath *columnLine = [NSBezierPath bezierPath];
NSPoint startPoint = NSMakePoint(currentColumnLine, NSMinY(rect));
NSPoint endPoint = NSMakePoint(currentColumnLine, NSMaxY(rect));
[columnLine moveToPoint:startPoint];
[columnLine lineToPoint:endPoint];
[columnLine setLineWidth:1.0];
[columnLine stroke];
} // isAMeasure or isAtSnap
} // currentColumnLine
// NOTES
for (OCNoteObject *note in [[self EditorDocument] Notes]) {
OCNoteObject *currentNote = note;
NSRect noteBounds = [self GetRectFromNote:currentNote];
//NSLog(#"noteBounds:%d", noteBounds);
// set the color for the note fill
// this will have to come from the parent Track
NSMutableArray *trackColors = [self EditorDocument].TrackColors;
if (note.Selected) {
[[trackColors objectAtIndex:0] set];
} else {
[[trackColors objectAtIndex:1] set];
}
[NSBezierPath fillRect:noteBounds];
// outline
[[NSColor blackColor] set];
[NSBezierPath strokeRect:noteBounds];
} // for each note
/*
if (EditorController.startingUpApplication == YES) {
[self setDefaultSettingForApplicationStartUp];
}
*/
//NSLog(#"OCEditorView:drawRect: END");
}
- (void)mouseDown:(NSEvent *)theEvent {
//NSLog(#"OCEditorObject:mouseDown: START");
// This converts the click into coordinates
MouseDownPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// Calculate the beat and pitch clicked into...
float startBeat = floor(MouseDownPoint.x / [self ZoomX]);
float pitch = floor(MouseDownPoint.y / [self ZoomY]);
float length = [[self EditorDocument] NewNoteLength];
//NSLog(#"X:%f, Y:%f", MouseDownPoint.x, MouseDownPoint.y);
//NSLog(#"beat:%f, pitch:%f", startBeat, pitch);
LastDragPoint = MouseDownPoint; // save the point just in case.
OCNoteObject *note = [self GetClickedNoteFromPoint:MouseDownPoint];
if ([EditorController EditorMode] == AddObjectMode) {
//NSLog(#"AddObjectMode)");
float snapToX = [[self EditorDocument] SnapToValue];
float snappedStartBeat = floor(startBeat / snapToX) * snapToX;
//NSLog(#"%f = %f / %f * %f", snappedStartBeat, startBeat, snapToX, snapToX);
OCNoteObject *newNote = [[self EditorDocument] CreateNote:snappedStartBeat Pitch:pitch Length:length];
//NSLog(#"newNote:%d", newNote);
[newNote Deselect];
} else if ([EditorController EditorMode] == EditObjectMode) {
//NSLog(#"EditObjectMode");
// if nothing was clicked, then clear the selections
// else if the shift key was pressed, add to the selection
if (note == nil) {
[self SelectNone];
} else {
//NSLog(#"mouseDown note.pitch:%f, oldPitch:%f", note.Pitch, note.OldPitch);
BOOL editingSelection = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
if (editingSelection) {
if (note.Selected) {
[self RemoveFromSelection:note];
} else {
[self AddToSelection:note];
}
} else {
if (note.Selected) {
// do nothing
} else {
[self SelectNone];
[self AddToSelection:note];
}
}
[self SetOldData];
} // (note == nil)
} else if ([EditorController EditorMode] == DeleteObjectMode) {
if (note != nil) {
[self RemoveFromSelection:note];
[[self EditorDocument] DestroyNote:note];
} // (note != nil)
} // EditorMode
[self setFrame:[[self EditorDocument] DocumentRect]];
[self setNeedsDisplay:YES];
}
- (void)mouseDragged:(NSEvent *)theEvent {
//NSLog(#"mouseDragged");
NSPoint currentDragPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// NSLog(#"currentDragPoint: %d", currentDragPoint)
float snapToValueInBeats = [[self EditorDocument] SnapToValue];
int deltaXinPixels = floor(currentDragPoint.x - MouseDownPoint.x);
int deltaYinPixels = floor(currentDragPoint.y - MouseDownPoint.y);
int deltaXinBeats = floor(deltaXinPixels / [self ZoomX]);
int deltaY = floor(deltaYinPixels / [self ZoomY]);
int deltaX = floor(deltaXinBeats / snapToValueInBeats) * snapToValueInBeats;
for (OCNoteObject *note in [self Selection]) {
[self MoveNote:note DeltaX:deltaX DeltaY:deltaY];
}
LastDragPoint = currentDragPoint;
[self autoscroll:theEvent];
[self setNeedsDisplay:YES]; //artifacts are left if this is off.
}
- (void)mouseUp:(NSEvent *)theEvent {
if ([EditorController EditorMode] == AddObjectMode) {
} else if ([EditorController EditorMode] == EditObjectMode) {
} else if ([EditorController EditorMode] == DeleteObjectMode) {
}
[self setNeedsDisplay:YES];
}
I could very well be missing something obvious, but I think I'm too close to the code to see the solution for what it is. Any help is greatly appreciated! Thanks!
I think you are misunderstanding the way drawRect: and its argument works:
The message drawRect: is sent by cocoa whenever your view or parts of it need to be redrawn. The CGRect argument is the bounding box of all updated areas for the current redraw. That means that you should not derive any positions of objects within your view from this rectangle. It is only passed to the method to allow for optimized drawing: if something is completely outside of this rectangle it does not need to be redrawn.
You should calculate all positions within your view from the views coordinate system: [self bounds]. This does not change each time drawRect: is performed and gives you an origin and size for the contents of the view.
There are a couple of other issues with your code (for instance, don't call setFrame: from within drawRect:) but I think you should first get the coordinates right and then look further into how to calculate pixel-aligned coordinates for your rectangles.
This code of yours looks rather more elaborate than it needs to be. Check out NSCenterScanRect(), and NSRectFillListWithColors(). Also, it's rather wasteful to create and discard paths in -drawRect:.