Container View Controllers pre iOS 5 - objective-c

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];

Related

Correct method to present a different NSViewController in NSWindow

I am developing an app that is a single NSWindow and clicking a button inside the window will present a NSViewController, and a button exists in that controller that will present a different NSViewController. I know how to swap out views in the window, but I ran into an issue trying to do this with the multiple view controllers. I have resolved the issue, but I don't believe I am accomplishing this behavior in an appropriate way.
I originally defined a method in the AppDelegate:
- (void)displayViewcontroller:(NSViewController *)viewController {
BOOL ended = [self.window makeFirstResponder:self.window];
if (!ended) {
NSBeep();
return;
}
[self.box setContentView:viewController.view];
}
I set up a target/action for an NSButton to the AppDelegate, and here's where I call that method to show a new view controller:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self displayViewcontroller:newVC];
}
This does work - it presents the new view controller's view. However if I then click any button in that view that has a target/action set up that resides within its view controller class, the app instantly crashes.
To resolve this issue, I have to change didTapContinue: to the following:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self.viewControllers addObject:newVC];
[self displayViewcontroller:[self.viewControllers lastObject]];
}
First of all, can you explain why that resolves the issue? Seems to be related to the way the controller is "held onto" in memory but I'm not positive.
My question is, how do I set this up so that I can swap out views from within any view controller? I was planning on getting a reference to the AppDelegate and calling displayViewcontroller: with a new controller I just instantiated in that class, but this causes the crash. I need to first store it in the array then send that reference into the method. Is that a valid approach - make the viewControllers array public then call that method with the lastObject, or how should this be set up?
What is interesting in your code is that you alloc/init a new view controller every time that you call the IBAction. It can be that your view its totally new every time you call the IBAction method, but I would think that you only have a limited number of views you want to show. As far as my knowledge goes this makes your view only to live as long as your IBAction method is long. That the view still exists, is because you haven't refreshed it. However, calling a method inside a view controller that is not in the heap anymore (since you left the IBAction method and all local objects, such as your view controller are taken of the heap thans to ARC) makes the app crash, because you reference a memory space that is not in use or used by something else.
Why does the app work when you ad the view to the viewcontrollers array? I assume this array is an array that has been initiated in the AppDelegate and now you add the view controller with a strong reference count to the viewcontrollers array. When you leave the IBAction method, the view controller still has a strong reference and ARC will not deallocate the view controller.
Is this the proper way? Well, it works. I would not think it is considered very good programming, since you don't alloc/init an object in a method that needs to stay alive after leaving the method. It would be better practice to allocate and initialize your view controller(s) somewhere in an init, awakeFromNIB or a windowDidLoad method of your AppDelegate. The problem with your current solution is that you are creating an endless array of view controllers of which you only use the last. Somewhere your program will feel the burden of this enormously long array of pretty heavy objects (view controllers) and will run out of memory.
Hope this helps.
By the way, this is independent of whether you use Mavericks or Yosemite. I was thinking in a storyboard solution, but that wouldn't answer your question.
Kind regards,
MacUserT

Link XIB to a custom NSView in a Framework

I am writing a Cocoa Framework (not an Application), it contains the definition of a customized NSView; the framework will be loaded in other applications, and the customized NSView will be dragged to the GUI of the applications and become initialized
The question is that I want to include a XIB file in the Framework
I want to add a button and a label to the XIB (in the framework), but the view in the application that consumes the framework, won't show the button and the label
I already set the File's Owner of XIB to the custom NSView in the framework
What else should I do?
Have the view load the nib and move the button, label, etc., from the view in the nib to itself.
It'll be easiest to do this just by getting the subviews of the nib's view and doing this for all of them.
If your nib uses Auto Layout, I think you'll also need to bring across any constraints owned by the nib's view, and you may also need to edit or replace any constraints that refer to that view (e.g., if a view is set to be X points away from an edge of the nib's view).
You may also need to do extra work to change the new view's frame, or to change the frames of the subviews (whether by relocation, resizing, or both) to match the frame given for the new view.
Yeah, I just solved the issue, and I hope the details are helpful to others.
In the framework, there is a complicated subclass of NSView, which contains many controls such as NSSplitView NSOutlineView and IKImageBrowserView, NSPathControl and etc; the framework contains a H file, a M file and a XIB file; H and M define the MyView class; in the XIB, there is a View object whose class is MyView.
On the application side, users need to drag a NSView item onto the main window of their app, and assign an outlet to the view, let's say mainView, and in the applicationDidFinishLaunching function of the "consumer" app, the follow code is necessary
NSBundle *frameworkBundle = [NSBundle bundleForClass:[MyView class]];
NSNib *nibby = [[[NSNib alloc] initWithNibNamed:#"MyView" bundle:frameworkBundle] autorelease];
NSArray *topLevelObjects = nil;
BOOL flag = [nibby instantiateNibWithOwner:nil topLevelObjects:&topLevelObjects];
assert(flag);
for (id topLevelObject in topLevelObjects) {
if ([topLevelObject isKindOfClass:[MyView class]]) {
[mainView addSubview: topLevelObject];
MyView* xView = topLevelObject;
[xView setFrameSize:mainView.frame.size];
break;
}
}
In the code above, the XIB files is loaded, thus the MyView object is initialized, then we fetch it out of the XIB, resize it and add it to the main view of the window

View Controller behaves differently when set as 'initial view controller' vs. loading with presentModalViewController

My app has a map that tracks the user's location. This map will only appear under certain circumstances, and will dominate the user's attention until a particular task is complete, which is why the map isn't part of a navigation or tab bar UI.
If my map VC is set as the initial view controller in storyboard, it works fine. But if I try to load the map VC from elsewhere like this;
MapViewController *mapVC = [[MapViewController alloc] init];
[self presentModalViewController:mapVC animated:YES];
I just get a black screen.
I can confirm with NSLog that the VC is calling viewDidLoad and viewDidAppear, but the 'map' property of the VC is (null). I don't understand why (or how) I need to create the map property manually when using this technique, but it gets done for me when it is the initial VC.
The MapViewController instance in your storyboard is configured with a view hierarchy, including an MKMapView, and whatever else you did to configure that particular instance in the storyboard.
Now in this code which you show here, you are creating a completely new instance of MapViewController. It has no relationship to the instance in the storyboard other than they happen to be of the same class. So the one you create here with [[MapViewController alloc] init] has no view hierarchy (which is why you see a black screen), and none of the outlets or other configuration you may have made to the other MapViewController in your storyboard.
So what you want is to load that MapViewController that you've already set up from the storyboard. Assuming you are doing this from within a method in another view controller loaded from the same storyboard already, you can just do this:
// within some method on another vc from a scene in the same storyboard:
// given an identifier for the map view controller we want to load:
static NSString *mapVCIdentifier = #"SomeAppropriateIdentifier";
NSLog(#"Storyboard: %#",self.storyboard); // make sure this vc(self) was loaded from a storyboard
MapViewController *mapVC = [self.storyboard instantiateViewControllerWithIdentifier:mapVCIdentifier];
[self presentModalViewController:mapVC animated:YES];
And then back in the storyboard, just make sure you set the identifier for this map view controller to "SomeAppropriateIdentifier".
Hope that helps.

Reusable bits of interface, designed in IB

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.

iPhone subview design (UIView vs UIViewController)

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.