Cannot load custom view from nib into another view made programmatically? - objective-c

Looking at some of the other questions, this is the code I implemented in the .m of the view implementing the custom keyboard.
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
self.userInteractionEnabled = YES;
[self addGestureRecognizer:
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(becomeFirstResponder)]];
NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:#"FormulaKeyboard" owner:self options:nil];
for (id object in bundle) {
if ([object isKindOfClass:[FormulaKeyboard class]])
keyboard = (FormulaKeyboard *)object;
}
self.inputView = keyboard;
}
return self;
}
Below is the error that showed up.
* Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key view.'
When I load the nib in an actual view controller, it works fine. However, as evidenced above, it doesn't work when I try loading it inside another view.
Also, in the nib, I set the file owner's class to UIViewController and attached it to the main view, since this was what another SO question instructed. I'm not sure if I need to modify this since I'm adding the custom view inside another view and not a viewc controller.
Thanks
EDIT:
.h of view implementing keyboard
#interface EquationTextField : UIView <KeyInput> {
FormulaKeyboard *keyboard;
}
#property (readwrite, retain) UIView *inputView;
#end

Inside the xib if you ctrl + click the File's Owner you will most likely see that you have a view outlet that is connected to the top level view object in the xib.
Refers to an IBOutlet in your code
Refers to an object in Interface Builder
The reason this works inside a UIViewController is that a UIViewController has a property view which gets connected up when loading the xib.
A UIView subclass does not normally have a property called view therefore you get an exception.
Seems as you are grabbing the item from the top level objects array returned by the loadNibNamed:owner:options: method you do not need this connection in the xib. Therefore you can disconnect this connection in the xib file.
Alternatively
You could:
Change the File's Owner class to the class of your custom view.
Make inputView an IBOutlet
Connect inputView to your custom keyboard in the xib
Then just use
[[NSBundle mainBundle] loadNibNamed:#"FormulaKeyboard" owner:self options:nil];

It seems like your view does not have a property called 'inputView', hence when you set the property on self, you get the exception.

Related

Custom view created with Interface Builder does not render when called in other views

I have an xib for the main window, and I created a custom view in the following steps:
Create a new class which inherits from NSView.
MyView.h:
#import <Cocoa/Cocoa.h>
IB_DESIGNABLE
#interface MyView : NSTableCellView
#end
MyView.m:
#import "MyView.h"
#implementation MyView
- (void)awakeFromNib {
NSLog(#"Post view awaking from nib.");
}
#end
Create a new xib, and set the root view's class to the class created above. And design in that xib.
Set outlets from the xib to the class.
And I tried to use this custom view in the main window in the following steps:
Drag a custom view to the main window's xib.
Set the class of that custom view to the class created above.
But nothing renders. From the log, I can see that code in awakeFromNib from the custom view class is executed. When I set the class to be IB_DESIGNABLE, the view gets empty in the main window's xib, different from what I designed.
I tried to set the file owner of the custom view's xib to the custom class, but nothing changed.
I guess the problem is that, the custom view's xib file is not actually loaded. When I googled it, there seem to be few references on this exact topic. So, how should I actually achieve this goal? I.e., design a view in IB, implement its methods in a class, associate these two, and expose it just like a system view for use in other xibs?
UPDATE:
I found a tutorial and realized what I lack (for correctly rendering the view when built). I have to add an outlet from the view in the xib to the view class:
#property (nonatomic, strong) IBOutlet NSView *view;
, and then load it in the (id)initWithCoder:(NSCoder *)coder method.
[[NSBundle mainBundle] loadNibNamed:#"MyView" owner:self topLevelObjects:nil];
[self addSubview:self.view];
But the view still won't render in the interface builder.
Your guess is correct: the xib is not being loaded. The nib loader doesn't know about your custom view's nib. The nib framework doesn't provide a facility for defining that connection, so you need to write code to load the xib.
Here's what I'd do. Add a contentView property to your custom view:
#interface MyView ()
#property (nonatomic, strong, readwrite) IBOutlet NSView *contentView;
#end
In your custom view's nib, set the custom class of the root view back to NSView and disconnect all the (no-longer-valid) outlet connections from it. Set the custom class of File's Owner to your custom class name (e.g. MyView). Connect the root view to File's Owner's contentView outlet, and connect all the other outlets from File's Owner to the appropriate objects in the nib.
Then implement awakeFromNib in your custom view subclass to load the nib and add the content view as a subview:
#implementation MyView {
BOOL hasLoadedOwnNib: 1;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self loadOwnNibIfNeeded];
}
- (void)loadOwnNibIfNeeded {
if (hasLoadedOwnNib) {
return;
}
hasLoadedOwnNib = YES;
[[NSBundle bundleForClass:self.class] loadNibNamed:NSStringFromClass(self.class) owner:self topLevelObjects:nil];
self.contentView.frame = self.bounds;
self.contentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[self addSubview:self.contentView];
}
#end
Note that you have to be careful not to allow infinite recursion. When your app loads the main window's nib, it will create an instance of MyView and (eventually) send it awakeFromNib. Then, in awakeFromNib, MyView loads its own nib, where it is the File's Owner. The nib loader sends awakeFromNib to File's Owner, and this will happen while you're already in -[MyView awakeFromNib]. If you don't check for this, you get a stack overflow due to unbounded recursion.
You aren't providing any code, but here are some sanity checks:
Are you specifying the nib name correctly? In iOS its caps sensitive, but I don't think it is for you.
Check the package, is the nib actually there? Make sure it is part of the target you are building.
Could also be a frame issue. Make sure your auto-resizing parameters are set up correctly and that everything is in frame.
Another check you can do is set your IBOutlets to the actual frame of a UIView (or other) that you are interested in. Then in awakeFromNib, you can make sure their frame exists, or that they exist at all.

Unable to set content in NSPopover

I'm showing an NSPopover in an NSView, originating from a point on an NSBezierPath. I'm able to show the popover without a problem, but I can't seem to set the string value of the two text fields in it. The popover and the content view are both a custom subclass of NSPopover and NSViewController, respectively. The NSPopover subclass is also the NSPopover's delegate, although I don't implement any delegate methods, so I'm not sure I even need to do that.
Here is my subclass of NSViewController:
#import <Cocoa/Cocoa.h>
#interface WeightPopoverViewController : NSViewController
#end
#import "WeightPopoverViewController.h"
#interface WeightPopoverViewController ()
#end
#implementation WeightPopoverViewController
- (id)init {
self = [super initWithNibName:#"WeightPopoverViewController" bundle:nil];
if (self) {
}
return self;
}
#end
And my subclass of NSPopover:
#import <Cocoa/Cocoa.h>
#interface WeightPopoverController : NSPopover <NSPopoverDelegate> {
NSTextField *dateLabel;
NSTextField *weightLabel;
}
#property (strong) IBOutlet NSTextField *dateLabel;
#property (strong) IBOutlet NSTextField *weightLabel;
#end
#import "WeightPopoverController.h"
#implementation WeightPopoverController
#synthesize weightLabel;
#synthesize dateLabel;
#end
This is the code in my NSView subclass that opens up the popover:
#interface WeightGraphViewController () {
WeightPopoverController *popover;
WeightPopoverViewController *vc;
}
...
-(void)mouseEntered:(NSEvent *)theEvent {
// initialize the popover and its view controller
vc = [[WeightPopoverViewController alloc] init];
popover = [[WeightPopoverController alloc] init];
// configure popover
[popover setContentViewController:vc];
[popover setDelegate:popover];
[popover setAnimates:NO];
// set labels
for (id key in (id)[theEvent userData]) {
[popover.weightLabel setStringValue:[(NSDictionary*)[theEvent userData] objectForKey:key]];
[popover.dateLabel setStringValue:key];
}
// set the location
(redacted, irrelevant)
// show popover
[popover showRelativeToRect:rect ofView:[self window].contentView preferredEdge:NSMaxYEdge];
}
-(void)mouseExited:(NSEvent *)theEvent {
[popover close];
popover = nil;
}
In WeightPopoverViewController.xib, I've set the File's Owner to WeightPopoverViewController and connected the view to the custom NSView. In this xib I also have an Object set to WeightPopoverController with the dateLabel and weightLabel connected to their text fields and the contentViewController set to File's Owner.
I think where I am going wrong is likely related to how I have configured my class / instance variables for the NSPopover, but from the research I've done and documentation I've read I can't seem to crack where I've gone wrong. Any help would be appreciated.
UPDATE:
I removed the NSPopover subclass from code and from IB. I put my outlets in my NSViewController and connected them in IB. However, I'm still not able to set the string values. The following won't compile with the error "Property 'weightLabel' not found on object of type NSPopover*'".
#interface WeightGraphViewController () {
NSPopover *popover;
...
}
-(void)mouseEntered:(NSEvent *)theEvent {
vc = [[WeightPopoverViewController alloc] init];
popover = [[NSPopover alloc] init];
[popover setContentViewController:vc];
[popover.dateLabel setStringValue:#"test"];
}
I have the property definition exactly as I had it in my NSPopover subclass, but now in my NSViewController. This is actually what I had before, and since I wasn't able to set the properties from the NSViewController, I figured I needed to do it through a subclass of NSPopover. This is why I thought I am having an issue with how I have configured my class / instance variables.
You seem to be creating two popovers, one in code (popover = [[WeightPopoverController alloc] init]) and one in Interface Builder (In this xib I also have an Object set to WeightPopoverController). Have a think about what you’re trying to achieve.
I would also advise against subclassing NSPopover. I believe this is causing confusion and is unnecessary. Instead, put the outlets to your dateLabel and weightLabel in the popover’s content view controller.
I've experienced something that I think is similar. The root problem is that the "outlets" connecting your view (XIB) to your controller are not initialized until after the view has been displayed. If the controller tries to set properties on any UI controls in the view before the popover has been opened, those changes are ignored (since all the controls will be nil).
Luckily, there's an easy solution (as mentioned in this answer): just invoke the view getter on your controller, and it will force the view to initialize sooner.
In other words:
popover = [NSPopover new];
myController = [[MyViewController alloc] initWithNibName:#"MyView" bundle:nil];
popover.contentViewController = myController;
[myController view]; // force view to initialize
...set some values on myController... // works because view is now loaded
[popover showRelativeToRect: ...];

Interface Builder: How to load view from nib file

I have a MyCustomView subclassed from NSView designed in a .xib.
I would like to insert this view into some of my other xib's round my application. How should I do this? If i drag a custom view and change the class to MyCustomView, but that does not load my xib-file. Can this only be done programmatically or is there a way to do this inside interface builder?
EDIT1:
Here is a very small demo-project:
http://s000.tinyupload.com/index.php?file_id=09538344018446482999
It contains the default MainMenu xib and my CustomView xib. I would like my CustomView.xib to be displayed inside the custom view added to my MainMenu.xib -- using as less code as possible.
For loading the view you need to add on your window:-
Created custom class of view inheriting to NSViewController
#import <Cocoa/Cocoa.h>
#interface NewViewController : NSViewController
#end
#import "NewViewController.h"
#implementation NewViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Initialization code here.
}
return self;
}
#end
Your xib name is yourview.xib
- (void)windowDidLoad {
NSViewController *yourVC = [[NewViewController alloc] initWithNibName:#"NewViewController" bundle:nil];
[[[self window] contentView] addSubview:[yourVC view]];
}
Sounds like you need a container view. But I think you will have to use storyboard for it to be doable in interface builder.
Use a view controller as it will handle nib loading for you and provide a place to hook up IBOutlet and IBActions in a reusable way.
In your app delegate or whatever controller create an instance of your view controller.
Ask your view controller to load its view.
Cast the return type to your view class name.
Then keep a reference to your view controller and possibly the view.
Tell whatever view to add your view as a subview.
Add any layout constraints.
( you can build out very generic constraints to add themselves in your view or view controller by overriding viewDidMoveToSuperview or viewDidMoveToWindow when superview or window are not nil. Use the same to remove your constraints. )
Oddly you remove a view by telling it to remove itself from its superview.
I'd advise just doing it programmatically:
Add a View to your main xib/storyboard and set the custom class to your custom view's class
In your xib for your custom view, set the File's Owner class to your custom view's class
Hook up any IBOutlets, etc. as needed
Make a __strong property/ivar for holding a reference to the top level NSView of the xib
Implement initFromFrame in your custom view's class roughly as follows:
#interface CustomView ()
{
__strong NSView *nibView;
}
#end
#implementation CustomView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSArray *nibObjects;
[[NSBundle mainBundle] loadNibNamed:#"CustomView" owner:self topLevelObjects:&nibObjects];
nibView = nibObjects[1];
[self addSubview:nibView];
}
return self;
}
The IBOutlet are connected up immediately after the loadNibNamed call, so you can do further initialization from there.
Another option is to do things purely programmatically:
1. In your custom xib, set the root View's class to your custom class
2. Implement awakeFromNib in your custom class to perform initialization
3. Call loadNibNamed: on your custom xib and programmatically add it to the user interface without interface builder.

property loses value in IBAction

I am setting the parent property for a window and when I check the property in windowDidLoad, everything is good. But when I check it in IBAction, it is nil. What am I missing here?
AppController.m
loginController = [[LoginController alloc] initWithWindowNibName:#"Login" owner:self];
loginController.parent = self;
[[loginController window] makeKeyAndOrderFront:self];
LoginController.h
#property (nonatomic, weak) AppController *parent;
LoginController.m
#synthesize parent;
- (void)windowDidLoad
{
[super windowDidLoad];
NSLog(#"Parent: %#", self.parent); //<--- Parent: <AppController: 0xblahblah>
}
- (IBAction)login:(id)sender
{
NSLog(#"Parent: %#", self.parent); //<--- nil
}
I think that your problem is just that you have set the wrong file owner, is login controller that holds the xib file, thus it has all the IBOutlets and IBActions bound.
Call initWithWindowNibName: instead of initWithWindowNibName:owner: , so that the file owner will be the newly created login controller, and not the app controller:
loginController = [[LoginController alloc] initWithWindowNibName:#"Login"];
Edit
Like I suspected, you have two separate instances of login controller and you think to have only one. See the xib file:
That object "Login Controller" in the xib file, creates another instance of login controller. It's not the same instance that you allocate in app controller.
The solution is to make parent be an IBOutlet:
#property (nonatomic, weak) IBOutlet AppController *parent;
And to don't allocate it in app controller, it will be load from the xib file automatically. All you have to do is to bind it to the instance of login controller in the xib file (if the file owner is app controller you should ctrl-drag the parent propert to the object icon, tell me if you meet some problem doing it). So that's why it prints null: the action is handled by another object, which hasn't the parent property initialized.
In viewDidLoad you access an iVar called parent.
In login: you access the property self.parent.
That can be the same or it can be different.
Did you #synthesize the property? If so, without any additional options? Then you should be fine. parent and self.parent should be the same thing.
Did you autosynthesize it? That is ok. But then the corresponding iVar is called _parent. Meaning then you must have an additional ivar parent which is not the same ivar.
Add all the declarations to your question for more details.

iOS Child Views, SRP and custom events

I'm kind of new to iOS development and need some advice. I have a chat like app. The UI should have a child view for posting new messages to the server and one child view for viewing messages in a table view.
I've build both child views in Interface Builder as XIB:s. But I'm not sure how to use these on the main view controller. Can I use IB to add my custom views to the design surface? Or do I need to add these programmatically?
What is the best way to send messages or custom events between these two child views? I'd like to have them as decoupled as possible. Mostly I'd like to send an event when the user log on or off, so that the UI can react to these changes. I'd also like the table view with messages to know when a new messages is posted from the write view.
// Johan
In order to get the contents of a xib file you've got to load it first sending loadNibNamed:owner:options: message to NSBundle class.
Consider you have a UIView subclass named CustomView and CustomView.xib file. In the xib file each view has a tag. Your .h file would look like:
#interface CustomView : UIView
#property (nonatomic, assign) UILabel *someTextLabel; //use assign in order to not to override dealloc method
#end
.m
#implementation CustomView
- (id)init {
self = [super init];
if (self) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomView" owner:nil options:nil];
[self addSubview:[topLevelObjects objectAtIndex:0]]; //this object is a CustomView.xib view
self.someTextLabel = (UILabel *)[self viewWithTag:5]; //consider you have a UILabel on CustomView.xib that has its tag set to 5
}
return self;
}
#end
This is about how to use .xibs for your custom UIView subclasses. If your app is like a chat then you'll have to add them programmatically.
As for the best way to send messages between two custom views, you'll have to create a weak reference to each other in each of them.
in one
#property (nonatomic, assign) CustomView *customView;
in another
#property (nonatomic, assign) AnotherCustomView *anotherCustomView;
and just send them messages when some even happens
- (void)buttonPressed {
[customView handleButtonPressedEvent];
}
Let me know if this is clear.