Combining an animated view with another view using ZStack - ios

I'm working on a WatchOS-app that needs to display an animation on top of another view while waiting for a task to finish.
My approach is the following (ConnectionView):
struct ConnectionView: View{
#EnvironmentObject var isConnected : Bool
var body: some View {
return VStack(alignment: .trailing){
ZStack{
ScrollView{
.....
}
if(!isConnected){
ConnectionLoadingView()
}
}
}
}
}
And for the ConnectionLoadingView:
struct ConnectionLoadView: View {
#State var isSpinning = false
#EnvironmentObject var isConnected : Bool
var body: some View {
var animation : Animation
///This is needed in order to make the animation stop when isConnected is true
if(!isConnected){
animation = Animation.linear(duration: 4.0)
}else{
animation = Animation.linear(duration: 0)
}
return Image(systemName: "arrowtriangle.left.fill")
.resizable()
.frame(width: 100, height: 100)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.animation(animation)
.foregroundColor(.green)
.onAppear(){
self.isSpinning = true
}
.onDisappear(){
self.isSpinning = false
}
}
}
The real problem consists of two parts:
On the very first ConnectionView that is displayed after the app is started, the ConnectionLoadView is displayed properly. On the subsequent runs the ConnectionLoadView has a weird "fade in" effect where it changes it's opacity throughout the animation (doesn't matter if I set the opacity for the view to 1, 0 or anything inbetween).
If I don't have the following code snippet in ConnectionLoadView:
if(!isConnected){
animation = Animation.linear(duration: 4.0)
}else{
animation = Animation.linear(duration: 0)
}
Without this the ConnectionView will continue to play the animation but move it from the foreground to background of the ZStack, behind the ScrollView, when it should just disappear straight away? Without this code snippet, the animation will only disappear as it should if the animation has stopped before the task has finished.
Is there any reason why the ConnectionLoadView is pushed to the background of the ZStack instead of just being removed from the view altogether when I clearly state that it should only be displayed if and only if !isConnected in ConnectionView?
I also can't quite figure out why there is a difference between the animation behaviour of the initial ConnectionView and the subsequent ones regarding the the opacity behaviour. Is the opacity changing part of the linear-animation?
Thanks!

You are approaching the animation wrong. You shouldn't use implicit animations for this. Explicit animations are better suited.
Implicit animations are the ones you apply with .animation(). This affect any animatable parameter that changes on a view.
Explicit animations are the ones you trigger with withAnimation { ... }. Only parameters affected by the variables modified inside the closure, are the ones that will animate. The rest will not.
The new code looks like this:
import SwiftUI
class Model: ObservableObject {
#Published var isConnected = false
}
struct Progress: View{
#EnvironmentObject var model: Model
var body: some View {
return VStack(alignment: .trailing){
ZStack{
ScrollView{
ForEach(0..<3) { idx in
Text("Some line of text in row # \(idx)")
}
Button("connect") {
self.model.isConnected = true
}
Button("disconect") {
self.model.isConnected = false
}
}
if !self.model.isConnected {
ConnectionLoadView()
}
}
}
}
}
struct ConnectionLoadView: View {
#State var isSpinning = false
#EnvironmentObject var model: Model
var body: some View {
return Image(systemName: "arrowtriangle.left.fill")
.resizable()
.frame(width: 100, height: 100)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.foregroundColor(.green)
.onAppear(){
withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) {
self.isSpinning = true
}
}
}
}

Related

SwiftUI animations are not working inside List

In SwiftUI the animations inside a List are not functioning properly. However, when I replace the List with a ScrollView and a LazyVStack, the animations perform as expected. Is there a solution to fix this? I don't want to switch to LazyVStack because I'm using onMove and onDelete modifiers and some other List-specific stuff.
Environment: Xcode 14.2
struct SomeView: View {
#State var showColor = false
var body: some View {
List {
if showColor {
Color.green
.frame(width: 200, height: 200)
.transition(.scale)
}
Button {
withAnimation {
showColor.toggle()
}
} label: {
Text("show/hide color")
}
}
}
}
Due to the way that the frame is updated for a list item, I am not really sure if there is a way to achieve the desired animation.
Two improvements I can think of, however, would be to add a VStack around the text and the color like so:
struct SomeView: View {
#State var showColor = false
var body: some View {
List {
VStack {
if showColor {
Color.green
.frame(width: 200, height: 200)
.transition(.scale)
}
Button {
withAnimation {
showColor.toggle()
}
} label: {
Text("show/hide color")
}
}
}
}
}
An alternative improvement, which would make the animation smoother, would be to replace the if statement with a "conditional" frame like so.
struct ContentView: View {
#State var showColor = false
var body: some View {
List {
Color.green
.frame(width: showColor ? 200 : 0, height: showColor ? 200 : 0)
.transition(.scale)
Button {
withAnimation {
showColor.toggle()
}
} label: {
Text("show/hide color")
}
}
}
}
By using a combination of both of those, you should be able to receive the best results for your use case. If the animations are still not good enough, I would consider changing the layout or not using a list.

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"

How to despawn a Button and spawn a scrollView xcode swiftui [duplicate]

How do I toggle the presence of a button to be hidden or not?
We have the non-conditional .hidden() property; but I need the conditional version.
Note: we do have the .disabled(bool) property available, but not the .hidden(bool).
struct ContentView: View {
var body: some View {
ZStack {
Color("SkyBlue")
VStack {
Button("Detect") {
self.imageDetectionVM.detect(self.selectedImage)
}
.padding()
.background(Color.orange)
.foreggroundColor(Color.white)
.cornerRadius(10)
.hidden() // ...I want this to be toggled.
}
}
}
}
I hope hidden modifier gets argument later, but since then, Set the alpha instead:
#State var shouldHide = false
var body: some View {
Button("Button") { self.shouldHide = true }
.opacity(shouldHide ? 0 : 1)
}
For me it worked perfectly to set the frame's height to zero when you do not want to see it. When you want to have the calculated size, just set it to nil:
SomeView
.frame(height: isVisible ? nil : 0)
If you want to disable it in addition to hiding it, you could set .disabled with the toggled boolean.
SomeView
.frame(height: isVisible ? nil : 0)
.disabled(!isVisible)
You can utilize SwiftUI's new two-way bindings and add an if-statement as:
struct ContentView: View {
#State var shouldHide = false
var body: some View {
ZStack {
Color("SkyBlue")
VStack {
if !self.$shouldHide.wrappedValue {
Button("Detect") {
self.imageDetectionVM.detect(self.selectedImage)
}
.padding()
.background(Color.orange)
.foregroundColor(Color.white)
.cornerRadius(10)
}
}
}
}
}
The benefit of doing this over setting the opacity to 0 is that it will remove the weird spacing/padding from your UI caused from the button still being in the view, just not visible (if the button is between other view components, that is).
all the answers here works specifically for a button to be hidden conditionally.
What i think might help is making a modifier itself conditionally e.g:
.hidden for button/view, or maybe .italic for text, etc..
Using extensions.
For text to be conditionally italic it is easy since .italic modifier returns Text:
extension Text {
func italicConditionally(isItalic: Bool) -> Text {
isItalic ? self.italic() : self
}
}
then applying conditional italic like this:
#State private var toggle = false
Text("My Text")
.italicConditionally(isItalic: toggle)
However for Button it is tricky, since the .hidden modifier returns "some view":
extension View {
func hiddenConditionally(isHidden: Bool) -> some View {
isHidden ? AnyView(self.hidden()) : AnyView(self)
}
}
then applying conditional hidden like this:
#State private var toggle = false
Button("myButton", action: myAction)
.hiddenConditionally(isHidden: toggle)
You can easily hide a view in SwiftUI using a conditional statement.
struct TestView: View{
#State private var isVisible = false
var body: some View{
if !isVisible {
HStack{
Button(action: {
isVisible.toggle()
// after click you'r view will be hidden
}){
Text("any view")
}
}
}
}
}
It isn't always going to be a pretty solution, but in some cases, adding it conditionally may also work:
if shouldShowMyButton {
Button(action: {
self.imageDetectionVM.detect(self.selectedImage)
}) {
Text("Button")
}
}
There will be an issue of the empty space in the case when it isn't being shown, which may be more or less of an issue depending on the specific layout. That might be addressed by adding an else statement that alternatively adds an equivalently sized blank space.
#State private var isHidden = true
VStack / HStack
if isHidden {
Button {
if !loadVideo(),
let urlStr = drill?.videoURL as? String,
let url = URL(string: urlStr) {
player = VideoPlayerView(player: AVPlayer(), videoUrl: url)
playVideo.toggle()
}
} label: {
Image(playVideo ? "ic_close_blue" : "ic_video_attached")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
}
.buttonStyle(BorderlessButtonStyle())
}
.onAppear {
if shouldShowButton {
isHidden = false
} else {
isVideoButtonHidden = true
}
}

How to Animate Binding of Child View

A bit confused as to why this code does not work as expected.
Some generic animatable view:
struct SomeAnimatableView: View, Animatable {
var percent: Double
var body: some View {
GeometryReader { geo in
ZStack {
Rectangle()
.fill(Color.blue)
.frame(height: 120)
Rectangle()
.fill(Color.red)
.frame(width: geo.size.width * CGFloat(self.percent), height: 116)
}
.frame(width: geo.size.width, height: geo.size.height)
}
}
var animatableData: Double {
set { percent = newValue }
get { percent }
}
}
A view which wraps said animatable view:
struct ProgressRectangle: View {
#Binding var percent: Double
var body: some View {
SomeAnimatableView(percent: percent)
}
}
Finally another view, consider it parent:
struct ContentView: View {
#State var percent: Double
var body: some View {
VStack {
ProgressRectangle(percent: $percent.animation())
Text("Percent: \(percent * 100)")
}
.onTapGesture {
self.percent = 1
}
}
}
Issue ⚠️ why doesn't the progress animate? $percent.animation() isn't that supposed cause animations? Lots of questions...
A fix 💡:
.onTapGesture {
withAnimation {
self.percent = 1
}
}
However, this doesn't actually fix the issue or answer the question to why that binding doesn't animate. Because this also seems to animate the entire view, notice the button animating its width in this example:
Thoughts, ideas?
Update: additional clarification
Animation to Binding is a mechanism to transfer animation inside child so when bound variable is changed internally it would be changed with animation.
Thus for considered scenario ProgressRectangle would look like
struct ProgressRectangle: View {
#Binding var percent: Double
var body: some View {
SomeAnimatableView(percent: percent)
.onTapGesture {
withAnimation(_percent.transaction.animation) { // << here !!
self.percent = 0 == self.percent ? 0.8 : 0
}
}
}
}
without onTapGesture in ContentView (similarly as would you have Toggle which changes value internally).
Test module on GitHub
Original (still valid and works)
Here is working solution. Tested with Xcode 11.4 / iOS 13.4
#State var percent: Double = .zero
var body: some View {
VStack {
ProgressRectangle(percent: $percent)
.animation(.default) // << place here !!
.onTapGesture {
self.percent = self.percent == .zero ? 1 : .zero
}
Text("Percent: \(percent * 100)")
}
}

Transition animation not working in SwiftUI

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()
}
}
}

Resources