TechTime: Designing Intention-Revealing APIs: A Practical SwiftUI Comparison Pattern

Article
Swift
TechTime
Dorin Danciu
iOS Expert
Share

SwiftUI offers numerous instances where you need to display two views side by side, such as showcasing a before-and-after comparison, conducting an A/B test, or performing a quick visual comparison. While containers like HStack or Group can be used, they don’t explicitly indicate that you’re building a comparison. This is where TupleView comes into play.

Although TupleView may not be widely known, it can be utilized to enforce strict view composition rules. By combining it with @ViewBuilder, you can create APIs like CompareView that are both expressive and safe, ensuring the robustness and ease of understanding of your UI code.

import SwiftUI

/// A view that displays side-by-side previews for comparison.
///
/// `CompareView` takes exactly two views and presents them horizontally next to each other,
/// making it useful for visual comparisons, before/after demonstrations, or A/B testing scenarios.
///
/// Example usage:
/// ```swift
/// CompareView {
///     Rectangle()
///         .fill(.blue)
///
///     Rectangle()
///         .fill(.red)
/// }
/// ```
///
/// - Note: Both views are sized equally within the container.
///
/// - Warning: The content builder must provide exactly two views to compare.
///   Providing more or fewer will result in a compilation error.
///
public struct CompareView<Leading: View, Trailing: View>: View {

    /// The left-hand-side content to display.
    private let leading: Leading

    /// The right-hand-side content to display.
    private let trailing: Trailing

    /// Creates a new `CompareView` instance with exactly two views for comparison.
    ///
    /// - Parameter content: A closure that returns exactly two views wrapped in a TupleView.
    public init(@ViewBuilder _ content: @escaping () -> TupleView<(Leading, Trailing)>) {
        let views = content().value
        self.leading = views.0
        self.trailing = views.1
    }

    public var body: some View {
        HStack(spacing: 0) {
            preview(leading)
            preview(trailing)
        }
    }

    @ViewBuilder
    private func preview(_ content: some View) -> some View {
        Rectangle()
            .fill(.clear)
            .overlay(content)
    }
}

// MARK: - Previews

#Preview("Standard Compare View") {
    CompareView {
        Rectangle()
            .fill(.blue)

        Rectangle()
            .fill(.red)
    }
}

#Preview("Compare View with an empty side") {
    CompareView {
        Rectangle()
            .fill(.blue)

        EmptyView()
    }
}

1. What Makes CompareView Unique?

CompareView leverages SwiftUI’s @ViewBuilder and TupleView to require exactly two views at compile time. Here’s how you use it:

CompareView {
    Rectangle()
        .fill(.blue)
    
    Rectangle()
        .fill(.red)
}

If you try to add a third view or omit one, the compiler will flag an error — making your intent and structure explicit and safe.

2. 3 Key Benefits of This Approach

  1. Compile-Time Safety
    By requiring a tuple of two views, CompareView ensures you can’t accidentally provide too many or too few views. This prevents subtle bugs and makes your code more robust.
  1. Declarative and Concise Syntax
    Thanks to @ViewBuilder, you can write your comparison views in a natural, SwiftUI style—without extra boilerplate or ceremony.
  1. Clear Intent and Readability
    The API makes it obvious that you’re comparing two views. This self-documenting structure helps both current and future maintainers understand your code at a glance.

3. Comparing to a More Verbose Alternative

A more traditional approach might look like this:

struct VerboseCompareView<Leading: View, Trailing: View>: View {
    @ViewBuilder var leading: () -> Leading
    @ViewBuilder var trailing: () -> Trailing

    var body: some View {
        HStack {
            leading()
            trailing()
        }
    }
}

#Preview("Usage") {
    VerboseCompareView {
        Rectangle().fill(.blue) 
    } trailing: { 
        Rectangle().fill(.red) 
    }
}

Drawbacks of the Verbose Approach:

  • More Boilerplate and Verbosity: You have to define two separate closures (leading and trailing), which adds extra ceremony to the API and makes usage more verbose. This can clutter your code, especially when the views are complex.
  • Can be easily misused: The verbose solution can be easily misused because each closure can return multiple views, either intentionally or by mistake. For example, if you add two views to the trailing closure, you end up with an uneven comparison or an unexpected layout. This lack of enforcement makes it easy to introduce subtle bugs or break the intended structure, whereas CompareView ensures exactly two views are provided, maintaining clarity and consistency.
VerboseCompareView {
    Rectangle().fill(.blue)
} trailing: {
    Rectangle().fill(.red)
    Rectangle().fill(.yellow)
}

4. Why CompareView Feels More Practical

By combining the power of @ViewBuilder with the strictness of tuple composition, CompareView offers a sweet spot:

  • It’s as easy to use as an HStack,
  • but as safe and intention-revealing as a custom API.
    This makes it ideal for scenarios where exactly two views must be compared, and helps prevent subtle bugs or misunderstandings in your UI code.

In summary:

TupleView is a great example of how SwiftUI’s advanced API design can lead to safer, clearer, and more maintainable code.

References

Download Resource
Subscribe to newsletter

Subscribe to receive the latest blog posts to your inbox every week.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.