There is this excellent article https://swiftui-lab.com/a-powerful-combo/ which demonstrates how to track mouse movement.
For this purpose, the author uses NSViewRepresentable and NSHostingView to get access to AppKit's NSView.
Instead of returning the mouse positions, we could return a boolean value indicating whether the mouse is inside the path of the Shape.
Since our content is a Shape and not a View, the where clauses need to be adjusted accordingly: where Content : Shape.
The solution uses NSTrackingArea with the .mouseMoved option. It makes sense to add the .mouseEnteredAndExited option in addition to ensure that the non-hovered state is restored when the mouse leaves the view.
To connect a Shape to the TrackingAreaView on the one hand and to the HoverViewModifier on the other hand, extensions to Shape can be created. Also the @State in the HooverViewModifier must be replaced by an @ObservedObject to be able to access it from the Shape. A corresponding ObservableObject can simply look like this:
class HoverModel: ObservableObject {
@Published var hovered: Bool = false
}
The extension could look like this:
extension Shape {
func hover(modifier: HoverViewModifier) -> some View {
return self.mouse { inside in
modifier.hoverModel.hovered = inside
}
.modifier(modifier)
}
func mouse(insideShape: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: insideShape) { self }
}
}
Accordingly, the calls must be changed slightly:
Circle().hover(modifier: HoverViewModifier())
MyStar().hover(modifier: HoverViewModifier())
A complete example with the adapted code from the question combined with the slightly extended example from the article mentioned at the beginning could then look something like this:
import SwiftUI
class HoverModel: ObservableObject {
@Published var hovered: Bool = false
}
struct HoverViewModifier : ViewModifier {
@ObservedObject var hoverModel = HoverModel()
func body(content: Content) -> some View {
content
.foregroundColor(hoverModel.hovered ? .accentColor : .primary)
}
}
extension Shape {
func hover(modifier: HoverViewModifier) -> some View {
return self.mouse { inside in
modifier.hoverModel.hovered = inside
}
.modifier(modifier)
}
func mouse(insideShape: @escaping (Bool) -> Void) -> some View {
TrackingAreaView(insideShape: insideShape) { self }
}
}
struct MyStar : Shape {
func path(in rect : CGRect) -> Path {
let points = [
(50, 0), (61, 33), (97, 34), (69, 56), (79, 90), (50, 70), (20, 90), (30, 56), ( 2, 34), (38, 33)
].map { (x : CGFloat, y : CGFloat) in
CGPoint(x: x * rect.width / 100, y: y * rect.height / 100)
}
var path = Path()
path.addLines(points)
return path
}
}
struct ContentView : View {
var body: some View {
HStack {
Circle().hover(modifier: HoverViewModifier())
MyStar().hover(modifier: HoverViewModifier())
}.frame(width: 200, height: 100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct TrackingAreaView<Content>: View where Content : Shape {
let insideShape: (Bool) -> Void
let content: () -> Content
init(insideShape: @escaping (Bool) -> Void, @ViewBuilder content: @escaping () -> Content) {
self.insideShape = insideShape
self.content = content
}
var body: some View {
TrackingAreaRepresentable(insideShape: insideShape, content: self.content())
}
}
struct TrackingAreaRepresentable<Content>: NSViewRepresentable where Content: Shape {
let insideShape: (Bool) -> Void
let content: Content
func makeNSView(context: Context) -> NSHostingView<Content> {
return TrackingNSHostingView(insideShape: insideShape, rootView: self.content)
}
func updateNSView(_ nsView: NSHostingView<Content>, context: Context) {
}
}
class TrackingNSHostingView<Content>: NSHostingView<Content> where Content : Shape {
let insideShape: (Bool) -> Void
var path = Path()
init(insideShape: @escaping (Bool) -> Void, rootView: Content) {
self.insideShape = insideShape
super.init(rootView: rootView)
setupTrackingArea()
}
override func layout() {
super.layout()
self.path = rootView.path(in: self.bounds)
}
required init(rootView: Content) {
fatalError("init(rootView:) has not been implemented")
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupTrackingArea() {
let options: NSTrackingArea.Options = [.mouseMoved, .mouseEnteredAndExited, .activeAlways, .inVisibleRect]
self.addTrackingArea(NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil))
}
override func mouseExited(with event: NSEvent) {
self.insideShape(false)
}
override func mouseMoved(with event: NSEvent) {
return self.checkInside(with: event)
}
override func mouseEntered(with event: NSEvent) {
return self.checkInside(with: event)
}
private func checkInside(with event: NSEvent) {
let inside = path.contains(self.convert(event.locationInWindow, from: nil))
self.insideShape(inside)
}
}
Demo
