Single Up/Down Animation on SwiftUI - iphone

I'm trying to make an animation where the coin flips and moves up then back down and stops after button click. With this code it moves up and down, but then snaps to the top position after the animation finishes. I know it must be because of the '.offset' but I don't know a way around it. Youtube hasn't been much help.
#State private var isClicked : Bool = false
Image(coins[numbers[0]])
.resizable()
.scaledToFit()
.scaleEffect(0.6)
.rotation3DEffect(isClicked ?
.init(degrees: 1080) : .init(degrees: 0),
axis: (x: 10.0, y: 0.0, z: 0.0))
.animation(Animation.easeInOut.repeatCount(2, autoreverses: true)
.speed(0.5))
.offset(y: isClicked ? -200 : 0)
Button(action: {self.animation()}, label: {
Text("FLIP!")
.aspectRatio(contentMode: .fit)
.foregroundColor(Color.black)
})
func animation() {
self.isClicked.toggle()
}

You should use Animation.easeInOut.repeatCount(3, autoreverses: true, .speed(0.5)
which has repeatCount(3 instead of repeatCount(2. The coin goes up and comes down and thats were it has done its 2 cycles of repeat that you declared. After that, the coin is supposed to be on top, but it has already done its 2 repeat counts so it can't animate itself to the top, and it just goes to the top without any animations. With repeatCount(3, you added one more cycle so the coin has another chance to go back to top and end the animation there.

Related

SwiftUI What exactly happens when use .offset and .position Modifier simultaneously on a View, which decides the final location?

Here I have this question when I try to give a View an initial position, then user can use drag gesture to change the location of the View to anywhere. Although I already solved the issue by only using .position(x:y:) on a View, at the beginning I was thinking using .position(x:y:) to give initial position and .offset(offset:) to make the View move with gesture, simultaneously. Now, I really just want to know in more detail, what exactly happens when I use both of them the same time (the code below), so I can explain what happens in the View below.
What I cannot explain in the View below is that: when I simply drag gesture on the VStack box, it works as expected and the VStack moves with finger gesture, however, once the gesture ends and try to start a new drag gesture on the VStack, the VStack box goes back to the original position suddenly (like jumping to the original position when the code is loaded), then start moving with the gesture. Note that the gesture is moving as regular gesture, but the VStack already jumped to a different position so it starts moving from a different position. And this causes that the finger tip is no long on top of the VStack box, but off for some distance, although the VStack moves with the same trajectory as drag gesture does.
My question is: why the .position(x:y:) modifier seems only take effect at the very beginning of each new drag gesture detected, but during the drag gesture action on it seems .offset(offset:) dominates the main movement and the VStack stops at where it was dragged to. But once new drag gesture is on, the VStack jumps suddenly to the original position. I just could not wrap my head around how this behavior happens through timeline. Can somebody provide some insights?
Note that I already solved the issue to achieve what I need, right now it's just to understand what is exactly going on when .position(x:y:) and .offset(offset:) are used the same time, so please avoid some advice like. not use them simultaneously, thank you. The code bellow suppose to be runnable after copy and paste, if not pardon me for making mistake as I delete few lines to make it cleaner to reproduce the issue.
import SwiftUI
struct ContentView: View {
var body: some View {
ButtonsViewOffset()
}
}
struct ButtonsViewOffset: View {
let location: CGPoint = CGPoint(x: 50, y: 50)
#State private var offset = CGSize.zero
#State private var color = Color.purple
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
self.offset = value.translation
print("offset onChange: \(offset)")
}
.onEnded{ _ in
if self.color == Color.purple{
self.color = Color.blue
}
else{
self.color = Color.purple
}
}
}
var body: some View {
VStack {
Text("Watch 3-1")
Text("x: \(self.location.x), y: \(self.location.y)")
}
.background(Color.gray)
.foregroundColor(self.color)
.offset(self.offset)
.position(x: self.location.x, y: self.location.y)
.gesture(dragGesture)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
Your issue has nothing to do with the use of position and offset. They actually both work simultaneously. Position sets the absolute position of the view, where as offset moves it relative to the absolute position. Therefore, you will notice that your view starts at position (50, 50) on the screen, and then you can drag it all around. Once you let go, it stops wherever it was. So far, so good. You then want to move it around again, and it pops back to the original position. The reason it does that is the way you set up location as a let constant. It needs to be state.
The problem stems from the fact that you are adding, without realizing it, the values of offset to position. When you finish your drag, offset retains the last values. However, when you start your next drag, those values start at (0,0) again, therefore the offset is reset to (0,0) and the view moves back to the original position. The key is that you need to use just the position or update the the offset in .onEnded. Don't use both. Here you have a set position, and are not saving the offset. How you handle it depends upon the purpose for which you are moving the view.
First, just use .position():
struct OffsetAndPositionView: View {
#State private var position = CGPoint(x: 50, y: 50)
#State private var color = Color.purple
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
position = value.location
print("position onChange: \(position)")
}
.onEnded{ value in
if color == Color.purple{
color = Color.blue
}
else{
color = Color.purple
}
}
}
var body: some View {
Rectangle()
.fill(color)
.frame(width: 30, height: 30)
.position(position)
.gesture(dragGesture)
}
}
Second, just use .offset():
struct ButtonsViewOffset: View {
#State private var savedOffset = CGSize.zero
#State private var dragValue = CGSize.zero
#State private var color = Color.purple
var offset: CGSize {
savedOffset + dragValue
}
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
dragValue = value.translation
print("dragValue onChange: \(dragValue)")
}
.onEnded{ value in
savedOffset = savedOffset + value.translation
dragValue = CGSize.zero
if color == Color.purple{
color = Color.blue
}
else{
color = Color.purple
}
}
}
var body: some View {
Rectangle()
.fill(color)
.frame(width: 30, height: 30)
.offset(offset)
.gesture(dragGesture)
}
}
// Convenience operator overload
func + (lhs: CGSize, rhs: CGSize) -> CGSize {
return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height)
}

Pause/resume animation when app is in background/foreground

I have a SwiftUI app in which there's a cloud that slowly moves across the screen, then resets to its starting position and begins the animation again, infinitely.
I accomplish this with the following code:
import SwiftUI
struct Clouds {
#State private var xOffset: CGFloat = 0.0
#State private var yOffset: CGFloat = 0.0
var body: some View {
Image("cloud")
.resizable()
.scaledToFit()
.aspectRatio(contentMode: .fit)
.frame(height: 200.0)
.offset(x: xOffset, y: yOffset)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
yOffset = MainData.screenHeight * CGFloat.random(in: 0...1)
withAnimation(Animation.linear(duration: 80).repeatForever(autoreverses: false)){
xOffset = MainData.screenWidth
}
}
}
}
}
Desired behavior: When the app is sent to the background, the cloud should stop moving -- and when the app is brought to the foreground, the cloud should resume its movement from where it left off.
Actual behavior: When the app is sent to the background, the cloud continues to animate. The animation is completed, then the cloud stays in its starting position until the app is brought to the foreground.
Question: How can I make it so that the animation pauses when the app is sent to the background, and resumes when the app is brought to the foreground?
Thank you!
As a workaround, I decided to render the clouds via SpriteKit instead of SwiftUI. SpriteKit has the desired behavior by default: the clouds stop animating when the app is brought out of the foreground and begin animating from the same position when the app is brought back into the foreground.
It'd be cool if someone else could answer the original question for SwiftUI, though -- so I won't accept my own answer.

SwiftUI: Synchronise 2 Animation Functions (CAMediaTimingFunction and Animation.timingCurve)

I was using CAMediaTimingFunction (for a window) and Animation.timingCurve (for the content) with the same Bézier curve and time to deal with the resizing. However, there can be a very small time difference and therefore it caused the window to flicker irregularly. Just like this (GitHub repository: http://github.com/toto-minai/DVD-Player-Components ; original design by 7ahang):
Red background indicates the whole window, while green background indicates the content. As you can see, the red background is visible at certain points during the animation, which means that the content is sometimes smaller than the window, so it generates extra small spaces and causes unwanted shaking.
In AppDelegate.swift, I used a custom NSWindow class for the main window and overrode setContentSize():
class AnimatableWindow: NSWindow {
var savedSize: CGSize = .zero
override func setContentSize(_ size: NSSize) {
if size == savedSize { return }
savedSize = size
NSAnimationContext.runAnimationGroup { context in
context.timingFunction = CAMediaTimingFunction(controlPoints: 0, 0, 0.58, 1.00) // Custom Bézier curve
context.duration = animationTime // Custom animation time
animator().setFrame(NSRect(origin: frame.origin, size: size), display: true, animate: true)
}
}
}
In ContentView.swift, the main structure (simplified):
struct ContentView: View {
#State private var showsDrawer: Bool = false
var body: some View {
HStack {
SecondaryAdjusters() // Left panel
HStack {
Drawer() // Size-changing panel
.frame(width: showsDrawer ? 175 : 0, alignment: .trailing)
DrawerBar() // Right bar
.onTapGesture {
withAnimation(Animation.timingCurve(0, 0, 0.58, 1.00, duration: animationTime)) { // The same Bézier curve and animation time
showsDrawer.toggle()
}
}
}
}
}
}
The animated object is the size-changing Drawer() in the middle. I suppose that it is because the different frame rate of two methods. I'm wondering how to synchronise the two functions, and whether there's another way to implement drawer-like View in SwiftUI. You could clone the repository here. Thanks for your patient.

How to get .onHover(...) events for custom Shapes

Say you have a Circle. How can you change its color when you hover inside the circle?
I tried
struct ContentView: View {
#State var hovered = false
var body: some View {
Circle()
.foregroundColor(hovered ? .purple : .blue)
.onHover { self.hovered = $0 }
}
}
But this causes hovered to be true even when the mouse is outside of the circle (but still inside its bounding box).
I noticed that the .onTapGesture(...) uses the hit testing of the actual shape and not the hit testing of its bounding box.
So how can we have similar hit testing behavior as the tap gesture but for hovering?
The answer depends on the precision you need. The hove in SwiftUI currently just monitor the MouseEnter and MouseExit events, so the working region for a view is the frame which is a Rectangle.
You may build a backgroundView in ZStack which can compose those Rectangle with a customized algorithm. In circle shape, it should be like a matrix with different some small rectangles. One oversimplified example could be like the following.
ZStack{
VStack{
HStack{
Rectangle().frame( width:100, height: 100).offset(x: -50, y: 0)
Rectangle().frame( width:100, height: 100).offset(x: 50, y: 0)
}}.onHover{print($0)}
Rectangle().foregroundColor(hovered ? .purple : .blue).clipShape(Circle())
}

onLongPressGesture frame in SwiftUI

I am trying to replicate a long press button effect like the Apple default one, but with a custom style.
I have a custom view where I call onLongPressGesture. The issue is that the pressing variable is getting set to false even though my finger is still pressing.
I just move my finger outside the the views onLongPressGesture frame.
I want the pressing variable to not get set to false when I move my finger outside the frame area.
How can i achieve that?
Here's my code:
.onLongPressGesture(minimumDuration: 1000000000, maximumDistance: 100, pressing: {
pressing in
if !pressing {
self.action?()
self.showNextScreen = true
} else {
withAnimation(.spring()) {
self.showGrayBackgound = true
}
}
}) { }
Use the maximumDistance parameter to set how far outside the view the gesture applies:
struct LongPressView: View {
#State var isPressing = false
let action: ()->()
var body: some View {
Rectangle()
.fill(isPressing ? Color.orange : .gray)
.frame(width: 50, height: 30)
.onLongPressGesture(minimumDuration: 1000000, maximumDistance: 1000, pressing: { pressing in
self.isPressing = pressing
if !pressing { self.action() }
}, perform: {})
}
}
A sufficiently large maximumDistance will be outside the boundary of the screen and the long press will remain active until it's released. However, the macOS behavior where you click and hold while dragging outside the frame and back so that the button's state is turned off and back on isn't possible with a LongPressGesture. Once outside the maximumDistance, the gesture is complete.