What Might Be Causing This Animation Bug with Swiftui and Navigationview

What might be causing this animation bug with SwiftUI and NavigationView?

You need to use explicit animations, instead of implicit. With implicit animations, any animatable parameter that changes, the framework will animate. Whenever possible, you should use explicit animations. Below is the updated code. Notice I remove the .animation() call and added two withAnimation() closures.

If you would like to expand your knowledge on implicit vs. explicit animations, check this link: https://swiftui-lab.com/swiftui-animations-part1/

struct ContentView: View {

@State var progressToggle = false
@State var progressRingEndingValue: CGFloat = 0.75

var ringColor: Color = Color.green
var ringWidth: CGFloat = 20
var ringSize: CGFloat = 200

var body: some View {
TabView{
NavigationView{
VStack{

Spacer()

ZStack{
Circle()
.trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
.stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
.frame(width: ringSize, height: ringSize)
.rotationEffect(.degrees(-90.0))
.onAppear() {
withAnimation(.easeInOut(duration: 1)) {
self.progressToggle.toggle()
}
}

Text("\(Int(progressRingEndingValue * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
}

Spacer()

Button(action: {
withAnimation(.easeInOut(duration: 1)) {
self.progressRingEndingValue = CGFloat.random(in: 0...1)
}
}) { Text("Randomize")
.font(.largeTitle)
.foregroundColor(ringColor)
}

Spacer()

}
.navigationBarTitle("ProgressRing", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Refresh Button Tapped")
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(Color.green)
}, trailing:
Button(action: {
print("Share Button Tapped")
}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(Color.green)
}
)
}
}
}
}

SwiftUI: Broken explicit animations in NavigationView?

Here is fixed part (another my answer with explanations is here).

Tested with Xcode 12 / iOS 14.

demo

struct EscapingAnimationTest_Inner: View {
@State var degrees: CGFloat = 0

var body: some View {
Circle()
.trim(from: 0.0, to: 0.3)
.stroke(Color.red, lineWidth: 5)
.rotationEffect(Angle(degrees: Double(degrees)))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: degrees)
.onAppear() {
DispatchQueue.main.async { // << here !!
degrees = 360
}
}
}
}

Update: the same will be using withAnimation

.onAppear() {
DispatchQueue.main.async {
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
degrees = 360
}
}

}

SwiftUI animation problem with a binding to a StateObject inside a NavigationView

To be honest with you, I am not sure why, but utilizing the animation modifier on the RectView allows the animation to occur without any issues.

struct RectView: View {
@Binding var isRed: Bool

var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.animation(.easeOut(duration: 1), value: isRed)
.onTapGesture { isRed.toggle() }
}
}

screen recording example

ScrollView + NavigationView animation glitch SwiftUI

Setting the top padding to 1 is breaking at least 2 major things:

  1. The scroll view does not extend under NavigationView and TabView - this making it loose the beautiful blur effect of the content that scrolls under the bars.
  2. Setting background on the scroll view will cause Large Title NavigationView to stop collapsing.

I've encountered these issues when i had to change the background color on all screens of the app i was working on.
So i did a little bit more digging and experimenting and managed to figure out a pretty nice solution to the problem.

Here is the raw solution:

We wrap the ScrollView into 2 geometry readers.

The top one is respecting the safe area - we need this one in order to read the safe area insets
The second is going full screen.

We put the scroll view into the second geometry reader - making it size to full screen.

Then we add the content using VStack, by applying safe area paddings.

At the end - we have scroll view that does not flicker and accepts background without breaking the large title of the navigation bar.

struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometryWithSafeArea in
GeometryReader { geometry in
ScrollView {
VStack {

Color.red.frame(width: 100, height: 100, alignment: .center)

ForEach(0..<5) { i in

Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)

Spacer()
}

Color.red.frame(width: 100, height: 100, alignment: .center)
}
.padding(.top, geometryWithSafeArea.safeAreaInsets.top)
.padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
.padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
.padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
}
.background(Color.yellow)
}
.edgesIgnoringSafeArea(.all)
}
.navigationBarTitle(Text("Example"))
}
}
}

The elegant solution

Since the solution is clear now - lets create an elegant solution that can be reused and applied to any existing ScrollView by just replacing the padding fix.

We create an extension of ScrollView that declares the fixFlickering function.

The logic is basically we wrap the receiver into the geometry readers and wrap its content into the VStack with the safe area paddings - that's it.

The ScrollView is used, because the compiler incorrectly infers the Content of the nested scroll view as should being the same as the receiver. Declaring AnyView explicitly will make it accept the wrapped content.

There are 2 overloads:

  • the first one does not accept any arguments and you can just call it on any of your existing scroll views, eg. you can replace the .padding(.top, 1) with .fixFlickering() - thats it.
  • the second one accept a configurator closure, which is used to give you the chance to setup the nested scroll view. Thats needed because we don't use the receiver and just wrap it, but we create a new instance of ScrollView and use only the receiver's configuration and content. In this closure you can modify the provided ScrollView in any way you would like, eg. setting a background color.
extension ScrollView {

public func fixFlickering() -> some View {

return self.fixFlickering { (scrollView) in

return scrollView
}
}

public func fixFlickering<T: View>(@ViewBuilder configurator: @escaping (ScrollView<AnyView>) -> T) -> some View {

GeometryReader { geometryWithSafeArea in
GeometryReader { geometry in
configurator(
ScrollView<AnyView>(self.axes, showsIndicators: self.showsIndicators) {
AnyView(
VStack {
self.content
}
.padding(.top, geometryWithSafeArea.safeAreaInsets.top)
.padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
.padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
.padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
)
}
)
}
.edgesIgnoringSafeArea(.all)
}
}
}

Example 1

struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)

ForEach(0..<5) { i in

Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)

Spacer()
}

Color.red.frame(width: 100, height: 100, alignment: .center)
}
}
.fixFlickering { scrollView in

scrollView
.background(Color.yellow)
}
.navigationBarTitle(Text("Example"))
}
}
}

Example 2

struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)

ForEach(0..<5) { i in

Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)

Spacer()
}

Color.red.frame(width: 100, height: 100, alignment: .center)
}
}
.fixFlickering()
.navigationBarTitle(Text("Example"))
}
}
}

Animation not working with combination of GeometryReader and NavigationView

In this case order of modifiers looks important.

The below variant works. Tested with Xcode 11.4 / iOS 13.4

struct TestView: View {

@State var offsetSwipeUp: CGFloat = 0

var body: some View{
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
return NavigationView {
GeometryReader { geometry in
Image(systemName: "chevron.up")
.animation(.easeInOut(duration: 1))
.offset(y: CGFloat(self.offsetSwipeUp))
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
.onReceive(timer){ _ in
if self.offsetSwipeUp == .zero{
self.offsetSwipeUp = -10
} else {
self.offsetSwipeUp = .zero
}
}
}
}


Related Topics



Leave a reply



Submit