AgentSkillsCN

UIKit Patterns

在不依赖 SwiftUI 桥接的前提下,运用纯 UIKit 模式进行状态管理、数据绑定与组件设计

SKILL.md
--- frontmatter
name: UIKit Patterns
description: Pure UIKit patterns for state management, data binding, and component design without SwiftUI bridging
version: 2.0.0

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.

swift
// 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.

swift
// 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.

swift
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.

swift
// 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.

swift
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

code
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

swift
// 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:

swift
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:

swift
// ❌ 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:

swift
// ✅ 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

swift
// ❌ 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

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

Observation Cleanup

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

Navigation Patterns

Coordinator Pattern

swift
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.