I wanted to share data between a SwiftUI view (and its viewmodel) and a SpriteView.. specifically I wanted the SwiftUI view to see the touch co-ordinates of the SpriteView and update them in the interface and for a Button to update the position of a target in the SpriteView.. I'd been unable to find any solutions online, but quite a few people asking similar questions to me. Here's my solution.. (I'm a noob so please excuse the code, and if there are better ways of doing things please let me know!
ContentView:
import SwiftUI
import SpriteKit
struct ContentView: View {
@StateObject var contentViewModel = ContentViewModel()
@State private var pan: CGFloat = 0
@State private var tilt: CGFloat = 0
// var scene: SKScene {
// let scene = TargetScene()
// scene.size = CGSize(width: 500, height: 300)
// scene.scaleMode = .fill
// contentViewModel.currentTargetScene = scene as TargetScene
// return scene
// }
var body: some View {
ZStack {
Text("This is the view underneath: ")
.offset(CGSize(width: 0, height: -50))
HStack {
SpriteView(scene: contentViewModel.scene, options: [.allowsTransparency])
.frame(width: 500, height: 300)
.ignoresSafeArea()
.border(Color.blue)
VStack {
Spacer()
Button("Reset to centre") {
contentViewModel.moveToCentre()
}
.padding()
Button("move to co-ords ") {
contentViewModel.moveToLocation()
}
.padding()
Button("get location") {
contentViewModel.getLocation()
}
.padding()
Text("Pan: \(pan)")
Text("Tilt: \(tilt)")
Spacer()
}
.onReceive(contentViewModel.currentTargetScene!.pointPublisher, perform: { target in
pan = target.x
tilt = target.y
contentViewModel.pan = target.x
contentViewModel.tilt = target.y
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ContentViewModel:
import Combine
import Foundation
import SpriteKit
class ContentViewModel: ObservableObject {
var currentTargetScene: TargetScene?
var targetPosition = CGPoint(x: 0, y: 0)
var scene: SKScene
init(){
scene = TargetScene()
scene.size = CGSize(width: 500, height: 300)
scene.scaleMode = .fill
currentTargetScene = scene as? TargetScene
}
@Published var pan: CGFloat = 0
@Published var tilt: CGFloat = 0
func moveToCentre() {
// move target to centre of SpriteView
currentTargetScene?.moveToCentre()
}
func getLocation() {
// update ContentView with the target's x + y coordinates
targetPosition = currentTargetScene?.targetPosition ?? targetPosition
pan = targetPosition.x
tilt = targetPosition.y
print(targetPosition)
}
func moveToLocation() {
currentTargetScene?.moveToLocation(x: 100, y:100)
}
}
TargetScene:
import Foundation
import SpriteKit
import Combine
class TargetScene: SKScene, ObservableObject {
weak var contentViewModel: ContentViewModel?
var lastUpdateTime: TimeInterval = 0
var dt:TimeInterval = 0
let image = SKSpriteNode(imageNamed: "Target.png")
let imageSize = CGSize(width: 100, height: 100)
let targetMovePointsPerSec: CGFloat = 500.0
var velocity = CGPoint.zero
var lastTouchLocation: CGPoint?
@Published var pan: CGFloat = 0 {
didSet {
pointPublisher.send(CGPoint(x: self.pan, y: self.tilt))
}
}
var tilt: CGFloat = 0 {
didSet {
pointPublisher.send(CGPoint(x: self.pan, y: self.tilt))
}
}
@Published var targetPosition = CGPoint(x: 0, y: 0)
public let pointPublisher = CurrentValueSubject<CGPoint, Never>(CGPoint(x: 0, y: 0))
private var cancellableSet = Set<AnyCancellable>()
override init() {
super.init(size: CGSize(width: 500, height: 300))
pointPublisher
.sink(receiveValue: { [unowned self] target in
self.targetPosition = target
})
.store(in: &cancellableSet)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(to view: SKView) {
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
image.position = CGPoint(x: 0, y: 0)
image.scale(to: imageSize)
addChild(image)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// print(location)
pan = location.x
tilt = location.y
lastTouchLocation = location
moveTargetTowards(location: location)
updateLocation()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// print(location)
pan = location.x
tilt = location.y
lastTouchLocation = location
moveTargetTowards(location: location)
updateLocation()
}
override func sceneDidLoad() {
scene?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let backgroundColor: UIColor = UIColor(white: 0, alpha: 0.3)
scene?.backgroundColor = backgroundColor
}
override func update(_ currentTime: TimeInterval) {
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
} else {
dt = 0
}
lastUpdateTime = currentTime
//To move sprite in a direction
// moveTarget(sprite: image, velocity: CGPoint(x: targetMovePointsPerSec, y: 0))
if let lastTouchLocation = lastTouchLocation {
let diff = lastTouchLocation - image.position
if (sqrt(diff.x * diff.x + diff.y * diff.y) <= targetMovePointsPerSec * CGFloat(dt)) {
image.position = lastTouchLocation
velocity = CGPoint.zero
} else {
moveTarget(sprite: image, velocity: velocity)
}
}
}
func moveTarget(sprite: SKNode, velocity: CGPoint) {
let amountToMove = CGPoint(x: velocity.x * CGFloat(dt), y: velocity.y * CGFloat(dt))
sprite.position = CGPoint(x: sprite.position.x + amountToMove.x, y: sprite.position.y + amountToMove.y)
}
func moveTargetTowards(location: CGPoint) {
let offset = CGPoint(x: location.x - image.position.x, y: location.y - image.position.y)
let length = sqrt(Double(offset.x * offset.x + offset.y * offset.y))
let direction = CGPoint(x: offset.x / CGFloat(length), y: offset.y / CGFloat(length))
velocity = CGPoint(x: direction.x * targetMovePointsPerSec, y: direction.y * targetMovePointsPerSec)
}
func moveToCentre() {
let actionMove = SKAction.move(to: CGPoint(x: 0, y: 0), duration: 0.5)
image.run(actionMove)
self.pan = 0
self.tilt = 0
updateLocation()
}
func updateLocation() {
}
func moveToLocation(x: CGFloat, y: CGFloat) {
let actionMove = SKAction.move(to: CGPoint(x: x, y: y), duration: 0.5)
image.run(actionMove)
self.pan = x
self.tilt = y
updateLocation()
}
}
Hope this helps someone else out there!