天天看点

iOS 自定义 View

开发中,为了最小模块化和控件复用(其实更多的是系统控件不满足需求时),我们常常需要自定义 View。这就涉及到需要了解 UIView 的生命周期,布局约束周期才能更好的自定义。

示例工程

基本声命周期

一个控件显示在屏幕需要这么一个过程

初始化 -> 约束和布局 -> 绘制渲染 -> 销毁

对应方法调用(可在示例工程中查看日志输出)

init(coder:)/init(frame:) // 【初始化】可视化加载/代码初始化
updateConstraints()       // 【约束更新】可选, 基于 AutoLayout 布局时调用
layoutSubviews()          // 【子视图布局】调用一次或者多次,基于 AutoLayout 一般会调用两次及以上
draw(_:)                  // 【绘制】
draw(_:in:)
           

初始化

UIView 有两个 init 方法,分别是

init(frme: CGRect)

: 代码初始化时调用。

init(coder: NSCoder)

: xib 或者 stroryboard 加载时调用。

如果你的自定义 View 需要满足代码和可视化初始化,那你应该同时重写这两个初始化方法,并且配置同样的设置,才能保证两种初始化方式一样。通常我都习惯创建 commonInit 来进行统一设置。

布局约束周期

需要理解 layoutSubviews 的调用时机和作用,才可以在自定义中保证视图是按照理想布局和生效的。

理解UIView 的布局与绘制显示相关方法调用时机和以及自动布局的约束过程再结合swift自定义 View 的正确做法可以得出以下结论:

  1. 基于 frame 坐标系布局的,建议在 layoutSubviews 方法中设置子控件 frame, 在

    init(coder:)

    中获取的 frame 是不准确的。
  2. 基于 AutoLayout 布局的,在添加控件之后就应该添加约束,不建议在

    updateConstraints()

    中添加约束,而是进行约束值的修改(同样建议在动作发生时修改约束值,而不是在该方法内),可能会引起循环。
  3. Auto Layout的布局过程是 updateConstraints-> layoutSubViews -> draw 这三步不是单向的,如果layout的过程中改变了constrait, 就会触发update constraints,进行新的一轮迭代。我们在实际代码中,应该避免在此造成死循环
重点:重写 updateConstraints()中一定要在最后调用 super.updateConstraints()

纯代码自定义 View

需求:实现如下图黑色方框内自定义 View,主要是上图下文本的形式。

iOS 自定义 View

完整代码见示例项目的

MyViewA

1.重写初始化方法

同时重写两个初始化方法,保证自定义 View支持代码初始化和 xib/storyboard 初始化

override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
           

2.定义公共初始化方法

公共初始化方法保证同样的配置。

添加 View 和设置控件
fileprivate func commonInit() {
    addSubview(imageView)
    label.textAlignment = .center
    label.font = UIFont.systemFont(ofSize: 14)
    label.textColor = .green
    addSubview(label)
 }

           

3.布局子控件

重写 layouSubviews() 并设置子控件位置大小

override func layoutSubviews() {
    super.layoutSubviews()
    let imageViewW: CGFloat = 60
    let imageViewX = (bounds.width-60)/2
    imageView.frame = CGRect(x: imageViewX, y: 8,width: imageViewW, height: imageViewW)
    let labelY = imageViewW + 8 + 8
    label.frame = CGRect(x: 0, y: labelY, width: bounds.width, height: 17)
}

           
如果使用 AutoLatout 可以在添加子控件之后集中添加约束,如果使用 frame 则建议在 layouSubviews 中设置子控件 frame, 因为这里 获取到的frame 可能不准确

其他示例参考纯代码创建 View

代码 + xib 结合自定义 View

相对纯代码自定义,我更喜欢代码 + xib 的方式,简单直观。

其主要步骤如下

  1. 生成同名 swift 文件和 xib 文件
  2. 将 xib 的 FileOwner 的 class 设置为自定义 view class
  3. 在代码文件内 通过 UINib 加载并实例化 view ,添加到自定义 view上(所以这种自定义 View 相当于有两层 View)
  4. 在 layouSubviews方法内调整 contentView 的 frame,让其与自身 bounds 一致。

完整代码见示例工程的

MyViewB

1.创建相同的名字的 xib和 swift 文件

创建相同文件名的 xib 和 swift类。便于管理和加载。

iOS 自定义 View

我猜你不使用相同的 xib 文件名也可以(貌似 Objective-C就需要使用不同文件名,否则会引起循环引用)

2.重写初始化方法

这里同样需要重写

init(frame:)

init(coder:)

以及建立通用

commonInit()

来统一配置。

override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
           

3.关联两个文件

由于 xib 文件含有 FilesOwner 和 view 两个地方的自定义class.如下图:

iOS 自定义 View

所以有三种关联方式。

  1. 将 FilesOwner的 class 设置为自定义 View的 class(有两层 View,内部初始化后加一层)
  2. 将 View 的 class 设置为自定义 View的 class (一层 View,外部初始化)
  3. 两者都设置 (一层 View,外部初始化)

详细区别参考深入理解自定义 View的 class 和 FilesOwner,理解 xib 的 View 的 class 和 File sOwner 的 class 作用和区别,选择合适的方式来使用。

这里我们选择第一种,这样自定义后的 View 就可以直接在其他的 xib 或者 storyboard 中使用了。

第一步选中 xib 的 FilesOwner 在 CustomeClass 设置目标类

iOS 自定义 View

之后创建子控件关联(@IBOutlet)

@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var label: UILabel!
           

4.加载 xib View

通过UINib 或者 Bundle加载 xib 的 View 实例

fileprivate func loadnib() {
        let nib = UINib(nibName: String(describing: MyViewB.self), bundle: nil)
        guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else {
            return
        }
        // 添加到当前 view
        addSubview(view)
        // 【重要】将背景色置为 clear, 这样不会影响父控件设置背景色。
        view.backgroundColor = .clear
        // 传出方法外,方便布局设置
        contentView = view
    }
           

5.调整布局

这一步是非常重要,否则定义 View控件的内容可能与预设不一致。

override func layoutSubviews() {
        super.layoutSubviews()
        // [重点] 这里需要设置 contenView 的 frame,否则 contenView 就和 xib 大小一致,在实际使用中大小与预期不一致
        contentView.frame = bounds
    	}
           

最后是纯代码与混合自定的样式如下图:

iOS 自定义 View

NibLoadable 快速实例化

如果设置 xib的 view 的 class,则需要在外部初始化,可以遵守以下协议避免写重复代码,快速实例化。

#if canImport(UIKit)
import UIKit
/// 适用于自定义 View (xib+代码) 获取 nib相关信息和创建实例化
public protocol NibLoadable: UIView {
    static var nibName: String { get }
    static var nib: UINib { get }
}
extension NibLoadable  {
   public static var nibName: String {
        return String(describing: self)
    }
   public static var nib: UINib {
        return nib()
    }
    
    /// 实例化对象
    /// - Parameter frame: frame位置大小,默认`zero`
    /// - Returns: 返回实例 xib View
    public static func instance(frame: CGRect = .zero) -> Self {
        guard let nibView = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
            fatalError("\(nibName).xib第一个 View 不是 \(nibName)View,请检查")
        }
        return nibView
    }
   public static func nib(bundle: Bundle? = Bundle.main) -> UINib {
        return UINib(nibName: nibName, bundle: bundle)
    }
}
#endif