Swift 子类 UIView

我想子类 UIView并显示一个类似登录的视图。我已经在 Objective-C 中创建了它,但是现在我想将它移植到 Swift。我不使用故事板,所以我用代码创建我所有的 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")
}

My question is where should I create my textfield etc... and if I never implement frame and coder how can I "hide" this?

78975 次浏览

I usually do something like this, its a bit verbose.

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()

(Edit: I've edited my answer so that the relation between the initializers is more clear)

This is more simple.

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


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

It's important that your UIView can be created by interface builder/storyboards or from code. I find it's useful to have a setup method to reduce duplicating any setup code. e.g.

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

Here's an example of how I usually build my subclasses(UIView). I have the content as variables so they can be accessed and tweaked maybe later in some other class. I've also shown how I use auto layout and adding content.

For example in a ViewController I have this view initialized In ViewDidLoad() since that is only called once when the view is visible. Then I use these functions I make here addContentToView() and then activateConstraints() to build the content and set constraints. If I later in a ViewController want the color of let's say a button to be red, I just do that in that specific function in that ViewController. Something like: 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)
}
}

Custom UIView Subclass Example

I usually create iOS apps without using storyboards or nibs. I'll share some techniques I've learned to answer your questions.

 

Hiding Unwanted init Methods

My first suggestion is to declare a base UIView to hide unwanted initializers. I've discussed this approach in detail in my answer to "How to Hide Storyboard and Nib Specific Initializers in UI Subclasses". Note: This approach assumes you will not use BaseView or its descendants in storyboards or nibs since it will intentionally cause the app to crash.

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")
}
}

Your custom UIView subclass should inherit from BaseView. It must call super.init() in its initializer. It does not need to implement init(coder:). This is demonstrated in the example below.

 

Adding a UITextField

I create stored properties for subviews referenced outside of the init method. I would typically do so for a UITextField. I prefer to instantiate subviews within the declaration of the subview property like this: let textField = UITextField().

The UITextField will not be visible unless you add it to the custom view's subview list by calling addSubview(_:). This is demonstrated in the example below.

 

Programmatic Layout Without Auto Layout

The UITextField will not be visible unless you set its size and position. I often do layout in code (not using Auto Layout) within the layoutSubviews method. layoutSubviews() is called initially and whenever a resize event happens. This allows adjusting layout depending on the size of CustomView. For example, if CustomView appears the full width on various sizes of iPhones and iPads and adjusts for rotation, it needs to accommodate many initial sizes and resize dynamically.

You can refer to frame.height and frame.width within layoutSubviews() to get the CustomView's dimensions for reference. This is demonstrated in the example below.

 

Example UIView Subclass

A custom UIView subclass containing a UITextField which does not need to implement 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)
}
}

 

Programmatic Layout with Auto Layout

You can also implement layout using Auto Layout in code. Since I don't often do this, I will not show an example. You can find examples of implementing Auto Layout in code on Stack Overflow and elsewhere on the Internet.

 

Programmatic Layout Frameworks

There are open source frameworks that implement layout in code. One I am interested in but have not tried is LayoutKit. It was written by the development team an LinkedIn. From the Github repository: "LinkedIn created LayoutKit because we have found that Auto Layout is not performant enough for complicated view hierarchies in scrollable views."

 

Why put fatalError in init(coder:)

When creating UIView subclasses that will never be used in a storyboard or nib, you might introduce initializers with different parameters and initialization requirements that could not be called by the init(coder:) method. If you did not fail init(coder:) with a fatalError, it could lead to very confusing problems down the line if accidentally used in a storyboard/nib. The fatalError asserts these intentions.

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

If you want to run some code when the subclass is created regardless of whether it is created in code or a storyboard/nib then you could do something like the following (based on Jeff Gu Kang’s answer)

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

Swift 4.0,If you want to use view from xib file, then this is for you. I created CustomCalloutView class Sub class of UIView. I have created a xib file and in IB just select file owner then select Attribute inspector set class name to CustomCalloutView, then create outlet in your class.

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

// Now adding it

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