WWDC24 deal: 30% off our Swift and SwiftUI books! Learn more ...WWDC24 deal:30% off our Swift and SwiftUI books >>

Tracking hover location in SwiftUI

For a while we only had onHover(perform:) modifier in SwiftUI that is called when the user moves the pointer over or away from the view’s frame. There used to be no official way to continuously track the pointer location. This changed with the introduction of onContinuousHover(coordinateSpace:perform:) in macOS 13 and iPadOS 16.

The new modifier lets us read the current HoverPhase and reports the exact location of the pointer when it's within the view's bounds. Let's see it in action.

# Read hover phase and pointer location

For this example, we are going to define a simple rounded rectangle and apply onContinuousHover() to it. The active phase of the hover contains the pointer location, we are going to save it into a State variable. We are also going to register when the pointer exists the view.

struct ContentView: View {
    @State private var hoverLocation: CGPoint = .zero
    @State private var isHovering = false
    
    var body: some View {
        RoundedRectangle(cornerRadius: 20, style: .continuous)
            .fill(.indigo)
            .frame(width: 400, height: 300)
            .onContinuousHover { phase in
                switch phase {
                case .active(let location):
                    hoverLocation = location
                    isHovering = true
                case .ended:
                    isHovering = false
                }
            }
    }
}

To check whether our code is working as expected we are going to place a Text view with the coordinates of the pointer in an overlay. The text will only be shown when the user is hovering over the rectangle.

struct ContentView: View {
    @State private var hoverLocation: CGPoint = .zero
    @State private var isHovering = false
    
    var body: some View {
        RoundedRectangle(cornerRadius: 20, style: .continuous)
            .fill(.indigo)
            .frame(width: 400, height: 300)
            .onContinuousHover { phase in
                switch phase {
                case .active(let location):
                    hoverLocation = location
                    isHovering = true
                case .ended:
                    isHovering = false
                }
            }
            .overlay {
                if isHovering {
                    Text("x: \(hoverLocation.x), y: \(hoverLocation.y)")
                        .foregroundColor(.white)
                        .font(.title)
                }
            }
    }
}

We will see that the text is reporting the x and y coordinates of the pointer in the local coordinate space of the RoundedRectangle view.

A gif showing that text on top of the rounded rectangle displays the x and y coordinates of the cursor

The local coordinate space is the default for onContinuousHover(perform:). We can specify global or named space instead if necessary.

# Add a circle in the pointer location

To demonstrate how we can use the information about the pointer coordinates, we are going to place a small circle at the current location of the cursor. The circle's position will have the coordinates of the hover location.

struct ContentView: View {
    @State private var hoverLocation: CGPoint = .zero
    @State private var isHovering = false
    
    var body: some View {
        RoundedRectangle(cornerRadius: 20, style: .continuous)
            .fill(.indigo)
            .frame(width: 400, height: 300)
            .onContinuousHover { phase in
                switch phase {
                case .active(let location):
                    hoverLocation = location
                    isHovering = true
                case .ended:
                    isHovering = false
                }
            }
            .overlay {
                if isHovering {
                    Circle()
                        .fill(.white)
                        .opacity(0.5)
                        .frame(width: 30, height: 30)
                        .position(x: hoverLocation.x, y: hoverLocation.y)
                }
            }
    }
}
A gif showing a small white circle added under the cursor that moves together with the cursor

Notice that because we are placing the circle in an overlay of the rounded rectangle, we can simply use the hover coordinates given to us in the local coordinate space of the rectangle. In other cases, you might need to use the global coordinate space or specify a named one.

Books by Natalia PanferovaBooks by Natalia Panferova
WWDC24: 30% off all books!
  • Swift Gems

    100+ tips to take your Swift code to the next level

  • Integrating SwiftUI into UIKit Apps

    A detailed guide on gradually adopting SwiftUI in UIKit projects

The offer is active until the 16th of June.