Why Is flowViewTemplate at Bottom–Left and columnViewTemplate Centered in the Window?


#1

Centered looks better. Does anyone know how to center the image of flowViewTemplate, to make it match columnViewTemplate? See below:

flowViewTemplate

columnViewTemplate


#2

Mark, I won’t be much help. Both views are centered in mine (I’ll put the code below so you can compare to what you have), however…Every time I run the program it opens a very small window. Also, as you can see, I chose to leave the SpeakLine tabView as part of it.

40%20PM

I can resize the window:

06%20PM

So it looks like it used to before adding the NerdTabViewController. I must be missing some line of code to tell the window what size to open. Anybody have any ideas?

NerdTabViewController.swift:

import Cocoa

protocol ImageRepresentable {
var image: NSImage? { get }
}

class NerdTabViewController: NSViewController {

var box = NSBox()
var buttons: [NSButton] = []

//-------------------------------------------------------------------------------------------------------

func selectTabAtIndex(index: Int) {
	assert(childViewControllers.indices.contains(index), "index out of range")
	for (i, button) in buttons.enumerated() {
		button.state = (index == i) ? NSControl.StateValue.on : NSControl.StateValue.off
	}
	let viewController = childViewControllers[index]
	box.contentView = viewController.view
}

//-------------------------------------------------------------------------------------------------------

@objc func selectTab(sender: NSButton) {
	let index = sender.tag
	selectTabAtIndex(index: index)
}

//-------------------------------------------------------------------------------------------------------

override func loadView() {
	view = NSView()
	reset()
}

//-------------------------------------------------------------------------------------------------------

func reset() {
	view.subviews = []
	
	let buttonWidth: CGFloat = 40
	let buttonHeight: CGFloat = 28
	
	let viewControllers = childViewControllers
	buttons = viewControllers.enumerated().map {
		(index, viewController) -> NSButton in
		let button = NSButton()
		button.setButtonType(.toggle)
		button.translatesAutoresizingMaskIntoConstraints = false
		button.isBordered = false
		button.target = self
		button.action = #selector(selectTab)
		button.tag = index
		if let viewController = viewController as? ImageRepresentable {
			button.image = viewController.image
		}
		else {
			button.title = viewController.title!
		}
//			button.image = NSImage(named: NSImage.Name.flowViewTemplate)
		button.addConstraints([NSLayoutConstraint(item: button,
												  attribute: .width,
												  relatedBy: .equal,
												  toItem: nil,
												  attribute: .notAnAttribute,
												  multiplier: 1.0,
												  constant: buttonWidth),
			NSLayoutConstraint(item: button,
							   attribute: .height,
							   relatedBy: .equal,
							   toItem: nil,
							   attribute: .notAnAttribute,
							   multiplier: 1.0,
							   constant: buttonHeight)
			])
		return button
	}
	
	let stackView = NSStackView()
	stackView.translatesAutoresizingMaskIntoConstraints = false
	stackView.orientation = .horizontal
	stackView.spacing = 4
	for button in buttons {
		stackView.addView(button, in: .center)
	}
	
	box.translatesAutoresizingMaskIntoConstraints = false
	box.borderType = .noBorder
	box.boxType = .custom
	
	let separator = NSBox()
	separator.boxType = .separator
	separator.translatesAutoresizingMaskIntoConstraints = false
	
	view.subviews = [stackView, separator, box]
	
	let views = ["stack": stackView, "separator": separator, "box": box]
	let metrics = ["buttonHeight": buttonHeight]
	
	func addVisualFormatContraints(visualFormat: String) {
		view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: visualFormat,
														   options: [],
														   metrics: metrics as [String : NSNumber],
														   views: views))
	}
	
	addVisualFormatContraints(visualFormat: "H:|[stack]|")
	addVisualFormatContraints(visualFormat: "H:|[separator]|")
	addVisualFormatContraints(visualFormat: "H:|[box(>=100)]|")
	addVisualFormatContraints(visualFormat: "V:|[stack(buttonHeight)][separator(==1)][box(>=100)]|")
	
	if childViewControllers.count > 0 {
		selectTabAtIndex(index: 0)
	}
}

//-------------------------------------------------------------------------------------------------------

override func insertChildViewController(_ childViewController: NSViewController, at index: Int) {
	super.insertChildViewController(childViewController, at: index)
	if isViewLoaded {
		reset()
	}
}

override func removeChildViewController(at index: Int) {
	super.removeChildViewController(at: index)
	if isViewLoaded {
		reset()
	}
  }
}

Joel


#3

Joel, adjust the “box” figures in the two lines of code near the end of reset() to look like this:

addVisualFormatConstraints(visualFormat: "H:|[box(>=600)]|")
addVisualFormatConstraints(visualFormat: "V:|[stack(buttonHeight)][separator(==1)][box(>=300)]|")

That should give you better opening dimensions.

I’m still trying to get the flowViewTemplate image centered! Your code didn’t seem substantively different from mine, except that I used NSLayoutConstraint.activate(constraints) in func addVisualFormatConstraints(visualFormat:). You used view.addConstraints instead. I made the substitution because the authors stated that activate(constraints) is now the preferred method of activating constraints. I found that tip on Big Nerd Ranch’s Swift 2 Companion Guide for Cocoa Programming for OS X, toward the end of the section for Chapter 31:

https://github.com/wookiee/cocoa-programming-for-osx-5e/blob/master/Swift2.md

For those of you who may not know about this resource, it will help you update the book exercises from Swift 1 to Swift 2. The compiler suggestions (and Googling the resulting error messages) will help get you the rest of the way to Swift 4.


#4

Joel, if you resize your window large, is the flowViewTemplate image (in the first tab) still centered, or is it in the lower left (like my first picture in this topic)? Thanks again.


#5

Mark, I’ve done your suggested changes to resize the initial window and that works great, Thanks. It did not change the orientation of the image. It still centers. I also made the NSLayoutConstraint.activate(constraints) change (thanks for the heads up on that GitHub site) and still, my images are centered. Seems odd yours isn’t. Feel free to post your code if you want another set of eyes on it to see if there is some subtle difference.


#6

Joel, thanks. Here are my files:

AppDelegate.swift

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

var window: NSWindow?

func applicationDidFinishLaunching(_ aNotification: Notification) {
    let flowViewController = ImageViewController()
    flowViewController.title = "Flow"
    flowViewController.image = NSImage(named: NSImage.Name.flowViewTemplate)
    
    let columnViewController = ImageViewController()
    columnViewController.title = "Column"
    columnViewController.image = NSImage(named: NSImage.Name.columnViewTemplate)
    
    let tabViewController = NerdTabViewController()
    tabViewController.addChildViewController(flowViewController)
    tabViewController.addChildViewController(columnViewController)
    
    let window = NSWindow(contentViewController: tabViewController)
    window.makeKeyAndOrderFront(self)
    self.window = window
}

func applicationWillTerminate(_ aNotification: Notification) {
    // Insert code here to tear down your application
}

}

NerdTabViewController.swift

import Cocoa

protocol ImageRepresentable {
var image: NSImage? { get }
}

class NerdTabViewController: NSViewController {

var box = NSBox()
var buttons: [NSButton] = []

func selectTabAt(index: Int) {
    let range = 0..<childViewControllers.count
    assert(range.contains(index), "index out of range")
    for (i, button) in buttons.enumerated() {
        button.state = (index == i) ? NSControl.StateValue.on : NSControl.StateValue.off
    }
    let viewController = childViewControllers[index]
    box.contentView = viewController.view
}

@objc func selectTab(sender: NSButton) {
    let index = sender.tag
    selectTabAt(index: index)
}

override func loadView() {
    view = NSView()
    reset()
}

func reset() {
    view.subviews = []

    let buttonWidth: CGFloat = 28
    let buttonHeight: CGFloat = 28

    let viewControllers = childViewControllers
    buttons = viewControllers.enumerated().map {
        (index, viewController) -> NSButton in
        let button = NSButton()
        button.setButtonType(.toggle)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.isBordered = false
        button.target = self
        button.action = #selector(selectTab(sender:))
        button.tag = index
        if let viewController = viewController as? ImageRepresentable {
            button.image = viewController.image
        }
        else {
            button.title = viewController.title!
        }
        button.addConstraints([
            NSLayoutConstraint(item: button,
                               attribute: .width,
                               relatedBy: .equal,
                               toItem: nil,
                               attribute: .notAnAttribute,
                               multiplier: 1.0,
                               constant: buttonWidth),
            NSLayoutConstraint(item: button,
                               attribute: .height,
                               relatedBy: .equal,
                               toItem: nil,
                               attribute: .notAnAttribute,
                               multiplier: 1.0,
                               constant: buttonHeight)])
        return button
    }
    let stackView = NSStackView()
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.orientation = .horizontal
    stackView.spacing = 4
    for button in buttons {
        stackView.addView(button, in: .center)
    }
    box.translatesAutoresizingMaskIntoConstraints = false
    box.borderType = .noBorder
    box.boxType = .custom

    let separator = NSBox()
    separator.boxType = .separator
    separator.translatesAutoresizingMaskIntoConstraints = false

    view.subviews = [stackView, separator, box]

    let views = ["stack": stackView, "separator": separator, "box": box]
    let metrics = ["buttonHeight": buttonHeight]

    func addVisualFormatConstraints(visualFormat: String) {
        let constraints = NSLayoutConstraint.constraints(withVisualFormat: visualFormat,
                                                         options: [],
                                                         metrics: metrics as [String : NSNumber],
                                                         views: views)
        NSLayoutConstraint.activate(constraints)
    }
    addVisualFormatConstraints(visualFormat: "H:|[stack]|")
    addVisualFormatConstraints(visualFormat: "H:|[separator]|")
    addVisualFormatConstraints(visualFormat: "H:|[box(>=600)]|")
    addVisualFormatConstraints(visualFormat: "V:|[stack(buttonHeight)][separator(==1)][box(>=300)]|")

    if childViewControllers.count > 0 {
        selectTabAt(index: 0)
    }
}

override func insertChildViewController(_ childViewController: NSViewController, at index: Int) {
    super.insertChildViewController(childViewController, at: index)
    if isViewLoaded {
        reset()
    }
}

override func removeChildViewController(at index: Int) {
    super.removeChildViewController(at: index)
    if isViewLoaded {
        reset()
    }
}

}

ImageViewController.swift

import Cocoa

class ImageViewController: NSViewController, ImageRepresentable {

@objc var image: NSImage?

override var nibName: NSNib.Name {
    return NSNib.Name("ImageViewController")
}

override func viewDidLoad() {
    super.viewDidLoad()
    // Do view setup here.
}

}

If my problem isn’t something in one of these Swift files, I must have an Interface Builder setting wrong. Thanks again for taking a look.


#7

I’m stumped too. I was hoping a different set of eyes could find something, but as you said, this should work. I can’t imagine it’s in the ImageViewController.xib file since both images run off of the same controller and one is correct and the other is off center for some reason. Just for a test (if you haven’t tried this), in AppDelegate.swift replace NSImage.Name.flowViewTemplate with NSImage.Name.columnViewTemplate to see if it centers properly in both tabs or if the first tab will still be off center with the same image in both. Maybe it is a problem with your flowViewTemplate image.


#8

Joel, I actually tried substituting the columnViewTemplate image in both tabs like you suggested, even before I received your suggestion. But the first tab still showed the image in the lower left, not centered. So I’m still at a loss. Thanks again for all your help.


#9

I’m trying to figure out if something with the tab selection could be causing this. I have seen a couple of differences between our codes, but I doubt this is it. You have the following:

When I typed the code into Xcode, it autocompleted this way:

@objc func selectTab(sender: NSButton) {
	let index = sender.tag
	selectTabAtIndex(index: index)
}

Mine comes up with selectTabAtIndex(index:) Yours has selectTabAt(index:)

Like I said, I doubt this is the problem, but it has to have something to do with the tabs.

Here is another difference (again, I didn’t think these difference were big enough so I didn’t mention them before). You have:

button.action = #selector(selectTab(sender:))

and I have:

button.action = #selector(selectTab)

Those are the only differences I can see. I can’t imagine that would affect one image and not the other though. Sorry I can be more help.


#10

Regarding the first difference, yes, I updated my method name to comport with the more modern Swift style (since first argument labels are now required by default).

Regarding the second issue, apparently argument labels in selector expressions aren’t required (unless needed to disambiguate). So both of those selector expressions compile and work identically.


#11

Figured so. Grasping at straws here to figure out why your firstView is doing what it’s doing. Maybe something will come to us eventually! Did you try starting all the way over with a clean slate and see if it still does the same thing? Just curious.


#12

Excellent suggestion. Creating a new ImageViewController.xib file cleared up the problem. I think the .xib file was corrupted, because in addition to the problem I posted about, the images wouldn’t scale up past a certain size either. (I didn’t know that they were supposed to until now.)

Thanks for your tenacity! Problem solved.


#13

Fantastic! Glad to hear it.