zumzum
zumzum

Reputation: 20148

NSImageView image aspect fill?

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?

Upvotes: 30

Views: 17390

Answers (8)

Vicente Garcia
Vicente Garcia

Reputation: 6380

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

Upvotes: 1

Confused Vorlon
Confused Vorlon

Reputation: 10446

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()
    }
}

Upvotes: 1

Surjeet Singh
Surjeet Singh

Reputation: 11939

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
};

Upvotes: 0

Pete
Pete

Reputation: 794

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
        }
    }

}

Upvotes: 15

Chris Demiris
Chris Demiris

Reputation: 336

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.

Upvotes: 31

Crocobag
Crocobag

Reputation: 2340

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 :]

Upvotes: 5

whale
whale

Reputation: 286

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.

Upvotes: 12

onekiloparsec
onekiloparsec

Reputation: 2063

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

Upvotes: 12

Related Questions