Dynamically adding cells to a NSMatrix laid out with Auto Layout has weird effects; why? - macos

I want to create a group of radio buttons using the NSMatrix method that Interface Builder uses, but in code. The matrix is laid out using Auto Layout. I have it mostly working, except for when I add new options at runtime.
In the following example, clicking Append Item a few times will work fine, then the matrix starts going out of the window near the top (at least I think it's clipped at the top). If you maximize this window after adding a bunch of items, the window will stay the same height and all the items will be clipped to about a pixel high each, which is a very undesirable thing :)
In my real program (not this test below), it works mostly fine, but if I add an option dynamically, after certain numbers of items (initially 5), the options will clip very slightly, appearing slightly squeezed or squished. Adding another option reverts this until the next magic number is hit.
What's going on? I'm testing this on OS X Yosemite. Thanks.
// 17 august 2015
import Cocoa
var keepAliveMainwin: NSWindow? = nil
var matrix: NSMatrix? = nil
class ButtonHandler : NSObject {
#IBAction func onClicked(sender: AnyObject) {
var lastRow = matrix!.numberOfRows
matrix!.renewRows(lastRow + 1, columns: 1)
var cell = matrix!.cellAtRow(lastRow, column: 0) as! NSButtonCell
cell.title = "New Item"
matrix!.sizeToCells()
}
}
var buttonHandler: ButtonHandler = ButtonHandler()
func appLaunched() {
var mainwin = NSWindow(
contentRect: NSMakeRect(0, 0, 320, 240),
styleMask: (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask),
backing: NSBackingStoreType.Buffered,
defer: true)
var contentView = mainwin.contentView as! NSView
var prototype = NSButtonCell()
prototype.setButtonType(NSButtonType.RadioButton)
prototype.font = NSFont.systemFontOfSize(NSFont.systemFontSizeForControlSize(NSControlSize.RegularControlSize))
matrix = NSMatrix(frame: NSZeroRect,
mode: NSMatrixMode.RadioModeMatrix,
prototype: prototype,
numberOfRows: 0,
numberOfColumns: 0)
matrix!.allowsEmptySelection = false
matrix!.selectionByRect = true
matrix!.intercellSpacing = NSMakeSize(4, 2)
matrix!.autorecalculatesCellSize = true
matrix!.drawsBackground = false
matrix!.drawsCellBackground = false
matrix!.autosizesCells = true
matrix!.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(matrix!)
var button = NSButton(frame: NSZeroRect)
button.title = "Append Item"
button.setButtonType(NSButtonType.MomentaryPushInButton)
button.bordered = true
button.bezelStyle = NSBezelStyle.RoundedBezelStyle
button.font = NSFont.systemFontOfSize(NSFont.systemFontSizeForControlSize(NSControlSize.RegularControlSize))
button.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(button)
button.target = buttonHandler
button.action = "onClicked:"
var views: [String: NSView]
views = [
"button": button,
"matrix": matrix!,
]
addConstraints(contentView, "V:|-[matrix]-[button]-|", views)
addConstraints(contentView, "H:|-[matrix]-|", views)
addConstraints(contentView, "H:|-[button]-|", views)
mainwin.cascadeTopLeftFromPoint(NSMakePoint(20, 20))
mainwin.makeKeyAndOrderFront(mainwin)
keepAliveMainwin = mainwin
}
func addConstraints(view: NSView, constraint: String, views: [String: NSView]) {
var constraints = NSLayoutConstraint.constraintsWithVisualFormat(
constraint,
options: NSLayoutFormatOptions(0),
metrics: nil,
views: views)
view.addConstraints(constraints)
}
class appDelegate : NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(note: NSNotification) {
appLaunched()
}
func applicationShouldTerminateAfterLastWindowClosed(app: NSApplication) -> Bool {
return true
}
}
func main() {
var app = NSApplication.sharedApplication()
app.setActivationPolicy(NSApplicationActivationPolicy.Regular)
// NSApplication.delegate is weak; if we don't use the temporary variable, the delegate will die before it's used
var delegate = appDelegate()
app.delegate = delegate
app.run()
}
main()

Apparently, you need to omit the call to sizeToCells() after calling renewRows(_:columns:). My guess is that it sets the frame size, which is mostly useless when using auto layout, but also clears a "dirty" flag somewhere that tells the matrix that it needs to invalidate its intrinsic size. In other words, the matrix thinks it already did the re-layout stuff it needed to do.

Related

How can we make a spacer view with automatic width between two views inside UIStackview?

If we have 2 labels inside UIStackView, and I want to set a flexible width between them like this image:
what is the best way? in SwiftUI there is a function named: Spacer() , is there something similar in UIKit?
You can add a UIView to the stack to push each view in the UIStackView:
hStack:
hStack.axis = .horizontal
hStack.addArrangedSubview(label1)
hStack.addArrangedSubview(UIView.spacer(for: .horizontal))
hStack.addArrangedSubview(label2)
constraints:
hStack.translatesAutoresizingMaskIntoConstraints = false
hStack.topAnchor.constraint(equalTo: "depending on your view").isActive = true
hStack.bottomAnchor.constraint(equalTo: "depending on your view").isActive = true
hStack.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
and the extension to make this work:
// MARK: - Spacer
extension UIView {
static func spacer(size: CGFloat = .greatestFiniteMagnitude, for layout: NSLayoutConstraint.Axis = .horizontal) -> UIView {
let spacer = UIView()
if layout == .horizontal {
let constraint = spacer.widthAnchor.constraint(equalToConstant: size)
constraint.priority = .defaultLow
constraint.isActive = true
} else {
let constraint = spacer.heightAnchor.constraint(equalToConstant: size)
constraint.priority = .defaultLow
constraint.isActive = true
}
return spacer
}
}
Side note:
You may get an error that
This NSLayoutConstraint is being configured with a constant that
exceeds internal limits
... so I would rather recommend creating a UIView as a container and then pin each label to the sides of the container (leading, trailing) where each label also has a widthAnchor (if you don't want the text to extend).

How do I add a toolbar to a macOS app using SwiftUI?

I am trying to add a toolbar inside the title bar to a macOS app using SwiftUI, something similar to what is shown below.
I am unable to figure out a way to achieve this using SwiftUI. Currently, I have my toolbar (which just has a text field) inside my view, but I want to move it into the title bar.
My current code:
struct TestView: View {
var body: some View {
VStack {
TextField("Placeholder", text: .constant("")).padding()
Spacer()
}
}
}
So, in my case, I need to have the textfield inside the toolbar.
As of macOS 11 you’ll likely want to use the new API as documented in WWDC Session 10104 as the new standard. Explicit code examples were provided in WWDC Session 10041 at the 12min mark.
NSWindowToolbarStyle.unified
or
NSWindowToolbarStyle.unifiedCompact
And in SwiftUI you can use the new .toolbar { } builder.
struct ContentView: View {
var body: some View {
List {
Text("Book List")
}
.toolbar {
Button(action: recordProgress) {
Label("Record Progress", systemImage: "book.circle")
}
}
}
private func recordProgress() {}
}
Approach 1:
This is done by adding a titlebar accessory. I was able to get this done by modifying the AppDelegate.swift file. I had to apply some weird padding to make it look right.
AppDelegate.swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the titlebar accessory
let titlebarAccessoryView = TitlebarAccessory().padding([.top, .leading, .trailing], 16.0).padding(.bottom,-8.0).edgesIgnoringSafeArea(.top)
let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize
let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
// Add the titlebar accessory
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
TitlebarAccessory.swift
import SwiftUI
struct TitlebarAccessory: View {
var body: some View {
TextField("Placeholder", text: .constant(""))
}
}
Result:
Approach 2 (Alternative method):
The idea here is to do the toolbar part using storyboard and the rest of the app using SwiftUI. This is done by creating a new app with storyboard as the user interface. Then go to the storyboard and delete the default View Controller and add a new NSHostingController. Connect the newly added Hosting Controller to the main window by setting its relationship. Add your toolbar to the window using interface builder.
Attach a custom class to your NSHostingController and load your SwiftUI view into it.
Example code below:
import Cocoa
import SwiftUI
class HostingController: NSHostingController<SwiftUIView> {
#objc required dynamic init?(coder: NSCoder) {
super.init(coder: coder, rootView: SwiftUIView())
}
}
Using this approach also gives you the ability to customize the toolbar.
Inspired by your first approach I managed to get a toolbar too. As I'm using Divider()s in it, your Paddings didn't work great for me.
This one seems to work a bit smoother with different Layout-Sizes:
let titlebarAccessoryView = TitlebarAccessory().padding([.leading, .trailing], 10).edgesIgnoringSafeArea(.top)
let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
accessoryHostingView.frame.size.height = accessoryHostingView.fittingSize.height+16
accessoryHostingView.frame.size.width = accessoryHostingView.fittingSize.width
Maybe there is an even smoother way to get rid of this +16 and the padding trailing and leading (there are several other options instead of fittingSize), but I couldn't find any that looks great without adding numerical values.
I've finally managed to do this without any fiddly padding and in a way which looks great in full screen as well. Also, The previous solutions do not allow horizontal resizing.
Wrap your title view in an HStack() and add in an invisible text view which is allowed to expand to infinity height. This seems to be what keeps everything centered. Ignore the safe area at the top to now center it in the full height of the titlebar.
struct TitleView : View {
var body: some View {
HStack {
Text("").font(.system(size: 0, weight: .light, design: .default)).frame(maxHeight: .infinity)
Text("This is my Title")
}.edgesIgnoringSafeArea(.top)
}
}
In your app delegate, when you add in the NSTitlebarAccessoryViewController() set the layoutAttribute to top. This will allow it to resize horizontally as your window size changes (leading and left fix the width to minimums and has caused me a lot of pain looking for the answer to this.
let titlebarAccessoryView = TitleView()
let accessoryHostingView = NSHostingView(rootView: titlebarAccessoryView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize
let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
titlebarAccessory.layoutAttribute = .top
In my case I also want some buttons on the very right which position independently of the rest of the title, so I chose to add them separately, making use of the ability to add multiple view controllers
let titlebarAccessoryRight = NSTitlebarAccessoryViewController()
titlebarAccessoryRight.view = accessoryHostingRightView
titlebarAccessoryRight.layoutAttribute = .trailing
window.toolbar = NSToolbar()
window.toolbar?.displayMode = .iconOnly
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.addTitlebarAccessoryViewController(titlebarAccessoryRight)
https://developer.apple.com/documentation/uikit/uititlebar
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
if let titlebar = windowScene.titlebar {
//toolbar
let identifier = NSToolbar.Identifier(toolbarIdentifier)
let toolbar = NSToolbar(identifier: identifier)
toolbar.allowsUserCustomization = true
toolbar.centeredItemIdentifier = NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier)
titlebar.toolbar = toolbar
titlebar.toolbar?.delegate = self
titlebar.titleVisibility = .hidden
titlebar.autoHidesToolbarInFullScreen = true
}
window.makeKeyAndVisible()
}
#if targetEnvironment(macCatalyst)
let toolbarIdentifier = "com.example.apple-samplecode.toolbar"
let centerToolbarIdentifier = "com.example.apple-samplecode.centerToolbar"
let addToolbarIdentifier = "com.example.apple-samplecode.add"
extension SceneDelegate: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == NSToolbarItem.Identifier(rawValue: toolbarIdentifier) {
let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: toolbarIdentifier), titles: ["Solver", "Resistance", "Settings"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
if itemIdentifier == NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier) {
let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), titles: ["Solver1", "Resistance1", "Settings1"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
if itemIdentifier == NSToolbarItem.Identifier(rawValue: addToolbarIdentifier) {
let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(self.add(sender:)))
let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem)
return button
}
return nil
}
#objc func toolbarGroupSelectionChanged(sender: NSToolbarItemGroup) {
print("selection changed to index: \(sender.selectedIndex)")
}
#objc func add(sender: UIBarButtonItem) {
print("add clicked")
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[NSToolbarItem.Identifier(rawValue: toolbarIdentifier), NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), NSToolbarItem.Identifier.flexibleSpace,
NSToolbarItem.Identifier(rawValue: addToolbarIdentifier),
NSToolbarItem.Identifier(rawValue: addToolbarIdentifier)]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
self.toolbarDefaultItemIdentifiers(toolbar)
}
}
#endif

UIView height constraint animation, which has anchor connection with other views

Xcode swift 4
I have some views that are added dynamically in code one below another.
Each new view top anchor is connected to previous view bottom anchor.
And each view have a button that make view to expand/collapse with animation. Here is button code :
let fullHeight : CGFloat = 240
let smallHeight : CGFloat = 44
let currentHeigth = rootView.frame.size.height //I use this to get current height and understand expanded view or not
let heighCons = rootView.constraints.filter //I use this to deactivate current height Anchor constraint
{
$0.firstAttribute == NSLayoutAttribute.height
}
NSLayoutConstraint.deactivate(heighCons)
rootView.layoutIfNeeded()
if currentHeigth == smallHeight
{
rootView.heightAnchor.constraint(equalToConstant: fullHeight).isActive = true
rootView.setNeedsLayout()
UIView.animate(withDuration: 0.5)
{
rootView.layoutIfNeeded() //animation itself
}
}
else
{
rootView.heightAnchor.constraint(equalToConstant: smallHeight).isActive = true
rootView.setNeedsLayout()
UIView.animate(withDuration: 0.5)
{
rootView.layoutIfNeeded() //animation itself
}
}
This all works perfectly but i have a problem : view that below current expanding view changes it y position immediately with no animation. Its just jumping to previous view bottom anchor, that would be active after animation finish.
So my question is :
1) what is the right way to make height constraint animation, when views are connected to each other by bottom/top animation?
2) my goal is just to make a view that would expand/collapse on button click, maybe i should do it another way?
Here's an approach using Visiblity Gone Extension
extension UIView {
func visiblity(gone: Bool, dimension: CGFloat = 0.0, attribute: NSLayoutAttribute = .height) -> Void {
if let constraint = (self.constraints.filter{$0.firstAttribute == attribute}.first) {
constraint.constant = gone ? 0.0 : dimension
self.layoutIfNeeded()
self.isHidden = gone
}
}
}
Usage
expanview.visiblity(gone: true,dimension: 0)
Example
#IBOutlet weak var msgLabel: UILabel!
#IBOutlet weak var expanview: UIView!
#IBAction func toggleCollapisbleView(_ sender: UIButton) {
if sender.isSelected{
sender.isSelected = false
expanview.visiblity(gone: false,dimension: 128)
sender.setTitle("Collapse",for: .normal)
}
else{
sender.isSelected = true
expanview.visiblity(gone: true,dimension: 0)
sender.setTitle("Expand",for: .normal)
msgLabel.text = "Visiblity gone"
}
}

NSDocument printOperationWithSettings not showing all pages

In NSDocument subclass, have this function:
override func printOperationWithSettings(printSettings: [String : AnyObject]) throws -> NSPrintOperation {
let printInfo: NSPrintInfo = self.printInfo
var pageSize = printInfo.paperSize
pageSize.width -= printInfo.leftMargin + printInfo.rightMargin
pageSize.height -= printInfo.topMargin + printInfo.bottomMargin
pageSize.width = pageSize.width * 2
pageSize.height = pageSize.height * 2
let myPage = MyPage(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: pageSize))
let printOperation = NSPrintOperation(view: myPage, printInfo: printInfo)
return printOperation
}
MyPage is, for this test, an NSView subclass that just draws an oval.
class MyPage: NSView {
override var flipped: Bool {
return true
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.greenColor().set() // choose color
let figure = NSBezierPath() // container for line(s)
figure.appendBezierPathWithOvalInRect(self.frame)
figure.stroke() // draw line(s)
}
}
I'd expect this to show four pages in the print panel, but it only shows two, equating to the top left and bottom left of the oval. No matter how wide I make myPage's frame, only the leftmost pages are shown. Any ideas why? Thank you!

Resizing the window according to a variable swift

I have a NSViewController and a variable num. I want to change the size of the window dynamically according to that variable. Is there any way to do that in swift?
Let's say your window has an IBOutlet named "window", and your dynamic number is named "myDynamicNumber":
func resize() {
var windowFrame = window.frame
let oldWidth = windowFrame.size.width
let oldHeight = windowFrame.size.height
let toAdd = CGFloat(myDynamicNumber)
let newWidth = oldWidth + toAdd
let newHeight = oldHeight + toAdd
windowFrame.size = NSMakeSize(newWidth, newHeight)
window.setFrame(windowFrame, display: true)
}
In Swift 3 to resize the window you use setFrame.
An example from the ViewController:
func resizeWin(size:(CGFloat,CGFloat)){
self.view.window?.setFrame(NSRect(x:0,y:0,width:size.0,height:size.1), display: true)
}
I needed to toggle viewing a text view so I overlaid the window an invisible view - hideRect just short of the text view; in this way I can resize to the smaller (hideRect) and restore later to the original size - origRect. Hide and original rect captured at viewDidLoad(). Swift 3/Xcode 8.3.3
// class global contants
let kTitleUtility = 16
let kTitleNormal = 22
#IBOutlet var hideView: NSView!
var hideRect: NSRect?
var origRect: NSRect?
#IBAction func toggleContent(_ sender: Any) {
// Toggle content visibility
if let window = self.view.window {
let oldSize = window.contentView?.bounds.size
var frame = window.frame
if toggleButton.state == NSOffState {
frame.origin.y += ((oldSize?.height)! - (hideRect?.size.height)!)
window.setFrameOrigin(frame.origin)
window.setContentSize((hideRect?.size)!)
window.showsResizeIndicator = false
window.minSize = NSMakeSize((hideRect?.size.width)!,(hideRect?.size.height)!+CGFloat(kTitleNormal))
creditScroll.isHidden = true
}
else
{
let hugeSize = NSMakeSize(CGFloat(Float.greatestFiniteMagnitude), CGFloat(Float.greatestFiniteMagnitude))
frame.origin.y += ((oldSize?.height)! - (origRect?.size.height)!)
window.setFrameOrigin(frame.origin)
window.setContentSize((origRect?.size)!)
window.showsResizeIndicator = true
window.minSize = NSMakeSize((origRect?.size.width)!,(origRect?.size.height)!+CGFloat(kTitleNormal))
window.maxSize = hugeSize
creditScroll.isHidden = false
}
}
}
This also preserved the widow's visual origin, and sizing minimum.

Resources