6.7 KiB
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.