Embedding content in a Navigation Link conditionally

I'm working on building out some forms where some of the fields are drop-downs that push on a list of options for the user to make their selection. In some cases, the user may not be allowed to edit a particular field or even the entire form based on their permissions or the state of the object the form represents. Visually, I would prefer that the disclosure indicator not be shown when the user isn't allowed to edit a drop down field.

A naive implementation might be:

if self.canEdit {
  NavigationLink {
    OptionList(...)
  } label: {
    DropDownField(...)
  }
} else {
  DropDownField(...)
}

That can get pretty messy though, especially on a form with a lot of drop down fields. It occurred to me that I could accomplish the same thing using a ViewModifier and an extension on View like this:

extension View {
  func navigationLink<Destination: View>(when condition: Bool, @ViewBuilder destination: @escaping () -> Destination) -> some View {
    modifier(ConditionalNavigationLink(when: condition, destination: destination))
  }
}

struct ConditionalNavigationLink<Destination: View>: ViewModifier {
  let condition: Bool
  let destination: () -> Destination

  init(when condition: Bool, @ViewBuilder destination: @escaping () -> Destination) {
    self.condition = condition
    self.destination = destination
  }

  func body(content: Content) -> some View {
    if condition {
      NavigationLink(destination: self.destination, label: { content })
    } else {
      content
    }
  }
}

Which would allow me to simplify my previous implementation to:

DropDownField(...)
  .navigationLink(when: self.canEdit) {
    OptionList(...)
  }

Does this seem like a reasonable solution? Are there any drawbacks I'm not thinking of or a better way of accomplishing this?

Just use the disabled view modifier to disable the navigation link.

Correct me if I'm wrong, but won't that continue to show the disclosure indicator?

Yes it will, but I think this looks better. It reminds the user that this is a navigation link, but that it is disabled. Notice that disabled greys out the view, which is consistent with the visual appearance of all other disabled controls.

Also, your view navigationLink view modifier can be simplified to the following:

extension View {
    
    @ViewBuilder func navigationLink<Destination: View>(
        when condition: Bool,
        @ViewBuilder destination: @escaping () -> Destination
    ) -> some View {
        
        if condition {
            NavigationLink(destination: destination(), label: { self })
        } else {
            self
        }
        
    }
   
}

I don't know why you used ViewModifier. Your modifier does not contain any mutable state, so using it is pointless.

1 Like

Yes, that's much nicer. I blame my inexperience with SwiftUI in general. Thanks for the pointer!

I think this is a bit subjective and depends on the context. In the app that I'm working on some users are only ever allowed to view content. In that case they are only interested in the value itself and I don't think they need to know that it ever was a navigation link. I also don't think the value text should appear disabled in this mode as that could make it harder to consume the data. I suppose there's an argument to be had about whether they should even share the same control at that point.

1 Like