69

I very rarely override drawRect in my UIView subclasses, usually preferring to set layer.contents with pre-rendering images and often employing multiple sublayers or subviews and manipulating these based on input parameters. Is there a way for IB to render these more complex view stacks?

Hari Honor
  • 8,677
  • 8
  • 51
  • 54
  • And .... http://stackoverflow.com/questions/37311649/layer-transform-matrix-catransform3d-etc-but-with-ibdesignable – Fattie May 18 '16 at 23:33

8 Answers8

142

Thanks, @zisoft for the clueing me in on prepareForInterfaceBuilder. There a few nuances with Interface Builder's render cycle which were the source of my issues and are worth noting...

  1. Confirmed: You don't need to use -drawRect.

Setting images on UIButton control states works. Arbitrary layer stacks seem to work if a few things are kept in mind...

  1. IB uses initWithFrame:

..not initWithCoder. awakeFromNib is also NOT called.

  1. init... is only called once per session

I.e. once per re-compile whenever you make a change in the file. When you change IBInspectable properties, init is NOT called again. However...

  1. prepareForInterfaceBuilder is called on every property change

It's like having KVO on all your IBInspectables as well as other built-in properties. You can test this yourself by having the your _setup method called, first only from your init.. method. Changing an IBInspectable will have no effect. Then add the call as well to prepareForInterfaceBuilder. Whahla! Note, your runtime code will probably need some additional KVO since it won't be calling the prepareForIB method. More on this below...

  1. init... is too soon to draw, set layer content, etc.

At least with my UIButton subclass, calling [self setImage:img forState:UIControlStateNormal] has no effect in IB. You need to call it from prepareForInterfaceBuilder or via a KVO hook.

  1. When IB fails to render, it doesn't blank our your component but rather keeps the last successful version.

Can be confusing at times when you are making changes that have no effect. Check the build logs.

  1. Tip: Keep Activity Monitor nearby

I get hangs all the time on a couple different support processes and they take the whole machine down with them. Apply Force Quit liberally.

(UPDATE: This hasn't really been true since XCode6 came out of beta. It seldom hangs anymore)

UPDATE

  1. 6.3.1 seems to not like KVO in the IB version. Now you seem to need a flag to catch Interface Builder and not set up the KVOs. This is ok as the prepareForInterfaceBuilder method effectively KVOs all the IBInspectable properties. It's unfortunate that this behaviour isn't mirrored somehow at runtime thus requiring the manual KVO. See the updated sample code below.

UIButton subclass example

Below is some example code of a working IBDesignable UIButton subclass. ~~Note, prepareForInterfaceBuilder isn't actually required as KVO listens for changes to our relevant properties and triggers a redraw.~~ UPDATE: See point 8 above.

IB_DESIGNABLE
@interface SBR_InstrumentLeftHUDBigButton : UIButton

@property (nonatomic, strong) IBInspectable  NSString *topText;
@property (nonatomic) IBInspectable CGFloat topTextSize;
@property (nonatomic, strong) IBInspectable NSString *bottomText;
@property (nonatomic) IBInspectable CGFloat bottomTextSize;
@property (nonatomic, strong) IBInspectable UIColor *borderColor;
@property (nonatomic, strong) IBInspectable UIColor *textColor;

@end



@implementation HUDBigButton
{
    BOOL _isInterfaceBuilder;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self _setup];
        
    }
    return self;
}

//---------------------------------------------------------------------

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self _setup];
    }
    return self;
}

//---------------------------------------------------------------------

- (void)_setup
{
    // Defaults.  
    _topTextSize = 11.5;
    _bottomTextSize = 18;
    _borderColor = UIColor.whiteColor;
    _textColor = UIColor.whiteColor;
}

//---------------------------------------------------------------------

- (void)prepareForInterfaceBuilder
{
    [super prepareForInterfaceBuilder];
    _isInterfaceBuilder = YES;
    [self _render];
}

//---------------------------------------------------------------------

- (void)awakeFromNib
{
    [super awakeFromNib];
    if (!_isInterfaceBuilder) { // shouldn't be required but jic...

        // KVO to update the visuals
        @weakify(self);
        [self
         bk_addObserverForKeyPaths:@[@"topText",
                                     @"topTextSize",
                                     @"bottomText",
                                     @"bottomTextSize",
                                     @"borderColor",
                                     @"textColor"]
         task:^(id obj, NSDictionary *keyPath) {
             @strongify(self);
             [self _render];
         }];
    }
}

//---------------------------------------------------------------------

- (void)dealloc
{
    if (!_isInterfaceBuilder) {
        [self bk_removeAllBlockObservers];
    }
}

//---------------------------------------------------------------------

- (void)_render
{
    UIImage *img = [SBR_Drawing imageOfHUDButtonWithFrame:self.bounds
                                                edgeColor:_borderColor
                                          buttonTextColor:_textColor
                                                  topText:_topText
                                              topTextSize:_topTextSize
                                               bottomText:_bottomText
                                       bottomTextSize:_bottomTextSize];
    
    [self setImage:img forState:UIControlStateNormal];
}

@end
Community
  • 1
  • 1
Hari Honor
  • 8,677
  • 8
  • 51
  • 54
  • 3
    Excellent answer - thank you! This is the best write-up I've seen on the subject. – RonDiamond Feb 27 '15 at 22:42
  • Any thoughts on why a UIButton with a boolean to toggle setting regular or bold font doesn't work via IBInspectable? It works for UILabel, but not UIButton. In my updateFont method (for UIButton subclass) I switch between regular and bold on self.titleLabel.font, but does not update in IB. – David James May 23 '15 at 13:26
  • Not sure but that problem sounds familiar. Using `setAttributedTitle` should work but it's a bit more convoluted. Also try reseting the title after you set the new font. – Hari Honor May 27 '15 at 08:28
  • Try all this things, but unfortunately have "Timed out" error during draw background image for subclass of `UIButton` – skywinder Jun 03 '15 at 08:54
  • I've added an answer http://stackoverflow.com/a/31325510/539149 that explains how to refresh the views if your view isn't updating in Interface Builder. – Zack Morris Jul 09 '15 at 18:33
  • 1
    I don't use KVO to track changes to the IBInspectables (too much overhead / code). I prefer to evaluate all layout-specific IBInspectables in layoutSubviews (the inspectables get set before the first layoutSubviews-call). If I really need to track a certain inspectable outside of layoutSubviews I just implement it's setter by hand. – marsbear Dec 15 '15 at 10:49
  • 1
    Just to clarify, IBDesignable uses `initWithFrame:` on iOS, but `initWithCoder:` on OS X. Yes, this is counter-intuitive, and is tracked in Radar (see http://www.openradar.me/19901337). In Cocoa documents, you can check "Prefer coder" in the Identity inspector to make run time (which used to default to `initWithFrame:`) match design time. For iOS documents, this mismatch still exists. – Quinn Taylor Mar 04 '16 at 22:32
  • In your point 5 you say: "init is too soon to draw". It's ok to me because I tried to print a CAShapeLayer in the init and it doesn't appear in the Storyboard. But why does this happen? Is there some docu related to this? – superpuccio Apr 19 '17 at 10:12
  • If not in interface builder and none of the observed properties are changed, `_render` is never called. Perhaps it should be called in `awakeFromNib`? – Nick Dec 16 '17 at 13:00
  • Also, prompt a redraw with 'Editor -> Refresh All Views' – wardw Jul 24 '18 at 18:38
16

This answer is related to overriding drawRect, but maybe it can give some ideas:

I have a custom UIView class which has complex drawings in drawRect. You have to take care about references which are not available during design time, i.e. UIApplication. For that, I override prepareForInterfaceBuilder where I set a boolean flag which I use in drawRect to distinguish between runtime and design time:

@IBDesignable class myView: UIView {
    // Flag for InterfaceBuilder
    var isInterfaceBuilder: Bool = false    

    override init(frame: CGRect) {
        super.init(frame: frame)
        // Initialization code
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func prepareForInterfaceBuilder() {
        self.isInterfaceBuilder = true
    }

    override func drawRect(rect: CGRect)
    {
        // rounded cornders
        self.layer.cornerRadius = 10
        self.layer.masksToBounds = true

        // your drawing stuff here

        if !self.isInterfaceBuilder {
            // code for runtime
            ...
        }

    }

}

An here is how it looks in InterfaceBuilder:

enter image description here

zisoft
  • 22,770
  • 10
  • 62
  • 73
9

You do not have to use drawRect, instead you can create your custom interface in a xib file, load it in initWithCoder and initWithFrame and it will be live rendering in IB after adding IBDesignable. Check this short tutorial: https://www.youtube.com/watch?v=L97MdpaF3Xg

Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
  • Thank you. This video was exactly what I was looking for. I needed an Objc example that properly sets up the view from the nib. When I tried other examples online, xcode would say that the view timed out when rendering. – drudru Jan 16 '15 at 22:44
  • Sometimes you just forget to add @IBDesignable :) – ergunkocak Aug 13 '16 at 20:17
  • In this video the developer don't use the prepareForInterfaceBuilder method, instead he/she relies on initWithFrame to do the init stuff. So, when is prepareForInterfaceBuilder needed? :-/ – superpuccio Feb 15 '19 at 17:55
7

I think layoutSubviews is the simplest mechanism.

Here is a (much) simpler example in Swift:

@IBDesignable
class LiveLayers : UIView {

    var circle:UIBezierPath {
        return UIBezierPath(ovalInRect: self.bounds)
    }

    var newLayer:CAShapeLayer {
        let shape = CAShapeLayer()
        self.layer.addSublayer(shape)
        return shape
    }
    lazy var myLayer:CAShapeLayer = self.newLayer

    // IBInspectable proeprties here...
    @IBInspectable var pathLength:CGFloat = 0.0 { didSet {
        self.setNeedsLayout()
    }}

    override func layoutSubviews() {
        myLayer.frame = self.bounds // etc
        myLayer.path = self.circle.CGPath
        myLayer.strokeEnd = self.pathLength
    }
}

I haven't tested this snippet, but have used patterns like this before. Note the use of the lazy property delegating to a computed property to simplify initial configuration.

Chris Conover
  • 8,889
  • 5
  • 52
  • 68
  • 1
    I am curious, why the downvotes? It's simpler, cleaner, works with autolayout, and as far as I can tell, is the textbook way of doing things. I've used this technique to draw custom controls that can set initially, from IB, and updated after the view has been displayed. – Chris Conover Jun 13 '15 at 05:14
  • Yeah I'm not sure. I've given you a +1. – Hari Honor Dec 22 '15 at 09:44
  • One drawback is that layoutSubviews could be called multiple times, when constraints changes, and when you do animations. – Paweł Brewczynski Jan 31 '17 at 20:28
7

To elaborate upon Hari Karam Singh's answer, this slideshow explains further:

http://www.splinter.com.au/presentations/ibdesignable/

Then if you aren't seeing your changes show up in Interface Builder, try these menus:

  • Xcode->Editor->Automatically Refresh Views
  • Xcode->Editor->Refresh All Views
  • Xcode->Editor->Debug Selected Views

Unfortunately, debugging my view froze Xcode, but it should work for small projects (YMMV).

Community
  • 1
  • 1
Zack Morris
  • 4,727
  • 2
  • 55
  • 83
4

In my case, there were two problems:

  1. I did not implement initWithFrame in custom view: (Usually initWithCoder: is called when you initialize via IB, but for some reason initWithFrame: is needed for IBDesignable only. Is not called during runtime when you implement via IB)

  2. My custom view's nib was loading from mainBundle: [NSBundle bundleForClass:[self class]] was needed.

adamjansch
  • 1,170
  • 11
  • 22
Jakub Truhlář
  • 20,070
  • 9
  • 74
  • 84
2

I believe you can implement prepareForInterfaceBuilder and do your core animation work in there to get it to show up in IB. I've done some fancy things with subclasses of UIButton that do their own core animation layer work to draw borders or backgrounds, and they live render in interface builder just fine, so i imagine if you're subclassing UIView directly, then prepareForInterfaceBuilder is all you'll need to do differently. Keep in mind though that the method is only ever executed by IB

Edited to include code as requested

I have something similar to, but not exactly like this (sorry I can't give you what I really do, but it's a work thing)

class BorderButton: UIButton {
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    func commonInit(){
        layer.borderWidth = 1
        layer.borderColor = self.tintColor?.CGColor
        layer.cornerRadius = 5    
    }

    override func tintColorDidChange() {
        layer.borderColor = self.tintColor?.CGColor
    }

    override var highlighted: Bool {
        willSet {
            if(newValue){
                layer.backgroundColor = UIColor(white: 100, alpha: 1).CGColor
            } else {
                layer.backgroundColor = UIColor.clearColor().CGColor
            }
        }
    }
}

I override both initWithCoder and initWithFrame because I want to be able to use the component in code or in IB (and as other answers state, you have to implement initWithFrame to make IB happy.

Then in commonInit I set up the core animation stuff to draw a border and make it pretty.

I also implement a willSet for the highlighted variable to change the background color because I hate when buttons draw borders, but don't provide feedback when pressed (i hate it when the pressed button looks like the unpressed button)

Alpine
  • 1,347
  • 12
  • 13
1

Swift 3 macro

#if TARGET_INTERFACE_BUILDER
#else
#endif

and class with function which is called when IB renders storyboard

@IBDesignable
class CustomView: UIView
{
    @IBInspectable
    public var isCool: Bool = true {
        didSet {
            #if TARGET_INTERFACE_BUILDER

            #else

            #endif
        }
    }

    override func prepareForInterfaceBuilder() {
        // code
    }
}

IBInspectable can be used with types below

Int, CGFloat, Double, String, Bool, CGPoint, CGSize, CGRect, UIColor, UIImage
Ako
  • 956
  • 1
  • 10
  • 13