In SwiftUI I am trying to start and STOP an animation with a button. I can't find a method within SwiftUI that lets me stop an automation. In Swift 4 I used to say "startButton.layer.removeAllAnimations()". Is there an equivalent in SwiftUI?
For the code below, how would I used the value of start to enable/disable the animation. I tried .animation(start ? Animation(...) : .none) without any luck [creates some odd animation within the button].
import SwiftUI
struct Repeating_WithDelay: View {
#State private var start = false
var body: some View {
VStack(spacing: 20) {
TitleText("Repeating")
SubtitleText("Repeat With Delay")
BannerText("You can add a delay between each repeat of the animation. You want to add the delay modifier BEFORE the repeat modifier.", backColor: .green)
Spacer()
Button("Start", action: { self.start.toggle() })
.font(.largeTitle)
.padding()
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.green))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 4)
.scaleEffect(start ? 2 : 0.9)
.opacity(start ? 0 : 1))
.animation(Animation.easeOut(duration: 0.6)
.delay(1) // Add 1 second between animations
.repeatForever(autoreverses: false))
Spacer()
}
.font(.title)
}
}
struct Repeating_WithDelay_Previews: PreviewProvider {
static var previews: some View {
Repeating_WithDelay()
}
}
Update - retested with Xcode 13.4 / iOS 15.5
The following should work, moved animation into overlay-only and added conditional to .default (tested with Xcode 11.2 / iOS 13.2)
Button("Start", action: { self.start.toggle() })
.font(.largeTitle)
.padding()
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.green))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 4)
.scaleEffect(start ? 2 : 0.9)
.opacity(start ? 0 : 1)
.animation(start ? Animation.easeOut(duration: 0.6)
.delay(1) // Add 1 second between animations
.repeatForever(autoreverses: false) : .default, value: start)
)
On GitHub
Related
I apply ".animation (.easeIn)" but it is deprecated.
tells me: animation 'has been deprecated in iOS 15.0: use instead with Animation or animation (: value :), but animation (: value :) I don't know which value I need to pass. I can't create animations that start slowly. Can you please help me understand how I can properly use animation (_: value :) to make animation work? I have to create the animation that when I move the slider, the images slowly recompose in space according to how much they are enlarged or reduced.
thank you.
'''
import SwiftUI
struct GalleryView: View {
// MARK: - PROPERTIES
#State private var selectedAnimal: String = "lion"
let animals: [Animal] = Bundle.main.decode("animals.json")
let haptics = UIImpactFeedbackGenerator(style: .medium)
#State private var gridLayout: [GridItem] = [GridItem(.flexible())]
#State private var gridColumn: Double = 3.0
func gridSwitch() {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
// MARK: - BODY
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 30) {
// MARK: - IMAGE
Image(selectedAnimal)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 8))
// MARK: - SLIDER
Slider(value: $gridColumn, in: 2...4, step: 1) // se aumento il valore nello slider ci saranno più sezioni
.padding(.horizontal)
.onChange(of: gridColumn, perform: { value in
gridSwitch()
})
// MARK: - GRID
LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {
ForEach(animals) { item in
Image(item.image)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.onTapGesture {
selectedAnimal = item.image
haptics.impactOccurred()
}
} //: LOOP
} //: GRID
.animation(.easeIn)
.onAppear(perform: {
gridSwitch()
})
} //: VSTACK
.padding(.horizontal, 10)
.padding(.vertical,50)
} //: SCROLL
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(MotionAnimationView())
}
}
// MARK: - PREVIEW
struct GalleryView_Previews: PreviewProvider {
static var previews: some View {
GalleryView()
}
}
As the error says, .animation() is deprecated, with iOS 15, you need to use .animation(value:) now. where the value is the binding that you want your animation to be triggered, in your case, I assume it is selectedAnimal.
Applying this would be something like
VStack {
}
.animation(.easeIn, value: selectedAnimal)
As discussed in comments, if you want your gridLayout to be animatable, it is a little bit more tricky. Because Arrays has to be Equatable if you want them to be animatable, since extending the GridItem is not a good solution, I came up with this:
delete your .animation method change your gridSwitch function with this:
struct GalleryView: View {
// ... irrelevant code
#State private var isGridChanged = false
func gridSwitch() {
if (isGridChanged) {
withAnimation {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
}
else {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
isGridChanged = true
}
}
isGridChanged is required because as you're changing your gridLayout when your View is initialized, it causes a weird bug that everything is getting scaled down when app launches because of withAnimation.
The .animation() modifier has been deprecated in iOS 15, but I'm not sure I understand how Xcode's suggested equivalent, animation(_:value:), works.
.animation(.easeInOut(duration: 2)) // ⚠️'animation' was deprecated in iOS 15.0: Use withAnimation or animation(_:value:) instead.
How would I change my code to get rid of the warning?
You need tell Xcode what exactly should be animated! With given a variable that conform to Equatable protocol. That could be State or Binding or any other wrapper that allow you to update the value of it.
Example:
struct ContentView: View {
#State private var offset: CGFloat = 200.0
var body: some View {
Image(systemName: "ant")
.font(Font.system(size: 100.0))
.offset(y: offset)
.shadow(radius: 10.0)
.onTapGesture { offset -= 100.0 }
.animation(Animation.easeInOut(duration: 1.0), value: offset)
}
}
Result:
You can also use the animation by using a boolean as the value.
Example Below:
When you tap the CardView(), it will toggle show which in turn will animate the offset both ways.
#State var show = false
CardView()
.offset(x: 0, y: show ? -100 : -10)
.animation(.easeInOut(duration: 1.0), value: show)
.onTapGesture {
show.toggle()
}
UUID
extension View {
func withoutAnimation() -> some View {
self.animation(nil, value: UUID())
}
}
demo
/*
.animation(
Animation
.easeInOut(duration: 1.5)
.repeatForever(autoreverses: true)
)
*/
// ⚠️ 'animation' was deprecated in iOS 15.0,
// Use withAnimation or animation(_:value:) instead.
// value: UUID ✅
.animation(
Animation
.easeInOut(duration: 1.5)
.repeatForever(autoreverses: true),
value: UUID()
)
refs
https://developer.apple.com/forums/thread/688947
Another approach would be to embed ontapGesture in a withAnimation
struct SwiftUWithAnimation: View {
#State private var offset: CGFloat = .zero
var body: some View {
Circle()
.frame(width: 100, height: 100, alignment: .center)
.offset(x: 0, y: offset)
.onTapGesture {
withAnimation(.default) {
offset += 100.0
}
}
}
}
.animation now takes a second argument called value.
https://developer.apple.com/documentation/swiftui/view/animation(_:value:)
I have a simple animation
Capsule()
.cornerRadius(25)
.scaleEffect(pulsate ? 1 : 1.2)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true).speed(1.5))
.foregroundColor(Color.red)
.frame(width: 75, height: 27)
.onAppear{
self.pulsate.toggle()
}
Everything works fine and the animation too. But if I open a keyboard, when I close it my view capsule starts jumping instead of just doing the scale effect only.
I really have no idea why and how to solve
Thank you all
You should use value for animation
import SwiftUI
struct ContentView: View {
#State private var pulsate: Bool = Bool()
#State private var stringOfText: String = String()
var body: some View {
Capsule()
.cornerRadius(25)
.scaleEffect(pulsate ? 1 : 1.2)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true).speed(1.5), value: pulsate) // <<: here!
.foregroundColor(Color.red)
.frame(width: 75, height: 27)
.onAppear{
self.pulsate.toggle()
}
TextField("enter text", text: $stringOfText)
}
}
I have an app with a view where I show images taken with the device's camera (stored
with Core Data) and I want to create the effect of time lapse - say you take photos
of a flower over a period of time and want to show those images in a stream. I have
been able to do that - but I want to have some transition effects between the images
and have not been able to do so. I can apply rotation and a static scale but cannot
fade from 0 to 1 opacity, nor scale up from zero to full frame. I must be missing
something really simple. Here's the code using an array of photos instead of Core Data
for simplicity.
struct ContentView: View {
#State private var activeImageIndex = 0
#State private var startTimer = false
#State private var myAnimationBool = true
let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
let myShots = ["CoolEquipment", "FishAndChipsMedium", "FlatheadLake1", "GlacierBusMedium", "Hike", "HuckALaHuckMedium", "JohnAndRiverMedium", "MacDonaldLodgeLakesideMedium"
]
var body: some View {
GeometryReader { geo in
VStack {
Text("Sequence")
.foregroundColor(.blue)
.font(.system(size: 25))
Image(self.myShots[self.activeImageIndex])
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width - 20, height: geo.size.width - 20, alignment: .center)
.cornerRadius(20)
.shadow(radius: 10, x: 10, y: 10)
//None of these do anything
//.animation(.easeInOut(duration: 0.5))
//.animation(.easeInOut)
//.transition(.opacity)
//.transition(self.myAnimationBool ? .opacity : .slide)
//.transition(.scale)
//.transition(AnyTransition.opacity.combined(with: .slide))
//this works but is static
//.scaleEffect(self.myAnimationBool ? 0.5 : 1.0)
//this works but again is static
//.rotationEffect(.degrees(self.myAnimationBool ? 90 : 0))
.onReceive(self.timer) { t in
print("in fixed timer")
if self.startTimer {
self.activeImageIndex = (self.activeImageIndex + 1) % self.myShots.count
}
}
HStack {
Button(action: {
self.startTimer.toggle()
}) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.frame(width: 200, height: 40)
Text(self.startTimer ? "Stop" : "Start").font(.headline)
}
}
}
.padding()
}//top VStack
}.onDisappear{ self.startTimer = false }
}
}
Any guidance would be appreciated. Xcode 11.3 (11C29)
I have created a round button with following code:
struct RoundButton : View {
var value = "..."
var body: some View {
GeometryReader { geometry in
Button(action: {}) {
VStack {
Text(self.value)
.font(.headline)
.color(Color("NightlyDark"))
Text("MIN")
.font(.footnote)
.color(.white)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
.clipShape(Circle())
}
}
}
But when clicking the button, the shape gets deformed. Any idea why this happens ?
Same happens when i use .mask(Circle())
Is this a beta thing or normal behavior ? And does anyone maybe know a better way to create rounded buttons ?
what happens here is Button Considers all screen[width + height] as their frame by default.
so you have to set Button also.
I think it's the default behavior in watchOS
Here Your Code :
struct RoundButton: View {
var value = "..."
var body: some View {
GeometryReader { geometry in
Button(action: {}) {
VStack {
Text(self.value)
.font(.headline)
.foregroundColor(Color("NightlyDark"))
Text("MIN")
.font(.footnote)
.foregroundColor(.white)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipShape(Circle())
}
}
}
Note: i'm using Xcode 11 Beta 4