ios 在 Swift 中使用 XIB 自定义 UIView 子类

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/32342145/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 07:28:44  来源:igfitidea点击:

Custom UIView subclass with XIB in Swift

iosswiftuiviewinterface-builderxib

提问by Fozzle

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

所以我有一个视图控制器,它将包含一些多对 UILabels 和 UIImageViews。我想把 UILabel-UIImageView 对放到一个自定义的 UIView 中,这样我就可以简单地在前面提到的视图控制器中重复使用相同的结构。(我知道这可以翻译成 UITableView,但为了〜学习〜请耐心等待)。结果证明这是一个比我想象的更复杂的过程,我无法找出使这一切在 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).

目前,我一直在纠结 UIView 子类和相应的 XIB,覆盖 init(frame:) 和 init(coder),从笔尖加载视图并将其添加为子视图。到目前为止,这是我在互联网上看到/阅读的内容。(大约是这样: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!

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

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.

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

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.

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

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?

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

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

期望的结果(假装整个事情是 VC,这些框是我想要的自定义 uiviews):

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.

以下是我的自定义 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
}
*/

}

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 InterfaceBuilderexcept instead of assigning the view outlet I assigned the individual component outlets.

灵感来自这个 SO 答案:加载笔尖但未设置视图插座 - 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?

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

采纳答案by Kamil Szostakowski

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 UIViewsubclass named P2View

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

This is how it looks like in interface builder

这就是界面构建器中的样子

Xib definition

Xib定义

This is how I embedded this custom view in the example view controller defined on a storyboard. Properties of P2Vieware set in the attributes inspector.

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

Custom view embedded in the view controller

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

There are 3 points worth mentioning

有3点值得一提

First:

第一的:

Use the Bundle(for: type(of: self))when loading the nib. This is because the interface builder renders the designables in the separate process which main bundle is not the same as your main bundle.

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

Second:

第二:

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

When combining IBInspectableswith IBOutletsyou have to remember that the didSetfunctions are called before awakeFromNibmethod. Because of that, the outlets are not initialized and your app will probably crash at this point. Unfortunatelly you cannot omit the didSetfunction because the interface builder won't render your custom view so we have to leave this dirty if here.

IBInspectablesIBOutlets你结合时,你必须记住didSet函数是在awakeFromNib方法之前调用的。因此,出口未初始化,此时您的应用程序可能会崩溃。不幸的是,您不能省略该didSet功能,因为界面构建器不会呈现您的自定义视图,因此如果在这里我们必须保留它。

Third:

第三:

    titleLabel.text = title
    iconView.image = image

We have to somehow initialize our controls. We were not able to do it when didSetfunction was called so we have to use the value stored in the IBInspectableproperties and initialize them at the end of the awakeFromNibmethod.

我们必须以某种方式初始化我们的控件。当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. It requires a hack, but it's possible.

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

回答by algal

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。Interface Builder 不是很擅长。

Specifically, xibs and storyboard are great ways to define views by re-using views that are defined in code. But they are not very good for defining views that you yourself wish to re-use within xibs and storyboards. (It can be done, but it is an advanced exercise.)

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

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 中仍然没有实时预览)。