1
votes

How can we have a custom mouse pointer one a view using SwiftUI + iPadOS?

Apple has an example of how to do that in this code but is for UIKit: https://developer.apple.com/documentation/uikit/pointer_interactions/enhancing_your_ipad_app_with_pointer_interactions

class QuiltView: UIView, UIPointerInteractionDelegate {


...
    


func sharedInit() {
    ...

    // Add a pointer interaction for the "editor" area Quilt view.
    self.addInteraction(UIPointerInteraction(delegate: self))
}

// Supply custom regions that correspond to grid lines when "useStraightLineStitch" is enabled.
func pointerInteraction(_ interaction: UIPointerInteraction,
                        regionFor request: UIPointerRegionRequest,
                        defaultRegion: UIPointerRegion) -> UIPointerRegion? {
    if useStraightLineStitch {
        let regionNumber = floor(request.location.y / gridHeight)
        return UIPointerRegion(rect: CGRect(x: 0, y: regionNumber * gridHeight, width: self.bounds.size.width, height: gridHeight))
    } else {
        return defaultRegion
    }
}

// Supply pointer style with custom crosshair shape.
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
    let crosshair = UIPointerShape.path(crosshairBezierPath())

    if useStraightLineStitch {
        return UIPointerStyle(shape: crosshair, constrainedAxes: [.vertical])
    } else {
        return UIPointerStyle(shape: crosshair)
    }
}

func crosshairBezierPath() -> UIBezierPath {
    let crosshairPath = UIBezierPath()
    crosshairPath.move(to: CGPoint(x: -1.5, y: 1.5))
    crosshairPath.addLine(to: CGPoint(x: -1.5, y: 10))
    crosshairPath.addArc(withCenter: CGPoint(x: 0, y: 10),
                         radius: 1.5,
                         startAngle: CGFloat.pi,
                         endAngle: 0,
                         clockwise: false)
    crosshairPath.addLine(to: CGPoint(x: 1.5, y: 1.5))
    crosshairPath.addLine(to: CGPoint(x: 10, y: 1.5))
    crosshairPath.addArc(withCenter: CGPoint(x: 10, y: 0),
                         radius: 1.5,
                         startAngle: CGFloat.pi / 2.0,
                         endAngle: CGFloat.pi * 1.5,
                         clockwise: false)
    crosshairPath.addLine(to: CGPoint(x: 1.5, y: -1.5))
    crosshairPath.addLine(to: CGPoint(x: 1.5, y: -10))
    crosshairPath.addArc(withCenter: CGPoint(x: 0, y: -10),
                         radius: 1.5,
                         startAngle: 0,
                         endAngle: CGFloat.pi,
                         clockwise: false)
    crosshairPath.addLine(to: CGPoint(x: -1.5, y: -1.5))
    crosshairPath.addLine(to: CGPoint(x: -10, y: -1.5))
    crosshairPath.addArc(withCenter: CGPoint(x: -10, y: 0),
                         radius: 1.5,
                         startAngle: CGFloat.pi * 1.5,
                         endAngle: CGFloat.pi / 2.0,
                         clockwise: false)
    crosshairPath.close()
    return crosshairPath
}


...

}

What I'm looking for is to achieve the same but using SwiftUI? thanks in advance

1

1 Answers

1
votes

If you want to do this with Apple's current APIs, I don't believe that is something you can do. Hopefully such a feature will come in a future release.

However, you can still enable this feature by doing some Introspection.

First, a warning: While common UIKit elements back SwiftUI views on iOS, this is not guaranteed to be true in all cases. Therefore, this solution may break at some point in the future.

Now, to a possible approach.

Add an introspection package to your app. In the past, I have used SwiftUI-Introspect (to add a UIDropInteraction to my views). Then import the Introspect module into your source file.

Most introspect packages, such as this one, include View extensions that let you examine the underlying UIKit components. Depending on what your view is, you'll use a specific modifier.

For instance, if you want to get the underlying view controller for a specific view, you would use the .introspectViewController() modifier. You provide it a closure that will be given the view controller. You can add your interaction to the view controller's view. (If you have a scroll view, use the introspectScrollView modifier, etc)

Example, assuming a class named PointerDelegate that conforms to UIPointerInteractionDelegate:

import SwiftUI
import Introspect

struct YourView: View {

  var body: some View {
    // Your view hierarchy
    // For instance
    List { ... }
      .introspectViewController { viewController in 
        viewController.view.addInteraction(UIPointerInteraction(delegate: PointerDelegate()))
      }
  }
}

This is a bit of a hack, and as warned earlier may break in the future. However, this is a way to gain access to some features that are only enabled in UIKit from within your SwiftUI apps.