Custom UIView subclass with XIB in Swift

时间:2015-09-01 22:57:02

标签: ios swift uiview interface-builder xib

I'm using Swift and Xcode 6.4 for what it's worth.

So I have a view controller that will be containing some multiple pairs of UILabels and UIImageViews. I wanted to put the UILabel-UIImageView pair into a custom UIView, so I could simply reuse the same structure repeatedly within the aforementioned view controller. (I'm aware this could be translated into a UITableView, but for the sake of ~learning~ please bear with me). This is turning out to be a more convoluted process than I imagined it would be, I'm having trouble figuring out the "right" way to make this all work in IB.

Currently I've been floundering around with a UIView subclass and corresponding XIB, overriding init(frame:) and init(coder), loading the view from the nib and adding it as a subview. This is what I've seen/read around the internet so far. (This is approximately it: http://iphonedev.tv/blog/2014/12/15/create-an-ibdesignable-uiview-subclass-with-code-from-an-xib-file-in-xcode-6).

This gave me the problem of causing an infinite loop between init(coder) and loading the nib from the bundle. Strangely none of these articles or previous answers on stack overflow mention this!

Ok so I put a check in init(coder) to see if the subview had already been added. That "solved" that, seemingly. However I started running into an issue with my custom view outlets being nil by the time I try to assign values to them.

I made a didSet and added a breakpoint to take a look...they are definitely being set at one point, but by the time I try to, say, modify the textColor of a label, that label is nil. I'm kind of tearing my hair out here.

Reusable components seem like software design 101, but I feel like Apple is conspiring against me. Should I be looking to use container VCs here? Should I just be nesting views and having a stupidly huge amount of outlets in my main VC? Why is this so convoluted? Why do everyone's examples NOT work for me?

Desired result (pretend the whole thing is the VC, the boxes are the custom uiviews I want):

enter image description here

Thanks for reading.

Following is my custom UIView subclass. In my main storyboard, I have UIViews with the subclass set as their class.

class StageCardView: UIView {

@IBOutlet weak private var stageLabel: UILabel! {
    didSet {
        NSLog("I will murder you %@", stageLabel)
    }
}
@IBOutlet weak private var stageImage: UIImageView!

var stageName : String? {
    didSet {
        self.stageLabel.text = stageName
    }
}
var imageName : String? {
    didSet {
        self.stageImage.image = UIImage(named: imageName!)
    }
}
var textColor : UIColor? {
    didSet {
        self.stageLabel.textColor = textColor
    }
}
var splatColor : UIColor? {
    didSet {
        let splatImage = UIImage(named: "backsplat")?.tintedImageWithColor(splatColor!)
        self.backgroundColor = UIColor(patternImage: splatImage!)
    }
}

// MARK: init
required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    if self.subviews.count == 0 {
        setup()
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}

func setup() {
    if let view = NSBundle.mainBundle().loadNibNamed("StageCardView", owner: self, options: nil).first as? StageCardView {
        view.frame = bounds
        view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight

        addSubview(view)
    }
}




/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
    // Drawing code
}
*/

}

EDIT: Here's what I've been able to get so far...

XIB:

enter image description here

Result:

enter image description here

Problem: When trying to access label or image outlets, they are nil. When checking at breakpoint of said access, the label and image subviews are there and the view hierarchy is as expected.

I'm OK with doing this all in code if thats what it takes, but I'm not huge into doing Autolayout in code so I'd rather not if there's a way to avoid it!

EDIT/QUESTION SHIFT:

I figured out how to make the outlets stop being nil.

enter image description here

Inspiration from this SO answer: Loaded nib but the view outlet was not set - new to InterfaceBuilder except instead of assigning the view outlet I assigned the individual component outlets.

Now this was at the point where I was just flinging shit at a wall and seeing if it'd stick. Does anyone know why I had to do this? What sort of dark magic is this?

2 个答案:

答案 0 :(得分:12)

关于查看重用的一般建议

你是对的,可重用和可组合的元素是软件101.Interface Builder不是很擅长。

具体来说,xib和storyboard是通过重用代码中定义的视图来定义视图的好方法。但是它们不适合定义您自己希望在xib和storyboard中重用的视图。 (可以这样做,但这是一项高级练习。)

所以,这是一条经验法则。如果要定义要从代码中重用的视图,请根据需要进行定义。但是,如果您要定义一个您希望能够在故事板中重用的视图,那么请在代码中定义该视图。

因此,在您的情况下,如果您尝试定义要从故事板重复使用的自定义视图,我会在代码中执行此操作。如果您已经决定通过xib定义视图,那么我将在代码中定义一个视图,并在其初始化程序中初始化您的xib定义视图并将其配置为子视图。

在这种情况下的建议

以下是您在代码中定义视图的方式:

class StageCardView: UIView {
  var stageLabel = UILabel(frame:CGRectZero)
  var stageImage = UIImageView(frame:CGRectZero)
  override init(frame:CGRect) {
   super.init(frame:frame)
   setup()
  }
  required init(coder aDecoder:NSCoder) { 
   super.init(coder:aDecoder)  
   setup()
  }
  private func setup() {
    stageImage.image = UIImage(named:"backsplat")
    self.addSubview(stageLabel)
    self.addSubview(stageImage)
    // configure the initial layout of your subviews here.
  }
}

您现在可以在代码中或通过故事板进行实例化,但您不会按原样在Interface Builder中进行实时预览。

另外,通过在代码定义的视图中嵌入xib定义的视图,大致如何定义基于xib的可重用视图:

class StageCardView: UIView {
  var embeddedView:EmbeddedView!
  override init(frame:CGRect) {
   super.init(frame:frame)
   setup()
  }
  required init(coder aDecoder:NSCoder) { 
   super.init(coder:aDecoder)  
   setup()
  }
  private func setup() {
    self.embeddedView = NSBundle.mainBundle().loadNibNamed("EmbeddedView",owner:self,options:nil).lastObject as! UIView
    self.addSubview(self.embeddedView)
    self.embeddedView.frame = self.bounds
    self.embeddedView.autoresizingMask = .FlexibleHeight | .FlexibleWidth
  }
}

现在您可以使用故事板或代码中的代码定义视图,它将加载其nib定义的子视图(并且在IB中仍然没有实时预览)。

答案 1 :(得分:3)

我能够解决它,但解决方案有点棘手。如果获得的收益值得努力,那就讨论了,但这是我在界面构建器中实现它的方式

首先,我定义了一个名为UIView

的自定义P2View子类
@IBDesignable class P2View: UIView
{
    @IBOutlet private weak var titleLabel: UILabel!
    @IBOutlet private weak var iconView: UIImageView!

    @IBInspectable var title: String? {
        didSet {
            if titleLabel != nil {
                titleLabel.text = title
            }
        }
    }

    @IBInspectable var image: UIImage? {
        didSet {
            if iconView != nil {
                iconView.image = image
            }
        }
    }

    override init(frame: CGRect)
    {
        super.init(frame: frame)
        awakeFromNib()
    }

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

    override func awakeFromNib()
    {
        super.awakeFromNib()

        let bundle = Bundle(for: type(of: self))

        guard let view = bundle.loadNibNamed("P2View", owner: self, options: nil)?.first as? UIView else {
            return
        }

        view.translatesAutoresizingMaskIntoConstraints = false
        addSubview(view)

        let bindings = ["view": view]

        let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"V:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)
        let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"H:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)

        addConstraints(verticalConstraints)
        addConstraints(horizontalConstraints)        
    }

    titleLabel.text = title
    iconView.image = image
}

这就是界面构建器

中的样子

Xib definition

这就是我在故事板上定义的示例视图控制器中嵌入此自定义视图的方法。 P2View的属性在属性检查器中设置。

Custom view embedded in the view controller

有三点值得一提

首先:

加载笔尖时使用Bundle(for: type(of: self))。这是因为界面构建器在单独的进程中呈现可设计的主捆绑包与主捆绑包不同。

第二名:

    @IBInspectable var title: String? {
        didSet {
            if titleLabel != nil {
                titleLabel.text = title
            }
        }
    }

IBInspectablesIBOutlets合并后,您必须记住在didSet方法之前调用awakeFromNib函数。因此,出口未初始化,您的应用程序可能会在此时崩溃。不幸的是,你不能省略didSet函数,因为界面构建器不会渲染你的自定义视图,所以如果在这里我们不得不把它弄脏。

<强>第三

    titleLabel.text = title
    iconView.image = image

我们必须以某种方式初始化我们的控件。调用didSet函数时我们无法执行此操作,因此我们必须使用IBInspectable属性中存储的值并在awakeFromNib方法的末尾初始化它们。

这是您可以在Xib上实现自定义视图,将其嵌入故事板,在故事板上进行配置,渲染并具有非崩溃应用程序的方法。它需要一个黑客,但它是可能的。

相关问题