OS X - How can a NSViewController find its window? - macos

I have a Document based core data app. The main document window has a number of views, each controlled by its own custom NSViewController which are switched in as necessary. I want each of these view controllers to be able to drop down a custom modal sheet from the document window. However because the views are separate and not in the MyDocument nib I cannot link the view to the document window in IB. This means that when I call
[NSApp beginSheet: sheetWindow modalForWindow: mainWindow modalDelegate: self didEndSelector: #selector(didEndSheet:returnCode:contextInfo:) contextInfo: nil];
I’m supplying nil for mainWindow and the sheet therefore appears detached.
Any suggestions?
Many Thanks

You can use [[self view] window]

Indeed, it's self.view.window (Swift).
This may be nil in viewDidLoad() and viewWillAppear(), but is set properly by the time you get to viewDidAppear().

One issue with the other answers (i.e., just looking at self.view.window) is that they don't take into account the case that when a view is hidden, its window property will be nil. A view might be hidden for a lot of reasons (for example, it might be in one of the unselected views in a tab view).
The following (swift) extension will provide the windowController for a NSViewController by ascending the view controller hierarchy, from which the window property may then be examined:
public extension NSViewController {
/// Returns the window controller associated with this view controller
var windowController: NSWindowController? {
return ((self.isViewLoaded == false ? nil : self.view)?.window?.windowController)
?? self.parent?.windowController // fallback to the parent; hidden views like those in NSTabView don't have a window
}
}

If your controller can get access to the NSDocument subclass, you can use -windowForSheet

more about Tim Closs answer :
-(void)viewDidAppear
{
self.view.window.title = #"title-viewDidAppear"; //this only works when and after viewDidAppeer is called
}
-(void)viewWillDisappear
{
self.view.window.title = #"title-viewWillDisappear"; //this only works before and when viewWillDisappear is called
}

Related

Use NSToolBar Outlet xcode 6 and Storyboard?

I am trying to add an outlet into my viewcontroller for a toolbar item in my window controller. I have tried playing around with first responder and bindings but have not been able to find any solutions.
A similar question that was answered provided some insight but no one has mentioned anything about IBOutlets other than still asking how to add them in the comments. The answer has been accepted so i am assuming no one will add to it.
How to use NSToolBar in Xcode 6 and Storyboard?
Incase my question is unclear at all, i would like to be able to add this to my storyboard program
#IBOutlet weak var Mytoolbar: NSToolbarItem!
func enabletoolbar()
{
Mytoolbar.action = "FunctionIn.ViewController.swift"
Mytoolbar.enabled = true
}
I found a decent workaround by adding IBOutlets to my custom NSWindow class and using the storyboard to connect my views to the IBOutlets. Then, I accessed these views from my NSViewController class by getting them from the custom NSWindow.
Basically you need to set the action and other properties to the toolbaritem but not in the toolbar. So try the same.
i ended up doing this in my view controller which seems to work
override func viewDidLayout() {
var x = self.view.window?.toolbar?.items[1].label
println(x)
if(self.view.window?.toolbar?.items[0].label! != "Check")
{
toobarediting()
}
println("didlay")
}
func toobarediting() {
self.view.window?.toolbar?.insertItemWithItemIdentifier("Check", atIndex: 0)
}
func toolbarcheck(functiontoset: Selector) {
var y = self.view.window?.toolbar?.items[0] as NSToolbarItem
y.action = functiontoset
if(functiontoset != nil)
{
y.enabled = true
}
}
It seems to allow me to make the tool bar button clickable/unclickable when ever i require it to change it just seems so much more bulky and error prone than
myitem.enable = fale
myitem.action = nil
is this really the best way for a storyboard based application in osx?
While connectiong IBActions works by using either the First Responder or by adding an "Object" to the scene, then changing its class to the window's view controller class, this doesn't help with IBOutlets and delegates that you'd like to point to the view controller.
Here's a work-around for that:
Add the Toolbar to the View Controller, not to its Window. That way, you can make all the IBOutlet connections in the View Controller Scene easily. I've done that for years and found no issues with it, even when using Tabs.
You'll have to assign the window's toolbar in code, then. E.g. like this:
#interface ViewController ()
#property (weak) IBOutlet NSToolbar *toolbar; // connect this in your storyboard to the Toolbar that you moved to the View Controller Scene
#end
- (void)viewWillAppear {
[super viewWillAppear];
self.view.window.toolbar = self.toolbar;
}

Cocoa Storyboard Responder Chain

Storyboards for Cocoa apps seems like a great solution as I prefer the methodology you find in iOS. However, while breaking things up into separate view controllers makes a lot of logical sense, I'm not clear as to how to pass window control (toolbar buttons) or menu interaction down to the view controllers that care. My app delegate is the first responder and it receives the the menu or toolbar actions, however, how can I access the view controller that I need to get that message to? Can you just drill down into the view controllers hierarchy. If so, how do you get there from the app delegate since it's the first responder? Can you make the window controller the first responder instead. If so, how? In the storyboard? Where?
Since this is a high level question it may not matter, however, I am using Swift for this project if you're wondering.
I'm not sure if there is a "proper" way to solve this, however, I have come up with a solution that I'll use for now. First a couple of details
My app is a document based application so each window has an instance of the document.
The document the app uses can act as the first responder and forward any actions I've connected
The document is able to get a hold of the top level window controller and from there I am able to drill down through the view controller hierarchy to get to the view controller I need.
So, in my windowDidLoad on the window controller, I do this:
override func windowDidLoad() {
super.windowDidLoad()
if self.contentViewController != nil {
var vc = self.contentViewController! as NSSplitViewController
var innerSplitView = vc.splitViewItems[0] as NSSplitViewItem
var innerSplitViewController = innerSplitView.viewController as NSSplitViewController
var layerCanvasSplitViewItem = innerSplitViewController.splitViewItems[1] as NSSplitViewItem
self.layerCanvasViewController = layerCanvasSplitViewItem.viewController as LayerCanvasViewController
}
}
Which gets me the view controller (which controls the view you see outlined in red below) and sets a local property in the window view controller.
So now, I can forward the toolbar button or menu item events directly in the document class which is in the responder chain and therefore receives the actions I setup in the menu and toolbar items. Like this:
class LayerDocument: NSDocument {
#IBAction func addLayer(sender:AnyObject) {
var windowController = self.windowControllers[0] as MainWindowController
windowController.layerCanvasViewController.addLayer()
}
// ... etc.
}
Since the LayerCanvasViewController was set as a property of the main window controller when it got loaded, I can just access it and call the methods I need.
For the action to find your view controllers, you need to implement -supplementalTargetForAction:sender: in your window and view controllers.
You could list all child controllers potentially interested in the action, or use a generic implementation:
- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
id target = [super supplementalTargetForAction:action sender:sender];
if (target != nil) {
return target;
}
for (NSViewController *childViewController in self.childViewControllers) {
target = [NSApp targetForAction:action to:childViewController from:sender];
if (![target respondsToSelector:action]) {
target = [target supplementalTargetForAction:action sender:sender];
}
if ([target respondsToSelector:action]) {
return target;
}
}
return nil;
}
I had the same Storyboard problem but with a single window app with no Documents. It's a port of an iOS app, and my first OS X app. Here's my solution.
First add an IBAction as you did above in your LayerDocument. Now go to Interface Builder. You'll see that in the connections panel to First Responder in your WindowController, IB has now added a Sent Action of addLayer. Connect your toolBarItem to this. (If you look at First Responder connections for any other controller, it will have a Received Action of addLayer. I couldn't do anything with this. Whatever.)
Back to windowDidLoad. Add the following two lines.
// This is the top view that is shown by the window
NSView *contentView = self.window.contentView;
// This forces the responder chain to start in the content view
// instead of simply going up to the chain to the AppDelegate.
[self.window makeFirstResponder: contentView];
That should do it. Now when you click on the toolbarItem it will go directly to your action.
I've been struggling with this question myself.
I think the 'correct' answer is to lean on the responder chain. For example, to connect a tool bar item action, you can select the root window controller's first responder. And then show the attributes inspector. In the attributes inspector, add your custom action (see photo).
Then connect your toolbar item to that action. (Control drag from your Toolbar item to the first responder and select the action you just added.)
Finally, you can then go to the ViewController (+ 10.10) or other object, so long as its in the responder chain, where you want to receive this event and add the handler.
Alternatively, instead of defining the action in the attributes inspector. You can simply write your IBAction in your ViewController. Then, go to the toolbar item, and control drag to the window controller's first responder -- and select the IBAction you just added. The event will then travel thru the responder chain until received by the view controller.
I think this is the correct way to do this without introducing any additional coupling between your controllers and/or manually forwarding the call.
The only challenge I've run into -- being new to Mac dev myself -- is sometimes the Toolbar item disabled itself after receiving the first event. So, while I think this is the correct approach, there are still some issues I've run into myself.
But I am able to receive the event in another location without any additional coupling or gymnastics.
As i'm a very lazy person i came up with the following solution based on Pierre Bernard
's version
#include <objc/runtime.h>
//-----------------------------------------------------------------------------------------------------------
IMP classSwizzleMethod(Class cls, Method method, IMP newImp)
{
auto methodReplacer = class_replaceMethod;
auto methodSetter = method_setImplementation;
IMP originalImpl = methodReplacer(cls, method_getName(method), newImp, method_getTypeEncoding(method));
if (originalImpl == nil)
originalImpl = methodSetter(method, newImp);
return originalImpl;
}
// ----------------------------------------------------------------------------
#interface NSResponder (Utils)
#end
//------------------------------------------------------------------------------
#implementation NSResponder (Utils)
//------------------------------------------------------------------------------
static IMP originalSupplementalTargetForActionSender;
//------------------------------------------------------------------------------
static id newSupplementalTargetForActionSenderImp(id self, SEL _cmd, SEL action, id sender)
{
assert([NSStringFromSelector(_cmd) isEqualToString:#"supplementalTargetForAction:sender:"]);
if ([self isKindOfClass:[NSWindowController class]] || [self isKindOfClass:[NSViewController class]]) {
id target = ((id(*)(id, SEL, SEL, id)) originalSupplementalTargetForActionSender)(self, _cmd, action, sender);
if (target != nil)
return target;
id childViewControllers = nil;
if ([self isKindOfClass:[NSWindowController class]])
childViewControllers = [[(NSWindowController*) self contentViewController] childViewControllers];
if ([self isKindOfClass:[NSViewController class]])
childViewControllers = [(NSViewController*) self childViewControllers];
for (NSViewController *childViewController in childViewControllers) {
target = [NSApp targetForAction:action to:childViewController from:sender];
if (NO == [target respondsToSelector:action])
target = [target supplementalTargetForAction:action sender:sender];
if ([target respondsToSelector:action])
return target;
}
}
return nil;
}
// ----------------------------------------------------------------------------
+ (void) load
{
Method m = nil;
m = class_getInstanceMethod([NSResponder class], NSSelectorFromString(#"supplementalTargetForAction:sender:"));
originalSupplementalTargetForActionSender = classSwizzleMethod([self class], m, (IMP)newSupplementalTargetForActionSenderImp);
}
// ----------------------------------------------------------------------------
#end
//------------------------------------------------------------------------------
This way you do not have to add the forwarder code to the window controller and all the viewcontrollers (although subclassing would make that a bit easier), the magic happens automatically if you have a viewcontroller for the window contentview.
Swizzling always a bit dangerous so it is far not a perfect solution, but I've tried it with a very complex view/viewcontroller hierarchy that using container views, worked fine.

Accessing controls of views of NSCollectionView

I'm using an NSCollectionView to display various objects. The whole things works rather well, except for one annoying thing. I cannot figure out how to access the various controls on the view used to represent each object in the collection.
Here's the setup:
I have dragged an NSCollectionView into my view in IB.
I made a custom subclass of NSCollectionViewItem. Mapped my class in IB.
I made a custom subclass of NSBox to act as the view for each object in the collection. Also mapped this class in IB and connected it to the view property of my NSCollectionViewItem subclass.
I made all the bindings in IB to display the correct information for each object.
The view:
The resulting collection view:
Reasoning that that my subclass of NSCollectionViewItem is basically a controller for each view in the collection, I made referencing outlets of the various controls in the view in my controller subclass:
#interface SourceCollectionViewItem : NSCollectionViewItem
#property (weak) IBOutlet NSTextField *nameTextField;
#property (weak) IBOutlet NSTextField *typeTextField;
#property (weak) IBOutlet RSLabelView *labelView;
#property (weak) IBOutlet NSButton *viewButton;
#end
When I inspect any instance of SourceCollectionViewItem in the debugger, all the properties show up as nil despite the fact that I can actually see them on my screen and that everything is displayed as it should be.
My setup was inspired by Apple's sample app IconCollection.
I am obviously missing something. What?
EDIT: I found various posts hinting at a similar issue:
CocoaBuilder.com and this question on SO.
EDIT: Just to be complete: this post deals with the subject as well and delivers a solution based on a combination of the options mentioned in the accepted answer.
Outlets are set during nib loading, and only the prototype item is loaded from nib and has its outlets assigned. All other ViewItems and their Views are cloned from the prototype, in that case outlets are just instance variables that are never initialized.
Here are the options I could come up with:
Override newItemForRepresentedObject: of collection view and reload nib instead of cloning the prototype. But this will probably hurt the performance greatly.
Override copyWithZone of collection view item and assign outlets manually using viewWithTag: to find them.
Give up and try to provide data via bindings only.
I found that overriding NSCollectionViewItem's -setRepresentedObject: could also be a good choice, as it is called on the new Item when all IBOutlet seem to be ready. After the call to super you can do whatever is needed:
- (void)setRepresentedObject:(id)representedObject
{
if (representedObject) {
[super setRepresentedObject:representedObject];
[self.anOutlet bind:#"property" toObject:self.representedObject withKeyPath:#"representeProperty" options:nil];
}
}
I used this method to bind a custom property of an interface object. The check is there to avoid useless calls, when the representedObject is not yet ready. The project uses a separate xib for the ViewItem, as explained in the links in the original edits.
Great question. Like #hamstergene suggests, you can use copyWithZone, it will be much more efficient compared to newItemForRepresentedObject. However viewWithTag is not always an option, first, because not everything can be tagged (easily), and, second, using tag for this purpose is a little wrong. Here's a cool approach with performance in mind, in Swift.
import AppKit
class MyViewController: NSCollectionItemView
{
// Here you are cloning the original item loaded from the storyboard, which has
// outlets available, but as you've seen the default implementation doesn't take
// care of them. Each view has a unique identifiers, which you can use to find it
// in sublayers. What's really cool about this, is that you don't need to assign
// any tags or do anything else while having advantage of better performance using
// cached nib object.
override func copyWithZone(zone: NSZone) -> AnyObject {
let copy: NSCollectionItemView = super.copyWithZone(zone) as! NSCollectionItemView
let oldView: RecordingView = self.view as! MyView
let newView: RecordingView = copy.view as! MyView
newView.foo = newView.viewWithIdentifier(oldView.foo.identifier!) as! NSTextfield
newView.bar = newView.viewWithIdentifier(oldView.bar.identifier!) as! NSImageView
return copy
}
}
#IBDesignable class MyView: View
{
// Custom collection view item view. Lets assume inside of it you have two subviews which you want
// to access in your code.
#IBOutlet weak var foo: NSTextfield!
#IBOutlet weak var bar: NSImageView!
}
extension NSView
{
// Similar to viewWithTag, finds views with the given identifier.
func viewWithIdentifier(identifier: String) -> NSView? {
for subview in self.subviews {
if subview.identifier == identifier {
return subview
} else if subview.subviews.count > 0, let subview: NSView = subview.viewWithIdentifier(identifier) {
return subview
}
}
return nil
}
}

Access parent view from a chid UIView

I have a UIViewController with an xib and using Interface Builder I've added a child UIView.
Within the child UIView, when I click on an object within that view, I want to be able to alter the title of the whole window.
Now I'd normally do that setting
self.title = #"hi";
on the parent UIViewController. But is there any way I can access the parent title from within the child?
I've tried
self.superview.title = #"i";
self.parentViewController.title = #"hi";
but neither work.
Any help much appreciated
thanks
self.superview.title = #"i"; evaluates to an object of type UIView, and UIView has no title property. UIViewControllers have a parentViewController property but UIViews don't.
So the fundamental problem is that you're not properly separating your controller and your view classes. What you'd normally do is make the view you want to catch taps on a subclass of UIControl (which things like UIButton already are, but if it's a custom UIView subclass then you can just change it into a UIControl subclass since UIControl is itself a subclass of UIView), then in your controller add something like:
- (void)viewDidLoad
{
[super viewDidLoad];
// we'll want to know if the view we care about is tapped;
// we've probably set up an IBOutlet to it but any way of
// getting to it is fine
[interestingView
addTarget:self
action:#selector(viewTapped:)
forControlEvents:UIControlEventTouchDown];
// UIButtons use UIControlEventTouchUpInside rather than
// touch down if wired up in the interface builder. Pick
// one based on the sort of interaction you want
}
// so now this is exactly like an IBAction
- (void)viewTapped:(id)sender
{
self.title = #"My new title";
}
So you explicitly don't invest the view with any knowledge about its position within the view hierarchy or how your view controllers intend to act. You just tell it to give you a shout out if it receives a user interaction.

Idiom for Initializing UINavigationItem

Since UIViewController navigationItem outlets are deprecated (yes, I'm a bit behind), what is the correct idiom specifying the elements of a UINavigationItem?
I've seen several approaches suggested, but none are entirely clear to me:
"Embed the navigation item in the view controller" in the view controller's XIB.
Initialize the navigation item's properties in code, with from-scratch values.
Create an outlet for the navigation controller in the view controller, wire up a navigation item in the view controller's XIB, and use its properties to initialize the (actual) navigation item's properties in code.
It isn't clear to me how to "embed" the navigation item (simply adding it as a child of the view controller in IB has no effect); and it isn't clear to me which of these approaches is better, or, for that matter, where (in what method) to do 2 or 3.
1) If controller is created in XIB you can drop UINavigationItem on it and tweak this item - it will work. For example when you're defining UINavigationControler in XIB you can put some controller inside as root view controller. So you can have UINavigationItem for this controller in XIB.
2) If controller loads it's view from XIB (it was created by alloc and then init or initWithNibName:bundle:) it's presented in XIB only as File's Owner which doesn't support UINavigationItem in it. In this case you should configure navigation item in code (I do it in viewDidLoad usually). There is no need to create it, it's already there in navigtionItem property of your controller
self.navigationItem.title = #"Price List";
Maybe it's possible to make outlet for navigation item but I wouldn't recommend this. Apple declared such outlet obsolete for a reason. I remember discussing this with co-worker one day, but I forgot what it was (it was obvious then).
On our project we have a requirement to make UI be as customizable from IB as possible.
So, I add UINavigationItem to xib, configure it, link it as outlet to my custom UIViewController subclass, and then copy all properties at runtime with method added to UIViewController using category:
- (void)setNavigationItemPropertiesFromOtherItem:(UINavigationItem *)navItem
{
// WORKAROUND: we can't link UINavigationItem to UIViewController from IB, and navigationItem property in UIViewController is readonly
self.navigationItem.title = navItem.title;
self.navigationItem.prompt = navItem.prompt;
self.navigationItem.hidesBackButton = navItem.hidesBackButton;
if (navItem.backBarButtonItem != nil)
{
self.navigationItem.backBarButtonItem = navItem.backBarButtonItem;
}
if (navItem.leftBarButtonItem != nil)
{
self.navigationItem.leftBarButtonItem = navItem.leftBarButtonItem;
}
if (navItem.rightBarButtonItem != nil)
{
self.navigationItem.rightBarButtonItem = navItem.rightBarButtonItem;
}
if (navItem.titleView != nil)
{
self.navigationItem.titleView = navItem.titleView;
}
}
This workaround also allows to link bar button items to UINavigationItem using IB.

Resources