Code sharing between iOS and watchOS

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?

Hi CatastropheZero,

  1. 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.

  2. 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.

  /// 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) }
  1. 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.

  2. 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) }
Code sharing between iOS and watchOS
 
 
Q