How can I activate multiple buttons in one slide gesture with SwiftUI? - ios

I have 25 buttons laid out in a grid for a game like this.
I'm generating these buttons like this:
VStack {
ForEach(1..<6) {_ in
HStack {
ForEach(1..<6) {_ in
Button(action: {
// Button clicked
doButton()
}) {
Rectangle()
.frame(width: 50, height: 50)
.border(Color.black, lineWidth: 1)
}
}
}
}
}
How can I make it so if the user drags their finger across multiple buttons, each one of the buttons are clicked, and doButton() is executed?
I tried using a DragGesture() but couldn't get it to work...

Swift 5.1, iOS 13.
I don't have a 100% solution for you, but I do have something to get you off the ground hopefully. These are not buttons, but simply squares. As I click/drag across them they^ll signal I passed by.
It isn't perfect, but should give you some thing more to work with.
I read a tutorial on medium on doing something similar with drag and drop. This one.
Can help but think you'll need to do something with drag here and the Geometry reader ultimately, detecting where they are and responding to that.
import SwiftUI
struct ContentView: View {
var body: some View {
return VStack {
ForEach(1..<6) {colY in
HStack {
ForEach(1..<6) {rowX in
boxView(row: rowX, col: colY)
}
}
}
}
}
}
struct boxView: View {
#State var row: Int
#State var col: Int
var body: some View {
let dragGesture = DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.global)
.onChanged { (value) in
print("trigger ",self.row,self.col)
}
return Rectangle()
.stroke(Color.black)
.frame(width: 50, height: 50)
.gesture(dragGesture)
}
}

import SwiftUI
class Container : ObservableObject {
#Published var location = CGPoint()
public func updateLocation(_ location: CGPoint) {
self.location = location
}
}
struct SliderGestureButtonsView: View {
#ObservedObject var container = Container()
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(1..<6) { colY in
HStack {
ForEach(1..<6) { rowX in
GeometryReader { gp in
let x = gp.frame(in:.named("hive")).midX
let y = gp.frame(in:.named("hive")).midY
let w = gp.frame(in:.named("hive")).size.width
let h = gp.frame(in:.named("hive")).size.height
let rect = CGRect(x: x, y: y, width: w, height: h)
boxView(row: rowX, col: colY, rect: rect, container: container)
}
}
}.frame(height: 60)
}
}.coordinateSpace(name: "hive").frame(width: 300)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.named("hive"))
.onChanged { (value) in
container.updateLocation(value.location)
}
)
}
}
struct boxView: View {
var row: Int
var col: Int
var rect: CGRect
#ObservedObject var container: Container
var body: some View {
let x = container.location.x
let y = container.location.y
let hw = rect.size.width / 2
let ox = rect.origin.x
let withinX = x > ox - hw && x < ox + hw
let hh = rect.size.height / 2
let oy = rect.origin.y
let withinY = y > oy - hh && y < oy + hh
var pressed = false
if(withinX && withinY) {
pressed = true
print("trigger ", self.row, self.col)
}
return Rectangle()
.stroke(Color.white)
.frame(width: 60, height: 60)
.background(pressed ? Color.green : Color.black)
}
}

Related

How do I offset each View for it's own when generated in a foreach loop SwiftUI

I am creating a Cardview stored with user data for each user in the usersList. Now I want to offset each card for itself by pressing on either button. If I press the button now I will offset every card created by the foreach. How can I make the button only affect the first card? It is probably a better Idea to offset the card in the CardView itself, like I did with the Draggesture and not in the ContentView
struct ContentView: View {
#State var usersList: [User] = []
#State var fetchingComplete: Bool = false
#State var offset = CGSize.zero
#State var buttonPushed: Bool = false
var body: some View {
VStack {
NavBar().onAppear() {
fetchUsers()
}
if fetchingComplete {
ZStack {
ForEach(usersList, id: \.id) { user in
CardView(user: user).offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)
}
}
}
ButtonsBar(offset: $offset, buttonPushed: $buttonPushed)
}
}
}
public func fetchUsers() -> [User]{
FirebaseManager.shared.firestore.collection("users").getDocuments { snapshot, error in
if let error = error {
print("Failed to fetch users: ", error)
return
}
for document in snapshot!.documents {
let user = User(dictionary: document.data())
usersList.append(user)
}
fetchingComplete.toggle()
}
return usersList
}
}
struct ButtonsBar:View {
#Binding var offset: CGSize
#Binding var buttonPushed: Bool
var body: some View {
HStack {
Button {
print("perform dislike")
offset = CGSize(width: -500, height: 0)
buttonPushed.toggle()
} label: {
Image("dismiss_circle")
}.frame(width: 65)
Button {
print("perform like")
offset = CGSize(width: 500, height: 0)
buttonPushed.toggle()
} label: {
Image("like_circle")
}.frame(width: 65)
}
}
}
struct CardView: View {
#State private var offset = CGSize.zero
var user: User
var body: some View {
VStack{
Image("lady")
.resizable()
}.cornerRadius(12)
.frame(width: 310, height: 392)
.scaledToFit()
.overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
} .onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
}
}
)
}
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
case 150...500:
offset = CGSize(width: 500, height: 0)
default:
offset = .zero
}
}
}
Right now, your current design will be unable to achieve what you're saying. How you have designed offset makes it so there's only the single source of truth being a single var offset. Thus, there's no way to give an offset to each card uniquely. Likewise, your design of a single ButtonBar will also be unable to achieve the goal of modifying each unique entry. How would this ButtonBar as it is choose any particular CardView? I know how to implement this, but I think you can figure it out once I explain more on how to uniquely associate values to each CardView.
I know you asked for how to just indent the first card only, but I don't think the easiest answer will be your desired outcome.
What you should do is that you have to track an offset and/or a buttonPressed for EACH user in your UserList. The easiest way is to create a CardViewModel so at the top of your CardView add
#StateObject var cardVM = CardViewModel()
and for the CardViewModel,
class CardViewModel : ObservableObject {
#Published var offset = CGSize.zero
#Published var buttonPushed: Bool = false
}
Now we have a unique offset and buttonPushed for each CardView. From here I would remove the modifiers
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)
and inside the CardView place your ButtonBar like so
struct CardView: View {
#StateObject var cardVM = CardViewModel()
var user: User
var body: some View {
VStack{
ButtonBar(offset: $cardVM.offset, buttonPushed: $cardVM.buttomPushed)
Image("lady")
.resizable()
}
.cornerRadius(12)
.frame(width: 310, height: 392)
.scaledToFit()
.overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
}
}
)
// the offset and effects based on button push
.offset(x: cardVM.offset.width, y: cardVM.offset.height * 0.4)
.rotationEffect(.degrees(Double(cardVM.offset.width / 40)))
.animation(.spring(), value: cardVM.buttonPushed)
}
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
case 150...500:
offset = CGSize(width: 500, height: 0)
default:
offset = .zero
}
}
}
Because this isn't an MRE, I'm not able to actually test this, but in general, this should follow the design of creating a CardViewModel for each CardView which contains unique to each view variables offset and buttonPressed and placing the ButtonBar in each CardView allows us to modify the CardViewModel uniquely too.
EDIT:
In your main View, create
#StateObject var buttonBarVM = ButtonBarViewModel()
which has a
#Published var cardVM = CardViewModel().
In CardView, do
#EnvironmentObject var buttonBarVM: ButtonBarViewModel and
.onTap of the CardView or whatever gesture to do self.ButtonBarVM.cardVM = self.cardVM
In ButtonBar, instantiate
#EnvironmentObject var buttonBarVM: ButtonBarViewModel
and change the buttons to modify self.buttonBarVM.cardVM.(states)
One thing I would do is also pass #Published var user: User into cardVM and so when everything is first instantiated, you can wrap the ButtonBar in an if to only show when the user in cardVM is not default because at the moment, unless you first set the ButtonBar, the actions won't do anything until you gesture a CardView.
Here, we create a barButtonsVM to be our controller of a cardVM that you need to select, and we can set that based on a gesture you do to a card. We must preserve #StateObject cardVM = CardViewModel() because that specifically creates a ViewModel for each CardView.

How to move a SwiftUI View by dragging one of its subview (child)?

struct ContentView: View {
#State private var location: CGPoint = CGPoint(x: 150, y: 100)
#GestureState private var startLocation: CGPoint? = nil
#State var windowSize = CGSize(width: 300, height: 200)
var dragGesture: some Gesture {
DragGesture().onChanged { value in
location = (startLocation ?? location) + value.translation
}.updating($startLocation) {
(_, startLocation, _) in
startLocation = startLocation ?? location
}
}
var body: some View {
VStack {
//Title
ZStack {
Color.green
Text("Title bar")
}.frame(height: 30)
.gesture(dragGesture) // (1)
//Contents
Spacer()
Text("contents")
Spacer()
}.background(Color.gray)
.frame(width: windowSize.width, height: windowSize.height)
.position(location) // (3)
//.gesture(dragGesture) // (2)
}
}
func +(lhs: CGPoint, rhs: CGSize) -> CGPoint {
return CGPoint(
x: lhs.x + rhs.width,
y: lhs.y + rhs.height
)
}
The dragging effect works if (1) is commented out and (2) uncommented. But I want to recognize drag gestures only on the title bar to move the whole View. What I tried is to add a gesture to the ZStack (1) modifying the state of the VStack (3).
I got a solution later.
ZStack {
TitleBar() //some view
.position(/*position for title bar*/)
.gesture(dragGesture)
Contents() //some view
.position(/*position for contents below, which can be a computed variable from TitleBar's (state) position*/)
}

Create sliding effect by offsetting button position with image

I have created a slider view which consists of 2 images, a chevron pointer and a slider line. My ultimate goal is to offset the button in the opposite direction to the chevron image so that when the user clicks the empty space the button will slide to its location. Below is a gif of me attempting to achieve this. My approach is quite manual and I suspect there is a sophisticated way of achieving my goal. I have it working when I click the button once, I suspect that on the other side I have my numbers wrong so it doesn't work when the image is on the left side of the screen.
import SwiftUI
extension Animation {
static func smooth() -> Animation {
Animation.spring()
.speed(0.7)
}
}
struct SliderView: View {
//this works as a toggle you can implement other booleans with this
#State var partyType: Bool = false
#State private var isOn = false
#State var width: CGFloat = 0
#State var height: CGFloat = 0
let screen = UIScreen.main.bounds
var body: some View {
ZStack {
Toggle("",isOn: $isOn).toggleStyle(SliderToggle())
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
func hostParty() -> Bool {
if partyType {
return true
} else {
return false
}
}
}
struct SlideEffect: GeometryEffect {
var offset: CGSize
var animatableData: CGSize.AnimatableData {
get { CGSize.AnimatableData(offset.width, offset.height) }
set { offset = CGSize(width: newValue.first, height: newValue.second) }
}
public func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(
translationX: offset.width,
y: offset.height
)
)
}
}
struct SliderView_Previews: PreviewProvider {
static var previews: some View {
SliderView()
}
}
struct SliderToggle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
GeometryReader{ geo in
VStack (alignment: .leading,spacing: 0){
Button(action: {
withAnimation() {
configuration.isOn.toggle()
}
}) {
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
.offset(x: geo.size.width - 100, y: 0)
}
.modifier(
SlideEffect(
offset: CGSize(
width: configuration.isOn ? -geo.size.width + imageWidthX(name: "chevronPointer") : 0,
height: 0.0
)
)
)
Image("sliderLine")
.resizable()
.scaledToFit()
.frame(width: geo.size.width, alignment: .center)
}
}
.frame(height: 40)
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
I worded the question pretty poorly but ultimately the goal I wanted to achieve was to have 2 buttons on a stack that controlled the view under it. When the opposite side is clicked the slider icon will travel to that side and vice versa. The offset made things a lot more complicated than necessary so I removed this code and basically created invisible buttons. IF there is a more elegant way top achieve this I would be truly greatful
HStack {
ZStack {
Button (" ") {
withAnimation(){
configuration.isOn.toggle()
}}
.padding(.trailing).frame(width: 70, height: 25)
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
//.offset(x: geo.size.width - 100, y: 0)
.modifier(SlideEffect(offset: CGSize(width: configuration.isOn ? geo.size.width - imageWidthX(name: "chevronPointer"): 0, height: 0.0)))
}
Spacer()
Button(" "){
withAnimation(){
configuration.isOn.toggle()
}
}.padding(.trailing).frame(width: 70, height: 25)
}

How to make a view's height animate from 0 to height in SwiftUI?

I'd like to make this bar's height (part of a larger bar graph) animate on appear from 0 to its given height, but I can't quite figure it out in SwiftUI? I found this code that was helpful, but with this only the first bar (of 7) animates:
struct BarChartItem: View {
var value: Int
var dayText: String
var topOfSCaleColorIsRed: Bool
#State var growMore: CGFloat = 0
#State var showMore: Double = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(getBarColor())
.frame(width: 40, height: growMore)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.linear(duration: 7.0)) {
growMore = CGFloat(value)
showMore = 1
}
}
}.opacity(showMore)
Text(dayText.uppercased())
.font(.caption)
}
}
And this is the chart:
HStack(alignment: .bottom) {
ForEach(pastRecoveryScoreObjects) { pastRecoveryScoreObject in
BarChartItem(value: pastRecoveryScoreObject.recoveryScore, dayText: "\(pastRecoveryScoreObject.date.weekdayName)", topOfSCaleColorIsRed: false)
}
}
This works: you can get the index of the ForEach loop and delay each bar's animation by some time * that index. All the bars are connected to the same state variable via a binding, and the state is set immediately on appear. You can play with the parameters to tune it to your own preferences.
struct TestChartAnimationView: View {
let values = [200, 120, 120, 90, 10, 80]
#State var open = false
var animationTime: Double = 0.25
var body: some View {
VStack {
Spacer()
HStack(alignment: .bottom) {
ForEach(values.indices, id: \.self) { index in
BarChartItem(open: $open, value: values[index])
.animation(
Animation.linear(duration: animationTime).delay(animationTime * Double(index))
)
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
open = true
}
}
}
}
struct BarChartItem: View {
#Binding var open: Bool
var value: Int
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: open ? CGFloat(value) : 0)
Text("Week".uppercased())
.font(.caption)
}
}
}
Edit: ForEach animations are kind of weird. I'm not sure why, but this works:
struct ContentView: View {
let values = [200, 120, 120, 90, 10, 80]
var body: some View {
HStack(alignment: .bottom) {
ForEach(values, id: \.self) { value in
BarChartItem(totalValue: value)
.animation(.linear(duration: 3))
}
}
}
}
struct BarChartItem: View {
#State var value: Int = 0
var totalValue = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: CGFloat(value))
Text("Week".uppercased())
.font(.caption)
}.onAppear {
value = totalValue
}
}
}
Instead of applying the animation in the onAppear, I applied an implicit animation inside the ForEach.
Result:

SwiftUI Card flip with two views

I am trying to create a card flip effect between two SwiftUI Views. When clicking on the original view, it 3D rotates on the Y axis like when flipping a card, and the second view should start being visible after 90 degrees have been made.
Using .rotation3DEffect() I can easily rotate a view, the issue is that with the animation() I don't know how to trigger the View change once the angle has reached 90 degrees...
#State var flipped = false
var body: some View {
return VStack{
Group() {
if !self.flipped {
MyView(color: "Blue")
} else {
MyView(color: "Red")
}
}
.animation(.default)
.rotation3DEffect(self.flipped ? Angle(degrees: 90): Angle(degrees: 0), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))
.onTapGesture {
self.flipped.toggle()
}
}
How to achieve such a rotation between two views ?
Simple Solution
The approach you're taking can be made to work by putting your two views in a ZStack and then showing/hiding them as the flipped state changes. The rotation of the second view needs to be offset. But this solution relies on a cross-fade between the two views. It might be OK for some uses cases. But there is a better solution - though it's a bit more fiddly (see below).
Here's a way to make your approach work:
struct SimpleFlipper : View {
#State var flipped = false
var body: some View {
let flipDegrees = flipped ? 180.0 : 0
return VStack{
Spacer()
ZStack() {
Text("Front").placedOnCard(Color.yellow).flipRotate(flipDegrees).opacity(flipped ? 0.0 : 1.0)
Text("Back").placedOnCard(Color.blue).flipRotate(-180 + flipDegrees).opacity(flipped ? 1.0 : 0.0)
}
.animation(.easeInOut(duration: 0.8))
.onTapGesture { self.flipped.toggle() }
Spacer()
}
}
}
extension View {
func flipRotate(_ degrees : Double) -> some View {
return rotation3DEffect(Angle(degrees: degrees), axis: (x: 1.0, y: 0.0, z: 0.0))
}
func placedOnCard(_ color: Color) -> some View {
return padding(5).frame(width: 250, height: 150, alignment: .center).background(color)
}
}
Better Solution SwiftUI has some useful animation tools - such as GeometryEffect - that can generate a really smooth version of this effect. There are some excellent blog posts on this topic at SwiftUI Lab. In particular, see: https://swiftui-lab.com/swiftui-animations-part2/
I've simplified and adapted one of examples in that post to provide the card flipping functionality.
struct FlippingView: View {
#State private var flipped = false
#State private var animate3d = false
var body: some View {
return VStack {
Spacer()
ZStack() {
FrontCard().opacity(flipped ? 0.0 : 1.0)
BackCard().opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.animate3d.toggle()
}
}
Spacer()
}
}
}
struct FlipEffect: GeometryEffect {
var animatableData: Double {
get { angle }
set { angle = newValue }
}
#Binding var flipped: Bool
var angle: Double
let axis: (x: CGFloat, y: CGFloat)
func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.flipped = self.angle >= 90 && self.angle < 270
}
let tweakedAngle = flipped ? -180 + angle : angle
let a = CGFloat(Angle(degrees: tweakedAngle).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
return ProjectionTransform(transform3d).concatenating(affineTransform)
}
}
struct FrontCard : View {
var body: some View {
Text("One thing is for sure – a sheep is not a creature of the air.").padding(5).frame(width: 250, height: 150, alignment: .center).background(Color.yellow)
}
}
struct BackCard : View {
var body: some View {
Text("If you know you have an unpleasant nature and dislike people, this is no obstacle to work.").padding(5).frame(width: 250, height: 150).background(Color.green)
}
}
Update
The OP asks about managing the flip status outside of the view. This can be done by using a binding. Below is a fragment that implements and demos this. And OP also asks about flipping with and without animation. This is a matter of whether changing the flip state (here with the showBack var) is done within an animation block or not. (The fragment doesn't include FlipEffect struct which is just the same as the code above.)
struct ContentView : View {
#State var showBack = false
let sample1 = "If you know you have an unpleasant nature and dislike people, this is no obstacle to work."
let sample2 = "One thing is for sure – a sheep is not a creature of the air."
var body : some View {
let front = CardFace(text: sample1, background: Color.yellow)
let back = CardFace(text: sample2, background: Color.green)
let resetBackButton = Button(action: { self.showBack = true }) { Text("Back")}.disabled(showBack == true)
let resetFrontButton = Button(action: { self.showBack = false }) { Text("Front")}.disabled(showBack == false)
let animatedToggle = Button(action: {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}) { Text("Toggle")}
return
VStack() {
HStack() {
resetFrontButton
Spacer()
animatedToggle
Spacer()
resetBackButton
}.padding()
Spacer()
FlipView(front: front, back: back, showBack: $showBack)
Spacer()
}
}
}
struct FlipView<SomeTypeOfViewA : View, SomeTypeOfViewB : View> : View {
var front : SomeTypeOfViewA
var back : SomeTypeOfViewB
#State private var flipped = false
#Binding var showBack : Bool
var body: some View {
return VStack {
Spacer()
ZStack() {
front.opacity(flipped ? 0.0 : 1.0)
back.opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: showBack ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}
Spacer()
}
}
}
struct CardFace<SomeTypeOfView : View> : View {
var text : String
var background: SomeTypeOfView
var body: some View {
Text(text)
.multilineTextAlignment(.center)
.padding(5).frame(width: 250, height: 150).background(background)
}
}
A cleaned up and extendable solution
Note that you can easily change flip direction by changing axis parameter in .rotation3DEffect.
import SwiftUI
struct FlipView<FrontView: View, BackView: View>: View {
let frontView: FrontView
let backView: BackView
#Binding var showBack: Bool
var body: some View {
ZStack() {
frontView
.modifier(FlipOpacity(percentage: showBack ? 0 : 1))
.rotation3DEffect(Angle.degrees(showBack ? 180 : 360), axis: (0,1,0))
backView
.modifier(FlipOpacity(percentage: showBack ? 1 : 0))
.rotation3DEffect(Angle.degrees(showBack ? 0 : 180), axis: (0,1,0))
}
.onTapGesture {
withAnimation {
self.showBack.toggle()
}
}
}
}
private struct FlipOpacity: AnimatableModifier {
var percentage: CGFloat = 0
var animatableData: CGFloat {
get { percentage }
set { percentage = newValue }
}
func body(content: Content) -> some View {
content
.opacity(Double(percentage.rounded()))
}
}

Resources