# CustomView **Repository Path**: Sven001/CustomView ## Basic Information - **Project Name**: CustomView - **Description**: iOS 自定义 View 示例, 包括纯代码和 xib+代码混合自定义 - **Primary Language**: Swift - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2020-08-11 - **Last Updated**: 2022-01-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

iOS 自定义 View

开发中,为了最小模块化和控件复用(其实更多的是系统控件不满足需求时),我们常常需要自定义 View。这就涉及到需要了解 UIView 的生命周期,布局约束周期才能更好的自定义。 [示例工程](https://gitee.com/Sven001/CustomView.git) ## 基本声命周期 一个控件显示在屏幕需要这么一个过程 **初始化 -> 约束和布局 -> 绘制渲染 -> 销毁** 对应方法调用(可在示例工程中查看日志输出) ```swift 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 的布局与绘制显示相关方法调用时机](https://blog.csdn.net/qq_14920635/article/details/65654678)和[以及自动布局的约束过程](https://blog.csdn.net/haungcancan/article/details/52996789)再结合[swift自定义 View 的正确做法](https://blog.usejournal.com/custom-uiview-in-swift-done-right-ddfe2c3080a)可以得出以下结论: 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 ![Alt](./Resources/demo.jpeg) 完整代码见示例项目的 `MyViewA`类 ### 1.重写初始化方法 同时重写两个初始化方法,保证自定义 View支持代码初始化和 xib/storyboard 初始化 ```swift override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } ``` ### 2.定义公共初始化方法 公共初始化方法保证同样的配置。 ```swift 添加 View 和设置控件 fileprivate func commonInit() { addSubview(imageView) label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 14) label.textColor = .green addSubview(label) } ``` ### 3.布局子控件 重写 layouSubviews() 并设置子控件位置大小 ```swift 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](https://juejin.im/post/6844903565069123592) ## 代码 + 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类。便于管理和加载。 ![Alt](./Resources/same_file_name.png) 我猜你不使用相同的 xib 文件名也可以(貌似 Objective-C就需要使用不同文件名,否则会引起循环引用) ### 2.重写初始化方法 这里同样需要重写 `init(frame:)`和`init(coder:)`以及建立通用`commonInit()`来统一配置。 ```swift override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } ``` ### 3.关联两个文件 由于 xib 文件含有 FilesOwner 和 view 两个地方的自定义class.如下图: ![Alt](./Resources/two_class.png) 所以有三种关联方式。 1. 将 FilesOwner的 class 设置为自定义 View的 class(有两层 View,内部初始化后加一层) 2. 将 View 的 class 设置为自定义 View的 class (一层 View,外部初始化) 3. 两者都设置 (一层 View,外部初始化) 详细区别参考[深入理解自定义 View的 class 和 FilesOwner](https://medium.com/@bhupendra.trivedi14/understanding-custom-uiview-in-depth-setting-file-owner-vs-custom-class-e2cab4bb9df8)及其[示例工程](https://github.com/bhupendratrivedi/SampleCustomView.git),理解 xib 的 View 的 class 和 File sOwner 的 class 作用和区别,选择合适的方式来使用。 这里我们选择第一种,这样自定义后的 View 就可以直接在其他的 xib 或者 storyboard 中使用了。 ![Alt](./Resources/files_owner_class.png) 之后创建子控件关联(@IBOutlet) ```swift @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var label: UILabel! ``` ### 4.加载 xib View 通过UINib 或者 Bundle加载 xib 的 View 实例 ```swift 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控件的内容可能与预设不一致。 ```swift override func layoutSubviews() { super.layoutSubviews() // [重点] 这里需要设置 contenView 的 frame,否则 contenView 就和 xib 大小一致,在实际使用中大小与预期不一致 contentView.frame = bounds } ``` 最后是纯代码与混合自定的样式如下图: ![Alt](./Resources/result.png)