NSViewController story board with coded anchors collapses window - swift

I'm having trouble mixing story boards and coded autolayout in Cocoa + Swift. It should be possible right?
I started with a NSTabViewController defined in a story board with default settings as dragged out of the toolbox. I added an NSTextField view via code. And I added anchors. Everything works as expected except the bottom anchor.
After adding the bottom anchor, the window and controller seem to collapse to the size of the NSTextField. I expected the opposite, that the text field get stretched to fill the height of the window.
What am I doing wrong? The literal Frame maybe? Or some option flag that I'm not setting?
class NSTabViewController : WSTabViewController {
var summaryView : NSTextField
required init?(coder: NSCoder) {
summaryView = NSTextField(frame: NSMakeRect(20,20,200,40))
summaryView.font = NSFont(name: "Menlo", size: 9)
super.init(coder: coder)
}
override func viewDidLoad() {
self.view.addSubview(summaryView)
summaryView.translatesAutoresizingMaskIntoConstraints = false
summaryView.topAnchor.constraintEqualToAnchor(self.view.topAnchor, constant: 5).active = true
summaryView.leftAnchor.constraintEqualToAnchor(self.view.leftAnchor, constant: 5).active = true
summaryView.rightAnchor.constraintEqualToAnchor(self.view.rightAnchor, constant: -5).active = true
summaryView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor, constant: -5).active = true
}

To prevent window from collapsing set lower priority for hugging:
summaryView.setContentHuggingPriority(249, forOrientation: .Vertical)
But you actually misuse tab view controller. It just manages views in common use... while you are adding text to the tab header area. There is a very good tutorial of how to use it correctly: https://www.youtube.com/watch?v=TS4H3WvIwpY

Related

Scrollable NSTextView with custom NSTextStorage for formatting

I'm trying to make a text editor with formatting for Mac OS. Which I have working using an NSTextView together with a custom NSTextStorage class. Which applies attributes like bold etc to NSAttributableStrings.
This all seems to work fine as seen in screenshot one below. Which is an NSTextView with a custom NSTextStorage class attached to it. Which applies the formatting through attributes on an NSAttributeableString
However, having everything the same, but getting a scrollable NSTextView from the Apple supplied function NSTextView.scrollableTextView() it does not display any text at all. Even though you can see in the screenshot that the NStextView is actually visible. Also, moving my mouse over the editor changes the cursor to the editor cursor. But I can't select, type or do anything.
Doing the exact same thing as above, but not supplying a text container to the text view does show that it is wired up correctly, since I do get a scrollable text view then. Where the scrolling actually works, but then of course the formatting is no longer applied.
So I'm confused on what I have to do now.
This is basically my setup:
//
// TextDocumentViewController.swift
//
// Created by Matthijn on 15/02/2022.
// Based on LayoutWithTextKit2 Sample from Apple
import Foundation
import AppKit
class TextDocumentViewController: NSViewController {
// Extends NSTextStorage, applies attributes to NSAttributeAbleString for formatting
private var textStorage: TextStorage
// Not custom yet, default implementation - do I need to subclass this specifically and implement something to support the scrolling behaviour? Which seems weird to me since it does work without scrolling support
private var layoutManager: NSLayoutManager
// Also default implementation
private var textContainer: NSTextContainer
private var textDocumentView: NSTextView
private var scrollView: NSScrollView
required init(content: String) {
textStorage = TextStorage(editorAttributes: MarkdownAttributes)
layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
textContainer = NSTextContainer()
// I'm not 100% sure if I need this on false or true or can just do defaults. No combination fixes it
// textContainer.heightTracksTextView = false
// textContainer.widthTracksTextView = true
layoutManager.addTextContainer(textContainer)
scrollView = NSTextView.scrollableTextView()
textDocumentView = (scrollView.documentView as! NSTextView)
// Basically commenting this out, stops applying my NSTextContainer, NSLayoutManager and NSTextContainer, but then of course the formatting is not applied. This one line changes it between it works without formatting, or it doesn't work at all. (Unless I have my text view not embedded in a scroll view) then it works but the scrolling of course then does not work.
textDocumentView.textContainer = textContainer
textDocumentView.string = content
textDocumentView.isVerticallyResizable = true
textDocumentView.isHorizontallyResizable = false
super.init(nibName: nil, bundle: nil)
}
override func loadView() {
view = scrollView
}
}
You can actually create the NSScrollView instance with scrollableTextView() and you can get the implicitly created documentView (NSTextView).
Finally, one can assign the existing LayoutManager of the documentView to the own TextStorage class inheriting from NSTextStorage.
In viewDidLoad you could then add the scrollView traditionally to the view using addSubview:. For AutoLayout, as always, translatesAutoresizingMaskIntoConstraints must be set to false.
With widthAnchor and a greaterThanOrEqualToConstant you can define a minimum size, so the window around it cannot be made smaller by the user. The structure also allows potential later simple extensions with additional sticky views (e.g. breadcrumb view etc).
Code
If you implement it this way, then for a small minimal test it might look something like this.
import Cocoa
class ViewController: NSViewController {
private var scrollView: NSScrollView
private var textDocumentView: NSTextView
required init(content: String) {
scrollView = NSTextView.scrollableTextView()
let textDocumentView = scrollView.documentView as! NSTextView
self.textDocumentView = textDocumentView
let textStorage = TextStorage(editorAttributes: MarkdownAttributes())
textStorage.addLayoutManager(textDocumentView.layoutManager!)
textDocumentView.string = content
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.widthAnchor.constraint(greaterThanOrEqualToConstant: 200.0),
scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200.0),
])
}
override func loadView() {
self.view = NSView()
}
}
Test
Here's a quick test showing that both display and scrolling work as expected with the setup:

NSTextField created from custom class appears randomly

I have a main NSView with 2 NSView subviews containing each a custom NSButton (all created in Interface Builder). My custom button class is programmatically creating an NSTextField below the buttons, by adding it from the superview:
And the custom NSButton class code:
class buttonUpDown: NSButton {
var myLabel:NSTextField!
required public init?(coder: NSCoder) {
super.init(coder: coder)
let labelWidth=CGFloat(90)
print("init")
superview?.autoresizesSubviews = false
myLabel = NSTextField(frame: NSMakeRect(frame.origin.x+frame.size.width/2-labelWidth/2,frame.origin.y-20,labelWidth,16))
myLabel?.stringValue = "Mesh Quality"
myLabel.alignment = .center
myLabel.font? = .systemFont(ofSize: 8)
myLabel.backgroundColor = .red
myLabel.isBordered = false
myLabel.isEditable = false
superview!.addSubview(myLabel)
}
}
When I have a single Button referencing the class, the label appears properly.
But when I have 2, only 1 label is appearing randomly left or right:
What do I miss to have a text displayed below each button ?
Found the issue. Creating a label in the init function will give erratic positioning of the view frame. Adding them from the draw func, makes it more reliable.

How to programmatically set the constraints of the subViews of a UIPageViewController?

I have contained the subViews of a UIPageViewController within a UIView so that my screen has a partial scrollView container. However, the subViewControllers extend beyond both, the UIView that is supposed to contain the (horizontal/swiping page style) scrollView and the screen of the device.
I have already tried to use autolayout constraints but the subViews still go beyond the device screen.
Here is the UIView that contains the subViews of the UIPVC:
let pagingContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
and here is the set up within viewDidLoad():
let pageController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
addChild(pageController)
pageController.didMove(toParent: self)
pagingContainer.addSubview(pageController.view)
In case I haven't articulated properly:
What I wish for to happen is that the bottom half of my screen is a horizontal-page-style swiping scrollView that contains x number of subViewControllers (under UIPVC), and the size of subViewControllers are limited to the size of the UIView(pagingContainer).
I think I might understand what you're asking.
It should be pretty simple, set your left/right/top/bottom constraints for the pageController.view to be equal to the pagingContainer
In my example, I'm using SnapKit, so I set the edges equal to superview (which is the paingContainer).
let pageController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
self.addChild(pageController)
pageController.didMove(toParent: self)
pagingContainer.addSubview(pageController.view)
// I set up constraints with SnapKit (since I mostly use that pod)
pageController.view.snp.makeConstraints({ (make) in
make.edges.equalToSuperview()
})
// But if I remember correctly, you can also set it like so:
pageController.view.translatesAutoresizingMaskIntoConstraints = false
pageController.view.widthAnchor.constraint(equalTo: self.pagingContainer.widthAnchor).isActive = true
pageController.view.heightAnchor.constraint(equalTo: self.pagingContainer.heightAnchor).isActive = true
pageController.view.centerXAnchor.constraint(equalTo: self.pagingContainer.centerXAnchor).isActive = true
Here is a quick gif of what it looks like. Main view controller only has red background and a pagingContainer on the bottom half and inset of 30 on each side (to demonstrate the size of pageController being within the pagingContainer and not overflowing)

Anchor Constraints not Honored in Xcode 10 / iOS 12

This all worked yesterday under Xcode 9 and iOS 11; however, after updating to Xcode 10 and iOS 12, the view is no longer showing. I was having a video displayed inside a view. Today I could hear but not see the video. I checked the frame and found it to be zero which explained the issue. However, nothing had changed from the prior version. I have removed the video stuff and have just looked at the view's frame after anchor constraints were applied and they are all zero.
import UIKit
import AVKit
class VideoView: UIView {
private var videoURL:URL!
private var parentView:UIView!
private var avPlayer:AVPlayer!
private var avPlayerLayer:AVPlayerLayer!
init(url:URL, parentView:UIView) {
super.init(frame: .zero)
self.videoURL = url
self.parentView = parentView
setup()
}
private func setup() {
self.translatesAutoresizingMaskIntoConstraints = false
self.parentView.addSubview(self)
self.topAnchor.constraint(equalTo: self.parentView.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
self.leadingAnchor.constraint(equalTo: self.parentView.leadingAnchor, constant: 8).isActive = true
self.trailingAnchor.constraint(equalTo: self.parentView.trailingAnchor, constant: -8).isActive = true
self.heightAnchor.constraint(equalToConstant: 200).isActive = true
print(self.parentView.frame)
print(self.frame)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
notice the two print statements at the end of setup(). The parentView return 0,0,414,736. The self view returns 0,0,0,0.
If in Init I set the frame size it will respect it; however, it does not apply the anchor constraints so the size of the view will remain whatever it is I put into the init.
It appears that the anchor constraints are not at all being taken into account. There is no error in the debugger about them and as we can see, translatesAutoresizingMaskIntoConstraints is set to false and all constraints have the isActive set to true.
Only the Xcode and iOS version have changed. What am I missing?
Update 1:
If I create a label and add it to self, the label shows fine. If I create a background color for self that also shows fine including the proper height as set with the anchor constraints. However, the frame remains at zero. So when trying to add an AVPlayerLayer by setting its frame to the bounds of self it doesn't work because of course self remains at zero. So the question remains as to why the frame's dimensions are not changing after initialization.
Update 2:
I added a self.layoutIfNeeded() just after applying the anchor constraints and that seems to have solved the issue. Although looking at the frame for self and I get -199,-100,398,200 Cannot say I understand the values for X and Y. Nevertheless the layOutIfNeeded seems to have solved the problem although why this is required in Xcode 10/ iOS 12 is also a mystery.
I posted what seems to be the answer in my second update, but for clarity:
I added a self.layoutIfNeeded() just after applying the anchor constraints and that seems to have solved the issue. Although looking at the frame for self and I get -199,-100,398,200 Cannot say I understand the values for X and Y. Nevertheless the layOutIfNeeded seems to have solved the problem although why this is required in Xcode 10/ iOS 12 is also a mystery.

UIView cropped off even though constraints are set

I've created a custom view in a xib file, in which I've set all simulated Metrics to "Inferred"
In my view I float one stack view x to the left by pinning the left and the top side and stack view to the right by pinning the right and top. Somehow like this (the = sign symbolizes the screen borders)
==========
=x y=
= =
= =
= =
= =
==========
I don't set any constraints for the width and the height, since everything should be inferred.
The corresponding class to my view is quite simple:
class MyView: UIView {
var view:UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
view = UIView.loadFromNibNamed("MyView")
addSubview(view)
}
}
extension UIView {
class func loadFromNibNamed(nibNamed: String, bundle : NSBundle? = nil) -> UIView? {
return UINib(
nibName: nibNamed,
bundle: bundle
).instantiateWithOwner(nil, options: nil)[0] as? UIView
}
}
I use this view in my storyboard by pinning it to the left, right, top and bottom.
.
The thing is that the view is loaded from the nib, but that the width is somehow not dynamic. I would expect to the view to resize itself depending on the size of the parent. However, approximately 1/3 of the right Stack View is cropped off and not visible on screen even though I've set the constraints.
What do I have to do to fix this?
You need to set the translatesAutoresizingMaskIntoConstraints property to false for any view you load from a XIB if you want it to have a dynamic frame size. Otherwise the system will automatically create layout constraints like a fixed width and a fixed height that will keep your view from resizing.
Additionally, you need to add some constraints in code after adding your view to the view hierarchy that pin its top, bottom, left and right edge to the corresponding edges of its superview. After all, you're adding the XIB's contents as a subview to your custom view MyView i.e. the two views are not the same and you need to tell the system how it should position the subview (the constraints you added in your storyboard only relate to your MyView instance, not to its subview). These additions to your code should do the trick:
class MyView: UIView {
var view:UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
view = UIView.loadFromNibNamed("MyView")
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
// Pin view to all four edges of its superview
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: [], metrics: nil, views: ["view": view]))
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: [], metrics: nil, views: ["view": view]))
}
}
Side notes:
Read a more detailed explanation on the translatesAutoresizingMaskIntoConstraints property in the official documentation.
All settings in the "simulated metrics" section in Interface Builder only apply to Interface Builder itself. They won't have any effect on your app when you run it on Simulator or a real device.
We need to identify were the issue is, so let's start by confirming that the stack view's are getting their frame's set. In your view controller where the stack views are we can override the following methods and check the frames of the stack views.
viewWillLayoyutSubviews()
viewDidLayoutSubviews()
Confirm that the frame's are getting set we can now look at the constraints that are being set in the storyboard. It is not clean fro the information you have provided how you want the stack views layout together, is one on top of the other view? Are they supposed to be side-by-side?
You will need to check that the constraints are valid for the layout you you want.
3.Confirm the Size Classes for your view(s) are configured correctly. Apple has a document called 'Size Classes Design Help' here. I believe this is your problem. Your custom class is set to have a size class of Any/Any and the super view of the custom view has a different set of size classes, for example Compact/Regular. So you custom view is not adjusting it's size based on size classes since it is set to be the same for any combination of size classes. Try configuring a different size class combination for your custom class to to see if there are any differences - Compact/Regular for example.