SwiftUI: fade out view - ios

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"

Related

Draw a view over the navigation bar

I'm working on a bottom sheet that can be invoked from any other screen. The bottom sheet will be displayed on top of a half-opaque overlay and I would like the overlay to render full screen over any other view including the navigation bar and the tab bar.
However, I can't seem to be able to figure out how to get the content of the navigation bar to be behind the overlay. Here is what a demo of my current implementation looks like. As you can see, it's possible to interact with the content of the navigation bar even though it is visually displayed behind the overlay.
Half Screen
Full Screen
Back button is still active
And here is the simplified code of my current implementation:
import SwiftUI
struct MainNavigationView: View {
var body: some View {
NavigationView {
NavigationLink(destination: AnoterView()) {
Text("Navigate to the next screen")
}
}
}
}
struct AnoterView: View {
var body: some View {
ZStack {
Color(uiColor: .red)
.edgesIgnoringSafeArea(.all)
.navigationTitle("Test")
.navigationBarTitleDisplayMode(.inline)
ViewWithOverlay()
}
}
}
struct ViewWithOverlay: View {
var body: some View {
ZStack {
// I'd like this overlay to be rendered over the navigation bar
Color(uiColor: .blue)
.edgesIgnoringSafeArea(.all)
Color(uiColor: .green)
}
}
}
And the outcome:
As you can see, while the blue color, which represent my overlay, is drawn over the red color, the title and the back button are still displayed on top of the blue color.
I understand why this is happening, but I cannot think of any workaround in SwiftUI to fix this that can be invoked from any view.
Any help is appreciated.
If you want to overlay everything then it should be on root, including over NavigationView as well, ie.
ZStack {
NavigationView {
Color(uiColor: .red).edgesIgnoringSafeArea(.all)
}
ViewWithOverlay() // << here !!
}
.edgesIgnoringSafeArea(.all)
One thing you can do is to put the NavigationView inside a ZStack. This way it will be in a lower layer hidden by the layer above. Here is the code that completely hides the NavigationBar on the tap of the button.
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
ZStack {
NavigationView {
Text("Hello World")
.navigationTitle("Welcome")
}
VStack {
}.frame(maxWidth: isPresented ? .infinity: 0, maxHeight: isPresented ? .infinity: 0)
.background(.green)
Button("Animate") {
withAnimation {
isPresented.toggle()
}
}
}
}
}

How to replace the current view in SwiftUI?

I am developing an app with SwiftUI.
I have a NavigationView and I have buttons on the navigation bar. I want to replace the current view (which is a result of a TabView selection) with another one.
Basically, when the user clicks "Edit" button, I want to replace the view with another view to make the edition and when the user is done, the previous view is restored by clicking on a "Done" button.
I could just use a variable to dynamically choose which view is displayed on the current tab view, but I feel like this isn't the "right way to do" in SwiftUI. And this way I could not apply any transition visual effect.
Some code samples to explain what I am looking for.
private extension ContentView {
#ViewBuilder
var navigationBarLeadingItems: some View {
if tabSelection == 3 {
Button(action: {
print("Edit pressed")
// Here I want to replace the tabSelection 3 view by another view temporarly and update the navigation bar items
}) {
Text("Edit")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
ContactPage()
.tabItem {
Text("1")
}
.tag(1)
Text("Chats")
.tabItem() {
Text("2")
}
.tag(2)
SettingsView()
.tabItem {
Text("3")
}
.tag(3)
}.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
Thank you
EDIT
I have a working version where I simply update a toggle variable in my button action that makes my view display one or another thing, it is working but I cannot apply any animation effect on it, and it doesn't look "right" in SwiftUI, I guess there is something better that I do not know.
If you just want to add animations you can try:
struct ContentView: View {
...
#State var showEditView = false
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
...
view3
.tabItem {
Text("3")
}
.tag(3)
}
.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
private extension ContentView {
var view3: some View {
VStack {
if showEditView {
FormView()
.background(Color.red)
.transition(.slide)
} else {
Text("View 3")
.background(Color.blue)
.transition(.slide)
}
}
}
}
struct FormView: View {
var body: some View {
Form {
Text("test")
}
}
}
A possible alternative is to use a ViewRouter: How To Navigate Between Views In SwiftUI By Using An #EnvironmentObject.

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.

Combining an animated view with another view using ZStack

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

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