I have a view setup in Interface Builder which contains a viewController that loads another Nib file. However, for some reason the objects contained in the nib file being loaded never gets called awakeFromNib. What am I missing here? Is there anyway to load views from Nib in interface builder and also manage their properties and sizing in the interface builder?
In general, what are the best practices for managing multiple nibs and composing them into complex views?
Final Solution:
I created a NSViewController subclass like this.
#interface NibLoadingViewController : NSViewController
// The placeholder would be replaced during run-time by the view
#property (weak, nonatomic) IBOutlet NSView *placeholder;
#end
#implementation NibLoadingViewController
#synthesize placeholder = _placeholder;
- (void)awakeFromNib {
if (self.placeholder)
self.view = self.view; // Trigger lazy loading
}
- (void)loadView {
[super loadView];
if (!self.view)
return;
// Replace the placehoder if it exists
if (self.placeholder) {
// Copy over relevant attributes
self.view.frame = self.placeholder.frame;
self.view.autoresizingMask = self.placeholder.autoresizingMask;
self.view.autoresizesSubviews = self.placeholder.autoresizesSubviews;
// Replaces the old view
[self.placeholder.superview replaceSubview:self.placeholder with:self.view];
self.placeholder = nil;
}
self.nextResponder = self.view.nextResponder;
self.view.nextResponder = self;
}
#end
This way, you just need to hook the placeholder outlet in the nib that contains the view controller and it will automatically load the other nibs for you and copy all the attributes from placeholder over and replace it in the main nib.
The content of the nib-file is lazy-loaded. If you want -(void)awakeFromNib to be called, you need to access something from the nib-file first.
NSViewController *controller = [[NSViewController alloc] initWithNibName:#"MyView" bundle:nil];
/*
* awakeFromNib was not called yet
*/
NSView *view = controller.view;
/*
* but now -(void)awakeFromNib was called.
*/
Related
I followed the advice here on how to setup a MainWindowController: NSWindowController for my project's single window. I used a Cocoa class to create the .h/.m files, and I checked the option Also create .xib for User Interface. As a result, Xcode automatically hooked up a window, which I renamed MainWindow.xib, to my MainWidowController.
Next, I deleted the window in the default MainMenu.xib file (in Interface Builder I selected the window icon, then I hit the delete key). After that, I was able to Build my project successfully, and my controller's window in MainWindow.xib displayed correctly with a few buttons on it.
Then I tried adding an NSTableView to my MainWindowController's window. In Xcode, I dragged the requisite delegate and datasource outlets for the NSTableView onto File's Owner, which is my MainWindowController, and I implemented the methods in MainWindowController.m that I thought would make the NSTableView display my data:
- tableView:viewForTableColumn:row:
- numberOfRowsInTableView:
Now, when I Build my project, I don't get any errors, but the data doesn't appear in the NSTableView.
My code is below. Any tips are welcome!
//
// AppDelegate.h
// TableViews1
//
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate>
#end
...
//
// AppDelegate.m
// TableViews1
//
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#property (strong) MainWindowController* mainWindowCtrl;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[self setMainWindowCtrl:[[MainWindowController alloc] init] ];
[[self mainWindowCtrl] showWindow:nil];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
#end
...
//
// MainWindowController.h
// TableViews1
//
#import <Cocoa/Cocoa.h>
#interface MainWindowController : NSWindowController
#end
...
//
// MainWindowController.m
// TableViews1
//
#import "MainWindowController.h"
#import "Employee.h"
#interface MainWindowController () <NSTableViewDataSource, NSTableViewDelegate>
#property (strong) NSMutableArray* employees;
#property (weak) IBOutlet NSTableView* tableView;
#end
#implementation MainWindowController
- (NSView*)tableView:(NSTableView *)tableView
viewForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
Employee* empl = [[self employees] objectAtIndex:row];
NSString* columnIdentifier = [tableColumn identifier];
//The column identifiers are "firstName" and "lastName", which match my property names.
//You set a column's identifier by repeatedly clicking on the TableView until only
//one of the columns is highlighted, then select the Identity Inspector and change the column's 'Identifier' field.
NSString* emplInfo = [empl valueForKey:columnIdentifier]; //Taking advantage of Key-Value coding
NSTableCellView *cellView =
[tableView makeViewWithIdentifier:columnIdentifier
owner:self];
NSLog(#"The Table view is asking for employee: %#", [empl firstName]);
[[cellView textField] setStringValue:emplInfo];
return cellView;
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return [[self employees] count];
}
- (void)windowDidLoad {
[super windowDidLoad];
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
Employee* e1 = [[Employee alloc] initWithFirstName:#"Joe" lastName:#"Blow"];
Employee* e2 = [[Employee alloc] initWithFirstName:#"Jane" lastName:#"Doe"];
[self setEmployees:[NSMutableArray arrayWithObjects:e1, e2, nil]];
//Test to see if the employees array was populated correctly:
Employee* e = [[self employees] objectAtIndex:0];
NSLog(#"Here is the first employee: %#", [e firstName]);
//I see the output: "Here is the first employee: Joe"
}
- (id)init {
return [super initWithWindowNibName:#"MainWindow"];
}
- (id)initWithWindowNibName:(NSString *)windowNibName {
NSLog(#"Clients cannot call -[%# initWithWindowNibName] directly!",
[self class]
);
[self doesNotRecognizeSelector:_cmd];
return nil;
}
#end
...
//
// Employees.h
// TableViews1
#import <Foundation/Foundation.h>
#interface Employee : NSObject
#property NSString* firstName;
#property NSString* lastName;
- initWithFirstName:(NSString*)first lastName:(NSString*)last;
#end
...
//
// Employees.m
// TableViews1
//
#import "Employee.h"
#implementation Employee
- (id)initWithFirstName:(NSString *)first lastName:(NSString *)last {
if (self = [super init]) {
_firstName = first; //I read that you shouldn't use the accessors in init methods.
_lastName = last;
}
return self;
}
#end
File's Owner(=MainWindowController) connections:
NSTableView connections:
Response to comments:
Here is why calling [self tableView] reloadData] at the end of -windowDidLoad, as suggested in the comments, didn't work:
My _tableView instance variable--created by my #property declaration in MainWindowController.m--doesn't point to anything; therefore calling:
[[self tableView] reloadData]
I think is equivalent to calling:
[nil reloadData]
which doesn't do anything.
I never assigned anything to the _tableView instance variable in the -init method, nor did I assign it a value by dragging an outlet somewhere in Interface Builder. To fix that problem, I selected MainWindow.xib (the controller's window) in the Project Navigator(left pane), and then in the middle pane(Interface Builder), I selected the cube representing the File's Owner(selecting the Identity Inspector in the right pane reveals that the File's Owner is the MainWindowController). Then in the right pane, I selected the Connections Inspector, and it revealed an outlet called tableView, which is the IBOutlet variable I declared in MainWindowController.m.
Next, I dragged from the tableView outlet onto the TableView in the middle pane:
Doing that assigns the NSTableView object to the _tableView instance variable that was created by my #property declaration in MyWindowControler.m:
#property (weak) IBOutlet NSTableView* tableView;
As an experiment, I disconnected the outlet, then commented out the #property declaration for tableview, and the tableView outlet no longer appeared in the Connections Inspector. Also, if I change the declaration from:
#property (weak) IBOutlet NSTableView* tableView;
to:
#property (weak) NSTableView* tableView;
...then the tableView outlet doesn't appear in the Connections Inspector. That experiment answered a couple of questions I had about whether I should declare a property as an IBOutlet or not: if you need to assign one of the objects in Interface Builder to one of your variables, then declare the variable as an IBOutlet.
Thereafter, calling [self tableView] reloadData] at the end of -windowDidLoad succeeds in populating the TableView. However, I have not seen any tutorials that call reloadData, and even Apple's guide does not do that.
So, I am still puzzled about whether calling -reloadData is a hack or it's the correct way to do things.
Without it, your table view sits there blissfully clueless about your
expectation that it should even bother asking its datasource for data.
I assumed that an NSTableView automatically queries its datasource when it is ready to display itself, and that my code needed to be able to provide the data at that time.
I don't see you sending -reloadData to your table view anywhere. Tacking it onto the end of -windowDidLoad would be a good place. Without it, your table view sits there blissfully clueless about your expectation that it should even bother asking its datasource for data.
For all it knows, the data is simply not ready / available, so why would it try? More importantly, when should it try? It'd be rather rude of it to try whenever it pleases, considering the UI may not have finished loading / connecting to outlets, or its datasource may be in a vulnerable state (like teardown during/after dealloc) and sending datasource requests may result in a crash, etc.
Two things:
1st, set some breakpoints on when you set your employees array in windowDidLoad vs. when the table first attempts to populate itself and your numberOfRowsInTableView implementation gets called. If the latter happens before the former, then you'll need to add a reloadData after you create your array.
2nd, I personally always use NSCell instead of NSViews for my tables, so I always implement objectValueForTableColumn in my table's datasource. So I'm not sure if there's something different you need to do when you use NSView objects and implement viewForTableColumn. Is there a reason you're not using NSCell?
In short: I bind an NSTextField to the File's Owner (the view controller) and Model Key Path of representedObject.firstName, but editing the text field does not change the firstName.
Here are more details. I have a simple program that does nothing but create an instance of Thing (a simple class with some properties), and ThingViewController. The controller has an associated .xib with a simple UI -- a couple text fields to bind to properties of the Thing.
#interface Thing : NSObject
#property (nonatomic, strong) NSString *firstName;
#property (nonatomic, strong) NSString *lastName;
#property (nonatomic) BOOL someBool;
#end
And in the app delegate...
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSView *cv = self.window.contentView;
ThingViewController *vc = [[ThingViewController alloc]
initWithNibName:#"ThingViewController" bundle:nil];
theThing = [Thing new];
theThing.firstName = #"Rob";
vc.representedObject = theThing;
[cv addSubview:vc.view];
}
The ThingViewController.xib is simple:
And here is the binding for that first text field:
When I run, the text field does show "Rob", so it works in that direction, but then as I edit the text field, the firstName property of theThing does not change.
What am I doing wrong?
Edit: Here's a link to a zipped project file for the above code: https://drive.google.com/file/d/0B2NHW8y0ZrBwWjNzbGszaDQzQ1U/edit?usp=sharing
Nothing is strongly referencing your view controller (ThingViewController), other than the local variable in -applicationDidFinishLaunching:. Once that goes out of scope, the view controller is released and dealloc'ed. The view itself is still around, since it is a subview of your window's contentView.
Once your view controller is released/gone, the text field has no connection back to the Thing object so it is in effect calling [nil setValue:#"New first name" forKeyPath:#"representedObject.firstName"].
Add a strong reference to your view controller (e.g., an instance variable of your app delegate) and try it again.
#implementation AppDelegate {
Thing *theThing;
ThingViewController *vc;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSView *cv = self.window.contentView;
vc = [[ThingViewController alloc] initWithNibName:#"ThingViewController" bundle:nil];
theThing = [Thing new];
theThing.firstName = #"Rob";
vc.representedObject = theThing;
[cv addSubview:vc.view];
}
I know that this is a newbie question but I am a newbie so here goes:
I wish to use Chalkduster font quite a lot throughout my app (buttons, labels etc) and have tried subclassing UILabel to achieve this. I have the following in Default.h:
#import <UIKit/UIKit.h>
#interface Default : UILabel
{
UILabel *theLabel;
}
#property (nonatomic, strong) IBOutlet UILabel *theLabel;
#end
and this in my .m:
#import "Default.h"
#implementation Default
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
UIFont *custom = [[UIFont alloc] init];
custom = [UIFont fontWithName:#"Chalkduster" size:18];
self.font = custom;
NSLog(#"h");
}
return self;
}
#end
When I change the class in interface builder and run, I'm not seeing the Chalkduster font. I'd appreciate a hand in getting this set up as I believe it will save me a lot of time.
Cheers.
Some problems to fix:
1) You're mixing up the idea of Default being a label and Default containing a label. To subclass, get rid of the property inside your class and make your changes to self rather than theLabel (inside the if (self) { section).
2) Anything you code after an unconditional return isn't going to get executed...and I'm surprised the compiler didn't complain about those statements.
Edit: ...and one more thing that just dawned on me.
3) If you're loading from a xib or storyboard, the initialization is done by initWithCoder: instead of initWithFrame:, so:
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
self.font = [UIFont fontWithName:#"Chalkduster" size:18];
}
return self;
}
First of all I don't think that You're subclassing UILabel correctlly. So I made tutorial for You explaining how to do it. You don't need to IBOutlet object which is subclassed. JUST CALL IT WITH SELF. for example: self.font = ... If You want to subclass UILabel do this:
Create new class with title myLabel like this:
.h
#import <UIKit/UIKit.h>
#interface MyLabel : UILabel {
}
#end
.m
#import "MyLabel.h"
#implementation MyLabel
-(void)awakeFromNib {
UIFont *custom = [[UIFont alloc] init];
custom = [UIFont fontWithName:#"Chalkduster" size:18];
self.font = custom;
}
#end
Now select Your label in storyboard and go to indentity inspector and in Custom Class select created class above. Like this:
Output:
Note: Don't forget to release custom because You are allocating it.
Move the return self; three lines down. You return from the init method before you do your custom initialization.
Edit to reflect new information from comment:
When deserializing the view from a nib you also have to override initWithCoder:
I'm building a collection of forms each of which contains several fields. Some of the fields are UITextFields that will display a date. I've created a new class called DatePickerTextField, a descendant of UITextField. When a DatePickerTextField is tapped I'd like for a UIDatePicker control to appear in a popover.
My question is how do I use the storyboard to implement the popover? I can do a segue when there is a specific, visible control in the scene. But how do I represent a generic popover in the scene that I can attach to any instantiated DatePickerTextField that becomes active?
You can create segue that is not connected to any control but I don't think that there would be way to specify anchor point for popover from code. Another option is to create ViewController that is not connected with any segue. When editing storyboard, create ViewController which will be placed in popover, select it and navigate to Utilities pane->Attributes Inspector. Set Size to Freeform, Status Bar to None, specify unique Identifier that will be used to instantiate ViewController from code. Now you can change the size of ViewController by selecting its View and navigating to Utilities pane->Size Inspector.
After that you can create popover from code:
- (IBAction)buttonPressed:(id)sender {
UIView *anchor = sender;
UIViewController *viewControllerForPopover =
[self.storyboard instantiateViewControllerWithIdentifier:#"yourIdentifier"];
popover = [[UIPopoverController alloc]
initWithContentViewController:viewControllerForPopover];
[popover presentPopoverFromRect:anchor.frame
inView:anchor.superview
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
One caveat is that you need to hold reference to popover as ivar of your class, otherwise it'll crash because UIPopoverController would be released and deallocated after buttonPressed returns:
#interface MyViewController : UIViewController {
// ...
UIPopoverController *popover;
// ...
}
So, I had a similar issue, and in case others might benefit, I figured I'd share it, since I benefit so much from stackoverflow.
This solution allows you to set the anchor of a customizable popover segue. It also allows you to configure the segue to be modal or not (I could not find a way to prevent the segue by dimming the exterior context, so if someone knows how to do that, I would be interested in hearing it); this is accomplished by setting the passthrough views for the popover controller. I also added the capacity to specify a custom view, rather than the view of the source viewcontroller (since I needed this capacity); this portion is not critical to the solution.
DynamicPopoverSegue.h
#interface DynamicPopoverSegue : UIStoryboardPopoverSegue
#property BOOL isModal;
#property UIView* sourceView;
#property CGRect anchor;
#end
DynamicPopoverSegue.m
#implementation DynamicPopoverSegue
- (void)perform
{
if (!self.popoverController.popoverVisible)
{
UIViewController* dst = (UIViewController*)self.destinationViewController;
UIViewController* src = (UIViewController*)self.sourceViewController;
UIView* inView = _sourceView ? _sourceView : src.view;
self.popoverController.contentViewController = dst;
if (!_isModal)
{
[self.popoverController setPassthroughViews:[[NSArray alloc] initWithObjects:inView, nil]];
}
[self.popoverController presentPopoverFromRect:_anchor
inView:inView
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
}
#end
Then you just set your segue to "Custom" in the storyboard, and set the segue class to "DynamicPopoverSegue". In my case, since I wanted to associate it with dynamic layers in a view, I could not set the anchor, so I created the segue by control clicking from the view controller icon in the bar beneath my view controller to the view controller I was using to present the popupover.
To call the popover segue:
[self performSegueWithIdentifier:#"MyPopoverSegue" sender:self];
And to configure the popover segue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"MyPopoverSegue"])
{
DynamicPopoverSegue* popoverSegue = (DynamicPopoverSegue*)segue;
// set the anchor to wherever you want it to be
popoverSegue.anchor = _destinationFrame;
}
}
- (IBAction)pressItemChooseOprateRoom:(id)sender {
if (isPad){
// UIView *anchor = sender;
UIViewController *viewControllerForPopover =
[self.storyboard instantiateViewControllerWithIdentifier:#"OperateRoomList"];
_myPopover = [[UIPopoverController alloc]
initWithContentViewController:viewControllerForPopover];
CGRect rc=[self getBarItemRc:sender];
[_myPopover presentPopoverFromRect:rc
inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
[MLControl shared].popover =self;
// [self perfformSegueWithIdentifier:SEGUE_POP_OPERATEROOM sender:self];
}else{
[self iphoneOpenOperateRoomList];
/* [self performSegueWithIdentifier:#"iPhonePushOperateRoom" sender:self];
*/
}
}
-(void)iphoneOpenOperateRoomList{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"OperateRoomList"];
// if (!index.showTabBar) {
// vc.hidesBottomBarWhenPushed = YES;
// }
[self.navigationController pushViewController:vc animated:YES];
}
Just used the answer from Jonnywho for my SWIFT project. In case you need it:
Here's the SWIFT version:
let anchor: UIView = sender
var viewControllerForPopover = self.storyboard?.instantiateViewControllerWithIdentifier("GameAboutViewController") as! UIViewController?
let popover = UIPopoverController(contentViewController: viewControllerForPopover!)
popover.presentPopoverFromRect(anchor.frame, inView: anchor, permittedArrowDirections: UIPopoverArrowDirection.Any, animated: true)
Add a UIView in the scene dock.
You can add it as a subview to any existing view on the view controller.
You can then toggle it's isHidden property as you require.
You can add multiple such subviews and create many such popups.
This technique will save you from setting up a new View Controller and using segues.
If I want to set my delegate and datasource for my uitableview my app crashes. What I'm doing wrong?
- (void)loadView {
NSLog(#"AddListViewController");
ShareListViewController *shareListViewController = [[ShareListViewController alloc] init];
UITableView *shareListView = [[UITableView alloc]initWithFrame:CGRectMake(100, 30, 100, 200) style:UITableViewStylePlain];
shareListView.delegate = shareListViewController;
shareListView.dataSource = shareListViewController;
[self.navigationController.view addSubview:shareListView];
[shareListViewController release];
}
And my ShareListViewController
#interface ShareListViewController : UITableViewController <UITableViewDelegate, UITableViewDataSource>{
}
#end
It also not working if I remove the protocols.
Thx for your help
You are releasing the shareListController controller. This is wrong, since both, that dataSourceproperty as well as the delegate property of a table view are declared as having retention policy assign, which means, that they do not retain their values. This is the responsibility of client application/view controller.
#property(nonatomic, assign) id<UITableViewDelegate> delegate
#property(nonatomic, assign) id<UITableViewDataSource> dataSource
See the UITableView Reference.
The best approach seems to be to declare an ivar in your view controller like:
#interface MyViewController: UIViewController {
... other stuff ...
UITableView* shareListView;
ShareListController* shareListController;
}
... more stuff ...
#end
In MyViewController's viewDidLoad (or, wherever else you set up the table view) alloc/init the shareListController as usual, but instead of releasing the object after you have set up the dataSource and delegate properties, you remember the reference (you still own it) in the shareListController ivar.
Finally, in the view controller's dealloc method, do:
- (void) dealloc {
... release other stuff ...
shareListView.delegate = nil; // Really, really defensive
shareListView.dataSource = nil; // here, but it won't hurt
[shareListView release];
[shareListController release];
[super dealloc];
}