For a couple of days I’ve been looking into creating a NSView, that simply shows a NSImage shaped in a circle. I could have done this in NSView’s drawRect method and be done with it. But I also want to expand my knowledge to a better knowledge of the AppKit framework. So stubborn as I am I decided to do this with CALayer like the iOS example I had. This to make it more smoothly animated and to learn some basics about Core Animation.
What I didn’t know before hand is that NSView has three different modes in which it handles it’s content. Compared to UIView from the Cocoa Touch Frameworks, which already has a CALayer ready to use.
Here are the different modes of an instance of the NSView Class:
- Classic view: DrawRect:dirtyRect is called to draw the screen. There is one draw back in this. All drawing takes place in the main thread. Which if performance is getting an issue can become an issue.
- Layer Backed view: The NSView now has a CALayer which is managed by the NSView. This actually takes up more memory space, but since the NSView knows of every CALayer that is underneath it. It’s system managed. View updates are called through the updatesLayer() method. The layers are drawn multithreaded by OS X.
- Layer Hosted view: In this mode the NSView has no knowledge about the CALayer or any of it’s sublayers it contains and it won’t care about it either. As a programmer you have to manage it. The NSView just shows it, but it will use drawRect:dirtyRect to draw the CALayer.
Nothing says as much as a bit of code. Here’s the code for creating a circular shaped NSView that actually shows an NSImage on screen. I have to remind you that I also wrote a quartzPath methode for NSBezierPath, since the Cocoa version doesn’t have a CGPath method that returns a CGPath of the NSBezierPath.
Here’s the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
import Foundation import AppKit import MCTools @IBDesignable public class CircularImageView: NSView { // This class provides a circular image view. // MARK: New In This Class var imageLayer: CALayer? public var image: NSImage? { didSet { if let newImage = image { newImage.lockFocus() imageLayer?.contents = newImage newImage.unlockFocus() } } } private func prepareLayer() { self.wantsLayer = true } private func drawImageInLayer() { let insetBounds = CGRectInset(bounds, edgeInset, edgeInset) var circularMask: CALayer { var circularImageMask = CAShapeLayer() self.layer!.addSublayer(circularImageMask) let circularMaskPath = NSBezierPath(ovalInRect: insetBounds) circularImageMask.path = circularMaskPath.quartzPath().takeRetainedValue() return circularImageMask } func createNewImageLayer() -> CALayer { let newImageLayer = CALayer() newImageLayer.frame = insetBounds if let imageToSet = image { imageToSet.lockFocus() newImageLayer.contents = imageToSet newImageLayer.contentsGravity = kCAGravityResizeAspectFill imageToSet.unlockFocus() } return newImageLayer } if imageLayer == nil { imageLayer = createNewImageLayer() self.layer!.mask = circularMask self.layer!.addSublayer(imageLayer) } if let imageToSet = image { imageToSet.lockFocus() imageLayer!.contents = imageToSet imageLayer!.contentsGravity = kCAGravityResizeAspectFill imageToSet.unlockFocus() } } // MARK: NSView stuff public override init(frame frameRect: NSRect) { super.init(frame: frameRect) prepareLayer() } public required init?(coder: NSCoder) { super.init(coder: coder) prepareLayer() } public override func updateLayer() { super.updateLayer() drawImageInLayer() } // MARK: Interface Builder Stuff @IBInspectable public var edgeInset: CGFloat = 10 public override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() // Load default test image. println("\(self): prepareForInterfaceBuilder") let processInfo = NSProcessInfo.processInfo() let environment = processInfo.environment let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]! let directories = projectSourceDirectories.componentsSeparatedByString(":") if directories.count != 0 { let firstPath = directories[0] as! String let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg") let image = NSImage(contentsOfFile: imagePath) image!.setName("Test Image") self.image = image } } } |
I hope this is somehow useful to you. Please feel free to use it anyway you like.