Launch Storyboard from within Nib? - xcode

I am looking at rebuilding the settings section of my app using the new functionality provided by storyboards. Not wanting to touch the rest of my app at this point, so my main NIB will be staying.
Now when going from my NIB'ed tabBar to another NIB I just add a viewController to the tabBar in IB and then set the NIB Name property to the NIB which I want to load when that tab is pressed.
But there is no 'storyboard name' property that I can see, so how is this done?

There is no "official" way to do it at the moment, but you can do it using some tricks.
1) add your view controller to your tabbar in nib in the usual way. Leave the nib field empty.
2) create your storyboard and add your viewcontroller. Set the class and set a storyboard ID (I'll use "theID" for this example)
3) add a static bool var to your .m file, outside implementation or interface
static BOOL aFlag = NO;
4) in your viewcontroller class override this method:
- (id) awakeAfterUsingCoder:(NSCoder *)aDecoder
{
if (!aFlag){
aFlag = YES;
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Storyboard" bundle:nil];
return [storyboard instantiateViewControllerWithIdentifier:#"theID"];
} else {
return self;
}
}
essentially:
when you load the object from the tab bar nib, a first call to "initWithCoder" is made, and the object loads without nib
after initWithCoder, awakeAfterUsingCoder is called and there you substitute the object with another loaded from storyboard. Object of the same class but archived in the storyboard
when you load the object from the storyboard, another call to both initWithCoder and awakeAfterUsingCoder. You use the flag to avoid a loop and return self (at the second call, the object is loaded from storyboard so returning self is ok)
I tried and it works good ;-)
If you want here is an example project: http://www.lombax.it/files/testTabNib.zip

Related

NSSplitViewController/NSSplitViewItem support in XIBs

Is there support for NSSplitViewController/NSSplitViewItem for XIBs? I see only NSSplitView
Can I just drag&drop NSViewController and subclass it as NSSplitViewController? How do I add NSSplitViewItem that it mostly works out of the box?
I can easily see support for them in storyboards.
The split view controller is not part of the object library for xib files. The easiest way to use split view controllers is to use storyboards.
If you are unwilling to use storyboards, your best option is to create a subclass of NSSplitViewController and select the checkbox to also create a xib file.
Add a split view to the split view controller xib file. Write code to load the xib file to set up the split view controller.
UPDATE
Look at the NSNib class reference for information on loading a xib file. The File's Owner of the xib file is your NSSplitViewController subclass. You may be able to use that information to set the split view controller. The worst case scenario is that you have to write code to load the split view from the xib file, set the split view controller's split view to the split view you loaded, and add the split view items to the split view controller. See the NSSplitViewController class reference for more information.
Yes it's possible. But it needs some wiring.
First add a custom subclass of NSSplitViewItem and expose viewController property as IBOutlet. Compiler will throw a warning so don't forget to mark property as dynamic.
#interface MySplitViewItem : NSSplitViewItem
#property IBOutlet NSViewController *viewController;
#end
#implementation MySplitViewItem
#dynamic viewController;
#end
In your XIB add 3 NSViewController objects. One of them change to custom class NSSplitViewController. It is important to note that one should NOT add NSSplitView. Wire NSViewControllers to it's views. Also add 2 objects and add custom class of MySplitViewItem which has exposed the viewController and wire it.
Last step. It is important to set property splitItems of NSSplitViewController before the views are loaded! Otherwise you are caught with NSAssert macro.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSNib *nib = [[NSNib alloc] initWithNibNamed:#"Empty" bundle:nil];
NSMutableArray *test = [NSMutableArray new];
NSMutableArray *splitItems = [NSMutableArray new];
NSSplitViewController *controller;
[nib instantiateWithOwner:self topLevelObjects:&test];
for (id object in test) {
if ([object isKindOfClass:[NSSplitViewController class]]) {
controller = object;
}
if ([object isKindOfClass:[NSSplitViewItem class]]) {
[splitItems addObject:object];
}
}
[controller setValue:splitItems forKey:#"splitViewItems"];
[[self window] setContentViewController:controller];
}
Here is a proof that everything is wired correctly. Note that I did not touch delegate in XIB and it is wired. Magic, I know.
PS: XIB has to be set to prefer Coder + auto layout.
Why do I prefer XIB? Because we can create larger XIB which doesn't suffer from data isolation (Easily can do bindings across NSViewControllers).
I have also experimented to add splitViewItems in viewDidLoad or setView or awakeFromNib: in custom subclass of NSSplitViewController (with exposed NSSplitViewItem properties). If someone finds solution here it will be greatly appreciated.
Solution that requires code only:
- (NSSplitViewController *)profilesSVC
{
if (!_profilesSVC) {
NSSplitViewController *splitVC = [[NSSplitViewController alloc] init];
ProfilesViewController *profilesVC = [[ProfilesViewController alloc] initWithNibName:#"Profiles" bundle:nil];
NSSplitViewItem *leftItem = [NSSplitViewItem splitViewItemWithViewController:profilesVC];
[splitVC addSplitViewItem:leftItem];
ProfileViewController *profileVC = [[ProfileViewController alloc] initWithNibName:#"Profile" bundle:nil];
NSSplitViewItem *rightItem = [NSSplitViewItem splitViewItemWithViewController:profileVC];
[splitVC addSplitViewItem:rightItem];
_profilesSVC = splitVC;
}
return _profilesSVC;
}
I too wanted to add a splitView controller to my projet (macOS app) that doesn't use storyboards.
As it turned out, this was rather easy (in XCode 12.4).
As suggested, one has to to add NSViewController objects to the xib and wire each view property to the corresponding 'pane' (subview of the split view) in interface builder.
Then create a subclass of NSSplitViewController (no need to create a xib file).
Add a third NSViewController object to the xib and change its class to your subclass. Then wire both it's view and splitView properties to your splitView. It doesn't load any view if you just wire the splitView property.
Using a subclass of NSSplitViewController may not be required, but it's convenient as you may set the splitViewItems within viewDidLoad (below). Since this object is (automatically) the delegate of the splitView, you can also override delegate methods if you wish.
That object should have outlets leading to the NSViewController objects which you previously wired to the panes in IB.
I set two outlets named leftController and rightController.
My awakeFromNib method looks like this (sorry, I don't use swift):
- (void) viewDidLoad {
self.splitView.wantsLayer = YES; // I think this is required if you use a left sidebar with vibrancy (which I do below). Otherwise appkit complains and forces the use of CA layers anyway
NSSplitViewItem *left =[NSSplitViewItem sidebarWithViewController:leftController];
[self addSplitViewItem:left];
NSSplitViewItem *right =[NSSplitViewItem splitViewItemWithViewController:rightController];
right.minimumThickness = 420;
[self addSplitViewItem:right];
}
VoilĂ !
However, I get crashes if I set thick dividers in IB as appkit calls splitView:shouldHideDividerAtIndex too early, when there is apparently no divider yet. Worse, it may pass a negative divider index (!!). But you may override the method and act accordingly and I have no issue with thin dividers.

Custom NSView with outlets

Am new to Mac apps, and am writing one simple app which has a common layout for different sections of the app. Its basically an image with one or two buttons(titles keep varying) in all sections.
So I thought of creating a CustomNSView with one Image Well and two rounded buttons in a new Nib file and in a separate class file (MyCustomView, which is a subclass of NSView) it would load this Nib in initWithframe method. So now when ever I drag drop a custom view and set its class to MyCustomView I get the Image and two buttons instantly without any additional code. But now how would I control these buttons(outlets/actionms) in other View Controllers ? The same view will be used every where so I cannot set files owner in nib to the view controller ?
Is it right in doing so ? Is there any way to create a custom view which would delegate all button actions to which ever view controller its included in ?
You can write your custom delegate. Though using that you can send messages from one object to another
Here's how I would do it. I wouldn't create a CustomNSView, I would create a CustomViewController (with its xib file included). On that CustomViewController, I would design the two buttons and setup CustomViewController.h like so.
#property (nonatomic, weak) id delegate; // Create a delegate to send call back actions
-(IBAction)buttonOneFromCustomVCClicked:(id)sender;
-(IBAction)buttonTwoFromCustomVCClicked:(id)sender;
CustomViewController.m like so.
-(void)buttonOneFromCustomVCClicked:(id)sender {
if ([self.delegate respondsToSelector:#selector(buttonOneFromCustomVCClicked:)]) {
[self.delegate buttonOneFromCustomVCClicked:sender];
}
}
-(void)buttonTwoFromCustomVCClicked:(id)sender {
if ([self.delegate respondsToSelector:#selector(buttonTwoFromCustomVCClicked:)]) {
[self.delegate buttonTwoFromCustomVCClicked:sender];
}
}
In interface builder of your customViewController, link up both button's SentAction event to the two methods (they should be displayed in file's owner).
Then in your otherClass in which you want to load the generic custom view, instantiate the generic view controller like so.
#import "customViewController.h"
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
customViewController *newCustomViewController = [[ViewController alloc] initWithNibName:#"customViewController" bundle:nil];
[newCustomViewController setDelegate:self];
self.backGroundView = [newCustomViewController view]; // Assuming **backGroundView** is an image view on your background that will display the newly instantiated view
}
-(void)buttonOneFromCustomVCClicked:(id)sender {
// Code for when button one is clicked
}
-(void)buttonTwoFromCustomVCClicked:(id)sender {
// Code for when button two is clicked
}

Interface-Builder: "combine" NSView-class with .xib

I'd like to set up a custom NSView in Interface-Builder, but I don't get it to work for OSX.
In my ViewController's .xib, I added a custom view and set the Class to MyCustomView. I created MyCustomView.h, MyCustomView.m and MyCustomView.xib.
In MyCustomView.xib, I set the Class to MyCustomView as well. In MyCustomView.m, - (void)awakeFromNib is called, but - (id)initWithCoder:(NSCoder *)aDecoder and - (id) awakeAfterUsingCoder:(NSCoder*)aDecoder aren't.
What I'd like to achieve is that in my ViewController, the view I added is "filled" with the view I set up in MyCustomView.xib. What's the best way to do that?
EDIT: I don't think I was clear enough...
I've got my ViewController containing a Custom View called MyCustomView.
This view should be of type MyCustomView, where
MyCustomView.h
MyCustomView.m
MyCustomView.xib
exists. I already set the File's Owner of MyCustomView.xib to MyCustomView and I already set the CustomView in my ViewController to MyCustomView - but it doesn't work.
If I do it with
- (void)awakeFromNib {
NSString* nibName = NSStringFromClass([self class]);
NSArray* topLevelObjects;
[[NSBundle mainBundle] loadNibNamed:nibName
owner:nil
topLevelObjects:&topLevelObjects];
NSView* view = topLevelObjects[0];
[view setFrame:[self bounds]];
[self addSubview:view];
}
I only get a view of type NSView, not MyCustomView... Is there no easy way to tell the ViewController.xib that it's a MyCustomView?
EDIT 2: I uploaded a simple project
At https://dl.dropboxusercontent.com/u/119600/Testproject.zip you find a simple project with the MyCustomView (not in a ViewController but in the window.xib) - but it doesn't show the button which is in MyCustomView.xib. I'd like to achieve exactly that - what's the simplest, best way?
EDIT - apologies, my existing answer failed to take into account the need to connect outlets and actions. This way should do it...
Given the files...
MyCustomView.h
MyCustomView.m
MyCustomView.xib
In MyCustomView.h
declare IBOutlets for your interface elements. You need at least one, to hold a pointer to the top-level view in the xib file
#property (nonatomic, strong) IBOutlet NSView *view;
In MyCustomView.xib
ensure that there is only one top-level view
set file's owner class to MyCustomView in the Identity Inspector
ensure that the top-level view is set to the default NSView class.
now you can connect IBOutlets declared in MyCustomView.h to interface objects in the xib file. At very least you need to connect up the top-level view to your view outlet.
In MyCustomView.m:
- (id)initWithFrame:(NSRect)frame
{
NSString* nibName = NSStringFromClass([self class]);
self = [super initWithFrame:frame];
if (self) {
if ([[NSBundle mainBundle] loadNibNamed:nibName
owner:self
topLevelObjects:nil]) {
[self.view setFrame:[self bounds]];
[self addSubview:self.view];
[self.myCustomButton setTitle:#"test success"];
}
}
return self;
}
In your window's xib file, add a custom NSView and change it's class to MyCustomView.
in OSX prior to 10.8 the method loadNibNamed was a class method - use it instead if you need backwards compatibility, but it is deprecated now:
[NSBundle loadNibNamed:#"NibView" owner:self]
Note that MyCustomView.xib's view is NOT MyCustomView's view, but the sole subview of it's view (this is similar to the way a tableViewCell possesses a single contentView).
In the project sample you have posted, you need to make the following changes:
in MyCustomView.h
. add an NSView property
in MyCustomView.xib:
. change the top-level view from MyCustomView custom class to NSView (the default)
. set the File's Owner to MyCustomView.
. connect IBOutlets from File's owner's view and myCustomButton to interface view and button
. for testing make the view a lot smaller and push the button up to the top right (you won't see it in your window as it is here)
in MyCustomView.m:
. replace all of your implementation code with the initWithFrame method here
In order to load a custom subclass of a view or a control (or any other class that can be used in Interface Builder for that matter), you need to add the base version (NSView in your case, and as you have done), then select that object in the window and go to the Identity Inspector (Cmd-Opt 3).
Instead of the pre-defined value for Class (NSView, in your case), type in the name of your custom subclass. VoilĂ !
A small detail related to the IB UX is that you'll probably have to move the focus from the input field in order for that change to be registered when you build and run the app.

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