Some usages of custom generic view modifiers in SwiftUI do not compile

413 Views Asked by At

I am trying to replace a standard sheet modifier with a custom one that applies the same changes to the content of all sheets as to the main view (it can be useful for changing accent color, although there is a UIKit approach for it, but specifically I want to apply privacySensitive modifier to all sheets).

The code that creates the modifiers compiles ok:

import SwiftUI

struct SheetForItem<T, C>: ViewModifier where T: Identifiable, C: View {
    var item: Binding<T?>
    var onDismiss: (() -> Void)? = nil
    var sheetContent: (T) -> C

    func body(content: Content) -> some View {
        content.sheet(item: item, onDismiss: onDismiss) {
            sheetContent($0).privacySensitive()
        }
    }
}

extension View {
    func appSheet<T, Content>(
        item: Binding<T?>,
        onDismiss: (() -> Void)? = nil,
        content: @escaping (T) -> Content
    ) -> some View where T: Identifiable, Content: View {
        modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content))
    }
}

Mostly it works, but some of the usages of appSheet in the chain of other modifiers instead of sheet do not compile with an error:

Type () cannot conform to View.

The example below doesn't compile (but it will compile if I replace appSheet with sheet):

import SwiftUI

enum Actions:Identifiable {
    case action1
    case action2

    var id: Self { self }
}

struct AppSheetExample: View {
    @State var showActions = false
    @State private var action: Actions?

    var body: some View {
        Button { showActions = true } label: {
            Image(systemName: "square.and.pencil")
                .resizable()
                .scaledToFit()
                .frame(width: 24, height: 24)
        }
        .confirmationDialog("Actions", isPresented: $showActions, titleVisibility: .visible) {
            Button("Action 1") { action = .action2 }
            Button("Action 2") { action = .action2 }
        }
        .appSheet(item: $action) { sheet in
            switch sheet {
            case .action1: Text("Action 1")
            case .action2: Text("Action 2")
            }
        }
    }
}

Thank you!

1

There are 1 best solutions below

4
On

You need to mark your content closure with @ViewBuilder since you're not explicitly returning a View(i.e: return Text("Action 1")):

extension View {
    func appSheet<Content>(
        isPresented: Binding<Bool>,
        onDismiss: (() -> Void)? = nil,
        @ViewBuilder content: @escaping () -> Content
    ) -> some View where Content: View {
        modifier(SheetIsPresented(isPresented: isPresented, onDismiss: onDismiss, sheetContent: content))
    }
    
    func appSheet<T, Content>(
        item: Binding<T?>,
        onDismiss: (() -> Void)? = nil,
        @ViewBuilder content: @escaping (T) -> Content
    ) -> some View where T: Identifiable, Content: View {
        modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content))
    }
}