NSImageView 72dpi image blurry on HiDPI display - cocoa

I'm loading a 72dpi image into an NSImageView and it looks very blurry on a HiDPI display. How can it get to render perfectly crisp?
I worked out I can improve by adding this entirely non-sensical override to NSImageView, but it doesn't fix it completely.
public class CustomImageView: NSImageView {
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
}
Here's how it looks by default:
With custom override:
As rendered in Preview:

I've found I can fix this by disabling image interpolation in the graphics context. This override will disable interpolation as long as the image is not larger than the draw rect, so it will look crisp if it fits in the view but retains the interpolation if it needs to scale down.
public class CustomImageView: NSImageView {
public override func draw(_ dirtyRect: NSRect) {
if let image = self.image, image.size.width <= dirtyRect.size.width && image.size.height <= dirtyRect.size.height {
NSGraphicsContext.current?.cgContext.interpolationQuality = .none
}
super.draw(dirtyRect)
NSGraphicsContext.current?.cgContext.interpolationQuality = .default
}
}
Still interested in any other answers or info about this. I'm surprised I can't find anything when searching, seems like it would be a common issue to me.
UPDATE
Setting the interpolation directly on the NSGraphicsContext isn't working on macOS 11 (possibly a bug in the system?). It only works if you target the cgContext.

Related

NSPopover Background Color (Including Triangle) with macOS Mojave Dark Mode

Swift 4.2, Xcode 10, macOS 10.14
I have created the following NSView subclass that I put on the root view of all my NSPopover instances in my storyboard. But I noticed that when I switch the color mode in macOS Mojave (from dark to light or the other way around) that it doesn't update the background color of my NSPopover.
class PopoverMain:NSView{
override func viewDidMoveToWindow() {
guard let frameView = window?.contentView?.superview else { return }
let backgroundView = NSView(frame: frameView.bounds)
backgroundView.backgroundColor(color: Color(named: "MyColor")!)
backgroundView.autoresizingMask = [.width, .height]
frameView.addSubview(backgroundView, positioned: .below, relativeTo: frameView)
}
}
I believe this is because a color mode transition only calls these methods (source) and not viewDidMoveToWindow():
updateLayer()
draw(_:)
layout()
updateConstraints()
Has anyone figured out a reliable way to color the background of an NSPopover (including its triangle) and have it work seamlessly on macOS Mojave?
It's funny how writing up your question leads to a solution (sometimes quickly). I realized I needed to create another NSView subclass responsible for generating the NSView that's loaded into the NSPopover. Note the addition of the PopoverMainView class:
class PopoverMain:NSView{
override func viewDidMoveToWindow() {
guard let frameView = window?.contentView?.superview else { return }
let backgroundView = PopoverMainView(frame: frameView.bounds)
backgroundView.autoresizingMask = [.width, .height]
frameView.addSubview(backgroundView, positioned: .below, relativeTo: frameView)
}
}
class PopoverMainView:NSView {
override func draw(_ dirtyRect: NSRect) {
Color(named: "MyColor")!.set()
self.bounds.fill()
}
}

Cocoa Autolayout issue - NSTextView inside a custom NSView

I'm currently writing a Cocoa app, in Swift to exercise the language. I'm not too familiar with the AppKit framework yet, but now I bumped into an interesting problem.
It simply contains an NSWindow, and my custom NSView inside. With autolayout I control the size of the NSView, depending the resized window.
Base structure
As for my custom view, I'd like to have my NSView as a container, and I have an NSTextView inside it.
import Foundation
import AppKit
class Fucky2View: NSView
{
var textView : NSTextView!
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect)
{
super.init(frame: frame)
commonInit()
}
func commonInit()
{
print("initing")
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.red.cgColor
textView = NSTextView.init(frame: self.bounds)
self.addSubview(textView)
self.translatesAutoresizingMaskIntoConstraints = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
textView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
textView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
textView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
func displayText(_ text : String)
{
let attrStr : NSAttributedString = NSAttributedString(string: text+"\n")
textView.textStorage?.append(attrStr)
}
}
So I just want to keep the NSTextView as same size as the view itself. I colored the view background to red, to see if its filling properly, but this is what happens:
https://www.youtube.com/watch?v=LTQndoGzzEM
The textview's height is sometime set properly, but most of the times not.
I recreated the same exact app with UIKit, UIViewController, UIView and UITextView, tested on iPhone (I resized the screen with rotation), and this was working correctly.
Do anyone has any idea? I played along with priorities, but did not helped. Tried several things from NSView, concurrent draw, etc, did not helped.
Tried with NSLayoutConstraint instead of anchors, no change.
The only thing was if I set the TextView's frame in the custom NSView's drawRect: method, but I would like to do a nicer solution.
Or has anyone any other idea?
macOS Sierra 10.12.6, XCode 9.2
Thanks

OSX CustomView Doesn't Work After Window Resize

I have a simple view that displays an NSBezierpath. On mouseDown inside the path, the path's fill color sets to yellow and the view redraws. On mouseDown outside the path, the path's fill color sets to blue and the view redraws.
In my storyboard, I have a single window controller with a window content segue to a view controller. The view, customview class HeartView (below) fills the entire view controller.
Everything works fine until the user resizes the window vertically. After that, the view exhibits bizarre behavior: mouseDown no longer works everywhere inside the path, the recolor sometimes happens on mouseDown outside the path, and the path sometimes (but not always) doesn't completely fill. I think something is going on in the superview, but I don't know what.
import Cocoa
class HeartView: NSView {
var mouseLocation : NSPoint = NSZeroPoint
func drawObject(){
//Create an empty Bezier path
let aBezier : NSBezierPath = NSBezierPath()
aBezier.moveToPoint(CGPoint(x: 176.95,y: 44.90))
aBezier.curveToPoint(CGPoint(x: 166.71,y: 145.89),
controlPoint1: CGPoint(x: 76.63,y: 76.78),
controlPoint2: CGPoint(x: 82.59,y: 206.70))
aBezier.curveToPoint(CGPoint(x: 176.95,y: 44.90),
controlPoint1: CGPoint(x: 237.55,y: 224.76),
controlPoint2: CGPoint(x: 276.83,y: 95.98))
aBezier.closePath()
if (aBezier.containsPoint(NSMakePoint(mouseLocation.x, mouseLocation.y))){
NSColor.yellowColor().setFill()
NSColor.greenColor().setStroke()
} else {
NSColor.blueColor().setFill()
NSColor.orangeColor().setStroke()
}
aBezier.fill()
aBezier.lineWidth = 2.0
aBezier.stroke()
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
drawObject()
}
override func mouseDown(theEvent: NSEvent) {
mouseLocation.x = theEvent.locationInWindow.x
mouseLocation.y = theEvent.locationInWindow.y
self.setNeedsDisplayInRect(self.frame)
}
}
I found the answer in Lucas Derraugh's video on Mouse Events (Cocoa Programming L27). Turns out, I was capturing the mouseDown event in the superview's coordinate system. In the mouseDown event, I used "locationInWindow," which is what caused the strange behavior. I changed the method to:
override func mouseDown(theEvent: NSEvent) {
var viewPoint:NSPoint = self.convertPoint(theEvent.locationInWindow, fromView: nil)
mouseLocation.x = viewPoint.x
mouseLocation.y = viewPoint.y
self.needsDisplay = true
}
to convert from the window's coordinate system to the view's. Things now work well after any window resize event.

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?
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.
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
}
}
}
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
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.
I was having an hard time trying to figure out how you can make an Aspect Fill Clip to Bounds :
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 :]
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
};
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()
}
}
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

Using Autolayout with expanding NSTextViews

My app consists of an NSScrollView whose document view contains a number of vertically stacked NSTextViews — each of which resizes in the vertical direction as text is added.
Currently, this is all managed in code. The NSTextViews resize automatically, but I observe their resizing with an NSViewFrameDidChangeNotification, recalc all their origins so that they don't overlap, and resize their superview (the scroll view's document view) so that they all fit and can be scrolled to.
This seems as though it would be the perfect candidate for autolayout! I set NSLayoutConstraints between the first text view and its container, the last text view and its container, and each text view between each other. Then, if any text view grows, it automatically "pushes down" the origins of the text views below it to satisfy contraints, ultimately growing the size of the document view, and everyone's happy!
Except, it seems there's no way to make an NSTextView automatically grow as text is added in a constraints-based layout? Using the exact same NSTextView that automatically expanded as text was entered before, if I don't specify a constraint for its height, it defautls to 0 and isn't shown. If I do specify a constraint, even an inequality such as >=20, it stays stuck at that size and doesn't grow as text is added.
I suspect this has to do with NSTextView's implementation of -intrinsicContentSize, which by default returns (NSViewNoInstrinsicMetric, NSViewNoInstrinsicMetric).
So my questions: if I subclasses NSTextView to return a more meaningful intrinsicContentSize based on the layout of my text, would my autolayout then work as expected?
Any pointers on implementing intrinsicContentSize for a vertically resizing NSTextView?
I am working on a very similar setup — a vertical stack of views containing text views that expand to fit their text contents and use autolayout.
So far I have had to subclass NSTextView, which is does not feel clean, but works superbly in practice:
- (NSSize) intrinsicContentSize {
NSTextContainer* textContainer = [self textContainer];
NSLayoutManager* layoutManager = [self layoutManager];
[layoutManager ensureLayoutForTextContainer: textContainer];
return [layoutManager usedRectForTextContainer: textContainer].size;
}
- (void) didChangeText {
[super didChangeText];
[self invalidateIntrinsicContentSize];
}
The initial size of the text view when added with addSubview is, curiously, not the intrinsic size; I have not yet figured out how to issue the first invalidation (hooking viewDidMoveToSuperview does not help), but I'm sure I will figure it out eventually.
I had a similar problem with an NSTextField, and it turned out that it was due to the view wanting to hug its text content tightly along the vertical orientation. So if you set the content hugging priority to something lower than the priorities of your other constraints, it may work. E.g.:
[textView setContentHuggingPriority:NSLayoutPriorityFittingSizeCompression-1.0 forOrientation:NSLayoutConstraintOrientationVertical];
And in Swift, this would be:
setContentHuggingPriority(NSLayoutConstraint.Priority.fittingSizeCompression, for:NSLayoutConstraint.Orientation.vertical)
Here is how to make an expanding NSTextView using Auto Layout, in Swift 3
I used Anchors for Auto Layout
Use textDidChange from NSTextDelegate. NSTextViewDelegate conforms to NSTextDelegate
The idea is that textView has edges constraints, which means whenever its intrinsicContentSize changes, it will expand its parent, which is scrollView
import Cocoa
import Anchors
class TextView: NSTextView {
override var intrinsicContentSize: NSSize {
guard let manager = textContainer?.layoutManager else {
return .zero
}
manager.ensureLayout(for: textContainer!)
return manager.usedRect(for: textContainer!).size
}
}
class ViewController: NSViewController, NSTextViewDelegate {
#IBOutlet var textView: NSTextView!
#IBOutlet weak var scrollView: NSScrollView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
activate(
scrollView.anchor.top.constant(100),
scrollView.anchor.paddingHorizontally(30)
)
activate(
textView.anchor.edges
)
}
// MARK: - NSTextDelegate
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
print(textView.intrinsicContentSize)
textView.invalidateIntrinsicContentSize()
}
}
Class ready for copying and pasting. Swift 4.2, macOS 10.14
class HuggingTextView: NSTextView, NSTextViewDelegate {
//MARK: - Initialization
override init(frame: NSRect) {
super.init(frame: frame)
delegate = self
}
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
super.init(frame: frameRect, textContainer: container)
delegate = self
}
required init?(coder: NSCoder) {
super.init(coder: coder)
delegate = self
}
//MARK: - Overriden
override var intrinsicContentSize: NSSize {
guard let container = textContainer, let manager = container.layoutManager else {
return super.intrinsicContentSize
}
manager.ensureLayout(for: container)
return manager.usedRect(for: container).size
}
//MARK: - NSTextViewDelegate
func textDidChange(_ notification: Notification) {
invalidateIntrinsicContentSize()
}
}

Resources