Swift 开发 iOS App 过程中写自定义控件的归纳总结

在 Swift 开发 iOS App 时,自定义控件的各类写法、适用场景和最佳实践,这是一个非常核心且实用的知识点,能帮助你写出可复用、易维护的 UI 代码。

一、自定义控件的核心分类与写法

根据自定义程度和实现方式,iOS 中自定义控件主要分为以下几类,每类都有其适用场景和标准写法:

1. 组合式自定义控件(最常用)

核心思路 :不从头绘制,而是将系统原生控件(如 UIButton、UILabel、UIImageView)组合起来,封装成一个独立的 UIView 子类,对外暴露统一的接口。适用场景:业务场景化的控件(如商品卡片、导航栏、登录项),90% 的自定义控件需求都可以用这种方式实现。

标准写法示例

swift

复制代码
import UIKit

// 1. 定义自定义控件类,继承UIView
class CustomProductCard: UIView {
    // 2. 封装子控件(私有化,仅内部管理)
    private let iconImageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFit
        iv.clipsToBounds = true
        iv.layer.cornerRadius = 8
        // 禁用自动布局转换,使用纯AutoLayout
        iv.translatesAutoresizingMaskIntoConstraints = false
        return iv
    }()
    
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
        label.textColor = .black
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let priceLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
        label.textColor = .red
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    // 3. 对外暴露配置接口(属性/方法)
    var productIcon: UIImage? {
        didSet {
            iconImageView.image = productIcon
        }
    }
    
    var productTitle: String? {
        didSet {
            titleLabel.text = productTitle
        }
    }
    
    var productPrice: String? {
        didSet {
            priceLabel.text = productPrice
        }
    }
    
    // 4. 重写初始化方法(保证代码复用)
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI() // 统一初始化UI
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUI() // 适配Storyboard/XIB
    }
    
    // 5. 统一布局方法
    private func setupUI() {
        // 设置控件背景
        self.backgroundColor = .white
        
        // 添加子控件
        addSubview(iconImageView)
        addSubview(titleLabel)
        addSubview(priceLabel)
        
        // AutoLayout布局
        NSLayoutConstraint.activate([
            // 图标:左16,上16,宽80,高80
            iconImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16),
            iconImageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 16),
            iconImageView.widthAnchor.constraint(equalToConstant: 80),
            iconImageView.heightAnchor.constraint(equalToConstant: 80),
            
            // 标题:图标右侧16,与图标顶部对齐
            titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16),
            titleLabel.topAnchor.constraint(equalTo: iconImageView.topAnchor),
            // 标题右侧最多距父视图16
            titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -16),
            
            // 价格:标题下方8,与标题左对齐
            priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
            priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
            // 价格底部距父视图至少16
            priceLabel.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -16)
        ])
    }
}

// 6. 使用示例
// 在ViewController中
let card = CustomProductCard(frame: CGRect(x: 0, y: 100, width: UIScreen.main.bounds.width, height: 112))
card.productIcon = UIImage(named: "product_icon")
card.productTitle = "iPhone 15 Pro"
card.productPrice = "¥7999"
view.addSubview(card)
2. 绘制式自定义控件(自定义绘制内容)

核心思路 :重写draw(_:)方法或使用 CAShapeLayer 绘制自定义图形(如折线图、进度条、自定义按钮背景)。适用场景:系统控件无法满足的图形化需求(如环形进度条、自定义形状按钮)。

标准写法示例(环形进度条)

swift

复制代码
import UIKit

class CircleProgressView: UIView {
    // 对外暴露的进度属性(0-1)
    var progress: CGFloat = 0.0 {
        didSet {
            // 进度变化时重绘
            progressLayer.strokeEnd = progress
        }
    }
    
    // 轨道层(灰色背景)
    private let trackLayer = CAShapeLayer()
    // 进度层(彩色进度)
    private let progressLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayers()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayers()
    }
    
    private func setupLayers() {
        // 1. 绘制圆形路径
        let center = CGPoint(x: bounds.width/2, y: bounds.height/2)
        let radius = min(bounds.width, bounds.height)/2 - 5 // 留5px边距
        let path = UIBezierPath(arcCenter: center, 
                                radius: radius, 
                                startAngle: -CGFloat.pi/2, // 从顶部开始
                                endAngle: CGFloat.pi*3/2, 
                                clockwise: true)
        
        // 2. 配置轨道层
        trackLayer.path = path.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineWidth = 8
        layer.addSublayer(trackLayer)
        
        // 3. 配置进度层
        progressLayer.path = path.cgPath
        progressLayer.strokeColor = UIColor.systemBlue.cgColor
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.lineWidth = 8
        progressLayer.strokeEnd = 0 // 初始进度0
        layer.addSublayer(progressLayer)
    }
    
    // 4. 布局变化时更新路径
    override func layoutSubviews() {
        super.layoutSubviews()
        setupLayers() // 重新计算路径
    }
}

// 使用示例
let progressView = CircleProgressView(frame: CGRect(x: 100, y: 200, width: 100, height: 100))
progressView.progress = 0.7 // 设置70%进度
view.addSubview(progressView)
3. 继承式自定义控件(扩展系统控件)

核心思路 :继承系统控件(如 UIButton、UITextField),重写其方法或属性,扩展功能。适用场景:仅需在系统控件基础上做少量修改(如自定义按钮点击效果、文本框输入限制)。

标准写法示例(自定义按钮)

swift

复制代码
import UIKit

class CustomButton: UIButton {
    // 自定义属性
    var cornerRadius: CGFloat = 8 {
        didSet {
            layer.cornerRadius = cornerRadius
        }
    }
    
    var normalColor: UIColor = .systemBlue {
        didSet {
            setBackgroundColor(normalColor, for: .normal)
        }
    }
    
    var highlightColor: UIColor = .systemBlue.withAlphaComponent(0.8) {
        didSet {
            setBackgroundColor(highlightColor, for: .highlighted)
        }
    }
    
    // 重写初始化方法
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupButton()
    }
    
    private func setupButton() {
        // 基础配置
        layer.cornerRadius = cornerRadius
        clipsToBounds = true
        setTitleColor(.white, for: .normal)
        titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
    }
    
    // 自定义方法:设置不同状态的背景色
    private func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
        UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
        UIGraphicsGetCurrentContext()?.setFillColor(color.cgColor)
        UIGraphicsGetCurrentContext()?.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
        let colorImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        setBackgroundImage(colorImage, for: state)
    }
    
    // 重写触摸方法,自定义点击效果
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        // 点击时缩小
        UIView.animate(withDuration: 0.1) {
            self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        // 松开时恢复
        UIView.animate(withDuration: 0.1) {
            self.transform = .identity
        }
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesCancelled(touches, with: event)
        // 取消触摸时恢复
        UIView.animate(withDuration: 0.1) {
            self.transform = .identity
        }
    }
}

// 使用示例
let customBtn = CustomButton(frame: CGRect(x: 50, y: 350, width: 300, height: 50))
customBtn.setTitle("自定义按钮", for: .normal)
customBtn.cornerRadius = 25
customBtn.normalColor = .systemGreen
customBtn.highlightColor = .systemGreen.withAlphaComponent(0.8)
view.addSubview(customBtn)
4. XIB/Storyboard 式自定义控件

核心思路 :用可视化工具(XIB)设计控件布局,再关联到 Swift 类中。适用场景:布局复杂、需要可视化调整的自定义控件。

标准写法步骤

  1. 创建 XIB 文件(如CustomCard.xib),拖入 UI 控件并布局;
  2. 创建 Swift 类(CustomCard.swift),继承 UIView;
  3. 关联 XIB 和 Swift 类,加载 XIB:

swift

复制代码
import UIKit

class CustomCard: UIView {
    // 关联XIB中的控件
    @IBOutlet weak var iconImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    
    // 加载XIB的核心方法
    private func loadXIB() {
        // 获取XIB文件
        let nib = UINib(nibName: "CustomCard", bundle: nil)
        // 加载XIB中的view
        if let contentView = nib.instantiate(withOwner: self).first as? UIView {
            // 设置frame并添加到当前view
            contentView.frame = self.bounds
            contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            self.addSubview(contentView)
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadXIB()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        loadXIB()
    }
    
    // 对外暴露配置方法
    func configure(icon: UIImage?, title: String?) {
        iconImageView.image = icon
        titleLabel.text = title
    }
}

二、自定义控件的通用最佳实践

  1. 封装性 :子控件私有化(private),对外仅暴露必要的配置接口(属性 / 方法);
  2. 初始化统一 :所有初始化路径(init(frame:)/init(coder:))都指向同一个 UI 初始化方法;
  3. 布局适配 :优先使用 AutoLayout,重写layoutSubviews()处理布局变化;
  4. 可复用性:避免硬编码(如颜色、字体、间距),通过属性暴露配置项;
  5. 性能优化
    • 绘制式控件避免在draw(_:)中做复杂计算;
    • 组合式控件减少层级嵌套;
    • 避免频繁刷新 UI(如用didSet触发刷新时加判断);
  6. 适配 Storyboard :实现init(coder:),支持可视化编辑。

总结

  1. 组合式控件是最常用的写法,适合 90% 的业务场景,核心是封装系统控件并暴露统一接口;
  2. 绘制式控件 用于自定义图形,核心是重写draw(_:)或使用 CAShapeLayer,需注意布局变化时更新绘制路径;
  3. 自定义控件的核心原则:高内聚(内部逻辑封装)、低耦合(对外接口清晰)、可复用(配置项可自定义)、易适配(支持代码 / Storyboard 两种创建方式)。

遵循以上分类和最佳实践,你可以写出结构清晰、复用性高的自定义控件,大幅提升 iOS UI 开发的效率和可维护性。

相关推荐
pop_xiaoli2 小时前
effective-Objective-C 第二章阅读笔记
笔记·学习·ios·objective-c·cocoa
未来侦察班11 小时前
一晃13年过去了,苹果的Airdrop依然很坚挺。
macos·ios·苹果vision pro
锐意无限17 小时前
Swift 扩展归纳--- UIView
开发语言·ios·swift
符哥200817 小时前
用Apollo + RxSwift + RxCocoa搭建一套网络请求框架
网络·ios·rxswift
Aftery的博客18 小时前
Xcode运行报错:SDK does not contain ‘libarclite‘ at the path
macos·cocoa·xcode
文件夹__iOS1 天前
AsyncStream 进阶实战:SwiftUI 全局消息流极简实现
ios·swiftui·swift
2501_916008891 天前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
忆江南1 天前
Flutter深度全解析
ios
山水域1 天前
Swift 6 严格并发检查:@Sendable 与 Actor 隔离的深度解析
ios