TechnologiesSwiftUI

HoverPhase enum

iOSmacOStvOSwatchOSvisionOS✓ renders

The current hovering state and value of the pointer.

How it works

HoverPhase describes where the pointer is in relation to a view at a single moment during a continuous-hover interaction. Rather than reporting a simple boolean enter/exit, it captures the pointer's location while it remains over the view and signals the moment it leaves, so you can drive position-aware effects such as spotlights, tooltips, or magnification. Reach for HoverPhase whenever you need the pointer's coordinates throughout a hover, not just the fact that a hover began or ended.

  1. Receive phases from onContinuousHover(perform:)

    HoverPhase is delivered by the onContinuousHover modifier, which calls your closure each time the pointer moves over the view or finally exits it. In the example, .onContinuousHover { phase in self.phase = phase } stores every incoming HoverPhase into @State so the view can react to it.

  2. Read the pointer location from the active case

    The active(_:) case carries the pointer's current position as a CGPoint in the view's local coordinate space, letting you follow the cursor as it travels. The example pattern-matches case .active(let point) and reads point.x and point.y to build its label.

  3. Detect the exit with the ended case

    When the pointer leaves the view, HoverPhase reports the ended case, which carries no associated value and is your cue to tear down or reset any hover-driven state. Here case .ended returns the "ended" label, and the isActive helper returns false for it so the rectangle falls back to .gray.

  4. Store phase as @State to drive the view

    Because HoverPhase is an ordinary value you can hold and compare, keeping it in state lets SwiftUI recompute the body as the phase changes. The example seeds @State private var phase: HoverPhase = .ended and derives both isActive and label from it, switching the fill between .blue and .gray.

  5. Handle future cases with @unknown default

    HoverPhase is a non-frozen enum, so SwiftUI may add cases in later releases; switching over it should include an @unknown default to stay forward-compatible. The label computed property does exactly this, returning "unknown" for any phase it doesn't explicitly handle.

Try it — In the active(_:) branch of label, swap the rounded coordinates for the raw point and append it elsewhere on screen so the box follows the cursor, revealing how active(_:) streams a fresh CGPoint on every pointer move.

Example & preview

Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.

HoverPhase.swift
struct HoverPhaseDemo: View {
    @State private var phase: HoverPhase = .ended

    var body: some View {
        VStack(spacing: 12) {
            Text("Hover over the box")
                .font(.headline)
            Text(label)
                .font(.title3)
                .foregroundStyle(.secondary)
            RoundedRectangle(cornerRadius: 12)
                .fill(isActive ? .blue : .gray)
                .frame(width: 160, height: 100)
                .onContinuousHover { phase in
                    self.phase = phase
                }
        }
        .padding()
    }

    private var isActive: Bool {
        if case .active = phase { return true }
        return false
    }

    private var label: String {
        switch phase {
        case .active(let point):
            return "active at (\(Int(point.x)), \(Int(point.y)))"
        case .ended:
            return "ended"
        @unknown default:
            return "unknown"
        }
    }
}
Live preview
Hover over the box label
Hover over the box label
swift → lexer → parser → sema → uiir → canvas Open in Studio ↗
What's new in SwiftUI 27 →