How to animate navigationBarHidden in SwiftUI? - swift

struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1, perform: {
withAnimation {
self.hideNavigationBar.toggle()
}
})
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
}
}
}
When you tap the red rectangle it snaps the navigation bar away. I thought withAnimation{} would fix this, but it doesn't. In UIKit you would do something like this navigationController?.setNavigationBarHidden(true, animated: true).
Tested in xCode 12 beta 6 and xCode 11.7

You could try using
.navigationBarHidden(hideNavigationBar).animation(.linear(duration: 0.5)) instead of .navigationBarHidden(hideNavigationBar)
and also move self.hideNavigationBar.toggle() out of the animation block. That is not required if you use the above approach for hiding of navigation bar with animation.

I think, the only solution is to use a position function in SwiftUI 2
var body: some View {
GeometryReader { geometry in
NavigationView {
ZStack {
Color("background")
.ignoresSafeArea()
// ContentView
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: logo, trailing: barButtonItems)
.toolbar {
ToolbarItem(placement: .principal) {
SearchBarButton(placeholder: LocalizedStringKey("home_vc.search_bar.placeholder"))
.opacity(isNavigationBarHidden ? 0 : 1)
.animation(.easeInOut(duration: data.duration))
}
}
}
.frame(height: geometry.size.height + (isNavigationBarHidden ? 70 : 0))
// This is the key ⬇
.position(x: geometry.size.width/2, y: geometry.size.height/2 - (isNavigationBarHidden ? 35 : 0))
.animation(.easeInOut(duration: 0.38))
.onTapGesture {
isNavigationBarHidden.toggle()
}
}
}

I'm still learning animation in SwiftUI but at this stage, I understand that you must animate the parent view.
So your code would become...
struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
.animation(.spring()) // for example
}
}
}
Note that the last argument in any function call can be placed into a single closure.
So...
.onTapGesture(count: 1, perform: {
self.hideNavigationBar.toggle()
})
can become...
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
Simpler syntax in my humble opinion.

Related

How to get different components to have different animations?

I have two simple ui components a Rectangle and an Image.
I just want a slide animation for Rectangle and scale animation for Image.
However, I got a default value animation for both of these.
Is there a problem in my code? I don't have any error. I'm using beta SF Symbols tho. Could this be the problem?
import SwiftUI
struct ContentView: View {
#State var animate: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack() {
if animate {
ZStack() {
Rectangle()
.frame(height: 50)
.transition(.slide)
Image(systemName: "figure.mixed.cardio")
.foregroundColor(.white)
.transition(.scale)
}
}
}
Button("animate") {
withAnimation {
animate.toggle()
}
}
}
}
}
Transition works when specific view appeared, but in your code ZStack loads views and then appears as a whole.
Here is a fix:
HStack() {
ZStack() {
if animate { // << here !!
Rectangle()
.frame(height: 50)
.transition(.slide)
Image(systemName: "figure.mixed.cardio")
.foregroundColor(.white)
.transition(.scale)
}
}
}
The problem here is neither your SF Symbols or Compiling Error.
Animation is a bit tricky especially if you want different animations for different views inside the same stack.
To achieve your goal, you need to wrap each sub view inside an independent stack, then they will have their own unique animation.
Try this code:
import SwiftUI
struct TestView: View {
#State var animate: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack() {
ZStack() {
//added
if animate {
ZStack {
Rectangle()
.frame(height: 50)
} .transition(.slide)
}
//added
if animate {
ZStack {
Image(systemName: "figure.mixed.cardio")
.foregroundColor(.white)
}.transition(.scale)
}
}
}
Button("animate") {
withAnimation {
animate.toggle()
}
}
}
}
}

ZStack blocks animation SwiftUI

So my goal is to be able to show a custom view from time to time over a SwiftUI tabview, so I thought I would place them both in a ZStack like this
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation(Animation.linear(duration: 10)) {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
}
This works just fine, but when I try to use withAnimation() no animation gets triggered. How can I make the overlaying view, disappear with animation?
Use .animation modifier with container, like below, so container could animate removing view
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
show = false // << withAnimation not needed anymore
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
.animation(Animation.linear(duration: 10), value: show) // << here !!
So I found a solution and what I think is the cause of this. My hypothesis is that the animation modifier does not handle ZIndex IF it is not explicitly set.
One solution to this is to set ZIndex to the view that should be on the top to something higher than the other view. Like this:
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
.zIndex(.infinity) // <-- this here makes the animation work
}
}
}

Choppy Animation SwiftUI Nested Views

I'm working on an animation that brings up a view from the bottom of the screen above part of the view that previously occupied the screen. My code is technically working, though I'm concerned that the animation looks too choppy. Basically, what I think is happening is that the new, rising view is composed of several other views, and when I animate it coming up, it also animates the sub-views coming together--something I don't like the look of.
Sample Code:
struct ButtonView: View {
#State var show: Bool = false
var body: some View {
ZStack{
VStack {
Button(action: { withAnimation(.linear(duration: 0.5)) { show = !show }} ) {
Text("Press Me")
}
Rectangle()
.foregroundColor(.gray)
}
}
if show {
VStack {
CollapsibleView()
}
}
}
}
struct CollapsibleView: View {
var body: some View {
ZStack {
VStack {
Text("Text 1")
Text("Text 2")
Text("Text 3")
}
.background(Color.white)
}
}
}
Note that the duration is set to be quite long for illustration purposes, but even at smaller duration values I can still notice the choppy effect.
How do I avoid this? Is there a way to just animate the motion?
Here a way for what you may looking for:
struct ContentView: View {
#State var show: Bool = Bool()
var body: some View {
VStack {
Button(action: { show.toggle() }, label: { show ? Text("hide") : Text("show") })
.animation(nil)
Color.gray
Group {
if show { CollapsibleView().transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .bottom))) }
}
.opacity(show ? 1.0 : 0.0)
}
.animation(Animation.spring(response: 0.4, dampingFraction: 0.4, blendDuration: 1.0), value: show)
}
}
struct CollapsibleView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Text 1")
Text("Text 2")
Text("Text 3")
}
.background(Color.white)
}
}

How to animate view appearance?

I have a custom view. It used as a cell in a List view. I would like to animate appearance of a Group subview on quote.expanded = true (e.g. fading).
.animation(.default) modifier does not work.
struct QuoteView: View {
var quote : QuoteDataModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(quote.latin)
.font(.title)
if quote.expanded {
Group() {
Divider()
Text(quote.russian).font(.body)
}
}
}
}
}
The following code animates for me. Note that animation inside a list, while still probably better than no animation, can still look kind of weird. This is because the height of the list rows themselves do not animate, and snap to their final height, while the view inside the row does animate. This is a SwiftUI issue, and there's not anything you can do about it for now other than file feedback that this behavior doesn't look great.
struct StackOverflowTests: View {
#State private var array = [QuoteDataModel(), QuoteDataModel(), QuoteDataModel()]
var body: some View {
List {
ForEach(array.indices, id: \.self) { index in
QuoteView(quote: self.array[index])
.onTapGesture { self.array[index].expanded.toggle() }
}
}
}
}
struct QuoteView: View {
var quote : QuoteDataModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(quote.latin)
.font(.title)
if quote.expanded {
Group() {
Divider()
Text(quote.russian).font(.body)
}
}
}
.animation(.default)
}
}
Use a Transition to animate the view Appearance:
https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-and-remove-views-with-a-transition
try this....but you will see, that there are still other problems, because the text is left aligned...
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Button("Tap me") {
withAnimation() {
self.expanded.toggle()
if self.expanded {
self.opacity = 1
} else {
self.opacity = 0
}
}
}
Text("aha")
.font(.title)
if expanded {
Group() {
Divider()
Text("oho").font(.body)
}.opacity(opacity)
}
}
}

Transition with a view with SwiftUI without animating the entire view

Using SwiftUI, I have a view that includes a slider that I'm using a transition to slide in from the bottom. All works well, until the slider is moved quickly back and forth. With that, the text field is being animated, and will show "..." when changing from 1 to two digits.
Here is my test code showing this:
struct TestSliderView: View {
#State private var val: Double = 0
#State private var showSlider: Bool = false
var body: some View {
VStack {
Button(action: {
self.showSlider.toggle()
}) {
Text("Show Slider")
}
Spacer()
if showSlider {
JustTheSlider(val: $val)
.padding()
.transition(.move(edge: .bottom))
.animation(.linear(duration: 0.4))
}
}
}
}
struct JustTheSlider: View {
#Binding var val: Double
var body: some View {
VStack {
Text("Slider")
.font(.title)
HStack {
Text("Value: ")
.frame(minWidth: 80, alignment: .leading)
Slider(value: $val, in: 0...30, step: 1)
Text("\(Int(val))")
.frame(minWidth: 20, alignment: .trailing)
.font(Font.body.monospacedDigit())
.padding(.horizontal)
}
}
}
}
One way around this would be to remove the .animation(.linear(duration: 0.4)) line and wrap the button action with an animation like so:
Button(action: {
withAnimation(.linear(duration: 0.4)) {
self.showSlider.toggle()
}
}) {
Text("Show Slider")
}
This stops the text from animating, but then the view only slides out, and just pops in without any slide animation.
Any ideas?
You need animate the state variable, not the View.
var body: some View {
VStack {
Button(action: {
withAnimation{
self.showSlider.toggle()}
}) {
Text("Show Slider")
}
Spacer()
if showSlider {
JustTheSlider(val: $val)
.padding()
.transition(.move(edge: .bottom))
}
}
}
As the last line shown.