118

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.

Michael Campsall
  • 4,135
  • 9
  • 32
  • 49
  • 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](http://stackoverflow.com/questions/23399061/objective-c-how-to-add-a-subview-that-has-its-own-uiviewcontroller/23403979#23403979) , [example](https://www.raywenderlich.com/86521/how-to-make-a-view-controller-transition-animation-like-in-the-ping-app) – Fattie Mar 19 '16 at 19:02
  • 2
    @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. – Echelon Sep 02 '16 at 10:40

4 Answers4

146

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

Suragch
  • 364,799
  • 232
  • 1,155
  • 1,198
  • 36
    Important not to set Xib view class as ResuableCustomView, but Owner of this Xib. Otherwise you will have error. – RealNmae Jan 05 '16 at 11:01
  • 7
    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. – Suragch Jan 06 '16 at 05:54
  • Good answer, your tutorial helped me a lot, thank you... but It didn't worked with autolayout. I followed http://stackoverflow.com/a/32062951/3052059 to get It working. – Thomás Pereira Jan 16 '16 at 15:08
  • 1
    Here is a clear answer I wrote with support for autolayout. http://stackoverflow.com/questions/30335089/create-a-uiview-xib-and-reuse-in-storyboard – Garfbargle Jan 19 '16 at 02:18
  • 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 https://gist.github.com/Blackjacx/c265534d5219e28f9147 that manages all the stuff to do. It is soooo simple! – blackjacx Mar 11 '16 at 20:35
  • 2
    @TomCalmon, I redid this project in Xcode 8 with Swift 3 and Auto Layout was working ok for me. – Suragch Sep 24 '16 at 06:13
  • 7
    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. – crizzis Oct 03 '16 at 09:42
  • 1
    It worked in Objective-C as well. Thank you @Suragch – KarenAnne Jan 05 '17 at 09:26
  • @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 unfortunately – Fattie Jan 14 '17 at 15:00
  • @crizzis, correct. You must use `Bundle(for:MyView.self)`. – Peter DeWeese Apr 27 '17 at 13:15
  • 5
    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: https://stackoverflow.com/questions/31265906/ibdesignable-crashing-agent – Dimitris Dec 02 '18 at 08:35
  • I am getting this error for the label otulet. Any idea? Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key label – Fmessina Aug 19 '19 at 12:02
  • 1
    @Fmessina, I'm not sure off hand but check out [this](https://stackoverflow.com/q/11118135/3681880), [this](https://stackoverflow.com/q/4779561/3681880) and [this](https://stackoverflow.com/q/3088059/3681880). – Suragch Aug 19 '19 at 13:25
  • 1
    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! :) – Neph Mar 18 '21 at 14:08
142

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.

Community
  • 1
  • 1
NJones
  • 26,931
  • 7
  • 68
  • 87
  • 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;` – NJones Feb 14 '12 at 23:46
  • 28
    I get an infinite recursion from initWithCoder. Any ideas? – disappearedng Feb 21 '13 at 05:39
  • 6
    Sorry to revive an old thread, making sure `File's Owner` is set to your class name solved my infinite recursion problem. – themanatuf May 13 '13 at 23:06
  • 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;` – MathewS Jun 13 '13 at 03:08
  • 20
    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" – X.Y. Jun 16 '13 at 22:50
  • 11
    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? – S.H. Nov 15 '13 at 01:22
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.

nacho4d
  • 39,335
  • 42
  • 151
  • 231
jyavenard
  • 2,071
  • 22
  • 35
  • 1
    i inflate custom views all the time, but never knew this little trick to load it in to the storyboard. MUCH appreciated! – MrTristan Feb 13 '14 at 18:20
  • 38
    The question is about views, not about view controllers. – Ricardo Mar 18 '14 at 10:01
  • Added title for clarification, "Answer for view controllers, not views:" – nacho4d Mar 04 '15 at 05:59
  • 1
    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. – Jeff Oct 31 '15 at 09:20
  • 1
    The exception I'm getting is `'Could not load NIB in bundle: 'NSBundle (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. – Jeff Nov 01 '15 at 05:36
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.

HamzaGhazouani
  • 5,674
  • 5
  • 28
  • 38
  • so how we can add iboutlet for the view uicontrols – Amr Angry Aug 24 '16 at 16:23
  • 1
    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 helps – HamzaGhazouani Aug 25 '16 at 08:07
  • 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/ fire – Amr Angry Aug 27 '16 at 12:20
  • 1
    @AmrAngry I update the example by adding the picker view, may be it will help you :) https://github.com/HamzaGhazouani/Stackoverflow/tree/master/CustomViewsProject – HamzaGhazouani Sep 01 '16 at 10:49
  • 1
    Thank you I was looking for this for a long time. – MH175 Apr 23 '17 at 22:48