有没有一种方法让Interface Builder渲染IBDesignable视图而不覆盖drawRect:方法?

69
在我的UIView子类中,我很少覆盖drawRect方法,通常更喜欢使用预先渲染的图像设置layer.contents,并经常使用多个子层或子视图并基于输入参数对其进行操作。是否有一种方法让IB渲染这些更复杂的视图堆栈?

而且....https://dev59.com/Rpffa4cB1Zd3GeqP4TKi - Fattie
8个回答

142
感谢@zisoft提供的prepareForInterfaceBuilder线索。在Interface Builder的渲染周期中有一些细微差别,这是我的问题来源,并值得注意...
  1. 确认:您不需要使用-drawRect

设置UIButton控件状态中的图像有效。如果记住了一些事情,任意层堆栈似乎也有效...

  1. IB使用initWithFrame:

..而不是initWithCoderawakeFromNib也没有被调用。

  1. init...每个会话只调用一次

即每次在文件中进行更改时重新编译时调用一次。当更改IBInspectable属性时,不会再次调用init。但是...

  1. prepareForInterfaceBuilder在每次属性更改时都会被调用

就像在所有IBInspectables以及其他内置属性上都有KVO一样。您可以通过首先仅从init..方法调用您的_setup方法来测试此功能。更改IBInspectable将不起作用。然后还要添加调用到prepareForInterfaceBuilder。Whahla!请注意,您的运行时代码可能需要一些额外的KVO,因为它不会调用prepareForIB方法。有关此问题的更多信息,请参见下文...

  1. init...太早绘制、设置层内容等。

至少对于我的UIButton子类,在IB中调用[self setImage:img forState:UIControlStateNormal]没有效果。您需要从prepareForInterfaceBuilder或通过KVO挂钩调用它。

  1. 当IB无法渲染时,它不会清空组件,而是保留最后一个成功版本。

有时在进行没有效果的更改时可能会令人困惑。检查构建日志。

  1. 提示:保持Activity Monitor附近

我经常在几个不同的支持进程上遇到停顿,并且它们会将整个机器拖垮。自由使用Force Quit

(更新:自XCode6退出beta以来,这并不真实。它很少再挂起了)

更新

  1. 6.3.1似乎不喜欢IB版本中的KVO。现在,你需要一个标志来捕捉Interface Builder并且不设置KVOs。这是可以的,因为prepareForInterfaceBuilder方法有效地KVO了所有的IBInspectable属性。遗憾的是,这种行为在运行时没有以某种方式反映出来,因此需要手动进行KVO。请参见下面更新后的示例代码。

UIButton子类示例

下面是一个工作中的IBDesignable UIButton子类的示例代码。注意,prepareForInterfaceBuilder实际上不是必需的,因为KVO会监听我们相关属性的更改并触发重绘。更新:请参见第8点。

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

3
非常好的回答,谢谢!这是我看过的关于这个主题最好的阐述。 - RonDiamond
不确定,但那个问题听起来很熟悉。使用 setAttributedTitle 应该可以解决,但它有点复杂。在设置新字体后,也尝试重置标题。 - Hari Honor
尝试了所有的方法,但是在为UIButton的子类绘制背景图像时遇到“超时”错误。 - skywinder
1
我不使用KVO来跟踪IBInspectables的更改(开销/代码太大)。我更喜欢在layoutSubviews中评估所有特定于布局的IBInspectables(在第一次layoutSubviews调用之前设置检查项)。如果我真的需要在layoutSubviews之外跟踪某个检查项,我只需手动实现它的setter。 - marsbear
1
只是为了澄清一下,IBDesignable在iOS上使用initWithFrame:,而在OS X上使用initWithCoder:。是的,这是令人困惑的,并且在Radar中有记录(请参见http://www.openradar.me/19901337)。在Cocoa文档中,您可以在标识检查器中选择“Prefer coder”来使运行时(以前默认为initWithFrame:)与设计时匹配。对于iOS文档,这个不匹配仍然存在。 - Quinn Taylor
显示剩余5条评论

16

这个回答与重写drawRect有关,但也许它可以提供一些思路:

我有一个自定义的UIView类,在drawRect中有复杂的绘图。您必须注意在设计时间不可用的引用,即UIApplication。为此,我重写了prepareForInterfaceBuilder,在那里我设置一个布尔标志,我在drawRect中使用它来区分运行时和设计时:

@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
            ...
        }

    }

}

这是在 InterfaceBuilder 中的样子:

enter image description here


9
显然也有一个编译器标志可以解决这个问题:#if !TARGET_INTERFACE_BUILDER - Hari Honor
这是否意味着每次小部件中的数字更新时,您都要重新绘制整个内容? - Hari Honor
我不确定你是否应该在视图中使用复杂的运行时代码,@zisoft。 - marcelosalloum

9

您不必使用drawRect,而是可以在xib文件中创建自定义接口,在initWithCoder和initWithFrame中加载它,添加IBDesignable后将在IB中进行实时渲染。查看这个简短的教程:https://www.youtube.com/watch?v=L97MdpaF3Xg


谢谢。这个视频正是我所需要的。我需要一个Objc示例,可以正确地从nib设置视图。当我尝试其他在线示例时,xcode会说视图在渲染时超时。 - drudru
有时候你只是忘了添加 @IBDesignable :) - ergunkocak
在这个视频中,开发者没有使用prepareForInterfaceBuilder方法,而是依赖initWithFrame来完成初始化工作。那么,什么时候需要使用prepareForInterfaceBuilder呢? :-/ - superpuccio

7

我认为layoutSubviews是最简单的机制。

下面是一个更简单的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
    }
}

我没有测试过这段代码片段,但之前使用过类似的模式。请注意使用lazy属性委托到计算属性来简化初始配置。


1
我很好奇,为什么会有人踩这个?这种做法更简单、更清晰,适用于自动布局,而且据我所知,这是标准的做法。我使用这种技术来绘制自定义控件,可以在 IB 中进行初始设置,并在显示视图后进行更新。 - Chris Conover
是的,我不确定。我已经给了你一个+1。 - Hari Honor
一个缺点是当约束改变和执行动画时,layoutSubviews 可能会被调用多次。 - Paweł Brewczynski

7

为了阐述Hari Karam Singh的答案,这个幻灯片向您进一步解释:

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

如果您在Interface Builder中没有看到您的更改,请尝试以下菜单:

  • Xcode->Editor->Automatically Refresh Views(Xcode->编辑器->自动刷新视图)
  • Xcode->Editor->Refresh All Views(Xcode->编辑器->刷新所有视图)
  • Xcode->Editor->Debug Selected Views(Xcode->编辑器->调试选定视图)

不幸的是,调试我的视图会使Xcode冻结,但对于小型项目应该可以正常工作(YMMV)。


4
在我的情况下,有两个问题:
  1. 我没有在自定义视图中实现 initWithFrame(通常情况下,通过IB初始化时会调用initWithCoder:,但由于某些原因,只有在IBDesignable中才需要调用initWithFrame:。在通过IB实现时不会在运行时调用)

  2. 我的自定义视图的nib文件是从mainBundle加载的:需要使用[NSBundle bundleForClass:[self class]]


2
我相信你可以实现prepareForInterfaceBuilder并在其中进行核心动画工作,以使其在IB中显示出来。 我已经用UIButton的子类做了一些花哨的事情,它们自己做核心动画层的工作来绘制边框或背景,并且在界面构建器中实时渲染,所以我想如果你直接子类化UIView,那么prepareForInterfaceBuilder就是你需要做的全部不同之处。但请记住,该方法只会被IB执行。

编辑以包括请求的代码

我的代码与此类似,但不完全相同(抱歉我不能给你我真正做的事情,因为这是一项工作)

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
            }
        }
    }
}

我重写了initWithCoderinitWithFrame,因为我希望能在代码或IB中使用该组件(正如其他答案所述,你必须实现initWithFrame以使IB happy)。接着,在commonInit中,我设置了核心动画,以绘制边框并使其变得美观。此外,我还实现了一个willSet来改变背景颜色,因为我讨厌那些按钮会绘制边框但按下时没有反馈的情况(我讨厌按下按钮看起来与未按下按钮相同的情况)。

如果您不介意分享的话,我很想看看您的UIButton代码是如何工作的... - Hari Honor

1

Swift 3

#if TARGET_INTERFACE_BUILDER
#else
#endif

并带有函数的类,当IB渲染故事板时调用该函数。

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

            #else

            #endif
        }
    }

    override func prepareForInterfaceBuilder() {
        // code
    }
}

IBInspectable可以用于以下类型

Int, CGFloat, Double, String, Bool, CGPoint, CGSize, CGRect, UIColor, UIImage

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接