Lab 050 – Hover Effect Namespace

Show and hide a symbol with a hover effect, using the shape of the parent view to trigger the effect.

Overview

I want a way to show a hover effect that doesn’t impact the content of the view being hovered. For example, in Looming Deadlines I’m considering a grid of countdown timers. Each one could have custom backgrounds (colors, gradients, maybe even photos). I don’t want to apply visual changes to that background. I want to show something when the view is hovered–some sort of indicator that tapping the view will do something.

What I came up with is a way to show and hide an image/symbol when the parent view is hovered. This is a bit of a hack, but it works. I have a custom view for my grid cell. In that view, I added a namespace that we can use in the hover effects.

@Namespace private var hoverNamespace

This view is a ZStack with a shape and an image. On the image I added a hover effect using the namespace. Whenever any effects using this namespace are hovered, we’ll chang the opacity of this image.

Image(systemName: "star.fill")
  .hoverEffect(in: HoverEffectGroup(hoverNamespace)) { effect, isActive, _ in
    effect.opacity(isActive ? 1.0 : 0)
}

On the outer ZStack, I did something similar. I didn’t want to apply a visual change to the ZStack or shape, so I needed to return an effect that does nothing. The hacky part is that we can just return the effect.

ZStack {}
.hoverEffect(in: HoverEffectGroup(hoverNamespace)) { effect, _, _ in
  effect
}

Here it is in practice:

Lab Code

struct Lab050: View {
    var body: some View {
        HStack(spacing: 12) {
            ShapeHoverDemo(color: .stepRed)
            ShapeHoverDemo(color: .stepGreen)
            ShapeHoverDemo(color: .stepBlue)
        }
        .padding()
    }
}

private struct ShapeHoverDemo: View {

    // 1. Create a namespace
    @Namespace private var hoverNamespace

    @State var color: Color = .stepRed

    var body: some View {
        ZStack {
            Rectangle()
                .foregroundStyle(color)

            Image(systemName: "star.fill")
                .foregroundColor(.white)
                .font(.largeTitle)
                // 2. Use HoverEffectGroup with the namespace we defined above
                .hoverEffect(in: HoverEffectGroup(hoverNamespace)) { effect, isActive, proxy in
                    effect.opacity(isActive ? 1.0 : 0)
                }
        }
        // 2. Use HoverEffectGroup with the namespace we defined above
        .hoverEffect(in: HoverEffectGroup(hoverNamespace)) { effect, isActive, proxy in
            // Hack: just return the effect without applying it to any view
            // This let's us use the shape of the ZStack to receive hover focus, without applying a visual change to it
            effect
        }
        .aspectRatio(4/3, contentMode: .fit)
        .clipShape(.rect(cornerRadius: 24))

    }
}

Support our work so we can continue to bring you new examples and articles.

Questions or feedback?