OSX CustomView Doesn't Work After Window Resize - macos

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.

Related

Animating constraints with layer-backed NSView

I'm attempting to implement an animation that shows/hides a view in a horizontal arrangement. I'd like this to happen with slide, and with no opacity changes. I'm using auto-layout everywhere.
Critically, the total width of the containing view changes with the window. So, constant-based animations are not possible (or so I believe, but happy to be proved wrong).
|- viewA -|- viewB -|
My first attempt was to use NSStackView, and animate the isHidden property of an arranged subview. Despite seeming like it might do the trick, I was not able to pull off anything close to what I was after.
My second attempt was to apply two constraints, one to force viewB to be zero width, and a second to ensure the widths are equal. On animation I change the priorities of these constraints from defaultHigh <-> defaultLow.
This results in the correct layout in both cases, but the animation is not working out.
With wantsLayer = true on the containing view, no animation occurs whatsoever. The views just jump to their final states. Without wantsLayer, the views do animate. However, when collapsing, viewA does a nice slide, but viewB instantly disappears. As an experiment, I changed the zero width to a fixed 10.0, and with that, the animation works right in both directions. However, I want the view totally hidden.
So, a few questions:
Is it possible to animate layouts like this with layer-backed views?
Are there other techniques possible for achieving the same effect?
Any ideas on how to achieve these nicely with NSStackView?
class LayoutAnimationViewController: NSViewController {
let containerView: NSView
let view1: ColorView
let view2: ColorView
let widthEqualContraint: NSLayoutConstraint
let widthZeroConstraint: NSLayoutConstraint
init() {
self.containerView = NSView()
self.view1 = ColorView(color: NSColor.red)
self.view2 = ColorView(color: NSColor.blue)
self.widthEqualContraint = view2.widthAnchor.constraint(equalTo: view1.widthAnchor)
widthEqualContraint.priority = .defaultLow
self.widthZeroConstraint = view2.widthAnchor.constraint(equalToConstant: 0.0)
widthZeroConstraint.priority = .defaultHigh
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = containerView
// view.wantsLayer = true
view.addSubview(view1)
view.addSubview(view2)
view.subviewsUseAutoLayout = true
NSLayoutConstraint.activate([
view1.topAnchor.constraint(equalTo: view.topAnchor),
view1.bottomAnchor.constraint(equalTo: view.bottomAnchor),
view1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// view1.trailingAnchor.constraint(equalTo: view2.leadingAnchor),
view2.topAnchor.constraint(equalTo: view.topAnchor),
view2.bottomAnchor.constraint(equalTo: view.bottomAnchor),
view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor),
view2.trailingAnchor.constraint(equalTo: view.trailingAnchor),
widthEqualContraint,
widthZeroConstraint,
])
}
func runAnimation() {
view.layoutSubtreeIfNeeded()
self.widthEqualContraint.toggleDefaultPriority()
self.widthZeroConstraint.toggleDefaultPriority()
// self.leadingConstraint.toggleDefaultPriority()
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
context.duration = 3.0
self.view.layoutSubtreeIfNeeded()
}) {
Swift.print("animation complete")
}
}
}
extension LayoutAnimationViewController {
#IBAction func runTest1(_ sender: Any?) {
self.runAnimation()
}
}
Also, some potentially relevant, but so far unhelpful, related questions:
Animating Auto Layout changes concurrently with NSPopover contentSize change
Animating Auto Layout constraints with NSView.layoutSubtreeIfNeeded() not working on macOS High Sierra
Hide view item of NSStackView with animation

NSStatusItem fullscreen issues

I'm making a statusbar app that displays an NSPopover when the NSStatusItem is clicked, like this:
I have added the ability to resize the popover by dragging on the edges, by subclassing the popover's view like this:
class CMView: NSView {
let tolerance : CGFloat = 10
var state = false
override func mouseDown(theEvent: NSEvent) {
let point = self.convertPoint(theEvent.locationInWindow, fromView: nil)
if (point.y <= tolerance) {
state = true
}
}
override func mouseDragged(theEvent: NSEvent) {
if (state) {
let point = self.convertPoint(theEvent.locationInWindow, fromView: nil)
self.frame = NSRect(
x: self.frame.origin.x,
y: self.frame.origin.y,
width: self.frame.size.width,
height: self.frame.size.height-point.y)
popover.contentSize = self.frame.size
}
}
override func mouseUp(theEvent: NSEvent) {
state = false
}
}
This only works if the desktop isn't in full screen. If I try to resize it in fullscreen, it simply doesn't work, and the popover arrow disappears mysteriously.
It seems like the popover isn't redrawing when invoked in a fullscreen environment. Is there any way around this problem?
Here at WWDC. Asking the same question. You have to have an app that's an UIElement app - meaning no dock icon, no main menu.

Handle NSButton click programmatically

I'm attempting to make a subclassed NSButton handle a click event with no luck.
To this point I have been customizing the drawRect functions in my views to get custom looks and drawing these views programmatically. To this end, I have essentially orphaned my usage of the Interface builder (simply to avoid its sheer complexity).
Is there a way for me to assign a click (or mouseDown in this scenario) handler programmatically, this doesn't seem to be working
class MainButton: NSButton {
override func mouseDown(theEvent: NSEvent!) {
let alert = NSAlert()
alert.messageText = "Title"
alert.runModal()
}
override func drawRect(rect: NSRect) {
super.drawRect(rect)
let circle : NSBezierPath = NSBezierPath(
roundedRect: rect, xRadius: rect.size.width / 2,
yRadius: rect.size.height / 2
)
circle.lineWidth = 2
NSColor(calibratedWhite: 0.2, alpha: 1).setStroke()
circle.stroke()
}
}
Here is a full gist of the app for context if needed.

Soft scroll animation NSScrollView scrollToPoint:

I want to create soft animation between transitions in simply UI:
view that moved
When a call scrollToPoint: for move view to point that transition doesn't animate.
I'm newbe in Cocoa programming (iOS is my background). And I don't know how right use .animator or NSAnimationContext.
Also I was read Core Animation guide but didn't find the solution.
The source can be reach on Git Hub repository
Please help !!!
scrollToPoint is not animatable. Only animatable properties like bounds and position in NSAnimatablePropertyContainer are animated. You don't need to do anything with CALayer: remove the wantsLayer and CALayer stuff. Then with following code it is animated.
- (void)scrollToXPosition:(float)xCoord {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:5.0];
NSClipView* clipView = [_scrollView contentView];
NSPoint newOrigin = [clipView bounds].origin;
newOrigin.x = xCoord;
[[clipView animator] setBoundsOrigin:newOrigin];
[_scrollView reflectScrolledClipView: [_scrollView contentView]]; // may not bee necessary
[NSAnimationContext endGrouping];
}
Swift 4 code of this answer
func scroll(toPoint: NSPoint, animationDuration: Double) {
NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = animationDuration
let clipView = scrollView.contentView
clipView.animator().setBoundsOrigin(toPoint)
scrollView.reflectScrolledClipView(scrollView.contentView)
NSAnimationContext.endGrouping()
}
The proposed answers have a significant downside: If the user tries to scroll during an ongoing animation, the input will be cause jittering as the animation will forcefully keep on going until completion. If you set a really long animation duration, the issue becomes apparent. Here is my use case, animating a scroll view to snap to a section title (while trying to scroll up at the same time):
I propose the following subclass:
public class AnimatingScrollView: NSScrollView {
// This will override and cancel any running scroll animations
override public func scroll(_ clipView: NSClipView, to point: NSPoint) {
CATransaction.begin()
CATransaction.setDisableActions(true)
contentView.setBoundsOrigin(point)
CATransaction.commit()
super.scroll(clipView, to: point)
}
public func scroll(toPoint: NSPoint, animationDuration: Double) {
NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = animationDuration
contentView.animator().setBoundsOrigin(toPoint)
reflectScrolledClipView(contentView)
NSAnimationContext.endGrouping()
}
}
By overriding the normal scroll(_ clipView: NSClipView, to point: NSPoint) (invoked when the user scrolls) and manually performing the a scroll inside a CATransaction with setDisableActions, we cancel the current animation. However, we don't call reflectScrolledClipView, instead we call super.scroll(clipView, to: point), which will perform other necessary internal procedures and then perform reflectScrolledClipView.
Above class produces better results:
Here is a Swift 4 extension version of Andrew's answer
extension NSScrollView {
func scroll(to point: NSPoint, animationDuration: Double) {
NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = animationDuration
contentView.animator().setBoundsOrigin(point)
reflectScrolledClipView(contentView)
NSAnimationContext.endGrouping()
}
}
I know, it's a little bit off topic, but I wanted to have a similar method to scroll to a rectangle with animation like in UIView's scrollRectToVisible(_ rect: CGRect, animated: Bool) for my NSView. I was happy to find this post, but apparently the accepted answer doesn't always work correctly. It turns out that there is a problem with bounds.origin of the clipview. If the view is getting resized (e.g. by resizing the surrounding window) bounds.origin is somehow shifted against the true origin of the visible rectangle in y-direction. I could not figure out why and by how much. Well, there is also this statement in the Apple docs not to manipulate the clipview directly since its main purpose is to function internally as a scrolling machine for views.
But I do know the true origin of the visible area. It’s part of the clipview’s documentVisibleRect. So I take that origin for the calculation of the scrolled origin of the visibleRect and shift the bounds.origin of the clipview by the same amount, and voilà: that works even if the view is getting resized.
Here is my implementation of the new method of my NSView:
func scroll(toRect rect: CGRect, animationDuration duration: Double) {
if let scrollView = enclosingScrollView { // we do have a scroll view
let clipView = scrollView.contentView // and thats its clip view
var newOrigin = clipView.documentVisibleRect.origin // make a copy of the current origin
if newOrigin.x > rect.origin.x { // we are too far to the right
newOrigin.x = rect.origin.x // correct that
}
if rect.origin.x > newOrigin.x + clipView.documentVisibleRect.width - rect.width { // we are too far to the left
newOrigin.x = rect.origin.x - clipView.documentVisibleRect.width + rect.width // correct that
}
if newOrigin.y > rect.origin.y { // we are too low
newOrigin.y = rect.origin.y // correct that
}
if rect.origin.y > newOrigin.y + clipView.documentVisibleRect.height - rect.height { // we are too high
newOrigin.y = rect.origin.y - clipView.documentVisibleRect.height + rect.height // correct that
}
newOrigin.x += clipView.bounds.origin.x - clipView.documentVisibleRect.origin.x // match the new origin to bounds.origin
newOrigin.y += clipView.bounds.origin.y - clipView.documentVisibleRect.origin.y
NSAnimationContext.beginGrouping() // create the animation
NSAnimationContext.current.duration = duration // set its duration
clipView.animator().setBoundsOrigin(newOrigin) // set the new origin with animation
scrollView.reflectScrolledClipView(clipView) // and inform the scroll view about that
NSAnimationContext.endGrouping() // finaly do the animation
}
}
Please note, that I use flipped coordinates in my NSView to make it match the iOS behaviour.
BTW: the animation duration in the iOS version scrollRectToVisible is 0.3 seconds.
https://www.fpposchmann.de/animate-nsviews-scrolltovisible/

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