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.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit