Swift子类化UIView

98
我想创建一个继承自UIView的类,并展示一个登录界面。我已经使用Objective-C创建了它,但现在我想将其移植到Swift上。我不使用storyboards, 因此我所有的UI都是在代码中创建的。
但第一个问题是我必须实现initWithCoder方法。我给它一个默认实现,因为它不会被调用。现在当我运行程序时,它会崩溃,因为我还必须实现initWithFrame方法。现在我的代码如下:
override init() {
    super.init()
    println("Default init")
}

override init(frame: CGRect) {
    super.init(frame: frame)
    println("Frame init")
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    println("Coder init")
}

我的问题是在哪里创建我的文本框等控件...如果我从未实现frame和coder,如何“隐藏”它们?

6个回答

178

我通常会做类似这样的事情,这有点啰嗦。

class MyView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        addBehavior()
    }

    convenience init() {
        self.init(frame: CGRect.zero)
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("This class does not support NSCoding")
    }

    func addBehavior() {
        print("Add all the behavior here")
    }
}



let u = MyView(frame: CGRect.zero)
let v = MyView()

(编辑:我已经编辑了我的答案,以便初始化之间的关系更加清晰)


4
因为 initFrame 和 init 都调用了 addBehavior 方法,所以 addBehavior 方法会被调用两次。如果你运行我的代码,会先打印出 initFrame,然后才是默认的 init。 - Haagenti
6
好的,谢谢。建议使用CGRect.zeroRect而不是CGRectZero - Mr Rogers
64
这个初始化器的东西非常复杂。 - Ian Warburton
3
有没有一种方法可以在自动布局中实现这个?框架已经过时了。 - devios1
9
这不是完整的答案。UIView确实支持initWithCoding方法。从nib或storyboard加载的任何视图都将调用initWithCoding方法,并且会崩溃。 - Iain Delaney
显示剩余7条评论

19

这更简单了。

override init (frame : CGRect) {
    super.init(frame : frame)
    // Do what you want.
}

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

如果您的视图使用xibs或故事板,init(frame:)方法将永远不会被调用。 - Jason Moore

12

自定义UIView子类示例

我通常在创建iOS应用程序时不使用storyboards或nibs。我将分享一些我学到的技巧来回答您的问题。

 

隐藏不必要的init方法

我的建议是声明一个基础UIView来隐藏不必要的初始化方法。我在"如何在UI子类中隐藏Storyboard和Nib特定的初始化器":my answer to "How to Hide Storyboard and Nib Specific Initializers in UI Subclasses"中详细讨论了这种方法。注意:此方法假定您不会在storyboards或nibs中使用BaseView或其子类,因为它会故意导致应用程序崩溃。

class BaseView: UIView {

    // This initializer hides init(frame:) from subclasses
    init() {
        super.init(frame: CGRect.zero)
    }

    // This attribute hides `init(coder:)` from subclasses
    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

您的自定义UIView子类应继承自BaseView。在其初始化程序中,必须调用super.init()。它不需要实现init(coder:)。下面的示例演示了这一点。

 

添加UITextField

我创建存储属性来引用在init方法之外的子视图。通常我会为UITextField这样做。我喜欢在声明子视图属性时实例化子视图,像这样:let textField = UITextField()

除非通过调用addSubview(_:)将其添加到自定义视图的子视图列表中,否则UITextField将不可见。下面的示例演示了如何做到这一点。

 

无需自动布局的编程布局

如果不设置UITextField的大小和位置,它将不可见。我经常在layoutSubviews方法中使用代码进行布局(不使用自动布局)。layoutSubviews()最初会被调用,每当发生调整大小事件时也会被调用。这允许根据CustomView的大小调整布局。例如,如果CustomView在各种iPhone和iPad上以全宽度显示并适应旋转,则需要适应许多初始大小并进行动态调整。

您可以在layoutSubviews()内引用frame.heightframe.width来获取CustomView的尺寸以供参考。下面的示例演示了这一点。

 

UIView子类示例

一个自定义的UIView子类,包含一个UITextField,不需要实现init?(coder:)方法。

class CustomView: BaseView {

    let textField = UITextField()

    override init() {
        super.init()

        // configure and add textField as subview
        textField.placeholder = "placeholder text"
        textField.font = UIFont.systemFont(ofSize: 12)
        addSubview(textField)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // Set textField size and position
        textField.frame.size = CGSize(width: frame.width - 20, height: 30)
        textField.frame.origin = CGPoint(x: 10, y: 10)
    }
}

 

使用自动布局实现编程布局

您也可以在代码中使用自动布局来实现布局。由于我不经常这样做,因此我不会展示一个例子。您可以在Stack Overflow和互联网上其他地方找到代码实现自动布局的示例。

 

编程布局框架

有一些开源框架可以通过代码实现布局。我感兴趣但尚未尝试的一个是LayoutKit。它是由LinkedIn的开发团队编写的。从Github存储库中可以看到:“LinkedIn创建了LayoutKit,因为我们发现Auto Layout在可滚动视图中的复杂视图层次结构中性能不够好。”

 

为什么在init(coder:)中使用fatalError

当创建的UIView子类不会在storyboard或nib中使用时,您可能会引入具有不同参数和初始化要求的初始化程序,这些初始化程序无法通过init(coder:)方法调用。如果没有使用fatalError失败init(coder:),如果意外在storyboard/nib中使用它,可能会导致非常混乱的问题。fatalError断言了这些意图。

required init?(coder aDecoder: NSCoder) {
    fatalError("NSCoding not supported")
}

如果您希望在子类创建时无论是在代码中还是在storyboard/nib中创建,都能运行一些代码,则可以按照以下方式进行操作(基于Jeff Gu Kang的回答

class CustomView: UIView {
    override init (frame: CGRect) {
        super.init(frame: frame)
        initCommon()
    }

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

    func initCommon() {
        // Your custom initialization code
    }
}

通过添加fatalError,您禁止使用xib文件初始化此视图。 - Vyachaslav Gerchicov
@VyachaslavGerchicov,我的回答假设您没有使用xibs或storyboards,就像被接受的答案和问题一样。问题指出:“我不使用storyboards,所以我在代码中创建所有的UI”。 - Mobile Dan
下次在dealloc方法中写fatalError并告诉我们它不起作用,因为该类应该是单例。如果您喜欢在代码中创建UI元素,则不应该手动禁止所有其他方式。最后的问题是如何“以编程方式而非故事板”创建,但未提及xibs / nibs。在我的情况下,我需要使用程序+xib创建单元格数组,并将它们传递到DropDownMenuKit中,但这种方式不起作用,因为该库的作者也禁止了xibs。 - Vyachaslav Gerchicov
@VyachaslavGerchicov 像是 Jeff Gu Kang 的回答 是你要找的,因为它可以容纳 Storyboards/Xibs。 - Mobile Dan
1
@VyachaslavGerchicov 问题也提到了,“如果我从未实现frame和coder如何“隐藏”它?”当创建 UIView 的子类,这些子类永远不会在 Xib/Storyboard 中使用时,您可能会引入具有不同参数的初始化程序,这些初始化程序不能通过 init(coder:) 方法调用。如果您没有通过 fatalError 失败 init(coder:),如果意外在 Xib/Storyboard 中使用,则可能导致非常令人困惑的问题。fatalError 表示了这些意图。根据被接受的答案所示,这是一种故意而普遍的做法。 - Mobile Dan
感谢您的反馈@VyachaslavGerchicov。我已经更新了我的答案,进一步强调它假设不使用storyboards/nibs。我添加了一个部分来解释fatalError的使用,并提供适用于storyboard/nib的代码。如果还有其他需要改进的地方,请告诉我。 - Mobile Dan

6

您的UIView能够通过界面生成器/故事板或代码创建非常重要。我发现拥有一个setup方法可以减少任何设置代码的重复。例如:

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

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

    func setup () {
        backgroundColor = .red
    }
}

4

Swift 4.0,如果您想使用 xib 文件中的视图,则可以使用此方法。我创建了 CustomCalloutView 类,它是 UIView 的子类。我已经创建了一个 xib 文件,并且只需在 IB 中选择文件所有者,然后选择属性检查器,将类名设置为 CustomCalloutView,然后在您的类中创建 outlet。

    import UIKit
    class CustomCalloutView: UIView {

        @IBOutlet var viewCallout: UIView! // This is main view

        @IBOutlet weak var btnCall: UIButton! // subview of viewCallout
        @IBOutlet weak var btnDirection: UIButton! // subview of viewCallout
        @IBOutlet weak var btnFavourite: UIButton! // subview of viewCallout 

       // let nibName = "CustomCalloutView" this is name of xib file

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

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

        func nibSetup() {
            Bundle.main.loadNibNamed(String(describing: CustomCalloutView.self), owner: self, options: nil)
            guard let contentView = viewCallout else { return } // adding main view 
            contentView.frame = self.bounds //Comment this line it take default frame of nib view
           // custom your view properties here
            self.addSubview(contentView)
        }
    }

// 现在添加它

    let viewCustom = CustomCalloutView.init(frame: CGRect.init(x: 120, y: 120, 50, height: 50))
    self.view.addSubview(viewCustom)

-1

这是我通常构建子类(UIView)的示例。我将内容作为变量存储,以便稍后可以在其他类中访问和调整。我还展示了如何使用自动布局和添加内容。

例如,在ViewController中,我初始化了这个视图In ViewDidLoad(),因为只有当视图可见时才会调用它。然后我使用这些函数addContentToView()activateConstraints()来构建内容并设置约束。如果我稍后在ViewController中想要让某个按钮的颜色变成红色,我只需要在那个特定的函数中进行设置。

像这样:func tweaksome(){ self.customView.someButton.color = UIColor.red}

class SomeView: UIView {


var leading: NSLayoutConstraint!
var trailing: NSLayoutConstraint!
var bottom: NSLayoutConstraint!
var height: NSLayoutConstraint!


var someButton: UIButton = {
    var btn: UIButton = UIButton(type: UIButtonType.system)
    btn.setImage(UIImage(named: "someImage"), for: .normal)
    btn.translatesAutoresizingMaskIntoConstraints = false
    return btn
}()

var btnLeading: NSLayoutConstraint!
var btnBottom: NSLayoutConstraint!
var btnTop: NSLayoutConstraint!
var btnWidth: NSLayoutConstraint!

var textfield: UITextField = {
    var tf: UITextField = UITextField()
    tf.adjustsFontSizeToFitWidth = true
    tf.placeholder = "Cool placeholder"
    tf.translatesAutoresizingMaskIntoConstraints = false
    tf.backgroundColor = UIColor.white
    tf.textColor = UIColor.black
    return tf
}()
var txtfieldLeading: NSLayoutConstraint!
var txtfieldTrailing: NSLayoutConstraint!
var txtfieldCenterY: NSLayoutConstraint!

override init(frame: CGRect){
    super.init(frame: frame)
    self.translatesAutoresizingMaskIntoConstraints = false
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    //fatalError("init(coder:) has not been implemented")
}



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

}
*/
func activateConstraints(){
    NSLayoutConstraint.activate([self.btnLeading, self.btnBottom, self.btnTop, self.btnWidth])
    NSLayoutConstraint.activate([self.txtfieldCenterY, self.txtfieldLeading, self.txtfieldTrailing])
}

func addContentToView(){
    //setting the sizes
    self.addSubview(self.userLocationBtn)

    self.btnLeading = NSLayoutConstraint(
        item: someButton,
        attribute: .leading,
        relatedBy: .equal,
        toItem: self,
        attribute: .leading,
        multiplier: 1.0,
        constant: 5.0)
    self.btnBottom = NSLayoutConstraint(
        item: someButton,
        attribute: .bottom,
        relatedBy: .equal,
        toItem: self,
        attribute: .bottom,
        multiplier: 1.0,
        constant: 0.0)
    self.btnTop = NSLayoutConstraint(
        item: someButton,
        attribute: .top,
        relatedBy: .equal,
        toItem: self,
        attribute: .top,
        multiplier: 1.0,
        constant: 0.0)
    self.btnWidth = NSLayoutConstraint(
        item: someButton,
        attribute: .width,
        relatedBy: .equal,
        toItem: self,
        attribute: .height,
        multiplier: 1.0,
        constant: 0.0)        


    self.addSubview(self.textfield)
    self.txtfieldLeading = NSLayoutConstraint(
        item: self.textfield,
        attribute: .leading,
        relatedBy: .equal,
        toItem: someButton,
        attribute: .trailing,
        multiplier: 1.0,
        constant: 5)
    self.txtfieldTrailing = NSLayoutConstraint(
        item: self.textfield,
        attribute: .trailing,
        relatedBy: .equal,
        toItem: self.doneButton,
        attribute: .leading,
        multiplier: 1.0,
        constant: -5)
    self.txtfieldCenterY = NSLayoutConstraint(
        item: self.textfield,
        attribute: .centerY,
        relatedBy: .equal,
        toItem: self,
        attribute: .centerY,
        multiplier: 1.0,
        constant: 0.0)
}
}

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