swiftUI auto delay animation after 2sec - rotation

hi guys im learning swiftUI and i have some problem with my project.
i have one main card that will rotate 5 random card, plus the back of the card. and at the bottom 5 button that represent the 5 random card.
when i press any of the 5 buttons to rotate the card, i would like that the card rotate back automatically on the cardBack after 2 sec.
here is my code :
import SwiftUI
struct CardBack: View {
var body: some View {
Image("back_card")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
}
}
struct ContentView: View {
#State var flipped = false
#State private var cardsFront = ["bigCard1", "bigCard2", "bigCard3", "bigCard4", "bigCard5" ]
#State private var cardBack = "back_card"
var body: some View {
VStack {
Spacer()
ZStack {
Image(flipped ? self.cardsFront.randomElement()! : self.cardBack)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
.rotation3DEffect(Angle(degrees: flipped ? 180 : 0 ), axis: (x: 0, y: 1, z: 0))
}
Spacer()
HStack {
Button(action: {
withAnimation(.spring()) {
self.flipped.toggle()
}
}) {
Image("circle")
.renderingMode(.original)
}
Button(action: {
}) {
Image("plus")
.renderingMode(.original)
}
Button(action: {
}) {
Image("wave")
.renderingMode(.original)
}
Button(action: {
}) {
Image("square")
.renderingMode(.original)
}
Button(action: {
}) {
Image("star")
.renderingMode(.original)
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Here is a demo on one button
Button(action: {
withAnimation(.spring()) {
self.flipped.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation(.spring()) {
self.flipped.toggle()
}
}
}) {
Image("circle")
.renderingMode(.original)
}

Related

Using SwiftUI DragGesture to scale down and remove the whole view but lead to infinity loop

I want to implement the animation like AppStore. Drag down detail page to scale down and remove itself view.
But when I started to drag, the console keeps printing var scale is changing and app is freezes. Looks like go into infinity loop. Here is my code:
import SwiftUI
struct TestView: View {
#State var showDetail = false
var body: some View {
ZStack {
Text("First View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.accentColor)
.onTapGesture {
withAnimation {
showDetail = true
}
}
if showDetail {
TestDetailView(showDetail: $showDetail)
}
}
.ignoresSafeArea()
}
}
struct TestDetailView: View {
#State var scale: CGFloat = 1 {
didSet {
print("scale: \(scale)")
}
}
#Binding var showDetail: Bool
var body: some View {
ScrollView {
VStack(spacing: 0) {
Text("Upper Part")
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
.gesture(DragGesture(minimumDistance: 0)
.onChanged { val in
let s = val.translation.height / UIScreen.main.bounds.height
if s > 0 && 1 - s > 0.7 {
scale = 1 - s
if scale < 0.8 {
withAnimation {
showDetail = false
}
}
}
}
)
Text("Lower Part")
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.primary)
.onTapGesture {
withAnimation(.easeInOut) {
showDetail = false
}
}
}
.frame(height: UIScreen.main.bounds.height)
}
.scaleEffect(scale)
.animation(.easeInOut, value: showDetail)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I know it's relative to the drag gesture and setting scale but I don't know how to fix that.
The images above is what I want to implement, my code simplified
the layout, you may try to drag down the upper part, then you'll know what happen.

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

SwiftUI: Lauch new view from a slider menu that is not covered by previous view

I am completely new to SwiftUI so I hope this is not a stupid question. In my project I have a home view (a map) and a Menu button in the top right corner. Upon clicking that button my menu is gonna slide in from the left. For each of the menu items I want to use NavigationLink to jump to the next detail views.
Now here is the problem: When I click on the menu items (eg. Payments) I get the view but my button and my map are still on top of that view and I cant get rid of it, or at least I dont know how.. :(.
I saw a few similar questions but I couldnt make it work for my case.
This is my view when the menu view is true
This is my problem view. When I click on eg. Payments I cant get rid of the Map and the button in that view
This is my HomeView (The Map and the button)code:
import SwiftUI
struct HomeView: View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return GeometryReader { geometry in
ZStack(alignment: .leading) {
if self.showMenu {
SlideInMenuView()
.frame(width: geometry.size.width/1)
.transition(.move(edge: .leading))
}
HomeSupportView()
.frame(width: geometry.size.width, height: geometry.size.height)
.cornerRadius(20)
.scaleEffect(self.showMenu ? 0.8 : 1)
.offset(x: self.showMenu ? 150 : 0, y : self.showMenu ? 50 : 0)
.disabled(self.showMenu ? true : false)
MenuButton(showMenu: self.$showMenu)
}
.gesture(drag)
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
struct MenuButton: View {
#Binding var showMenu: Bool
var body: some View {
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}){
Image(systemName: "line.horizontal.3")
.imageScale(.large)
.frame(width: 100, height: 60)
.background(Color.black)
.clipShape(Circle())
.opacity(0.8)
.foregroundColor(.white)
.rotationEffect(.degrees(self.showMenu ? 90 : 0))
.scaleEffect(self.showMenu ? 1.2 : 1)
}
.padding(.top, -380)
}
}
This is my menu view code with my try of making the NavigationLink work. I tried it on the "Payments" HStack in multiple versions but I just cant make it work
import SwiftUI
struct SlideInMenuView: View {
#State private var showingPaymentDetails = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
HStack {
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Profile")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 150.0)
HStack {
Image(systemName: "person.2.fill")
.foregroundColor(.gray)
.imageScale(.large)
Text("Matches")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
NavigationLink(destination:
PaymentDetailsView())
{
HStack {
Image(systemName: "creditcard")
.foregroundColor(.gray)
.imageScale(.large)
Text("Payments")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
}
HStack {
Image(systemName: "hammer")
.foregroundColor(.gray)
.imageScale(.large)
Text("Settings")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
Spacer()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}
}
struct SlideInMenuView_Previews: PreviewProvider {
static var previews: some View {
SlideInMenuView()
}
}
If anyone knows how to help me out here...
Thank you very much in advance guys.
Stay healthy.
Cheers,
LeBob
#Chris: This is how I want it to be. I put in 4 consecutive screenshots with the respective clicks.
***** UPDATED ANSWER *****
thank you for your clarifying pictures
try this:
import SwiftUI
class Global : ObservableObject {
#Published var showMenu = true
#Published var showMenuButton = true
}
struct PaymentDetailsView : View {
#EnvironmentObject var global : Global
var body: some View {
Text("payment")
.onAppear() {
withAnimation() {
self.global.showMenuButton = false
}
}
.onDisappear() {
withAnimation() {
self.global.showMenuButton = true
}
}
}
}
struct HomeSupportView : View {
#EnvironmentObject var global : Global
var body: some View {
ZStack {
Rectangle().fill(Color.blue)
Text("home")
}
}
}
struct SlideInMenuView: View {
#EnvironmentObject var global : Global
#State private var showingPaymentDetails = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
HStack {
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Profile")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 150.0)
HStack {
Image(systemName: "person.2.fill")
.foregroundColor(.gray)
.imageScale(.large)
Text("Matches")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
NavigationLink(destination:
PaymentDetailsView().environmentObject(self.global))
{
HStack {
Image(systemName: "creditcard")
.foregroundColor(.gray)
.imageScale(.large)
Text("Payments")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
}
HStack {
Image(systemName: "hammer")
.foregroundColor(.gray)
.imageScale(.large)
Text("Settings")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
Spacer()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}
}
struct SlideInMenuView_Previews: PreviewProvider {
static var previews: some View {
SlideInMenuView().environmentObject(Global())
}
}
struct ContentView: View {
#EnvironmentObject var global : Global
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.global.showMenu = false
}
}
}
return GeometryReader { geometry in
ZStack(alignment: .leading) {
if self.global.showMenu {
SlideInMenuView()
.environmentObject(self.global)
.frame(width: geometry.size.width/1)
.transition(.move(edge: .leading))
.zIndex(3)
}
HomeSupportView()
.environmentObject(self.global)
.frame(width: geometry.size.width, height: geometry.size.height)
.cornerRadius(20)
.scaleEffect(self.global.showMenu ? 0.8 : 1)
.offset(x: self.global.showMenu ? 150 : 0, y : self.global.showMenu ? 50 : 0)
.disabled(self.global.showMenu ? true : false)
.zIndex((2))
MenuButton().environmentObject(self.global)
.zIndex(4)
}
.gesture(drag)
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Global())
}
}
struct MenuButton: View {
#EnvironmentObject var global: Global
var body: some View {
Button(action: {
withAnimation {
self.global.showMenu.toggle()
}
}){
if self.global.showMenuButton {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
.frame(width: 100, height: 60)
.background(Color.black)
.clipShape(Circle())
.opacity(0.8)
.foregroundColor(.white)
.rotationEffect(.degrees(self.global.showMenu ? 90 : 0))
.scaleEffect(self.global.showMenu ? 1.2 : 1)
} else {
EmptyView()
}
}
.padding(.top, -380)
}
}
SwiftUI has a zIndex() modifier that you can use to order the views.
This could help in your situation.

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.

Resources