SwiftUI Reusable View via Protocol Inheritance

Viewed 2109

The Goal

The goal I have is to make a reusable view protocol that inherits the SwiftUI View protocol and provides some default functionality, for loading showing different views based on the status of the views content.

This way, I don't have to rewrite identical code for every view I create and makes code cleaner.

The Problem

I made a "DelayedContentView" protocol that displays two different view bodies based on whether the view's content has loaded. The problem arises when I try to implement it. The "loadedBody" and "unloadedBody" properties can't be of type "some View" even though they are the same type as the SwiftUI View's Body associatedType.

Background

I have a various views in my app that fetch data remotely. Each view shows two view bodies: one for when the content is being fetched and the other for when the fetch is finished.

Apple's SwiftUI View Protocol

public protocol View {

    associatedtype Body : View

    var body: Self.Body { get }
}

My View Protocol

protocol DelayedContentView:View {
    
    func contentLoaded() -> Bool
    
    var loadedBody: Self.Body { get }
    
    var unloadedBody: Self.Body { get }
    
}


extension DelayedContentView {
    
    //Default implementation that I won't have to rewrite for each view.
    var body: some View {
        if contentLoaded() {
            return self.loadedBody
        }else{
            return self.unloadedBody
        }
    }
    
}

Implementation

struct ExampleView:DelayedContentView {
    
    func contentLoaded() -> Bool {
        //Ask viewModel if content is loaded.
        return false
    }

    var loadedBody: some View {
        Text("Content is loaded.")
    }

    var unloadedBody: some View {
        Text("Fetching content...")
    }
        
    
}
Compile Error: Type 'ExampleView' does not conform to protocol 'DelayedContentView'

I thought protocol inheritance was the right tool for a case like this, but maybe I'm mistaken?

1 Answers

The protocol DelayedContentView has to add two more associated types to represent two additional views types: one for a "loaded" view and one - for "unloaded". It cannot be Self.Body because it clearly will be violated since you need a conditional view in the body.

protocol DelayedContentView: View {
    
    associatedtype LoadedBody: View
    associatedtype UnloadedBody: View
    
    func contentLoaded() -> Bool
    
    var loadedBody: LoadedBody { get }
    
    var unloadedBody: UnloadedBody { get }
}

Now that you have two of those generic types, Self.Body will be a composite type of the two:

extension DelayedContentView {

    @ViewBuilder // needed to create a _ConditionalContent type from if/else
    var body: some View {
        if contentLoaded() {
            self.loadedBody
        } else {
            self.unloadedBody
        }
    }
}

Now, your specific View will declare what LoadedView and UnloadedView types are. For example, in your ExampleView, they would both be Text, and Body would be _ConditionalContent<Text,Text>.

Related