swift-ios-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Swift iOS UI Skill

Swift iOS UI 开发技能

Generate production-quality iOS UIKit code from UI designs, screenshots, or descriptions.
根据UI设计图、截图或描述生成可用于生产环境的iOS UIKit代码。

Tech Stack (Always Use These)

技术栈(必须使用以下库)

PurposeLibrary
LayoutSnapKit — Auto Layout DSL
Popups / ToastsSwiftEntryKit
Image LoadingKingfisher or SDWebImage
JSON ParsingSwiftyJSON

用途
布局SnapKit — 自动布局DSL
弹窗/提示框SwiftEntryKit
图片加载Kingfisher 或 SDWebImage
JSON解析SwiftyJSON

Step-by-Step Workflow

分步工作流程

0. Understand the Input

0. 理解输入内容

The user may provide one of the following — adapt accordingly:
Input typeHow to handle
Screenshot / imageCarefully read all visible text, colors, layout, spacing, component types
Figma / Sketch descriptionExtract component hierarchy, spacing tokens, color styles
Text descriptionAsk clarifying questions if key details (colors, layout direction, data shape) are missing
No input yetAsk the user: "Please share a UI screenshot, design spec, or describe the screen you want to build."
⚠️ Do NOT start generating code until you have enough UI information. If the user provides a screenshot, analyze it fully before writing a single line.
用户可能提供以下任意一种输入,请按需处理:
输入类型处理方式
截图/图片仔细识别所有可见文本、颜色、布局、间距、组件类型
Figma/Sketch设计描述提取组件层级、间距参数、颜色样式
文字描述如果关键细节(颜色、布局方向、数据结构)缺失,询问用户补充
尚未提供输入询问用户:"请提供UI截图、设计规范或描述您想要构建的界面。"
⚠️ 在获取足够的UI信息前,请勿开始生成代码。如果用户提供了截图,请先完整分析后再编写任何代码。

1. Analyze the UI

1. 分析UI

Before writing any code, examine the design and identify:
  • Screen type: full screen / modal / bottom sheet / popup
  • Layout structure: navigation bar, scroll view, table/collection view, static views
  • Components: buttons, labels, images, text fields, cards, cells
  • Colors: extract hex values from the design (or best-match hex if approximate)
  • Spacing: margins, padding, gaps between elements
  • Interactive states: normal / highlighted / disabled / loading / empty / error
编写代码前,请先分析设计并确定:
  • 界面类型:完整界面/模态框/底部弹窗/弹出层
  • 布局结构:导航栏、滚动视图、表格/集合视图、静态视图
  • 组件:按钮、标签、图片、文本框、卡片、单元格
  • 颜色:从设计中提取十六进制值(如果是近似值则匹配最接近的十六进制)
  • 间距:外边距、内边距、元素间的空隙
  • 交互状态:正常/高亮/禁用/加载/空数据/错误

2. Plan the Architecture

2. 规划架构

Choose the right UIKit pattern:
  • UIViewController
    +
    UIScrollView
    → scrollable content screens
  • UIViewController
    +
    UITableView
    → list screens
  • UIViewController
    +
    UICollectionView
    → grid / complex layouts
  • UIView
    subclass → reusable components / cells
  • SwiftEntryKit
    → popups, toasts, bottom sheets, alerts
选择合适的UIKit模式:
  • UIViewController
    +
    UIScrollView
    → 可滚动内容界面
  • UIViewController
    +
    UITableView
    → 列表界面
  • UIViewController
    +
    UICollectionView
    → 网格/复杂布局
  • UIView
    子类 → 可复用组件/单元格
  • SwiftEntryKit
    → 弹窗、提示框、底部弹窗、警告框

3. Generate the Code

3. 生成代码

Follow all conventions in the Code Conventions section below.
遵循下方代码规范中的所有约定。

4. Output Structure

4. 输出结构

For each screen, produce:
  1. Main
    ViewController
    or
    View
    file
  2. Any custom
    UITableViewCell
    /
    UICollectionViewCell
    subclasses
  3. Any reusable subviews extracted as separate
    UIView
    subclasses
  4. A
    Model
    struct/class if JSON data is involved

针对每个界面,生成以下内容:
  1. ViewController
    View
    文件
  2. 任何自定义
    UITableViewCell
    /
    UICollectionViewCell
    子类
  3. 提取为独立
    UIView
    子类的可复用子视图
  4. 如果涉及JSON数据,提供
    Model
    结构体/类

Code Conventions

代码规范

File Structure

文件结构

swift
// MARK: - Properties
// MARK: - Lifecycle
// MARK: - Setup
// MARK: - Layout (SnapKit)
// MARK: - Actions
// MARK: - Data / Network
// MARK: - Helpers
swift
// MARK: - Properties
// MARK: - Lifecycle
// MARK: - Setup
// MARK: - Layout (SnapKit)
// MARK: - Actions
// MARK: - Data / Network
// MARK: - Helpers

SnapKit Layout Rules

SnapKit布局规则

  • Always call
    setupUI()
    and
    setupConstraints()
    from
    viewDidLoad
    (or
    init
    for UIView)
  • Add subviews in
    setupUI()
    , define constraints in
    setupConstraints()
  • Never use
    frame
    or
    autoresizingMask
    — SnapKit only
  • Use named constants for spacing:
    enum Layout { static let margin: CGFloat = 16 }
swift
// ✅ Correct SnapKit usage
private func setupConstraints() {
    titleLabel.snp.makeConstraints { make in
        make.top.equalTo(headerView.snp.bottom).offset(Layout.margin)
        make.leading.trailing.equalToSuperview().inset(Layout.margin)
    }
    
    confirmButton.snp.makeConstraints { make in
        make.bottom.equalTo(view.safeAreaLayoutGuide).inset(Layout.margin)
        make.leading.trailing.equalToSuperview().inset(Layout.margin)
        make.height.equalTo(50)
    }
}
  • 务必从
    viewDidLoad
    (或
    UIView
    init
    方法)中调用
    setupUI()
    setupConstraints()
  • setupUI()
    中添加子视图,在
    setupConstraints()
    中定义约束
  • 绝不要使用
    frame
    autoresizingMask
    — 仅使用SnapKit
  • 使用命名常量定义间距:
    enum Layout { static let margin: CGFloat = 16 }
swift
// ✅ 正确的SnapKit用法
private func setupConstraints() {
    titleLabel.snp.makeConstraints { make in
        make.top.equalTo(headerView.snp.bottom).offset(Layout.margin)
        make.leading.trailing.equalToSuperview().inset(Layout.margin)
    }
    
    confirmButton.snp.makeConstraints { make in
        make.bottom.equalTo(view.safeAreaLayoutGuide).inset(Layout.margin)
        make.leading.trailing.equalToSuperview().inset(Layout.margin)
        make.height.equalTo(50)
    }
}

Typography — PingFangSC (Always Use This)

字体 — 苹方字体(必须使用本字体)

⚠️ Never use
.systemFont
or raw
UIFont(name:)
strings — always use
.pingFangSC()
via the extension below.
swift
// UIFont+PingFangSC.swift — include this extension in every project
extension UIFont {
    enum PingFangSC: String {
        case ultralight = "PingFangSC-Ultralight"
        case thin       = "PingFangSC-Thin"
        case light      = "PingFangSC-Light"
        case regular    = "PingFangSC-Regular"
        case medium     = "PingFangSC-Medium"
        case semibold   = "PingFangSC-Semibold"
    }

    static func pingFangSC(_ style: PingFangSC, size: CGFloat) -> UIFont {
        return UIFont(name: style.rawValue, size: size) ?? .systemFont(ofSize: size)
    }
}
Usage reference:
WeightCallTypical use
Ultralight
.pingFangSC(.ultralight, size: n)
Decorative, large display numbers
Thin
.pingFangSC(.thin, size: n)
Subtle secondary info
Light
.pingFangSC(.light, size: n)
Body text, descriptions
Regular
.pingFangSC(.regular, size: n)
Default body / labels
Medium
.pingFangSC(.medium, size: n)
Emphasized labels, button text
Semibold
.pingFangSC(.semibold, size: n)
Titles, nav bar, headers
swift
// ✅ Correct — always call the extension method
titleLabel.font    = .pingFangSC(.semibold, size: 18)
bodyLabel.font     = .pingFangSC(.regular, size: 14)
priceLabel.font    = .pingFangSC(.medium, size: 16)
subtitleLabel.font = .pingFangSC(.light, size: 13)

// ❌ Never do this
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
titleLabel.font = UIFont(name: "PingFangSC-Semibold", size: 18)  // raw string usage forbidden

⚠️ 绝不要使用
.systemFont
或原始
UIFont(name:)
字符串 — 务必通过下方的扩展调用
.pingFangSC()
swift
// UIFont+PingFangSC.swift — 请在每个项目中包含此扩展
extension UIFont {
    enum PingFangSC: String {
        case ultralight = "PingFangSC-Ultralight"
        case thin       = "PingFangSC-Thin"
        case light      = "PingFangSC-Light"
        case regular    = "PingFangSC-Regular"
        case medium     = "PingFangSC-Medium"
        case semibold   = "PingFangSC-Semibold"
    }

    static func pingFangSC(_ style: PingFangSC, size: CGFloat) -> UIFont {
        return UIFont(name: style.rawValue, size: size) ?? .systemFont(ofSize: size)
    }
}
使用参考:
字重调用方式典型用途
Ultralight
.pingFangSC(.ultralight, size: n)
装饰性大字号数字
Thin
.pingFangSC(.thin, size: n)
次要辅助信息
Light
.pingFangSC(.light, size: n)
正文文本、描述
Regular
.pingFangSC(.regular, size: n)
默认正文/标签
Medium
.pingFangSC(.medium, size: n)
强调标签、按钮文本
Semibold
.pingFangSC(.semibold, size: n)
标题、导航栏、页眉
swift
// ✅ 正确用法 — 务必调用扩展方法
titleLabel.font    = .pingFangSC(.semibold, size: 18)
bodyLabel.font     = .pingFangSC(.regular, size: 14)
priceLabel.font    = .pingFangSC(.medium, size: 16)
subtitleLabel.font = .pingFangSC(.light, size: 13)

// ❌ 请勿这样做
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
titleLabel.font = UIFont(name: "PingFangSC-Semibold", size: 18)  // 禁止使用原始字符串

Colors — UIColor.colorWithHexString (Always Use This)

颜色 — UIColor.colorWithHexString(必须使用本方法)

⚠️ Never use
UIColor(hexString:)
,
UIColor(hex:)
, Hue, or SwiftHEXColors — always use
UIColor.colorWithHexString(hex:)
via the extension below.
swift
// UIColor+Hex.swift — include this extension in every project
extension UIColor {
    static func colorWithHexString(hex: String, alpha: CGFloat = 1.0) -> UIColor {
        var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
        hexSanitized = hexSanitized.hasPrefix("#") ? String(hexSanitized.dropFirst()) : hexSanitized

        var rgb: UInt64 = 0
        Scanner(string: hexSanitized).scanHexInt64(&rgb)

        let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
        let g = CGFloat((rgb & 0x00FF00) >> 8)  / 255.0
        let b = CGFloat(rgb & 0x0000FF)          / 255.0

        return UIColor(red: r, green: g, blue: b, alpha: alpha)
    }
}
Usage:
swift
// ✅ Correct
let primary = UIColor.colorWithHexString(hex: "#007AFF")
let dimmed  = UIColor.colorWithHexString(hex: "#212226", alpha: 0.5)

// ❌ Never do this
let c1 = UIColor(hexString: "#FF6B35")   // SwiftHEXColors — forbidden
let c2 = UIColor(hex: "#2C3E50")         // SwiftHEXColors — forbidden
let c3 = primary.lighten(byAmount: 0.2)  // Hue — forbidden

⚠️ 绝不要使用
UIColor(hexString:)
UIColor(hex:)
、Hue或SwiftHEXColors — 务必通过下方的扩展调用
UIColor.colorWithHexString(hex:)
swift
// UIColor+Hex.swift — 请在每个项目中包含此扩展
extension UIColor {
    static func colorWithHexString(hex: String, alpha: CGFloat = 1.0) -> UIColor {
        var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
        hexSanitized = hexSanitized.hasPrefix("#") ? String(hexSanitized.dropFirst()) : hexSanitized

        var rgb: UInt64 = 0
        Scanner(string: hexSanitized).scanHexInt64(&rgb)

        let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
        let g = CGFloat((rgb & 0x00FF00) >> 8)  / 255.0
        let b = CGFloat(rgb & 0x0000FF)          / 255.0

        return UIColor(red: r, green: g, blue: b, alpha: alpha)
    }
}
用法:
swift
// ✅ 正确用法
let primary = UIColor.colorWithHexString(hex: "#007AFF")
let dimmed  = UIColor.colorWithHexString(hex: "#212226", alpha: 0.5)

// ❌ 请勿这样做
let c1 = UIColor(hexString: "#FF6B35")   // SwiftHEXColors — 禁止使用
let c2 = UIColor(hex: "#2C3E50")         // SwiftHEXColors — 禁止使用
let c3 = primary.lighten(byAmount: 0.2)  // Hue — 禁止使用

Image Loading with Kingfisher

Kingfisher图片加载

swift
// Basic
imageView.kf.setImage(with: URL(string: urlString))

// With placeholder + options
imageView.kf.setImage(
    with: URL(string: urlString),
    placeholder: UIImage(named: "placeholder"),
    options: [
        .transition(.fade(0.25)),
        .cacheOriginalImage
    ]
)

// Cancel on reuse (in UITableViewCell)
override func prepareForReuse() {
    super.prepareForReuse()
    imageView.kf.cancelDownloadTask()
    imageView.image = nil
}
swift
// 基础用法
imageView.kf.setImage(with: URL(string: urlString))

// 带占位图和选项的用法
imageView.kf.setImage(
    with: URL(string: urlString),
    placeholder: UIImage(named: "placeholder"),
    options: [
        .transition(.fade(0.25)),
        .cacheOriginalImage
    ]
)

// 复用时取消下载(在UITableViewCell中)
override func prepareForReuse() {
    super.prepareForReuse()
    imageView.kf.cancelDownloadTask()
    imageView.image = nil
}

JSON Parsing with SwiftyJSON

SwiftyJSON解析

swift
import SwiftyJSON

struct UserModel {
    let id: Int
    let name: String
    let avatar: String
    let score: Double
    
    init(json: JSON) {
        self.id     = json["id"].intValue
        self.name   = json["name"].stringValue
        self.avatar = json["avatar"].stringValue
        self.score  = json["score"].doubleValue
    }
    
    static func list(from json: JSON) -> [UserModel] {
        return json.arrayValue.map { UserModel(json: $0) }
    }
}
swift
import SwiftyJSON

struct UserModel {
    let id: Int
    let name: String
    let avatar: String
    let score: Double
    
    init(json: JSON) {
        self.id     = json["id"].intValue
        self.name   = json["name"].stringValue
        self.avatar = json["avatar"].stringValue
        self.score  = json["score"].doubleValue
    }
    
    static func list(from json: JSON) -> [UserModel] {
        return json.arrayValue.map { UserModel(json: $0) }
    }
}

SwiftEntryKit — Popups & Toasts

SwiftEntryKit — 弹窗与提示框

Toast / Snackbar

提示框/消息条

swift
func showToast(message: String, isSuccess: Bool = true) {
    var attributes = EKAttributes.topToast
    attributes.entryBackground = .color(color: EKColor(isSuccess ? AppColor.primary : .systemRed))
    attributes.displayDuration = 2.5
    attributes.shadow = .active(with: .init(color: .black, opacity: 0.2, radius: 6))
    
    let style = EKProperty.LabelStyle(
        font: .pingFangSC(.medium, size: 14),
        color: EKColor(.white)
    )
    let labelContent = EKProperty.LabelContent(text: message, style: style)
    let contentView = EKNoteMessageView(with: labelContent)
    SwiftEntryKit.display(entry: contentView, using: attributes)
}
swift
func showToast(message: String, isSuccess: Bool = true) {
    var attributes = EKAttributes.topToast
    attributes.entryBackground = .color(color: EKColor(isSuccess ? AppColor.primary : .systemRed))
    attributes.displayDuration = 2.5
    attributes.shadow = .active(with: .init(color: .black, opacity: 0.2, radius: 6))
    
    let style = EKProperty.LabelStyle(
        font: .pingFangSC(.medium, size: 14),
        color: EKColor(.white)
    )
    let labelContent = EKProperty.LabelContent(text: message, style: style)
    let contentView = EKNoteMessageView(with: labelContent)
    SwiftEntryKit.display(entry: contentView, using: attributes)
}

Center Alert Popup

居中警告弹窗

swift
func showAlertPopup(title: String, message: String, confirmAction: @escaping () -> Void) {
    var attributes = EKAttributes.centerFloat
    attributes.entryBackground = .color(color: EKColor(.white))
    attributes.roundCorners = .all(radius: 16)
    attributes.shadow = .active(with: .init(color: .black, opacity: 0.15, radius: 10))
    attributes.screenInteraction = .absorbTouches
    attributes.entryInteraction = .absorbTouches
    attributes.displayDuration = .infinity
    
    // Build your custom UIView popup, then:
    let popupView = CustomAlertView(title: title, message: message)
    popupView.onConfirm = {
        SwiftEntryKit.dismiss()
        confirmAction()
    }
    SwiftEntryKit.display(entry: popupView, using: attributes)
}
swift
func showAlertPopup(title: String, message: String, confirmAction: @escaping () -> Void) {
    var attributes = EKAttributes.centerFloat
    attributes.entryBackground = .color(color: EKColor(.white))
    attributes.roundCorners = .all(radius: 16)
    attributes.shadow = .active(with: .init(color: .black, opacity: 0.15, radius: 10))
    attributes.screenInteraction = .absorbTouches
    attributes.entryInteraction = .absorbTouches
    attributes.displayDuration = .infinity
    
    // 构建自定义UIView弹窗,然后:
    let popupView = CustomAlertView(title: title, message: message)
    popupView.onConfirm = {
        SwiftEntryKit.dismiss()
        confirmAction()
    }
    SwiftEntryKit.display(entry: popupView, using: attributes)
}

Bottom Sheet ⚠️ Always use this exact configuration

底部弹窗 ⚠️ 务必使用此精确配置

swift
/// Standard bottom sheet — MUST use this attribute setup exactly.
/// popupView is a UIView subclass that self-sizes via its own constraints (height: .intrinsic).
func showBottomSheet(popupView: UIView) {
    var attributes = EKAttributes()
    attributes.position = .bottom
    attributes.displayDuration = .infinity
    attributes.screenBackground = .color(
        color: .init(
            light: UIColor(white: 0, alpha: 0.4),
            dark:  UIColor(white: 0, alpha: 0.4)
        )
    )
    attributes.entryBackground = .clear           // popup view draws its own background
    attributes.screenInteraction = .dismiss        // tap outside to dismiss
    attributes.entryInteraction = .forward         // touches pass through to content
    attributes.scroll = .disabled
    attributes.positionConstraints.size = .init(width: .fill, height: .intrinsic)
    attributes.positionConstraints.safeArea = .overridden  // extend under home indicator
    attributes.positionConstraints.verticalOffset = 0

    SwiftEntryKit.display(entry: popupView, using: attributes)
}

// Dismiss from inside the popup:
// SwiftEntryKit.dismiss()
Rules for the popup UIView:
  • Draw its own background (white + top rounded corners) — NOT via
    entryBackground
  • Use SnapKit so the view's intrinsic height is driven by its own content constraints
  • Add a bottom padding area to account for home indicator
swift
final class SampleBottomSheetView: UIView {

    // MARK: - UI
    private let containerView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        v.layer.cornerRadius = 20
        v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        v.clipsToBounds = true
        return v
    }()

    // MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
        setupConstraints()
    }
    required init?(coder: NSCoder) { fatalError() }

    // MARK: - Setup
    private func setupUI() {
        backgroundColor = .clear
        addSubview(containerView)
        // add content subviews to containerView...
    }

    private func setupConstraints() {
        containerView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            // ⚠️ Do NOT pin bottom to superview — let content drive height
        }
        // ...content constraints inside containerView...

        // Safe-area bottom padding (home indicator)
        let bottomPadding: CGFloat = 34
        containerView.snp.makeConstraints { make in
            make.bottom.equalToSuperview().inset(0)
        }
        // Add a spacer view at the bottom of containerView with height 34
    }
}

swift
/// 标准底部弹窗 — 必须完全使用此属性配置。
/// popupView是一个UIView子类,其内部约束会自动适配高度(height: .intrinsic)。
func showBottomSheet(popupView: UIView) {
    var attributes = EKAttributes()
    attributes.position = .bottom
    attributes.displayDuration = .infinity
    attributes.screenBackground = .color(
        color: .init(
            light: UIColor(white: 0, alpha: 0.4),
            dark:  UIColor(white: 0, alpha: 0.4)
        )
    )
    attributes.entryBackground = .clear           // 弹窗视图自行绘制背景
    attributes.screenInteraction = .dismiss        // 点击外部关闭
    attributes.entryInteraction = .forward         // 触摸事件传递给内容
    attributes.scroll = .disabled
    attributes.positionConstraints.size = .init(width: .fill, height: .intrinsic)
    attributes.positionConstraints.safeArea = .overridden  // 延伸至Home指示器下方
    attributes.positionConstraints.verticalOffset = 0

    SwiftEntryKit.display(entry: popupView, using: attributes)
}

// 从弹窗内部关闭:
// SwiftEntryKit.dismiss()
弹窗UIView规则:
  • 自行绘制背景(白色+顶部圆角)— 不要通过
    entryBackground
    设置
  • 使用SnapKit,使视图的内在高度由自身内容约束决定
  • 添加底部内边距区域以适配Home指示器
swift
final class SampleBottomSheetView: UIView {

    // MARK: - UI
    private let containerView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        v.layer.cornerRadius = 20
        v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        v.clipsToBounds = true
        return v
    }()

    // MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
        setupConstraints()
    }
    required init?(coder: NSCoder) { fatalError() }

    // MARK: - Setup
    private func setupUI() {
        backgroundColor = .clear
        addSubview(containerView)
        // 向containerView中添加内容子视图...
    }

    private func setupConstraints() {
        containerView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            // ⚠️ 不要将底部固定到父视图 — 让内容驱动高度
        }
        // ...containerView内部的内容约束...

        // 安全区域底部内边距(适配Home指示器)
        let bottomPadding: CGFloat = 34
        containerView.snp.makeConstraints { make in
            make.bottom.equalToSuperview().inset(0)
        }
        // 在containerView底部添加高度为34的间隔视图
    }
}

UITableView / UICollectionView Best Practices

UITableView / UICollectionView最佳实践

swift
// Cell registration
tableView.register(ProductCell.self, forCellReuseIdentifier: ProductCell.reuseId)

// Cell class template
final class ProductCell: UITableViewCell {
    static let reuseId = "ProductCell"
    
    // MARK: - UI
    private let containerView = UIView()
    private let thumbImageView = UIImageView()
    private let titleLabel = UILabel()
    private let priceLabel = UILabel()
    
    // MARK: - Init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUI()
        setupConstraints()
    }
    required init?(coder: NSCoder) { fatalError() }
    
    // MARK: - Setup
    private func setupUI() {
        selectionStyle = .none
        contentView.addSubview(containerView)
        containerView.addSubview(thumbImageView)
        containerView.addSubview(titleLabel)
        containerView.addSubview(priceLabel)
        
        titleLabel.font  = .pingFangSC(.medium, size: 15)
        titleLabel.textColor = AppColor.text
        priceLabel.font  = .pingFangSC(.semibold, size: 16)
        priceLabel.textColor = AppColor.primary
    }
    
    private func setupConstraints() {
        containerView.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16))
        }
        thumbImageView.snp.makeConstraints { make in
            make.leading.top.bottom.equalToSuperview()
            make.width.height.equalTo(80)
        }
        titleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(12)
            make.leading.equalTo(thumbImageView.snp.trailing).offset(12)
            make.trailing.equalToSuperview().inset(12)
        }
        priceLabel.snp.makeConstraints { make in
            make.bottom.equalToSuperview().inset(12)
            make.leading.equalTo(thumbImageView.snp.trailing).offset(12)
        }
    }
    
    // MARK: - Configure
    func configure(with model: ProductModel) {
        titleLabel.text = model.name
        priceLabel.text = \(model.price)"
        thumbImageView.kf.setImage(with: URL(string: model.imageUrl), placeholder: UIImage(named: "placeholder"))
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        thumbImageView.kf.cancelDownloadTask()
        thumbImageView.image = nil
    }
}

swift
// 单元格注册
tableView.register(ProductCell.self, forCellReuseIdentifier: ProductCell.reuseId)

// 单元格类模板
final class ProductCell: UITableViewCell {
    static let reuseId = "ProductCell"
    
    // MARK: - UI
    private let containerView = UIView()
    private let thumbImageView = UIImageView()
    private let titleLabel = UILabel()
    private let priceLabel = UILabel()
    
    // MARK: - Init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUI()
        setupConstraints()
    }
    required init?(coder: NSCoder) { fatalError() }
    
    // MARK: - Setup
    private func setupUI() {
        selectionStyle = .none
        contentView.addSubview(containerView)
        containerView.addSubview(thumbImageView)
        containerView.addSubview(titleLabel)
        containerView.addSubview(priceLabel)
        
        titleLabel.font  = .pingFangSC(.medium, size: 15)
        titleLabel.textColor = AppColor.text
        priceLabel.font  = .pingFangSC(.semibold, size: 16)
        priceLabel.textColor = AppColor.primary
    }
    
    private func setupConstraints() {
        containerView.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16))
        }
        thumbImageView.snp.makeConstraints { make in
            make.leading.top.bottom.equalToSuperview()
            make.width.height.equalTo(80)
        }
        titleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(12)
            make.leading.equalTo(thumbImageView.snp.trailing).offset(12)
            make.trailing.equalToSuperview().inset(12)
        }
        priceLabel.snp.makeConstraints { make in
            make.bottom.equalToSuperview().inset(12)
            make.leading.equalTo(thumbImageView.snp.trailing).offset(12)
        }
    }
    
    // MARK: - 配置
    func configure(with model: ProductModel) {
        titleLabel.text = model.name
        priceLabel.text = \(model.price)"
        thumbImageView.kf.setImage(with: URL(string: model.imageUrl), placeholder: UIImage(named: "placeholder"))
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        thumbImageView.kf.cancelDownloadTask()
        thumbImageView.image = nil
    }
}

Navigation Bar Customization

导航栏自定义

swift
private func setupNavigationBar() {
    title = "Page Title"
    navigationController?.navigationBar.tintColor = AppColor.primary
    navigationController?.navigationBar.titleTextAttributes = [
        .foregroundColor: AppColor.text,
        .font: UIFont.pingFangSC(.semibold, size: 17)
    ]
    // Right bar button
    let rightBtn = UIBarButtonItem(image: UIImage(systemName: "bell"), 
                                   style: .plain, 
                                   target: self, 
                                   action: #selector(rightButtonTapped))
    navigationItem.rightBarButtonItem = rightBtn
}

swift
private func setupNavigationBar() {
    title = "页面标题"
    navigationController?.navigationBar.tintColor = AppColor.primary
    navigationController?.navigationBar.titleTextAttributes = [
        .foregroundColor: AppColor.text,
        .font: UIFont.pingFangSC(.semibold, size: 17)
    ]
    // 右侧导航按钮
    let rightBtn = UIBarButtonItem(image: UIImage(systemName: "bell"), 
                                   style: .plain, 
                                   target: self, 
                                   action: #selector(rightButtonTapped))
    navigationItem.rightBarButtonItem = rightBtn
}

Empty State & Loading State

空数据状态与加载状态

swift
// Loading
func showLoading() {
    let indicator = UIActivityIndicatorView(style: .medium)
    indicator.tag = 999
    indicator.startAnimating()
    view.addSubview(indicator)
    indicator.snp.makeConstraints { $0.center.equalToSuperview() }
}

func hideLoading() {
    view.viewWithTag(999)?.removeFromSuperview()
}

// Empty state
func showEmptyState(message: String = "暂无数据") {
    let label = UILabel()
    label.text = message
    label.textColor = AppColor.subtext
    label.font = .pingFangSC(.regular, size: 15)
    label.tag = 998
    view.addSubview(label)
    label.snp.makeConstraints { $0.center.equalToSuperview() }
}

swift
// 加载状态
func showLoading() {
    let indicator = UIActivityIndicatorView(style: .medium)
    indicator.tag = 999
    indicator.startAnimating()
    view.addSubview(indicator)
    indicator.snp.makeConstraints { $0.center.equalToSuperview() }
}

func hideLoading() {
    view.viewWithTag(999)?.removeFromSuperview()
}

// 空数据状态
func showEmptyState(message: String = "暂无数据") {
    let label = UILabel()
    label.text = message
    label.textColor = AppColor.subtext
    label.font = .pingFangSC(.regular, size: 15)
    label.tag = 998
    view.addSubview(label)
    label.snp.makeConstraints { $0.center.equalToSuperview() }
}

Output Checklist

输出检查清单

Before finishing, verify:
  • No
    frame
    /
    AutoresizingMask
    usage — SnapKit only
  • All fonts use
    .pingFangSC()
    extension — never
    .systemFont
    or raw
    UIFont(name:)
    strings
  • Safe area insets handled (
    safeAreaLayoutGuide
    )
  • All colors use
    UIColor.colorWithHexString(hex:)
    via
    AppColor
    enum — never Hue or SwiftHEXColors
  • Images use
    kf.setImage
    with placeholder
  • prepareForReuse
    cancels Kingfisher tasks in cells
  • JSON models use
    SwiftyJSON
    with
    init(json: JSON)
  • Popups use
    SwiftEntryKit
    (no
    UIAlertController
    unless truly native alert)
  • All UI created programmatically (no Storyboard/XIB unless asked)
  • // MARK:
    sections used for code organization
  • Spacing values extracted to
    enum Layout
  • Bottom sheets use the exact
    EKAttributes
    config from the Bottom Sheet section (never
    EKAttributes.bottomFloat
    )
  • Bottom sheet popup view sets
    entryBackground = .clear
    and draws its own background
  • Bottom sheet config includes
    safeArea = .overridden
    and
    verticalOffset = 0
完成前,请验证:
  • 未使用
    frame
    /
    AutoresizingMask
    — 仅使用SnapKit
  • 所有字体均使用
    .pingFangSC()
    扩展 — 绝不要使用
    .systemFont
    或原始
    UIFont(name:)
    字符串
  • 已处理安全区域内边距(
    safeAreaLayoutGuide
  • 所有颜色均通过
    AppColor
    枚举使用
    UIColor.colorWithHexString(hex:)
    — 绝不要使用Hue或SwiftHEXColors
  • 图片加载使用
    kf.setImage
    并带有占位图
  • 单元格的
    prepareForReuse
    方法会取消Kingfisher下载任务
  • JSON模型使用
    SwiftyJSON
    并实现
    init(json: JSON)
  • 弹窗使用
    SwiftEntryKit
    (除非是原生警告框,否则不要使用
    UIAlertController
  • 所有UI均通过代码创建(除非用户要求,否则不要使用Storyboard/XIB)
  • 使用
    // MARK:
    注释组织代码
  • 间距值已提取到
    enum Layout
  • 底部弹窗使用底部弹窗章节中的精确
    EKAttributes
    配置(绝不要使用
    EKAttributes.bottomFloat
  • 底部弹窗视图设置
    entryBackground = .clear
    并自行绘制背景
  • 底部弹窗配置包含
    safeArea = .overridden
    verticalOffset = 0