【问题标题】:Custom UIView subclass with XIB in Swift在 Swift 中使用 XIB 自定义 UIView 子类
【发布时间】:2015-11-27 07:29:07
【问题描述】:

我正在使用 Swift 和 Xcode 6.4 来获得它的价值。

所以我有一个视图控制器,它将包含一些多对 UILabel 和 UIImageViews。我想将 UILabel-UIImageView 对放入自定义 UIView 中,因此我可以简单地在上述视图控制器中重复使用相同的结构。 (我知道这可以翻译成 UITableView,但为了~学习~请多多包涵)。事实证明,这比我想象的要复杂得多,我很难找出“正确”的方法来让这一切在 IB 中发挥作用。

目前我一直在纠结于 UIView 子类和相应的 XIB,覆盖 init(frame:) 和 init(coder),从 nib 加载视图并将其添加为子视图。到目前为止,这是我在互联网上看到/阅读的内容。 (大概是这样:http://iphonedev.tv/blog/2014/12/15/create-an-ibdesignable-uiview-subclass-with-code-from-an-xib-file-in-xcode-6)。

这给了我一个导致 init(coder) 和从包中加载 nib 之间无限循环的问题。奇怪的是,这些文章或以前关于堆栈溢出的答案都没有提到这一点!

好的,所以我检查了 init(coder) 以查看是否已经添加了子视图。这似乎“解决了”这个问题。但是,当我尝试为它们分配值时,我开始遇到我的自定义视图出口为零的问题。

我做了一个 didSet 并添加了一个断点来查看...它们肯定是在某个点设置的,但是当我尝试修改标签的 textColor 时,该标签为 nil。 我有点扯我的头发在这里。

可重用组件看起来像是软件设计 101,但我觉得 Apple 正在密谋反对我。我应该在这里使用容器 VC 吗?我应该只是嵌套视图并在我的主要 VC 中拥有大量愚蠢的渠道吗?为什么这么复杂?为什么每个人的例子都不适合我?

想要的结果(假装整个是VC,盒子是我想要的自定义uiviews):

感谢阅读。

以下是我的自定义 UIView 子类。在我的主情节提要中,我将 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
}
*/

}

编辑:这是我目前所能得到的......

XIB:

结果:

问题:当尝试访问标签或图像出口时,它们是 nil。在所述访问的断点处检查时,标签和图像子视图都在那里,并且视图层次结构符合预期。

如果需要的话,我可以在代码中完成这一切,但我并不热衷于在代码中执行 Autolayout,所以如果有办法避免它,我宁愿不这样做!

编辑/问题转换:

我想出了如何让 outlet 不再为零。

来自这个 SO 答案的灵感:Loaded nib but the view outlet was not set - new to InterfaceBuilder 除了分配视图出口之外,我分配了各个组件出口。

现在,我只是在墙上扔屎,看看它是否会粘住。有谁知道我为什么要这样做?这是什么黑魔法?

【问题讨论】:

  • 你找到解决办法了吗?

标签: ios swift uiview interface-builder xib


【解决方案1】:

我能够解决它,但解决方案有点棘手。收益是否值得努力尚有争议,但这是我纯粹在界面构建器中实现它的方式

首先我定义了一个名为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
}

这就是它在界面生成器中的样子

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

有3点值得一提

第一:

加载笔尖时使用Bundle(for: type(of: self))。这是因为界面生成器在单独的进程中呈现可设计元素,该进程的主包与您的主包不同。

第二:

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

IBInspectablesIBOutlets 结合使用时,您必须记住didSet 函数在awakeFromNib 方法之前调用。因此,outlet 没有初始化,此时您的应用程序可能会崩溃。不幸的是,您不能省略 didSet 函数,因为界面生成器不会呈现您的自定义视图,所以如果这里我们必须保留它。

第三:

    titleLabel.text = title
    iconView.image = image

我们必须以某种方式初始化我们的控件。在调用didSet 函数时我们无法做到这一点,因此我们必须使用存储在IBInspectable 属性中的值并在awakeFromNib 方法结束时对其进行初始化。

这是您如何在 Xib 上实现自定义视图、将其嵌入故事板、在故事板上对其进行配置、对其进行渲染并拥有一个不崩溃的应用程序的方法。这需要破解,但这是可能的。

【讨论】:

    【解决方案2】:

    关于视图重用的一般建议

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

    具体来说,xib 和故事板是通过重用代码中定义的视图来定义视图的好方法。但是它们对于定义您自己希望在 xib 和故事板中重用的视图不是很好。 (可以,但这是一个高级练习。)

    所以,这是一个经验法则。如果您正在定义一个想要从代码中重用的视图,那么您可以随意定义它。但是,如果您正在定义一个您希望能够在情节提要中重复使用的视图,那么请在代码中定义该视图。

    因此,在您的情况下,如果您尝试定义要从情节提要中重复使用的自定义视图,我会在代码中完成。如果您对通过 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 中仍然没有实时预览)。

    【讨论】:

    • 感谢您的建议。我对使用 XIB 文件来布局自定义视图有点固执,因为它过去在 Obj-C 中对我很有效,所以在我认输并纯粹通过代码来做之前,也许你有关于这种行为的一些想法:我能够让我的 XIB 设计的自定义子视图显示在视图控制器中,但是插座是零,所以我不能以编程方式修改它。我注意到子视图存在,似乎插座引用不存在。任何线索为什么会这样?
    猜你喜欢
    • 1970-01-01
    • 2018-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-17
    • 1970-01-01
    相关资源
    最近更新 更多