I am aware this question has been asked before but the answers are contradicting and I am confused, so please don't flame me.
I want to have a reusable UIView subclass throughout my app. I want to describe the interface using a nib file.
Now let's say it's a loading indicator view with an activity indicator in it. I would like on some event to instantiate this view and animate in to a view controller's view. I could describe the view's interface no problem programmatically, creating the elements programmatically and setting their frame inside an init method etc.
How can I do this using a nib though? Maintaining the size given in interface builder without having to set a frame.
I've managed to do it like this, but I'm sure it is wrong (it's just a view with a picker in it):
- (id)initWithDataSource:(NSDictionary *)dataSource {
self = [super init];
if (self){
self = [[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:#"%#", [self class]] owner:self options:nil] objectAtIndex:0];
self.pickerViewData = dataSource;
[self configurePickerView];
}
return self;
}
But I'm overwriting self, and when I instantiate it:
FSASelectView *selectView = [[FSASelectView alloc] initWithDataSource:selectViewDictionary];
selectView.delegate = self;
selectView.frame = CGRectMake(0, self.view.bottom + 50, [FSASelectView width], [FSASelectView height]);
I have to manually set the frame rather than have it picked up from IB.
EDIT: I want to create this custom view in a view controller, and have access to control the view's elements. I don't want a new view controller.
Thanks
EDIT: I Don't know if this is best practice, I'm sure it's not, but this is how I did it:
FSASelectView *selectView = [[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:#"%#",[FSASelectView class]] owner:self options:nil] objectAtIndex:0];
selectView.delegate = self;
[selectView configurePickerViewWithData:ds];
selectView.frame = CGRectMake(0, self.view.bottom + 50, selectView.width, selectView.height);
selectView.alpha = 0.9;
[self.view addSubview:selectView];
[UIView animateWithDuration: 0.25 delay: 0 options:UIViewAnimationOptionAllowUserInteraction |UIViewAnimationOptionCurveEaseInOut animations:^{
selectView.frame = CGRectMake(0, self.view.bottom - selectView.height, selectView.width, selectView.height);
selectView.alpha = 1;
} completion:^(BOOL finished) {
}];
Correct practice still wanted
Should this have been done using a view controller and init with nib name? Should I have set the nib in some UIView initialisation method in the code? Or is what I have done ok?
MyViewClass *myViewObject = [[[NSBundle mainBundle] loadNibNamed:#"MyViewClassNib" owner:self options:nil] objectAtIndex:0]
I'm using this to initialise the reusable custom views I have.
Note that you can use "firstObject" at the end there, it's a little cleaner. "firstObject" is a handy method for NSArray and NSMutableArray.
Here's a typical example, of loading a xib to use as a table header. In your file YourClass.m
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return [[NSBundle mainBundle] loadNibNamed:#"TopArea" owner:self options:nil].firstObject;
}
Normally, in the TopArea.xib, you would click on File Owner and set the file owner to YourClass. Then actually in YourClass.h you would have IBOutlet properties. In TopArea.xib, you can drag controls to those outlets.
Don't forget that in TopArea.xib, you may have to click on the View itself and drag that to some outlet, so you have control of it, if necessary. (A very worthwhile tip is that when you are doing this for table cell rows, you absolutely have to do that - you have to connect the view itself to the relevant property in your code.)
If you want to keep your CustomView and its xib independent of File's Owner, then follow these steps
Leave the File's Owner field empty.
Click on actual view in xib file of your CustomView and set its Custom Class as CustomView (name of your custom view class)
Add IBOutlet in .h file of your custom view.
In .xib file of your custom view, click on view and go in Connection Inspector. Here you will all your IBOutlets which you define in .h file
Connect them with their respective view.
in .m file of your CustomView class, override the init method as follow
-(CustomView *) init{
CustomView *result = nil;
NSArray* elements = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner:self options: nil];
for (id anObject in elements)
{
if ([anObject isKindOfClass:[self class]])
{
result = anObject;
break;
}
}
return result;
}
Now when you want to load your CustomView, use the following line of code
[[CustomView alloc] init];
Follow the following steps
Create a class named MyView .h/.m of type UIView.
Create a xib of same name MyView.xib.
Now change the File Owner class to UIViewController from NSObject in xib. See the image below
Connect the File Owner View to your View. See the image below
Change the class of your View to MyView. Same as 3.
Place controls create IBOutlets.
Here is the code to load the View:
UIViewController *controller=[[UIViewController alloc] initWithNibName:#"MyView" bundle:nil];
MyView* view=(MyView*)controller.view;
[self.view addSubview:myview];
Hope it helps.
Clarification:
UIViewController is used to load your xib and the View which the UIViewController has is actually MyView which you have assigned in the MyView xib..
Demo
I have made a demo grab here
Answering my own question about 2 or something years later here but...
It uses a protocol extension so you can do it without any extra code for all classes.
/*
Prerequisites
-------------
- In IB set the view's class to the type hook up any IBOutlets
- In IB ensure the file's owner is blank
*/
public protocol CreatedFromNib {
static func createFromNib() -> Self?
static func nibName() -> String?
}
extension UIView: CreatedFromNib { }
public extension CreatedFromNib where Self: UIView {
public static func createFromNib() -> Self? {
guard let nibName = nibName() else { return nil }
guard let view = NSBundle.mainBundle().loadNibNamed(nibName, owner: nil, options: nil).last as? Self else { return nil }
return view
}
public static func nibName() -> String? {
guard let n = NSStringFromClass(Self.self).componentsSeparatedByString(".").last else { return nil }
return n
}
}
// Usage:
let myView = MyView().createFromNib()
In Swift:
For example, name of your custom class is InfoView
At first, you create files InfoView.xib and InfoView.swiftlike this:
import Foundation
import UIKit
class InfoView: UIView {
class func instanceFromNib() -> UIView {
return UINib(nibName: "InfoView", bundle: nil).instantiateWithOwner(nil, options: nil)[0] as! UIView
}
Then set File's Owner to UIViewController like this:
Rename your View to InfoView:
Right-click to File's Owner and connect your view field with your InfoView:
Make sure that class name is InfoView:
And after this you can add the action to button in your custom class without any problem:
And usage of this custom class in your MainViewController:
func someMethod() {
var v = InfoView.instanceFromNib()
v.frame = self.view.bounds
self.view.addSubview(v)
}
Well you could either initialize the xib using a view controller and use viewController.view. or do it the way you did it. Only making a UIView subclass as the controller for UIView is a bad idea.
If you don't have any outlets from your custom view then you can directly use a UIViewController class to initialize it.
Update: In your case:
UIViewController *genericViewCon = [[UIViewController alloc] initWithNibName:#"CustomView"];
//Assuming you have a reference for the activity indicator in your custom view class
CustomView *myView = (CustomView *)genericViewCon.view;
[parentView addSubview:myView];
//And when necessary
[myView.activityIndicator startAnimating]; //or stop
Otherwise you have to make a custom UIViewController(to make it as the file's owner so that the outlets are properly wired up).
YourCustomController *yCustCon = [[YourCustomController alloc] initWithNibName:#"YourXibName"].
Wherever you want to add the view you can use.
[parentView addSubview:yCustCon.view];
However passing the another view controller(already being used for another view) as the owner while loading the xib is not a good idea as the view property of the controller will be changed and when you want to access the original view, you won't have a reference to it.
EDIT: You will face this problem if you have setup your new xib with file's owner as the same main UIViewController class and tied the view property to the new xib view.
i.e;
YourMainViewController -- manages -- mainView
CustomView -- needs to load from xib as and when required.
The below code will cause confusion later on, if you write it inside view did load of YourMainViewController. That is because self.view from this point on will refer to your customview
-(void)viewDidLoad:(){
UIView *childView= [[[NSBundle mainBundle] loadNibNamed:#"YourXibName" owner:self options:nil] objectAtIndex:0];
}
Related
OK, there are dozens of posts on StackOverflow about this, but none are particularly clear on the solution. I'd like to create a custom UIView with an accompanying xib file. The requirements are:
No separate UIViewController – a completely self-contained class
Outlets in the class to allow me to set/get properties of the view
My current approach to doing this is:
Override -(id)initWithFrame:
-(id)initWithFrame:(CGRect)frame {
self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:self
options:nil] objectAtIndex:0];
self.frame = frame;
return self;
}
Instantiate programmatically using -(id)initWithFrame: in my view controller
MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
[self.view insertSubview:myCustomView atIndex:0];
This works fine (although never calling [super init] and simply setting the object using the contents of the loaded nib seems a bit suspect – there is advice here to add a subview in this case which also works fine). However, I'd like to be able to instantiate the view from the storyboard also. So I can:
Place a UIView on a parent view in the storyboard
Set its custom class to MyCustomView
Override -(id)initWithCoder: – the code I've seen the most often fits a pattern such as the following:
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self initializeSubviews];
}
return self;
}
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initializeSubviews];
}
return self;
}
-(void)initializeSubviews {
typeof(view) view = [[[NSBundle mainBundle]
loadNibNamed:NSStringFromClass([self class])
owner:self
options:nil] objectAtIndex:0];
[self addSubview:view];
}
Of course, this doesn't work, as whether I use the approach above, or whether I instantiate programatically, both end up recursively calling -(id)initWithCoder: upon entering -(void)initializeSubviews and loading the nib from file.
Several other SO questions deal with this such as here, here, here and here. However, none of the answers given satisfactorily fixes the problem:
A common suggestion seems to be to embed the entire class in a UIViewController, and do the nib loading there, but this seems suboptimal to me as it requires adding another file just as a wrapper
Could anyone give advice on how to resolve this problem, and get working outlets in a custom UIView with minimum fuss/no thin controller wrapper? Or is there an alternative, cleaner way of doing things with minimum boilerplate code?
Note that this QA (like many) is really just of historic interest.
Nowadays For years and years now in iOS everything's just a container view. Full tutorial here
(Indeed Apple finally added Storyboard References, some time ago now, making it far easier.)
Here's a typical storyboard with container views everywhere. Everything's a container view. It's just how you make apps.
(As a curiosity, KenC's answer shows exactly how, it used to be done to load an xib to a kind of wrapper view, since you can't really "assign to self".)
I'm adding this as a separate post to update the situation with the release of Swift. The approach described by LeoNatan works perfectly in Objective-C. However, the stricter compile time checks prevent self being assigned to when loading from the xib file in Swift.
As a result, there is no option but to add the view loaded from the xib file as a subview of the custom UIView subclass, rather than replacing self entirely. This is analogous to the second approach outlined in the original question. A rough outline of a class in Swift using this approach is as follows:
#IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
// below doesn't work as returned class name is normally in project module scope
/*let viewName = NSStringFromClass(self.classForCoder)*/
let viewName = "ExampleView"
let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
owner: self, options: nil)[0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
}
The downside of this approach is the introduction of an additional redundant layer in the view hierarchy which does not exist when using the approach outlined by LeoNatan in Objective-C. However, this could be taken as a necessary evil and a product of the fundamental way things are designed in Xcode (it still seems crazy to me that it is so difficult to link a custom UIView class with a UI layout in a way that works consistently over both storyboards and from code) – replacing self wholesale in the initializer before never seemed like a particularly interpretable way of doing things, although having essentially two view classes per view doesn't seem so great either.
Nonetheless, one happy result of this approach is that we no longer need to set the view's custom class to our class file in interface builder to ensure correct behaviour when assigning to self, and so the recursive call to init(coder aDecoder: NSCoder) when issuing loadNibNamed() is broken (by not setting the custom class in the xib file, the init(coder aDecoder: NSCoder) of plain vanilla UIView rather than our custom version will be called instead).
Even though we cannot make class customizations to the view stored in the xib directly, we are still able to link the view to our 'parent' UIView subclass using outlets/actions etc. after setting the file owner of the view to our custom class:
A video demonstrating the implementation of such a view class step by step using this approach can be found in the following video.
STEP1. Replacing self from Storyboard
Replacing self in initWithCoder: method will fail with following error.
'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Instead, you can replace decoded object with awakeAfterUsingCoder: (not awakeFromNib). like:
#implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
#end
STEP2. Preventing recursive call
Of course, this also causes recursive call problem. (storyboard decoding -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> ...)
So you have to check current awakeAfterUsingCoder: is called in Storyboard decoding process or XIB decoding process.
You have several ways to do that:
a) Use private #property which is set in NIB only.
#interface MyCustomView : UIView
#property (assign, nonatomic) BOOL xib
#end
and set "User Defined Runtime Attributes" only in 'MyCustomView.xib'.
Pros:
None
Cons:
Simply does not work: setXib: will be called AFTER awakeAfterUsingCoder:
b) Check if self has any subviews
Normally, you have subviews in the xib, but not in the storyboard.
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if(self.subviews.count > 0) {
// loading xib
return self;
}
else {
// loading storyboard
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
}
Pros:
No trick in Interface Builder.
Cons:
You cannot have subviews in your Storyboard.
c) Set a static flag during loadNibNamed: call
static BOOL _loadingXib = NO;
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if(_loadingXib) {
// xib
return self;
}
else {
// storyboard
_loadingXib = YES;
typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
_loadingXib = NO;
return view;
}
}
Pros:
Simple
No trick in Interface Builder.
Cons:
Not safe: static shared flag is dangerous
d) Use private subclass in XIB
For example, declare _NIB_MyCustomView as a subclass of MyCustomView.
And, use _NIB_MyCustomView instead of MyCustomView in your XIB only.
MyCustomView.h:
#interface MyCustomView : UIView
#end
MyCustomView.m:
#import "MyCustomView.h"
#implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In Storyboard decoding path.
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
#end
#interface _NIB_MyCustomView : MyCustomView
#end
#implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In XIB decoding path.
// Block recursive call.
return self;
}
#end
Pros:
No explicit if in MyCustomView
Cons:
Prefixing _NIB_ trick in xib Interface Builder
relatively more codes
e) Use subclass as placeholder in Storyboard
Similar to d) but use subclass in Storyboard, original class in XIB.
Here, we declare MyCustomViewProto as a subclass of MyCustomView.
#interface MyCustomViewProto : MyCustomView
#end
#implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In storyboard decoding
// Returns MyCustomView loaded from NIB.
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
owner:nil
options:nil] objectAtIndex:0];
}
#end
Pros:
Very safe
Clean; No extra code in MyCustomView.
No explicit if check same as d)
Cons:
Need to use subclass in storyboard.
I think e) is the safest and cleanest strategy. So we adopt that here.
STEP3. Copy properties
After loadNibNamed: in 'awakeAfterUsingCoder:', You have to copy several properties from self which is decoded instance f the Storyboard. frame and autolayout/autoresize properties are especially important.
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
// copy layout properities.
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
// copy autolayout constraints
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in self.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == self) firstItem = view;
if(secondItem == self) secondItem = view;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
// move subviews
for(UIView *subview in self.subviews) {
[view addSubview:subview];
}
[view addConstraints:constraints];
// Copy more properties you like to expose in Storyboard.
return view;
}
FINAL SOLUTION
As you can see, this is a bit of boilerplate code. We can implement them as 'category'.
Here, I extend commonly used UIView+loadFromNib code.
#import <UIKit/UIKit.h>
#interface UIView (loadFromNib)
#end
#implementation UIView (loadFromNib)
+ (id)loadFromNib {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
owner:nil
options:nil] objectAtIndex:0];
}
- (void)copyPropertiesFromPrototype:(UIView *)proto {
self.frame = proto.frame;
self.autoresizingMask = proto.autoresizingMask;
self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in proto.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == proto) firstItem = self;
if(secondItem == proto) secondItem = self;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
for(UIView *subview in proto.subviews) {
[self addSubview:subview];
}
[self addConstraints:constraints];
}
Using this, you can declare MyCustomViewProto like:
#interface MyCustomViewProto : MyCustomView
#end
#implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
MyCustomView *view = [MyCustomView loadFromNib];
[view copyPropertiesFromPrototype:self];
// copy additional properties as you like.
return view;
}
#end
XIB:
Storyboard:
Result:
Your problem is calling loadNibNamed: from (a descendant of) initWithCoder:. loadNibNamed: internally calls initWithCoder:. If you want to override the storyboard coder, and always load your xib implementation, I suggest the following technique. Add a property to your view class, and in the xib file, set it to a predetermined value (in User Defined Runtime Attributes). Now, after calling [super initWithCoder:aDecoder]; check the value of the property. If it is the predetermined value, do not call [self initializeSubviews];.
So, something like this:
-(instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self && self._xibProperty != 666)
{
//We are in the storyboard code path. Initialize from the xib.
self = [self initializeSubviews];
//Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
//self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:#"backgroundColor"];
}
return self;
}
-(instancetype)initializeSubviews {
id view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
return view;
}
Don't forget
Two important points:
Set the File's Owner of the .xib to class name of your custom view.
Don't set the custom class name in IB for the .xib's root view.
I came to this Q&A page several times while learning to make reusable views. Forgetting the above points made me waste a lot of time trying to find out what was causing infinite recursion to happen. These points are mentioned in other answers here and elsewhere, but I just want to reemphasize them here.
My full Swift answer with steps is here.
There is a solution which is much more cleaner than the solutions above:
https://www.youtube.com/watch?v=xP7YvdlnHfA
No Runtime properties, no recursive call problem at all.
I tried it and it worked like a charm using from storyboard and from XIB with IBOutlet properties (iOS8.1, XCode6).
Good luck for coding!
My Views Hierarchy is next:
My root View Controller that contains custom view
My custom UIView that contain custom UITableView
My custom UITableView
And i want to use delegate methods of my custom UITableView, but i don't really know how to access it's delegates because UITableView is added in my custom UIView class. Please clear me up, i'm little confused about this question.
Here's my code:
Adding my custom UITableView to UIView(i'm using CollapseClick):
-(void) initMehod{
DetailsView *testView = [[[NSBundle mainBundle] loadNibNamed:#"DetailsView" owner:self options:nil] lastObject];
test1View = testView;
//collapse click init
CGRect viewRect = [test1View frame];
viewRect.origin.x = 10;
test1View.frame = viewRect;
[test1View setBackgroundColor:[UIColor magentaColor]];
self.myCollapseClick.CollapseClickDelegate = self;
[self.myCollapseClick reloadCollapseClick];
// If you want a cell open on load, run this method:
[self.myCollapseClick openCollapseClickCellAtIndex:0 animated:NO];
}
...
And i'm using PagedFlowView in my root View Controller to add CustomView:
- (UIView *)flowView:(PagedFlowView *)flowView cellForPageAtIndex:(NSInteger)index{
mainDetailV = (MainDetailView *)[flowView dequeueReusableCell];
if (!mainDetailV) {
mainDetailV = [[[NSBundle mainBundle] loadNibNamed:#"MainDetailView" owner:self options:nil] lastObject];
//mainDetailV.layer.cornerRadius = 6;
mainDetailV.layer.masksToBounds = YES;
}
[usedViewControllers insertObject:mainDetailV atIndex:index];
return mainDetailV;
}
Everything works perfectly, except i can't use my custom UITableView delegate (for example didSelectRowAtIndexPath: method).
EDIT: I have fixed my problem, PagedFlowView actually handles tag gesture recogniser and overrides my custom tableView delegate methods.
Similar question:
UITableView as a subview?
First of all your collapsable tableview abstracts tableview delegates (i.e its a wrapper).
It provides you with a method didClickCollapseClickCellAtIndex instead. Use that.
It seems to be that you have some problem in understanding delegation pattern.
Avoid breaking the cascading effect of delegation pattern. It makes debugging easy
and helps to understand the flow of the code. Below diagram might help you to understand what I mean
Implement you didSelectRowAtIndexPath in custom view instead of rootViewcontroller.
// This is your custom view
-(id)init{
----
self.myCollapseClick.CollapseClickDelegate = self;
----
}
//collapsable delegate
-(void)didClickCollapseClickCellAtIndex:(int)index isNowOpen:(BOOL)open {
// you need to create your own custom view delegate with mehthod didSelectIndex:index
[self.customViewDelegate didSelectIndex:index];
}
In your rootviewcontroller you need to do this
customView.customViewDelegate = self;
-(void)didSelectIndex:(int)index{
// do your stuff here
}
I have a MainWindowViewController with the proper protocols. I also have the dataSouce methods implemented in MainWindowViewController.
#interface MainWindowController : UIViewController < UITableViewDelegate, UITableViewDataSource, UAModalPanelDelegate, UIGestureRecognizerDelegate>
I have the delegate and dataSource set in the viewDidLoad of MainWindowViewController.
self.friendsTableView.delegate = self;
self.friendsTableView.dataSource = self;
What should happen is that I press the friends button. A xib file name FriendsPopUpView_iPhone is loaded and it should bring up a UITableView of friends. But the friendsPopUpView's tableview shows up with empty rows. What am I doing wrong?
FriendsPopUpView_iPhone.xib contains a UITableView.
friendsTableView is an outlet from the tableview created in FriendsPopUpView_iPhone.xib.
friendsPopUpView is a UIView outlet for the the view in FriendsPopUpView_iPhone.xib.
Here is the action connected to the friend button on the main MainWindowController.
- (IBAction)on_friends:(id)sender {
if (self.friendsPopUpView == nil) {
[[NSBundle mainBundle] loadNibNamed:#"FriendsPopUpView_iPhone" owner:self options:nil];
[self.view addSubview:self.friendsPopUpView];
UIButton* clickedButton = (UIButton*) sender;
CGRect sFrame = CGRectMake(clickedButton.frame.origin.x-100, clickedButton.frame.origin.y,
self.friendsPopUpView.frame.size.width,
self.friendsPopUpView.frame.size.height);
self.friendsPopUpView.frame = sFrame;
}
}
Does the popup view nib contain hooked up outlets to the MainWindowViewController class (like self.friendsPopUpView)? It must in order for anything to work.
You can't set the delegate and datasource before the table view exists. It doesn't exist when the MainWindowViewController viewDidLoad fires. To setup the delegate and datasource in code, do it after the nib is loaded, once the table exists.
If you setup the other outlets (like the friendsPopUp and the friendsTableView) as nib outlets (connected to the "file's owner" which you would have set as MainWindowViewController) then you can set the delegate and datasource the same way, no code required. Otherwise, do it in code after you load the nib...
- (IBAction)on_friends:(id)sender {
if (self.friendsPopUpView == nil) {
[[NSBundle mainBundle] loadNibNamed:#"FriendsPopUpView_iPhone" owner:self options:nil];
// assuming you have a friendsPopUpView outlet setup in the nib
// also assuming you have a friendsTableView outlet setup in the nib, both of these connected
// now this will work
self.friendsTableView.delegate = self;
self.friendsTableView.dataSource = self;
I am currently making an app that will have multiple timers, which are basically all the same.
I want to create a custom class that uses all of the code for the timers as well as the layout/animations, so I can have 5 identical timers that operate independently of each other.
I created the layout using IB (xcode 4.2) and all the code for the timers is currently just in the viewcontroller class.
I am having difficulty wrapping my brain around how to encapsulate everything into a custom class and then add it to the viewcontroller, any help would be much appreciated.
Swift example
Updated for Xcode 10 and Swift 4 (and reportedly still works for Xcode 12.4/Swift 5)
Here is a basic walk through. I originally learned a lot of this from watching this Youtube video series. Later I updated my answer based on this article.
Add custom view files
The following two files will form your custom view:
.xib file to contain the layout
.swift file as UIView subclass
The details for adding them are below.
Xib file
Add a .xib file to your project (File > New > File... > User Interface > View). I am calling mine ReusableCustomView.xib.
Create the layout that you want your custom view to have. As an example, I will make a layout with a UILabel and a UIButton. It is a good idea to use auto layout so that things will resize automatically no matter what size you set it to later. (I used Freeform for the xib size in the Attributes inspector so that I could adjust the simulated metrics, but this isn't necessary.)
Swift file
Add a .swift file to your project (File > New > File... > Source > Swift File). It is a subclass of UIView and I am calling mine ReusableCustomView.swift.
import UIKit
class ResuableCustomView: UIView {
}
Make the Swift file the owner
Go back to your .xib file and click on "File's Owner" in the Document Outline. In the Identity Inspector write the name of your .swift file as the custom class name.
Add Custom View Code
Replace the ReusableCustomView.swift file's contents with the following code:
import UIKit
#IBDesignable
class ResuableCustomView: UIView {
let nibName = "ReusableCustomView"
var contentView:UIView?
#IBOutlet weak var label: UILabel!
#IBAction func buttonTap(_ sender: UIButton) {
label.text = "Hi"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
}
Be sure to get the spelling right for the name of your .xib file.
Hook up the Outlets and Actions
Hook up the outlets and actions by control dragging from the label and button in the xib layout to the swift custom view code.
Use you custom view
Your custom view is finished now. All you have to do is add a UIView wherever you want it in your main storyboard. Set the class name of the view to ReusableCustomView in the Identity Inspector.
Well to answer conceptually, your timer should likely be a subclass of UIView instead of NSObject.
To instantiate an instance of your timer in IB simply drag out a UIView drop it on your view controller's view, and set it's class to your timer's class name.
Remember to #import your timer class in your view controller.
Edit: for IB design (for code instantiation see revision history)
I'm not very familiar at all with storyboard, but I do know that you can construct your interface in IB using a .xib file which is nearly identical to using the storyboard version; You should even be able to copy & paste your views as a whole from your existing interface to the .xib file.
To test this out I created a new empty .xib named "MyCustomTimerView.xib". Then I added a view, and to that added a label and two buttons. Like So:
I created a new objective-C class subclassing UIView named "MyCustomTimer". In my .xib I set my File's Owner class to be MyCustomTimer. Now I'm free to connect actions and outlets just like any other view/controller. The resulting .h file looks like this:
#interface MyCustomTimer : UIView
#property (strong, nonatomic) IBOutlet UILabel *displayLabel;
#property (strong, nonatomic) IBOutlet UIButton *startButton;
#property (strong, nonatomic) IBOutlet UIButton *stopButton;
- (IBAction)startButtonPush:(id)sender;
- (IBAction)stopButtonPush:(id)sender;
#end
The only hurdle left to jump is getting this .xib on my UIView subclass. Using a .xib dramatically cuts down the setup required. And since you're using storyboards to load the timers we know -(id)initWithCoder: is the only initializer that will be called. So here is what the implementation file looks like:
#import "MyCustomTimer.h"
#implementation MyCustomTimer
#synthesize displayLabel;
#synthesize startButton;
#synthesize stopButton;
-(id)initWithCoder:(NSCoder *)aDecoder{
if ((self = [super initWithCoder:aDecoder])){
[self addSubview:
[[[NSBundle mainBundle] loadNibNamed:#"MyCustomTimerView"
owner:self
options:nil] objectAtIndex:0]];
}
return self;
}
- (IBAction)startButtonPush:(id)sender {
self.displayLabel.backgroundColor = [UIColor greenColor];
}
- (IBAction)stopButtonPush:(id)sender {
self.displayLabel.backgroundColor = [UIColor redColor];
}
#end
The method named loadNibNamed:owner:options: does exactly what it sounds like it does. It loads the Nib and sets the "File's Owner" property to self. We extract the first object in the array and that is the root view of the Nib. We add the view as a subview and Voila it's on screen.
Obviously this just changes the label's background color when the buttons are pushed, but this example should get you well on your way.
Notes based on comments:
It is worth noting that if you are getting infinite recursion problems you probably missed the subtle trick of this solution. It's not doing what you think it's doing. The view that is put in the storyboard is not seen, but instead loads another view as a subview. That view it loads is the view which is defined in the nib. The "file's owner" in the nib is that unseen view. The cool part is that this unseen view is still an Objective-C class which may be used as a view controller of sorts for the view which it brings in from the nib. For example the IBAction methods in the MyCustomTimer class are something you would expect more in a view controller than in a view.
As a side note, some may argue that this breaks MVC and I agree somewhat. From my point of view it's more closely related to a custom UITableViewCell, which also sometimes has to be part controller.
It is also worth noting that this answer was to provide a very specific solution; create one nib that can be instantiated multiple times on the same view as laid out on a storyboard. For example, you could easily imagine six of these timers all on an iPad screen at one time. If you only need to specify a view for a view controller that is to be used multiple times across your application then the solution provided by jyavenard to this question is almost certainly a better solution for you.
Answer for view controllers, not views:
There is an easier way to load a xib from a storyboard.
Say your controller is of MyClassController type which inherit from UIViewController.
You add a UIViewController using IB in your storyboard; change the class type to be MyClassController. Delete the view that had been automatically added in the storyboard.
Make sure the XIB you want called is called MyClassController.xib.
When the class will be instantiated during the storyboard loading, the xib will be automatically loaded.
The reason for this is due to default implementation of UIViewController which calls the XIB named with the class name.
This is not really an answer, but I think it is helpful to share this approach.
Objective-C
Import CustomViewWithXib.h and CustomViewWithXib.m to your
project
Create the custom view files with the same name (.h / .m /
.xib)
Inherit your custom class from CustomViewWithXib
Swift
Import CustomViewWithXib.swift to your project
Create the custom view files with the same name (.swift and .xib)
Inherit your custom class from CustomViewWithXib
Optional :
Go to your xib file, set the owner with your custom class name if you
need to connect some elements (for more details see the part Make
the Swift file the owner of #Suragch answer's)
It's all, now you can add your custom view into your storyboard and it will be shown :)
CustomViewWithXib.h :
#import <UIKit/UIKit.h>
/**
* All classes inherit from CustomViewWithXib should have the same xib file name and class name (.h and .m)
MyCustomView.h
MyCustomView.m
MyCustomView.xib
*/
// This allows seeing how your custom views will appear without building and running your app after each change.
IB_DESIGNABLE
#interface CustomViewWithXib : UIView
#end
CustomViewWithXib.m :
#import "CustomViewWithXib.h"
#implementation CustomViewWithXib
#pragma mark - init methods
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// load view frame XIB
[self commonSetup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// load view frame XIB
[self commonSetup];
}
return self;
}
#pragma mark - setup view
- (UIView *)loadViewFromNib {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
// An exception will be thrown if the xib file with this class name not found,
UIView *view = [[bundle loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
return view;
}
- (void)commonSetup {
UIView *nibView = [self loadViewFromNib];
nibView.frame = self.bounds;
// the autoresizingMask will be converted to constraints, the frame will match the parent view frame
nibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// Adding nibView on the top of our view
[self addSubview:nibView];
}
#end
CustomViewWithXib.swift :
import UIKit
#IBDesignable
class CustomViewWithXib: UIView {
// MARK: init methods
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
// MARK: setup view
private func loadViewFromNib() -> UIView {
let viewBundle = NSBundle(forClass: self.dynamicType)
// An exception will be thrown if the xib file with this class name not found,
let view = viewBundle.loadNibNamed(String(self.dynamicType), owner: self, options: nil)[0]
return view as! UIView
}
private func commonSetup() {
let nibView = loadViewFromNib()
nibView.frame = bounds
// the autoresizingMask will be converted to constraints, the frame will match the parent view frame
nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
// Adding nibView on the top of our view
addSubview(nibView)
}
}
You can find some examples here.
Hope that helps.
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