Restarting perpetual animation after stopping in SwiftUI - swift

Background
In this learning app, I've followed and excellent tutorial from Hacking with Swift on generating a wave-like animation. I've modified this app further adding some functionalities:
Providing Start/Stop mechanism for the wave animation
Perpetually generating random numbers for the duration of the animation
Modifying animation if an "interesting" number is found. Initially, I've implemented logic that defines even numbers as interesting but that could be easily changes to flag prime numbers, etc.
Problem
After stopping the animation does not "run" again. This is demonstrated in the gif below.
After stopping the animation does not restart.
Code
//
// ContentView.swift
// WaveExample
//
// Created by Konrad on 28/07/2021.
// Original tutorial: https://www.hackingwithswift.com/plus/custom-swiftui-components/creating-a-waveview-to-draw-smooth-waveforms
//
import SwiftUI
/**
Creates wave shape object
- Parameter strength: How tall the wave should be
- Parameter frequency: How densly the wave should be packed
- returns: Shape
*/
struct Wave: Shape {
// Basic wave characteristics
var strength: Double // Height
var frequency: Double // Number of hills
var phase: Double // Offsets the wave, can be used to animate the view
// Required to define that animation relates to moving the wave from left to right
var animatableData: Double {
get { phase }
set { self.phase = newValue }
}
// Path drawing function
func path(in rect: CGRect) -> Path {
let path = UIBezierPath()
// Basic waveline characteristics
let width = Double(rect.width)
let height = Double(rect.height)
let midWidth = width / 2
let midHeight = height / 2
let wavelength = width / frequency
let oneOverMidWidth = 1 / midWidth
// Path characteristics
path.move(to: CGPoint(x: 0, y: midHeight))
// By determines the nmber of calculations, can be decreased to run faster
for xPosition in stride(from: 0, through: width, by: 1) {
let relativeX = xPosition / wavelength // How far we are from the start point
let distanceFromMidWidth = xPosition - midWidth // Distance from the middle of the space
let normalDistance = distanceFromMidWidth * oneOverMidWidth // Get values from -1 to 1, normalize
// let parabola = normalDistance // Small waves in the middle
let parabola = -(normalDistance * normalDistance) + 1 // Big wave in the middle
let sine = sin(relativeX + phase) // Offset based on phase
let yPosition = parabola * strength * sine + midHeight // Moving this halfway
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
return Path(path.cgPath)
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
// Positioning
let midHeight = rect.height / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: midHeight))
path.addLine(to: CGPoint(x: rect.width, y: midHeight))
return Path(path.cgPath)
}
}
struct ContentView: View {
#State private var phase = 0.0 // Used to animate the wave
#State private var waveStrength: Double = 10.0 // How tall, change for interesting numbers
#State private var waveFrequency: Double = 10.0 // How frequent, change for interesting numbers
#State var isAnimating: Bool = false // Currently running animation
#State private var randNum: Int16 = 0 // Random number to keep generating while animating
#State private var isNumberInteresting: Bool = false // Will take 'true' of the random number has some interesting properties
// Timer publisher reflecting frequent animation changes
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
// Stop timer
func stopTimer() {
self.timer.upstream.connect().cancel()
}
// Start timer
func startTimer() {
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
// Check if number is interesting
func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}
var body: some View {
VStack {
if self.isAnimating {
VStack {
Button("Stop") {
self.isAnimating = false
stopTimer()
}
.font(.title)
.foregroundColor(Color(.blue))
Text("Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))")
.onReceive(timer, perform: { _ in
randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)
})
}
} else {
Button("Start") {
self.isAnimating = true
startTimer()
}
.font(.title)
.foregroundColor(Color(.red))
}
if self.isAnimating {
// Animation
ZStack {
ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}
}
.onReceive(timer) { _ in
// withAnimation requires info on how to animate
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
self.phase = .pi * 2 // 180 degrees of sine being calculated
if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}
}
}
.frame(height: UIScreen.main.bounds.height * 0.8)
} else {
// Static line
ZStack {
Line()
.stroke(Color.blue)
}
.frame(height: UIScreen.main.bounds.height * 0.8)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Side notes
In addiction to the problem above any good practice pointers on working with Swift are always welcome.

I made your project works, you can see the changed code // <<: Here!, the issue was there that you did not show the Animation the changed value! you showed just one time! and after that you keep it the same! if you see your code in your question you are repeating self.phase = .pi * 2 it makes no meaning to Animation! I just worked on your ContentView the all project needs refactor work, but that is not the issue here.
struct ContentView: View {
#State private var phase = 0.0
#State private var waveStrength: Double = 10.0
#State private var waveFrequency: Double = 10.0
#State var isAnimating: Bool = false
#State private var randNum: Int16 = 0
#State private var isNumberInteresting: Bool = false
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var stringOfText: String = String() // <<: Here!
func stopTimer() {
self.timer.upstream.connect().cancel()
phase = 0.0 // <<: Here!
}
func startTimer() {
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(500)) { phase = .pi * 2 } // <<: Here!
}
func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}
var body: some View {
VStack {
Button(isAnimating ? "Stop" : "Start") { // <<: Here!
isAnimating.toggle() // <<: Here!
isAnimating ? startTimer() : stopTimer() // <<: Here!
}
.font(.title)
.foregroundColor(isAnimating ? Color.red : Color.blue) // <<: Here!
ZStack {
if isAnimating {
ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}
}
else {
Line().stroke(Color.blue)
}
}
.frame(height: UIScreen.main.bounds.height * 0.8)
.overlay(isAnimating ? Text(stringOfText) : nil, alignment: .top) // <<: Here!
.onReceive(timer) { _ in
if isAnimating { // <<: Here!
randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)
stringOfText = "Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))" // <<: Here!
if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}
}
else {
stopTimer() // For safety! Killing Timer in case! // <<: Here!
}
}
.animation(nil, value: stringOfText) // <<: Here!
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) // <<: Here!
.id(isAnimating) // <<: Here!
}
}
}

Related

Changing duration for Slider animation doesn't make any difference

I'm trying to animate Slider smoothly. My original problem was animating it for 1 second every second for a small value (building an audio player scrubber), but the animation was a split second instead of a full second.
To isolate a problem, I built a playground where I'm trying to animate Slider change from 0 to maxValue for maxValue number of seconds. However, whatever maxValue is, the animation happens in a fraction of a second.
Here is the code:
struct SliderTest: View {
#State private var sliderValue = 0.0
let maxValue = 30.0
var body: some View {
VStack(spacing: 10) {
Slider(value: $sliderValue, in: 0...maxValue)
Button("Animate Slider") {
withAnimation(.linear(duration: maxValue-sliderValue)) {
sliderValue = maxValue
}
}
Button("Reset to Random") {
sliderValue = Double.random(in: 0..<maxValue)
}
Button("Reset") {
sliderValue = 0
}
}
}
}
Here you can get the code with a preview for Swift Playgrounds:
https://gist.github.com/OgreSwamp/6e6423d6ef2d26425e3f993042ac208d
First of all, animation is not working here because when it comes into withAnimation it will assign sliderValue to maxValue immediately. That causes the reason above like you said.
For the problem, I will separate the slider just for change UI depends on sliderValue only. Then building sliderTimer for handle the change value of slider and everytime the timer is excuted it will automatically increase sliderValue by step.
The code will be like this
struct ContentView: View {
#State private var sliderValue = 0.0
private var maxValue = 30.0
private var sliderTimeAnimation = 2.0
private var step = 1.0
#State private var sliderTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 10) {
Slider(
value: $sliderValue,
in: 0...maxValue
).onReceive(sliderTimer) {
_ in
sliderValue += step
if (sliderValue > maxValue) {
stopTimer()
}
}
.onAppear() {
self.stopTimer()
}
Button("Animate Slider") {
startTimer()
}
Button("Reset to Random") {
stopTimer()
sliderValue = Double.random(in: 0..<maxValue)
}
Button("Reset") {
stopTimer()
sliderValue = 0
}
}
}
func startTimer() {
self.sliderTimer = Timer.publish(every: sliderTimeAnimation * step / maxValue, on: .main, in: .common).autoconnect()
}
func stopTimer() {
self.sliderTimer.upstream.connect().cancel()
}
}
In this case, I set sliderTimeAnimation is 2.0, step is 1.0 (if you need faster animation you can set step is 0.5 or lower) and the max value is 30.0 like your code.
The result is like this.

Why animation is speeding up after changing a property? SwiftUI

I tried to do this Particle Animation and wanted to change the color property. Every time I do it, the animation speeds up. How can I prevent this from happening? I'm kinda new to this, so is there a better approach?
This is my Emitter
struct EmitterView: View {
private struct ParticleView: View {
let image: Image
#State private var isActive = false
let position: ParticleState<CGPoint>
let opacity: ParticleState<Double>
let rotation: ParticleState<Angle>
let scale: ParticleState<CGFloat>
var body: some View {
image
.opacity(isActive ? opacity.end : opacity.start)
.scaleEffect(isActive ? scale.end : scale.start)
.rotationEffect(isActive ? rotation.end : rotation.start)
.position(isActive ? position.end : position.start)
.onAppear{self.isActive = true}
}
}
private struct ParticleState<T> {
var start: T
var end: T
init(_ start: T, _ end: T) {
self.start = start
self.end = end
}
}
var images: [String]
var particleCount: Int
var creationPoint = UnitPoint.center
var creationRange = CGSize.zero
var colors = [Color.white]
var blendMode = BlendMode.normal
var angle = Angle.zero
var angleRange = Angle.zero
var opacity = 1.0
var opacityRange = 0.0
var opacitySpeed = 0.0
var rotation = Angle.zero
var rotationRange = Angle.zero
var rotationSpeed = Angle.zero
var scale: CGFloat = 1
var scaleRange: CGFloat = 0
var scaleSpeed: CGFloat = 0
var speed = 0.0
var speedRange = 0.0
var animation = Animation.linear.repeatForever(autoreverses: false)
var animationDelayTreshold = 0.0
var body: some View {
GeometryReader { geo in
ZStack {
ForEach(0..<self.particleCount, id: \.self) { i in
ParticleView(
image: Image(images.randomElement()!),
position: self.position(in: geo),
opacity: self.makeOpacity(),
rotation: self.makeRotation(),
scale: self.makeScale()
)
.animation(self.animation.delay(Double.random(in: 0...self.animationDelayTreshold)))
.colorMultiply(self.colors.randomElement() ?? .white)
.blendMode(self.blendMode)
}
}
}
}
private func position(in proxy: GeometryProxy) -> ParticleState<CGPoint> {
let halfCreationRangeWidth = creationRange.width / 2
let halfCreationRangeHeight = creationRange.height / 2
let creationOffsetX = CGFloat.random(in: -halfCreationRangeWidth...halfCreationRangeWidth)
let creationOffsetY = CGFloat.random(in: -halfCreationRangeHeight...halfCreationRangeHeight)
let startX = (proxy.size.width * (creationPoint.x + creationOffsetX))
let startY = (proxy.size.height * (creationPoint.y + creationOffsetY))
let start = CGPoint(x: startX, y: startY)
let halfSpeedRange = speedRange / 2
let actualSpeed = Double.random(in: speed - halfSpeedRange...speed + halfSpeedRange)
let halfAngleRange = angleRange.radians / 2
let totalRange = Double.random(in: angle.radians - halfAngleRange...angle.radians + halfAngleRange)
let finalX = cos(totalRange - .pi / 2) * actualSpeed
let finalY = sin(totalRange - .pi / 2) * actualSpeed
let end = CGPoint(x: Double(startX) + finalX, y: Double(startY) + finalY)
return ParticleState(start, end)
}
private func makeOpacity() -> ParticleState<Double> {
let halfOpacityRange = opacity / 2
let randomOpacity = Double.random(in: -halfOpacityRange...halfOpacityRange)
return ParticleState(opacity + randomOpacity, opacity + opacitySpeed + randomOpacity)
}
private func makeScale() -> ParticleState<CGFloat> {
let halfScaleRange = scaleRange / 2
let randomScale = CGFloat.random(in: -halfScaleRange...halfScaleRange)
return ParticleState(scale + randomScale, scale + scaleSpeed + randomScale)
}
private func makeRotation() -> ParticleState<Angle> {
let halfRotationRange = (rotationRange / 2).radians
let randomRotation = Double.random(in: -halfRotationRange...halfRotationRange)
let randomRotationAngle = Angle(radians: randomRotation)
return ParticleState(rotation + randomRotationAngle, rotation + rotationSpeed + randomRotationAngle)
}
mutating func makeRed() {
colors = [.red]
}
}
And this is how I implemented it
import SwiftUI
struct ContentView: View {
#State var emitter = EmitterView(images: ["spark"], particleCount: 200, creationRange: CGSize(width: 0.4, height: 0.2), colors: [.white], blendMode: .screen, angle: .degrees(0), angleRange: .degrees(360), opacityRange: 0, opacitySpeed: 15, scale: 0.5, scaleRange: 0.2, scaleSpeed: -0.2, speed: 50, speedRange: 120, animation: Animation.linear(duration: 1).repeatForever(autoreverses: false), animationDelayTreshold: 1)
var body: some View {
ZStack {
emitter
.ignoresSafeArea()
}
.background(.black)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
.onTapGesture {
emitter.makeRed()
}
}
}
I also tried with transaction, but I couldn't make it work, the animation won't restart.
View should be in body, animation should be joined to corresponding trigger state.
Find below fixed parts. Tested with Xcode 13.4 / iOS 15.5
#State var colors = [Color.white] // data !!
var body: some View {
ZStack {
// view is here !!
EmitterView(images: ["spark"], particleCount: 200, creationRange: CGSize(width: 0.4, height: 0.2), colors: colors, blendMode: .screen, angle: .degrees(0), angleRange: .degrees(360), opacityRange: 0, opacitySpeed: 15, scale: 0.5, scaleRange: 0.2, scaleSpeed: -0.2, speed: 50, speedRange: 120, animation: Animation.linear(duration: 1).repeatForever(autoreverses: false), animationDelayTreshold: 1)
.ignoresSafeArea()
}
.background(.black)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
.onTapGesture {
colors = [.red] // update !!
}
}
and animation where is trigger
private struct ParticleView: View {
let image: Image
#State private var isActive = false
let position: ParticleState<CGPoint>
let opacity: ParticleState<Double>
let rotation: ParticleState<Angle>
let scale: ParticleState<CGFloat>
var animation: Animation
var delayTreshold = 0.0
var body: some View {
image
.opacity(isActive ? opacity.end : opacity.start)
.scaleEffect(isActive ? scale.end : scale.start)
.rotationEffect(isActive ? rotation.end : rotation.start)
.position(isActive ? position.end : position.start)
// here is animation, depends on isActive !!
.animation(self.animation.delay(Double.random(in: 0...self.delayTreshold)), value: isActive)
.onAppear{self.isActive = true}
}
}
Complete test module is here

SwiftUI: Update struct View when calling mutating func

New to Switch. I need to implement an Analog Clock with auto-updating every second to the current time (like a typical analog clock).
Drawing the Views etc is no problem. But I'm having troubles auto-updating the time.
View
struct ClockView: View {
var viewModel: ClockViewModel
var clock:ClockViewModel.TransformedClock
var size: CGFloat
#State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Circle()
.fill(.white)
.frame(width: size, height: size)
.overlay{
GeometryReader{ geometry in
let radius = geometry.size.width/2
let midX = geometry.safeAreaInsets.top + radius
let midY = geometry.safeAreaInsets.leading + radius
ClockFace(size: size, radius: radius, x: midX, y: midY)
ClockHand(type: .second, angle: clock.secondAngle, x: midX, y: midY, length: radius-size*0.04, size: size)
.onReceive(timer){ _ in updateAngleHere}
}
}
}
}
struct ClockHand:View{
let type: ClockHandType
#State var angle: Double
var x: CGFloat
var y: CGFloat
var length: CGFloat
var size: CGFloat
var body: some View {
RoundedRectangle(cornerRadius: 5)
.fill(type.getColor())
.frame(width: size*type.getWidth(), height: length )
.position(x: x, y: y)
.offset(y: -length/2)
.rotationEffect(Angle.degrees(angle))
}
mutating func update(angle: Double) {
print("calling \(self.angle)")
self.angle = angle
}
}
ViewModel
class ClockViewModel: ObservableObject {
#Published private var model: ClockModel
init(){
model = ClockViewModel.initClocks()
}
static func initClock()->ClockModel {
let places = [
"Europe/Bern",
]
return ClockModel(number: places.count){ index in
places[index]
}
}
var activeClock: TransformedClock{
TransformedClock(clock: model.activeWorldClock!)
}
struct TransformedClock:Identifiable{
var id:Int
var secondAngle:Double
var clock:ClockModel.WorldClock
init(clock:ClockModel.WorldClock){
let second = clock.calendar.component(.second, from: Date())
self.clock = clock
self.id = clock.id
self.secondAngle = Double(second)/60.0*360
}
}
}
Model
struct ClockModel{
var worldClocks: [WorldClock]
init(number: Int, timeFactory: (Int)->String){
worldClocks = [WorldClock]()
for index in 0..<number{
let place = timeFactory(index)
var calendar = Calendar.current
worldClocks.append(
WorldClock(
id: index,
calendar: calendar,
place: formatPlaceString(place)
)
)
}
}
struct WorldClock:Identifiable {
var calendar: Calendar
var place: String
}
}
Find my code above. First of all, I just would like to move the ClockHand for seconds to different positions. For this, I've been told to use a timer and make use of onReceive. My first idea was to call a mutating func of that struct in the closure - but since I can't seem to specifically call it on that struct, I guess it's the wrong option.
So, I need to find a way to teach my second-ClockFace struct to update / redraw itself, whenever I call the mutating func.
Examples I've found only shown functions outside a struct...
Any inut would be much appreciated.

CAEmitterLayer Stops Displaying

I adapted this code for SwiftUI to display confetti particles, but sometimes the particle emitter does not work. I've noticed that this often happens after sending to background (not killing the app entirely) and reopening it, or simply letting the app sit for a while then trying again.
I've tried using beginTime as other answers have mentioned (on both the emitter and cells), but that fully breaks things. I've also tried toggling various other emitter properties (birthRate, isHidden). It might have to do with the fact that I'm adapting this with UIViewRepresentable. It seems like the emitter layer just disappears, even though the debug console says its still visible.
class ConfettiParticleView: UIView {
var emitter: CAEmitterLayer!
public var colors: [UIColor]!
public var intensity: Float!
private var active: Bool!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
colors = [UIColor(Color.red),
UIColor(Color.blue),
UIColor(Color.orange),
]
intensity = 0.7
active = false
emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: UIScreen.main.bounds.width / 2.0, y: 0) // emit from top of view
emitter.emitterShape = .line
emitter.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 100) // line spans the whole top of view
// emitter.beginTime = CACurrentMediaTime()
var cells = [CAEmitterCell]()
for color in colors {
cells.append(confettiWithColor(color: color))
}
emitter.emitterCells = cells
emitter.allowsGroupOpacity = false
self.layer.addSublayer(emitter)
}
func startConfetti() {
emitter.lifetime = 1
// i've tried toggling other properties here like birthRate, speed
active = true
}
func stopConfetti() {
emitter.lifetime = 0
active = false
}
func confettiWithColor(color: UIColor) -> CAEmitterCell {
let confetti = CAEmitterCell()
confetti.birthRate = 32.0 * intensity
confetti.lifetime = 15.0 * intensity
confetti.lifetimeRange = 0
confetti.name = "confetti"
confetti.color = color.cgColor
confetti.velocity = CGFloat(450.0 * intensity) // orig 450
confetti.velocityRange = CGFloat(80.0 * intensity)
confetti.emissionLongitude = .pi
confetti.emissionRange = .pi / 4
confetti.spin = CGFloat(3.5 * intensity)
confetti.spinRange = 300 * (.pi / 180.0)
confetti.scaleRange = CGFloat(intensity)
confetti.scaleSpeed = CGFloat(-0.1 * intensity)
confetti.contents = #imageLiteral(resourceName: "confetti").cgImage
confetti.beginTime = CACurrentMediaTime()
return confetti
}
func isActive() -> Bool {
return self.active
}
}
view representable
struct ConfettiView: UIViewRepresentable {
#Binding var isStarted: Bool
func makeUIView(context: Context) -> ConfettiParticleView {
return ConfettiParticleView()
}
func updateUIView(_ uiView: ConfettiParticleView, context: Context) {
if isStarted && !uiView.isActive() {
uiView.startConfetti()
print("confetti started")
} else if !isStarted {
uiView.stopConfetti()
print("confetti stopped")
}
}
}
swiftui view for testing
struct ConfettiViewTest: View {
#State var isStarted = false
var body: some View {
ZStack {
ConfettiView(isStarted: $isStarted)
.ignoresSafeArea()
Button(action: {
isStarted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
isStarted = false
}
}) {
Text("toggle")
.padding()
.background(Color.white)
}
}
}
}

How to update a view during a CADisplayLink animation

I'm trying to implement a simple animation using CADisplayLink. A black square should move slowly across the screen.
But the square appears stationary because the body of Squares doesn't get updated when step() increments testX and testY.
From what I can gather (I'm new to Swift), I'm using the #ObservedObject paradigm correctly, so I don't understand why the view won't refresh.
How do I get Squares's body to update along with the animation?
My code is below, thanks for your help!
class MyAnimations: NSObject, ObservableObject {
#Published var testX: Double = 0
#Published var testY: Double = 0
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
#objc func step(link: CADisplayLink) {
testX += 0.5
testY += 0.5
}
}
struct Squares: View {
#ObservedObject var animation = MyAnimations()
var body: some View {
Path { path in
path.addRect(CGRect(x: self.animation.testX, y: self.animation.testY, width: 50, height: 50))
}
.fill(Color.black)
}
}
struct SomeOtherView: View {
var body: some View {
Squares()
}
}
Update: retested with Xcode 13.4 / iOS 15.5 - works as-is
This is because your displaylink is created on stack, so destroyed right after quit from function. You need to make it property.
Here is modified tested code. Tested with Xcode 12 / iOS 14.
class MyAnimations: NSObject, ObservableObject {
#Published var testX: Double = 0
#Published var testY: Double = 0
private var displaylink: CADisplayLink! // << here !!
func createDisplayLink() {
if nil == displaylink {
displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
}
#objc func step(link: CADisplayLink) {
testX += 0.5
testY += 0.5
}
}
struct Squares: View {
#ObservedObject var animation = MyAnimations()
var body: some View {
Path { path in
path.addRect(CGRect(x: self.animation.testX, y: self.animation.testY, width: 50, height: 50))
}
.fill(Color.black)
.onAppear {
animation.createDisplayLink()
}
}
}