I'm making an app that includes the same group of buttons in many different contexts. The buttons send their actions to a different object in each context. I'd like to be able to design a single NSView in IB containing the buttons, and then be able to put copies of that view in many places in my nibs, while maintaining the link, so changes propagate. I'd like to connect each of those instances to different objects, and have the buttons send their actions to whatever object their parent view is connected to.
I thought of creating a subclass of NSView which, when loaded, replaces itself with another view which it loads from a nib file, setting the connected object as File's Owner, but I'm not convinced this is the cleanest method. Here's my implementation of that idea (which -does- work):
#implementation AVNViewFromNib
- (void)awakeFromNib
{
//Load the nib whose name is specified by the "nibFile" key
NSNib* viewNib = [[NSNib alloc] initWithNibNamed:[self valueForKey:#"nibFile"] bundle:[NSBundle mainBundle]];
NSMutableArray* topLevelObjects = [NSMutableArray new];
[viewNib instantiateNibWithOwner:relatedObject topLevelObjects:&topLevelObjects];
//Find our replacement view in that nib
for (id currentObject in topLevelObjects)
{
if ([currentObject isKindOfClass:NSClassFromString(#"AVNReplacementView")])
{
representedView = currentObject;
break;
}
}
//Copy appropriate properties from us to our representedView
[representedView setAutoresizingMask:[self autoresizingMask]];
[representedView setFrame:[self frame]];
[[self superview] addSubview:representedView];
//We were never here. :)
[self removeFromSuperview];
[viewNib autorelease];
}
#end
#implementation AVNReplacementView
#end
Is that the cleanest method? Is there a standard way of going about this?
You can create the view with the buttons in it in IB, then drag that view into the Library window and save it. The catch is, there's no “link” between them; editing one won't change anything about the others.
If you want that, you'll need to make a subclass of NSView instead.
I thought of creating a subclass of NSView which, when loaded, replaces itself with another view which it loads from a nib file, setting the connected object as File's Owner, but I'm not convinced this is the cleanest method.
That could work. I don't think that's really all that dirty; the reason init methods return an object is that they explicitly can return a different object. However, I'm not sure how you'd handle views of different frames, since the loaded view will have whatever frame it has in the nib.
Another way would be to load the buttons from a nib, but you'd have to adjust their frames before adding them as subviews.
Related
iOS 5 adds a nice feature allowing you to nest UIViewControllers. Using this pattern it was easy for me to create a custom alert view -- I created a semi-transparent view to darken the screen and a custom view with some widgets in it that I could interact with. I added the VC as a child of the VC in which I wanted it to display, then added its views as subviews and did a little animation to bring it on the screen.
Unfortunately, I need to support iOS 4.3. Can something like this be done, or do I have to manage my "alert" directly from the VC in which I want to display it?
MORE INFO
So if I create a custom view in a nib whose file owner is "TapView" and who has a child view that is a UIButton. I tie the UIButton action to a IBAction in TapView.
Now in my MainControllerView I simple add the TapView:
TapView *tapView = [[TapView alloc] init];
[[self view] addSubview:tapView];
I see my TapView, but I can't interact with the UIButton on it and can interact with a UIButton on the MainControllerView hidden behind it. For some reason I am not figuring out what I'm missing...
Not sure if this helps, but, in situations where I've needed more control over potential several controllers, I've implemented a pattern where I have a "master" controller object (doesn't need to be descendent from UIViewController), which implements a delegate protocol (declared separately in it's own file), and then have whatever other controllers I need to hook into declare an object of that type as a delegate, and the master can do whatever it needs to do in response to messages from the controllers with the delegate, at whatever point you need; in your case, that being displaying the alert and acting as it's delegate to handle the button selection. I find this approach to be very effective, simpler and usually cleaner. YMMV ;-)
Regd. your second query, where you are trying to create a custom view using nib. Don't change the FileOwner type, instead set "TapView" for the class property of the top level view object.
Once you have done this, you might experience difficulty when making connections. For that just manually choose the TapView file for making connections.
Also, to load the view you need to load its nib file. For which you can create a class level helper method in TapView class like below
+(TapView *) getInstance
{
NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:#"TapView" owner:self options:nil];
TapView *view;
for (id object in bundle) {
if ([object isKindOfClass:[TapView class]]) {
view = (TapView *) object;
break;
}
}
return view;
}
Now, you get a refrence to you view object like this
TapView *tapView = [TapView getInstance];
I have a UIViewController subclass that contains an instance of UIImagePickerController. Let's call this controller CameraController. Among other things, the CameraController manages the UIImagePickerController instance's overlayView, and other views, buttons, labels etc. that are displayed when the UIImagePickerController, let's call this instance photoPicker, is displayed as the modal controller.
The photoPicker's camera overlay and the elemets that are part of the CameraController view hierarchy display and function as expected. The problem I'm having is that I cannot use UIViewController's default initializer to create the CameraController's view heirarchy.
I am initializing CameraController from within another UIViewController. Let's call this controller the WebViewController. When the user clicks on a button in a view managed by WebViewController, the launchCamera method is called. It currently looks like this:
- (void) launchCamera{
if (!cameraController) {
cameraController = [[CameraController alloc] init];
// cameraController = [[CameraController alloc] initWithNibName:#"CameraController"
// bundle:[NSBundle mainBundle]];
cameraController.delegate = self;
}
[self presentModalViewController:cameraController.photoPicker animated:NO];
}
I want to be able to create CameraController by calling initWithNibName:bundle: but it's not working
as I'll explain.
CameraController's init method looks like this:
- (id) init {
if (self == [super init]) {
// Create and configure the image picker here...
// Load the UI elements for the camera overlay.
nibContents = [[NSBundle mainBundle] loadNibNamed:#"CameraController" owner:self options:nil];
[nibContents retain];
photoPicker.cameraOverlayView = overlay;
// More initialization code here...
}
return self;
}
The only way I can get the elements to load from the CameraController.xib file is to call loadNibNamed:owner:options:. Otherwise the camera takes over but no overlay nor other view components are displayed. It appears that a side-effect of this problem is that none of the view management methods on CameraController are ever called, like viewDidLoad, viewDidAppear etc.
However, all outlets defined in the nib seem to be working. For example, when the camera loads a view is displayed with some instructions for the user. On this view is a button to dismiss it. The button is declared in CameraController along with the method that is called that dismisses this instructions view. It is all wired together through the nib and works great. Furthermore, the button to take a picture is on the view that servers as photoPicker's overlay. This button and the method that is called when it's pressed is managed by CameraController and all wired up in the nib. It works fine too.
So what am I missing? Why can't I use UIViewController's default initializer to create the CameraController instance. And, why are none of CameraController's view mangement methods ever called.
Thanks.
Your problem is easy but need some steps.
Well... First, if overlay is an IBOutlet, it can not be loaded at init time. So move picker and co in viewDidLoad. Place also here all other items that your say that they are not loaded. They should be loaded there (viewDIDLoad). Check that outlets are connected.
Second, call
cameraController = [[CameraController alloc] initWithNibName:#"CameraController"
bundle:nil];
and ensure that CameraController contains (just) a view, and CameraController inherits UIViewController. Check also file's owner.
And at some time, you may consider that calling :
[self presentModalViewController:cameraController.photoPicker animated:NO];
does not make the CameraController control your picker. Does that make sense to you ?
What does that do regarding your problem ?
It seems you are confusing some things. I try to explain in another way :
The one that controls the picker is the one that is its delegate. Your may consider creating in a MAIN view.
The controller of the overlay (added as subview) is the one that own its view in File's Owner. That may be created from the MAIN view, adding its view as subview of the controller. Basically, it is loaded just to get the overlay, but viewDidLoad, ... won't be called.
That's all and I belive those steps are not ok in your code.
That should give something like :
MainController
Loadcamera {
self.picker = [UIImagePicker alloc] init.....];
self.picker.delegate = self;
SecondController* scnd = [[SecondController alloc] initWithNibName:#"SecondController" bundle:nil];
[self.picker addOverlay:scnd.view];
[self presentModalViewController:self.picker animated:NO];
}
/// And here manage your picker delegate methods
SecondController
// Here manage your IBActions and whatever you want for the overlay
Basically I want to use a nib file and view controller as a template for a view that I plan to create a number of times. This nib will have a couple of labels and custom views.
The idea is that I will iterate through an array of objects and for each I will create an instance of this controller and set a property to that of the object from the array.
This all works nicely at the moment except for one thing - the labels won't update when I call setStringValue: !!!
I'm using a method within the view controller's code to make the change but it just doesn't work, I'm guessing that the IBOutlet isn't being hooked up properly, which is strange because the custom views are hooking up perfectly.
Any ideas?
Set a breakpoint on awakeFromNib and look in the debugger what the value of the label outlet is. All outlets should have been connected before awakeFromNib is being called. If it is still nil, you have your answer. Calling setStringValue: on nil does exactly "nothing". In that case you have not correctly bound the outlet or maybe you once had it bound correctly and later on changed the name, in that case there should be a yellow warning triangle in Xcode4 or interface builder indicating that something is wrong; however it will not prevent your app from building or running, the outlet will simply keep its initial value after object creation (which is nil).
When you alloc your NSViewControllers, just init with name of NIB:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
Thanks for the replies, they were helpful but not quite what I was getting at.
I ended up solving it by creating an empty NIB and filling it with just a custom NSView and a few other controls. I created an NSView subclass with IBOutlets for those controls and set the custom view's identity to my subclass in interface builder.
The trick in getting it to work each time I wanted to draw it was by making a class method in my subclass that would load the nib and return the view set up the way I wanted.
Code below:
+(id)todoViewFromNibWithFrame:(NSRect)frameRect todoList:(TodoList *)aTodoList
{
NSNib *todoViewNib = [[NSNib alloc] initWithNibNamed:#"TodoView" bundle:nil];
NSArray *objects = nil;
id todoView = nil;
[todoViewNib instantiateNibWithOwner:nil topLevelObjects:&objects];
for (id object in objects) {
if ([object isKindOfClass:[self class]]) {
todoView = object;
[todoView setTodoList:aTodoList];
break;
}
}
[todoViewNib release];
return todoView;
}
Thanks again for the replies!
Steve
I'm designing a simple Quiz application. The application needs to display different types of QuizQuestions. Each type of QuizQuestion has a distinct behavior and UI.
The user interface will be something like this:
alt text http://dl.getdropbox.com/u/907284/Picture%201.png
I would like to be able to design each type of QuizQuestion in Interface Builder.
For example, a MultipleChoiceQuizQuestion would look like this:
alt text http://dl.getdropbox.com/u/907284/Picture%202.png
Originally, I planned to make the QuizQuestion class a UIViewController. However, I read in the Apple documentation that UIViewControllers should only be used to display an entire page.
Therefore, I made my QuizController (which manages the entire screen e.g. prev/next buttons) a UIViewController and my QuizQuestion class a subclass of UIView.
However, to load this UIView (created in IB), I must[1] do the following in my constructor:
//MultipleQuizQuestion.m
+(id)createInstance {
UIViewController *useless = [[UIViewController alloc] initWithNibName:#"MultipleQuizQuestion" bundle:nil];
UIView *view = [[useless.view retain] autorelease];
[useless release];
return view; // probably has a memory leak or something
}
This type of access does not seem to be standard or object-oriented. Is this type of code normal/acceptable? Or did I make a poor choice somewhere in my design?
Thankyou,
edit (for clarity): I'd like to have a separate class to control the multipleChoiceView...like a ViewController but apparently that's only for entire windows. Maybe I should make a MultipleChoiceViewManager (not controller!) and set the File's Owner to that instead?
You're on the right track. In your QuizController xib, you can create separate views by dragging them to the xib's main window rather than to the QuizController's main view. Then you can design each view you need according to your question types. When the user taps next or previous, remove the previous view and load the view you need based on your question type using -addSubview on the view controller's main view and keep track of which subview is currently showing. Trying something like this:
[currentView removeFromSuperView];
switch(questionType)
{
case kMultipleChoice:
[[self view] addSubview:multipleChoiceView];
currentView = multipleChoiceView;
break;
case kOpenEnded:
[[self view] addSubview:openEndedView];
currentView = openEndedView;
break;
// etc.
}
Where multipleChoice view and openEndedView are UIView outlets in your QuizController connected to the views you designed in IB. You may need to mess with the position of your view within the parent view before you add it to get it to display in the right place, but you can do this with calls to -setBounds/-setFrame and/or -setCenter on the UIView.
Yeah, IB on iPhone really wants File's Owner to be a UIViewController subclass, which makes what you want to a bit tricky. What you can do is load the nib against an existing UIViewController instead of instantiating one using the nib:
#implementation QuizController
- (void) loadCustomViewFromNib:(NSString *)viewNibName {
(void)[[NSBundle mainBundle] loadNibNamed:viewNibName owner:self options:nil];
}
#end
That will cause the runtime to load the nib, but rather than creating a new view controller to connect the actions and outlets it will use what you pass in as owner. Since we pass self in the view defined in that nib will be attached to whatever IBOutlet you have it assigned to after the call.
So I have an NSTabView that I'm dynamically resizing and populating with NSView subclasses. I would like to design the pages in IB and then instantiate them and add them to the NSTabView. I got the programmatic adding of NSView subclasses down, but I'm not sure how to design them in IB and then instantiate them.
I think I got it. Let me know if this is not a good thing to do.
I made a new xib file, set its File's Owner to be an NSViewController and set its "view" to the custom view I designed in the xib.
Then you just need:
NSViewController *viewController = [[NSViewController alloc] initWithNibName:#"MyViewXib" bundle:nil];
NSView *myView = [viewController view];
#toastie had a really good answer. Mine is similar, but requires a bit more explanation.
Let's say you've already got a controller object and you don't want to instantiate a new controller object just to get at a view, and let's say that you're going to need multiple copies of this view (for example, you've designed a custom UITableViewCell in IB and you want to instantiate it again and again from your UITableViewController). Here's how you would do that:
Add a new IBOutlet to your existing class called "specialView" (or something like that). It may also be helpful to declare it as a (nonatomic, retain) property.
Create a new view xib called "SpecialView", and build the view however you like.
Set the File's Owner of the view to be your controller object.
Set the specialView outlet of File's Owner to be the new view.
Whenever you need a new copy of the view in your code, you can simply do the following.
(gratuitous text to get formatting working properly)
NSNib * viewNib = [[NSNib alloc] initWithNibNamed:#"SpecialView" bundle:nil];
[viewNib instantiateNibWithOwner:self topLevelObjects:nil];
[viewNib release];
NSView * myInstantiatedSpecialView = [[[self specialView] retain] autorelease];
[self setSpecialView:nil];
Yes, it's a bit more code than other ways, but I prefer this method simply because the view shows up in the designated IBOutlet. I retain and autorelease the view, because I like to reset the outlet to nil once I have the view, so it can be immediately ready to load a new copy of the view. I'll also point out that the code for this is even shorter on the iPhone, which requires one line to load the view, and not 3 (as it does on the Mac). That line is simply:
[[NSBundle mainBundle] loadNibNamed:#"SpecialView" owner:self options:nil];
HTH!