Cocoa Autolayout issue - NSTextView inside a custom NSView - cocoa

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

Related

Why is NSComboBox content in the incorrect position?

I'm trying to layout an NSComboBox in a Mac app and I'm getting some weird behaviour. In reality I've got a view which has an NSComboBox as a subview along with some other subviews, but I've created a simple example to display the same issue I'm seeing.
For some reason, the text field isn't exactly vertically centered like I'd expect.
Here's my TestComboBox:
Here's an Apple NSComboBox (note the text is vertically centered and there's a small amount of leading spacing before the highlight colour):
I've created a simple example to show the issue:
class TestComboBox: NSView {
private let comboBox = NSComboBox(labelWithString: "")
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
comboBox.isEditable = true
addSubview(comboBox)
}
override func layout() {
comboBox.sizeToFit()
comboBox.frame = CGRect(x: 0, y: 0, width: bounds.width, height: comboBox.bounds.height)
}
}
If you add the above view to a basic Mac app (Storyboard) and pin it to the view controller with the following constraints:
I think I'm trying to layout the combo box correctly, but I'm not sure why it looks slightly different to the Apple example as they're both using NSComboBox!
Any guidance much appreciated!
The combo box is initialized with
private let comboBox = NSComboBox(labelWithString: "")
but init(labelWithString:) is a convenience initializer of NSTextField to create a label. Use init() instead:
private let comboBox = NSComboBox()

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

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

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

Does NSView have a backgroundColor? If it does, I can't find it

I'm seeing all over the place online where people are referring to NSView's backgroundColor. I need to set a custom backgroundColor to my NSView, but I'm not seeing that property. I can't see it in code, or in IB. I am unable to set the background color of my simple NSView.
What could I be missing?
They must be thinking of UIView, which does have a backgroundColor property. NSView does not have a backgroundColor property.
You will have to achieve your effect some other way, e.g., through subclassing NSView.
Strangely enough NSView does not provide this (sigh). I use one consistent class I wrote myself throughout all my macOs projects. Just change the CustomView class in the Identity Inspector tab in IB to ColorView. You will then be able to set the background color in the Attribute Inspector, just like you would for a UIView. Here's the code, hope this helps!
import Cocoa
class ColorView: NSView
{
#IBInspectable var backgroundColor:NSColor?
required init?(coder decoder: NSCoder)
{
super.init(coder: decoder)
wantsLayer = true
}
override init(frame frameRect: NSRect)
{
super.init(frame: frameRect)
wantsLayer = true
}
override func layout()
{
layer?.backgroundColor = backgroundColor?.cgColor
}
}
You can reach it via the view's layer, e.g. (in Swift):
view.layer?.backgroundColor = NSColor.blue.cgColor
Strangely it can be set in the xib. In the identity inspector for the view, add the User Defined Runtime Attribute backgroundColor, with the Type color.

Resources