How do I create a custom iOS view class and instantiate multiple copies of it (in IB)?
Asked Answered
A

4

123

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.

Aftereffect answered 12/2, 2012 at 18:2 Comment(2)
For anyone googling to here ... it's now five years on from container views coming to iOS! Container views are the basic, central idea of how you do everything in iOS now. They are incredibly simple to use, and there are any number of excellent (five year old!) tutorials on container views around, example , exampleEudemon
@JoeBlow Except you cannot use a container view in a UITableViewCell or UICollectionViewCell. In my case I need a small but fairly complex view that I can use many times in controllers and collection views. And the designers keep reworking it, so I want a single place the layout is defined. Hence, a nib.Maitund
T
144

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.

enter image description here

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:

enter image description here

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.

Torrefy answered 12/2, 2012 at 18:8 Comment(6)
This actually seems like a separate question. But I think it can be answered quickly, so here goes. Each time you create a new notification it is a new notification but if you need them to have the same name want to add something to differentiate them then use the userInfo property on the notification @property(nonatomic,copy) NSDictionary *userInfo;Torrefy
I get an infinite recursion from initWithCoder. Any ideas?Tibetoburman
Sorry to revive an old thread, making sure File's Owner is set to your class name solved my infinite recursion problem.Illbehaved
This was the clearest answer I found to adding a custom view/xib to another view. However I was having problems with the size of my views. I 'solved' it by connecting an outlet called 'view' to my base UIView in my xib to my custom subclass of UIView then after the addSubView method in initWithCoder setting the frame with _view.frame = self.frame;Katey
Another reason for infinite recursion is setting the root view in the xib to your custom class. You don't want to do that, leave it as the default "UIView"Desex
There is just one thing that draws my attention about this approach... Doesn't it bother anyone that 2 Views are in the same class now? the Original File .h/.m inheriting from UIView being the first one and by doing [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Adding the Second UIView... This kinda looks wierd for me noone else feels the same way? If so is this something apple made by design ? Or is the solution someone found by doing this?Bashemath
A
153

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.)

enter image description here

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.

enter image description here

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.

enter image description here

Advocation answered 30/12, 2015 at 6:45 Comment(14)
Important not to set Xib view class as ResuableCustomView, but Owner of this Xib. Otherwise you will have error.Ashur
Yes, good reminder. I don't know how much time I've wasted trying to solve infinite recursion problems caused by setting the Xib's view (rather that the File Owner) to the class name.Advocation
Good answer, your tutorial helped me a lot, thank you... but It didn't worked with autolayout. I followed https://mcmap.net/q/23766/-swift-reusable-uiview-in-storyboard-and-sizing-constraints to get It working.Lidless
Here is a clear answer I wrote with support for autolayout. #30335589Statue
That's the right way how Storyboard is working with NIB's! I read a lot about replacing the decoded view in awakeAfterUsingCoder. But that has a lot disadvantages since you must copy all the properties you set on your view in the storyboard by hand wich is very error prone. This is the correct answer since all properties set in the storyboard are applied correctly to the resulting view! See my UIView superclass gist.github.com/Blackjacx/c265534d5219e28f9147 that manages all the stuff to do. It is soooo simple!Rightful
@TomCalmon, I redid this project in Xcode 8 with Swift 3 and Auto Layout was working ok for me.Advocation
If anyone else has a problem with the view rendering in IB, I found that changing Bundle.main to Bundle(for: ...) does the trick. Apparently, Bundle.main is unavailable at design time.Owenowena
It worked in Objective-C as well. Thank you @AdvocationJoanejoanie
@crizzis, good tip, however I tried Bundle(for: type(of: self)).loadNibNamed("Teste", owner: self, options: nil)?[0] as! UIView and it really didn't work in some situations unfortunatelyEudemon
@crizzis, correct. You must use Bundle(for:MyView.self).Angy
The reason why this does not work with @IBDesignable is that you did not implement init(frame:). Once you add this (and pull all the initialisation code into some commonInit() function) it works fine and shows in IB. See more info here: #31266406Fulford
I am getting this error for the label otulet. Any idea? Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<View 0x11000f450> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key labelSeptuplicate
@Fmessina, I'm not sure off hand but check out this, this and this.Advocation
This still works the same way in Xcode 12.4/Swift 5. Thanks for posting this clear and well structured answer! Looks like there's no need to mess with AppDelegate, like a bunch of other tutorials want to make you believe! Btw, if you want to create one of the views from code, just do: let rcv = ReusableCustomView() and add it to the scene with parentView.addSubview(rcv)- yes, it's that easy! :)Gorget
T
144

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.

enter image description here

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:

enter image description here

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.

Torrefy answered 12/2, 2012 at 18:8 Comment(6)
This actually seems like a separate question. But I think it can be answered quickly, so here goes. Each time you create a new notification it is a new notification but if you need them to have the same name want to add something to differentiate them then use the userInfo property on the notification @property(nonatomic,copy) NSDictionary *userInfo;Torrefy
I get an infinite recursion from initWithCoder. Any ideas?Tibetoburman
Sorry to revive an old thread, making sure File's Owner is set to your class name solved my infinite recursion problem.Illbehaved
This was the clearest answer I found to adding a custom view/xib to another view. However I was having problems with the size of my views. I 'solved' it by connecting an outlet called 'view' to my base UIView in my xib to my custom subclass of UIView then after the addSubView method in initWithCoder setting the frame with _view.frame = self.frame;Katey
Another reason for infinite recursion is setting the root view in the xib to your custom class. You don't want to do that, leave it as the default "UIView"Desex
There is just one thing that draws my attention about this approach... Doesn't it bother anyone that 2 Views are in the same class now? the Original File .h/.m inheriting from UIView being the first one and by doing [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Adding the Second UIView... This kinda looks wierd for me noone else feels the same way? If so is this something apple made by design ? Or is the solution someone found by doing this?Bashemath
A
39

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.

Aviate answered 18/10, 2013 at 8:19 Comment(5)
i inflate custom views all the time, but never knew this little trick to load it in to the storyboard. MUCH appreciated!Ladanum
The question is about views, not about view controllers.Munmro
Added title for clarification, "Answer for view controllers, not views:"Kunin
Does not appear to be working in iOS 9.1. If I remove the auto-created (default) view, it crashes after returning from the VC's viewDidLoad. I don't think the view in the XIB gets connected in place of the default view.Rigorism
The exception I'm getting is 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Note the whacko nibName. I'm using the correct class name for the in the nib.Rigorism
E
16

This is not really an answer, but I think it is helpful to share this approach.

Objective-C

  1. Import CustomViewWithXib.h and CustomViewWithXib.m to your project
  2. Create the custom view files with the same name (.h / .m / .xib)
  3. Inherit your custom class from CustomViewWithXib

Swift

  1. Import CustomViewWithXib.swift to your project
  2. Create the custom view files with the same name (.swift and .xib)
  3. Inherit your custom class from CustomViewWithXib

Optional :

  1. 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.

Eugenieeugenio answered 12/2, 2016 at 15:5 Comment(5)
so how we can add iboutlet for the view uicontrolsConstable
Hey @AmrAngry, to connect the views you should do the 4 step : Go to your xib file, in "File's Owner" set your class, and drag and drop your views to the interface by tapping the touch "ctrl", I update the code source with this example, don't hesitate if you have any question, hope that helpsEugenieeugenio
thanks a lot for your replay, i already do this but i guess my problem not in this part. my Problem is i add a UIDatePIcker and try to add a target action for this control from the ViewController to set and uiControleer event is value changed , but the method is never called/ fireConstable
@AmrAngry I update the example by adding the picker view, may be it will help you :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…Eugenieeugenio
Thank you I was looking for this for a long time.Scorpaenid

© 2022 - 2024 — McMap. All rights reserved.