SwiftUI Animation Slide In and Out - animation

I have a View that will display over another View. The view animation slides in from the right perfectly, but when I click on the Close button, the view disappears without the desired animation of sliding back to the right before disappearing.
I have tried using .opacity(self.isShowing ? 1 : 0), but then the View fades in and out, I need it to slide in and out. Other variations have not produced the desired results.
What am I doing wrong? Any guidance, even a duplicate solution (that I could not find) would be greatly appreciated.
struct NotificationView<parentView>: View where parentView: View {
#Binding var isShowing: Bool
let parentView: () -> parentView
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.parentView()
if(self.isShowing == true){
VStack {
Text("This is a test view\n")
Button(action: {
self.isShowing.toggle()
}) {
Text("Close")
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color(UIColor.systemBackground))
// .opacity(self.isShowing ? 1 : 0)
.transition(.move(edge: self.isShowing ? .trailing : .leading))
.animation(Animation.easeInOut(duration: 1.0))
}
}
}
}
}

Move conditional part into container and add animation to container, so it will animate content, like
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.parentView()
VStack { // << here !!
if(self.isShowing == true){
VStack {
Text("This is a test view\n")
Button(action: {
self.isShowing.toggle()
}) {
Text("Close")
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color(UIColor.systemBackground))
.transition(.move(edge: self.isShowing ? .trailing : .leading))
}
}.animation(Animation.easeInOut(duration: 1.0)) // << here !!
}
}
}

Related

Animating changes to #FocusState SwiftUI

I am using the new #FocusState to control how my views react to the user deciding to start inputting information into text fields. My current need is to wrap an animation around my top view leaving the screen as the keyboard moves up. Usually this kind of thing can be accomplished by simply wrapping withAnimation() around a boolean toggle, but since Swift is toggling my focus state bool under the hood, I can't wrap an animation around it in this way. How else should I do it?
Here is a minimal reproducible example. Basically I want to animate the top (red) view leaving / coming back into view with changes to my focus state isFocused var.
struct ContentView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
var body: some View {
ZStack {
VStack {
if !isFocused {
Text("How to Animate this?")
.frame(width: 300, height: 300)
.background(Color.red)
.animation(.easeInOut(duration: 5), value: isFocused)
}
Text("Middle Section")
.frame(width: 300, height: 300)
.background(Color.green)
Spacer()
TextField("placeholder", text: $text)
.focused($isFocused)
}
if isFocused {
Color.white.opacity(0.1)
.onTapGesture {
isFocused = false
}
}
}
}
}
I don't think the animation modifier that's currently on the top view is doing anything, but I imagine that that's where I'll put some animation code.
Here is something that works. I've done this before to make an animation happen upon an #FocusState property changing its value. Can't really tell you why though, it's just something I figured out with trial and error.
struct ContentView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
#State private var showRedView = false
var body: some View {
ZStack {
VStack {
if !showRedView {
Text("How to Animate this?")
.frame(width: 300, height: 300)
.background(Color.red)
}
Text("Middle Section")
.frame(width: 300, height: 300)
.background(Color.green)
Spacer()
TextField("placeholder", text: $text)
.focused($isFocused)
}
.onChange(of: isFocused) { bool in
withAnimation(.easeInOut(duration: 5)) {
showRedView = bool
}
}
if isFocused {
Color.white.opacity(0.1)
.onTapGesture {
isFocused = false
}
}
}
}
}

How to animate transition when adding view to hierarchy in SwiftUI

I am trying to build an overlay of "popover" views and animate transitioning in and out. Transitioning out works, but transitioning in doesn't -- as a popover view is added it just suddenly appears (wrong), but when a popover view is removed it slides out to the right (correct). How can I make the popover slide in (from right) when it's added to the view hierarchy in this code?
Fully functional code in iOS 14.
import SwiftUI
struct ContentView: View {
var body: some View {
Popovers()
}
}
struct Popovers : View {
#State var popovers : [AnyView] = []
var body : some View {
Button("Add a view ...") {
withAnimation {
popovers += [new()]
}
}
.blur(radius: 0 < popovers.count ? 8 : 0)
.overlay(ZStack {
ForEach(0..<self.popovers.count, id: \.self) { i in
popovers[i]
.frame(maxWidth: .infinity, maxHeight: .infinity)
.blur(radius: (i+1) < popovers.count ? 8 : 0)
.transition(.move(edge: .trailing)) // works only when popover is removed
}
})
}
func new() -> AnyView {
let popover = popovers.count
return AnyView.init(
VStack(spacing: 64) {
Button("Close") {
withAnimation {
_ = popovers.removeLast()
}
}
.font(.largeTitle)
.padding()
Button("Add") {
withAnimation {
popovers += [new()]
}
}
.font(.largeTitle)
.padding()
Text("This is popover #\(popover)")
.font(.title)
.foregroundColor(.white)
.fixedSize()
}
.background(Color.init(hue: 0.65-(Double(3*popover)/100.0), saturation: 0.3, brightness: 0.9).opacity(0.98))
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension View {
var asAnyView : AnyView {
AnyView(self)
}
}
The solution is to add instead animation to container. Tested with Xcode 12 / iOS 14.
struct Popovers : View {
#State var popovers : [AnyView] = []
var body : some View {
Button("Add a view ...") {
withAnimation {
popovers += [new()]
}
}
.blur(radius: 0 < popovers.count ? 8 : 0)
.overlay(ZStack {
ForEach(0..<self.popovers.count, id: \.self) { i in
popovers[i]
.frame(maxWidth: .infinity, maxHeight: .infinity)
.blur(radius: (i+1) < popovers.count ? 8 : 0)
.transition(.move(edge: .trailing))
}
}.animation(.default)) // << add animation to container
}
func new() -> AnyView {
let popover = popovers.count
return AnyView.init(
VStack(spacing: 64) {
Button("Close") {
_ = popovers.removeLast()
}
.font(.largeTitle)
.padding()
Button("Add") {
popovers += [new()]
}
.font(.largeTitle)
.padding()
Text("This is popover #\(popover)")
.font(.title)
.foregroundColor(.white)
.fixedSize()
}
.background(Color.init(hue: 0.65-(Double(3*popover)/100.0), saturation: 0.3, brightness: 0.9).opacity(0.98))
)
}
}

Animating a View by its height in SwiftUI

I am attempting to make a view which will animate another content view in from the bottom of the screen. The below code works, however, as the content view will have unknown height the 200 offset may not be correct. How can I get the height of the content in order to offset the view correctly?
struct Test<Content>: View where Content : View {
#State var showing: Bool = false
var content: Content
var body: some View {
VStack {
Button(action: {
withAnimation {
self.showing.toggle()
}
}) {
Text("Toggle")
}
Spacer()
HStack {
Spacer()
content
Spacer()
}
.background(Color.red)
.padding(10)
.offset(y: showing ? 200 : 0)
}
}
}
Here is possible approach to read content height directly from it during alignment...
struct Test<Content>: View where Content : View {
var content: Content
#State private var showing: Bool = false
#State private var contentHeight: CGFloat = .zero
var body: some View {
VStack {
Button(action: {
withAnimation {
self.showing.toggle()
}
}) {
Text("Toggle")
}
Spacer()
HStack {
Spacer()
content
.alignmentGuide(VerticalAlignment.center) { d in
DispatchQueue.main.async {
self.contentHeight = d.height
}
return d[VerticalAlignment.center]
}
Spacer()
}
.background(Color.red)
.padding(10)
.offset(y: showing ? contentHeight : 0)
}
}
}

SwiftUI: Custom Modal Animation

I made a custom modal using SwiftUI. It works fine, but the animation is wonky.
When played in slow motion, you can see that the ModalContent's background disappears immediately after triggering ModalOverlay's tap action. However, ModalContent's Text views stay visible the entire time.
Can anyone tell me how I can prevent ModalContent's background from prematurely disappearing?
Slow-mo video and code below:
import SwiftUI
struct ContentView: View {
#State private var isShowingModal = false
var body: some View {
GeometryReader { geometry in
ZStack {
Button(
action: { withAnimation { self.isShowingModal = true } },
label: { Text("Show Modal") }
)
ZStack {
if self.isShowingModal {
ModalOverlay(tapAction: { withAnimation { self.isShowingModal = false } })
ModalContent().transition(.move(edge: .bottom))
}
}.edgesIgnoringSafeArea(.all)
}
}
}
}
struct ModalOverlay: View {
var color = Color.black.opacity(0.4)
var tapAction: (() -> Void)? = nil
var body: some View {
color.onTapGesture { self.tapAction?() }
}
}
struct ModalContent: View {
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
VStack(spacing: 16) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.frame(width: geometry.size.width)
.padding(.top, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.background(Color.white)
}
}
}
}
The solution (thanks to #JWK):
It's probably a bug. It seems that, during the transition animation (when the views are disappearing) the zIndex of the two views involved (the ModalContent and the ModalOverlay) is not respected. The ModalContent (that is supposed to be in front of the ModalOverlay) is actually moved under the ModalOverlay at the beginning of the animation. To fix this we can manually set the zIndex to, for example, 1 on the ModalContent view.
struct ContentView: View {
#State private var isShowingModal = false
var body: some View {
GeometryReader { geometry in
ZStack {
Button(
action: { withAnimation { self.isShowingModal = true } },
label: { Text("Show Modal") }
)
ZStack {
if self.isShowingModal {
ModalOverlay(tapAction: { withAnimation(.easeOut(duration: 5)) { self.isShowingModal = false } })
ModalContent()
.transition(.move(edge: .bottom))
.zIndex(1)
}
}.edgesIgnoringSafeArea(.all)
}
}
}
}
The investigation that brings to a solution
Transition animations in SwiftUI have still some issues. I think this is a bug. I'm quite sure because:
1) Have you tried to change the background color of your ModalContent from white to green?
struct ModalContent: View {
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
VStack(spacing: 16) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.frame(width: geometry.size.width)
.padding(.top, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.background(Color.green)
}
}
}
}
This way it works (see the following GIF):
2) Another way to make the bug occur is to change the background color of your ContentView to, for example, green, leaving the ModalContent to white:
struct ContentView: View {
#State private var isShowingModal = false
var body: some View {
GeometryReader { geometry in
ZStack {
Button(
action: { withAnimation(.easeOut(duration: 5)) { self.isShowingModal = true } },
label: { Text("Show Modal") }
)
ZStack {
if self.isShowingModal {
ModalOverlay(tapAction: { withAnimation(.easeOut(duration: 5)) { self.isShowingModal = false } })
ModalContent().transition(.move(edge: .bottom))
}
}
}
}
.background(Color.green)
.edgesIgnoringSafeArea(.all)
}
}
struct ModalOverlay: View {
var color = Color.black.opacity(0.4)
var tapAction: (() -> Void)? = nil
var body: some View {
color.onTapGesture { self.tapAction?() }
}
}
struct ModalContent: View {
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
VStack(spacing: 16) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.frame(width: geometry.size.width)
.padding(.top, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.background(Color.white)
}
}
}
}
Even in this case it works as expected:
3) But if you change your ModalContent background color to green (so you have both the ContentView and the ModalContent green), the problem occurs again (I won't post another GIF but you can easily try it yourself).
4) Yet another example: if you change the appearance of you iPhone to Dark Appearance (the new feature of iOS 13) your ContentView will automatically become black and, since your ModalView is white, the problem won't occur and everything goes fine.

SwiftUI round button deforms when clicked

I have created a round button with following code:
struct RoundButton : View {
var value = "..."
var body: some View {
GeometryReader { geometry in
Button(action: {}) {
VStack {
Text(self.value)
.font(.headline)
.color(Color("NightlyDark"))
Text("MIN")
.font(.footnote)
.color(.white)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
.clipShape(Circle())
}
}
}
But when clicking the button, the shape gets deformed. Any idea why this happens ?
Same happens when i use .mask(Circle())
Is this a beta thing or normal behavior ? And does anyone maybe know a better way to create rounded buttons ?
what happens here is Button Considers all screen[width + height] as their frame by default.
so you have to set Button also.
I think it's the default behavior in watchOS
Here Your Code :
struct RoundButton: View {
var value = "..."
var body: some View {
GeometryReader { geometry in
Button(action: {}) {
VStack {
Text(self.value)
.font(.headline)
.foregroundColor(Color("NightlyDark"))
Text("MIN")
.font(.footnote)
.foregroundColor(.white)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipShape(Circle())
}
}
}
Note: i'm using Xcode 11 Beta 4

Resources