Files
skills/ios-application-dev/references/layout-system.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

6.7 KiB

Layout System

iOS layout system guide covering touch targets, safe areas, UICollectionView, and Compositional Layout.

Touch Targets

Interactive elements need adequate tap areas. The recommended minimum is 44x44 points.

let actionButton = UIButton(type: .system)
actionButton.setTitle("Submit", for: .normal)
view.addSubview(actionButton)

actionButton.snp.makeConstraints { make in
    make.height.greaterThanOrEqualTo(44)
    make.leading.trailing.equalToSuperview().inset(16)
    make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
}

Use 8-point increments for spacing (8, 16, 24, 32, 40, 48) to maintain visual consistency.

Safe Area

Always constrain content to the safe area to avoid the notch, Dynamic Island, and home indicator.

class MainViewController: UIViewController {
    private let contentStack = UIStackView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        contentStack.axis = .vertical
        contentStack.spacing = 16
        view.addSubview(contentStack)
        
        contentStack.snp.makeConstraints { make in
            make.top.bottom.equalTo(view.safeAreaLayoutGuide)
            make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
        }
    }
}

UICollectionView with Diffable Data Source

class ItemsViewController: UIViewController {
    enum Section { case main }
    
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView()
        configureDataSource()
    }
    
    private func setupCollectionView() {
        var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        config.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
            self?.makeSwipeActions(for: indexPath)
        }
        
        let layout = UICollectionViewCompositionalLayout.list(using: config)
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        
        view.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
    
    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { 
            cell, indexPath, item in
            var content = cell.defaultContentConfiguration()
            content.text = item.title
            content.secondaryText = item.subtitle
            cell.contentConfiguration = content
        }
        
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 
            collectionView, indexPath, item in
            collectionView.dequeueConfiguredReusableCell(
                using: cellRegistration, for: indexPath, item: item
            )
        }
    }
    
    func updateItems(_ items: [Item]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot)
    }
}

Grid Layout

private func createGridLayout() -> UICollectionViewLayout {
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1/3),
        heightDimension: .fractionalHeight(1.0)
    )
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
    
    let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalWidth(1/3)
    )
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
    
    let section = NSCollectionLayoutSection(group: group)
    return UICollectionViewCompositionalLayout(section: section)
}

Sectioned List with Headers

class CategorizedListVC: UIViewController {
    enum Section: Hashable {
        case favorites, recent, all
    }
    
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    
    private func setupCollectionView() {
        var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        config.headerMode = .supplementary
        
        let layout = UICollectionViewCompositionalLayout.list(using: config)
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
    }
    
    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { 
            cell, indexPath, item in
            var content = cell.defaultContentConfiguration()
            content.text = item.title
            cell.contentConfiguration = content
        }
        
        let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(
            elementKind: UICollectionView.elementKindSectionHeader
        ) { [weak self] header, elementKind, indexPath in
            guard let section = self?.dataSource.sectionIdentifier(for: indexPath.section) else { return }
            var content = header.defaultContentConfiguration()
            content.text = self?.title(for: section)
            header.contentConfiguration = content
        }
        
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 
            collectionView, indexPath, item in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }
        
        dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
            collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
        }
    }
    
    func applySnapshot(favorites: [Item], recent: [Item], all: [Item]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        if !favorites.isEmpty {
            snapshot.appendSections([.favorites])
            snapshot.appendItems(favorites, toSection: .favorites)
        }
        if !recent.isEmpty {
            snapshot.appendSections([.recent])
            snapshot.appendItems(recent, toSection: .recent)
        }
        snapshot.appendSections([.all])
        snapshot.appendItems(all, toSection: .all)
        dataSource.apply(snapshot)
    }
}

Spacing Guidelines

Spacing Usage
8pt Compact element spacing
16pt Standard padding
24pt Section spacing
32pt Large section separation
48pt Screen margins (large screens)

UIKit and Apple are trademarks of Apple Inc. SnapKit is a trademark of its respective owners.