本文へ移動
cccskills
無料GitHub で公開

UIKit Patterns

Pure UIKit patterns for state management, data binding, and component design without SwiftUI bridging

インストール方法を見る

含まれるファイル(2)

  • SKILL.md15.4 KB
  • references/uikit-to-swiftui-mapping.md8.1 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

UIKit Patterns (Pure UIKit)

This skill covers pure UIKit patterns for building iOS applications. No SwiftUI bridging - all patterns use native UIKit approaches.

State Management (Without SwiftUI)

Delegate Pattern

The classic UIKit approach for communication between components.

// MARK: - Protocol Definition

protocol UserProfileDelegate: AnyObject {
    func userProfileDidUpdate(_ profile: UserProfile)
    func userProfileDidRequestLogout()
    func userProfileDidFail(with error: Error)
}

// MARK: - Delegate Implementation

final class UserProfileViewController: UIViewController {

    weak var delegate: UserProfileDelegate?

    private func handleProfileUpdate() {
        delegate?.userProfileDidUpdate(currentProfile)
    }

    @objc private func logoutTapped() {
        delegate?.userProfileDidRequestLogout()
    }
}

// MARK: - Parent ViewController

final class SettingsViewController: UIViewController, UserProfileDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        let profileVC = UserProfileViewController()
        profileVC.delegate = self
    }

    func userProfileDidUpdate(_ profile: UserProfile) {
        // Handle profile update
    }

    func userProfileDidRequestLogout() {
        // Handle logout
    }

    func userProfileDidFail(with error: Error) {
        showAlert(for: error)
    }
}

Closure-Based Binding

Modern UIKit approach using closures for reactive-style updates.

// MARK: - ViewModel with Closure Bindings

final class UserListViewModel {

    // MARK: - State

    private(set) var users: [User] = [] {
        didSet { onUsersChanged?(users) }
    }

    private(set) var isLoading: Bool = false {
        didSet { onLoadingChanged?(isLoading) }
    }

    private(set) var error: Error? {
        didSet {
            if let error = error {
                onError?(error)
            }
        }
    }

    // MARK: - Bindings

    var onUsersChanged: (([User]) -> Void)?
    var onLoadingChanged: ((Bool) -> Void)?
    var onError: ((Error) -> Void)?

    // MARK: - Actions

    func loadUsers() {
        isLoading = true
        Task { @MainActor in
            defer { isLoading = false }
            do {
                users = try await userService.fetchAll()
            } catch {
                self.error = error
            }
        }
    }
}

// MARK: - ViewController Binding

final class UserListViewController: UIViewController {

    private var viewModel = UserListViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.loadUsers()
    }

    private func bindViewModel() {
        viewModel.onUsersChanged = { [weak self] users in
            self?.tableView.reloadData()
        }

        viewModel.onLoadingChanged = { [weak self] isLoading in
            self?.loadingIndicator.isHidden = !isLoading
            if isLoading {
                self?.loadingIndicator.startAnimating()
            } else {
                self?.loadingIndicator.stopAnimating()
            }
        }

        viewModel.onError = { [weak self] error in
            self?.showError(error)
        }
    }
}

Property Observers (didSet)

Use didSet for simple property-driven updates.

final class ProfileHeaderView: UIView {

    var user: User? {
        didSet {
            updateUI()
        }
    }

    var isEditing: Bool = false {
        didSet {
            editButton.isHidden = isEditing
            saveButton.isHidden = !isEditing
            nameTextField.isEnabled = isEditing
        }
    }

    private func updateUI() {
        guard let user = user else { return }
        nameLabel.text = user.name
        emailLabel.text = user.email
        avatarImageView.load(from: user.avatarURL)
    }
}

NotificationCenter for Broadcasts

Use for app-wide events that multiple components need to observe.

// MARK: - Notification Names

extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
    static let userDidLogout = Notification.Name("userDidLogout")
    static let cartDidUpdate = Notification.Name("cartDidUpdate")
}

// MARK: - Posting Notifications

final class AuthService {

    func login(user: User) {
        // ... login logic
        NotificationCenter.default.post(
            name: .userDidLogin,
            object: nil,
            userInfo: ["user": user]
        )
    }

    func logout() {
        NotificationCenter.default.post(name: .userDidLogout, object: nil)
    }
}

// MARK: - Observing Notifications

final class DashboardViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setupNotificationObservers()
    }

    private func setupNotificationObservers() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleUserLogin),
            name: .userDidLogin,
            object: nil
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleUserLogout),
            name: .userDidLogout,
            object: nil
        )
    }

    @objc private func handleUserLogin(_ notification: Notification) {
        guard let user = notification.userInfo?["user"] as? User else { return }
        updateUIForUser(user)
    }

    @objc private func handleUserLogout(_ notification: Notification) {
        resetUI()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

KVO (Key-Value Observing)

For observing properties on NSObject subclasses.

final class DownloadManager: NSObject {

    @objc dynamic var progress: Double = 0.0
    @objc dynamic var isDownloading: Bool = false
}

final class DownloadViewController: UIViewController {

    private let downloadManager = DownloadManager()
    private var progressObservation: NSKeyValueObservation?
    private var downloadingObservation: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupObservations()
    }

    private func setupObservations() {
        progressObservation = downloadManager.observe(\.progress, options: [.new]) { [weak self] _, change in
            guard let progress = change.newValue else { return }
            DispatchQueue.main.async {
                self?.progressView.progress = Float(progress)
            }
        }

        downloadingObservation = downloadManager.observe(\.isDownloading, options: [.new]) { [weak self] _, change in
            guard let isDownloading = change.newValue else { return }
            DispatchQueue.main.async {
                self?.downloadButton.isEnabled = !isDownloading
            }
        }
    }
}

SwiftUI → UIKit Mappings

When converting SwiftUI code to UIKit, use these mappings:

State Management

SwiftUIUIKit Equivalent
@StateInstance property + didSet
@BindingDelegate or closure callback
@ObservedObjectProtocol delegate + closure bindings
@ObservableClass with closure bindings
@EnvironmentDependency injection via init
@EnvironmentObjectShared singleton or passed reference

View Components

SwiftUIUIKit
NavigationStackUINavigationController
NavigationLinkpushViewController(_:animated:)
ListUITableView
LazyVGridUICollectionView with compositional layout
ScrollViewUIScrollView
VStackUIStackView(axis: .vertical)
HStackUIStackView(axis: .horizontal)
ZStackMultiple subviews with constraints
ButtonUIButton
TextUILabel
TextFieldUITextField
TextEditorUITextView
ImageUIImageView
ToggleUISwitch
SliderUISlider
PickerUIPickerView or UISegmentedControl
DatePickerUIDatePicker
ProgressViewUIProgressView or UIActivityIndicatorView

Modifiers → Methods/Properties

SwiftUI ModifierUIKit
.font()label.font =
.foregroundStyle()label.textColor =
.background()view.backgroundColor =
.frame()NSLayoutConstraint
.padding()layoutMargins or constraint constants
.cornerRadius()layer.cornerRadius
.shadow()layer.shadow* properties
.opacity()view.alpha =
.hidden()view.isHidden =
.disabled()control.isEnabled =
.onTapGesture()UITapGestureRecognizer
.onAppear()viewWillAppear(_:)
.onDisappear()viewWillDisappear(_:)
.sheet()present(_:animated:)
.alert()UIAlertController

UIKit View Lifecycle

init(coder:) / init(nibName:bundle:)
    ↓
loadView()
    ↓
viewDidLoad()                  ← Setup UI, bind ViewModel
    ↓
viewWillAppear(_:)             ← Refresh data, start animations
    ↓
viewWillLayoutSubviews()
    ↓
viewDidLayoutSubviews()        ← Adjust for final frame sizes
    ↓
viewDidAppear(_:)              ← Analytics, start tracking
    ↓
[User interaction]
    ↓
viewWillDisappear(_:)          ← Pause tasks
    ↓
viewDidDisappear(_:)           ← Stop expensive operations
    ↓
deinit                         ← Clean up observers

Data Binding Without SwiftUI

Protocol-Based ViewModel

// MARK: - ViewModel Protocol

protocol ProductDetailViewModelProtocol {
    var product: Product { get }
    var isInCart: Bool { get }
    var formattedPrice: String { get }

    var onProductUpdated: (() -> Void)? { get set }
    var onCartStatusChanged: ((Bool) -> Void)? { get set }
    var onError: ((Error) -> Void)? { get set }

    func addToCart()
    func removeFromCart()
    func refresh()
}

// MARK: - Implementation

final class ProductDetailViewModel: ProductDetailViewModelProtocol {

    private(set) var product: Product {
        didSet { onProductUpdated?() }
    }

    private(set) var isInCart: Bool = false {
        didSet { onCartStatusChanged?(isInCart) }
    }

    var formattedPrice: String {
        currencyFormatter.string(from: NSNumber(value: product.price)) ?? ""
    }

    var onProductUpdated: (() -> Void)?
    var onCartStatusChanged: ((Bool) -> Void)?
    var onError: ((Error) -> Void)?

    private let cartService: CartServiceProtocol
    private let currencyFormatter: NumberFormatter

    init(product: Product, cartService: CartServiceProtocol) {
        self.product = product
        self.cartService = cartService
        self.currencyFormatter = NumberFormatter()
        currencyFormatter.numberStyle = .currency

        checkCartStatus()
    }

    func addToCart() {
        Task { @MainActor in
            do {
                try await cartService.add(product)
                isInCart = true
            } catch {
                onError?(error)
            }
        }
    }

    func removeFromCart() {
        Task { @MainActor in
            do {
                try await cartService.remove(product)
                isInCart = false
            } catch {
                onError?(error)
            }
        }
    }

    func refresh() {
        checkCartStatus()
    }

    private func checkCartStatus() {
        Task { @MainActor in
            isInCart = await cartService.contains(product)
        }
    }
}

Combine with UIKit (Optional)

If using Combine without SwiftUI:

import Combine

final class SearchViewModel {

    @Published private(set) var results: [SearchResult] = []
    @Published private(set) var isSearching = false

    private var cancellables = Set<AnyCancellable>()

    func search(query: String) {
        isSearching = true

        searchService.search(query: query)
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isSearching = false
                    if case .failure(let error) = completion {
                        // Handle error
                    }
                },
                receiveValue: { [weak self] results in
                    self?.results = results
                }
            )
            .store(in: &cancellables)
    }
}

final class SearchViewController: UIViewController {

    private var viewModel = SearchViewModel()
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
    }

    private func bindViewModel() {
        viewModel.$results
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.tableView.reloadData()
            }
            .store(in: &cancellables)

        viewModel.$isSearching
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isSearching in
                self?.activityIndicator.isHidden = !isSearching
            }
            .store(in: &cancellables)
    }
}

NO SwiftUI Bridging

IMPORTANT: Do NOT use these patterns in pure UIKit projects:

// ❌ DO NOT USE - UIHostingController
let hostingController = UIHostingController(rootView: SomeSwiftUIView())

// ❌ DO NOT USE - UIViewRepresentable
struct MyUIKitView: UIViewRepresentable { ... }

// ❌ DO NOT USE - UIViewControllerRepresentable
struct MyUIKitViewController: UIViewControllerRepresentable { ... }

Instead, use pure UIKit alternatives:

// ✅ USE - Direct UIKit
let viewController = MyUIKitViewController()
navigationController?.pushViewController(viewController, animated: true)

// ✅ USE - Container view controller
addChild(childVC)
view.addSubview(childVC.view)
childVC.view.frame = containerView.bounds
childVC.didMove(toParent: self)

Memory Management

Weak References in Closures

// ❌ WRONG - Strong reference cycle
button.addAction(UIAction { _ in
    self.handleTap()  // Strong capture
}, for: .touchUpInside)

// ✅ CORRECT - Weak capture
button.addAction(UIAction { [weak self] _ in
    self?.handleTap()
}, for: .touchUpInside)

Weak Delegates

// Always use weak for delegate references
weak var delegate: SomeDelegate?

Observation Cleanup

deinit {
    NotificationCenter.default.removeObserver(self)
    // NSKeyValueObservation automatically invalidates in deinit
}

Navigation Patterns

Coordinator Pattern

protocol Coordinator: AnyObject {
    var navigationController: UINavigationController { get }
    var childCoordinators: [Coordinator] { get set }
    func start()
}

final class AppCoordinator: Coordinator {
    var navigationController: UINavigationController
    var childCoordinators: [Coordinator] = []

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let loginCoordinator = LoginCoordinator(navigationController: navigationController)
        loginCoordinator.delegate = self
        childCoordinators.append(loginCoordinator)
        loginCoordinator.start()
    }
}

extension AppCoordinator: LoginCoordinatorDelegate {
    func loginCoordinatorDidFinish(_ coordinator: LoginCoordinator) {
        childCoordinators.removeAll { $0 === coordinator }
        showMainFlow()
    }
}

Router Pattern (VIP)

See the VIP+W Architecture skill for Router implementation.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

When the user wants to A/B test App Store product page elements to improve conversion rate. Also use when the user mentions "A/B test", "product page optimization", "test my screenshots", "test my icon", "conversion rate optimization", "CPP", or "custom product pages". For screenshot design, see screenshot-optimization. For metadata optimization, see metadata-optimization.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Discover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

Measure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

alarmkit

無料

Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, and Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, AlarmButton stop and snooze actions, authorization, state observation, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

When the user wants to optimize their Google Play Store listing — title, short description, full description, keywords, ratings, or Play Store-specific features. Use when the user mentions "Google Play", "Android", "Play Store", "Play Console", "short description", "full description indexed", "Google Play ASO", or wants Google Play-specific keyword, creative, or ratings strategy. For iOS App Store optimization, see aso-audit and metadata-optimization.

日本語の概要は準備中です。原文の説明を表示しています。

JordanCoin/ios-skills-collection62026年9月10日 更新

JordanCoin のスキルをすべて見る

このスキルの問題を報告する