swiftuiviewmodifier

SwiftUI ViewModifier for contextMenu with generics


I want to add the iOS16 API for 'contextMenu forSelectionType: menu:', but I also need to support iOS15, so I need a conditional ViewModifier for this. I'm struggling with the correct syntax for it.

From what I can work out, this is what my ViewModifier should look like:

struct CompatibilityListContextMenuModifier<V>: ViewModifier where V: View {

    let menu: (Set<Person.ID>) -> V
    
    func body(content: Content) -> some View {
      if #available(iOS 16.0, macOS 13.0, *) {
          content
              .contextMenu(forSelectionType: Person.ID.self, menu: menu)
        } else {
            content
        }
    }
}

But how do I use this in my List:

List (selection: $contactsListOptions.multiSelectedContacts){
   // ...
}
.modifier(CompatibilityListContextMenuModifier(menu: items in {
            if items.isEmpty { // Empty area menu.
                Button("New Item") { }

            } else if items.count == 1 { // Single item menu.
                Button("Copy") { }
                Button("Delete", role: .destructive) { }
            }
            else {
                Button("MultipleSelection Items") { }
            }
        }))

This gives me a syntax error:

Cannot find 'items' in scope

If I try to pass in the selection binding:

.modifier(CompatibilityListContextMenuModifier(menu: contactsListOptions.multiSelectedContacts in {
        if contactsListOptions.multiSelectedContacts.isEmpty { // Empty area menu.
            Button("New Item") { }

        } else if contactsListOptions.multiSelectedContacts.count == 1 { // Single item menu.
            Button("Copy") { }
            Button("Delete", role: .destructive) { }
        }
        else {
            Text("MultipleSelection Items")
        }
    }))

I get the error:

Cannot convert value of type 'Set<Person.ID>' (aka 'Set') to expected argument type '(Set<Person.ID>) -> V' (aka '(Set) -> V')

and

Generic parameter 'V' could not be inferred

What's the right way to conditionally compile this API?


Solution

  • The syntax for creating the modifier should be:

    CompatibilityListContextMenuModifier { items in ... }
    

    The closure parameter should go inside the { ... }.

    The closure should also be a @ViewBuilder, otherwise you cannot use if statements like that in the closure.

    // I also abstracted away Person.ID as the Selection type parameter
    struct CompatibilityListContextMenuModifier<Menu, Selection>: ViewModifier
        where Menu: View, Selection: Hashable {
    
        @ViewBuilder
        let menu: (Set<Selection>) -> Menu
        
        func body(content: Content) -> some View {
          if #available(iOS 16.0, macOS 13.0, *) {
              content
                  .contextMenu(forSelectionType: Selection.self, menu: menu)
            } else {
                content
            }
        }
    }
    

    Now you can use it like this:

    .modifier(CompatibilityListContextMenuModifier<_, Person.ID> { items in
        if items.isEmpty {
            Button("New Item") { }
    
        } else if items.count == 1 {
            Button("Copy") { }
            Button("Delete", role: .destructive) { }
        }
        else {
            Button("MultipleSelection Items") { }
        }
    })
    

    I'd also suggest that you write your own View extension to write this more conveniently:

    extension View {
        func compatibilityContextMenu<Menu: View, Selection: Hashable>(
            forSelectionType type: Selection.Type, 
            @ViewBuilder menu: @escaping (Set<Selection>) -> Menu
        ) -> some View {
            modifier(CompatibilityListContextMenuModifier(menu: menu))
        }
    }
    
    .compatibilityContextMenu(forSelectionType: Person.ID.self) { items in
        // ...
    }