UIView Subclass backed by xib with Size Classes wrong frame - cocoa-touch

I am working on a project for iOS 7.0+ with a storyboard, using Size Classes with AutoLayout and I'm using a UIView subclass backed by a xib file of the same name.
What I'am trying to do is I'am instantiating a UIView from xib programmatically and adding it to a ViewController from a Storyboard. This ViewController has AutoLayout up and running but the UIView I am adding doesn't respect the frame of the ViewController.
I'm instantiating my UIView subclass like this:
tabBarView = [[SHDTabBarView alloc] initWithFrame:CGRectMake(0, self.view.height-50, self.view.width, 50)];
[self.view addSubview:tabBarView];
And inside the subclass I'm using a set up of creating a UIView IBOutlet called container to instantiate it form code like this:
-(id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self == nil) return nil;
[self initalizeSubviews];
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self == nil) return nil;
[self initalizeSubviews];
return self;
}
-(void)initalizeSubviews{
NSString *nibName = NSStringFromClass([self class]);
UINib *nib = [UINib nibWithNibName:nibName bundle:nil];
[nib instantiateWithOwner:self options:nil];
//Add the view loaded from the nib into self.
[self addSubview:self.container];
}
This is how my xib looks in the Interface Builder (notice the width of the canvas is 320 px):
And that's how it looks on the iPhone 6 (notice how it's getting cut off from the right side):
I've tried to use a multitude of solutions, including doing it all in code with an open-source solution PureLayout, using a manual constraint set up, etc.
None of my findings seem to work right. Ideally, I want to set up everything in Interface Builder, then just add the view to the superview of the ViewController with according frame and let AutoLayout do its magic.
How should I approach this task? Any advices are more than welcome.

Try to set the frame of your subview in the viewDidLayoutSubviews(). Looks like you init your subview before view fully layouted

Related

Using a UICollectionView over an SKScene

I have an app with several SKScenes. To keep it snappy, I have a single UIViewController, that handles 1 SKView.
I'm trying to add a UICollectionView to one of the SKScenes. However the problem comes when I try to set the delegate for the collection view to the SKScene initialising it.
I initialise it here:
- (id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size]) {
//Initialise collectionView
UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height) collectionViewLayout:layout];
_collectionView.delegate = self;
_collectionView.dataSource = self;
[_collectionView setBackgroundColor:[UIColor redColor]];
}
return self;
}
After initialising, I add the collection view as a subview of the UIViewController's view by calling:
- (void)didMoveToView:(SKView *)view
{
[self.view addSubview:_collectionView];
}
But the delegate or datasource methods aren't called. I've set up the header:
#interface BrowseScene : SKScene <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
I'm guessing that despite setting self as the delegate, once the collection view is added, it is added as a subview of the UIViewController's SKView, and therefore has no reference to the SKScene that initialised it. I tried calling:
_collectionView.delegate = self.view.scene;
But I get a compiler error saying SKScene is an incompatible type for UICollectionViewDelegate.
So my question is, how best to approach this?
It turns out, the problem lay in how I was initialising the UICollectionView, not where. And it's perfectly ok to set an SKScene as the delegate class.
The main reason the delegates weren't being called was because I'd initialised the collectionView with an empty layout object (as UICollectionViewLayout is just an abstract class). I initialised with a standard UICollectionViewFlowLayout instead, and the methods were then called when the class initialised.
I was also missing a call to
[_collectionView registerClass:[collectionViewCell class] forCellWithReuseIdentifier:#"collectionViewCell"];
(In the complete code, I had calls to reloadData, so that wasn't the problem here).

Use xib without a view controller

I need to create several similar views
In a easy way,I create some views in xib(each full screen)
And I have a view controller to use this xib's views,code like this:
NSArray* views = [[NSBundle mainBundle] loadNibNamed:#"MyXibName" owner:nil options:nil];
[self.view addSubview:[views objectAtIndex:aIndex]];
At this moment,view shows alright.
Now,there's some buttons in those views,so I connect a outlet for each view
bad thing happened
app crashed due to
uncaught exception 'NSUnknownKeyException', reason: '[<NSObject 0x969db50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key
Analyse:
Although my xib file's "File Owner" has been set,but there's no connection between xib and the only view controller.
How could I get the pointer of a view's button?
You can do it like this:
NSNib* aNib = [[NSNib alloc] initWithNibNamed:#"MyGreatNib" bundle:nil];
NSArray* topLevelObjs = nil;
for (SomeClass *obj in myOwnerObjects) {
topLevelObjs = nil;
if (![aNib instantiateNibWithOwner:obj topLevelObjects:&topLevelObjs])
{
NSLog(#"Warning! Could not load nib file.\n");
return;
}
for (id topLevelObj in topLevelObjs) {
if ([topLevelObj isKindOfClass:[NSView class]]) {
NSView *otView = (NSView *)topLevelObj;
// set frame...
[self addSubview:otView];
}
}
}
Oops...
I just found something.
UINib* xib = [UINib nibWithNibName:#"MyXibName" bundle:nil];
UIView* view = [[xib instantiateWithOwner:self options:nil] lastObject];
It works!
You can design xib as per your need by defining class UIView
Code in .m file:
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:#"Interface" owner:nil options:nil];
UIView* mainView = [objects objectAtIndex:0];
for (UIView* view in [mainView subviews]) {
if([view isKindOfClass:[UILabel class]])
{
UILabel* label = (UILabel*)view;
//....As You Wish...
}
}
Default implementation of -loadView creates the view or loads NIB. As far as I know, there is no way to know the final size of the view at time of creation in -loadView. So the default view size is set to [[UIScreen mainScreen] bounds]. This is because it may be difficult to work work with zero frame view in -viewDidLoad and other methods.
Your one-line implementation may look like this:
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
}
You don't need to set the autoresizing mask, because you don't know in what context the view will be displayed. The caller is responsible to set you correct frame, autoresizing mask and similar external attributes (I call them like this).
Imagine this in a UINavigationController method:
// we are pushing new VC, view is accessed for the first time
pushedVC.view.frame = CGRectMake(...);
It is setting the correct frame, but your -loadView is called just before that -setFrame:. So during -viewDidLoad you have temporary non-zero frame, just to be able to setup subviews and internal autoresizing. After this, the correct frame is set to you and in -viewWillAppear: you have final frame.

Infinite loop when overriding initWithCoder

I have a UIViewController with some controllers and some views. Two of these views (Grid Cell) are other nibs. I've got outlets from the Grid Cells to File's Owner, but they aren't loaded automatically.
So I try to override GridCell.m's initWithCoder.
This starts an infinite loop.
I know it's possible to just override initWithFrame and add the subview from code, but this is not what I want. I want to be able to move the view around in Interface Builder and have Xcode initialize the view with the right frame.
How do I go about achieving this?
EDIT 1
I'm trying to get it working with the help of Alexander. This is how I've now got it set up:
MainView has UIView with a Custom class set as GridCell. It got an outlet in the MainView/File's Owner.
Removed all init-code from GridCell.m and set up an outlet to my custom class
The MainView don't still display the GridCell though. There's no error, just a lonely, empty space where the red switch should be. What am I doing wrong?
I'm very close to just doing this programmatically. I would love to learn how to this with nibs though.
Loading the nib causes initWithCoder to be called again, so you only want to do so if the subclass currently doesn't have any subviews.
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
if (self.subviews.count == 0) {
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil];
UIView *subview = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
subview.frame = self.bounds;
[self addSubview:subview];
}
}
return self;
}
Loading a nib will cause the corresponding owner in a
-(id) initWithCoder:(NSCoder *) coder;
call
Therefore your coude in this method:
self = [[[NSBundle mainBundle] loadNibNamed: #"GridCell"
owner: self
options: nil] objectAtIndex:0];
will cause again a call of the initWithCoder method. That's because you try to load the nib again. If you define a custom UIView and create a nib file to lay out its subviews you can't just add a UIView to another nib file, change the class name in IB to your custom class and expect the nib loading system to figure it out.
What you could do is the following:
Your custom view's nib file needs to have the 'File's owner' class set to your custom view class and you need to have an outlet in your custom class called 'toplevelSubView' connected to a view in your custom view nib file that is acting as a container for all the subviews. Add additional outlets to your view class and connect up the subviews to 'File's owner' (your custom UIView). (See https://stackoverflow.com/a/7589220/925622)
EDIT
Okay, to answer your edited question I would do the following:
Go to the nib file where you want to include the custom view with it's nib file layouting it.
Do not make it to the custom view (GridCell) itself, instead make a view which will contain your grid cell (gridCellContainer for example, but it should be a UIView)
Customize the initWithFrame method within your custom view like you did in initWithCoder:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"GridCell" owner:self options:nil];
self = [nib objectAtIndex:0];
self.frame = frame;
}
return self;
}
And then, in the viewController which is the fileOwner for the view where you want to include your custom view (the one with the gridCellContainer view) do this in viewDidLoad e.g.
//...
GridCell *gridCell = [[GridCell alloc] initWithFrame:self.viewGridCellContainer.bounds];
[self.viewGridCellContainer addSubview:gridCell];
Now eveything should work as you expected
The File's owner will not get a call to
-(id) initWithCoder:(NSCoder *) coder;
when loading a xib.
However, every view defined in that xib will get a call to
-(id) initWithCoder:(NSCoder *) coder;
when loading a xib.
If you have a subclass of a UIView (i.e. GridCell) defined in a xib and also try to load that same xib in your subclass's initWithCoder, you will end up with an infinite loop. However, I can't see what will the use case be.
Usually you design your UIView's subclass (i.e. GridCell) in one xib, then use that subclass in a view controller’s xib for example.
Also, can't see a use case where your custom view will have a subview in it's initWithCoder, i.e.
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
if (self.subviews.count == 0) {
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil];
UIView *subview = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
subview.frame = self.bounds;
[self addSubview:subview];
}
}
return self;
}
Unless you want to be able to override its view hierarchy on demand in some other xib. Which IMO assumes an external dependency (i.e. a hierarchy defined in another xib) and kinda defeats the purpose of having a reusable UIView in the first place.
Bare in mind that when loading a xib, passing an instance as the File's owner, will have all of its IBOutlet(s) set. In that case, you would be replacing self (i.e. GridCell) with whatever the root view in that GridCell.xib is, losing all IBOutlet connections in the process.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"GridCell" owner:self options:nil];
self = [nib objectAtIndex:0];
self.frame = frame;
}
return self;
}
There is a more detailed post on "How to implement a reusable UIView." that goes into a bit more detail as well and hopefully clears things up.
loadNibNamed:: will call initWithCoder:
Why don't you follow this pattern?
-(id)initWithCoder:(NSCoder *)coder
{
if (self = [super initWithcoder:coder]) {
// do stuff here ...
}
return self;
}
Does [super initWithcoder:coder] do things that you want to avoid?
I had the same issue when I'm trying to override initWithsomething method, we need
-(id)initWithsomething:(Something *)something
{
if (self = [super initWithsomething:something]) {
// do stuff here ...
}
return self;
}
instead
-(id)initWithsomething:(Something *)something
{
if (self = [super init]) {
// do stuff here ...
}
return self;
}

Multiple Views in Xcode 4.2

I'm having a lot of trouble finding a tutorial for implementing multiple views in Xcode 4.2 without storyboard, this is for a class so I can't use storyboard yet. I'm just trying to have a 2nd view with a UIPicker come up when a button is clicked in the main view, I just can't find one for this version of Xcode and it's different enough from the older versions to confuse me.
Any help appreciated if someone can give me a quick description of what I need to do this or a newer tutorial I'd appreciate it :)
I think you should read the UIView Programming Guide to get a good handle on how UIViews work exactly. I find nibs/storyboard are really great at confusing new iOS developers.
In essence, a UIViewController has 1 view which you set in the viewDidLoad or loadView method by using the [self setView:someUIView]. You add more stuff to the screen by adding UIViews as a subview of the viewcontroller's "Main" view. For example
-(void)loadView {
// Create a view for you view controller
UIView *mainView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[self setView:mainView];
// Now we have a view taking up the whole screen and we can add stuff to it
// Let's try a button, a UIButton is a subview of UIView
UIButton *newButton = [[UIButton buttonWithType:UIButtonTypeRoundedRect];
// We need to set a frame for any view we add so that we specify where it should
// be located and how big it should be!
[newButton setFrame:CGRectMake(x,y,width,height)];
// Now let's add it to our view controller's view
[self.view addSubview:newButton];
// You can do the same with any UIView subclasses you make!
MyView *myView = [[MyView alloc] init];
[myView setFrame:CGRectMake(x,y,width,height)];
[self.view addSubview:myView];
}
Now here we have our viewController who'se view is just a plain UIView which in turn has 2 subviews; newButton and myView. Since we created the MyView class, maybe it contains subviews as well! Let's take a look at what a UIView subclass could look like:
// Here is the init method for our UIView subclass
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Let's add a button to our view
UIButton *newButton2 = [[UIButton buttonWithType:UIButtonTypeRoundedRect];
// Of course, the frame is in reference to this view
[newButton2 setFrame:CGRectMake(x,y,width,height)];
// We add just using self NOT self.view because here, we are the view!
[self addSubview:newButton2];
}
return self;
}
So in this example we would have a view controller who'se view now contains 2 button! But the view structure is a tree:
mainView
/ \
newButton myView
\
newButton2
Let me know if you have any other questions!
Matt

Subviews not showing up in UIView class

I'm trying to lay out some images in code using addSubview, and they are not showing up.
I created a class (myUIView) that subclasses UIView, and then changed the class of the nib file in IB to be myUIView.
Then I put in the following code, but am still getting a blank grey screen.
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Initialization code
[self setupSubviews];
}
return self;
}
- (void)setupSubviews
{
self.backgroundColor = [UIColor blackColor];
UIImageView *black = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"black.png"]];
black.center = self.center;
black.opaque = YES;
[self addSubview:black];
[black release];
}
yes, just implement initWithCoder.
initWithFrame is called when a UIView is created dynamically, from code.
a view that is loaded from a .nib file is always instantiated using initWithCoder, the coder takes care of reading the settings from the .nib file
i took the habit to do the initialization in a separate method, implementing both initWithCode and initWithFrame (and my own initialization methods when required)
try implementing initWithCoder: sometimes I've had trouble with IB and initWithFrame:
or at least add a logging call to see if your init method is executed