--- 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 | SwiftUI | UIKit Equivalent | |---------|------------------| | `@State` | Instance property + `didSet` | | `@Binding` | Delegate or closure callback | | `@ObservedObject` | Protocol delegate + closure bindings | | `@Observable` | Class with closure bindings | | `@Environment` | Dependency injection via init | | `@EnvironmentObject` | Shared singleton or passed reference | ### View Components | SwiftUI | UIKit | |---------|-------| | `NavigationStack` | `UINavigationController` | | `NavigationLink` | `pushViewController(_:animated:)` | | `List` | `UITableView` | | `LazyVGrid` | `UICollectionView` with compositional layout | | `ScrollView` | `UIScrollView` | | `VStack` | `UIStackView(axis: .vertical)` | | `HStack` | `UIStackView(axis: .horizontal)` | | `ZStack` | Multiple subviews with constraints | | `Button` | `UIButton` | | `Text` | `UILabel` | | `TextField` | `UITextField` | | `TextEditor` | `UITextView` | | `Image` | `UIImageView` | | `Toggle` | `UISwitch` | | `Slider` | `UISlider` | | `Picker` | `UIPickerView` or `UISegmentedControl` | | `DatePicker` | `UIDatePicker` | | `ProgressView` | `UIProgressView` or `UIActivityIndicatorView` | ### Modifiers → Methods/Properties | SwiftUI Modifier | UIKit | |------------------|-------| | `.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 ```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() 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() 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.