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.
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 @IBAction
s 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
.
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:
- What is the value at which the animation was paused?
- 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
How to Restrict an Enum to Certain Cases of Another Enum
Why 'Self.Self' Compiles and Run in Swift
Swiftui View Does Not Updated When Observedobject Changed
Swift: Print Name of a Function Stored in a Variable
Swift Combine: What Are Those Multicast Functions for and How to Use Them
Tap Gesture Not Working as Expected When Added to Uiview in Collectionview Cell
How to Create Apple Watchos5 Complication
How Many Way Are There to Do Crud Operation in Sqlite Swift
How to Read Id3 Tags/Other Metadata from an Hls Stream in Swift/Avkit
How to Switch to Swift 4.0 in Xcode 9.3
Why Are Properties of an Immutable Object Mutable in Swift
Migration from Swift 3 to Swift 4 - Cannot Convert String to Expected String.Element
C-Style Uninitialized Pointer Passing in Apple Swift
Make a Grid of Buttons of Same Width and Height in Swiftui
Swiftui: How to Find the Height of an Image and Use It to Set the Size of a Frame