Repeating Action Continuously in Swiftui

Repeating Action Continuously In SwiftUI

Animation.basic is deprecated. Basic animations are now named after their curve types: like linear, etc:

var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}

Source:
https://forums.swift.org/t/swiftui-animation-basic-duration-curve-deprecated/27076

Repeating animation on SwiftUI Image

Here is possible solution for continuous progressing on appear & start/stop. Tested with Xcode 11.4 / iOS 13.4.

demo

struct PeopleList : View {
@State private var isAnimating = false
@State private var showProgress = false
var foreverAnimation: Animation {
Animation.linear(duration: 2.0)
.repeatForever(autoreverses: false)
}

var body: some View {
Button(action: { self.showProgress.toggle() }, label: {
if showProgress {
Image(systemName: "arrow.2.circlepath")
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.animation(self.isAnimating ? foreverAnimation : .default)
.onAppear { self.isAnimating = true }
.onDisappear { self.isAnimating = false }
} else {
Image(systemName: "arrow.2.circlepath")
}
})
.onAppear { self.showProgress = true }
}
}

How to make a LongPressGesture that runs repeatedly while the button is still being held down in SwiftUI?

You can do this by using timer. Make the timer starts when the user long pressed the image, and if the timer reaches 0, you can add two actions: 1. resetting the timer back to 0.5 seconds and 2.code you want to run every 0.5 seconds

    struct ContentView: View {
@State var timeRemaining = 0.5
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
@State var userIsPressing = false //detecting whether user is long pressing the screen

var body: some View {
VStack {
Image(systemName: "chevron.left").onReceive(self.timer) { _ in
if self.userIsPressing == true {
if self.timeRemaining > 0 {
self.timeRemaining -= 0.5
}
//resetting the timer every 0.5 secdonds and executing code whenever //timer reaches 0

if self.timeRemaining == 0 {
print("execute this code")
self.timeRemaining = 0.5
}
}
}.gesture(LongPressGesture(minimumDuration: 0.5)
.onChanged() { _ in
//when longpressGesture started
self.userIsPressing = true
}
.onEnded() { _ in
//when longpressGesture ended
self.userIsPressing = false

}
)
}
}
}

Why does Xcode throw an error when I add .repeatsForever() to an animation?

You must write Animation before .easeIn

Correct your modifier to this:

.animation(Animation.easeIn(duration: 1.0).repeatForever(autoreverses: true))

As you probably already know, SwiftUI (at the moment) doesn't point to the correct line of error. Thats why it points to the offset.

Stop UIKeyCommand repeated actions

In general, you don't need to deal with this, since the new view would usually become the firstReponder and that would stop the repeating. For the playSound case, the user would realize what is happening and take her finger off of the key.

That said, there are real cases where specific keys should never repeat. It would be nice if Apple provided a public API for that. As far as I can tell, they do not.

Given the '//Not allowed in the AppStore' comment in your code, it seems like you're OK using a private API. In that case, you could disable repeating for a keyCommand with:

UIKeyCommand *keyCommand =  [UIKeyCommand ...];
[keyCommand setValue:@(NO) forKey:@"_repeatable"];

Press-and-hold button for repeat fire

You want rapid repeat fire when your button is held down.

Your buttonDown and buttonUp methods need to be defined at the top level, and not inside of another function. For demonstration purposes, it is clearer to forgo wiring up @IBActions from the Storyboard and just set up the button in viewDidLoad:

class ViewController: UIViewController {

@IBOutlet weak var button: UIButton!
var timer: Timer?
var speedAmmo = 20

@objc func buttonDown(_ sender: UIButton) {
singleFire()
timer = Timer.scheduledTimer(timeInterval: 0.3, target: self, selector: #selector(rapidFire), userInfo: nil, repeats: true)
}

@objc func buttonUp(_ sender: UIButton) {
timer?.invalidate()
}

func singleFire() {
print("bang!")
}

@objc func rapidFire() {
if speedAmmo > 0 {
speedAmmo -= 1
print("bang!")
} else {
print("out of speed ammo, dude!")
timer?.invalidate()
}
}

override func viewDidLoad() {
super.viewDidLoad()

// These could be added in the Storyboard instead if you mark
// buttonDown and buttonUp with @IBAction
button.addTarget(self, action: #selector(buttonDown), for: .touchDown)
button.addTarget(self, action: #selector(buttonUp), for: [.touchUpInside, .touchUpOutside])
}
}

Also, I changed .touchUpOutside to [.touchUpInside, .touchUpOutside] (to catch both touch up events) and call singleFire on the initial buttonDown for single fire. With these changes, pressing the button fires immediately, and then fires every 0.3 seconds for as long as the button is held down.


The button can be wired up in the Storyboard instead of setting it up in viewDidLoad. In this case, add @IBAction to buttonDown and buttonUp. Then Control-click on your button in the Storyboard and drag from the circle next to Touch Down to func buttonDown, and drag from the circles next to Touch Up Inside and Touch Up Outside to func buttonUp.

Sample Image

SwiftUI pause/resume rotation animation

Pausing and resuming the animation in SwiftUI is really easy and you're doing it right by determining the animation type in the withAnimation block.

The thing you're missing is two additional pieces of information:

  1. What is the value at which the animation was paused?
  2. What is the value to which the animation should proceed while resumed?

These two pieces are crucial because, remember, SwiftUI views are just ways of expressing what you want your UI to look like, they are only declarations. They are not keeping the current state of UI unless you yourself provide the update mechanism. So the state of the UI (like the current rotation angle) is not saved in them automatically.

While you could introduce the timer and compute the angle values yourself in a discreet steps of X milliseconds, there is no need for that. I'd suggest rather letting the system compute the angle value in a way it feels appropriate.

The only thing you need is to be notified about that value so that you can store it and use for setting right angle after pause and computing the right target angle for resume. There are few ways to do that and I really encourage you to read the 3-part intro to SwiftUI animations at https://swiftui-lab.com/swiftui-animations-part1/.

One approach you can take is using the GeometryEffect. It allows you to specify transform and rotation is one of the basic transforms, so it fits perfectly. It also adheres to the Animatable protocol so we can easily participate in the animation system of SwiftUI.

The important part is to let the view know what is the current rotation state so that it know what angle should we stay on pause and what angle should we go to when resuming. This can be done with a simple binding that is used EXCLUSIVELY for reporting the intermediate values from the GeometryEffect to the view.

The sample code showing the working solution:

import SwiftUI

struct PausableRotation: GeometryEffect {

// this binding is used to inform the view about the current, system-computed angle value
@Binding var currentAngle: CGFloat
private var currentAngleValue: CGFloat = 0.0

// this tells the system what property should it interpolate and update with the intermediate values it computed
var animatableData: CGFloat {
get { currentAngleValue }
set { currentAngleValue = newValue }
}

init(desiredAngle: CGFloat, currentAngle: Binding<CGFloat>) {
self.currentAngleValue = desiredAngle
self._currentAngle = currentAngle
}

// this is the transform that defines the rotation
func effectValue(size: CGSize) -> ProjectionTransform {

// this is the heart of the solution:
// reporting the current (system-computed) angle value back to the view
//
// thanks to that the view knows the pause position of the animation
// and where to start when the animation resumes
//
// notice that reporting MUST be done in the dispatch main async block to avoid modifying state during view update
// (because currentAngle is a view state and each change on it will cause the update pass in the SwiftUI)
DispatchQueue.main.async {
self.currentAngle = currentAngleValue
}

// here I compute the transform itself
let xOffset = size.width / 2
let yOffset = size.height / 2
let transform = CGAffineTransform(translationX: xOffset, y: yOffset)
.rotated(by: currentAngleValue)
.translatedBy(x: -xOffset, y: -yOffset)
return ProjectionTransform(transform)
}
}

struct DemoView: View {
@State private var isRotating: Bool = false

// this state keeps the final value of angle (aka value when animation finishes)
@State private var desiredAngle: CGFloat = 0.0

// this state keeps the current, intermediate value of angle (reported to the view by the GeometryEffect)
@State private var currentAngle: CGFloat = 0.0

var foreverAnimation: Animation {
Animation.linear(duration: 1.8)
.repeatForever(autoreverses: false)
}

var body: some View {
Button(action: {
self.isRotating.toggle()
// normalize the angle so that we're not in the tens or hundreds of radians
let startAngle = currentAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2)
// if rotating, the final value should be one full circle furter
// if not rotating, the final value is just the current value
let angleDelta = isRotating ? CGFloat.pi * 2 : 0.0
withAnimation(isRotating ? foreverAnimation : .linear(duration: 0)) {
self.desiredAngle = startAngle + angleDelta
}
}, label: {
Text("quot;)
.font(.largeTitle)
.modifier(PausableRotation(desiredAngle: desiredAngle, currentAngle: $currentAngle))
})
}
}

while loop that checks it's condition every second in swift

Using while loop like this will create an infinite loop actually. You should use Timer().

timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.loop), userInfo: nil, repeats: true)

And then

@objc
func loop() {
if yourCondition {
// your code here
timer.invalidate()
}
}

Make sure you declare timer with your other variable declarations, so it can be invalidated once your condition has been met:

var timer: Timer!


Related Topics



Leave a reply



Submit