Why does my SwiftUI animation revert to the previous frame on rotation? - ios

I have a SwiftUI View that is meant to be sort of a "warning" background that flashes on and off. It appears inside another View with a size that changes depending on device orientation. Everything works properly upon the first appearance, but if the device is rotated while the warning view is active it incorporates the frame of the previous orientation into the animation, making it not only flash on and off, but also move in and out of the view its positioned in, as well as change to and from the size it was on the previous orientation. Why is this happening and how can I fix it? Generic code:
struct PulsatingWarningBackground: View {
let color : Color
let speed : Double
#State private var opacity : Double = 1
var body: some View {
GeometryReader {geometry in
self.color
.opacity(self.opacity)
.animation(
Animation
.linear(duration: self.speed)
.repeatForever(autoreverses: true)
)
.onAppear(perform: {self.opacity = 0})
}
}
}
struct PulsatingWarningViewHolder: View {
var body: some View {
GeometryReader {geometry in
ZStack {
Color.white
ZStack {
Color.black
PulsatingWarningBackground(color: .red, speed: 1/4)
}.frame(width: geometry.frame(in: .local).width/5, height: geometry.frame(in: .local).height/10, alignment: .center)
}
}
}
}

You can apply animation only to opacity by using withAnimation(_:_:) inside onAppear(perform:). It works properly as you want.
struct PulsatingWarningBackground: View {
let color : Color
let speed : Double
#State private var opacity : Double = 1
var body: some View {
self.color
.opacity(self.opacity)
.onAppear(perform: {
withAnimation(Animation.linear(duration: self.speed).repeatForever()) {
self.opacity = 0
}
})
}
}
struct PulsatingWarningViewHolder: View {
var body: some View {
GeometryReader {geometry in
ZStack {
Color.white
ZStack {
Color.black
PulsatingWarningBackground(color: .red, speed: 1/4)
}
.frame(width: geometry.frame(in: .local).width/5, height: geometry.frame(in: .local).height/10, alignment: .center)
}
}
}
}

Related

SwiftUI iOS15 Image being animated from top left corner instead of cetner

I ma having an unusual issue.I have a loading screen where the center image is suppose to give a heart beat animation. I am accomplishing this by using the scaleEffect. This works fine in iOS 16. However, in iOS 15 (and even 15.4) The animation is wrong.
What is happening is that the animation's reference point is in the top-left corner instead of the picture's center. Additionally, it appears that the entire view is animating rather then the picture.
Coud I get some feedback on my code? I believe that I am setting something incorrectly or maybe there is a better way of accomplishing what I want.
The issue is happening in the LoadingView struct
struct LoadingView: View{
#State private var animatingLogo = false
var body: some View{
// GeometryReader{geoReader in
VStack(alignment: .center, spacing: 6) {
Image("JHLogo")
.frame(width: 200, height: 200, alignment: .center)
.aspectRatio(contentMode: .fill)
.scaleEffect(animatingLogo ? 0.15 : 0.1, anchor: .center)
.onAppear{
withAnimation(.easeInOut(duration: 1).repeatForever()){
animatingLogo.toggle()
}
}
}
.navigationBarHidden(true)
}
}
And here is the top level view in case this is the one that has the issue:
// Initial view that is loaded on app startup
struct MainView: View{
#State private var isLoading = false
var body : some View {
GeometryReader{geoReader in
NavigationView {
if (isLoading) {
LoadingView()
}
else {
CoverPageView()
}
}
.frame(width: geoReader.size.width, height:geoReader.size.height)
.fixedSize()
.aspectRatio(contentMode: .fill)
.onAppear {
startLoadingScreen()
}
}
}
func startLoadingScreen() {
isLoading = true;
DispatchQueue.main.asyncAfter(deadline: .now() + 3){
isLoading = false
}
}
}

Why is rotationEffect moving vertically too?

I have my animation for the rotation working beautifully, but the image is also moving vertically. Why?
struct SyncView: View {
#State var isSyncing = false
let animation: Animation = Animation.linear(duration: 2.0).repeatForever(autoreverses: false)
var body: some View {
VStack(spacing: 16) {
HStack {
VStack(alignment: .leading) {
Text("Linked Device's Name")
Text("Updating (Last update: Oct 1, 2022)")
.font(.callout)
.foregroundColor(.gray)
}
Spacer()
Text(Image(systemName: "arrow.triangle.2.circlepath"))
.foregroundStyle(Color.yellow)
.font(.title)
.rotationEffect(Angle.degrees(isSyncing ? 360 : 0))
.animation(animation, value: isSyncing)
}
}
.padding()
.onAppear {
isSyncing = true
}
}
}
Try setting the frame of the Text() view to values that make up a square (e.g. 10 x 10). The animation might be trying to center an uneven frame of the image, thus the movement.

Scaled view unexpectedly moving up and down when pushed from navigation view

I have a view that should scale in and out, starting immediately when the view is shown and repeating forever. However, I find that it's actually animating up and down as well as scaling much like in this post when it's pushed from a navigation view:
struct PlaceholderView: View {
#State private var isAnimating = false
var body: some View {
Circle()
.frame(width: 30, height: 30)
.scaleEffect(self.isAnimating ? 0.8 : 1)
.animation(Animation.easeInOut(duration: 1).repeatForever())
.onAppear {
self.isAnimating = true
}
.frame(width: 50, height: 50)
.contentShape(
Rectangle()
)
}
}
struct SettingsView: View {
#State private var showPlaceholder = false
var body: some View {
NavigationView {
ZStack {
Button(
action: {
showPlaceholder = true
}, label: {
Text("Go to placeholder")
}
)
NavigationLink(
destination: PlaceholderView(),
isActive: $showPlaceholder
) {
EmptyView()
}
.hidden()
}
}
.navigationViewStyle(.stack)
}
}
Why is this and how can I stop this from happening?
UPDATE:
Wrapping self.isAnimating = true in DispatchQueue.main.async {} fixes the issue, but I don't understand why...
I can reproduce the problem, but I wasn't able to reproduce your DispatchQueue.main.async {} fix.
Here is how I fixed it:
Animation is made by comparing a before state and and after state and animating between them. With an implicit animation, the before state is the position of the circle before it is added to the navigation view. By switching to an explicit animation and setting your before state after the view appears, the move animation is avoided and you only get the scaling:
Replace isAnimating with scale. In onAppear {}, first establish the before scale of 0.8, then animate the change to scale 1:
struct PlaceholderView: View {
// To work correctly, this initial value should be different
// than the before value set in .onAppear {}.
#State private var scale = 1.0
var body: some View {
Circle()
.frame(width: 30, height: 30)
.scaleEffect(self.scale)
.onAppear {
self.scale = 0.8 // before
withAnimation(.easeInOut(duration: 1).repeatForever()) {
self.scale = 1 // after
}
}
.frame(width: 50, height: 50)
.contentShape(
Rectangle()
)
}
}

SwiftUI unexpected animation on appear

I have created custom TabBar.
Everything works fine, until I embed one of the tab's view in to NavigationView, than result - appearance animation of a View from very top left corner to right corner (screen with text and yellow color):
How tabBar is done: basically View's body created using GeometryReader VStack/Zstack:
var body: some View {
GeometryReader { geometry in
let height = geometry.size.height
let width = geometry.size.width
let bottomSafeAreaInset = geometry.safeAreaInsets.bottom
let topSafeAreaInset = geometry.safeAreaInsets.top
let verticalSafeAreaInset = bottomSafeAreaInset + topSafeAreaInset
VStack(spacing: 0) {
// content
mainContentBody
.frame(width: width, height: height - heightOfTabBar)
.zIndex(0)
// some calculation ...
// tabBar
Spacer(minLength: 0)
BottomBar(barButtonItems: buttons)
.frame(width: width, height: tabBarHeightWithOffset)
.background(Color.gray)
.offset(y: isMenuShown ? tabBarHeightWithOffset : 0)
.edgesIgnoringSafeArea(.all)
.opacity(isMenuShown ? 0 : 1)
.tabContainerAnimation() // simple wrapper for animation with duration
//... other view in ZStack
// button
ZStack {
overlayButton
}
.offset(y: -initialButtonOffset + additionalOffsetForButton)
.tabContainerAnimation(delay: 0.25)
}
}
}
And Code for view on tab1:
struct Tab1View: View {
var body: some View {
NavigationView {
VStack {
Text("sdsd")
Color.orange
}
}
}
}
If I remove NavigationView this effect is removed also. So my question is - why do i have this unexpected animation? what done wrong?
Here is fix (tested with Xcode 12.1 / iOS 14.1)
struct Tab1View: View {
var body: some View {
GeometryReader { g in
NavigationView {
VStack {
Text("sdsd")
Color.orange
}
}
.animation(.none) // << this one !!
}
}
}

Multiple animations do not work as expected in SwiftUI

I want to create a pulsing circle with forever repeating animation. It should have a gradual change of size and a momentary change of color when animation direction changes.
I have two different animations for that, but only the last one having an effect on both (size and color change).
struct SwiftUIView: View {
#State private var animationStarted = false
let side = UIScreen.main.bounds.size.width
let animation1 = Animation.easeInOut(duration: 0.3).delay(5.7)
.repeatForever(autoreverses: true)
let animation2 = Animation.easeInOut(duration: 6)
.repeatForever(autoreverses: true)
var body: some View {
HStack {
VStack {
HStack {
Circle()
.foregroundColor(self.animationStarted ? Color.pink : Color.blue)
.animation(animation1)
.frame(width: self.animationStarted ? 10 : side, height: self.animationStarted ? 10 : side)
.animation(animation2)
.onAppear {
self.animationStarted.toggle()
}
}
}.frame(height: side)
}.frame(width: side)
}
}

Resources