How to Build Custom UI Components with Swift/iOS

Complete step-by-step tutorial for building reusable Swift/iOS UI components including UIView subclasses and SwiftUI wrappers.

Published: March 09, 2026

Category: Tech & Development

Introduction

Knowing how to build robust, reusable custom UI components is the difference between a codebase that scales gracefully and one that becomes a maintenance nightmare as your iOS app grows. This step-by-step tutorial walks you through the complete process—from creating your component file to integrating it across multiple screens—using production-ready patterns for Swift/iOS mobile app development.

What is a Custom UI Component in Swift/iOS?

A custom UI component in Swift/iOS is a self-contained, reusable view class that encapsulates a specific piece of user interface logic. It combines visual presentation, layout, and behavior into a single, importable unit. Unlike copying and pasting UI code between view controllers—a practice that leads to fragmented, hard-to-update interfaces—custom components follow the DRY (Don't Repeat Yourself) principle. Whether you're targeting iOS natively or thinking about architecture for cross-platform mobile app development with React Native or Flutter, the component-based mindset is universally beneficial and is a core pillar of how leading development teams like GSoft Technologies structure their projects.

Key Features / Why It Matters

Building custom UI components in Swift brings several essential benefits to iOS mobile app development:

  • Separation of Concerns: UI rendering logic stays inside the component, keeping view controllers focused on data flow and navigation rather than pixel management.
  • Testability: Isolated components are far easier to unit test and visually regression-test using Xcode's snapshot testing libraries.
  • Designer Collaboration: With @IBDesignable and @IBInspectable, non-engineer team members can tweak visual properties directly in Xcode's Interface Builder.
  • Consistent Branding: A single change to a shared button or card component instantly propagates across the whole app, ensuring visual consistency across all screens.
  • Platform Adaptability: Well-structured custom components are straightforward to adapt for iPad, Dynamic Type, Dark Mode, and accessibility features.

Step-by-Step: Building a Reusable Status Pill Component

We'll build a StatusPillView—a colored pill label used to display order status, task state, or user roles.

Step 1: Create a new Swift file — StatusPillView.swift

import UIKitenum PillStatus {    case active, pending, completed, error    var backgroundColor: UIColor {        switch self {        case .active:    return UIColor(hex: "#E8F5E9")        case .pending:   return UIColor(hex: "#FFF3E0")        case .completed: return UIColor(hex: "#E3F2FD")        case .error:     return UIColor(hex: "#FFEBEE")        }    }    var textColor: UIColor {        switch self {        case .active:    return UIColor(hex: "#2E7D32")        case .pending:   return UIColor(hex: "#E65100")        case .completed: return UIColor(hex: "#1565C0")        case .error:     return UIColor(hex: "#C62828")        }    }    var label: String {        switch self {        case .active:    return "Active"        case .pending:   return "Pending"        case .completed: return "Completed"        case .error:     return "Error"        }    }}

Step 2: Build the UIView subclass

@IBDesignableclass StatusPillView: UIView {    private let label: UILabel = {        let l = UILabel()        l.font = UIFont.systemFont(ofSize: 12, weight: .semibold)        l.translatesAutoresizingMaskIntoConstraints = false        return l    }()    var status: PillStatus = .pending {        didSet { updateAppearance() }    }    override init(frame: CGRect) {        super.init(frame: frame)        configure()    }    required init?(coder: NSCoder) {        super.init(coder: coder)        configure()    }    private func configure() {        layer.cornerRadius = 10        layer.masksToBounds = true        addSubview(label)        NSLayoutConstraint.activate([            label.topAnchor.constraint(equalTo: topAnchor, constant: 4),            label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),            label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10)        ])        updateAppearance()    }    private func updateAppearance() {        backgroundColor = status.backgroundColor        label.textColor = status.textColor        label.text = status.label    }}

Step 3: Use it in a UITableViewCell

class OrderCell: UITableViewCell {    let statusPill = StatusPillView()    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {        super.init(style: style, reuseIdentifier: reuseIdentifier)        statusPill.translatesAutoresizingMaskIntoConstraints = false        contentView.addSubview(statusPill)        NSLayoutConstraint.activate([            statusPill.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),            statusPill.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)        ])    }    required init?(coder: NSCoder) { fatalError() }    func configure(with order: Order) {        statusPill.status = order.isPending ? .pending : .completed    }}

Step 4: SwiftUI wrapper (for mixed codebases)

import SwiftUIstruct StatusPill: UIViewRepresentable {    var status: PillStatus    func makeUIView(context: Context) -> StatusPillView {        StatusPillView()    }    func updateUIView(_ uiView: StatusPillView, context: Context) {        uiView.status = status    }}// Usage in SwiftUIStatusPill(status: .active)    .fixedSize()

Best Practices

When building custom UI components for iOS mobile app development at scale, a consistent set of practices ensures longevity and team-wide adoption. Always provide a UIViewRepresentable wrapper for UIKit components you want to use in SwiftUI views—mixed codebases are the reality for most active iOS projects. Adopt semantic naming for your component APIs: statusPill.status = .active is far more readable than statusPill.backgroundColor = .green sprinkled throughout view controllers. Write snapshot tests for each component state using a library like Point-Free's swift-snapshot-testing—this catches unintended visual regressions as your design system evolves. Finally, maintain a component library module separate from your main app target so it can be shared across app extensions, widgets, and potential cross-platform mobile app development efforts.

Real-World Use Cases at GSoft Technologies

At GSoft Technologies, we've built and maintained shared component libraries for iOS mobile app development projects across industries including fintech, healthcare, and e-commerce. One of our most impactful projects involved creating a 30+ component design system for a logistics platform, covering everything from status indicators like the one above to interactive map callout views. This library became the single source of truth for UI across five different screens, reducing bug reports related to inconsistent visual states by over 60% in the first quarter after deployment. Whether building natively in Swift or architecting a cross-platform solution, our team brings the same component-first discipline to every iOS project.

Conclusion

Building custom UI components step by step—from the enum-backed state model through the UIView subclass to the SwiftUI bridge—gives you a battle-tested foundation for scalable iOS mobile app development. Components that are testable, designer-friendly, and adaptable to both UIKit and SwiftUI codebases dramatically reduce the cost of UI changes over the life of a project. Start with the smallest possible component that solves a real problem, build it well, and expand your library from there. Contact GSoft Technologies to build your next Swift/iOS project with a team that has deep experience in scalable mobile UI architecture.

Back to Blog | Home | Services | Contact Us