swiftmacosswiftuisprite-kitspriteview

How do I send scroll wheel events into a SwiftUI SpriteView


I'm trying to migrate all my SpriteKit code into a SwiftUI SpriteView container. My program is a mix of SwiftUI and SpriteKit on MacOS to trade crypto.

The problem I'm having is that the scrollWheel(with event: NSEvent) function inside my SKScene object isn't getting triggered when I scroll my mouse wheel.

Previously I solved this by making the SKScene the nextResponder which worked great when I was using Cocoa.

Below is what my main entry point to SwiftUI looks like.

@main
struct ManualTradingApp: App {

    var exchangeManger: ExchangeManager = BinanceManager()
    
    var chartScene: ChartScene {
        let scene = ChartScene(size: CGSize(width: 600, height: 600), manager: exchangeManger)
        scene.scaleMode = .resizeFill
        return scene
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView(manager: exchangeManger, chartScene: chartScene)
        }
    }
}

Here is the function inside of the ChartScene class I made that is supposed to receive the scroll wheel events.

override func scrollWheel(with event: NSEvent) {
        
    if chartNode.contains(event.locationInWindow) {
        chartNode.scrollWheel(with: event)
    } else if chartNode2.contains(event.locationInWindow) {
        chartNode2.scrollWheel(with: event)
    }
        
    if symbolRowContainer.contains(event.locationInWindow) {
        moveSymbolContainer(deltaY: -event.scrollingDeltaY * 10.0)
    }
}

Solution

  • you can capture scrollWheel events via an NSView, then post them as notifications, which can be received by the SKScene.

    first add a notification observer to your SKScene

    class GameSceneScrollWheel: SKScene {
        override func didMove(to view: SKView) {
            NotificationCenter.default.addObserver(self,
                                                   selector: #selector(self.scrollWheelWithEvent(notification:)),
                                                   name: Notification.Name("scrollWheelWithEvent"),
                                                   object: nil)
        }
        
        //receive notification and passthrough to normal `scrollWheel` function
        @objc func scrollWheelWithEvent(notification: Notification) {
            guard let event = notification.object as? NSEvent else { return }
            self.scrollWheel(with:event)
        }
        
        override func scrollWheel(with event: NSEvent) {
            print("event.deltaY: \(event.deltaY)") //success!
        }
    }
    

    second, intercept events in NSView and post a notification

    struct ScrollWheelEventView : NSViewRepresentable {
        
        class ScrollWheelView : NSView {
            override var acceptsFirstResponder: Bool { true }
            override func acceptsFirstMouse(for event: NSEvent?) -> Bool { return true }
                    
            override func scrollWheel(with event: NSEvent) {
                NotificationCenter.default.post(name: Notification.Name("scrollWheelWithEvent"), object: event)
            }
        }
            
        func makeNSView(context: Context) -> some NSView {
            return ScrollWheelView()
        }
        
        func updateNSView(_ nsView: NSViewType, context: Context) {}
    }
    

    and finally, add the NSViewRepresentable to SwiftUI

    struct ScrollWheel: View {
        @State private var scene = GameSceneScrollWheel()
    
        var body: some View {
            ZStack {
                SpriteView(scene: scene)
                ScrollWheelEventView()
            }
        }
    }