swiftuiswift-protocols

Introduce a view modifier that is based on properties set by protocols in SwiftUI


I am currently trying to set up a convenience architecture for our view redaction. I therefore added protocols that constrain View and ViewModel to certain properties. I then want to implement a view modifier that makes use of these properties.

Let's assume we have the following setup:

protocol ViewModelDriven {
    associatedtype ViewModel
    var viewModel: ViewModel { get }
}

protocol RedactionProviding {
    var isRedacted: Bool { get }
}

extension View where Self: ViewModelDriven, ViewModel: RedactionProviding {
    func redact() -> some View {
        redacted(reason: viewModel.isRedacted ? .placeholder : [])
    }
}

Up to that everything works fine. The modifier uses viewModel and isRedacted provided by the protocols.

When I now try to implement a View/ViewModel using this setup, I get the following error when using the modifier: Referencing instance method 'redact()' on 'View' requires that 'some View' conform to 'ViewModelDriven'

struct FooView: View, ViewModelDriven {
    typealias ViewModel = FooViewModel
    var viewModel: FooViewModel
    
    var body: some View {
        EmptyView()
            .redact()
    }
}

class FooViewModel: RedactionProviding {
    var isRedacted: Bool { true }
}

As far as I understand, this is due to the modifier being attached to an opaque some View.

Does anybody know if and how I can work around that? Is it even intended to do things like that with SwiftUI or am I on the wrong path? Any help appreciated!


Solution

  • I think your intention is something more like this

    extension ViewModelDriven where ViewModel : RedactionProviding {
        @ViewBuilder func redact<Content>(content: () -> Content) -> some View where Content : View {
            switch viewModel.isRedacted {
            case true:
                Text("Placeholder")
            case false:
                content()
            }
        }
    }
    

    that way redact is available within FooView and not on the complex views that make up the body.

    You can then use it

    struct FooView: View, ViewModelDriven {
        typealias ViewModel = FooViewModel
        var viewModel: ViewModel
        
        var body: some View {
            redact {
                Text("Hello World!!")
            }
        }
    }
    

    Here is a full example

    struct FooView: View, ViewModelDriven {
        typealias ViewModel = FooViewModel
        var viewModel: ViewModel
        
        var body: some View {
            redact {
                Text("Hello World!!")
            }
        }
    }
    
    class FooViewModel: RedactionProviding {
        var isRedacted: Bool { true }
    }
    @MainActor
    protocol ViewModelDriven {
        associatedtype ViewModel
        var viewModel: ViewModel { get }
    }
    
    protocol RedactionProviding {
        var isRedacted: Bool { get }
    }
    
    extension ViewModelDriven where ViewModel : RedactionProviding {
        @ViewBuilder func redact<Content>(content: () -> Content) -> some View where Content : View {
            switch viewModel.isRedacted {
            case true:
                Text("Placeholder")
            case false:
                content()
            }
        }
    }
    

    Note that "ViewModelDriven" will present many more challenges with SwiftUI, this is just a means to answer your question and shouldn't be used as an example of efficiency.