SwiftUI: Update struct View when calling mutating func - swift

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.

Related

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

Canvas doesn't get redrawn in SwiftUI

I have a project in SwiftUI on macOS where I draw to a canvas twice per second.
This is my ContentView:
struct ContentView: view {
#State var score: Int = 0
var body: some View {
VStack {
Text("Score: \(self.score)")
.fixedSize(horizontal: true, vertical: true)
Canvas(renderer: { gc, size in
start(
gc: &gc,
size: size
onPoint: { newScore in
self.score = newScore
}
)
)
}
}
}
The start function:
var renderer: Renderer
func start(
gc: inout GraphicsContext,
size: size,
onPoint: #escaping (Int) -> ()
) {
if renderer != nil {
renderer!.set(gc: &gc)
} else {
renderer = Renderer(
context: &gc,
canvasSize: size,
onPoint: onPoint
)
startGameLoop(renderer: renderer!)
}
renderer!.drawFrame()
}
var timer: Timer
func startGameLoop(renderer: Renderer) {
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: {
renderer!.handleNextFrame()
}
}
And the renderer roughly looks like this:
class Renderer {
var gc: GraphicsContext
var size: CGSize
var cellSize: CGFloat
let pointCallback: (Int) -> ()
var player: (Int, Int) = (0,0)
init(
context: inout GraphicsContext,
canvasSize: CGSize,
onPoint: (Int) -> ()
) {
self.gc = gc
self.size = canvasSize
self.pointCallback = onPoint
self.cellSize = min(self.size.width, self.size.height)
}
}
extension Renderer {
func handleNextFrame() {
self.player = (self.player.0 + 1, self.player.1 + 1)
self.drawFrame
}
func drawFrame() {
self.gc.fill(
Path(
x: CGFloat(self.player.0) * self.cellSize,
y: CGFloat(self.player.1) * self.cellSize,
width: self.cellSize,
height: self.cellSize
)
)
}
}
So the handleNextFrame method is called twice per second, which calls the drawFrame method, drawing the position of the player to the canvas.
However, there is nothing being drawn to the canvas.
Only the first frame is drawn, which comes from the renderer!.drawFrame() in start. When a point is scored, the canvas is also redrawn, because the start function gets called again.
The problem is that there is nothing being drawn to the Canvas when the drawFrame is called from handleNextFrame.
Where lies my problem, and how can I fix this issue?
Thanks in advance,
Jonas
I had the same issue.
Go to your project settings, select 'Build Settings' tab, and make sure 'Strict Concurrency Checking' is ON.
It will give you hints about GraphicsContext not being thread-safe, because it does not conform to Sendable protocol.
I am no expert on the topic, but I believe this means that GraphicsContext is not meant to be used in asynchronous context, you are not meant to save its reference for future use. You are given a GraphicsContext instance and you are meant to use it so long as the renderer closure's execution lasts (and this execution, of course, ends before your asynchronous callbacks could use the GraphicsContext instance).
What you really want is a TimelineView:
import SwiftUI
private func nanosValue(for date: Date) -> Int { return Calendar.current.component(.nanosecond, from: date)
}
struct ContentView: View {
var body: some View {
TimelineView(.periodic(from: Date(), by: 0.000001)) { timeContext in
Canvas { context, size in
let value = nanosValue(for: timeContext.date) % 2
let rect = CGRect(origin: .zero, size: size).insetBy(dx: 25, dy: 25)
// Path
let path = Path(roundedRect: rect, cornerRadius: 35.0)
// Gradient
let gradient = Gradient(colors: [.green, value == 1 ? .blue : .red])
let from = rect.origin
let to = CGPoint(x: rect.width + from.x, y: rect.height + from.y)
// Stroke path
context.stroke(path, with: .color(.blue), lineWidth: 25)
// Fill path
context.fill(path, with: .linearGradient(gradient,
startPoint: from,
endPoint: to))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This will change the gradient colour of the drawn rectangle every nanosecond.
Funny thing, I have heard on some blog, you need a completely different mindset to use SwiftUI, haha. It just does not work the traditional way.

Draw multiple circles in view | ForEach

What I am trying to achieve
I am learning swift. I am trying to draw a line with multiple circle on it.
Then I want to be able to modify the Color of a circle when the user tap the circle.
And eventually do a drag and drop for moving the circle. Such as a custom bezier curve.
Currently, I am stuck on drawing multiple circle on the line.
What I trying to do :
1 - I create an array containing CGPoint
public var PointArray:[CGPoint]=[]
2 - Then doing an Identifiable struct
private struct Positions: Identifiable {
var id: Int
let point: CGPoint
}
3 - With CurveCustomInit, I fill the array. I got this error
No exact matches in call to instance method 'append'
I have done a lot, but I never success at using a ForEach function in one view.
I am using struct shapes as I want to customise shapes and then reuse the component. And also add a gesture function to each circle.
There is the whole code :
Note that a GeometryReader is sending the size of the view
import SwiftUI
public var PointArray:[CGPoint]=[]
public var PointArrayInit:Bool = false
private struct Positions: Identifiable {
var id: Int
let point: CGPoint
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: centerCustom, radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
var incr = 0
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
let value = Positions(id:incr, point:Point )
PointArray.append(value)
path.addLine(to: Point)
xStepLoopIncrement += xStep
incr += 1
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
// initialisation
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
.overlay(Arc(startAngle: .degrees(0),endAngle: .degrees(360),clockwise:true).stroke(Color.blue, lineWidth: 10))
} else {
// Update point
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
What I am trying to achieve :
Your array PointArray contains values of type CGFloat but you are trying to append a value of type Positions. That's why you're getting this error.

Restarting perpetual animation after stopping in SwiftUI

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!
}
}
}

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()
}
}
}