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()
Related
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
I am using an IBInspectable NSColor property to set the background colour of my view but when I use the colour picker in xcode it lets me initially set the value but then when I go to change any other value on the picker it no longer takes effect.
Only when I reclick the colour picker will it allow me to make futher changes.
Am I missing something here?
Kind Regards
Brian
#IBDesignable class PianoRollGridView: NSView {
#IBInspectable var backgroundColour: NSColor = NSColor(deviceRed:0.16, green:0.17, blue:0.21, alpha:1.00) {
didSet {
layer?.backgroundColor = backgroundColour.cgColor
}
}
required init?(coder: NSCoder) {
super.init(coder:coder)
self.wantsLayer = true
}
}
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.
How do I get my NSStackView to lay out my view one after the other ? My blue box is drawing on top of my red box.
import Cocoa
class TestView : NSView{
override init() {
super.init(frame: NSRect(x: 0,y: 0,width: 100,height: 100))
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.redColor().setFill()
NSBezierPath.fillRect(self.bounds)
}
}
class TestView2 : NSView{
override init() {
super.init(frame: NSRect(x: 0,y: 0,width: 100,height: 100))
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.blueColor().setFill()
NSBezierPath.fillRect(self.bounds)
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var searchFacetItems: SearchFacetSelectorView!
#IBOutlet weak var searchFacetHeader: SearchFacetSelectorHeader!
var content : NSStackView!
let testView = TestView()
let testView2 = TestView2()
func applicationDidFinishLaunching(aNotification: NSNotification?) {
// Insert code here to initialize your application
content = NSStackView(frame: window.contentView.bounds)
content.orientation = NSUserInterfaceLayoutOrientation.Vertical
content.alignment = NSLayoutAttribute.CenterX
content.addView(testView, inGravity: NSStackViewGravity.Center)
content.addView(testView2, inGravity: NSStackViewGravity.Center)
window.contentView = content!
}
func applicationWillTerminate(aNotification: NSNotification?) {
// Insert code here to tear down your application
}
}
learn by reading
It's not appkit, but maybe i'll get some clues: HOW TO USE UIVIEWS WITH AUTO LAYOUT PROGRAMMATICALLY
Auto Layout Guide
You should be able to do this by adjusting the NSRect coordinates. Currently you've got both NSRects using the same exact coordinates:
NSRect(x: 0, y: 0, width: 100, height: 100)
If you want the NSView horizontally (on the right) located next of the other:
NSRect(0, 0, 100, 100) // TestView1
NSRect(100, 0, 100, 100) // TestView2
vertically below:
NSRect(0, 0, 100, 100) // TestView1
NSRect(0, -100, 100, 100) // TestView2
The views don't have any constraints that describe their preferred sizes, such as intrinsicContentSizes or explicit constraints. And NSStackView only adds constraints that positions its stacked views relative to each other, not ones to size individual views.
Without them, their size in the stacking axis becomes ambiguous and will typically give all of the sizing to a single view. In your example, I'd guess that the blue box is not drawing on top of the red box, but just that the red box has a 0 height (and is stacked after the blue view).
Adding NSButtons to a stack view don't have this problem, as they do have a defined intrinsicContentSize.
Depending on what your views are -- do they have intrinsic sizes, or will they be defined by constraints to internal subviews -- you'll either want to override intrinsicContentSize or add those constraints that end up defining their heights.
Edit: Oh, and make sure translatesAutoresizingMaskIntoConstraints is set to false on the views you're adding to the stack view (it doesn't look like you are from the sample code). If not, you'll quickly run into trouble where the autoresizing constraints are trying to position the view and conflicting with constraints that the stack view adds.
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()
}
}