Swift Custom Views
Introduction
Custom views are an essential part of iOS development that allow you to create reusable UI components. By creating custom views, you can encapsulate complex UI elements and their behaviors into modular, reusable components. This not only makes your code more organized but also promotes the DRY (Don't Repeat Yourself) principle.
In this tutorial, we'll explore how to create custom views in Swift using both UIKit and SwiftUI approaches. Whether you're looking to build a custom button, a specialized input field, or a completely unique UI element, understanding custom views will significantly enhance your iOS development skills.
Understanding Custom Views
At its core, a custom view is simply a subclass of UIView
(in UIKit) or a custom View
(in SwiftUI) that you design to fulfill a specific purpose in your application's user interface.
Why Create Custom Views?
- Reusability: Build once, use anywhere in your application
- Encapsulation: Package related UI elements and functionality together
- Maintainability: Make updates in one place rather than throughout your codebase
- Consistency: Ensure UI elements look and behave the same throughout your app
Creating Custom Views in UIKit
Let's start by creating a simple custom view using UIKit. We'll build a RoundedButton
with customizable properties.
Method 1: Creating via Code
import UIKit
class RoundedButton: UIButton {
// MARK: - Properties
var cornerRadius: CGFloat = 8.0 {
didSet {
updateAppearance()
}
}
var buttonColor: UIColor = .systemBlue {
didSet {
updateAppearance()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupButton()
}
// MARK: - Setup
private func setupButton() {
setTitleColor(.white, for: .normal)
titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
updateAppearance()
}
private func updateAppearance() {
backgroundColor = buttonColor
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
}
}
Method 2: Creating via XIB/Nib File
For more complex views, using Interface Builder with XIB files can be beneficial:
- First, create a new file using the "View" template and name it
CardView.xib
- Create a corresponding Swift file called
CardView.swift
:
import UIKit
class CardView: UIView {
// MARK: - Outlets
@IBOutlet weak var contentView: UIView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var thumbnailImageView: UIImageView!
// MARK: - Properties
var title: String? {
didSet {
titleLabel.text = title
}
}
var descriptionText: String? {
didSet {
descriptionLabel.text = descriptionText
}
}
var thumbnail: UIImage? {
didSet {
thumbnailImageView.image = thumbnail
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupFromNib()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupFromNib()
}
// MARK: - Setup
private func setupFromNib() {
let bundle = Bundle(for: CardView.self)
bundle.loadNibNamed("CardView", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
// Styling
layer.cornerRadius = 12
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 4
}
}
Using Your Custom UIKit View
Here's how to use the custom views we've created:
// Using the RoundedButton
let roundedButton = RoundedButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
roundedButton.setTitle("Press Me", for: .normal)
roundedButton.cornerRadius = 25 // Makes it pill-shaped
roundedButton.buttonColor = .systemRed
roundedButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view.addSubview(roundedButton)
// Using the CardView
let cardView = CardView(frame: CGRect(x: 20, y: 200, width: view.frame.width - 40, height: 120))
cardView.title = "Custom Card View"
cardView.descriptionText = "This is a reusable card component created with a XIB file"
cardView.thumbnail = UIImage(named: "thumbnail")
view.addSubview(cardView)
Creating Custom Views in SwiftUI
SwiftUI makes creating custom views even more straightforward. Let's create similar components using SwiftUI:
Custom Button in SwiftUI
import SwiftUI
struct RoundedButtonView: View {
let title: String
let backgroundColor: Color
let cornerRadius: CGFloat
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(backgroundColor)
.cornerRadius(cornerRadius)
}
}
}
// Preview
struct RoundedButtonView_Previews: PreviewProvider {
static var previews: some View {
RoundedButtonView(
title: "Press Me",
backgroundColor: .blue,
cornerRadius: 25
) {
print("Button pressed!")
}
.padding()
.previewLayout(.sizeThatFits)
}
}
Custom Card in SwiftUI
import SwiftUI
struct CardView: View {
let title: String
let description: String
let imageName: String?
var body: some View {
HStack(spacing: 12) {
if let imageName = imageName {
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 70)
.clipped()
.cornerRadius(8)
}
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.foregroundColor(.primary)
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
Spacer()
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 2)
}
}
// Preview
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(
title: "Custom Card View",
description: "This is a reusable card component created with SwiftUI",
imageName: "thumbnail"
)
.padding()
.previewLayout(.sizeThatFits)
}
}
Using Your Custom SwiftUI Views
Using these custom views in SwiftUI is very straightforward:
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
CardView(
title: "Swift Programming",
description: "Learn to build iOS apps using Swift and SwiftUI",
imageName: "swift-icon"
)
RoundedButtonView(
title: "Sign Up Now",
backgroundColor: .blue,
cornerRadius: 25
) {
// Handle button tap
print("Button tapped!")
}
.padding(.horizontal)
}
.padding()
}
}
Advanced Custom Views
Custom View with User Interaction (UIKit)
Let's create a rating control that allows users to select a star rating:
import UIKit
protocol StarRatingViewDelegate: AnyObject {
func starRatingView(_ view: StarRatingView, didSelectRating rating: Int)
}
class StarRatingView: UIView {
// MARK: - Properties
private let starCount: Int
private var buttons: [UIButton] = []
private(set) var rating: Int = 0
weak var delegate: StarRatingViewDelegate?
// MARK: - Initialization
init(frame: CGRect, starCount: Int) {
self.starCount = starCount
super.init(frame: frame)
setupStars()
}
required init?(coder: NSCoder) {
self.starCount = 5 // Default value
super.init(coder: coder)
setupStars()
}
// MARK: - Setup
private func setupStars() {
// Remove any existing buttons
buttons.forEach { $0.removeFromSuperview() }
buttons.removeAll()
let starSize: CGFloat = 30
let spacing: CGFloat = 5
for i in 0..<starCount {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "star"), for: .normal)
button.setImage(UIImage(systemName: "star.fill"), for: .selected)
button.tintColor = .systemYellow
button.frame = CGRect(
x: CGFloat(i) * (starSize + spacing),
y: 0,
width: starSize,
height: starSize
)
button.tag = i + 1
button.addTarget(self, action: #selector(starTapped(_:)), for: .touchUpInside)
addSubview(button)
buttons.append(button)
}
}
// MARK: - Actions
@objc private func starTapped(_ sender: UIButton) {
let selectedRating = sender.tag
setRating(selectedRating)
delegate?.starRatingView(self, didSelectRating: selectedRating)
}
func setRating(_ rating: Int) {
self.rating = rating
for (index, button) in buttons.enumerated() {
// +1 because index is 0-based and ratings are 1-based
button.isSelected = index + 1 <= rating
}
}
}
Custom Control with SwiftUI
Here's the same star rating control implemented in SwiftUI:
import SwiftUI
struct StarRatingView: View {
let maxRating: Int
@Binding var rating: Int
let starSize: CGFloat
let starColor: Color
init(maxRating: Int = 5, rating: Binding<Int>, starSize: CGFloat = 30, starColor: Color = .yellow) {
self.maxRating = maxRating
self._rating = rating
self.starSize = starSize
self.starColor = starColor
}
var body: some View {
HStack(spacing: 5) {
ForEach(1...maxRating, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.resizable()
.frame(width: starSize, height: starSize)
.foregroundColor(starColor)
.onTapGesture {
rating = star
}
}
}
}
}
// Preview
struct StarRatingView_Previews: PreviewProvider {
static var previews: some View {
StarRatingView(rating: .constant(3))
.padding()
.previewLayout(.sizeThatFits)
}
}
Real-World Applications
Creating a Message Bubble Component
Let's create a chat message bubble that you might use in a messaging app:
import SwiftUI
struct MessageBubble: View {
let message: String
let isFromCurrentUser: Bool
let timestamp: Date
private var backgroundColor: Color {
isFromCurrentUser ? .blue : Color(.systemGray5)
}
private var textColor: Color {
isFromCurrentUser ? .white : .black
}
private var alignment: HorizontalAlignment {
isFromCurrentUser ? .trailing : .leading
}
private var timeString: String {
let formatter = DateFormatter()
formatter.timeStyle = .short
return formatter.string(from: timestamp)
}
var body: some View {
VStack(alignment: alignment) {
HStack {
if isFromCurrentUser {
Spacer()
}
VStack(alignment: .leading, spacing: 2) {
Text(message)
.padding(10)
.foregroundColor(textColor)
.background(backgroundColor)
.cornerRadius(18)
Text(timeString)
.font(.caption2)
.foregroundColor(.gray)
.padding(.horizontal, 5)
}
.padding(.horizontal, 4)
if !isFromCurrentUser {
Spacer()
}
}
}
}
}
// Preview
struct MessageBubble_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 20) {
MessageBubble(
message: "Hey there! How's it going?",
isFromCurrentUser: false,
timestamp: Date()
)
MessageBubble(
message: "I'm doing great! Learning about custom views in Swift.",
isFromCurrentUser: true,
timestamp: Date()
)
}
.padding()
}
}
Creating a Profile Header View
Here's a profile header view that could be used in a social media or profile screen:
import SwiftUI
struct ProfileHeaderView: View {
let username: String
let bio: String
let avatarImage: Image
let followerCount: Int
let followingCount: Int
let isFollowing: Bool
let onFollowTapped: () -> Void
var body: some View {
VStack(spacing: 16) {
// Avatar and counts
HStack(spacing: 24) {
avatarImage
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(Circle())
VStack(spacing: 4) {
HStack(spacing: 24) {
CountView(value: followerCount, label: "Followers")
CountView(value: followingCount, label: "Following")
}
Button(action: onFollowTapped) {
Text(isFollowing ? "Following" : "Follow")
.fontWeight(.medium)
.foregroundColor(isFollowing ? .primary : .white)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(isFollowing ? Color(.systemGray5) : Color.blue)
.cornerRadius(16)
}
}
}
// User info
VStack(alignment: .leading, spacing: 4) {
Text(username)
.font(.headline)
Text(bio)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(3)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
}
}
struct CountView: View {
let value: Int
let label: String
var body: some View {
VStack(spacing: 2) {
Text("\(value)")
.font(.headline)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
// Example usage
struct ProfileView: View {
@State private var isFollowing = false
var body: some View {
ScrollView {
VStack(spacing: 20) {
ProfileHeaderView(
username: "swift_developer",
bio: "iOS developer passionate about creating beautiful user interfaces with Swift and SwiftUI. Based in San Francisco.",
avatarImage: Image("profile_avatar"),
followerCount: 1024,
followingCount: 256,
isFollowing: isFollowing,
onFollowTapped: {
isFollowing.toggle()
}
)
// Other profile content would go here
}
.padding()
}
}
}
Best Practices for Custom Views
-
Single Responsibility Principle: Each custom view should have one responsibility and do it well.
-
Reusability: Design views to be reusable by accepting parameters for customization.
-
Documentation: Add comments to explain the purpose and usage of your custom views.
-
Accessibility: Ensure your custom views work well with accessibility features like VoiceOver.
-
Test Different Sizes: Make sure your views adapt appropriately to different screen sizes.
-
Separation of Concerns: Keep your UI logic separate from business logic.
-
Use Composition: Build complex views by composing simpler custom views.
Summary
Custom views are powerful tools in Swift development that allow you to create reusable, maintainable UI components. We've explored:
- Creating custom views in UIKit both programmatically and using XIB files
- Building custom views in SwiftUI
- Implementing interactive components like rating controls
- Real-world examples like message bubbles and profile headers
- Best practices for custom view development
By mastering custom views, you'll be able to create more modular and maintainable iOS applications with consistent user interfaces.
Additional Resources
Exercises
- Create a custom toggle switch view that animates between on/off states.
- Build a custom progress bar that shows completion percentage.
- Design a custom card view that includes an image, title, description, and action buttons.
- Implement a custom text field with validation and error display.
- Create a reusable tab bar with custom icons and selection indicators.
By working through these exercises, you'll strengthen your understanding of custom views and improve your UI development skills in Swift.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)