programmatic-uikit-layout

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Programmatic UIKit Layout with Auto Layout

基于Auto Layout的UIKit编程式布局

Overview

概述

Programmatic UIKit layout with anchors provides type-safe, maintainable UI code used by production apps at scale. Stack views solve 50%+ of layout problems, anchors provide compile-time safety, design systems ensure consistency.
Core principle: Anchors-only (no Visual Format Language), stack-first composition, reusable components with centralized spacing/colors.
**基于Auto Layout的UIKit编程式布局通过锚点提供类型安全、可维护的UI代码,被大规模生产应用所采用。**Stack View可解决50%以上的布局问题,锚点提供编译时安全性,设计系统确保一致性。
**核心原则:**仅使用锚点(不使用可视化格式语言)、优先采用Stack View组合、使用集中式间距/颜色配置的可复用组件。

Foundation Setup

基础设置

Step 1: Disable Storyboards

步骤1:禁用Storyboard

Remove Main.storyboard:
  1. Delete Main.storyboard file
  2. In Info.plist, delete "Main storyboard file base name" entry
  3. Delete "Storyboard Name" in Target → Info → Custom iOS Target Properties
Configure SceneDelegate:
swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = MainViewController()
        window?.makeKeyAndVisible()
    }
}
移除Main.storyboard:
  1. 删除Main.storyboard文件
  2. 在Info.plist中,删除"Main storyboard file base name"条目
  3. 在Target → Info → Custom iOS Target Properties中删除"Storyboard Name"
配置SceneDelegate:
swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = MainViewController()
        window?.makeKeyAndVisible()
    }
}

Step 2: Essential UIView Setup

步骤2:必备UIView设置

CRITICAL: Every view needs
translatesAutoresizingMaskIntoConstraints = false
swift
// ❌ WRONG: Constraints conflict with autoresizing mask
let label = UILabel()
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
// Runtime error: conflicting constraints

// ✅ CORRECT: Disable autoresizing mask
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
Rule: Set
translatesAutoresizingMaskIntoConstraints = false
BEFORE adding constraints.
**关键:**每个视图都需要设置
translatesAutoresizingMaskIntoConstraints = false
swift
// ❌ WRONG: Constraints conflict with autoresizing mask
let label = UILabel()
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
// Runtime error: conflicting constraints

// ✅ CORRECT: Disable autoresizing mask
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
规则:在添加约束之前设置
translatesAutoresizingMaskIntoConstraints = false

Layout Anchors (Preferred Method)

布局锚点(推荐方法)

Why Anchors Over NSLayoutConstraint

为什么选择锚点而非NSLayoutConstraint

FeatureLayout AnchorsNSLayoutConstraint
Type safety✅ Compile-time checks❌ Runtime errors
Readability✅ Fluent API❌ Verbose initializer
Error prevention✅ Can't mix X/Y axes❌ Easy to make mistakes
swift
// ❌ WRONG: NSLayoutConstraint (verbose, error-prone)
NSLayoutConstraint(
    item: label,
    attribute: .top,
    relatedBy: .equal,
    toItem: view,
    attribute: .top,
    multiplier: 1.0,
    constant: 20
).isActive = true

// ✅ CORRECT: Layout anchors (concise, type-safe)
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
特性布局锚点NSLayoutConstraint
类型安全✅ 编译时检查❌ 运行时错误
可读性✅ 流畅API❌ 冗长的初始化器
错误预防✅ 无法混合X/Y轴❌ 容易出错
swift
// ❌ WRONG: NSLayoutConstraint (verbose, error-prone)
NSLayoutConstraint(
    item: label,
    attribute: .top,
    relatedBy: .equal,
    toItem: view,
    attribute: .top,
    multiplier: 1.0,
    constant: 20
).isActive = true

// ✅ CORRECT: Layout anchors (concise, type-safe)
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true

Anchor Types

锚点类型

X-axis:
leadingAnchor
,
trailingAnchor
,
leftAnchor
,
rightAnchor
,
centerXAnchor
Y-axis:
topAnchor
,
bottomAnchor
,
centerYAnchor
Dimension:
widthAnchor
,
heightAnchor
Type safety:
swift
// ✅ Compiles: Same axis
label.leadingAnchor.constraint(equalTo: view.leadingAnchor)

// ❌ Compiler error: Mixed axes
label.leadingAnchor.constraint(equalTo: view.topAnchor)  // Won't compile!
X轴:
leadingAnchor
,
trailingAnchor
,
leftAnchor
,
rightAnchor
,
centerXAnchor
Y轴:
topAnchor
,
bottomAnchor
,
centerYAnchor
尺寸:
widthAnchor
,
heightAnchor
类型安全:
swift
// ✅ Compiles: Same axis
label.leadingAnchor.constraint(equalTo: view.leadingAnchor)

// ❌ Compiler error: Mixed axes
label.leadingAnchor.constraint(equalTo: view.topAnchor)  // Won't compile!

Basic Constraint Patterns

基础约束模式

Pin to edges:
swift
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

NSLayoutConstraint.activate([
    label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
    label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
    label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20)
])
Center with size:
swift
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

NSLayoutConstraint.activate([
    label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    label.widthAnchor.constraint(equalToConstant: 200),
    label.heightAnchor.constraint(equalToConstant: 50)
])
Aspect ratio:
swift
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)

NSLayoutConstraint.activate([
    imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 9.0/16.0)  // 16:9
])
固定到边缘:
swift
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

NSLayoutConstraint.activate([
    label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
    label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
    label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20)
])
居中并设置尺寸:
swift
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

NSLayoutConstraint.activate([
    label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    label.widthAnchor.constraint(equalToConstant: 200),
    label.heightAnchor.constraint(equalToConstant: 50)
])
宽高比:
swift
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)

NSLayoutConstraint.activate([
    imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 9.0/16.0)  // 16:9
])

Safe Area Layout Guide

安全区域布局指南

CRITICAL: Use
safeAreaLayoutGuide
for modern iPhones (notch, dynamic island).
swift
// ❌ WRONG: Content hidden behind notch
label.topAnchor.constraint(equalTo: view.topAnchor)

// ✅ CORRECT: Respects safe area
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
Standard pattern:
swift
NSLayoutConstraint.activate([
    contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
**关键:**针对现代iPhone(刘海屏、灵动岛)使用
safeAreaLayoutGuide
swift
// ❌ WRONG: Content hidden behind notch
label.topAnchor.constraint(equalTo: view.topAnchor)

// ✅ CORRECT: Respects safe area
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
标准模式:
swift
NSLayoutConstraint.activate([
    contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])

Stack Views (Stack-First Philosophy)

Stack View(优先使用Stack的理念)

CRITICAL: Stack views solve 50%+ of layout problems. Use them liberally.
dot
digraph layout_choice {
    "Need to arrange multiple views?" [shape=diamond];
    "All in line (row/column)?" [shape=diamond];
    "Use UIStackView" [shape=box, style=filled, fillcolor=lightgreen];
    "Use nested UIStackViews" [shape=box, style=filled, fillcolor=lightblue];
    "Use anchors" [shape=box, style=filled, fillcolor=yellow];

    "Need to arrange multiple views?" -> "All in line (row/column)?" [label="yes"];
    "Need to arrange multiple views?" -> "Use anchors" [label="no"];
    "All in line (row/column)?" -> "Use UIStackView" [label="yes"];
    "All in line (row/column)?" -> "Use nested UIStackViews" [label="no (complex grid)"];
}
**关键:**Stack View可解决50%以上的布局问题,尽量多使用。
dot
digraph layout_choice {
    "Need to arrange multiple views?" [shape=diamond];
    "All in line (row/column)?" [shape=diamond];
    "Use UIStackView" [shape=box, style=filled, fillcolor=lightgreen];
    "Use nested UIStackViews" [shape=box, style=filled, fillcolor=lightblue];
    "Use anchors" [shape=box, style=filled, fillcolor=yellow];

    "Need to arrange multiple views?" -> "All in line (row/column)?" [label="yes"];
    "Need to arrange multiple views?" -> "Use anchors" [label="no"];
    "All in line (row/column)?" -> "Use UIStackView" [label="yes"];
    "All in line (row/column)?" -> "Use nested UIStackViews" [label="no (complex grid)"];
}

Basic Stack View

基础Stack View

swift
let stackView = UIStackView()
stackView.axis = .vertical  // or .horizontal
stackView.spacing = 16
stackView.alignment = .fill  // .leading, .center, .trailing
stackView.distribution = .fill  // .fillEqually, .equalSpacing, etc.
stackView.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stackView)
NSLayoutConstraint.activate([
    stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
    stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
    stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])

stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(subtitleLabel)
stackView.addArrangedSubview(button)
swift
let stackView = UIStackView()
stackView.axis = .vertical  // or .horizontal
stackView.spacing = 16
stackView.alignment = .fill  // .leading, .center, .trailing
stackView.distribution = .fill  // .fillEqually, .equalSpacing, etc.
stackView.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stackView)
NSLayoutConstraint.activate([
    stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
    stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
    stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])

stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(subtitleLabel)
stackView.addArrangedSubview(button)

Nested Stack Views (Complex Layouts)

嵌套Stack View(复杂布局)

swift
// Horizontal stack with icon + labels
let horizontalStack = UIStackView()
horizontalStack.axis = .horizontal
horizontalStack.spacing = 12
horizontalStack.alignment = .center

let iconImageView = UIImageView(image: UIImage(systemName: "star.fill"))
iconImageView.widthAnchor.constraint(equalToConstant: 24).isActive = true
iconImageView.heightAnchor.constraint(equalToConstant: 24).isActive = true

// Vertical stack for title + subtitle
let verticalStack = UIStackView()
verticalStack.axis = .vertical
verticalStack.spacing = 4

verticalStack.addArrangedSubview(titleLabel)
verticalStack.addArrangedSubview(subtitleLabel)

horizontalStack.addArrangedSubview(iconImageView)
horizontalStack.addArrangedSubview(verticalStack)

view.addSubview(horizontalStack)
// Pin horizontalStack with anchors
swift
// Horizontal stack with icon + labels
let horizontalStack = UIStackView()
horizontalStack.axis = .horizontal
horizontalStack.spacing = 12
horizontalStack.alignment = .center

let iconImageView = UIImageView(image: UIImage(systemName: "star.fill"))
iconImageView.widthAnchor.constraint(equalToConstant: 24).isActive = true
iconImageView.heightAnchor.constraint(equalToConstant: 24).isActive = true

// Vertical stack for title + subtitle
let verticalStack = UIStackView()
verticalStack.axis = .vertical
verticalStack.spacing = 4

verticalStack.addArrangedSubview(titleLabel)
verticalStack.addArrangedSubview(subtitleLabel)

horizontalStack.addArrangedSubview(iconImageView)
horizontalStack.addArrangedSubview(verticalStack)

view.addSubview(horizontalStack)
// Pin horizontalStack with anchors

Spacer Views

间隔视图

swift
// Add flexible spacing in stack view
let spacer = UIView()
spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
stackView.addArrangedSubview(spacer)

// Pattern: Button at bottom
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.addArrangedSubview(spacer)  // Pushes button to bottom
stackView.addArrangedSubview(button)
swift
// Add flexible spacing in stack view
let spacer = UIView()
spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
stackView.addArrangedSubview(spacer)

// Pattern: Button at bottom
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.addArrangedSubview(spacer)  // Pushes button to bottom
stackView.addArrangedSubview(button)

Design System Pattern

设计系统模式

Theme (Centralized Spacing/Colors)

主题(集中式间距/颜色)

swift
enum Theme {
    enum Spacing {
        static let tiny: CGFloat = 4
        static let small: CGFloat = 8
        static let medium: CGFloat = 16
        static let large: CGFloat = 24
        static let xLarge: CGFloat = 32
    }

    enum CornerRadius {
        static let small: CGFloat = 4
        static let medium: CGFloat = 8
        static let large: CGFloat = 12
        static let xLarge: CGFloat = 16
    }

    enum Colors {
        static let primary = UIColor.systemBlue
        static let secondary = UIColor.systemGray
        static let background = UIColor.systemBackground
        static let text = UIColor.label
        static let textSecondary = UIColor.secondaryLabel
    }
}

// Usage:
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.medium
label.textColor = Theme.Colors.text
swift
enum Theme {
    enum Spacing {
        static let tiny: CGFloat = 4
        static let small: CGFloat = 8
        static let medium: CGFloat = 16
        static let large: CGFloat = 24
        static let xLarge: CGFloat = 32
    }

    enum CornerRadius {
        static let small: CGFloat = 4
        static let medium: CGFloat = 8
        static let large: CGFloat = 12
        static let xLarge: CGFloat = 16
    }

    enum Colors {
        static let primary = UIColor.systemBlue
        static let secondary = UIColor.systemGray
        static let background = UIColor.systemBackground
        static let text = UIColor.label
        static let textSecondary = UIColor.secondaryLabel
    }
}

// Usage:
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.medium
label.textColor = Theme.Colors.text

Reusable Components

可复用组件

Card View:
swift
final class CardView: UIView {
    private let containerView = UIView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) not implemented")
    }

    private func setupView() {
        backgroundColor = Theme.Colors.background
        layer.cornerRadius = Theme.CornerRadius.large
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.1
        layer.shadowOffset = CGSize(width: 0, height: 2)
        layer.shadowRadius = 4

        containerView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(containerView)

        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: topAnchor, constant: Theme.Spacing.medium),
            containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Theme.Spacing.medium),
            containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Theme.Spacing.medium),
            containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Theme.Spacing.medium)
        ])
    }

    func setContent(_ view: UIView) {
        containerView.subviews.forEach { $0.removeFromSuperview() }
        view.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(view)

        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: containerView.topAnchor),
            view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
    }
}
Primary Button:
swift
final class PrimaryButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) not implemented")
    }

    private func setupButton() {
        backgroundColor = Theme.Colors.primary
        setTitleColor(.white, for: .normal)
        layer.cornerRadius = Theme.CornerRadius.medium
        titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
        contentEdgeInsets = UIEdgeInsets(
            top: Theme.Spacing.medium,
            left: Theme.Spacing.large,
            bottom: Theme.Spacing.medium,
            right: Theme.Spacing.large
        )
    }
}
卡片视图:
swift
final class CardView: UIView {
    private let containerView = UIView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) not implemented")
    }

    private func setupView() {
        backgroundColor = Theme.Colors.background
        layer.cornerRadius = Theme.CornerRadius.large
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.1
        layer.shadowOffset = CGSize(width: 0, height: 2)
        layer.shadowRadius = 4

        containerView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(containerView)

        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: topAnchor, constant: Theme.Spacing.medium),
            containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Theme.Spacing.medium),
            containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Theme.Spacing.medium),
            containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Theme.Spacing.medium)
        ])
    }

    func setContent(_ view: UIView) {
        containerView.subviews.forEach { $0.removeFromSuperview() }
        view.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(view)

        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: containerView.topAnchor),
            view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
    }
}
主按钮:
swift
final class PrimaryButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) not implemented")
    }

    private func setupButton() {
        backgroundColor = Theme.Colors.primary
        setTitleColor(.white, for: .normal)
        layer.cornerRadius = Theme.CornerRadius.medium
        titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
        contentEdgeInsets = UIEdgeInsets(
            top: Theme.Spacing.medium,
            left: Theme.Spacing.large,
            bottom: Theme.Spacing.medium,
            right: Theme.Spacing.large
        )
    }
}

Layout Priority & Content Hugging/Compression

布局优先级与内容拥抱/压缩

Content Hugging Priority

内容拥抱优先级

Controls how much view resists growing beyond intrinsic content size.
swift
// Default: 250 (low)
// Higher priority = resists growing more

label.setContentHuggingPriority(.required, for: .horizontal)  // 1000
button.setContentHuggingPriority(.defaultLow, for: .horizontal)  // 250

// Result: Button expands, label stays at intrinsic width
控制视图抗拒超出内在内容尺寸的程度。
swift
// Default: 250 (low)
// Higher priority = resists growing more

label.setContentHuggingPriority(.required, for: .horizontal)  // 1000
button.setContentHuggingPriority(.defaultLow, for: .horizontal)  // 250

// Result: Button expands, label stays at intrinsic width

Content Compression Resistance

内容压缩抵抗优先级

Controls how much view resists shrinking below intrinsic content size.
swift
// Default: 750 (high)
// Higher priority = resists shrinking more

titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)  // 1000
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)  // 250

// Result: Subtitle truncates before title
Common pattern: Label in horizontal stack
swift
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 8

let label = UILabel()
label.text = "Very long text that might truncate..."
label.setContentHuggingPriority(.defaultLow, for: .horizontal)  // Allows expansion
label.setContentCompressionResistancePriority(.required, for: .horizontal)  // Prevents shrinking

let button = UIButton()
button.setContentHuggingPriority(.required, for: .horizontal)  // Stays at intrinsic width

stackView.addArrangedSubview(label)
stackView.addArrangedSubview(button)
控制视图抗拒缩小到内在内容尺寸以下的程度。
swift
// Default: 750 (high)
// Higher priority = resists shrinking more

titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)  // 1000
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)  // 250

// Result: Subtitle truncates before title
常见模式:水平Stack中的标签
swift
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 8

let label = UILabel()
label.text = "Very long text that might truncate..."
label.setContentHuggingPriority(.defaultLow, for: .horizontal)  // Allows expansion
label.setContentCompressionResistancePriority(.required, for: .horizontal)  // Prevents shrinking

let button = UIButton()
button.setContentHuggingPriority(.required, for: .horizontal)  // Stays at intrinsic width

stackView.addArrangedSubview(label)
stackView.addArrangedSubview(button)

Common Mistakes

常见错误

MistakeRealityFix
"Forgot translatesAutoresizingMaskIntoConstraints = false"Conflicting constraints, layout breaksSet to false BEFORE constraints
"Using view.topAnchor instead of safeAreaLayoutGuide"Content hidden behind notchAlways use safeAreaLayoutGuide
"Individual .isActive = true for each constraint"Verbose, inefficientUse NSLayoutConstraint.activate([])
"Visual Format Language is easier"VFL is deprecated, error-proneUse anchors only
"Don't need content hugging/compression"Labels truncate unexpectedly, buttons expand wrongSet priorities explicitly
"Adding subview after constraints"Crash: view not in hierarchyaddSubview() BEFORE activate()
错误实际情况修复方法
"忘记设置translatesAutoresizingMaskIntoConstraints = false"约束冲突,布局崩溃在添加约束之前设置为false
"使用view.topAnchor而非safeAreaLayoutGuide"内容被刘海遮挡始终使用safeAreaLayoutGuide
"为每个约束单独设置.isActive = true"冗长且低效使用NSLayoutConstraint.activate([])
"可视化格式语言更简单"VFL已被弃用,容易出错仅使用锚点
"不需要内容拥抱/压缩优先级"标签意外截断,按钮错误扩展显式设置优先级
"添加约束后再添加子视图"崩溃:视图不在层级中添加子视图后再激活约束

Debugging Layout Issues

调试布局问题

Ambiguous Layout

模糊布局

swift
// In UIViewController viewDidLoad or custom view init
#if DEBUG
DispatchQueue.main.async {
    if self.view.hasAmbiguousLayout {
        print("⚠️ Ambiguous layout detected")
        self.view.exerciseAmbiguityInLayout()  // Animate between valid layouts
    }
}
#endif
swift
// In UIViewController viewDidLoad or custom view init
#if DEBUG
DispatchQueue.main.async {
    if self.view.hasAmbiguousLayout {
        print("⚠️ Ambiguous layout detected")
        self.view.exerciseAmbiguityInLayout()  // Animate between valid layouts
    }
}
#endif

Constraint Conflicts

约束冲突

Runtime error: "Unable to simultaneously satisfy constraints..."
Debug:
  1. Read error message for constraint IDs
  2. Add identifiers to constraints:
swift
let constraint = label.topAnchor.constraint(equalTo: view.topAnchor)
constraint.identifier = "label-top"
constraint.isActive = true
  1. Lower priority of less important constraint:
swift
let constraint = label.heightAnchor.constraint(equalToConstant: 50)
constraint.priority = .defaultHigh  // 750 instead of 1000
constraint.isActive = true
运行时错误:"Unable to simultaneously satisfy constraints..."
调试方法:
  1. 阅读错误信息中的约束ID
  2. 为约束添加标识符:
swift
let constraint = label.topAnchor.constraint(equalTo: view.topAnchor)
constraint.identifier = "label-top"
constraint.isActive = true
  1. 降低次要约束的优先级:
swift
let constraint = label.heightAnchor.constraint(equalToConstant: 50)
constraint.priority = .defaultHigh  // 750 instead of 1000
constraint.isActive = true

Quick Reference

快速参考

Basic setup:
swift
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
parentView.addSubview(view)

NSLayoutConstraint.activate([
    view.topAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.topAnchor),
    view.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
    view.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
    view.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
])
Stack view:
swift
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = Theme.Spacing.medium
stack.addArrangedSubview(view1)
stack.addArrangedSubview(view2)
Design system:
swift
view.backgroundColor = Theme.Colors.background
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.large
基础设置:
swift
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
parentView.addSubview(view)

NSLayoutConstraint.activate([
    view.topAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.topAnchor),
    view.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
    view.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
    view.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
])
Stack View:
swift
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = Theme.Spacing.medium
stack.addArrangedSubview(view1)
stack.addArrangedSubview(view2)
设计系统:
swift
view.backgroundColor = Theme.Colors.background
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.large

Red Flags - STOP and Reconsider

危险信号 - 停止并重新考虑

  • Using Visual Format Language → Switch to anchors
  • Not disabling
    translatesAutoresizingMaskIntoConstraints
    → Constraint conflicts
  • Ignoring safe area layout guide → Content hidden on modern iPhones
  • Creating 5+ constraints manually → Consider UIStackView
  • Hard-coded spacing values (8, 16, 24) scattered everywhere → Centralize in Theme
  • Activating constraints individually → Batch with activate([])
  • Ambiguous layout in production → Add intrinsic size or missing constraints
  • 使用可视化格式语言 → 切换到锚点
  • 未禁用
    translatesAutoresizingMaskIntoConstraints
    → 约束冲突
  • 忽略安全区域布局指南 → 现代iPhone上内容被遮挡
  • 手动创建5个以上约束 → 考虑使用UIStackView
  • 分散的硬编码间距值(8、16、24) → 在Theme中集中管理
  • 单独激活约束 → 使用activate([])批量处理
  • 生产环境中存在模糊布局 → 添加内在尺寸或缺失的约束

Real-World Impact

实际影响

Before: Mixed VFL + anchors + raw NSLayoutConstraint. 500+ lines for profile screen. Frequent constraint conflicts.
After: Anchors-only + stack views. 200 lines, zero conflicts, easier to maintain.

Before: Hard-coded spacing (8px, 12px, 16px) across 30 view controllers. Design update = edit 30 files.
After: Theme.Spacing.medium. Design update = edit 1 file, affects entire app.

Before: Custom complex constraint logic for card layout. 100 lines, hard to debug.
After: Reusable CardView component. 30 lines, used in 15 places consistently.
**之前:**混合使用VFL + 锚点 + 原生NSLayoutConstraint。个人资料页面需要500多行代码,经常出现约束冲突。
**之后:**仅使用锚点 + Stack View。200行代码,零冲突,更易于维护。

**之前:**30个视图控制器中分散着硬编码间距(8px、12px、16px)。设计更新需要修改30个文件。
**之后:**使用Theme.Spacing.medium。设计更新只需修改1个文件,影响整个应用。

**之前:**为卡片布局编写自定义复杂约束逻辑。100行代码,难以调试。
**之后:**可复用CardView组件。30行代码,在15个场景中一致使用。

Sources

参考资料