Reusing a uiview created from a nib in multiple viewcontrollers - iphone

OK, sorry, In a nutshell: What is the best/easiest/recommended way of reusing a view created from a nib in multiple viewcontrollers that are also created from nibs?

Use the - (NSArray *)loadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary *)options method of NSBundle to load your nib.
Create a nib file and set the File's owner to one of your view controllers. It shouldn't matter which one as long as all the relevant properties are present in both.
Set up the nib, linking your views/buttons/etc. to the file's owner properties.
In your view controllers do the following
#interface MyViewController : UIViewController {
}
#property (retain, nonatomic) IBOutlet UIView *myView;
// Add whatever other outlets you need for your nib.
#end
#implementation MyViewController
#synthesize myView;
- (void)viewDidLoad {
[super viewDidLoad];
// You set up your other views/ivars/etc. here
[[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
// Assuming that your bundle contains a single top-level object that is linked to the
// 'myView' property in your view controller, everything should be properly retained
}
#end

Related

Best Practice: Presenting a Subclassed UIView (with own xib) as subview of UIViewControllers

My goal is, to have a subclassed UIView (lets call it infoView) designed in his own XIB so that I can present it in many UIViewController's.
The Problem:
So far, when I was adding UIView's to a UIViewController I always had to make an UIViewController the file's owner of the UIView's .xib file to load the view with something like:
...
//this is inside the calling UIViewController's method
// InfoView *infoView is ivar and a subclass of UIView
infoView = nil;
NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:#"InfoView"
owner:self options:nil];
for (id object in bundle) {
if ([object isKindOfClass:[InfoView class]])
infoView = (InfoView *)object;
}
[[self view] addSubview:infoView];
...
But I want to use the same UIView in many different UIViewController's, so I actually don't want a file's owner except maybe the class itself. In ThomasM's question he was setting the UIView itself to be the file's owner but without success.
In the answers there I found a solution to set the file's owner to nil. To do so I had to add all calling UIViewController objects from the Interface Builder object library to the InfoView.xib file and connect them with their infoView outlets.
But this doesn't feel right. So here I would like to collect solutions to
encapsulate a UIView together with his xib-file to use it in many different view controllers. How do you guys handle that?
Thx for any help.
EDIT:
The infoView is something like an overlay which appears when the user presses a button on one of the view controllers. It's NOT the View controllers "main" view. It gives detailed informations about the view of his superviews view controller and will disappear afterwards. I only fill the infoView with different contents threw out all the calling view controllers.
Like Hollance answer was pointing out I am using UINib.
To use it, leave the .xib files owner nil and place all customization of the infoView inside the initWithCoder: method of your InfoView class implementation. This will get called if you obtain the InfoView.xib like:
// here InfoView is the name of the .xib file
UINib *infoNib = [UINib nibWithNibName:#"InfoView" bundle:nil];
NSArray *topLevelObjects = [infoNib instantiateWithOwner:self options:nil];
QInfoView *infoView = [topLevelObjects objectAtIndex:0];
So you want to load a UIView from a nib that you wish to use in more than one UIViewController, and you want to connect it to an outlet on each of those view controllers. Is that correct?
Then make a UIViewController subclass (let's call it FakeViewController) with an IBOutlet property. Set that FakeViewController as the nib's File's Owner and connect your UIView to its outlet.
Done.
You just need to make sure all your other view controllers also have these outlet properties (although they don't need to be IBOutlets), but the nib loader doesn't actually check to make sure the class that you pass into the owner parameter equals the class name you specified in Interface Builder. So you can fake it.
Oh, and if you're OS 4.0 and higher, use UINib to load the nib file.
And yet another way is to create your own "controller" based on NSObject to define your own life-circle (instead of standard UIViewController life-circle).
For example:
BaseSubview.h:
#interface BaseSubview : NSObject {
UIView* _view;
}
#property (nonatomic, retain) IBOutlet UIView* view;
- (void)myMethod;
#end
BaseSubview.m:
#import "BaseSubview.h"
#implementation BaseSubview
#synthesize view = _view;
- (id)init
{
self = [super init];
if (self) {
// ...
}
return self;
}
- (void)dealloc
{
[_view removeFromSuperView];
[super dealloc];
}
- (void)myMethod
{
// view specific logic here
_view.backgroundColor = [UIColor redColor];
}
#end
InfoView.h:
#import "BaseSubview"
#interface InfoView : BaseSubview {
UILabel* _labelInfo;
}
#property (nonatomic, retain) IBOutlet UILabel* labelInfo;
#end
InfoView.m:
#import "InfoView.h"
#implementation InfoView
#synthesize labelInfo = _labelInfo;
- (id)init
{
self = [super init];
if (self) {
// ...
}
return self;
}
- (void)myMethod
{
// view specific logic here
_labelInfo.text = #"current time...";
[super myMethod];
}
#end
InfoView.xib:
file owner is InfoView
assign of outlets as usual
view is parent all other controls (such as labels, etc)
HugeAndComplicatedViewController.h:
// ...
// among other var definitions
InfoView* _infoView;
// ...
HugeAndComplicatedViewController.m, most interesting part:
// when you decide to show your view
// probably in loadView
_infoView = [[InfoView alloc] init];
[[NSBundle mainBundle] loadNibNamed:#"InfoView" owner:_infoView options:nil];
[self.view addSubview:_infoView.view];
// possibly perform specific logic
[_infoView myMethod];
// no need sub-view any more
// probably in dealloc
[_infoView release];
So now you have your own sub-view with logic and design separated from "Huge & Complicated" view-controller. It can have any life-circle you need for your current project.
does infoView need to be a subview?
in your viewController:
-(id) init {
self = [super initWithNibName:#"myNib" bundle:nil];
if (self) {
// code here
}
}

pass data from mainView to a subview

I am building a utility-based application, the data is stored in the MainViewController, and now I know how to pass data to the FlipsideViewController (many regards to this thread BTW, Sending data from Mainview to Flipside?). But I am getting the data onto an subview (subclass of UIView) that I have added to the flipside view. How can I pass data to this subview? I saw there is already a delegate and protocol set up in the FlipsideViewController.h, I am really new to the delegate sort of things. Any help would be greatly appreciated!
Updates:
On the main view, I have a couple of text fields for users to input to create an object. All the objects are stored in an array. Namely, my data is created and stored in the MainViewController. Now on the flip side, I have a custom UIView subclass which allows me to do my own drawing based on the data in that array. What I need to do here is pass the data that stored in MainViewController to this subview. Here is my relevant code:
In the MainViewController.m
- (IBAction)showInfo:(id)sender {
FlipsideViewController *controller = [[FlipsideViewController alloc] initWithNibName:#"FlipsideView" bundle:nil];
controller.delegate = self;
controller.receiver = data;//this is what I've done.
controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController:controller animated:YES];
[controller release];
}
In the FlipsideViewController.h
#protocol FlipsideViewControllerDelegate;
#interface FlipsideViewController : UIViewController {
id <FlipsideViewControllerDelegate> delegate;
DataModel *receiver; //create a property to receive the data transferred from main view
}
#property (nonatomic, assign) id <FlipsideViewControllerDelegate> delegate;
#property (nonatomic, retain) DataModel *receiver;
- (IBAction)done:(id)sender;
#end
#protocol FlipsideViewControllerDelegate
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller;
#end
In the above code, "data" is an DataModel object declared in the MainViewController.h file.
And I want to do my custom drawing in drawing class (subclass of UIView), how can I pass data from the FlipsideViewControllerto this subview? Do I need to make use of delegate declared in the FlipsideViewController.h file? Thanks in advance!
I have had a quick look at the template and think you are getting confused with what the delegate is being used for.
The delegate in this template is not transferring data. When you have clicked the done button it calls back to MainViewController and asks it to call the dismissModalViewControllerAnimated method so that it can remove the view controller. This seems a bit superflous as the documentation states
If you call this method on the modal view controller itself, however, the modal view controller automatically forwards the message to its parent view controller.
Therefore you don't really need to call the parent to do this.
In Interface builder you can see that the FlipsideView.xib has it's File's Owner set to FlipsideViewController.xib.
Now if you right click the File's Owner you will see that view is connected to View this basically means that view is the name of the property in FlipsideViewController and View is the element in Interface Builder.
Therefore we can access elements in the xib file from FlipsideViewController using outlets.
To say draw a label you will need to do a couple of things
First add a property in the .h and synthesize it in the .m like
// FlipsideViewController.h
#interface FlipsideViewController : UIViewController
#property (nonatomic, retain) IBOutlet UILabel *testLabel; // <----- Added this
#property (nonatomic, assign) id <FlipsideViewControllerDelegate> delegate;
- (IBAction)done:(id)sender;
#end
// FlipsideViewController.m
#implementation FlipsideViewController
#synthesize delegate = _delegate;
#synthesize testLabel = _testLabel; // <----- Added this
// More methods
- (void)dealloc
{
[_testLabel release]; // Always do you memory management
[super dealloc];
}
Then back in Interface Builder
Add a UILabel element to your view
ctrl + drag from File's Owner to the UILabel you added
Select the label in my example it is testLabel
Now these are hooked up correctly. The place where you want to be setting the value of the label is in viewDidLoad: which you can now do like this
- (void)viewDidLoad
{
[super viewDidLoad];
self.testLabel.text = #"It Works"; // You would use the data passed in from `MainViewController`
}
I find the easiest way to pass data from one view to another is by directly setting the data in the next view from the original view.
For example;
In your FlipsideViewController.h, declare a 'container' for the data you want to pass. It must be the same class on both sides to work properly, ie. NSArray to NSArray, NSMutableDictionary to NSMutableDictionary.
NSMutableArray *newData;
...
#property (nonatomic, retain) NSMutableArray *newData; // This allows you to access this object from outside this class.
and in FlipsideViewController.m
#synthesize newData;
...
[newData release];
Now we need to pass the data, so to speak. Let's say the data we want to 'send' is stored in a NSMutableArray called 'results'.
In our MainViewController.m, when we are instantiating our next view controller (in this case FlipsideViewController) we can directly reference the newData mutable array after we initalize the nib.
FlipsideViewController *controller = [[FlipsideViewController alloc] initWithNibName:#"FlipsideView" bundle:nil];
controller.newData = results;
controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController:controller animated:YES];
[controller release];
Make sure you are importing your FlipsideViewController in your MainViewController.h file.
If the property is declared in your .h file, you can pretty much reference the contents of the object from anywhere within the view stack!
Hope that helps :D

Load a UIView from a NIB as a subview of another UIView loaded also from a NIB

The idea basically is:
Inside of myViewController NIB I have a UIView as a subview, with connections to an IBOutlet on MyViewController class. I want to load the UIView subview from a NIB also. The subView also has a class associated with it. But the subview is not showing. Here is what I do
on MyViewController.h
#interface MyViewController : UIViewController {
IBOutlet SubView *subView; // this outlet is connected to an empty UIView on IB
}
#end
on MyViewController.m
-(id)init {
if ((self = [super init])) {
// load the main view
[[NSBundle mainBundle] loadNibNamed:#"myViewController" owner:self options:nil];
// load the sub view from another NIB
self.subView = [[[NSBundle mainBundle] loadNibNamed:#"subView" owner:self options:nil] objectAtIndex:0];
}
}
the SubView.h is defined as:
#interface SubView : UIView {
}
#end
What am I doing wrong?
What you need to do is set your file owner to be a UIViewController, hook up your view to the view property of the file owner, save your nib. In your code, load your nib like this:
UIViewController* c = [[UIViewController alloc] initWithNibName:#"Foo" bundle:nil];
SubView* subView = [c view];
Should be off to the races.
One final note, is that the type of subView should be the same as whatever is defined as the class name in interface builder. If it's a SubView there, then it's safe to declare it as a SubView* in code.
GianPac - shameless self-promotion, but I did a blog post about this about a month ago. Feel free to ignore the drop-shadow related code.
http://nathanhjones.com/2011/02/20/creating-reusable-uiviews-with-a-drop-shadow-tutorial/
Also, I (thanks to advice from several people) avoid ever having more than one view controller on my 'view'. The method I outline above allows you to accomplish this and still use IB to do the layout/outlets.

UIView subclass with its own XIB [duplicate]

This question already has answers here:
UIView and initWithFrame and a NIB file. How can I get the NIB file loaded?
(5 answers)
Closed 7 years ago.
I created a custom UIView subclass, and would prefer to not layout the UI in code in the UIView subclass. I'd like to use a xib for that. So what I did is the following.
I created a class "ShareView" which subclasses UIView. I created a XIB file with its file's owner set to "ShareView". Then I link some outlets I declared in my "ShareView.h".
Next I have a ViewController, MainViewController, which adds the ShareView as a subview. whith this code:
NSArray *arr = [[NSBundle mainBundle] loadNibNamed:#"ShareView" owner:nil options:nil];
UIView *fv = [[arr objectAtIndex:0] retain];
fv.frame = CGRectMake(0, 0, 320, 407);
[self.view addSubview:fv];
But now I get NSUnknownKeyException errors on the outlets I declared in my ShareView.
The reason I did all this is because I want a UIView, with its own logic in a seperate XIB file. I read in several places that ViewControllers are only used to manage a full screen, i.e. not parts of a screen...
So what am I doing wrong? I want my logic for ShareView in a seperate class, so my MainController class doesn't get bloated with logic from ShareView (which I think is an aption to solve this problem?)
ThomasM,
We had similar ideas about encapsulating behavior inside a custom view (say, a slider with companion labels for min/max/current values, with value-changed events also handled by the control internally).
In our current best-practice, we would design the ShareView in Interface Builder (ShareView.xib), as described by Eimantas in his answer. We then embed the ShareView to the view hierarchy in MainViewController.xib.
I wrote up how we embed custom-view Nibs inside other Nibs in our iOS developer blog. The crux is overriding -awakeAfterUsingCoder: in your custom view, replacing the object loaded from MainViewController.xib with the one loaded from the "embedded" Nib (ShareView.xib).
Something along these lines:
// ShareView.m
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder {
BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
if (theThingThatGotLoadedWasJustAPlaceholder) {
// load the embedded view from its Nib
ShareView* theRealThing = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([ShareView class]) owner:nil options:nil] objectAtIndex:0];
// pass properties through
theRealThing.frame = self.frame;
theRealThing.autoresizingMask = self.autoresizingMask;
[self release];
self = [theRealThing retain];
}
return self;
}
You defined owner of the loaded xib as nil. Since file owner in xib itself has outlets connected and is defined as instance of ShareView you get the exception about unknown keys (nil doesn't have outleted properties you defined for ShareView).
You should define the loader of the xib as owner (i.e. view controller responsible for loading the xib). Then add separate UIView object to xib and define it as instance of ShareView. Then when loading the xib.
ShareView *shareView = [[[[NSBundle mainBundle] loadNibNamed:#"ShareView" owner:self options:nil] objectAtIndex:0] retain];
You can also define shareView as an IBOutlet in view controller's interface (and connect the outlet from file owner to that view in the xib itself). Then when you load the xib there won't be any need for reassigning the shareView instance variable since the xib loading process will reconnect the view to the instance variable directly.
I would like to add to the answer. I hope people would improve this answer though.
First of all it DOES work.
XIB:
Result:
I would like to subclass UIView for a long time especially for tableViewCell.
This is how I did it.
It's succesful, but some part is still "awkward" in my opinion.
First I created a usual .h, .m, and xib file. Notice that Apple do not have the check box to automatically create an xib if the subclass you created is not a subclass of UIViewController. Well create those anyway.
#import <UIKit/UIKit.h>
#import "Business.h"
#interface BGUIBusinessCellForDisplay : UITableViewCell
+ (NSString *) reuseIdentifier;
- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz;
#end
Really simple UITableViewCell, that I want to initialize latter with biz.
I put reuseidentifier which you should do for UITableViewCell
//#import "Business.h"
#interface BGUIBusinessCellForDisplay ()
#property (weak, nonatomic) IBOutlet UILabel *Title;
#property (weak, nonatomic) IBOutlet UIImageView *Image;
#property (weak, nonatomic) IBOutlet UILabel *Address;
#property (weak, nonatomic) IBOutlet UILabel *DistanceLabel;
#property (weak, nonatomic) IBOutlet UILabel *PinNumber;
#property (strong, nonatomic) IBOutlet BGUIBusinessCellForDisplay *view;
#property (weak, nonatomic) IBOutlet UIImageView *ArrowDirection;
#property (weak, nonatomic) Business * biz;
#end
#implementation BGUIBusinessCellForDisplay
- (NSString *) reuseIdentifier {
return [[self class] reuseIdentifier];
};
+ (NSString *) reuseIdentifier {
return NSStringFromClass([self class]);
};
Then I eliminated most init codes and put this instead:
- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz
{
if (self.biz == nil) //First time set up
{
self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
NSString * className = NSStringFromClass([self class]);
//PO (className);
[[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
[self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
}
if (biz==nil)
{
return self;
}
self.biz = biz;
self.Title.text = biz.Title; //Let's set this one thing first
self.Address.text=biz.ShortenedAddress;
//if([self.distance isNotEmpty]){
self.DistanceLabel.text=[NSString stringWithFormat:#"%dm",[biz.Distance intValue]];
self.PinNumber.text =biz.StringPinLineAndNumber;
Notice that it's really awkward.
First of all the init can be used in 2 ways.
It can be used to right after aloc
It can be used by we having another existing class and then we just want to init that existing cell to another biz.
So I did:
if (self.biz == nil) //First time set up
{
self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
NSString * className = NSStringFromClass([self class]);
//PO (className);
[[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
[self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
}
Another icky things that I did is when I do [self addSubview:self.view];
The thing is I want self to be the view. Not self.view. Somehow it works nevertheless. So yea, please help me improve, but that's essentially the way to implement your own subclass of UIView.
You can create your custom UIView designed in xib and even make Interface Builder to display it inside other xib files or storyboards in new Xcode 6 using IB_DESIGNABLE. In xib set file owner to your custom class but do not set UIView class to avoid recurrency loading problems. Just leave default UIView class and you will add this UIView as a subview of your custom class view. Connect all your outlets to file owner and in your custom class load your xib like in the code below. You can check my video tutorial here: https://www.youtube.com/watch?v=L97MdpaF3Xg
IB_DESIGNABLE
#interface CustomControl : UIView
#end
#implementation CustomControl
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
[self load];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
[self load];
}
return self;
}
- (void)load
{
UIView *view = [[[NSBundle bundleForClass:[self class]] loadNibNamed:#"CustomControl" owner:self options:nil] firstObject];
[self addSubview:view];
view.frame = self.bounds;
}
#end
If you are using autolayout then you might want to change: view.frame = self.bounds; to:
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];
To use Yang's pattern with Auto-Layout, you need to add the following somewhere in the -awakeWithCoder: method.
theRealThing.translatesAutoresizingMaskIntoConstraints = NO;
If you don't turn off -translatesAutoResizingMaskIntoConstraints it can cause your layout to be incorrect as well as causing a LOT of debugging nonsense in the console.
EDIT: Auto-layout can still be a pain. Certain constraints aren't respected, but other are (e.g. pinning to the bottom doesn't work but pinning to the top does). We're not exactly sure why, but you can work around this by manually passing constraints from the placeholder to theRealThing.
It's also worth noting that this pattern works just the same way with Storyboards as it does with regular .xibs (i.e. you can create a UI Element in a .xib and drop it into a StoryBoard View controller by following your steps.)
Instead of subclassing UIView why don't you subclass UIViewController. Check out the following link. In that made a "RedView" and "BlueView" UIViewControllers with their xibs and added them to the MultipleViewsController view by creating and instance of the former two classes and adding [self.view addSubview:red.view] and [self.view addSubview:blue.view] in the MultipleViewsController's viewDidLoad method
MultipleControllers in one view
Just add (id)sender to the button pressed function in RedView and BlueView in the code of the above link.

Custom view with nib as subview doesn't seem to be loading

I've created a custom view that loads its content from a nib, like this:
/* PricingDataView.h */
#import <UIKit/UIKIt.h>
#interface PricingDataView : UIView {
UIView *contentView;
}
#property (nonatomic, retain) IBOutlet UIView *contentView;
#end
/* PricingDataView.m */
#import "PricingDataView.h"
#implementation PricingDataView
#synthesize contentView;
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[[NSBundle mainBundle] loadNibNamed:#"PricingDataView" owner:self options:nil];
[contentView setFrame:frame];
[self addSubview:contentView];
}
return self;
}
/* ... */
In the nib file I set PricingDataView as the type of the File's Owner, and connected the contentView outlet in IB. I placed a regular UIView from the Interface Library onto the full-sized view shown to the user, and then changed it's class name to PricingDataView. It all builds, but at runtime, nothing is rendered where my custom view is supposed to be.
I put breakpoints in PricingDataView.initWithFrame, but they don't hit, so I know I'm missing something that would cause the view to be initialized. What I'm curious about is that in the process of loading my other views from nibs, all the initialization happens for me, but not with this one. Why?
Are you sure your other views aren't using a UIViewController? Here's a quote from the documentation for initWithFrame: from UIView:
If you use Interface Builder to design
your interface, this method is not
called when your view objects are
subsequently loaded from the nib file.
Objects in a nib file are
reconstituted and then initialized
using their initWithCoder: method,
which modifies the attributes of the
view to match the attributes stored in
the nib file. For detailed information
about how views are loaded from a nib
file, see Resource Programming Guide.
when you add files in Interface Builder at runtime it calls the
-(id)initWithCoder:(NSCoder *)aDecoder and not the initWithFrame.
just Undo the class name (PricingDataView) of the view that you add to UIView. In this case the initWithFrame: method would be called.
previously awakeFromNib was called in loop. As your customView and the View you are adding via Nib as a subView are of the same class type.