Files
skills/ios-application-dev/references/uikit-components.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

8.5 KiB

UIKit Components

Common UIKit components guide covering UIStackView, buttons, alerts, search, and context menus.

UIStackView

Stack views simplify auto layout for linear arrangements:

class FormViewController: UIViewController {
    private let mainStack = UIStackView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mainStack.axis = .vertical
        mainStack.spacing = 16
        mainStack.alignment = .fill
        mainStack.distribution = .fill
        
        view.addSubview(mainStack)
        mainStack.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).offset(20)
            make.leading.trailing.equalToSuperview().inset(16)
        }
        
        let headerStack = UIStackView()
        headerStack.axis = .horizontal
        headerStack.spacing = 12
        headerStack.alignment = .center
        
        let avatarView = UIImageView()
        avatarView.snp.makeConstraints { make in
            make.size.equalTo(48)
        }
        
        let labelStack = UIStackView()
        labelStack.axis = .vertical
        labelStack.spacing = 4
        labelStack.addArrangedSubview(titleLabel)
        labelStack.addArrangedSubview(subtitleLabel)
        
        headerStack.addArrangedSubview(avatarView)
        headerStack.addArrangedSubview(labelStack)
        
        mainStack.addArrangedSubview(headerStack)
        mainStack.addArrangedSubview(contentView)
        mainStack.addArrangedSubview(actionButton)
        
        mainStack.setCustomSpacing(24, after: headerStack)
    }
}

StackView Properties

Property Options Usage
axis .horizontal, .vertical Layout direction
distribution .fill, .fillEqually, .fillProportionally, .equalSpacing, .equalCentering Space distribution
alignment .fill, .leading, .center, .trailing Cross-axis alignment
spacing CGFloat Uniform spacing
setCustomSpacing(_:after:) - Variable spacing

UIButton.Configuration (iOS 15+)

let primaryButton = UIButton(type: .system)
primaryButton.configuration = .filled()
primaryButton.setTitle("Continue", for: .normal)

let secondaryButton = UIButton(type: .system)
secondaryButton.configuration = .tinted()
secondaryButton.setTitle("Save for Later", for: .normal)

let destructiveButton = UIButton(type: .system)
destructiveButton.configuration = .plain()
destructiveButton.setTitle("Remove", for: .normal)
destructiveButton.tintColor = .systemRed

Custom Button Configuration

var config = UIButton.Configuration.filled()
config.title = "Add to Cart"
config.image = UIImage(systemName: "cart.badge.plus")
config.imagePadding = 8
config.cornerStyle = .capsule
config.baseBackgroundColor = .systemBlue
config.baseForegroundColor = .white
let cartButton = UIButton(configuration: config)

Button State Handling

var config = UIButton.Configuration.filled()
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
    var outgoing = incoming
    outgoing.font = .boldSystemFont(ofSize: 16)
    return outgoing
}

config.configurationUpdateHandler = { button in
    var config = button.configuration
    config?.showsActivityIndicator = button.isSelected
    button.configuration = config
}

UIAlertController

Alert

func confirmDeletion() {
    let alert = UIAlertController(
        title: "Remove Item?",
        message: "This cannot be undone.",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Remove", style: .destructive) { _ in
        self.performDeletion()
    })
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
    present(alert, animated: true)
}

Action Sheet

func showOptions() {
    let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
    sheet.addAction(UIAlertAction(title: "Share", style: .default) { _ in })
    sheet.addAction(UIAlertAction(title: "Edit", style: .default) { _ in })
    sheet.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in })
    sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
    
    if let popover = sheet.popoverPresentationController {
        popover.sourceView = optionsButton
        popover.sourceRect = optionsButton.bounds
    }
    
    present(sheet, animated: true)
}

Alert with Text Field

func showInputAlert() {
    let alert = UIAlertController(
        title: "Rename",
        message: "Enter a new name",
        preferredStyle: .alert
    )
    
    alert.addTextField { textField in
        textField.placeholder = "Name"
        textField.autocapitalizationType = .words
    }
    
    alert.addAction(UIAlertAction(title: "Save", style: .default) { _ in
        if let name = alert.textFields?.first?.text {
            self.rename(to: name)
        }
    })
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
    
    present(alert, animated: true)
}

UISearchController

class SearchableListVC: UIViewController, UISearchResultsUpdating {
    private let searchController = UISearchController(searchResultsController: nil)
    private var allItems: [Item] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSearch()
    }
    
    private func setupSearch() {
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.placeholder = "Search"
        navigationItem.searchController = searchController
        definesPresentationContext = true
    }
    
    func updateSearchResults(for searchController: UISearchController) {
        let query = searchController.searchBar.text ?? ""
        let filtered = query.isEmpty ? allItems : allItems.filter {
            $0.title.localizedCaseInsensitiveContains(query)
        }
        updateItems(filtered)
    }
}

Search Bar Configuration

searchController.searchBar.scopeButtonTitles = ["All", "Recent", "Favorites"]
searchController.searchBar.showsScopeBar = true
searchController.searchBar.delegate = self

extension SearchableListVC: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
        filterContent(scope: selectedScope)
    }
}

UIContextMenuInteraction

extension PhotoCell: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(
        _ interaction: UIContextMenuInteraction,
        configurationForMenuAtLocation location: CGPoint
    ) -> UIContextMenuConfiguration? {
        UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
            let share = UIAction(
                title: "Share",
                image: UIImage(systemName: "square.and.arrow.up")
            ) { _ in }
            
            let favorite = UIAction(
                title: "Favorite",
                image: UIImage(systemName: "heart")
            ) { _ in }
            
            let delete = UIAction(
                title: "Delete",
                image: UIImage(systemName: "trash"),
                attributes: .destructive
            ) { _ in }
            
            return UIMenu(children: [share, favorite, delete])
        }
    }
}

Context Menu with Preview

func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
    UIContextMenuConfiguration(
        identifier: itemID as NSCopying,
        previewProvider: { [weak self] in
            return self?.makePreviewController()
        },
        actionProvider: { _ in
            return self.makeMenu()
        }
    )
}

func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
    animator: UIContextMenuInteractionCommitAnimating
) {
    animator.addCompletion {
        self.showDetail()
    }
}

CollectionView Context Menu

func collectionView(
    _ collectionView: UICollectionView,
    contextMenuConfigurationForItemAt indexPath: IndexPath,
    point: CGPoint
) -> UIContextMenuConfiguration? {
    let item = dataSource.itemIdentifier(for: indexPath)
    return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: nil) { _ in
        return self.makeMenu(for: item)
    }
}

UIKit and Apple are trademarks of Apple Inc.