本文將分兩個(gè)部分內(nèi)容:基于代碼的自定義控件和基于XIB的自定義控件。
基于代碼的自定義控件
實(shí)現(xiàn)的基本步驟(純文字版, 閱讀障礙者請(qǐng)?zhí)^~)
- 創(chuàng)建一個(gè)新類,基類選擇UIView
- 實(shí)現(xiàn)init(frame:)方法
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
func setupSubviews() {
// ..
}
- 實(shí)現(xiàn)init()方法
convenience init() {
self.init(frame: CGRect.zero)
}
- 實(shí)現(xiàn)init(coder:)方法
基本邏輯跟init(frame:)是類似的
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupSubviews()
}
init(coder:)和init(frame:)是在兩個(gè)不同的情境下被調(diào)用的。前者是當(dāng)自定義控件在XIB中使用的時(shí)候被調(diào)用,后者是使用代碼直接創(chuàng)建自定義控件時(shí)被調(diào)用的。
當(dāng)自定義控件在XIB中使用時(shí)候還有一個(gè)方法會(huì)被觸發(fā):awakeFromeNib,此方法是當(dāng)控件從NIB文件加載完畢之后觸發(fā)的, 它和init(coder:)的觸發(fā)機(jī)制是不同的, init(coder:)要做真正的反序列化的工作, awakeFromNib是反序列化完畢并且設(shè)置好相關(guān)的Outlet變量之后被調(diào)用的。
- 重寫layoutSubviews方法,調(diào)整子View的大小。
override func layoutSubviews() {
asubview.bounds = self.bounds
// ...
}
有時(shí)候也可以在上述創(chuàng)建子View的時(shí)候設(shè)置相關(guān)的大小和約束,但是如果子View的大小或約束需要依賴self的大小的話,就只有在這個(gè)方法里進(jìn)行, 因?yàn)橹筮@個(gè)方法被調(diào)用的時(shí)候self的frame才是正確的, 同時(shí), 在self的frame發(fā)生變化的時(shí)候(包括發(fā)生了旋轉(zhuǎn)), 這個(gè)方法也會(huì)再次被調(diào)用。
基于XIB創(chuàng)建自定義控件
我們假設(shè)想寫一個(gè)類似百度個(gè)稅計(jì)算機(jī)的App, 想讓其中的稅率條目做成自定義控件
自定義控件界面
-
創(chuàng)建一個(gè)新類MyCustomView,派生類選擇UIView
創(chuàng)建新類 -
創(chuàng)建一個(gè)和類同名的XIB文件
創(chuàng)建XIB文件 -
修改XIB文件中View的Size 為Freedom, Status bar為None
Metrices屬性 -
設(shè)計(jì)自定義控件的視圖布局和約束
設(shè)計(jì)布局和約束 - 在XIB的File's Owner界面的Custom Class中選擇自定義類的名字
File's Owner
注意:不是在View自身的Custom Class中填寫,如果你調(diào)試的時(shí)候出現(xiàn)init(coder:)遞歸調(diào)用了的話,那就是這個(gè)地方弄錯(cuò)了
錯(cuò)誤的示范
File's Owner設(shè)置的CustomView是設(shè)定子View關(guān)聯(lián)Outlet變量到哪個(gè)類的對(duì)象, 這個(gè)對(duì)象跟后面通過UINib加載Xib文件傳遞的Owner對(duì)象的關(guān)系就是"類"與"對(duì)象"的關(guān)系.
而View的CustomView是指當(dāng)Xib文件被加載進(jìn)來的時(shí)候其跟View對(duì)象的類型. 默認(rèn)是UIView, 后面我們會(huì)展示在Xib中使用我們的自定義控件MyCustomView的時(shí)候, 我們就會(huì)用到這個(gè)特性.
- 給子View拉線關(guān)聯(lián)必要的Outlet變量。
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var selfTaxRateInput: UITextField!
@IBOutlet weak var selfTaxResultLabel: UILabel!
@IBOutlet weak var companyTaxRateInput: UITextField!
@IBOutlet weak var companyTaxResultLabel: UILabel!
- 定義一個(gè)加載XIB文件的方法和一個(gè)完整的初始化子類的方法
// load view from xib
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: String(self.dynamicType), bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
// setup view's auto resize to fill parent, and add a % view to input view
func setupSubviews() {
view = loadViewFromNib()
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
addSubview(view)
selfTaxRateInput.rightViewMode = .Always
let labelMaker:()->UIView = {
let precentLabel = UILabel();
precentLabel.text = "%";
return precentLabel
}
selfTaxRateInput.rightView = labelMaker()
companyTaxRateInput.rightViewMode = .Always
companyTaxRateInput.rightView = labelMaker()
}
// reset subview's frame
override func layoutSubviews() {
view.frame = bounds
let precentRect = CGRect(x: 0, y: 0, width: 20, height: bounds.size.height)
selfTaxRateInput.rightView?.bounds = precentRect
companyTaxRateInput.rightView?.bounds = precentRect
}
這里需要一個(gè)添加一個(gè)view的屬性, 用來保存從Xib中加載得到的View, 然后作為子View添加給self. 這里可能有點(diǎn)迷惑, 我們不就是一個(gè)view嗎? 其實(shí), 在現(xiàn)在這個(gè)用法里面, self只是一個(gè)容器, 它的作為就是作為Xib中View的容器
- 編寫我們的各個(gè)初始化函數(shù)
weak var view: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
convenience init() {
self.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupSubviews()
}
上面的步驟已經(jīng)完成了自定義控件的編寫了, 下面接著介紹如何在Xib中使用它?
-
打開我們主界面的Xib文件, 拖入一個(gè)UIView 設(shè)置好大小位置和約束
使用自定義控件 -
把這個(gè)UIView的CustomView設(shè)置為我們上面開發(fā)的"MyCustomView"
設(shè)置CustomView
這里設(shè)置了子View的CustomView為MyCustomView, 那么當(dāng)主Xib被加載的時(shí)候, 解析到這個(gè)子View的時(shí)候, 就會(huì)用這個(gè)CustomView的類型來創(chuàng)建對(duì)象. 偽代碼類似這樣:
let theClass: AnyClass! = NSClassFromString(CustomClassName)
let classObject: NSObject.Type! = theClass as! NSObject.Type!
let obj = classObject.init()
rootView.addSubview(obj)
-
好, 運(yùn)行一下看看效果.
效果
上面的使用就介紹的差不多了, 但是有個(gè)美中不足的地方, 那就是不能在設(shè)計(jì)階段看到大致的效果. 也不能在設(shè)計(jì)階段修改一些屬性 -- 比方說設(shè)置title.
下面我們來修改一下MyCustomView, 讓它可以支持一些設(shè)計(jì)時(shí)實(shí)時(shí)展示的特性.
- 首先, 我們?cè)贛yCustomView的定義前面加上 @IBDesignable
@IBDesignable
class MyCustomView: UIView {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var selfTaxRateInput: UITextField!
- 定義一個(gè)title的屬性, 并使用@IBInspectable修飾
@IBInspectable var title: String = "" {
didSet {
self.titleLabel.text = title
}
}
完成之后, 我們?cè)俅未蜷_主Xib的Interface Builder界面, 這時(shí)候, 我們就已經(jīng)可以實(shí)時(shí)看到控件的界面了, 并且在屬性面板里頭, 我們也多了一個(gè)叫"Title"的屬性可以設(shè)置, 我們?cè)囍O(shè)置成"公積金", 設(shè)計(jì)界面就自動(dòng)更新了. 是不是很有點(diǎn)標(biāo)準(zhǔn)控件的感覺呢?
注意, 你必須給變量定義一個(gè)明確的類型, 否則在IB上是看不到的
錯(cuò)誤的示范:
@IBInspectable var title = "" {
didSet {
self.titleLabel.text = title
}
}
如果你對(duì)實(shí)時(shí)設(shè)計(jì)感興趣的話, 還可以閱讀文章《如何創(chuàng)建iOS可視化控件》