Using Autolayout with expanding NSTextViews - cocoa

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

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

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

Custom TableViewCell with Subview whose height changes

I'm using a TableView and have a custom TableViewCell that I've added a subview to.
The problem is that I need the subview's height to change sometimes and therefore, the table's contentView would have to be updated as well as the row's height.
The subview of the custom TableViewCell is represented by the yellow background.
These images show what's currently happening in my simulator.
On Load
After the event that causes the subview's height to increase
What's the best approach to take with something like this?
Should I use constraints? And if so, what kind of constraints should I use? Would I have to then reload the tableview too every time the subview's size changes?
Here is the code I'm currently using for my custom TableViewCell:
import UIKit
class CustomCell: UITableViewCell {
var newView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.newView = UIView(frame: self.frame)
self.newView.backgroundColor = .yellowColor()
self.addSubview(newView)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.newView.frame.size.width = self.frame.size.width // because self.frame.width is different than it was in the init method
}
func somethingHappenedThatMySubviewHasToIncreaseInHeight() {
self.newView.frame.size.height = self.frame.size.height + 40
}
}
The best approach is to use Auto Layout and self-sizing cells. Setup constraints in storyboard for your custom cell.
You will not need to reload the tableView. Each cell will automatically adjust its height, based on how much vertical space its subview takes.
For more information, see the detailed walkthrough by smileyborg in his answer to Using Auto Layout in UITableView for dynamic cell layouts & variable row heights.

Setting minimum width of NSSplitViews

I'm having a heck of a time setting up a simple split view. The first split view is collapsed. I need to set a minimum width for it. Everything I see online (scarce for NSSplitViewController/NSSplitView) is for Objective-C, puts everything in the app delegate, and uses XIBs.
Here's the scenario:
Window Controller with a segue to a SplitView Controller, which has two split views (2 view controllers).
Which object needs to have the NSSplitViewDelegate?
EDIT: Adding code snippet:
For example, I have this:
import Cocoa
class ViewController: NSSplitViewController, NSSplitViewDelegate {
#IBOutlet weak var pdlSplitView: NSSplitView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func splitView(splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat {
return proposedMinimumPosition + 200
}
}
Is there more that I'm missing?
Thanks
UPDATE
Based on comments below, I've made a change, but now I get a sigAbort on the class definition for the AppDelegate. Full code
ViewController:
import Cocoa
class ViewController: NSSplitViewController, NSSplitViewDelegate {
#IBOutlet weak var pdlSplitView: NSSplitView!
let publicDataListings : PDL = PDL()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.pdlSplitView.delegate = self
}
override func splitView(splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat {
return proposedMinimumPosition + 200
}
}
SidebarViewController:
import Cocoa
class SidebarViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
DatasetViewController:
import Cocoa
class DatasetViewController: NSViewController, NSSplitViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
Update
I took away my custom NSSplitViewController class and created two NSSplitView classes, one with the constraint method. Now, I see both subviews, but they're far smaller than they should be:
Is there anyone at all that has done this with Swift and Storyboards?
No coding is required to set a minimum width in a storyboard with auto layout for a NSSplitViewController/NSSplitView.
Select the CustomView that you require a minimum width for (e.g. 200), and add a width constraint set to the required value which will add a "Equal" constraint (e.g. Custom View.Width equals 200).
Next locate that new constraint and change the constraint relation to "Greater Than or Equal" (e.g. so you now have width ≥ 200).
You now have a minimum width in an NSSplitView. You can then use the Priority field to resolve any conflicts with any other auto layout constraints.
These values are not exposed in the storyboard, which is a great shame, but NSSplitViewItem has minimumThickness and maximumThickness properties which you can use. (This overrides the holding priority, so if you set minimumThickness for one splitViewItem, the other one(s) will now shrink into nothing if you make the window small enough.)
There is also automaticMaximumThickness (I cannot work out how this interacts with the other values) and preferredThicknessFraction which had no effect when I played with it under 10.13.
Set NSSplitViewController as delegate of NSSplitView (the split view you want to constrain). In your case it should be - in xib hook the delegate outlet of the NSSplitView to file owner (I guess the file owner is NSSplitViewController subclass)
Implement
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)dividerIndex { ... }
in NSSplitViewController
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSSplitViewDelegate_Protocol/index.html#//apple_ref/occ/intfm/NSSplitViewDelegate/splitView:constrainMinCoordinate:ofSubviewAt:

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