资料参考
UIView中与AutoLayout相关的几个方法对比: 详细介绍了UIView约束布局相关调用顺序,好好理解有助于子空间布局和约束更新。
UIView在AutoLayout下的布局过程
iOS UIkit 提供简单的基本控件,但是有时候为了需求往往需要将多个控件整合到一个View控件中,此时就是自定义View
两种方式:
1.大神:纯代码(代码创建控件、约束布局)
2.小懒神:xib (控件创建与约束) + 代码(对象化管理)
1.公共初始化方法(保证代码初始化和xib初始化一致,使用者代码和xib都调用来初始),如果是使用xib自定义,代码使用控件时一定要保证控件已经加载。
2.保证UIView的两个初始化方法都可以进行相同的初始化(通过代码创建自定义视图和在IB中穿创建),因为两种方式(如下a.b)调用的初始化方法不一致,你需要在重写两个初始化中保证同样的初始操作(小技巧:创建一个commonInit然后调用)
a.代码:init(frame:CGRect)
b.IB/xib:init(coder aDecoder:NSCoder)
3.布局约束
需要保证获取正确的frame,
对象控件.setNeedsLayout() //需要更新标志
对象控件.layoutIfNeeded() // 更新操作
// 之后就可以获取到布局后的正确frame
// 注意不可以在layoutSubviews中执行 self.这两个方法,否则会循环
注意:此方法仅可以用于基于UIView和其部分子类的自定义。
1.创建一个空的View(两个选一个都可以,如果选择Empty则需要自己拖一个view到画布),命名为MyView,如下图:
2.默认的空View和手机页面大小一致的,为了可以改变视图的大小,在属性检查器中设置size为freeform,将topBar也设置为none,如下图:
3.选择view上方的file Owner,将其Custom class设置为MyView(这里需要先继承UIview,生成一个MyView的类文件),这样才可以保证将自定义控件添加到类中。
注意:设置custom class的时候一定不能选择View,让它默认为UIView
4.在View中添加子控件,并设置约束
为了能够让自定义控件的一些属性像系统控件一样可以在检查其中直接设置(可视化设置)并且同步显示,需要使用@IBDesignable【OC:IB_DESIGNABLE】 和@IBInspectable[OC:IBInspectable](具体用法见代码)
1.生成一个和xib文件名一样的继承UIview的子类(MyView)
2.将xib文件的控件添加链接到类中,包括最底层的View【重点】
3.做可视化属性的设置
4.定义初始化器(注意还需要从xib文件中加载视图放在其中)
Swift示例代码如下:
////可视化控件
import UIKit
import Foundation
@IBDesignable //可视化的关键字
class MyView: UIView {
@IBOutlet var contentView: UIView!//xib中最低层的View
@IBOutlet weak var l2: UILabel!
@IBOutlet weak var l1: UILabel!
@IBInspectable //属性可是化设置的关键字
var l1Text:String = "标签1"{
didSet{//设置属性观察器,保证实时改变
l1.text = l1Text
}
}
@IBInspectable
var l2Text:String = "标签2"{
didSet{
l2.text = l2Text
}
}
@IBInspectable
var l1TextColor:UIColor = .black{
didSet{
l1.textColor = l1TextColor
}
}
//MARK:实现初始化构造器
//使用代码构造此自定义视图时调用
override init(frame: CGRect) { //每一步都必须
super.init(frame: frame) //实现父初始化
contentView = loadViewFromNib() //从xib中加载视图
contentView.frame = bounds //设置约束或者布局
addSubview(contentView) //将其添加到自身中
}
//可视化IB初始化调用
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
contentView = loadViewFromNib()
contentView.frame = bounds
addSubview(contentView)
}
//MARK:自定义方法
func loadViewFromNib() -> UIView {
//重点注意,否则使用的时候不会同步显示在IB中,只会在运行中才显示。
//注意下面的nib加载方式直接影响是否可视化,如果bundle不确切(为nil或者为main)则看不到实时可视化
let nib = UINib(nibName:String(describing: MyView.self), bundle: Bundle(for:MyView.self))//【????】怎么将类名变为字符串:String(describing: MyView.self) Bundle的参数为type(of: self)也可以。
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
}
1.首先你需要在storyboard的ViewController的需要使用的地方拖入一个父视图(UIView),将父视图的class设置为创建的MyView,如图
当然也可能会遇到添加了自定义类后什么都没有,并且还有错误,但是可以运行,错误1如下:
IB Designables: Failed to update auto layout status: The agent raised a "NSInternalInconsistencyException" exception: Could not load NIB in bundle: 'NSBundle (loaded)' with name 'MyView1
和错误2
Main.storyboard: error: IB Designables: Failed to render and update auto layout status for MyTableViewController (yC2-JG-gpT): The agent threw an exception.
那么,恭喜你,懵逼啦,请将自定义类中加载UINib的方式更改啦。
我表示进过这个坑,将bundle设置为nil或者main都不行,人家要具体点,有人说另外一种加载nib的方式也不行,没试过。
原本你的加载nib方式可能为:UINib.init(nibName: String(describing:OrderStateView.self), bundle:nil )
修改为:UINib.init(nibName: String(describing:OrderStateView.self), bundle:Bundle(for: self.classForCoder)) //具体的bundle
另外,你还有可能陷入无限循环的crash中,那么一定是你的某个自定义类设置错啦!
扩展参考:
http://www.cocoachina.com/cms/wap.php?action=article&id=17279
示例:实现如下图所示自定义控件
//MARK: - init
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
//MARK: - private method
///公共初始化(只创建,不管位置,这里不要调用self的布局属性,是不准确的)
fileprivate func commonInit() -> Void {
self.containerView = UIView()
self.containerView.backgroundColor = UIColor.clear
topLine = UIView()
topLine.backgroundColor = lineColor
self.containerView.addSubview(topLine)
buttomLine = UIView()
buttomLine.backgroundColor = lineColor
self.containerView.addSubview(buttomLine)
self.addSubview(containerView)
let iconImages:[String] = ["tool_addImage",
"tool_addAlarm",
"tool_a_n",
"tool_list",
"tool_num",
"tool_indent",
"tool_noIndent"]
buttonArr = [imageBtn,alarmBtn,fontBtn,listBtn,listNumBtn,indentBtn,noIndentBtn]
for (index,imageNamed) in iconImages.enumerated(){
var indexButton = buttonArr[index]
indexButton = UIButton()
indexButton.setImage(UIImage.init(named: iconImages[index]), for: .normal)
if index == 2{ //字体修改
indexButton.setImage(UIImage.init(named: "tool_a_s"), for: .selected)
}
containerView.addSubview(indexButton)
buttonArr[index] = indexButton
}
}
//一定要在这里布局 因为从xib初始化得到的父位置是不精准的
override func layoutSubviews() {
super.layoutSubviews()
if isFirstLayout{ //保证只初始化布局一次
containerView.frame = self.bounds //这里的bounds是准确的
topLine.frame = CGRect.init(x: 0, y: 0, width: self.bounds.width, height: 1)
buttomLine.frame = CGRect.init(x: 0, y: self.bounds.height - 1 , width: self.bounds.width, height: 1)
let itemWidth = (self.bounds.width - 30.0)/CGFloat.init(buttonArr.count)
for (index,button) in buttonArr.enumerated(){
let x = 15.0+(CGFloat.init(index))*itemWidth
let y = (self.bounds.height-itemWidth)/2
button.frame = CGRect.init(x: x, y: y, width: itemWidth, height: itemWidth)
}
isFirstLayout = false
}
}