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,主要是上图下文本的形式。

在这里插入图片描述

完整代码见示例项目的 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类。便于管理和加载。

在这里插入图片描述

我猜你不使用相同的 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.如下图:

在这里插入图片描述

所以有三种关联方式。

  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 设置目标类

在这里插入图片描述

之后创建子控件关联(@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
    	}

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

在这里插入图片描述

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
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的 iOS 自定义 View 实现 K 线图的示例代码: ```swift import UIKit class KLineChartView: UIView { // K线图数据 var data: [KLineChartData] = [] // K线图相关配置 var config: KLineChartConfig = KLineChartConfig() // Y轴最大值 var yAxisMaxValue: CGFloat = 0 // Y轴最小值 var yAxisMinValue: CGFloat = 0 override func draw(_ rect: CGRect) { super.draw(rect) guard let context = UIGraphicsGetCurrentContext() else { return } // 绘制背景色 context.setFillColor(config.backgroundColor.cgColor) context.fill(rect) // 计算 Y 轴最大值和最小值 calculateYAxisValue() // 绘制 Y 轴 drawYAxis(context: context) // 绘制 K 线图 drawKLineChart(context: context) } // 计算 Y 轴最大值和最小值 private func calculateYAxisValue() { var maxValue: CGFloat = 0 var minValue: CGFloat = CGFloat.greatestFiniteMagnitude for item in data { if item.high > maxValue { maxValue = item.high } if item.low < minValue { minValue = item.low } } yAxisMaxValue = maxValue yAxisMinValue = minValue } // 绘制 Y 轴 private func drawYAxis(context: CGContext) { context.setStrokeColor(config.yAxisColor.cgColor) context.setLineWidth(config.yAxisLineWidth) // 计算 Y 轴刻度值 let stepValue = (yAxisMaxValue - yAxisMinValue) / CGFloat(config.yAxisTickCount - 1) for i in 0..<config.yAxisTickCount { // 计算 Y 轴刻度值对应的坐标 let y = bounds.height - CGFloat(i) / CGFloat(config.yAxisTickCount - 1) * bounds.height // 绘制 Y 轴刻度线 context.move(to: CGPoint(x: 0, y: y)) context.addLine(to: CGPoint(x: config.yAxisTickLength, y: y)) context.strokePath() // 绘制 Y 轴刻度值 let textRect = CGRect(x: 0, y: y - config.yAxisTickTextSize / 2, width: config.yAxisTickTextSize, height: config.yAxisTickTextSize) let text = String(format: "%.2f", yAxisMinValue + CGFloat(i) * stepValue) let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: config.yAxisTextColor, .font: UIFont.systemFont(ofSize: config.yAxisTickTextSize)] let attributedText = NSAttributedString(string: text, attributes: attributes) attributedText.draw(in: textRect) } } // 绘制 K 线图 private func drawKLineChart(context: CGContext) { let barWidth = bounds.width / CGFloat(data.count) for (index, item) in data.enumerated() { let x = CGFloat(index) * barWidth + barWidth / 2 let highY = bounds.height - (item.high - yAxisMinValue) / (yAxisMaxValue - yAxisMinValue) * bounds.height let lowY = bounds.height - (item.low - yAxisMinValue) / (yAxisMaxValue - yAxisMinValue) * bounds.height let openY = bounds.height - (item.open - yAxisMinValue) / (yAxisMaxValue - yAxisMinValue) * bounds.height let closeY = bounds.height - (item.close - yAxisMinValue) / (yAxisMaxValue - yAxisMinValue) * bounds.height // 绘制 K 线图 context.setStrokeColor(item.color.cgColor) context.setLineWidth(config.kLineWidth) context.move(to: CGPoint(x: x, y: highY)) context.addLine(to: CGPoint(x: x, y: lowY)) context.strokePath() // 绘制 K 线实体 if item.close > item.open { context.setFillColor(config.riseColor.cgColor) } else { context.setFillColor(config.fallColor.cgColor) } let bodyRect = CGRect(x: x - config.kLineBodyWidth / 2, y: min(openY, closeY), width: config.kLineBodyWidth, height: abs(openY - closeY)) context.fill(bodyRect) } } } // K线图数据结构体 struct KLineChartData { var open: CGFloat = 0 // 开盘价 var close: CGFloat = 0 // 收盘价 var high: CGFloat = 0 // 最高价 var low: CGFloat = 0 // 最低价 var color: UIColor = .black // 颜色 } // K线图配置 struct KLineChartConfig { var backgroundColor: UIColor = .white // 背景色 var yAxisColor: UIColor = .black // Y轴颜色 var yAxisLineWidth: CGFloat = 1 // Y轴线宽 var yAxisTextColor: UIColor = .black // Y轴文本颜色 var yAxisTickTextSize: CGFloat = 10 // Y轴刻度值文本大小 var yAxisTickCount: Int = 5 // Y轴刻度值数量 var yAxisTickLength: CGFloat = 5 // Y轴刻度线长度 var kLineWidth: CGFloat = 1 // K线线宽 var kLineBodyWidth: CGFloat = 6 // K线实体宽度 var riseColor: UIColor = .red // 上涨颜色 var fallColor: UIColor = .green // 下跌颜色 } ``` 使用方法: ```swift let kLineChartView = KLineChartView(frame: CGRect(x: 0, y: 0, width: 300, height: 200)) kLineChartView.data = [ KLineChartData(open: 100, close: 120, high: 125, low: 95, color: .black), KLineChartData(open: 120, close: 110, high: 135, low: 105, color: .black), KLineChartData(open: 110, close: 130, high: 140, low: 100, color: .black), KLineChartData(open: 130, close: 115, high: 145, low: 110, color: .black), KLineChartData(open: 115, close: 125, high: 130, low: 105, color: .black), KLineChartData(open: 125, close: 135, high: 140, low: 120, color: .black), KLineChartData(open: 135, close: 120, high: 145, low: 115, color: .black), ] kLineChartView.config.yAxisTickCount = 5 kLineChartView.config.kLineBodyWidth = 4 kLineChartView.config.riseColor = .red kLineChartView.config.fallColor = .green view.addSubview(kLineChartView) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值