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.
Related
I'm trying to load a NSWindow from an NSViewController on OS X and i'm doing the following:
private lazy var discoverable: DiscoverableWindow = {
return DiscoverableWindow.instanceFromNib()
} ()
The static method instanceFromNib() is defined as below:
class func instanceFromNib() -> DiscoverableWindow {
var instance = DiscoverableWindow()
var objects: NSArray?
NSBundle.mainBundle().loadNibNamed("DiscoverableWindow", owner: instance, topLevelObjects: &objects)
return instance
}
I'm using the window to show from my NSViewController:
NSApp.beginSheet(self.discoverable, modalForWindow: NSApplication.sharedApplication().mainWindow!, modalDelegate: nil, didEndSelector: nil, contextInfo: nil)
However, when I load it I see the following:
Is there something i'm doing incorrectly? Why is the NSWindow blank? I read the following on this:
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Extensions.html
Cocoa - loadNibNamed:owner:topLevelObjects: from loaded bundle
Cocoa: NSApp beginSheet sets the application delegate?
Why don't you make a window controller to handle the DiscoverableWindow?
Create a subclass of NSWindowController, make sure "Also create xib file for user interface" is selected. Configure your window in the DiscoverableWindowController xib, uncheck "Visible At Launch" on the properties inspector for the window.
Then, in your ViewController:
#IBAction func showSheet(sender: NSButton) {
let discoverableWC = DiscoverableWindowController(windowNibName: "DiscoverableWindowController")
view.window?.beginSheet(discoverableWC.window!, completionHandler: nil)
}
Generally, each window in your app should be managed by its own window controller, let the window controller handle the nib loading and instantiation for you.
Download the sample project here.
The nib I am loading is a custom About window for my app. When the 'About' NSMenuItem is pressed I load the nib in AppDelegate in the following manner:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var about = NSWindowController()
#IBAction func aboutClicked(sender: NSMenuItem) {
about = AboutWindow(windowNibName: "AboutWindow") as AboutWindow
NSBundle.mainBundle().loadNibNamed("AboutWindow", owner: about, topLevelObjects: nil)
}
*both the class that I want to hook up to the nib and nib itself are named 'AboutWindow'.
Once I create my NSWindowController and it's nib file, The custom class option for the nib's NSWindow does not allow me to put in the 'AboutWindow' class that I created alongside the nib.
As you can see the nib's custom class is set to NSWindow and it won't change
Any help on how to hook up this custom class and the nib is greatly appreciated.
Can you describe what the following doesn't do that you want to be done:
//
// AboutWindowController.swift
// AboutWindow
//
import Cocoa
class AboutWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}
}
That was hooked up automatically by Xcode to AboutWindowController.xib, whose name I changed to AboutWindow.xib.
Next file:
//
// AppDelegate.swift
// AboutWindow
//
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var about = NSWindowController()
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
#IBAction func aboutClicked(sender: NSMenuItem) {
about = AboutWindowController(windowNibName: "AboutWindow")
about.showWindow(self)
}
}
In IB, I dragged from the About menu item to the First Responder object to hook up the action. When I run the app and click on the About menu item, the AboutWindow displays.
I did not attempt to change any class names in IB.
Response to comment:
If I change AboutWindowController to this:
//
// AboutWindowController.swift
// AboutWindow
//
import Cocoa
class AboutWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
println("The About window has loaded")
}
}
I see the message printed in the console.
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.
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.
I have an existing OS X app, and after converting to Storyboards as the main interface, my app delegate is no longer being used. Before, the MainMenu.xib had an "App Delegate" object, and I could set its class to my app delegate. However, the Storyboard contains no such object.
How do I get my AppDelegate back and keep storyboards? I feel like I'm missing something obvious.
If you don't specify it to be a Document-Based Application, Xcode will create an AppDelegate.swift class and connect it up in the Application Scene for you.
As of right now (Xcode Beta-2), new Document-Based apps don't come with a stub AppDelegate.swift file. Instead, there's ViewController.swift and Document.swift. Worse, the Document.swift file incorrectly instantiates the same Main.storyboard for documents.
Here's one way I got it to work:
Create an AppDelegate class (e.g.: an NSObject that adopts the NSApplicationDelegate protocol)
Drag an Object object from the Object library, into the Application Scene of Main.storyboard and set it to the AppDelegate class.
Control-drag from the Application object in the Application Scene to the AppDelegate object, and connect up its delegate.
Remove everything else from the Main.storyboard and create a new Document.storyboard for the Document window. Change the Document.swift file to instantiate that Storyboard instead of Main.
If you want to have a main application window and/or a preferences window in addition to your document windows, create an Application.storyboard and/or Preferences.storyboard for those windows, and use the AppDelegate class to instantiate them. This way, the AppDelegate can customize the main window appearance and do other handy things, including receiving IBActions sent from any window in the app.
Here's a working example of an AppDelegate.swift file for a Document-Based app that also has a separate, single main Application window, and a non-modal Preference window:
// AppDelegate.swift
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
//init() {
// super.init()
// remove this if you don't use it
//}
var application: NSApplication? = nil
func applicationDidFinishLaunching(notification: NSNotification) {
application = notification.object as? NSApplication
let path = NSBundle.mainBundle().pathForResource("Defaults", ofType: "plist")
let defaults = NSDictionary(contentsOfFile:path)
NSUserDefaults.standardUserDefaults().registerDefaults(defaults)
NSUserDefaultsController.sharedUserDefaultsController().initialValues = defaults
NSUserDefaultsController.sharedUserDefaultsController().appliesImmediately = true
}
func applicationDidBecomeActive(notification: NSNotification) {
if application?.orderedDocuments?.count < 1 { showApplication(self) }
}
//func applicationWillFinishLaunching(notification: NSNotification) {
// remove this if you don't use it
//}
func applicationWillTerminate(notification: NSNotification) {
NSUserDefaults.standardUserDefaults().synchronize()
}
func applicationShouldOpenUntitledFile(app: NSApplication) -> Bool { return false }
func applicationShouldTerminateAfterLastWindowClosed(app: NSApplication) -> Bool { return false }
var applicationController: NSWindowController?
#IBAction func showApplication(sender : AnyObject) {
if !applicationController {
let storyboard = NSStoryboard(name: "Application", bundle: nil)
applicationController = storyboard.instantiateInitialController() as? NSWindowController
if let window = applicationController?.window {
window.titlebarAppearsTransparent = true
window.titleVisibility = NSWindowTitleVisibility.Hidden
window.styleMask |= NSFullSizeContentViewWindowMask
}
}
if applicationController { applicationController!.showWindow(sender) }
}
var preferencesController: NSWindowController?
#IBAction func showPreferences(sender : AnyObject) {
if !preferencesController {
let storyboard = NSStoryboard(name: "Preferences", bundle: nil)
preferencesController = storyboard.instantiateInitialController() as? NSWindowController
}
if preferencesController { preferencesController!.showWindow(sender) }
}
}
Here's another cheap and easy way to do it, if all you want to do is customize the appearance of the main window before it appears:
Make your own subclass of NSWindowController, and connect it up as the delegate of the main window.
Implement windowDidUpdate as a hook to the window so you can set up the desired options, but also remove the window delegate so the function only gets called once. This is all the code you need to make that work:
// WindowController.swift
import Cocoa
class WindowController: NSWindowController, NSWindowDelegate {
func windowDidUpdate(notification: NSNotification!) {
if let window = notification.object as? NSWindow! {
window.titlebarAppearsTransparent = true
window.titleVisibility = NSWindowTitleVisibility.Hidden
window.styleMask |= NSFullSizeContentViewWindowMask
window.delegate = nil }
}
}
Actually, an even easier way to apply those appearance options to the window, is by using Interface Builder to add them as User Defined Runtime Attributes to the NSWindow object. You don't need to subclass NSWindowController or write any code at all. Just plug in these values to the window object via the Identity Inspector pane:
Keypath: titlebarAppearsTransparent, Type: Boolean, Value: Checked
Keypath: titleVisibility, Type: Number, Value: 1
Keypath: styleMask, Type: Number, Value: 32783
Of course, you can't specify individual bits of the styleMask, but it's easy enough to add them all together and get a single number to specify the style.
With Storyboard architecture, and the new powers given to NSViewController, there's not as much need to subclass NSWindowController anymore.