Transition animation not working in SwiftUI - ios

I'm trying to create a really simple transition animation that shows/hides a message in the center of the screen by tapping on a button:
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
}
}
}
}
According to the documentation of the .transition(.opacity) animation
A transition from transparent to opaque on insertion, and from opaque
to transparent on removal.
the message should fade in when the showMessage state property becomes true and fade out when it becomes false. This is not true in my case. The message shows up with a fade animation, but it hides with no animation at all. Any ideas?
EDIT: See the result in the gif below taken from the simulator.

The problem is that when views come and go in a ZStack, their "zIndex" doesn't stay the same. What is happening is that the when "showMessage" goes from true to false, the VStack with the "Hello World" text is put at the bottom of the stack and the yellow color is immediately drawn over top of it. It is actually fading out but it's doing so behind the yellow color so you can't see it.
To fix it you need to explicitly specify the "zIndex" for each view in the stack so they always stay the same - like so:
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow.zIndex(0)
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}.zIndex(1)
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
.zIndex(2)
}
}
}
}

My findings are that opacity transitions don't always work. (yet a slide in combination with an .animation will work..)
.transition(.opacity) //does not always work
If I write it as a custom animation it does work:
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
.zIndex(1)

I found a bug in swiftUI_preview for animations. when you use a transition animation in code and want to see that in SwiftUI_preview it will not show animations or just show when some views disappear with animation. for solving this problem you just need to add your view in preview in a VStack. like this :
struct test_UI: View {
#State var isShowSideBar = false
var body: some View {
ZStack {
Button("ShowMenu") {
withAnimation {
isShowSideBar.toggle()
}
}
if isShowSideBar {
SideBarView()
.transition(.slide)
}
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SomeView()
}
}
}
after this, all animations will happen.

I believe this is a problem with the canvas. I was playing around with transitions this morning and while the don't work on the canvas, they DO seem to work in the simulator. Give that a try. I've reported the bug to Apple.

I like Scott Gribben's answer better (see below), but since I cannot delete this one (due to the green check), I'll just leave the original answer untouched. I would argue though, that I do consider it a bug. One would expect the zIndex to be implicitly assigned by the order views appear in code.
To work around it, you may embed the if statement inside a VStack.
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}
VStack {
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
}
}
}
}
}

zIndex may cause the animation to be broken when interrupted. Wrap the view you wanna apply transition to in a VStack, HStack or any other container will make sense.

I just gave up on .transition. It's just not working. I instead animated the view's offset, much more reliable:
First I create a state variable for offset:
#State private var offset: CGFloat = 200
Second, I set the VStack's offset to it. Then, in its .onAppear(), I change the offset back to 0 with animation:
VStack{
Spacer()
HStack{
Spacer()
Image("MyImage")
}
}
.offset(x: offset)
.onAppear {
withAnimation(.easeOut(duration: 2.5)) {
offset = 0
}
}

Below code should work.
import SwiftUI
struct SwiftUITest: View {
#State private var isAnimated:Bool = false
var body: some View {
ZStack(alignment:.bottom) {
VStack{
Spacer()
Button("Slide View"){
withAnimation(.easeInOut) {
isAnimated.toggle()
}
}
Spacer()
Spacer()
}
if isAnimated {
RoundedRectangle(cornerRadius: 16).frame(height: UIScreen.main.bounds.height/2)
.transition(.slide)
}
}.ignoresSafeArea()
}
}
struct SwiftUITest_Previews: PreviewProvider {
static var previews: some View {
VStack {
SwiftUITest()
}
}
}

Related

SwiftUI: fade out view

I have the following code:
struct ContentView: View {
#State var show = false
var body: some View {
VStack {
ZStack {
Color.black
if show {
RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.transition(.opacity)
}
}
Button {
withAnimation(.easeInOut(duration: 1)) {
show.toggle()
}
} label: {
Text("TRIGGER")
}
}
}
}
I want the RoundedRectangle to fade in and out. Right now it only fades in. This is a simplified version of a more complex view setup I have. Depending on the state I may have the view I want to fade in or not. So, I am looking for a way to fade in (like it works now) but then also fade out so that the view is totally removed from the hierarchy and not just hidden or something.
How can I have this code also fade OUT the view and not only fade in?
As a reference I followed this approach:
https://swiftui-lab.com/advanced-transitions/
....
if show {
LabelView()
.animation(.easeInOut(duration: 1.0))
.transition(.opacity)
}
Spacer()
Button("Animate") {
self.show.toggle()
}.padding(20)
....
But, in my case it is NOT fading out.
SwiftUI ZStack transitions are finicky. You need to add a zIndex to make sure the hierarchy is preserved, enabling the animation.
RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.transition(.opacity)
.zIndex(1) /// here!
You need to link the opacity directly to the state, so that it is directly animating any changes.
struct ContentView: View {
#State var show = false
var body: some View {
VStack {
ZStack {
Color.black
(RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.opacity(show ? 1 : 0)
)
}
Button {
withAnimation(.easeInOut(duration: 1)) {
show.toggle()
}
} label: {
Text("TRIGGER")
}
}
}
}
EDIT: to reflect the comment requiring the view to be removed, not just faded out...
To remove the view (and trigger .onDisappear) you could modify as below:
ZStack {
Color.black
show ? (RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.zIndex(1). //kudos to #aheze for this!
).onDisappear{print("gone")}
: nil
}
This will fade in/out as above, but will actually remove the view & print "gone"

Transitions not working inside NavigationView

I am trying to navigate between two views without a NavigationLink.
Here is the code:
import SwiftUI
struct NextView: View {
#State var text = ""
#Binding var displayView: Bool
var body: some View {
// 3
//NavigationView {
VStack {
Spacer()
TextField("Type something!", text: $text)
Button("Remove View") {
withAnimation {
displayView = false
}
}
Spacer()
}.background(Color.cyan)
//}
}
}
struct InitialView: View {
#State var displayView = false
var body: some View {
// 1
//NavigationView {
if displayView {
//2
//NavigationView {
NextView(displayView: $displayView)
.transition(.slide)
//}
} else {
Button("Tap to continue") {
withAnimation {
displayView = true
}
}
}
//}
}
}
I tried to place the NavigationView in 3 different places, one at a time. I managed to get the animation I wanted only by placing the structure in position 3 or not using it. Could anyone tell me why this happens and other possible solutions to use the NavigationView in position 1?
I tested only in the iPhone 12 simulator and am using XCode Version 13.4.1.
I think you just wanted this - animatable transition
struct InitialView: View {
#State var displayView = false
var body: some View {
NavigationView { // << just root view, not important
ZStack { // << owner container !!
if displayView {
NextView(displayView: $displayView)
.transition(.slide)
} else {
Button("Tap to continue") {
displayView = true
}
}
}
.animation(.default, value: displayView) // << animation !!
}
}
}
Tested with Xcode 13.4 / iOS 15.5
This is a tricky question. If you look closely at your code, you see that in postion 3 .transition is applied to the NavigationView. However, in positions 1 & 2 it’s not.
Transitions must be applied to the View that is transitioning, in your case, it’s the NavigationView that is wrapping the content. Implying that the NavigationView needs the modifier.
Add the transition to the NavigationView in any position & it should work as expected.

ZStack is not changing color

I've added Color.orange to my ZStack - but my view still has the default white/greyish background:
struct Settings: View {
#State var minAge = UserSettings().minAge
#State var maxAge = UserSettings().maxAge
#State var chosenSeeking = UserSettings.Seeking.both
var body: some View {
ZStack {
Color.orange
VStack {
NavigationView {
Form {
Section {
Picker("Look for", selection: $chosenSeeking) {
ForEach(UserSettings.Seeking.allCases) { i in
Text(String(i.rawValue))
}
}
}
Section {
Text("Min age")
Slider(value: $minAge, in: 18...99, step: 1, label: {Text("Label")})
Text(String(Int(minAge)))
}
Section {
Text("Max age")
Slider(value: $maxAge, in: 18...99, step: 1)
Text(String(Int(maxAge)))
}
}.navigationBarTitle(Text("Settings"))
}
}
}
}
}
Any idea what the problem is?
Best I could find was the colorMultiply:
NavigationView {
...
}.colorMultiply(.orange)
Could you try editing your code below format?
I put ZStack under NavigationView, and in this case, the background color changes to orange.
NavigationView{
ZStack{
Color.orange.edgesIgnoringSafeArea(.all)
VStack{
//some code
}
}
}
Your problem is that your NavigationView blocks the orange color. You would have to change the background of the NavigationView itself. With default views such as NavigationView, this is typically done by implementing a custom style of that view. In the case of Button that would be ButtonStyle. NavigationView does have NavigationViewStyle, however this is not yet publicly available. Our best hope might be the next major SwiftUI iteration, which will most likely be announced at WWDC this month.

SwiftUI add subview dynamically but the animation doesn't work

I would like to create a view in SwiftUI that add a subview dynamically and with animation.
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
if isButtonVisible {
AnyView(DetailView())
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}else{
AnyView(Text("test"))
}
}
}
}
The above code works fine with the animation . however when i move the view selection part into a function, the animation is not working anymore (since i want to add different views dynamically, therefore, I put the logic in a function.)
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView().transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
func subView() -> some View {
if isButtonVisible {
return AnyView(DetailView())
}else{
return AnyView(Text("test"))
}
}
}
it looks totally the same to me, however, i don't understand why they have different result. Could somebody explain me why? and any better solutions? thanks alot!
Here's your code, modified so that it works:
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView()
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
}
func subView() -> some View {
Group {
if isButtonVisible {
DetailView()
} else {
Text("test")
}
}
}
}
Note two things:
Your two examples above are different, which is why you get different results. The first applies a transition and animation to a DetailView, then type-erases it with AnyView. The second type-erases a DetailView with AnyView, then applies a transition and animation.
Rather that using AnyView and type-erasure, I prefer to encapsulate the conditional logic inside of a Group view. Then the type you return is Group, which will animate properly.
If you wanted different animations on the two possibilities for your subview, you can now apply them directly to DetailView() or Text("test").
Update
The Group method will only work with if, elseif, and else statements. If you want to use a switch, you will have to wrap each branch in AnyView(). However, this breaks transitions/animations. Using switch and setting custom animations is currently not possible.
I was able to get it to work with a switch statement by wrapping the function that returns an AnyView in a VStack. I also had to give the AnyView an .id so SwiftUI can know when it changes. This is on Xcode 11.3 and iOS 13.3
struct EnumView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
view(for: viewModel.viewState)
.id(viewModel.viewState)
.transition(.opacity)
}
}
func view(for viewState: ViewModel.ViewState) -> AnyView {
switch viewState {
case .loading:
return AnyView(LoadingStateView(viewModel: self.viewModel))
case .dataLoaded:
return AnyView(LoadedStateView(viewModel: self.viewModel))
case let .error(error):
return AnyView(ErrorView(error: error, viewModel: viewModel))
}
}
}
Also for my example in the ViewModel I need to wrap the viewState changes in a withAnimation block
withAnimation {
self.viewState = .loading
}
In iOS 14 they added the possibility to use if let and switch statements in function builders. Maybe it helps for your issues:
https://www.hackingwithswift.com/articles/221/whats-new-in-swiftui-for-ios-14 (at the article's bottom)

Push, Segue, Summon, Navigate to View Programmatically SwiftUI

I'm trying to do the simplest of things. I just want to summon a new SwiftUI view programmatically - not with a button, but with code. I've read a couple of dozens posts and Apple docs on this - but almost all that I've found relates to code that has been renamed or deprecated. The closest I have found is:
NavigationLink(destination: NewView(), isActive: $something) {
EmptyView()
}
But this does not work for me in Xcode Beta 7. Here's the trivial app:
struct ContentView: View {
#State private var show = false
var body: some View {
VStack {
Text("This is the ContentView")
Toggle(isOn: $show) {
Text("Toggle var show")
}
.padding()
Button(action: {
self.show = !self.show
}, label: {
Text(self.show ? "Off" : "On")
})
Text(String(show))
//this does not work - the ContentView is still shown
NavigationLink(destination: SecondView(), isActive: $show)
{
EmptyView()
}
//this does not work - it adds SecondView to ContentView
//I want a new view here, not an addition
//to the ContentView()
// if show {
// //I want a new view here, not an addition to the ContentView()
// SecondView()
// }
}
}
}
And the brutally simple destination:
struct SecondView: View {
var body: some View {
Text("this is the second view!")
}
}
I must be missing something extremely simple. Any guidance would be appreciated.
iOS 13.1, Catalina 19A546d, Xcode 11M392r
A couple of things. First, NavigationLink must be imbedded in a NavigationView to work. Second, the link doesn't need a view as you showed it. This should show the second view. I will leave to you to update the other elements.
var body: some View {
NavigationView{
VStack {
Text("This is the ContentView")
Toggle(isOn: $show) {
Text("Toggle var show")
}
.padding()
Button(action: {
self.show = !self.show
}, label: {
Text(self.show ? "Off" : "On")
})
Text(String(show))
//this does not work - the ContentView is still shown
NavigationLink(destination: SecondView()){
Text("Click to View")}
Spacer()
// {
// EmptyView()
// }
//this does not work - it adds SecondView to ContentView
//I want a new view here, not an addition
//to the ContentView()
// if show {
// //I want a new view here, not an addition to the ContentView()
// SecondView()
// }
}
}
}

Resources