简体   繁体   English

Swift中XIB的自定义UIView子类

[英]Custom UIView subclass with XIB in Swift

I'm using Swift and Xcode 6.4 for what it's worth. 我正在使用Swift和Xcode 6.4来实现其价值。

So I have a view controller that will be containing some multiple pairs of UILabels and UIImageViews. 因此,我有一个视图控制器,其中将包含一些多对UILabel和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. 我想将UILabel-UIImageView对放入自定义UIView中,因此我可以简单地在上述视图控制器中重复使用相同的结构。 (I'm aware this could be translated into a UITableView, but for the sake of ~learning~ please bear with me). (我知道这可以转换为UITableView,但是为了〜学习〜请耐心等待)。 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. 事实证明,这比我想象的要复杂得多,我在弄清楚使这一切在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. 目前,我一直在苦苦研究UIView子类和相应的XIB,覆盖init(frame :)和init(coder),从笔尖加载视图并将其添加为子视图。 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 ). (大约是这样: 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. 这给了我一个问题,该问题导致init(coder)和从包中加载笔尖之间出现无限循环。 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. 好的,所以我检查init(coder)以查看是否已经添加了子视图。 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. 我做了一个didSet并添加了一个断点来查看...它们肯定被设置在一点上,但是当我尝试修改标签的textColor时,该标签为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. 可重用组件看起来像软件设计101,但我感觉苹果正在密谋反对我。 Should I be looking to use container VCs here? 我应该在这里使用容器VC吗? Should I just be nesting views and having a stupidly huge amount of outlets in my main VC? 我是否应该只是嵌套视图并在我的主要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): 所需的结果(假装整个东西是VC,方框是我想要的自定义uiview):

在此处输入图片说明

Thanks for reading. 谢谢阅读。

Following is my custom UIView subclass. 以下是我的自定义UIView子类。 In my main storyboard, I have UIViews with the subclass set as their class. 在我的主故事板上,我将UIViews设置为子类。

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: XIB:

在此处输入图片说明

Result: 结果:

在此处输入图片说明

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. 我想出了如何使网点停止为零的方法。

在此处输入图片说明

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. 这样的答案的启示: 装入了笔尖,但未设置视图出口-InterfaceBuilder的新增功能,除了分配视图出口外,我分配了各个组件出口。

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? 这是什么黑魔法?

General advice on view re-use 有关视图重用的一般建议

You're right, re-usable and composable elements is software 101. Interface Builder is not very good at it. 没错,软件101是可重用和可组合的元素。InterfaceBuilder并不是很好。

Specifically, xibs and storyboard are great ways to define views by re-using views that are defined in code. 具体地说,XIB和情节提要是通过重用代码中定义的视图来定义视图的好方法。 But they are not very good for defining views that you yourself wish to re-use within xibs and storyboards. 但是它们对于定义您自己希望在xib和情节提要中重用的视图不是很好。 (It can be done, but it is an advanced exercise.) (可以完成,但这是高级练习。)

So, here's a rule of thumb. 因此,这是一个经验法则。 If you are defining a view that you want to re-use from code, then define it however you wish. 如果要定义要从代码中重复使用的视图,则可以根据需要定义它。 But if you are defining a view that you want to be able to re-use possibly from within a storyboard, then define that view in code. 但是,如果您要定义一个希望能够从情节提要中重复使用的视图,请在代码中定义该视图。

So in your case, if you're trying to define a custom view which you want to re-use from a storyboard, I'd do it in code. 因此,在您的情况下,如果您想定义一个要从情节提要中重复使用的自定义视图,则可以在代码中完成。 If you are dead set on defining your view via a xib, then I'd define a view in code and in its initializer have it initialize your xib-defined view and configure that as a subview. 如果您不愿意通过xib定义视图,那么我将在代码中定义一个视图,并在其初始化程序中让它初始化xib定义的视图并将其配置为子视图。

Advice in this case 在这种情况下的建议

Here's roughly how you'd define your view in code: 这大致是您在代码中定义视图的方式:

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

You can now instantiate this in code and or via a storyboard, although you won't get a live preview in Interface Builder as is. 现在,您可以在代码中或通过情节提要实例化此实例,尽管您将无法在Interface Builder中获得实时预览。

And alternatively, here's roughly how you might define a re-usable view based fundamentally on a xib, by embedding the xib-defined view in a code-defined view: 另外,这里大致介绍了如何通过将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
  }
}

Now you can use the code-defined view from storyboards or from code, and it will load its nib-defined subview (and there's still no live preview in IB). 现在,您可以从情节提要或代码中使用代码定义的视图,它将加载其笔尖定义的子视图(并且IB中仍然没有实时预览)。

I was able to work it around but the solution is a little bit tricky. 我可以解决它,但是解决方案有点棘手。 It's up to debate if the gain is worth an effort but here is how I implemented it purely in interface builder 是否值得付出努力尚有争议,但这是我纯粹在接口生成器中实现的方法

First I defined a custom UIView subclass named P2View 首先,我定义了一个名为P2View的自定义UIView子类。

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

This is how it looks like in interface builder 这是界面生成器中的外观

Xib定义

This is how I embedded this custom view in the example view controller defined on a storyboard. 这就是我将此自定义视图嵌入到故事板上定义的示例视图控制器中的方式。 Properties of P2View are set in the attributes inspector. 在属性检查P2View中设置P2View的属性。

嵌入视图控制器中的自定义视图

There are 3 points worth mentioning 有3点值得一提

First: 第一:

Use the Bundle(for: type(of: self)) when loading the nib. 加载笔尖时使用Bundle(for: type(of: self)) This is because the interface builder renders the designables in the separate process which main bundle is not the same as your main bundle. 这是因为界面生成器在单独的过程中渲染可设计项,而主捆绑包与您的主捆绑包不同。

Second: 第二:

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

When combining IBInspectables with IBOutlets you have to remember that the didSet functions are called before awakeFromNib method. IBInspectablesIBOutlets结合使用时,必须记住在awakeFromNib方法之前调用didSet函数。 Because of that, the outlets are not initialized and your app will probably crash at this point. 因此,出口未初始化,此时您的应用可能会崩溃。 Unfortunatelly you cannot omit the didSet function because the interface builder won't render your custom view so we have to leave this dirty if here. 不幸的是,您不能省略didSet函数,因为接口构建器不会呈现您的自定义视图,因此,如果在这里,我们必须将其保留为脏状态。

Third: 第三:

    titleLabel.text = title
    iconView.image = image

We have to somehow initialize our controls. 我们必须以某种方式初始化控件。 We were not able to do it when didSet function was called so we have to use the value stored in the IBInspectable properties and initialize them at the end of the awakeFromNib method. 调用didSet函数时,我们无法执行此操作,因此我们必须使用IBInspectable属性中存储的值,并在awakeFromNib方法的末尾对其进行初始化。

This is how you can implement a custom view on a Xib, embed it on a storyboard, configure it on a storyboard, have it rendered and have a non-crashing app. 这是您可以在Xib上实现自定义视图,将其嵌入到情节提要中,在情节提要上进行配置,渲染并具有不崩溃的应用程序的方式。 It requires a hack, but it's possible. 它需要破解,但是有可能。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM