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.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

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

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

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

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35