Uikit expert
Agent skill for writing correct, modern UIKit code in Swift. Covers lifecycle, memory, concurrency, collection views, and iOS 13-26 APIs.
npx -y skills add ivan-magda/uikit-expert-skill --skill uikit-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write, review, or improve UIKit code following best practices for view controller lifecycle, Auto Layout, collection views, navigation, animation, memory management, and modern iOS 18–26 APIs. Use when building new UIKit features, refactoring existing views or view controllers, reviewing code quality, adopting modern UIKit patterns (diffable data sources, compositional layout, cell configuration), or bridging UIKit with SwiftUI. Does not cover SwiftUI-only code.
SKILL.md
13.4 KB, as published. Nobody here has run it
UIKit Expert Skill
Overview
Favor native APIs and Apple's documented guidance. Operating principles: facts over architecture (no MVVM/VIPER/Coordinator mandates, but do separate business logic for testability); correctness first, then performance; reserve "always"/"never" for correctness, use "suggest"/"consider" for optional optimizations.
Workflows
All three modes draw on the same Core Guidelines below; each topic links to its reference file there. Pick the entry point:
- Review — Walk the Review Checklist as pass/fail gates. For any failure, fix per the matching Core Guideline. Prioritize correctness gates (lifecycle, Auto Layout, memory, concurrency) over style; treat performance items as optional.
- Improve — For each issue, apply the modern replacement from the Deprecated → Modern table and the matching Core Guideline. Present performance work (downsampling, prefetching, constraint-churn fixes) as optional optimizations, never mandates.
- Implement new — Build in this order, applying the relevant Core Guideline at each step:
- Design data flow — owned state, injected dependencies, model layer
- Lifecycle — one-time setup in
viewDidLoad, geometry inviewIsAppearing - Auto Layout — batch activation, zero churn
- Collection views — DiffableDataSource + CompositionalLayout + CellRegistration
- Navigation — all 4 appearance slots, concurrent-transition guards
- Animation, memory (
[weak self], Task cancellation), concurrency (@MainActor) - Interop, image loading, keyboard, Dynamic Type / VoiceOver / dark mode
- Gate iOS 26+ features with
#availableand sensible fallbacks
Core Guidelines
View Controller Lifecycle — see references/view-controller-lifecycle.md
- Use
viewDidLoadfor one-time setup: subviews, constraints, delegates — NOT geometry - Use
viewIsAppearing(back-deployed iOS 13+) for geometry-dependent work, trait-based layout, scroll-to-item viewDidLayoutSubviewsfires multiple times — use only for lightweight layer frame adjustmentsviewWillAppearis limited to transition coordinator animations and balanced notification registration- Always call
superin every lifecycle override - Child VC containment:
addChild→addSubview→didMove(toParent:)— in that exact order - Verify deallocation with
deinitlogging during development
Auto Layout — see references/auto-layout.md
- Always set
translatesAutoresizingMaskIntoConstraints = falseon programmatic views - Use
NSLayoutConstraint.activate([])— never individual.isActive = true - Create constraints once, toggle
isActiveor modify.constant— never remove and recreate - Never change priority from/to
.required(1000) at runtime — use 999 - Animate constraints: update constant → call
layoutIfNeeded()inside animation block on superview - iOS 26+: use
.flushUpdatesoption to simplify constraint animation - Avoid deeply nested UIStackViews in reusable cells
- Set
constraint.identifieron key constraints soUnable to simultaneously satisfy constraintslogs are readable
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false // required on programmatic views
view.addSubview(label)
NSLayoutConstraint.activate([ // one batch = one solve pass
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
Collection Views & Data Sources — see references/modern-collection-views.md, references/cell-configuration.md, references/list-performance.md
- Use
UICollectionViewDiffableDataSourcewith stable identifiers (UUID/database ID, not full model structs) - Use
reconfigureItemsfor content updates,reloadItemsonly when cell type changes - Use
applySnapshotUsingReloadDatafor initial population (bypasses diffing) - Use
UICollectionViewCompositionalLayoutfor any non-trivial layout - Use
UICollectionView.CellRegistration— no string identifiers, no manual casting - Use
UIContentConfigurationfor cell content andUIBackgroundConfigurationfor cell backgrounds - Use
configurationUpdateHandlerfor state-driven styling (selection, highlight)
// Store the registration once — never create it inside the cell provider (crashes on iOS 15+).
private lazy var cellRegistration = UICollectionView.CellRegistration<PhotoCell, Photo> { cell, _, photo in
cell.configure(with: photo)
}
// Data source is keyed by the stable ID, so the provider receives an id — look up the model, then configure with it.
dataSource = .init(collectionView: collectionView) { [weak self] cv, indexPath, id in
guard let self, let photo = self.photo(for: id) else { return nil }
return cv.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: photo)
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Photo.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(photos.map(\.id))
dataSource.apply(snapshot, animatingDifferences: true)
Navigation — see references/navigation-patterns.md
- Configure all 4
UINavigationBarAppearanceslots (standard, scrollEdge, compact, compactScrollEdge) - Set appearance on
navigationItem(per-VC) inviewDidLoad, not onnavigationBarinviewWillAppear - Use
setViewControllers(_:animated:)for deep links — not sequential push calls - Guard against concurrent transitions — check
transitionCoordinatorbefore push/pop - Set
prefersLargeTitlesonce on the bar; uselargeTitleDisplayModeper VC
Animation — see references/animation-patterns.md
UIView.animate— simple one-shot animations; checkfinishedin completionUIViewPropertyAnimator— gesture-driven, interruptible; respect state machine (inactive → active → stopped)CABasicAnimation— layer-only properties (cornerRadius, shadow, 3D transforms); set model value first- iOS 17+ spring API:
UIView.animate(springDuration:bounce:)aligns with SwiftUI - Constraint animation: flush layout → update constant → animate
layoutIfNeeded()on superview
Memory Management — see references/memory-management.md
- Default to
[weak self]in all escaping closures - Timer: use block-based API with
[weak self], invalidate inviewWillDisappear - CADisplayLink: use weak proxy pattern (no block-based API available)
- NotificationCenter:
[weak self]in closure, remove observer indeinit - Nested closures: re-capture
[weak self]in stored inner closures - Delegates: always
weak var delegate: SomeDelegate?withAnyObjectconstraint - Verify deallocation with
deinit— if never called, a retain cycle exists
Concurrency — see references/concurrency-main-thread.md
UIViewControlleris@MainActor— all subclass methods are implicitly main-actor- Store
Taskreferences, cancel inviewDidDisappear— notdeinit - Check
Task.isCancelledbefore UI updates afterawait Task.detacheddoes NOT inherit actor isolation — explicitMainActor.runneeded for UI- Never call
DispatchQueue.main.syncfrom background — useawait MainActor.run
UIKit–SwiftUI Interop — see references/uikit-swiftui-interop.md
- UIHostingController: full child VC containment (
addChild→addSubview→didMove), retain as stored property sizingOptions = .intrinsicContentSize(iOS 16+) for Auto Layout containers- UIViewRepresentable: set mutable state in
updateUIView, notmakeUIView; guard against update loops - UIHostingConfiguration (iOS 16+) for SwiftUI content in collection view cells
Image Loading — see references/image-loading.md
- Decoded bitmap size = width × height × 4 bytes (a 12MP photo = ~48MB RAM)
- Downsample with ImageIO at display size — never load full bitmap and resize
- iOS 15+: use
byPreparingThumbnail(of:)orprepareForDisplay()for async decoding - Cell reuse: cancel Task in
prepareForReuse, clear image, verify identity on completion
Keyboard & Scroll — see references/keyboard-scroll.md
- Use
UIKeyboardLayoutGuide(iOS 15+) — pin content bottom toview.keyboardLayoutGuide.topAnchor - iPad: set
followsUndockedKeyboard = truefor floating keyboards - Replace all manual keyboard notification handling with the layout guide
Adaptive Layout & Accessibility — see references/adaptive-appearance.md
- Use
registerForTraitChanges(iOS 17+) instead of deprecatedtraitCollectionDidChange - Dynamic Type:
UIFont.preferredFont(forTextStyle:)+adjustsFontForContentSizeCategory = true - Dark mode: use semantic colors (
.label,.systemBackground); re-resolve CGColor on trait changes - VoiceOver: set
accessibilityLabel,accessibilityTraits,accessibilityHinton custom views - Use
UIAccessibilityCustomActionfor complex list item actions
Quick Reference — Deprecated → Modern APIs
| Deprecated / Legacy | Modern Replacement | Since |
|---|---|---|
traitCollectionDidChange | registerForTraitChanges(_:handler:) | iOS 17 |
| Keyboard notifications | UIKeyboardLayoutGuide | iOS 15 |
cell.textLabel / detailTextLabel | UIListContentConfiguration | iOS 14 |
register + string dequeue | UICollectionView.CellRegistration | iOS 14 |
reloadItems on snapshot | reconfigureItems | iOS 15 |
barTintColor / isTranslucent | UINavigationBarAppearance (4 slots) | iOS 13 |
UICollectionViewFlowLayout (complex) | UICollectionViewCompositionalLayout | iOS 13 |
Manual layoutIfNeeded() in animations | .flushUpdates option | iOS 26 |
| Legacy app lifecycle | UIScene + SceneDelegate | Mandatory iOS 26 |
ObservableObject + manual invalidation | @Observable + UIObservationTrackingEnabled | iOS 18 |
Review Checklist
One gate per domain — the correctness items to verify (details in the matching Core Guideline above):
- Lifecycle — no geometry in
viewDidLoad(useviewIsAppearing); every override callssuper; child VC =addChild → addSubview → didMove;deinitconfirms dealloc - Auto Layout —
translatesAutoresizingMaskIntoConstraints = false;NSLayoutConstraint.activate([]); no churn (toggleisActive/.constant); nosetNeedsLayout()insidelayoutSubviews(infinite loop) - Collection views — diffable + stable identifiers;
reconfigureItemsnotreloadItems; no duplicate snapshot IDs (BUG_IN_CLIENTcrash);CellRegistrationnot string dequeue - Navigation — all 4 appearance slots; on
navigationItemnotnavigationBar; concurrent-transition guard - Animation — API fits use case; constraint animation flush → update → animate;
CAAnimationsets model value first - Memory —
[weak self]in escaping closures;Tasks cancelled inviewDidDisappear; delegatesweak; no strong-self recapture in nested closures - Concurrency —
Task.isCancelledchecked afterawait; noDispatchQueue.main.syncfrom background; no redundant@MainActoron VC subclasses - Images — downsampled to display size; cell load cancels in
prepareForReuse+ verifies identity;NSCachesized by decoded bytes - Interop —
UIHostingControllerretained + full containment;updateUIViewguards update loops - Keyboard —
UIKeyboardLayoutGuide, not notifications - Adaptive/A11y —
registerForTraitChanges; Dynamic Type; CGColor re-resolved on trait change;accessibilityLabel/Traitson custom views - iOS 26+ —
#availableguards with fallbacks;UIScenelifecycle; considerUIObservationTrackingEnabled
References
references/view-controller-lifecycle.md— Lifecycle ordering, viewIsAppearing, child VC containmentreferences/auto-layout.md— Batch activation, constraint churn, priority, animation, debuggingreferences/modern-collection-views.md— Diffable data sources, compositional layout, CellRegistrationreferences/cell-configuration.md— UIContentConfiguration, UIBackgroundConfiguration, configurationUpdateHandlerreferences/list-performance.md— Prefetching, cell reuse, reconfigureItems, scroll performancereferences/navigation-patterns.md— Bar appearance, concurrent transitions, large titles, deep linksreferences/animation-patterns.md— UIView.animate, UIViewPropertyAnimator, CAAnimation, springsreferences/memory-management.md— Retain cycles, [weak self], Timer/CADisplayLink/nested closure trapsreferences/concurrency-main-thread.md— @MainActor, Task lifecycle, Swift 6, GCD migrationreferences/uikit-swiftui-interop.md— UIHostingController, UIViewRepresentable, sizing, state bridgingreferences/image-loading.md— Downsampling, decoded bitmap math, cell reuse race conditionreferences/keyboard-scroll.md— UIKeyboardLayoutGuide, scroll view insets, iPad floating keyboardreferences/adaptive-appearance.md— Trait changes, Dynamic Type, dark mode, VoiceOver, accessibilityreferences/modern-uikit-apis.md— Observation framework, updateProperties(), .flushUpdates, UIScene, Liquid Glass