Add multiple subviews to NSWindow in Swift - macos

I have a window with an outlet and a custom view (in my .xib file) which contains a button and a text field. When a button is pressed in the window I want to add an instance of the custom view into the window.
Currently I have an outlet to the window and the custom view (configWindow and customView) and this action is called when the button is pressed:
#IBAction func addView(sender: NSButton) {
configWindow.contentView.addSubview(customView)
// Print the views in the window, see what's been added
for i in configWindow.contentView.subviews {
println(i)
}
}
This will only ever add one view to the window.
Is this the right way to go about it, or should I be using a completely different approach?

You can't add the same view twice. It sounds like you are trying to add the same instance of customView to configWindow multiple times, which you can't do. If you think about it, it's fairly obvious why -- how will the superview manage two subviews which are the same? How will it know the difference between the two of them?
You should be adding different instances of the CustomView class instead:
#IBAction func addView(sender: NSButton) {
let customView = CustomView(frame: <some frame>)
configWindow.contentView.addSubview(customView)
// Print the views in the window, see what's been added
for i in configWindow.contentView.subviews {
println(i)
}
}
Edited to add
I've created an example project that you can download at https://bitbucket.org/abizern/so-27874883/get/master.zip
This basically initialises multiple views out of a nib file and adds them randomly to a view.
The Interesting part is:
class CustomView: NSView {
#IBOutlet weak var label: NSTextField!
class func newCustomView() -> CustomView {
let nibName = "CustomView"
// Instantiate an object of this class from the nib file and return it.
// Variables are explicitly unwrapped, since a failure here is a compile time error.
var topLevelObjects: NSArray?
let nib = NSNib(nibNamed: nibName, bundle: NSBundle.mainBundle())!
nib.instantiateWithOwner(nil, topLevelObjects: &topLevelObjects)
var view: CustomView!
for object: AnyObject in topLevelObjects! {
if let obj = object as? CustomView {
view = obj
break
}
}
return view
}
}
Where I create a factory method of the custom class that loads itself from the nib, and then returns the first top level object of the correct class.

Related

NSTableCellView inside a custom view refuses first responder

Instead of a TableView I want to use a NSTableCellView inside a custom view. So I created a xib file with a standard NSTableCellView and loaded that into a view.
The table cell view is displayed as expected. But I can't make its textfield the first responder. It's not responding to mouse events and it's not even reacting when I explicitly make it the first responder. makeFirstResponder returns true, but I see no blinking cursor and it doesn't respond to any key events.
Adding a regular textfield to the view does work however.
class ViewController: NSViewController
{
#IBOutlet var myView: NSView!
#IBOutlet var cellView: NSTableCellView! //ViewController is the file owner of the xib file
override func viewDidLoad()
{
super.viewDidLoad()
NSBundle(forClass: ViewController.self).loadNibNamed("Cell", owner: self, topLevelObjects: nil)
myview.addSubview(cellView)
cellView.layer?.borderColor = Color.blackColor().CGColor
cellView.layer?.borderWidth = 1
}
override func mouseDown(event: NSEvent)
{
let result = self.view.window?.makeFirstResponder(cellView.textField!)
print(result) //prints true
}
Why am I doing this? Well, I am trying to understand how a view-based table view would be implemented. It seems that at its core a tableview is just a view with lots of (probably cached and reused?) subviews. Also, I always thought that the number of views was limited for efficiency purposes on OS X?

self.window is always nil

I'm currently trying to display a window with a window controller.
That is what I have:
NSWindow subclass
import Cocoa
import CoreLocation
class TweetWindow: NSWindow {
var locationManager: CLLocationManager!
var geoCoder: CLGeocoder!
#IBAction func tweetButtonPressed(sender:NSButton) {
}
func initialize() {
self.titleVisibility = NSWindowTitleVisibility.Hidden;
self.locationManager = CLLocationManager();
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
self.locationManager.distanceFilter = 10;
self.geoCoder = CLGeocoder();
}
func windowWillShow() {
if !self.visible {
let systemAppearanceName = (NSUserDefaults.standardUserDefaults().stringForKey("AppleInterfaceStyle") ?? "Light").lowercaseString;
let systemAppearance = systemAppearanceName == "dark" ? NSAppearance(named: NSAppearanceNameVibrantDark) : NSAppearance(named: NSAppearanceNameVibrantLight);
self.appearance = systemAppearance;
self.locationManager.startUpdatingLocation();
}
}
func windowWillClose() {
self.locationManager.stopUpdatingLocation();
}
}
NSWindowController subclass:
import Cocoa
class TweetWindowController: NSWindowController {
var tweetWindow: TweetWindow { return self.window as! TweetWindow; }
override func windowDidLoad() {
super.windowDidLoad()
self.tweetWindow.initialize()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}
override func showWindow(sender: AnyObject?) {
self.tweetWindow.windowWillShow()
super.showWindow(sender)
}
}
Of course, I've got a .xib-file, too, that contains my window. It is called "TweetWindow.xib".
Now, so far this should be ok.
In my AppDelegate.swift I do the following:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var tweetWindowController:TweetWindowController!;
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
tweetWindowController = TweetWindowController(windowNibName: "TweetWindow");
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
func showTweetWindowInternal() {
NSApp.activateIgnoringOtherApps(true);
tweetWindowController.showWindow(nil);
}
#IBAction func showTweetWindow(sender: AnyObject) {
showTweetWindowInternal();
}
#IBAction func quitApp(sender: AnyObject) {
NSApplication.sharedApplication().terminate(self);
}
}
My problem is the following:
When I try to click on my button that is associated with the IBAction down there to show the window, an exception is thrown here:
var tweetWindow: TweetWindow { return self.window as! TweetWindow; }
It says fatal error: unexpectedly found nil while unwrapping an Optional value, so window is nil.
Why is window nil there? Am I trying to access the value too early or something?
Here are some photos:
Thanks.
Initializing an instance of NSWindowController or a subclass does not load the NIB. The NIB is not loaded until the window property is accessed or the showWindow() method is called (which basically accesses the window property indirectly). In your case, since you're overriding showWindow(), it's important to know that the NIB is not loaded until the superclass implementation is called.
So, yes, your call to self.tweetWindow.windowWillShow() in your showWindow() override, before calling through to super, is too early. The NIB has not been loaded at that point, so the window outlet has not been connected to anything.
Of course, you have to make sure the outlet is actually connected in the NIB or it will never be connected, even after the NIB is loaded. But trying to access it before it's loaded is the first problem.
I think your windowWillShow() method is misguided, at least as implemented. The window can be shown in various ways, not just by the window controller's showWindow() method. For example, something outside of both the window and the window controller could do tweetWindowController.window.makeKeyAndOrderFront(nil). If you really want to do something like this, have the window class override the various order...() methods to see if the window is being ordered in for the first time and, if so, call your method.
Update:
You have several things misconfigured in your NIB. Here's what you need to do to fix them:
Break the current connection from the "delegate" outlet of File's Owner to the window. Click the "x" button seen in either of the screenshots you posted.
Change the class of File's Owner. It is currently TweetWindow. It should be TweetWindowController. The controller is what loads and owns the NIB, so the class of File's Owner should be the controller class.
Connect the "window" outlet of File's Owner to the window.
Connect the "delegate" outlet of the window to File's Owner.

Pass continuous colorwell data to second view controller

I have the following situation:
two ViewControllers each containing a box that is to be colored to a color picked from a color well in ViewController
The colorwell is set as continuous in order to see the changes reflected immediately
I am looking for a way to continuously pass the color well value on to the SecondViewController and on to a callback method that will color a box in the SecondViewController.
I found that the prepareForSegue method is commonly used to pass data between view controllers, this however only occurs once during the transition and not continuously.
Can someone point me out in the right direction? Googled for hours but I got really stuck with this.
Thanks.
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var box: NSBox!
#IBOutlet weak var well: NSColorWell!
#IBAction func well(sender: AnyObject) {
box.fillColor = well.color
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
let second = segue.destinationController as! SecondViewController
second.representedObject = well.color
}
}
import Cocoa
class SecondViewController: NSViewController {
#IBOutlet weak var box: NSBox!
override func viewWillAppear() {
// Note that box.fillColor requires box type to be custom
box.fillColor = self.representedObject as! NSColor
}
}
The prepareForSegue method is a chance to create links between two view controllers. It's pretty common for the source view controller to set itself up as the delegate of the destination view controller. It's also possible for the source view controller to save a reference to the destination view controller for future reference.
If you define a protocol with a method like
func colorValueHasChanged(newColor: NSColor)
Then you can use it in the IBAction for your color well to pass information about changes in the color well from one view controller to the other.

how to presentViewControllerAsSheet on OSX Mavericks?

It's a long story, but to cut it short; my first OSX app was written (on Yosemite) in Swift using a storyboard until I found out my (finished) app will not run on Mavericks. I need to run on Mavericks, so I have replaced the storyboard with NIBs.
My problem is with the segues; I was using 'sheet type' segues to show other view controllers in a sheet over the main view controller. A call to the presentViewControllerAsSheet method of NSViewController is a good replacement as it looks the same, but this API was introduced in Yosemite - so I need to work out how to do this for Mavericks.
In the action for a button on the main view, I've tried using beginSheet like this:
secondViewController = SecondViewController(nibName: "SecondViewController", bundle: nil)
self.view.window?.beginSheet(secondViewController!view.window!, completionHandler: nil)
But the second view controller's window is null at runtime. I've tried adding the new view controller as a subview to the application window but this is an unrecognised selector:
NSApplication.sharedApplication().windows[0].addSubView(secondViewController!.view)
I've search high and low for a description of how to show a sheet and all I can find is: Can a view controller own a sheet? but I'm sorry to admit I don't understand the answer. Can anybody help me with some working code? I'm beginning to worry that I'm trying to do something unusual but it looks OK on Yosemite, so how did people do this before Yosemite was released?
EDIT
I still haven't got to the solution, so I have put together a small app which shows the problems I'm having.
In AppDelegate.swift:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mainViewController: FirstView!
func applicationDidFinishLaunching(aNotification: NSNotification) {
mainViewController = FirstView(nibName:"FirstView", bundle: nil)
window.contentView = mainViewController.view
mainViewController.view.frame = (window.contentView as! NSView).bounds
}
}
In FirstView.swift (associated NIB has a 'open sheet' button)
class FirstView: NSViewController {
var secondView: SecondView?
var secondWindow: SecondWinCon?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func pressButton(sender: AnyObject) {
secondView = SecondView(nibName: "SecondView", bundle: nil)!
// method 1 - this is the behaviour I want (but it only works on OSX 10.10)
// presentViewControllerAsSheet(secondView!)
// method 2 - this just creates a floating window
// self.view.addSubview(secondView!.view)
// self.view.window?.beginSheet(secondView!.view.window!, completionHandler: nil)
// method 3 - this also creates a floating window
secondWindow = SecondWinCon(windowNibName: "SecondWinCon")
self.view.window?.beginSheet(secondWindow!.window!, completionHandler: nil)
}
}
In SecondView.swift (associated NIB has a 'close' button)
class SecondView: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismissPressed(sender: AnyObject) {
if (presentingViewController != nil) {
presentingViewController?.dismissViewController(self)
} else {
self.view.window?.sheetParent?.endSheet(self.view.window!)
}
}
}
In SecondWinCon.swift (Associated NIB is empty)
class SecondWinCon: NSWindowController {
var secondView: SecondView?
override func windowDidLoad() {
super.windowDidLoad()
secondView = SecondView(nibName: "SecondView", bundle: nil)!
self.window?.contentView.addSubview(secondView!.view)
}
}
If method 1 is uncommented, you will see the behaviour I'm trying to emulate (remember it only works on OS X 10.10). Method 2 or 3 displays the second view, but not as a sheet.
I have the same problem, and found maybe is't an issue related to view life cycle.
When I call presentViewControllerAsSheet in viewDidLoad, sheet will not shown, and you will get this in console:
Failed to set (contentViewController) user defined inspected property on (NSWindow): presentViewController:animator:: View '''s view is not in a window/view hierarchy.
If you trigger this in viewWillAppear or viewDidAppear, it's totally no problem.
UPDATE
Okay, let's make it clear.
For this initial storyboard, NSWindowController is connected with a view controller, think this as a root view controller (RootVC).
Create another view controller desired as a sheet in storyboard (SheetVC).
in viewWillAppear or viewDidAppear of RootVC, [self presentViewControllerAsSheet: SheetVC]
The sheet will show, no additional code required.
If you get here looking for a solution, I was nearly there with method 3. The important step I had missed was to turn off "Visible At Launch" in the NSWindowController's NIB (it's an attribute of the NSWindow). In my sample code, this was in SecondWinCon.nib.

Connect to ViewController from AppDelegate (Swift)

I have created a new OS X Cocoa Application using the standard Xcode Swift template (using StoryBoards).
I have implemented an IBAction in AppDelegate.swift to handle when the users selects "Open..." from the "File" menu. If the chosen file is a valid image file, I create an NSImage which I then want to display in the view of ViewController.
#IBAction func openFile(sender: NSMenuItem) {
var openPanel = NSOpenPanel()
openPanel.beginWithCompletionHandler { (result :Int) -> Void in
if result == NSFileHandlingPanelOKButton {
if let imageURL = openPanel.URL {
let image = NSImage(contentsOfURL: imageURL)
// PRESENT image IN THE VIEW CONTROLLER
}
}
}
However, I don't see any way to connect to ViewController from AppDelegate. I have only managed to find suggestions that I should look at self.window! in AppDelegate, but there is no such thing as a window in AppDelegate.
Thanks,
Michael Knudsen
It seems that AppDelegate can connect to objects only within Application Scene in a storyboard. If you want to get a ViewController, instantiate it from a storyboard.
sample:
#IBAction func menuAction(sender: AnyObject) {
if let storyboard = NSStoryboard(name: "Main", bundle: nil) {
let controller = storyboard.instantiateControllerWithIdentifier("VC1") as NSViewController
if let window = NSApplication.sharedApplication().mainWindow {
window.contentViewController = controller // just swap
}
}
}
You can access the mainWinow property and the contentViewController property to create a reference to your custom ViewController class. This is similar to the iOS rootViewController property.
let rootViewController = NSApplication.shared().mainWindow?.windowController?.contentViewController as! ViewController
Now you can use this reference to access IBOutlets on your main storyboard from your AppDelegate.
rootViewController.myTextView.textStorage?.mutableString.setString("Cats and dogs.")
This is good for a simple app with one Window with one ViewController.
I was stuck trying to do this same thing recently and managed to get the event I needed to update my view by creating the #IBAction in my ViewController and control dragging to my Application's First Responder (above the menu in my storyboard view).
Here's the question that got me out of the woods:
Application Menu Items Xcode
And thanks to Bluedome for the suggestion to connect it to First Responder's action.
If you control-drag from the menu to the first responder (red cube above menu) and picked an existing action, then you can "responder chain" to your view controller. In my case I attached Open to openFile and then in my view controller I added the following
override var acceptsFirstResponder: Bool {
return true
}
func openFile(sender: NSMenuItem) {
print("In view controller")
}
and it worked without any changes in AppDelegate. Most of the menus are already hooked up to first responder so just add the matching function name in your view controller.
See this comment and this document on Event Handling Basics for more info.
In Swift 5 and accessing new windows array:
#IBAction func menuAction(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateInitialViewController()
// The windows in the array are ordered from back to front by window level;
// thus, the last window in the array is on top of all other app windows.
// On app launch, UIApplication.shared.windows.count == 1 anyway.
if let window = UIApplication.shared.windows.last {
window.rootViewController = controller
}
}

Resources