iOS Controllers for universal (iPhone/iPad) app - ios

I am just working on iOS app and I want to make it universal for both iPhones and iPads. This is done and works without any problems:
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
self.viewController_iPhone = [[ViewController_iPhone alloc] initWithNibName:#"ViewController_iPhone" bundle:nil];
} else {
self.viewController_iPad = [[ViewController_iPad alloc] initWithNibName:#"ViewController_iPad" bundle:nil];
}
if (self.viewController_iPhone == nil)
self.window.rootViewController = self.viewController_iPad;
else
self.window.rootViewController = self.viewController_iPhone;
There is a view for each controller (ViewController_iPad.xib, ViewController_iPhone.xib). It doesn't matter which view is loaded in my problem. In a view there is a subview added (UIScrolView). And in this ScrollView there are two views from xib:
NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:#"SubView1" owner:self options:nil];
UIView *view = [nibContents objectAtIndex:0];
view.frame = CGRectMake(2, 0, scrollView.frame.size.width - 2, scrollView.frame.size.height);
[scrollView addSubview:view];
nibContents = [[NSBundle mainBundle] loadNibNamed:#"SubView2" owner:self options:nil];
view = [nibContents objectAtIndex:0];
view.frame = CGRectMake(scrollView.frame.size.width + 2 , 0, scrollView.frame.size.width - 4, scrollView.frame.size.height);
[scrollView addSubview:view];
(This code is in iPad/iPhone controller). Still everything is OK. But I don't know how to set owners (in IB) of these subviews that are shown in ScrollView. These subviews are in ScrollView which is in a main view so I want to set owners of these subviews as iPad/iPhone controller. But as a owner can be only one class. Can you tell me how to set owners if I have two main controllers and I don't know which one will be loaded in runtime. Thank you.
EDIT: I have another question: I have ViewController_iPhone. It has a View property and this property is assigned to the "root" view in the main view in ViewController_iPhone (.xib). Can I assign this view property also to subview view? Because I got EXC_BAD_ACCESS error if I assign view property of ViewController_iPhone to a "root" view of subview in IB.

Looks like you need to use a class cluster. This will abstract the iPhone/iPad instantiation, so you don't explicitly need to instantiate one of the two.
You can read a bit about class clustering in the Apple documentation:
http://developer.apple.com/library/ios/#documentation/general/Conceptual/DevPedia-CocoaCore/ClassCluster.html
It boils down to creating a master view controller which will handle the allocation of iPhone or iPad subclasses based on the current device.
You should override the ViewController alloc class method:
+ (id)alloc {
NSString *classString = NSStringFromClass([self class]);
NSString *append = nil;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
append = #"_iPhone";
} else {
append = #"_iPad";
}
NSString *subClassString = [classString stringByAppendingString:append];
id subClass = NSClassFromString(subClassString);
id object;
if (subClass && ![self isKindOfClass:[subClass class]]) {
object = [subClass alloc];
} else {
object = [super alloc];
}
return object;
}
This way you can just allocate the ViewController class and at runtime the correct class definition will be used to instantiate your view controller.
This will allow you to use the ViewController class as the owner in IB provided that you create an abstraction of iPhone and iPad interfaces and define it in their super class.

Related

iPhone - Create custom small view for all view controller in storyboard

A custom view for displaying on tapping map pin should be created using storyboard.
It may possible to create custom view outside of View controller inside a scene.
But creating a single custom view should be used on all other view controllers which are using map control.
How to create a custom view(width:300, height:150) on storyboard and that created view can be used on other view controller also?
Please advice...
Just build UIView in XIB I guess, create base class whit accessing awakes this view from XIB and adding it as a subview.
to load view from XIB you can use UIView category (name XIB same as classname):
#implementation UIView (NIB)
+ (id)loadFromNIBWithName:(NSString *)aName {
Class klass = [self class];
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:aName owner:self options:nil];
for (id object in objects) {
if ([object isKindOfClass:klass]) {
return object;
}
}
[NSException raise:#"WrongNibFormat" format:#"Nib for '%#' must contain one UIView, and its class must be '%#'", aName, NSStringFromClass(klass)];
return nil;
}
+ (UIView *)loadFromNIBWithName:(NSString *)aName frame:(CGRect)aFrame {
UIView *v = (UIView *)[self loadFromNIBWithName:aName];
v.frame = aFrame;
return v;
}
+ (id)loadFromNIB {
return [self loadFromNIBWithName:[self nibName]];
}
+ (UIView *)loadFromNIBWithFrame:(CGRect)aFrame {
UIView *v = (UIView *)[self loadFromNIB];
v.frame = aFrame;
return v;
}
+ (NSString*)nibName {
return NSStringFromClass([self class]);
}
#end
one more solution you can create UIViewController with your view, create custom segue to it, override - (void)perform method to take view from destinationViewController and put in on sourceViewController.view

Delegate reference disappears on modal Popup

This is convoluted, so I will do my best to give as much info as possible. My main UIViewController opens a modal popup in the form of an Info screen.
here is the call from MainViewController
infoPopup = [ModalPopup modalPopupWithDelegate:self];
[infoPopup presentInView:self.view.window];
[self.view addSubview:infoPopup];
and here is the receiving method in ModalPopup
+ (id)modalPopupWithDelegate:(id <ModalPopupDelegate>)dlg {
ModalPopup *info = [[ModalPopup alloc] init];
info.delegate = dlg;
return info;
}
In ModalPopup I create a protocol with an optional method of "modalPopupFinished" and make MainViewController the delegate.
in ModalPopup I've add a UIScrollView and insert 5 UIViews into the scrollview.
I created the views all in the same XIB file
NSString *infoXib;
if (IS_IPAD)
infoXib = #"info_iPad";
else
infoXib = #"info_iPhone";
NSArray *views;
views = [[NSBundle mainBundle] loadNibNamed:infoXib owner:self options:nil];
UIView *v1 = [views objectAtIndex:0];
views = [[NSBundle mainBundle] loadNibNamed:infoXib owner:self options:nil];
UIView *v2 = [views objectAtIndex:1];
views = [[NSBundle mainBundle] loadNibNamed:infoXib owner:self options:nil];
UIView *v3 = [views objectAtIndex:2];
views = [[NSBundle mainBundle] loadNibNamed:infoXib owner:self options:nil];
UIView *v4 = [views objectAtIndex:3];
views = [[NSBundle mainBundle] loadNibNamed:infoXib owner:self options:nil];
UIView *v5 = [views objectAtIndex:4];
NSArray *pages = [NSArray arrayWithObjects:v1, v2, v3, v4, v5, nil];
[self setPagesInArray:pages];
- (void)setPagesInArray:(NSArray *)pages {
if (pages) {
int numberOfPages = pages.count;
[pageScroll setContentSize:CGSizeMake(pageScroll.frame.size.width * numberOfPages, pageHeight)];
pageControl.numberOfPages = numberOfPages;
NSUInteger i = 0;
while (i < numberOfPages) {
UIView *page = [pages objectAtIndex:i];
page.frame = CGRectMake(pageWidth * i, 0, pageWidth, pageHeight);
[pageScroll addSubview:page];
i++;
}
}
}
the Views load fine in the scrollview and I can scroll through them all as expected.
One of the views in the XIB as some buttons and I've made the view a member of the Custom Class ModalPopup. I've wired the buttons to some IBActions in ModalPopup, and they fire as expected.
In ModalPopup I create a close button that fires the delegates "modalPopupFinished" event on MainViewController.
- (void)finishCloseAnimation {
if ([_delegate respondsToSelector:#selector(modalPopupFinished)])
[_delegate modalPopupFinished];
[self removeFromSuperview];
}
No Problems everything works great...except
when I press one of the buttons from the View and goto fire a delegate method, it has lost its brain and can't remember the delegate
- (IBAction)facebook:(id)sender {
if ([_delegate respondsToSelector:#selector(sendLikeFacebook)])
[_delegate sendLikeFacebook];
}
In fact when I get into the Facebook method on ModalPopup everything is nil.
The method that I am trying to fire in MainViewController opens
[self presentViewController:composeController animated:YES completion:nil];
thus I want to open it from the main page.
It sounds to me like you have 2 instances of ModalPopup. Since you say that you wired your buttons to ModalPopup in IB, this leads me to believe you are creating an instance of ModalPopup in the xib which is distinct from the ModalPopup that you have created programmatically. The fact that the ModalPopup that is called when the button is pressed is "completely empty" is further evidence.
If this is the case, instead of programmatically creating the ModalPopup, you should get the instance from the xib, just like you get the views. I would also suspect that you already added the views to the ModalPopup in the xib which means you do not have to manually add them as well.

ObjC: ARC releasing dynamically created view controller too early

I have a view controller, named AllThingsViewController that dynamically creates other view controllers, named ThingViewController, and adds their top level view to a UIScrollView. (I'm writing proprietary code so I've changed the names of my classes, but the structure of my code is exactly the same.)
Here's what its loadView method contains:
NSArray *things = [[ThingDataController shared] getThings];
if ([things count] == 0) {
// code in this block is not relevant as it's not being executed...
} else {
for(unsigned int i = 0; i < [things count]; ++i) {
ThingViewController *thingViewController = [[ThingViewController alloc] init];
[thingViewController loadView];
[scrollView addSubview:thingViewController.topView];
thingViewController.topView.frame = CGRectNewOrigin(thingViewController.topView.frame,
0, thingViewController.topView.frame.size.height*i);
[thingViewController displayThing:thing[i]];
}
}
ThingViewController's loadView method looks like this:
- (void)loadView
{
NSArray *topLevelObjs = nil;
topLevelObjs = [[NSBundle mainBundle] loadNibNamed:#"ThingView" owner:self options:nil];
if (topLevelObjs == nil)
{
NSLog(#"Error: Could not load ThingView xib\n");
return;
}
}
When my app starts up everything displays correctly, until I try to tap one of the buttons that exists in the xib being loaded by ThingViewController, at which point it crashes due to an exception: "unrecognized selector sent to instance". It seems that ARC is releasing my ThingViewController instances too early.
Looking at my code, I figured it was because they weren't being held on to anything, so I created an NSMutableArray as an instance variable in my AllThingsViewController class, and started adding the ThingViewControllers to it thusly:
NSArray *things = [[ThingDataController shared] getThings];
if ([things count] == 0) {
// not being executed...
} else {
for(unsigned int i = 0; i < [things count]; ++i) {
ThingViewController *thingViewController = [[ThingViewController alloc] init];
[thingViewController loadView];
[scrollView addSubview:thingViewController.topView];
thingViewController.topView.frame = CGRectNewOrigin(thingViewController.topView.frame,
0, thingViewController.topView.frame.size.height*i);
[thingViewController displayThing:thing[i]];
[allThingsViewControllers addObject:thingViewController];
}
}
However, it didn't change anything, even though those objects are being added to the array. Finally, just to confirm that this is ARC releasing it early, I changed "thingViewController" to be an instance variable in AllThingsViewController and changed:
ThingViewController *thingViewController = [[ThingViewController alloc] init];
to be:
thingViewController = [[ThingViewController alloc] init];
Sure enough, the last item in the scrollable list doesn't crash when I tap its buttons, but the other ones do, because its ThingViewController isn't being deallocated.
I'm still relatively new to ARC, but after a bunch of Googling I have no idea how to fix this. What do I do?
Couple of things.
Problem 1:
This looks like the cause of your bug:
[allBillViewControllers addObject:billViewController];
It should be:
[allBillViewControllers addObject:thingViewController];
Right?
Problem 2
You are not properly adding the view controller to your view hierarchy. It should be this:
[self addChildViewController:childViewController];
[childViewController.view setFrame:targetFrame];
[scrollView addSubview:childViewController.view];
[childViewController didMoveToParentViewController:self];
And similarly when removing a child view controller:
[childViewController willMoveToParentViewController:nil];
[childViewController.view removeFromSuperview];
[childViewController removeFromParentViewController];
Problem 3
Never call loadView explicitly on a view controller. It gets called by UIKit whenever you access the view property of a view controller.
Problem 4
You must add the view of the child view controller to your scroll view, not an arbitrary subview topView in its view hierarchy. Refactor your ThingViewController class to make this simpler for yourself. :-)
Let's look at your code:
for(unsigned int i = 0; i < [things count]; ++i) {
ThingViewController *thingViewController = [[ThingViewController alloc] init];
[thingViewController loadView];
[scrollView addSubview:thingViewController.topView];
thingViewController.topView.frame = CGRectNewOrigin(thingViewController.topView.frame,
0, thingViewController.topView.frame.size.height*i);
[billViewController displayThing:thing[i]];
[allBillViewControllers addObject:billViewController];
}
After each loop of the for loop executes, nothing will have a strong reference to the ThingViewController. Thus, it gets released and destroyed.
If ThingViewController is a subclass of UIViewController, then it should be made a "child view controller" of the scrollview's view controller. I recommend reading the section on from the View Controller Programming Guide on creating custom container view controllers (i.e., a view controller that encapsulates and displays other view controllers).

ios multiple views with a scroll view deallocated instance

I am have an issue with my scrollview which holds multiple views. I think the problem is that subviews are being released. I have buttons in the subviews and when I click the buttons I get this error, [GraphDisplayViewController performSelector:withObject:withObject:]:
message sent to deallocated instance
. If there is only one subview then I can just use a property and this works, but since the number of subviews varies(one or more), it does not work and I don't know how to solve this.
I currently load all the views at once in the beginning. I'm working on only loading one subview at a time and assigning the property to that view, but I'm not sure if that will work.
My layout is as follows, a parent view(DetailViewController) contains a scrollview, I add views(GraphDisplayViewController) to the scrollview, the subviews each load a view(GraphView).
Any help or advice would be greatly appreciated. If you need any more details please let me know. Thank you for your time.
Code sample of how I add the subviews,
DetailViewController
- (void)loadScrollViewWithPage:(int)page
{
if (page < 0) return;
if (page >= pageControl.numberOfPages) return;
subView = [viewControllers objectAtIndex:page];
NSString *description;
NSString *packsize;
if ((NSNull *)subView == [NSNull null])
{
subView = [[GraphDisplayViewController alloc] initWithNibName:#"GraphDisplayViewController" bundle:nil];
[viewControllers replaceObjectAtIndex:page withObject:subView];
subView = [[GraphDisplayViewController alloc] init];
subView.molecule = moleculeName;
subView.description = description;
subView.dataArray = moleculePrices;
}
else
{
return;
}
// add the controller's view to the scroll view
if (nil == subView.view.superview)
{
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
frame.origin.y = 0;
subView.view.frame = frame;
[scrollView addSubview:subView.view];
}
}
UPDATE
There was a mistake in the code, testing to see if that solves anything
subView = [[GraphDisplayViewController alloc] initWithNibName:#"GraphDisplayViewController" bundle:nil];
[viewControllers replaceObjectAtIndex:page withObject:subView];
subView = [[GraphDisplayViewController alloc] init]; <- Mistake
you are not retaining the GraphDisplayViewController assigned to subView variable. and hence you are loosing it at some point of time.
as we can see you are fetching the subView object like subView = [viewControllers objectAtIndex:page];
then you should also store it in viewControllers (though i am not sure what logic you have implemented but in ur code for each allocated subView this must be executed : [viewControllers replaceObjectAtIndex:page withObject:subView]; and you need to be sure that viewControllers variable is also retained for everything to work smoothly.) array so that it can be retained to avoid the crash you are facing.
I hope this will work for you..best of luck
There was a simple error in my code, I init the view twice.
subView = [[GraphDisplayViewController alloc] initWithNibName:#"GraphDisplayViewController" bundle:nil];
subView = [[GraphDisplayViewController alloc] init]; <- Mistake

Loading UIView from a nib file without guesswork

Ok, here's another question.
I am creating a UIView called ProgressView that is a semi-transparent view with an activity indicator and a progress bar.
I want to be able to use this view throughout different view controllers in my app, when required.
I know of 3 different ways of doing this (but I am only interested in one):
1) Create the entire view programatically, instantiate and configure as required. No worries I get that one.
2) Create the UIView in interface builder, add the required objects and load it using a method like the below. Problem with this is that we are basically guessing that the view is the objectAtIndex:0 because nowhere in the documentation I found a reference to the order of the elements returned from the [[NSBundle mainBundle] loadNibName: function.
NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:#"yournib"
owner:self
options:nil];
UIView *myView = [nibContents objectAtIndex:0];
myView.frame = CGRectMake(0,0,300,400); //or whatever coordinates you need
[scrollview addSubview:myView];
3) Subclass UIViewController and let it manage the view as per normal. In this case I would never be actually pushing the view controller onto the stack, but only its main view:
ProgressViewController *vc = [[ProgressViewController alloc] initWithNibName:#"ProgressView" bundle:nil];
[vc.view setCenter:CGPointMake(self.view.center.x, self.view.center.y)];
[self.view addSubview:vc.view];
[vc release];
As far as I can tell, #3 is the the correct way of doing this (apart from programatically) but I am not entirely sure if it is safe to release the ProgressView's view controller whilst another controller's view is retaining its main view (gut feel says it is going to leak?)?
What do I do in terms of memory management in this case, where and when should I release the ProgressView's view controller?
Thanks in advance for your thoughts.
Cheers,
Rog
I think that your solution #3 adds unnecessary complexity by introducing a UIViewController instance just as a container for your ProgressView so that you can setup nib bindings. While I do think that it is nice to be able to work with an IBOutlet bound property rather than iterating through the nib contents you can do so without introducing a UIViewController whose behavior you neither need nor want. This should avoid your confusion around how and when to release the view controller and what, if any, side effects it might have on the responder chain or other behaviors of the loaded view.
Instead please reconsider using NSBundle and taking advantage of the power of that owner argument.
#interface ProgressViewContainer : NSObject {
}
#property (nonatomic, retain) IBOutlet ProgressView *progressView;
#end
#implementation ProgressViewContainer
#synthesize progressView = progressView;
- (void) dealloc {
[progressView release];
[super dealloc];
}
#end
#interface ProgressView : UIView {
}
+ (ProgressView *) newProgressView;
#end
#implementation ProgressView
+ (ProgressView *) newProgressView {
ProgressViewContainer *container = [[ProgressViewContainer alloc] init];
[[NSBundle mainBundle] loadNibNamed:#"ProgressView" owner:container options:nil];
ProgressView *progressView = [container.progressView retain];
[container release];
return progressView;
}
#end
Create a nib named "ProgressView" containing a ProgressView and set it's File's Owner class to ProgressViewContainer. Now you can create ProgressViews loaded from your nib.
ProgressView *progressView = [ProgressView newProgressView];
[scrollView addSubview:progressView];
[progressView release];
If you have multiple configurations of your progress view then maybe you'll want to implement a -initWithNibNamed: method on ProgressView instead of +newProgressView so you can specify which nib to use to create each ProgressView instance.
I vote for option #2. The return value from -[NSBundle loadNibNamed] is an array of the top-level objects. So as long as you have just one top level object in your nib, then the index 0 will be correct. The other views are subviews and not top level objects.
Another option of course is to do something like create a superclass for all of your view controllers that includes an outlet called something like 'progressView' and then connect your view to that outlet on file's owner in the nib. Seems like overkill for this, though.
I also prefer alternative #2. If the "0" is bothering you, you could:
Create a subclass of UIView called ProgressView
Create a nib-file called ProgressView.xib describing your progress view.
Select the topmost view in your nib, and set its Class to ProgressView in interface builder
then do
NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:#"ProgressView" owner:self options:nil];
ProgressView *progressView = nil;
for (UIView *view in nibContents) {
if ([view isKindOfClass:[ProgressView class]]) {
progressView = (ProgressView *) view;
break;
}
}
if (progressView != nil) {
//Use progressView here
}
I ended up adding a category to UIView for this:
#import "UIViewNibLoading.h"
#implementation UIView (UIViewNibLoading)
+ (id) loadNibNamed:(NSString *) nibName {
return [UIView loadNibNamed:nibName fromBundle:[NSBundle mainBundle] retainingObjectWithTag:1];
}
+ (id) loadNibNamed:(NSString *) nibName fromBundle:(NSBundle *) bundle retainingObjectWithTag:(NSUInteger) tag {
NSArray * nib = [bundle loadNibNamed:nibName owner:nil options:nil];
if(!nib) return nil;
UIView * target = nil;
for(UIView * view in nib) {
if(view.tag == tag) {
target = [view retain];
break;
}
}
if(target && [target respondsToSelector:#selector(viewDidLoad)]) {
[target performSelector:#selector(viewDidLoad)];
}
return [target autorelease];
}
#end
explanation here: http://gngrwzrd.com/blog-view-controller-less-view-loading-ios-mac.html

Resources