跳到正文
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(原文)

安装前可以查看交给 AI 代理的指令内容。

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.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

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 的全部技能

报告此技能的问题