Show SpriteKit score in SwiftUI View - swiftui

I am trying to make a ZStack that will overlay the Score from my SpriteKit scene. I got the following code right now, this works showing the code within the actually scene, but I wanna show it in the View
import SwiftUI
import SpriteKit
var gameScore = 0
class GameScene: SKScene, SKPhysicsContactDelegate {
let removeLabel = SKAction.sequence([SKAction.fadeIn(withDuration: 0.3), SKAction.wait(forDuration: 0.8), SKAction.fadeOut(withDuration: 0.3)])
override func sceneDidLoad() {
super.sceneDidLoad()
}
Here is the function:
// MARK: - Add Score
func addScore(){
if gameScore < 250 {
gameScore += 1
scoreLabel.text = String(format: "%06d", gameScore)
let possibleScores: Set = [10, 20, 30, 40, 50, 65, 80, 95, 110, 125, 150, 175, 200, 250]
if possibleScores.contains(gameScore) {
startNewLevel()
}
} else {
gameScore += 2
scoreLabel.text = String(format: "%06d", gameScore)
let possibleScores: Set = [10, 20, 30, 40, 50, 65, 80, 95, 110, 125, 150, 175, 200, 250]
if possibleScores.contains(gameScore) {
startNewLevel()
}
}
}
My View code looks like this:
import SwiftUI
import SpriteKit
struct PageTwo: View {
#State var gameScore = 0
var body: some View {
ZStack {
GameView()
ZStack {
Text("Score: \(gameScore)")
.foregroundColor(.white)
}
}
}
}
It is showing the Score, but not counting it, so maybe someone can tell me where I am going wrong here? This SpriteKit + SwiftUI is new to me and still not quite got it.

You need to use ObservableObject and a publisher for that, I looked to your codes, there is some code source missing, how ever it is an example for you:
class GameScene: SKScene, SKPhysicsContactDelegate, ObservableObject { // <<: Here 1
#Published var gameScore = 0 // <<: Here 2
let removeLabel = SKAction.sequence([SKAction.fadeIn(withDuration: 0.3), SKAction.wait(forDuration: 0.8), SKAction.fadeOut(withDuration: 0.3)])
override func sceneDidLoad() {
super.sceneDidLoad()
}
func addScore(){
if gameScore < 250 {
gameScore += 1
} else {
gameScore += 2
}
}
}
struct PageTwo: View {
#StateObject var gameScene: GameScene = GameScene() // <<: Here 3
var body: some View {
Text("Score: \(gameScene.gameScore)") // <<: Here 4
.onTapGesture {
gameScene.addScore() // <<: Here 5
}
}
}

Related

How to use picker as Int in qr code generator with swiftui

I am new to swift and i am using swiftui to create a qr code generator. I want to use a picker to select number and it shows in the qr code, when i finished the picker and i was trying to add function as qr code generator and the integer needs to be convert to string, so it can be used in the function. How can i make the function work with Integer?
Here is my code which i tried:
I used age as Integer with picker
import Foundation
import SwiftUI
import CoreImage.CIFilterBuiltins
struct QRCode: View {
#State private var selectedAge = 1
let age = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
#State var navigated = false
var body: some View {
NavigationView{
VStack{
Picker(“Select Age”, selection: $selectedAge) {
ForEach(age,id:\.self) {
ages in Text(“\(ages)”) }
}
}
}
}
}
//Below is the other view with the function
import Foundation
import SwiftUI
import CoreImage.CIFilterBuiltins
struct Generate: View {
#Binding var Ages: Int
let filter = CIFilter.qrCodeGenerator()
let cont = CIContext()
var body: some View {
NavigationView{
Image(uiImage: GenerateQRCode(old: old))
.interpolation(.none)
.resizable()
.frame(width: 150, height: 150, alignment: .center)
}
}
func GenerateQRCode(old: Int)-> UIImage {
let com = Ages
let con = com.data(using: .uft8)
filter.setValue(con, forKey: “inputMessage”)
if let qr = filter.outputImage {
if let qrImage = cont.createCGImage(qr, from: qr.extent) {
return UIImage(cgImage: qrImage)
}
}
return UIImage(systemName: “xmark”) ?? UIImage()
}
}
Try this simple approach to "...make the function work with Integer".
Note the name change ages/age for clarity.
struct ContentView: View {
var body: some View {
QRCode()
}
}
struct QRCode: View {
#State private var selectedAge = 1
let ages = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] // <-- ages
#State var navigated = false
var body: some View {
NavigationView {
VStack {
Picker("Select Age", selection: $selectedAge) {
ForEach(ages, id: \.self) { age in
Text("\(age)")
}
}.pickerStyle(.wheel) // <-- for testing
Generate(age: $selectedAge)
}
}
}
}
struct Generate: View {
#Binding var age: Int
let filter = CIFilter.qrCodeGenerator()
let cont = CIContext()
var body: some View {
Image(uiImage: GenerateQRCode(old: age))
.interpolation(.none)
.resizable()
.frame(width: 150, height: 150, alignment: .center)
}
func GenerateQRCode(old: Int)-> UIImage {
let data = String(old).data(using: .utf8) // <-- here
filter.setValue(data, forKey: "inputMessage") // <-- here
if let qr = filter.outputImage {
if let qrImage = cont.createCGImage(qr, from: qr.extent) {
return UIImage(cgImage: qrImage)
}
}
return UIImage(systemName: "xmark") ?? UIImage()
}
}

Animate changes to Array - SwiftUI ForEach

I'm new to SwiftUI coming from UIKit. I'm trying to achieve the following:
Given an array of mutable states in a view model that represents current progress to be displayed on a Circle using trim(from:to:)
Loop over each item to render a circle with the current progress and animate changes from the old to the new position
A view model will periodically update the array that is a #Published value in an ObservableObject.
I setup a demo view that looks like this
#StateObject var demoViewModel: DemoViewModel = DemoViewModel()
var body: some View {
ForEach(demoViewModel.progress, id: \.self) { progress in
Circle()
.trim(from: 0, to: progress.progress)
.stroke(style: StrokeStyle(lineWidth: 15, lineCap: .round))
.foregroundColor(.red)
.frame(width: 100)
.rotationEffect(.degrees(-90))
.animation(.linear(duration: 1), value: demoViewModel.progress)
}
}
and a test view model like this
class DemoViewModel: ObservableObject {
#Published var progress: [Demo] = [.init(progress: 0)]
struct Demo: Hashable {
let progress: Double
}
init() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
let current = self.progress[0]
if current.progress < 1 {
self.progress[0] = .init(progress: current.progress + 0.1)
}
}
}
}
For the sake of the demo the array only has one item. What I expected was on each iteration a new value is written to the item in the array which triggers the animation to animate to that value. What actually happens is the progress updates, but it jumps to each new value for no animation.
Try this approach, where the self.demos[0].progress in init()
is updated rather than creating a new Demo(...) at each time tick.
Note, modified a few other things as well, such as the .animation(...value:..) and in particular the ForEach loop with the added Demo unique id.
struct ContentView: View {
#StateObject var demoViewModel: DemoViewModel = DemoViewModel()
var body: some View {
VStack {
ForEach(demoViewModel.demos) { demo in // <-- here
Circle()
.trim(from: 0, to: demo.progress)
.stroke(style: StrokeStyle(lineWidth: 15, lineCap: .round))
.foregroundColor(.red)
.frame(width: 100)
.rotationEffect(.degrees(-90))
.animation(.linear(duration: 1), value: demo.progress) // <-- here
}
}
}
}
class DemoViewModel: ObservableObject {
#Published var demos:[Demo] = [Demo(progress: 0)]
struct Demo: Identifiable, Hashable { // <-- here
let id = UUID() // <-- here
var progress: Double // <-- here
}
init() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
if self.demos[0].progress < 1 {
self.demos[0].progress = self.demos[0].progress + 0.1 // <-- here
}
}
}
}

How do you handle onChange() of #EnvironmentObject in an SKScene, and not break the scene rendering?

In the test code below the blue ellipse is drawn, but the red ellipse is not drawn. Is there a way to handle the onChange and call a drawing function in a SpriteKit scene? (The print statement inside handleDataChange() does print to the console.)
The gameEnvData is being changed in a separate scene.
import SwiftUI
import SpriteKit
struct NextPieceView: View {
#EnvironmentObject var gameEnvData: GameData
var scene: NextPieceScene {
let scene = NextPieceScene()
scene.size = CGSize(width: 200, height: 100)
scene.gameData = gameEnvData
return scene
}
var body: some View {
SpriteView(scene: scene)
.onChange(of: gameEnvData.pieceCount, perform: { _ in
scene.handleDataChange()
})
}
}
import SpriteKit
class NextPieceScene: SKScene {
var gameData: GameData = GameData()
override func didMove(to view: SKView) {
drawTestShape(position: CGPoint(x: 25, y: 50), color: .blue) // this works
}
func handleDataChange() {
print("handleDataChange called")
drawTestShape(position: CGPoint(x: 75, y: 50), color: .red) // does not draw
}
func drawTestShape(position: CGPoint, color: UIColor) {
let shapeNode = SKShapeNode(ellipseOf: CGSize(width: 40, height: 40))
shapeNode.fillColor = color
shapeNode.position = position
addChild(shapeNode)
}
}
Fixes:
Used the #State wrapper when declaring the scene
The scene could no longer be a computed property, due to wrapper
Now passing the #EO gameEnvData in via the onChange handler
struct NextPieceView: View {
#EnvironmentObject var gameEnvData: GameData
#State var scene: NextPieceScene = NextPieceScene(
size: CGSize(width: 200, height: 100))
var body: some View {
SpriteView(scene: scene)
.onChange(of: gameEnvData.pieceCount, perform: { _ in
scene.handleDataChange(gameEnvData: gameEnvData) })
}
}

How to animate a view in a circular motion using its real-time position coordinates?

I'm currently working on a SwiftUI project, and in order to detect intersections/collisions, I need real-time coordinates, which SwiftUI animations cannot offer. After doing some research, I came across a wonderful question by Kike regarding how to get the real-time coordinates of a view when it is moving/transitioning. And Pylyp Dukhov's answer to that topic recommended utilizing CADisplayLink to calculate the position for each frame and provided a workable solution that did return the real time values when transitioning.
But I'm so unfamiliar with CADisplayLink and creating custom animations that I'm not sure I'll be able to bend it to function the way I want it to.
So this is the animation I want to achieve using CADisplayLink that animates the orange circle view in a circular motion using its position coordinates and repeats forever:
Here is the SwiftUI code:
struct CircleView: View {
#Binding var moveClockwise: Bool
#Binding var duration: Double // Works as speed, since it repeats forever
let geo: GeometryProxy
var body: some View {
ZStack {
Circle()
.stroke()
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
//MARK: - What I have with SwiftUI animation
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
.offset(x: -CGFloat(geo.size.width / 2))
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(
.linear(duration: duration)
.repeatForever(autoreverses: false), value: moveClockwise
)
//MARK: - What I need with CADisplayLink
// Circle()
// .fill(.orange)
// .frame(width: 35, height: 35, alignment: .center)
// .position(CGPoint(x: pos.realTimeX, y: realTimeY))
Button("Start Clockwise") {
moveClockwise = true
// pos.startMovement
}.foregroundColor(.orange)
}.fixedSize()
}
}
struct ContentView: View {
#State private var moveClockwise = false
#State private var duration = 2.0 // Works as speed, since it repeats forever
var body: some View {
VStack {
GeometryReader { geo in
CircleView(moveClockwise: $moveClockwise, duration: $duration, geo: geo)
}
}.padding(20)
}
}
This is what I have currently with CADisplayLink, I added the coordinates to make a circle and that’s about it & it doesn’t repeat forever like the gif does:
Here is the CADisplayLink + real-time coordinate version that I’ve tackled and got lost:
struct Point: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
}
}
struct ContentView: View {
#StateObject var P: Position = Position()
var body: some View {
VStack {
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width, alignment: .center)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
Text("X: \(P.realtimePosition.x), Y: \(P.realtimePosition.y)")
}.onAppear() {
P.startMovement()
}
}
}
class Position: ObservableObject, Equatable {
struct AnimationInfo {
let startDate: Date
let duration: TimeInterval
let startPoint: CGPoint
let endPoint: CGPoint
func point(at date: Date) -> (point: CGPoint, finished: Bool) {
let progress = CGFloat(max(0, min(1, date.timeIntervalSince(startDate) / duration)))
return (
point: CGPoint(
x: startPoint.x + (endPoint.x - startPoint.x) * progress,
y: startPoint.y + (endPoint.y - startPoint.y) * progress
),
finished: progress == 1
)
}
}
#Published var realtimePosition = CGPoint.zero
private var mainTimer: Timer = Timer()
private var executedTimes: Int = 0
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
return displayLink
}()
private let animationDuration: TimeInterval = 0.1
private var animationInfo: AnimationInfo?
private var coordinatesPoints: [CGPoint] {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
// great progress haha
let radius: Double = Double(screenWidth / 2)
let center = CGPoint(x: screenWidth / 2, y: screenHeight / 2)
var coordinates: [CGPoint] = []
for i in stride(from: 1, to: 360, by: 10) {
let radians = Double(i) * Double.pi / 180 // raiments = degrees * pI / 180
let x = Double(center.x) + radius * cos(radians)
let y = Double(center.y) + radius * sin(radians)
coordinates.append(CGPoint(x: x, y: y))
}
return coordinates
}
// Conform to Equatable protocol
static func ==(lhs: Position, rhs: Position) -> Bool {
// not sure why would you need Equatable for an observable object?
// this is not how it determines changes to update the view
if lhs.realtimePosition == rhs.realtimePosition {
return true
}
return false
}
func startMovement() {
mainTimer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(movePoint),
userInfo: nil,
repeats: true
)
}
#objc func movePoint() {
if (executedTimes == coordinatesPoints.count) {
mainTimer.invalidate()
return
}
animationInfo = AnimationInfo(
startDate: Date(),
duration: animationDuration,
startPoint: realtimePosition,
endPoint: coordinatesPoints[executedTimes]
)
displayLink.isPaused = false
executedTimes += 1
}
#objc func displayLinkAction() {
guard
let (point, finished) = animationInfo?.point(at: Date())
else {
displayLink.isPaused = true
return
}
realtimePosition = point
if finished {
displayLink.isPaused = true
animationInfo = nil
}
}
}
Inside Position you're calculating position related to whole screen. But .position modifier requires value related to the parent view size.
You need to make your calculations based on the parent size, you can use such sizeReader for this purpose:
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry in
Color.clear
.onAppear {
block(geometry.size)
}
.onChange(of: geometry.size, perform: block)
}
)
}
}
Usage:
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
.sizeReader { size in
P.containerSize = size
}
Also CADisplayLink is not used in the right way. The whole point of this tool is that it's already called on each frame, so you can calculate real time position, so your animation is gonna be really smooth, and you don't need a timer or pre-calculated values for only 180(or any other number) positions.
In the linked answer timer was used because a delay was needed between animations, but in your case the code can be greatly simplified:
class Position: ObservableObject {
#Published var realtimePosition = CGPoint.zero
var containerSize: CGSize?
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
displayLink.isPaused = true
return displayLink
}()
private var startDate: Date?
func startMovement() {
startDate = Date()
displayLink.isPaused = false
}
let animationDuration: TimeInterval = 5
#objc func displayLinkAction() {
guard
let containerSize = containerSize,
let timePassed = startDate?.timeIntervalSinceNow,
case let progress = -timePassed / animationDuration,
progress <= 1
else {
displayLink.isPaused = true
startDate = nil
return
}
let frame = CGRect(origin: .zero, size: containerSize)
let radius = frame.midX
let radians = CGFloat(progress) * 2 * .pi
realtimePosition = CGPoint(
x: frame.midX + radius * cos(radians),
y: frame.midY + radius * sin(radians)
)
}
}
I've tried to make more simplified the implementation, here is the SwiftUI code,
struct RotatingDotAnimation: View {
#State private var moveClockwise = false
#State private var duration = 1.0 // Works as speed, since it repeats forever
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
value: moveClockwise
)
}
.onAppear {
self.moveClockwise.toggle()
}
}
}
It'll basically create animation like this,
enter image description here

SwiftUI Button if Statement

I have the following problem:
I built an app that has 6 buttons and each of these buttons is assigned a CL region.
I have a GPS track file that provides the app with locations and simulates a walk.
Now buttons should turn yellow when the region assigned to them is reached. It works so far, but the problem is that I don't know where to ask whether or not to reach the Region of Button.
I tried .onAppear () {if ...}, but that is only triggered once and I need something that monitors the If statement all the time.
Is there anything?
Thanks in advance
here my code:
import MapKit
import SwiftUI
import CoreLocation
var region1 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505088, longitude: 13.333574), radius: 20, identifier: "region1")
var region2 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504829, longitude: 13.336798), radius: 20, identifier: "region2")
var region3 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504733, longitude: 13.341834), radius: 20, identifier: "region3")
var region4 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.503312, longitude: 13.347718), radius: 20, identifier: "region4")
var region5 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505699, longitude: 13.352198), radius: 20, identifier: "region5")
var region6 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.513517, longitude: 13.350253), radius: 20, identifier: "region6")
class LocationEnviroment: NSObject, CLLocationManagerDelegate, ObservableObject{
var locationManager = CLLocationManager()
#Published var currentPosition = ""
var Waypoints = [region1, region2, region3, region4, region5, region6];
#Published var RegionIndex: Int = 6
func initPositionService(){
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
func isWayPoint(mylocation : CLLocation) -> Int{
for i in self.Waypoints{
if(i == mylocation){
if(i.identifier == "region1"){
return 0
}else if(i.identifier == "region2"){
return 1
}else if(i.identifier == "region3"){
return 2
}else if(i.identifier == "region4"){
return 3
}else if(i.identifier == "region5"){
return 4
}else if(i.identifier == "region6"){
return 5
}
}
}
return 6 // <- nothing found
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let myLocation = locations.last
let lat = myLocation?.coordinate.latitude
let lon = myLocation?.coordinate.longitude
self.currentPosition = "lat: \(lat ?? 0) lon: \(lon ?? 0)"
self.RegionIndex = isWayPoint(mylocation: myLocation!)
}
}
struct ContentView: View {
#StateObject var locationEnviroment = LocationEnviroment()
var Todo = ["Friseur / Haare schneiden", "Dönerbude / zu Mittag essen", "Bank / Geld abheben", "Supermarkt / einkaufen", "Blumeneladen /\n Blumen kaufen", " Lisa's Wohnung /\n Lisa Blumen überreichen" ]
#State private var disabled = Array(repeating: true, count: 6)
#State private var red = 255.0
#State private var blue = 0.0
#State private var green = 0.0
var body: some View {
VStack(alignment: .center, spacing: 10){
Text(locationEnviroment.currentPosition)
.padding()
ForEach(0..<6) { row in
Button(action : {
if(disabled[row] == false){
red = 0.0
green = 255.0
blue = 0.0
}
}) {
Text(Todo[row])
.frame(width : 200, height: 40, alignment: .center)
.clipShape(Rectangle())
.padding()
.border(Color.black, width: 1)
.font(.system(size: 15))
.foregroundColor(.black)
}
.background(Color(red: red/255, green: green/255, blue: blue/255))
.lineLimit(nil)
.cornerRadius(8.0)
.disabled(disabled[row])
.onAppear(){
if(locationEnviroment.RegionIndex == row){
disabled[row] = false
red = 255.0
green = 255.0
blue = 0.0
}
}
}
}
.onAppear(){
locationEnviroment.initPositionService()
}
}
}
You can use onChange to monitor the status of RegionIndex. It might look like this (replacing your onAppear):
.onChange(of: locationEnviroment.RegionIndex) { regionIndex in
if regionIndex == row {
disabled[row] = false
red = 255.0
green = 255.0
blue = 0.0
}
}
A couple of side notes:
Generally in Swift, variable and property names start with lowercase letters
You may want to investigate using computed properties to change/determine the colors. I started writing a solution with those but then realized I didn't totally understand what the full logic of the color system was, especially with the interaction between pressing and disabling the buttons. But, in general, your view will re-render any time a #Published property on your #StateObject changes, so you don't necessarily have to use something like onChange to trigger an update.