What's the recommended pattern for sharing a complex SpriteKit game scene (and basic game code) between iOS and watchOS targets without code duplication, given the platform differences in input and rendering?
-
For the hosting view, use SpriteKit's SwiftUI SpriteView, which renders an SKScene and works on both iOS and watchOS — so you avoid the platform-specific SKView (iOS) vs. WKInterfaceSKScene (watchOS) split.
-
For input, SwiftUI supports the same basic tap/drag gestures on watchOS as on iOS, though you'll often wire them to different in-game actions per platform. There are also inputs unique to watchOS — most notably the Digital Crown via. digitalCrownRotation. To support both cleanly, keep the game logic platform-agnostic and feed it semantic intents rather than raw input. The shared engine never references a touch or the crown — each platform translates its own input into the same intents.
e.g.
/// The platform-agnostic input vocabulary. Every platform maps its own
/// hardware (touches, drags, Digital Crown) onto these cases.
public enum GameIntent {
case aim(at: CGPoint) // a scene-space point (e.g. iOS drag/tap location)
case moveHorizontal(Double) // a relative nudge (e.g. watchOS Digital Crown delta)
case primaryAction // fire / jump / select — a tap on either platform
}
public final class GameScene: SKScene {
private let player = SKShapeNode(circleOfRadius: 20)
private var targetX: CGFloat?
public override func didMove(to view: SKView) { ... }
/// The ONE entry point for input. Platforms call this; the scene never
/// touches UIResponder, gestures, or the crown directly.
public func apply(_ intent: GameIntent) {
switch intent {
case .aim(let p): targetX = p.x
case .moveHorizontal(let d): targetX = (targetX ?? player.position.x) + CGFloat(d) * 8
case .primaryAction: fire()
}
}
public override func update(_ currentTime: TimeInterval) { ... }
private func fire() { ... }
}
===================================================================================
// iOS — touches → intents
SpriteView(scene: scene)
.gesture(DragGesture(minimumDistance: 0)
.onChanged { scene.apply(.aim(at: scene.convertPoint(fromView: $0.location))) }
.onEnded { _ in scene.apply(.primaryAction) })
// watchOS — Digital Crown + tap → the SAME intents
SpriteView(scene: scene)
.focusable()
.digitalCrownRotation($crown)
.onChange(of: crown) { _, new in scene.apply(.moveHorizontal(new - last)); last = new }
.onTapGesture { scene.apply(.primaryAction) }