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.
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)
}
}
}
}
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.