Creating Views and Reusable SwiftUI Components
Course: iOS App Development Training
Topic: Creating Views and Reusable SwiftUI Components
Course Link: iOS App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to SwiftUI Views
SwiftUI is Apple's declarative user interface framework for building applications across platforms such as iOS, iPadOS, macOS, watchOS, and tvOS. It allows developers to describe what the interface should display for a given state, while SwiftUI manages much of the work required to update and render the interface.
A view is a fundamental building block of a SwiftUI interface. Text labels, buttons, images, lists, forms, cards, and complete screens can all be represented as views. Developers combine smaller views to build larger, organized interfaces.
Reusable SwiftUI components help developers avoid repeating the same layout code in multiple places. Instead of creating an identical course card, profile section, or action button several times, a developer can create one reusable view and configure it with different data.
Why Views and Reusable Components Matter
- Organize an application into smaller, understandable sections.
- Reduce duplicated code and maintenance effort.
- Maintain a consistent visual design throughout the application.
- Make interfaces easier to test and update.
- Support responsive layouts across different screen sizes.
- Encourage separation of presentation, data, and business logic.
2. Understanding the View Protocol
In SwiftUI, a custom view is typically a structure that conforms to the View protocol. The protocol requires a body property that describes the view's content.
import SwiftUI
struct WelcomeView: View {
var body: some View {
Text("Welcome to SwiftUI")
.font(.title)
.padding()
}
}
Explanation:
import SwiftUI makes SwiftUI types and APIs available.
struct WelcomeView: View defines a custom view.
body describes the interface returned by the view.
Text displays a string on the screen.
.font(.title) applies a title text style.
.padding() adds space around the text.
The expression some View is an opaque return type. It allows the implementation to return a specific concrete view type without exposing that full type in the declaration.
3. Understanding Built-in SwiftUI Views
SwiftUI provides built-in views for displaying information, accepting user input, arranging content, and presenting navigation.
| View |
Purpose |
Example Use |
Text |
Displays text. |
Headings, labels, descriptions. |
Image |
Displays images or system symbols. |
Profile pictures, icons, banners. |
Button |
Performs an action when activated. |
Save, submit, or delete actions. |
TextField |
Accepts editable text. |
Name, email, or search input. |
VStack |
Arranges views vertically. |
Forms and vertically organized content. |
HStack |
Arranges views horizontally. |
Profile rows and action groups. |
ZStack |
Layers views on top of one another. |
Text over images or layered backgrounds. |
ScrollView |
Allows content to scroll. |
Long pages and dashboards. |
List |
Displays rows of data in a list. |
Settings, messages, and course lists. |
Spacer |
Expands to create flexible empty space. |
Separating content or aligning controls. |
4. Creating a Simple Custom View
A custom view groups related interface elements into a meaningful unit. It can be used inside another view just like a built-in SwiftUI component.
import SwiftUI
struct CourseTitleView: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("iOS App Development")
.font(.title2)
.fontWeight(.bold)
Text("Learn to build native applications with Swift.")
.font(.body)
.foregroundStyle(.secondary)
}
.padding()
}
}
struct ContentView: View {
var body: some View {
CourseTitleView()
}
}
In this example, CourseTitleView contains a heading and a description. The parent view, ContentView, uses it by writing CourseTitleView(). The parent does not need to repeat the internal layout code.
5. Combining Views with Layout Containers
SwiftUI layout containers determine how child views are positioned relative to each other. Choosing the correct container improves readability and helps interfaces adapt to different available sizes.
5.1 VStack
VStack arranges views from top to bottom. It is useful for forms, profile sections, settings screens, and vertically arranged cards.
VStack(alignment: .leading, spacing: 12) {
Text("Course Details")
.font(.headline)
Text("Course: SwiftUI Fundamentals")
Text("Level: Beginner")
Text("Format: Practical Projects")
}
5.2 HStack
HStack arranges views from leading to trailing. It is useful for placing an icon beside text or grouping buttons into a row.
HStack(spacing: 12) {
Image(systemName: "book.closed.fill")
.font(.title2)
VStack(alignment: .leading) {
Text("SwiftUI Basics")
.font(.headline)
Text("Learn reusable interface design")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
5.3 ZStack
ZStack layers views along the depth axis. It is helpful for placing text over a background, adding badges to icons, or creating simple overlays.
ZStack(alignment: .bottomLeading) {
Image("courseBanner")
.resizable()
.scaledToFill()
.frame(height: 180)
.clipped()
Text("SwiftUI Course")
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(.white)
.padding()
}
When creating image-based components, ensure that the asset exists in the project and choose a suitable scaling and clipping strategy for the intended layout.
6. Making Views Reusable with Properties
A reusable view should be configurable. Properties allow the same component to display different titles, descriptions, icons, colors, or other values without duplicating its implementation.
import SwiftUI
struct InfoCardView: View {
let title: String
let description: String
let symbol: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: symbol)
.font(.title2)
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.headline)
Text(description)
.font(.subheadline)
.foregroundStyle(.secondary)
.fixedSize(
horizontal: false,
vertical: true
)
}
Spacer(minLength: 0)
}
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 16) {
InfoCardView(
title: "Learn SwiftUI",
description: "Create modern declarative interfaces.",
symbol: "swift"
)
InfoCardView(
title: "Build Projects",
description: "Practice by building real applications.",
symbol: "hammer.fill"
)
}
.padding()
}
}
Key concepts:
title, description, and symbol are input properties.
- The component owns the layout and styling for its content.
- Each instance receives different values.
- Changes to the component's implementation can improve every place where it is used.
7. Stored Properties, Initializers, and Default Values
SwiftUI views can use stored properties to receive configuration data. Swift automatically provides a memberwise initializer for many structures, allowing developers to create an instance by passing values for its properties.
struct BadgeView: View {
let text: String
var symbol: String = "checkmark.seal.fill"
var body: some View {
Label(text, systemImage: symbol)
.font(.subheadline)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(.blue.opacity(0.12))
.clipShape(Capsule())
}
}
// Usage:
BadgeView(text: "Completed")
BadgeView(
text: "Verified",
symbol: "checkmark.circle.fill"
)
The text property must be supplied when creating the component. The symbol property has a default value, so the caller can omit it when the default icon is appropriate.
Use clear property names and sensible defaults. Avoid exposing configuration options that are not actually needed by the component's users.
8. Understanding View Modifiers
View modifiers configure the appearance, layout, accessibility, and behavior of a view. Examples include font, padding, foregroundStyle, background, frame, and clipShape.
Text("Reusable Component")
.font(.headline)
.foregroundStyle(.white)
.padding()
.frame(maxWidth: .infinity)
.background(.blue)
.clipShape(RoundedRectangle(cornerRadius: 12))
Modifiers return modified views and can be chained. Their order can change the final appearance because each modifier operates on the view produced by the preceding modifier.
Example: Modifier Order
// Padding is included inside the blue background.
Text("Hello")
.padding()
.background(.blue)
// The background is applied before the outer padding.
Text("Hello")
.background(.blue)
.padding()
The first example generally creates a blue area that includes the padding around the text. In the second example, the background is applied to the text's initial layout before the outer padding is added.
9. Creating Reusable Styling with Custom Modifiers
When the same combination of styling modifiers appears in many places, developers can create a custom ViewModifier. This makes the design consistent and reduces repeated modifier chains.
import SwiftUI
struct PrimaryCardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(uiColor: .secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 14))
}
}
extension View {
func primaryCardStyle() -> some View {
modifier(PrimaryCardStyle())
}
}
struct ExampleCardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Learning Progress")
.font(.headline)
Text("Your course progress appears here.")
.font(.subheadline)
}
.primaryCardStyle()
}
}
The custom modifier packages the styling rules into one reusable method. If the card's corner radius or background needs to change later, the developer can update the modifier rather than editing every card individually.
10. Extracting Complex Interfaces into Smaller Views
Large SwiftUI views can become difficult to read when a single body contains many nested stacks, conditional branches, and repeated sections. Extracting meaningful sections into separate views improves readability and makes the code easier to maintain.
For example, a shopping screen might contain:
ProductImageView for displaying the product image.
ProductTitleView for the product name and description.
PriceView for price and discount information.
AddToCartButton for the primary action.
ProductDetailView to combine the smaller components.
struct ProductTitleView: View {
let name: String
let description: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(name)
.font(.title2)
.fontWeight(.semibold)
Text(description)
.font(.body)
.foregroundStyle(.secondary)
}
}
}
struct PriceView: View {
let price: String
var body: some View {
Text(price)
.font(.title3)
.fontWeight(.bold)
}
}
struct ProductDetailView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
ProductTitleView(
name: "Wireless Headphones",
description: "Comfortable headphones for daily use."
)
PriceView(price: "₹2,499")
}
.padding()
}
}
Extract a view when it represents a meaningful part of the interface, has reusable behavior, or makes a complex layout easier to understand. Creating a separate view for every trivial expression can make a project unnecessarily fragmented.
11. Managing State Inside Reusable Views
Some reusable views only display input data. Other views need to manage temporary interface state, such as whether a disclosure section is expanded or whether a toggle is enabled. SwiftUI provides property wrappers for these situations.
11.1 Using @State
@State stores local, view-owned state. SwiftUI updates the affected interface when that state changes.
struct ExpandableDescriptionView: View {
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Course Description")
.font(.headline)
if isExpanded {
Text("This course introduces SwiftUI views, reusable components, layout containers, state management, and practical interface development.")
} else {
Text("Learn the fundamentals of reusable SwiftUI interfaces.")
}
Button(isExpanded ? "Show Less" : "Read More") {
isExpanded.toggle()
}
}
.padding()
}
}
The state variable controls which description is shown. When the button changes the Boolean value, SwiftUI recalculates the relevant view description.
11.2 Using @Binding
@Binding allows a child view to read and update state owned by another view. It is useful when a reusable component needs to edit a value that belongs to its parent.
struct NotificationToggleView: View {
let title: String
@Binding var isEnabled: Bool
var body: some View {
Toggle(title, isOn: $isEnabled)
}
}
struct SettingsView: View {
@State private var notificationsEnabled = true
var body: some View {
Form {
NotificationToggleView(
title: "Enable Notifications",
isEnabled: $notificationsEnabled
)
Text(
notificationsEnabled
? "Notifications are enabled."
: "Notifications are disabled."
)
}
}
}
In this example, SettingsView owns the value through @State, while NotificationToggleView receives a binding. The toggle can therefore update the parent's state without owning a separate copy.
11.3 Choosing the Correct Property Wrapper
| Property Wrapper |
Purpose |
Typical Use |
@State |
Owns local state for a view. |
Expanded sections, selected tabs, local toggles. |
@Binding |
Reads and writes state owned elsewhere. |
Reusable form fields and toggle components. |
@Observable |
Defines a reference type whose observable properties can drive updates in modern Observation-based code. |
Shared models and application state, where appropriate. |
@Environment |
Reads values supplied through the environment. |
Environment settings, dismiss actions, shared dependencies. |
@EnvironmentObject |
Reads an observable object supplied by an ancestor. |
Shared observable objects in applications using this pattern. |
Choose state ownership deliberately. A view should own a value when that value belongs to its own UI behavior. Use bindings when a child needs to edit a parent's value, and use an appropriate observable model when state is shared across multiple parts of an application.
12. Passing Actions into Reusable Components
A reusable view often needs to report that the user performed an action. A closure property allows the parent to decide what should happen without hardcoding application-specific behavior into the child component.
struct ActionButtonView: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
struct CheckoutView: View {
var body: some View {
ActionButtonView(title: "Place Order") {
submitOrder()
}
.padding()
}
private func submitOrder() {
// Validate the order and start the checkout workflow.
}
}
The button component handles presentation, while the parent provides the action. This separation makes the button reusable in different screens and workflows.
13. Creating Reusable Form Components
Forms commonly contain repeated patterns such as a label, input field, validation message, and consistent spacing. Reusable form components can standardize these patterns and simplify form screens.
struct LabeledTextField: View {
let title: String
let placeholder: String
@Binding var text: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
TextField(placeholder, text: $text)
.textFieldStyle(.roundedBorder)
}
}
}
struct RegistrationView: View {
@State private var fullName = ""
@State private var email = ""
var body: some View {
Form {
LabeledTextField(
title: "Full Name",
placeholder: "Enter your full name",
text: $fullName
)
LabeledTextField(
title: "Email Address",
placeholder: "Enter your email",
text: $email
)
Button("Register") {
// Validate and submit the form.
}
}
}
}
The reusable field receives its label and placeholder as ordinary values and receives editable text through a binding. Real registration screens should also validate inputs, provide clear error messages, and use suitable keyboard types and text-content settings.
14. Making Reusable Components Responsive
Reusable components should work in different containers and on different screen sizes. A card that looks correct on an iPhone should not automatically stretch every line of text across a wide iPad display.
Responsive Design Techniques
- Use flexible frames when a component should fill the available width.
- Use a maximum width when text or forms should remain readable on large displays.
- Allow descriptions and headings to wrap onto multiple lines.
- Use stack views and grids to arrange related elements.
- Use scrolling when content may exceed the available height.
- Avoid fixed heights for views containing variable-length text.
- Test components inside different parent layouts, not only in isolation.
struct ResponsiveInfoCard: View {
let title: String
let description: String
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(title)
.font(.headline)
Text(description)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
}
.padding()
.frame(maxWidth: 520, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .center)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
}
This component limits its content width to a readable maximum while allowing the surrounding layout to center it within a wider area. The chosen maximum should be adjusted to the design and content requirements of the application.
15. Organizing SwiftUI Components in a Project
A clear project structure makes reusable views easier to locate and maintain. The best organization depends on the size of the application, but related views should generally be grouped by feature or responsibility.
LearningApp/
├── App/
│ └── LearningApp.swift
├── Models/
│ └── Course.swift
├── Features/
│ ├── Dashboard/
│ │ ├── DashboardView.swift
│ │ └── DashboardViewModel.swift
│ └── Courses/
│ ├── CourseListView.swift
│ └── CourseDetailView.swift
├── Components/
│ ├── CourseCardView.swift
│ ├── LabeledTextField.swift
│ ├── PrimaryButton.swift
│ └── EmptyStateView.swift
└── Resources/
└── Assets.xcassets
Organization Guidelines
- Use descriptive names that explain each component's purpose.
- Keep feature-specific views near the feature they support.
- Place genuinely shared UI components in a shared components area.
- Separate data models from presentation views.
- Keep network requests and business rules out of simple presentation components where practical.
- Extract shared styling when the same design pattern appears repeatedly.
16. Previewing and Testing Reusable Views
Xcode previews make it possible to inspect a SwiftUI view during development. Previews can help identify spacing, alignment, and appearance problems before running the complete application.
#Preview {
VStack(spacing: 16) {
InfoCardView(
title: "Reusable Views",
description: "Create once and configure with different data.",
symbol: "square.stack.3d.up"
)
InfoCardView(
title: "Responsive Design",
description: "Adapt to the available layout space.",
symbol: "rectangle.expand.vertical"
)
}
.padding()
}
Testing Checklist
- Preview the component with short and long text.
- Test default and custom property values.
- Check that buttons trigger the expected action.
- Verify that bindings update the correct state.
- Inspect the layout on compact and regular widths.
- Test Dynamic Type and accessibility labels.
- Confirm that colors and backgrounds work in light and dark appearances.
- Check that the component remains usable when placed inside a scroll view, list, form, or grid.
17. Common Mistakes and How to Avoid Them
| Mistake |
Why It Causes Problems |
Recommended Solution |
| Repeating the same layout code |
Changes must be applied in several locations. |
Extract a reusable view with configurable properties. |
| Putting the entire screen in one large view |
The code becomes difficult to understand and maintain. |
Split meaningful sections into smaller components. |
| Using @State for parent-owned data in a child |
The child can maintain an independent value instead of editing the parent's value. |
Use @Binding when the child needs to edit parent-owned state. |
| Hardcoding text and behavior inside a generic component |
The component becomes tied to one particular screen. |
Pass values and action closures through properties. |
| Using fixed dimensions everywhere |
Content can clip or look awkward on other devices. |
Prefer flexible layouts and appropriate minimum or maximum dimensions. |
| Creating too many tiny components |
The code can become fragmented without improving clarity. |
Extract views when they have a meaningful responsibility or improve reuse. |
| Ignoring accessibility |
Controls and text may be difficult to use with assistive technologies. |
Use semantic controls, accessible labels, and Dynamic Type support. |
| Mixing network and presentation logic |
Views become harder to test and reuse. |
Move complex data-loading and business logic into suitable models or services. |
18. Best Practices for Reusable SwiftUI Components
- Give each component a clear and focused responsibility.
- Use properties for configurable content and appearance.
- Use default values when they make a component easier to use.
- Use bindings when a child must update state owned elsewhere.
- Pass closures for actions that belong to the parent or application workflow.
- Keep business logic separate from presentation where appropriate.
- Prefer semantic fonts, accessible controls, and adaptable layouts.
- Use custom modifiers for repeated styling patterns.
- Test components with different inputs and layout environments.
- Avoid abstraction that adds complexity without meaningful reuse.
19. Practical Exercises
Exercise 1: Create a Profile Card
Create a reusable profile card with an image, name, role, and optional action button. Pass the name and role through properties so the same view can display multiple profiles.
Expected result: One component displays different profiles without duplicating the layout code.
Exercise 2: Build a Reusable Button
Create a primary action button with configurable text and an action closure. Use the component in a login screen and a registration screen.
Expected result: Both screens share the same button design while performing different actions.
Exercise 3: Build a Form Field
Create a labeled text field with a title, placeholder, and binding. Use it for name, email, and city inputs.
Expected result: Each field follows the same design and updates the correct parent-owned value.
Exercise 4: Create a Responsive Card Grid
Build reusable course cards and display them in a scrollable adaptive grid. Verify that the cards adjust to different widths and that longer course descriptions remain readable.
Expected result: The component works across iPhone and iPad layouts without unnecessary fixed widths.
20. Mini Project: Reusable Course Dashboard
Project objective: Develop a SwiftUI dashboard that displays multiple courses using reusable components. The project should demonstrate custom views, configurable properties, view composition, and basic state management.
Project Requirements
- Create a course model containing an identifier, title, description, and progress value.
- Create a reusable course card that displays course details.
- Create a reusable progress indicator.
- Create a reusable action button for opening course details.
- Use an adaptive grid or suitable list layout to display multiple courses.
- Keep the main dashboard organized into smaller views.
- Use local state or bindings where interaction requires them.
- Support scrolling and different screen sizes.
Sample Model
import Foundation
struct Course: Identifiable {
let id: UUID
let title: String
let description: String
let progress: Double
init(
id: UUID = UUID(),
title: String,
description: String,
progress: Double
) {
self.id = id
self.title = title
self.description = description
self.progress = min(max(progress, 0), 1)
}
}
Sample Reusable Course Card
import SwiftUI
struct CourseCardView: View {
let course: Course
let onOpen: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(course.title)
.font(.headline)
Text(course.description)
.font(.subheadline)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
ProgressView(value: course.progress)
.accessibilityLabel("Course progress")
.accessibilityValue(
"\(Int(course.progress * 100)) percent"
)
Button("Open Course", action: onOpen)
.buttonStyle(.borderedProminent)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(uiColor: .secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 14))
}
}
Learning outcome: This project demonstrates how to create a view that accepts a model and a closure, enabling the same component to display different courses and report user actions to its parent.
21. Interview Questions and Answers
Q1. What is a View in SwiftUI?
Answer: A view is a building block of a SwiftUI interface. It conforms to the View protocol and describes the interface through its body property.
Q2. What is the purpose of the View protocol?
Answer: The View protocol defines the structure expected of a SwiftUI view, including the body property that describes its content.
Q3. What is a reusable component?
Answer: A reusable component is a view that can be used in multiple places. It often accepts properties for configurable content and avoids repeated layout code.
Q4. What is the difference between VStack and HStack?
Answer: VStack arranges its children vertically, while HStack arranges them horizontally.
Q5. What is the purpose of ZStack?
Answer: ZStack layers views on top of each other, making it useful for overlays, backgrounds, and layered interface elements.
Q6. Why are properties useful in reusable views?
Answer: Properties allow the same view implementation to display different values, such as titles, descriptions, symbols, or model data.
Q7. What is the difference between @State and @Binding?
Answer: @State stores state owned by a view, while @Binding provides access to a value owned elsewhere so that the child can read and update it.
Q8. How can a reusable component report a user action?
Answer: A component can accept a closure property and call it when the user performs an action. This lets the parent determine the behavior.
Q9. What is a custom ViewModifier?
Answer: A custom ViewModifier packages repeated styling or behavior into a reusable modifier that can be applied to different views.
Q10. How do you test a reusable SwiftUI view?
Answer: Preview the view with different input values, test its interactions and bindings, inspect its behavior across screen sizes, and check accessibility and appearance settings.
Q11. Should every small interface element become a separate view?
Answer: No. Extract views when doing so improves clarity, reuse, or separation of responsibility. Excessive extraction can make simple interfaces harder to navigate.
Q12. How can reusable views support responsive design?
Answer: They can use flexible frames, adaptive containers, stack views, grids, and content-driven sizing so the same component can work in different layout environments.
22. Quick Revision Checklist
- A SwiftUI view conforms to the View protocol.
- The body property describes the view's interface.
- Built-in views include Text, Image, Button, List, and ScrollView.
- VStack, HStack, and ZStack organize content in different ways.
- Properties make custom views configurable and reusable.
- View modifiers control appearance, layout, and behavior.
- Custom ViewModifier types centralize repeated styling.
- @State stores local view-owned state.
- @Binding allows a child view to edit state owned elsewhere.
- Closures let reusable components report user actions.
- Small, focused views improve readability and maintainability.
- Previews and tests help verify behavior across different inputs and layouts.
- Responsive and accessible design should be considered from the beginning.
23. Conclusion
Creating views and reusable SwiftUI components is a fundamental skill for developing clean, maintainable iOS applications. SwiftUI's declarative approach allows developers to combine built-in views, create custom components, configure them through properties, and respond to state changes.
By organizing interfaces into focused components, using the correct state-management tools, sharing common styles through modifiers, and testing across different layouts, developers can reduce duplicated code and deliver consistent user experiences. These techniques provide a strong foundation for building larger applications such as dashboards, registration forms, shopping interfaces, and educational apps.
Explore more topics through the JustAcademy iOS App Development Training Course, register for a course demo, or visit the JustAcademy official website.