State Variables and ForEach Swift - swift

I'm trying to build a simple view in swiftUI with a foreach loop that contains three buttons, and animates these buttons based on a state variable. The problem is, when one button is pressed, all of them animate. I've been working on it for a good few hours but I can't figure out a way to have only one of the buttons in the view animate when it is pressed without just not using a foreach loop and just making three different state variables for three different CardView views. Here is the code for reference:
struct CardView: View {
var flipped:Bool
var body: some View {
VStack {
Image(flipped ? "cardFront" : "cardBack").renderingMode(.original)
.resizable()
.scaledToFit()
.frame(height: 200)
.rotation3DEffect(Angle(degrees: flipped ? 0 : 180), axis: (x: 0, y: 1, z: 0))
.opacity(1)
Text("Flip").padding()
}
}
}
struct ContentView: View {
#State var isFlipped = false
var body: some View {
HStack {
ForEach(0..<3) {number in
Button(action: {
withAnimation(.spring()) {
self.isFlipped.toggle()
}
}) {
CardView(flipped: self.isFlipped)
}
}
}
}
}
Would be very grateful for any pointers as I'm pretty new to all of this. I'd really like to figure out how to make this work in a foreach loop without the aforementioned work around.

Put the #State variable and the Button in the CardView. Here is a basic example:
I needed to use a Rectangle since I didn't have the images you used.
struct CardView: View {
#State var isFlipped = false
var body: some View {
VStack {
Rectangle()
.foregroundColor(self.isFlipped ? Color.blue : Color.red)
.frame(width: 100, height: 200)
.rotation3DEffect(Angle(degrees: self.isFlipped ? 0 : 180), axis: (x: 0, y: 1, z: 0))
.opacity(1)
Button(action: {
withAnimation(.spring()) {
self.isFlipped.toggle()
}
}) {
Text("Flip")
}
}
}
}
struct ContentView: View {
var body: some View {
HStack {
ForEach(0..<3) {number in
CardView()
}
}
}
}
I hope this helps!

Related

Tapping on a View to also change its sibling views

Within a VStack, I have 3 views. A view's selection and colour are toggled when tapping on them. I want the previously selected View to be deselected when selecting the next view.
The tapGesture is implemented in each view. I am not sure what is the best way to achieve this.
Thanks.
Here is the code sample:
struct ContentView: View {
#State var tile1 = Tile()
#State var tile2 = Tile()
#State var tile3 = Tile()
var body: some View {
VStack {
TileView(tile: tile1 )
TileView(tile: tile2 )
TileView(tile:tile3 )
}
.padding()
}
}
struct Tile: Identifiable, Equatable{
var id:UUID = UUID()
var isSelected:Bool = false
}
struct TileView: View {
#State var tile:Tile
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill( tile.isSelected ? Color.red : Color.yellow )
.frame(height: 100)
.padding()
.onTapGesture {
tile.isSelected.toggle()
}
}
}
You need to relate the 3 tiles somehow. An Array is an option. Then once they are related you can change the selection at that level.
extension Array where Element == Tile{
///Marks the passed `tile` as selected and deselects other tiles.
mutating func select(_ tile: Tile) {
for (idx, t) in self.enumerated(){
if t.id == tile.id{
self[idx].isSelected.toggle()
}else{
self[idx].isSelected = false
}
}
}
}
Then you can change your views to use the new function.
struct MyTileListView: View {
#State var tiles: [Tile] = [Tile(), Tile(), Tile()]
var body: some View {
VStack {
ForEach(tiles) { tile in
TileView(tile: tile, onSelect: {
//Use the array to select the tile
tiles.select(tile)
})
}
}
.padding()
}
}
struct TileView: View {
//#State just create a copy of the tile `#Binding` is a two-way connection if needed
let tile:Tile
///Called when the tile is selected
let onSelect: () -> Void
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(tile.isSelected ? Color.red : Color.yellow)
.frame(height: 100)
.padding()
.onTapGesture {
onSelect()
}
}
}

ZStack blocks animation SwiftUI

So my goal is to be able to show a custom view from time to time over a SwiftUI tabview, so I thought I would place them both in a ZStack like this
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation(Animation.linear(duration: 10)) {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
}
This works just fine, but when I try to use withAnimation() no animation gets triggered. How can I make the overlaying view, disappear with animation?
Use .animation modifier with container, like below, so container could animate removing view
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
show = false // << withAnimation not needed anymore
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
.animation(Animation.linear(duration: 10), value: show) // << here !!
So I found a solution and what I think is the cause of this. My hypothesis is that the animation modifier does not handle ZIndex IF it is not explicitly set.
One solution to this is to set ZIndex to the view that should be on the top to something higher than the other view. Like this:
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
.zIndex(.infinity) // <-- this here makes the animation work
}
}
}

Add views to parent swiftui view and then remove them programatically

Right now I've got a parent view written in SwiftUI with some Buttons. What I'd like to happen is to programmatically create and overlay some new child views on top of the entire parent view when the buttons are pressed and then have them fade out, removing them from the view. With SwiftUI being based on state, I'm not sure how to go about this. Would I have to keep track of every view that is created with many at #State variables? My code looks like the following right now:
struct ContentView: View {
var key: String
#State private var touches: [Touch] = []
struct Touch: Identifiable {
let id = UUID()
let location: CGPoint
}
var body: some View {
ZStack {
Color.blue
ripplesLayer
}
.gesture(
DragGesture(minimumDistance: 0)
.onEnded { value in
touches.append(
Touch(location: value.location)
)
}
)
.edgesIgnoringSafeArea(.all)
}
var ripplesLayer: some View {
ForEach(touches.suffix(15)) { touch in
RippleView(key: key)
.position(
x: touch.location.x,
y: touch.location.y
)
}
}
struct RippleView: View {
var key: String
#State private var isHidden = false
#State private var size: CGFloat = 50
var body: some View {
Circle()
.fill(Color.white.opacity(isHidden ? 0 : 0.5))
.frame(width: size, height: size)
.transition(.opacity)
.animation(.easeOut(duration: 0.5))
.onAppear {
Sound.play(file: key, fileExtension: ".wav")
withAnimation {
isHidden = true
size = 200
}
}
}
}
}
I've got this struct that references the ripple child views in the foreach loop as in this answer but don't know how to keep track of multiple disappearing views or even implement it in the first place. Multitouch is enabled and. I want to be able to dynamically create a lot of child views.
struct FreePiano: View {
#State var numberOfPianos = 0
var whiteKeys = ["c1", "d1", "e1", "f1", "g1", "a1", "b1", "c2", "d2", "e2", "f2", "g2", "a2", "b2", "c3"]
var blackKeys = ["c#1", "d#1", "f#1", "g#1", "a#1", "c#2", "d#2", "f#2", "g#2", "a#2"]
var body: some View {
ZStack {
HStack {
VStack {
ForEach(whiteKeys, id: \.self) { whiteKey in
FreePianoKey(numberOfImages: $numberOfPianos, key: whiteKey)
}
}
VStack {
ForEach(blackKeys, id: \.self) { blackKey in
FreePianoKey(numberOfImages: $numberOfPianos, key: blackKey)
}
}
}
ForEach(0 ... numberOfPianos, id, \.self) { _ in
ContentView()
.allowsHitTesting(false)
.frame(width: 300, height: 300)
.position(x: 100, y:100)
}
}
}
}
Would something like SpriteKit be easier for what I'm trying to do?
I do the same thing in my app. In the parent view, I keep a state variable that keeps track of the child views. If your app only allows one view at a time, you could use a state variable that is simply an enum (I call it AppState). Then, as a child view is launched, you would change the AppState.
Alternatively, the state variable could be something more complex, like a stack. Or, in the case of allowing multiple child views at a time, it could be an array of tuples of all child windows and their state (for instance, 0 = closed, 1 = open).

SwiftUI Text animation on opacity does not work

Question is simple: how in the world do i get a Text to animate properly?
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
The problem: the view insists on doing some dumb animation where the text is replaced with the new text, but truncated with ellipses, and it slowly expands widthwise until the entirety of the new text is shown.
Naturally, this is not an animation on opacity. It's not a frame width problem, as I've verified with drawing the borders.
Is this just another dumb bug in SwiftUI that i'm going to have to deal with, and pray that someone fixes it?
EDIT: ok, so thanks to #Mac3n, i got this inspiration, which works correctly, even if it's a little ugly:
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(op)
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.op = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.foozle += "omo"
withAnimation(.easeIn(duration: 0.3)) {
self.op = 1
}
}
}
}) {
Text("ugh")
}
The problem is that SwiftUI sees Text view as the same view. You can use the .id() method on the view to set it. In this case I've just set the value to a hash of the text itself, so if you change the text, the entire view will get replaced.
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.id(self.foozle.hashValue)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
Transition works when view appeared/disappeared. In your use-case there is no such workflow.
Here is a demo of possible approach to hide/unhide text with opacity animation:
struct DemoTextOpacity: View {
var foozle: String = "uuuuuuuuu"
#State private var hidden = true
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(hidden ? 0 : 1)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.hidden.toggle()
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
If you want to animate on opacity you need to change opacity value on your text element.
code example:
#State private var textValue: String = "Sample Data"
#State private var opacity: Double = 1
var body: some View {
VStack{
Text("\(textValue)")
.opacity(opacity)
Button("Next") {
withAnimation(.easeInOut(duration: 0.5), {
self.opacity = 0
})
self.textValue = "uuuuuuuuuuuuuuu"
withAnimation(.easeInOut(duration: 1), {
self.opacity = 1
})
}
}
}

SwiftUI: Stop an Animation that Repeats Forever

I would like to have a 'badge' of sorts on the screen and when conditions are met, it will bounce from normal size to bigger and back to normal repeatedly until the conditions are no longer met. I cannot seem to get the badge to stop 'bouncing', though. Once it starts, it's unstoppable.
What I've tried:
I have tried using a few animations, but they can be classified as animations that use 'repeatForever' to achieve the desired effect and those that do not. For example:
Animation.default.repeatForever(autoreverses: true)
and
Animation.spring(response: 1, dampingFraction: 0, blendDuration: 1)(Setting damping to 0 makes it go forever)
followed by swapping it out with .animation(nil). Doesn't seem to work. Does anyone have any ideas? Thank you so very much ahead of time! Here is the code to reproduce it:
struct theProblem: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation( active ? Animation.default.repeatForever(autoreverses: true): nil )
.frame(width: 100, height: 100)
.onTapGesture {
self.active = !self.active
}
}
}
I figured it out!
An animation using .repeatForever() will not stop if you replace the animation with nil. It WILL stop if you replace it with the same animation but without .repeatForever(). ( Or alternatively with any other animation that comes to a stop, so you could use a linear animation with a duration of 0 to get a IMMEDIATE stop)
In other words, this will NOT work: .animation(active ? Animation.default.repeatForever() : nil)
But this DOES work: .animation(active ? Animation.default.repeatForever() : Animation.default)
In order to make this more readable and easy to use, I put it into an extension that you can use like this: .animation(Animation.default.repeat(while: active))
Here is an interactive example using my extension you can use with live previews to test it out:
import SwiftUI
extension Animation {
func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
if expression {
return self.repeatForever(autoreverses: autoreverses)
} else {
return self
}
}
}
struct TheSolution: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation(Animation.default.repeat(while: active))
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct TheSolution_Previews: PreviewProvider {
static var previews: some View {
TheSolution()
}
}
As far as I have been able to tell, once you assign the animation, it will not ever go away until your View comes to a complete stop. So if you have a .default animation that is set to repeat forever and auto reverse and then you assign a linear animation with a duration of 4, you will notice that the default repeating animation is still going, but it's movements are getting slower until it stops completely at the end of our 4 seconds. So we are animating our default animation to a stop through a linear animation.
How about using a Transaction
In the code below, I turn off or turn on the animation depending on the state of the active
Warning: Be sure to use withAnimation otherwise nothing will work
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect(active ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true), value: active)
.frame(width: 100, height: 100)
.onTapGesture {
useTransaction()
}
}
func useTransaction() {
var transaction = Transaction()
transaction.disablesAnimations = active ? true : false
withTransaction(transaction) {
withAnimation {
active.toggle()
}
}
}
After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.
struct WiggleAnimation<Content: View>: View {
var content: Content
#Binding var animate: Bool
#State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) {
self.content = content()
self._animate = animate
}
}
Use
#State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)
Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}
How does it work?
.id(animate) modifier here does not refresh the view but just replaces it with a new one, so it is back to its original state.
Again this might not be the best solution but it works for my case.
There is nothing wrong in your code, so I assume it is Apple's defect. It seems there are many with implicit animations (at least with Xcode 11.2). Anyway...
I recommend to consider alternate approach provided below that gives expected behaviour.
struct TestAnimationDeactivate: View {
#State var active: Bool = false
var body: some View {
VStack {
if active {
BlinkBadge()
} else {
Badge()
}
}
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct Badge: View {
var body: some View {
Circle()
}
}
struct BlinkBadge: View {
#State private var animating = false
var body: some View {
Circle()
.scaleEffect(animating ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true))
.onAppear {
self.animating = true
}
}
}
struct TestAnimationDeactivate_Previews: PreviewProvider {
static var previews: some View {
TestAnimationDeactivate()
}
}
Aspid comments on the accepted solution that an Xcode update broke it. I was struggling with a similar problem while playing around with an example from Hacking with Swift, and
.animation(active ? Animation.default.repeatForever() : Animation.default)
was not working for me either on Xcode 13.2.1. The solution I found was to encapsulate the animation in a custom ViewModifier. The code below illustrates this; the big button toggles between active and inactive animations.
`
struct ContentView: View {
#State private var animationAmount = 1.0
#State private var animationEnabled = false
var body: some View {
VStack {
Button("Tap Me") {
// We would like to stop the animation
animationEnabled.toggle()
animationAmount = animationEnabled ? 2 : 1
}
.onAppear {
animationAmount = 2
animationEnabled = true
}
.padding(50)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
)
.modifier(AnimatedCircle(animationAmount: $animationAmount, animationEnabled: $animationEnabled))
}
}
}
struct AnimatedCircle: ViewModifier {
#Binding var animationAmount: Double
#Binding var animationEnabled: Bool
func body(content: Content) -> some View {
if animationEnabled {
return content.animation(.easeInOut(duration: 2).repeatForever(autoreverses: false),value: animationAmount)
}
else {
return content.animation(.easeInOut(duration: 0),value: animationAmount)
}
}
}
`
It may not be the best conceivable solution, but it works. I hope it helps somebody.