SwiftUI

How to Switch on a Protocol Conforming to SwiftUI's View

Tired of messy if-else chains in SwiftUI? Learn the elegant, scalable pattern for switching on a protocol to render dynamic views for different data types.

A

Alexandre Dubois

Senior iOS Engineer specializing in SwiftUI architecture and building scalable, maintainable mobile apps.

7 min read14 views

Ever found yourself staring at a chunk of SwiftUI code, trying to display a list of items that all do similar things but aren't the exact same type? You have an array of `[any FeedItem]`, where `FeedItem` is a protocol. Your list might contain news articles, video posts, and user polls. You need to show a different view for each one, and you suddenly feel a messy `if-let` chain or a giant `switch` statement creeping into your `View` body. There has to be a better way, right?

There is. It’s an elegant, scalable pattern that leverages the power of protocols to create truly dynamic and maintainable SwiftUI views. Let's ditch the conditional spaghetti and learn how to switch on a protocol the right way.

The Core Problem: Heterogeneous Collections

In SwiftUI, the `ForEach` view is a powerhouse for creating lists. However, it expects a collection of items where it can generate a consistent view type for each element. When you have a homogeneous array like `[String]` or `[User]`, it's simple. But what happens when you have a mixed array, often called a heterogeneous collection?

Let's define our scenario. We have a `FeedItem` protocol and a few different types that conform to it:

// Our shared behavior
protocol FeedItem: Identifiable {
    var id: UUID { get }
}

// A few different types of feed items
struct ArticleItem: FeedItem {
    let id = UUID()
    let title: String
    let author: String
}

struct VideoItem: FeedItem {
    let id = UUID()
    let videoURL: URL
    let thumbnail: String
}

struct PollItem: FeedItem {
    let id = UUID()
    let question: String
    let options: [String]
}

Now, we want to display an array of these items. In modern Swift, we'd use the `any` keyword to create an existential type:

let feedItems: [any FeedItem] = [
    ArticleItem(title: "SwiftUI's New Tricks", author: "Jane Doe"),
    VideoItem(videoURL: someURL, thumbnail: "swiftui-thumb"),
    PollItem(question: "Favorite new feature?", options: ["Macros", "Observation"])
]

If you try to throw this directly into a `ForEach`, you'll hit a wall. How do you tell SwiftUI which view to render? The `item` inside the `ForEach` is just an `any FeedItem`, not a concrete `ArticleItem` or `VideoItem`.

The Brute-Force Approach (And Why It Hurts)

The most intuitive first attempt is to use conditional casting inside the loop. It looks something like this:

struct FeedView: View {
    let items: [any FeedItem]

    var body: some View {
        List(items) { item in
            // This gets messy fast...
            if let article = item as? ArticleItem {
                ArticleCell(article: article)
            } else if let video = item as? VideoItem {
                VideoCell(video: video)
            } else if let poll = item as? PollItem {
                PollCell(poll: poll)
            } else {
                EmptyView()
            }
        }
    }
}

While this works, it has several major drawbacks:

  • Violation of the Open/Closed Principle: Every time you add a new `FeedItem` type (e.g., `QuizItem`), you have to come back and modify this `if-else` block. Your view should be closed for modification but open for extension.
  • Poor Scalability: With just three types, it's already getting crowded. Imagine ten or twenty. This code becomes a maintenance nightmare.
  • Wrong Responsibility: The `FeedView` now has to know about every single possible type of `FeedItem`. Its job is to display a list, not to be a catalog of all possible cell types.
Advertisement

We can do so much better by delegating the responsibility of view creation to the types themselves.

The Elegant Solution: Let the Protocol Provide the View

This is the paradigm shift. Instead of the view asking, "What type are you?", we'll have the item declare, "Here is my view." We achieve this by adding a requirement to our protocol.

Let's create a new, more descriptive protocol called `Displayable`. Any type conforming to this protocol will be responsible for providing its own SwiftUI view.

import SwiftUI

protocol Displayable: Identifiable {
    // This is the magic!
    // Each conforming type will provide its own view body.
    @ViewBuilder var view: any View { get }
}

Look at that! We added a computed property called `view` that returns `any View`. The `@ViewBuilder` attribute gives us the same flexibility we have inside a `View`'s `body` property. The `any View` return type is a modern Swift feature (since 5.7) that erases the concrete view type, allowing us to return different kinds of views from the same property.

Implementing the Displayable Protocol

Now, let's make our concrete types conform to `Displayable`. Each type will know exactly how it should be rendered.

struct ArticleItem: Displayable {
    let id = UUID()
    let title: String
    let author: String

    var view: any View {
        ArticleCell(article: self)
    }
}

struct VideoItem: Displayable {
    let id = UUID()
    let videoURL: URL
    let thumbnail: String

    var view: any View {
        VideoCell(video: self)
    }
}

struct PollItem: Displayable {
    let id = UUID()
    let question: String
    let options: [String]

    var view: any View {
        PollCell(poll: self)
    }
}

// The actual views (these can be as complex as you need)
struct ArticleCell: View { 
    let article: ArticleItem
    var body: some View { VSplit { Text(article.title).font(.headline); Text(article.author).font(.subheadline) } }
}
// ... VideoCell and PollCell would be defined similarly

Notice how clean this is. The knowledge of how to display an `ArticleItem` is encapsulated with `ArticleItem`. The main `FeedView` doesn't need to know anything about `ArticleCell` at all.

The Beautifully Simple Result

With our protocol and conforming types in place, our main `FeedView` becomes breathtakingly simple and robust.

struct FeedView: View {
    let items: [any Displayable]

    var body: some View {
        List(items) { item in
            item.view
        }
    }
}

// And creating it is just as easy:
let feedItems: [any Displayable] = [
    ArticleItem(title: "SwiftUI's New Tricks", author: "Jane Doe"),
    VideoItem(videoURL: someURL, thumbnail: "swiftui-thumb"),
    PollItem(question: "Favorite new feature?", options: ["Macros", "Observation"])
]

// In your app:
FeedView(items: feedItems)

That's it. That's the entire implementation inside the `List`. It's clean, declarative, and completely decoupled. When you create a new `QuizItem` that conforms to `Displayable`, you simply add its implementation and it will just work in the `FeedView` without a single line of change there. Your view is now open for extension but closed for modification. Mission accomplished.

When to Use This Pattern

This protocol-driven view pattern is incredibly powerful for any UI that needs to display a dynamic list of different-but-related components. Think of:

  • Settings Screens: A list of toggles, text fields, pickers, and navigation links. Each can be a type conforming to a `SettingItem` protocol.
  • Dashboards: A grid of widgets, where each widget (a chart, a summary number, a recent activity list) conforms to a `Widget` protocol.
  • Content Feeds: Our example of articles, videos, and polls is a classic use case.
  • Form Builders: Dynamically generated forms where each field type (text, date, checkbox) conforms to a `FormField` protocol.

For a small, fixed set of 2-3 types that will never change, a `switch` statement might be acceptable. But the moment your list becomes truly dynamic or you anticipate adding more types in the future, this protocol-oriented approach will save you countless hours and headaches.

Conclusion: A Smarter Way to Build

We've journeyed from a tangled `if-else` block to a clean, single line of code: `item.view`. By delegating the responsibility of view creation from the container to the items themselves, we unlock a more scalable, maintainable, and testable architecture.

This isn't just a clever trick; it's a fundamental concept in building robust software that aligns perfectly with the declarative nature of SwiftUI. So the next time you're faced with a list of `any Thing`, remember to give that `Thing` a voice and let it tell you how it wants to be seen.

Tags

You May Also Like