27

So I am used to UIImageView, and being able to set different ways of how its image is displayed in it. Like for example AspectFill mode etc...

I would like to accomplish the same thing using NSImageView on a mac app. Does NSImageView work similarly to UIImageView in that regard or how would I go about showing an image in an NSImageView and picking different ways of displaying that image?

Lukas Würzburger
  • 6,543
  • 7
  • 41
  • 75
zumzum
  • 17,984
  • 26
  • 111
  • 172

8 Answers8

31

You may find it much easier to subclass NSView and provide a CALayer that does the aspect fill for you. Here is what the init might look like for this NSView subclass.

- (id)initWithFrame:(NSRect)frame andImage:(NSImage*)image
{
  self = [super initWithFrame:frame];
  if (self) {
    self.layer = [[CALayer alloc] init];
    self.layer.contentsGravity = kCAGravityResizeAspectFill;
    self.layer.contents = image;
    self.wantsLayer = YES;
  }
  return self;
}

Note that the order of setting the layer, then settings wantsLayer is very important (if you set wantsLayer first, you'll get a default backing layer instead).

You could have a setImage method that simply updates the contents of the layer.

Lukas Würzburger
  • 6,543
  • 7
  • 41
  • 75
Chris Demiris
  • 336
  • 3
  • 3
  • According to doc https://developer.apple.com/documentation/appkit/nsimage it is "Although you can assign an NSImage object directly to the contents property of a CALayer object, doing so may not always yield the best results. Instead of using your image object, you can use the layerContents(forContentsScale:) method to obtain an object that you can use for your layer’s contents." – valR Sep 01 '17 at 09:32
  • Is it possible this behaves differently on different OS versions? I tried this on 10.13.6 and it didn't work while a colleage running mojave showed it working fine – Juan Carlos Ospina Gonzalez Oct 12 '18 at 12:56
  • `[[CALayer alloc] init]` can be replaced with `self.wantsLayer = YES;`. – Jinwoo Kim Jun 11 '23 at 12:13
14

Here is what I'm using, written with Swift. This approach works well with storyboards - just use a normal NSImageView, then replace the name NSImageView in the Class box, with MyAspectFillImageNSImageView ...

open class MyAspectFillImageNSImageView : NSImageView {
  
  open override var image: NSImage? {
    set {
      self.layer = CALayer()
      self.layer?.contentsGravity = kCAGravityResizeAspectFill
      self.layer?.contents = newValue
      self.wantsLayer = true
      
      super.image = newValue
    }
    
    get {
      return super.image
    }
  }

    public override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
    }
    
    //the image setter isn't called when loading from a storyboard
    //manually set the image if it is already set
    required public init?(coder: NSCoder) {
        super.init(coder: coder)
        
        if let theImage = image {
            self.image = theImage
        }
    }

}
Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
Pete
  • 784
  • 8
  • 9
12

I had the same problem. I wanted to have the image to be scaled to fill but keeping the aspect ratio of the original image. Strangely, this is not as simple as it seems, and does not come out of the box with NSImageView. I wanted the NSImageView scale nicely while it resize with superview(s). I made a drop-in NSImageView subclass you can find on github: KPCScaleToFillNSImageView

onekiloparsec
  • 2,013
  • 21
  • 32
11

You can use this: image will be force to fill the view size

( Aspect Fill )

imageView.imageScaling = .scaleAxesIndependently


( Aspect Fit )

imageView.imageScaling = .scaleProportionallyUpOrDown


( Center Top )

imageView.imageScaling = .scaleProportionallyDown

It works for me.

baudsp
  • 4,076
  • 1
  • 17
  • 35
whale
  • 258
  • 1
  • 3
  • 7
  • 15
    From the documentation: _This setting does not preserve the aspect ratio of the image._ – DarkDust Jul 20 '16 at 12:20
  • Worked for me. Thanks! – Sean Aug 23 '17 at 02:17
  • One problem here - because this uses a CALayer to show the image. It won't change the contents in response to dark mode changes. See my answer for a different approach which works around that issue. – Confused Vorlon Jun 24 '20 at 14:03
  • imageView.imageScaling = .scaleAxesIndependently This is not for "Aspect Fill", this is just for "Scale to Fill". – Guru Dev Oct 15 '21 at 09:56
5

I was having an hard time trying to figure out how you can make an Aspect Fill Clip to Bounds :

Credit : https://osxentwicklerforum.de/index.php/Thread/28812-NSImageView-Scaling-Seitenverh%C3%A4ltnis/ Picture credit: https://osxentwicklerforum.de/index.php/Thread/28812-NSImageView-Scaling-Seitenverh%C3%A4ltnis/

Finally I made my own Subclass of NSImageView, hope this can help someone :

import Cocoa

@IBDesignable
class NSImageView_ScaleAspectFill: NSImageView {

    @IBInspectable
    var scaleAspectFill : Bool = false


    override func awakeFromNib() {
        // Scaling : .scaleNone mandatory
        if scaleAspectFill { self.imageScaling = .scaleNone }
    }

    override func draw(_ dirtyRect: NSRect) {

        if scaleAspectFill, let _ = self.image {

            // Compute new Size
            let imageViewRatio   = self.image!.size.height / self.image!.size.width
            let nestedImageRatio = self.bounds.size.height / self.bounds.size.width
            var newWidth         = self.image!.size.width
            var newHeight        = self.image!.size.height


            if imageViewRatio > nestedImageRatio {

                newWidth = self.bounds.size.width
                newHeight = self.bounds.size.width * imageViewRatio
            } else {

                newWidth = self.bounds.size.height / imageViewRatio
                newHeight = self.bounds.size.height
            }

            self.image!.size.width  = newWidth
            self.image!.size.height = newHeight

        }

        // Draw AFTER resizing
        super.draw(dirtyRect)
    }
}

Plus this is @IBDesignable so you can set it on in the StoryBoard

WARNINGS

  • I'm new to MacOS Swift development, I come from iOS development that's why I was surprised I couldn't find a clipToBound property, maybe it exists and I wasn't able to find it !

  • Regarding the code, I suspect this is consuming a lot, and also this has the side effect to modify the original image ratio over the time. This side effect seemed negligible to me.

Once again if their is a setting that allow a NSImageView to clip to bounds, please remove this answer :]

Olympiloutre
  • 2,268
  • 3
  • 28
  • 38
1

Here is another approach which uses SwiftUI under the hood

The major advantage here is that if your image has dark & light modes, then they are respected when the system appearance changes (I couldn't get that to work with the other approaches)

This relies on an image existing in your assets with imageName

import Foundation
import AppKit
import SwiftUI


open class AspectFillImageView : NSView {

    @IBInspectable
    open var imageName: String?
    {
        didSet {
            if imageName != oldValue {
                insertSwiftUIImage(imageName)
            }
        }
    }

    open override func prepareForInterfaceBuilder() {
        self.needsLayout = true
    }


    func insertSwiftUIImage(_ name:String?){
        self.removeSubviews()
        
        guard let name = name else {
            return
        }
        
        let iv = Image(name).resizable().scaledToFill()
        
        let hostView = NSHostingView(rootView:iv)
        self.addSubview(hostView)
      
        //I'm using PureLayout to pin the subview. You will have to rewrite this in your own way...
        hostView.autoPinEdgesToSuperviewEdges()
    }
    
    
    func commonInit() {
        insertSwiftUIImage(imageName)
    }

    
    public override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        
        commonInit()
    }


    required public init?(coder: NSCoder) {
        super.init(coder: coder)

        commonInit()
    }
}
Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
0

Image scalling can be updated with below function of NSImageView.

[imageView setImageScaling:NSScaleProportionally];

Here are more options to change image display property.

enum {
    NSScaleProportionally = 0, // Deprecated. Use NSImageScaleProportionallyDown
    NSScaleToFit,              // Deprecated. Use NSImageScaleAxesIndependently
    NSScaleNone                // Deprecated. Use NSImageScaleNone
};
Surjeet Singh
  • 11,691
  • 2
  • 37
  • 54
0

Answers already given here are very good, but most of them involve subclassing NSView or NSImageView.

You could also achieve the result just by using a CALayer. But in that case you wouldn't have auto layout capabilities.

The simplest solution is to have a NSView, without subclassing it, and setting manually it's layer property. It could also be a NSImageView and achieve the same result.

Example

Using Swift

let view = NSView()
view.layer = .init() // CALayer()
view.layer?.contentsGravity = .resizeAspectFill
view.layer?.contents = image // image is a NSImage, could also be a CGImage
view.wantsLayer = true
vicegax
  • 4,709
  • 28
  • 37