使用XIB创建Swift中的自定义UIView子类

10

我正在使用 Swift 和 Xcode 6.4。

所以我有一个视图控制器,其中包含一些多个 UILabel 和 UIImageView 的组合。我想将 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) 和从 bundle 加载 nib 之间产生无限循环的问题。奇怪的是,没有这些文章或以前在 stack overflow 上的答案提到这一点!

好的,所以我在 init(coder) 中添加了一个检查来查看子视图是否已经添加。这样就“解决”了这个问题,看起来是这样的。但是,当我尝试给自定义视图的 outlet 赋值时,我的自定义视图 outlet 却变成了 nil。

我使用了 didSet 并添加了断点来查看...他们确实被设置了一次,但是当我尝试修改标签文本颜色之类的东西时,那个标签却变成了 nil。我有点抓狂了。

可重复使用的组件似乎是软件设计 101,但我觉得苹果正在合谋对付我。我应该在这里寻找使用容器 VC 吗?我应该只嵌套视图并在主 VC 中拥有大量 outlet 吗?为什么这么复杂?为什么每个人的示例都对我无效?

期望结果(假设整个内容是 VC,方框是我想要的自定义 uiviews):

enter image description here

感谢阅读。以下是我的自定义 UIView 子类。在我的主 storyboard 中,我有设置了该子类作为其类的 UIView。

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:

enter image description here

结果:

enter image description here

问题:当尝试访问标签或图像输出时,它们是零。在检查所述访问断点时,标签和图像子视图都存在,并且视图层次结构与预期相同。

如果必须这样做,我可以全用代码来实现,但是我不太想用代码编写自动布局,因此如果有避免的方法,我宁愿不使用!

编辑/问题转移:

我找出了如何使输出停止为nil。

enter image description here

从这个SO回答中获得启示:Loaded nib but the view outlet was not set - new to InterfaceBuilder,除了赋值视图输出之外,我还分配了单个组件输出。

现在,这是我只是随便尝试了一下并看看它是否奏效的时候。有谁知道为什么我不得不这样做吗?这是什么黑魔法?


你找到任何解决方案了吗? - H Raval
2个回答

13

关于视图重用的一般建议

你是对的,可重用和可组合的元素是软件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.
  }
}

现在你可以通过代码或者storyboard来实例化它,但是你不能在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
  }
}
现在你可以从storyboards或代码中使用代码定义的视图,它将加载其由nib定义的子视图(并且IB仍然没有实时预览)。

2
谢谢您的建议。我有点固执,想使用XIB文件来布局自定义视图,因为在Obj-C中它曾经对我很有效,所以在我放弃并纯粹通过代码实现之前,也许您有一些关于这种行为的想法:我能够让我的XIB设计的自定义子视图显示在视图控制器中,但是输出口为空,所以我无法通过编程方式修改它。我注意到子视图确实存在,只是似乎没有输出口引用。有什么线索吗? - Fozzle

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

有三个值得提及的点。

第一点:

加载nib时,请使用Bundle(for: type(of: self))。这是因为界面构建器在单独的进程中呈现可设计对象,其主要bundle与您的主要bundle不同。

第二点:

    @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上实现自定义视图,将其嵌入到故事板中,在故事板上进行配置,呈现并拥有非崩溃应用程序的方法。它需要一些技巧,但是可以实现。

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