mashers
mashers

Reputation: 1089

Apply NSAffineTransform to NSView

I want to do a couple of simple transforms on an NSView subclass to flip it on the X axis, the Y axis, or both. I am an experienced iOS developer but I just can't figure out how to do this in macOS. I have created an NSAffineTransform with the required translations and scales, but cannot determine how to actually apply this to the NSView. The only property I can find which will accept any kind of transform is [[NSView layer] transform], but this requires a CATransform3D.

The only success I have had is using the transform to flip the image if an NSImageView, by calling lockFocus on a new, empty NSImage, creating the transform, then drawing the unflipped image inside the locked image. This is far from satisfactory however, as it does not handle any subviews and is presumably more costly than applying the transform directly to the NSView/NSImageView.

Upvotes: 4

Views: 811

Answers (2)

drootang
drootang

Reputation: 2503

I can't believe how many hours of searching and experimenting I had to do before I was able to simply flip an image horizontally in AppKit. I cannot upvote this question and mashers' answer enough.

Here is an updated version of my solution for swift to flip an image horizontally. This method is implemented in an NSImageView subclass.

override func draw(_ dirtyRect: NSRect) {
    // NSViews are not backed by CALayer by default in AppKit. Must request a layer
    self.wantsLayer = true
    if self.flippedHoriz {
        // If a horizontal flip is desired, first multiple every X coordinate by -1. This flips the image, but does it around the origin (lower left), not the center
        var trans = AffineTransform(scaledByX: -1, byY: 1)
        // Add a transform that moves the image by the width so that its lower left is at the origin
        trans.append(AffineTransform(translationByX: self.frame.size.width, byY: 0)
        // AffineTransform is bridged to NSAffineTransform, but it seems only NSAffineTransform has the set() and concat() methods, so convert it and add the transform to the current graphics context
        (trans as NSAffineTransform).concat()
    }

    // Don't be fooled by the Xcode placehoder. This must be *after* the above code
    super.draw(dirtyRect)
}

The behavior of the transforms also took a bit of experimentation to understand. The help for NSAffineTransform.set() explains:

it removes the existing transformation matrix, which is an accumulation of transformation matrices for the screen, window, and any superviews.

This will very likely break something. Since I wanted to still respect all the transformations applied by the window and superviews, the concat() method is more appropriate.

concat() multiplies the existing transform matrix by your custom transform. This is not exactly cumulative, though. Each time draw is called, your transform is applied to the original transform for the view. So repeatedly calling draw doesn't continuously flip the image. Because of this, to not flip the image, simply don't apply the transform.

Upvotes: 0

mashers
mashers

Reputation: 1089

This was the solution:

- (void)setXScaleFactor:(CGFloat)xScaleFactor {
    _xScaleFactor = xScaleFactor;
    [self setNeedsDisplay];
}

- (void)setYScaleFactor:(CGFloat)yScaleFactor {
    _yScaleFactor = yScaleFactor;
    [self setNeedsDisplay];
}

- (void)drawRect:(NSRect)dirtyRect {
    NSAffineTransform *transform = [[NSAffineTransform alloc] init];
    [transform scaleXBy:self.xScaleFactor yBy:self.yScaleFactor];
    [transform set];

    [super drawRect:dirtyRect];
}

Thank you to l'L'l for the hint about using NSGraphicsContext.

Upvotes: 2

Related Questions