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.
Related
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)
}
I have a slider for my music app that updates its knob position every second using a Timer publisher. But I don't want it to pause animating after each second instead animate smoothly and continuously like the slider/scrubber from WhatsApp Media View or Apple Music. Here is the code that I tried:
This is what WhatsApp does
This is what my app does
//My View Model
class AudioPlaybackManager: NSObject, ObservableObject {
var timer = Timer.publish(every: 1.0, tolerance: 0.5, on: .main, in: .common).autoconnect()
#Published var sliderValue = 0.0
}
//My View
struct Scrubber: View {
#EnvironmentObject var audioPlaybackManager: AudioPlaybackManager
var body: some View {
Slider(value: $audioPlaybackManager.sliderValue, in: 0...audioPlaybackManager.duration) { _ in
audioPlaybackManager.audioPlayer?.currentTime = audioPlaybackManager.sliderValue
}
.onReceive(audioPlaybackManager.timer) { _ in
//Right here, I want the slider to move/animate continuously instead of animate for each second and pause for a while.
withAnimation(.easeInOut) {
if audioPlaybackManager.isPlaying {
audioPlaybackManager.sliderValue = audioPlaybackManager.audioPlayer?.currentTime ?? 0.0
}
}
}
}
}
I think you're seeing the behavior you are because you're using the .easeInOut Animation, which does not apply the animation at a constant speed, but instead starts slowly, speeds up and ends slowly - easing in and out. I think if you change to use .linear instead, it will fix your behavior because the animation will be applied evenly over time instead.
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.
In my project, I have a SCNView encapsulated in UIViewRepresentable, that should resize in height according to the DragGesture of another view.
The issue is that when dragging, or after drag gesture ended, the memory usage is abnormally high. Checking for memory leaks in Instruments yielded no leaks. It also only occurs in a physical device, not in Simulator.
When using the Simulator,
The memory use is stable at around 19MB, regardless of resizing or not.
When using a physical device,
The memory use spiked badly upon each resize, and doesn't fall after end. (Worst I've seen was around 1.9GB of memory usage, before crash.)
Any experts here know the reason why it happens, and how to fix it? Thanks in advance.
UPDATE: I noticed that after DragGesture ended, if I were to move the camera of the SCNView, the memory will drop back to normal values.
Heres the code of the main ContentView.
Basically its a split view layout where if I were to adjust the RoundedRectangle (which acts as a handle), the top and bottom views will resize. It is when resizing that will cause the memory spike issue, which, at worst, will cause a crash, and Message from debugger: Terminated due to memory issue.
VStack {
SceneView()
.frame(height: (UIScreen.main.bounds.height / 2) + self.gestureTranslation.height)
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 6)
.foregroundColor(Color.gray)
.padding(2)
.gesture(DragGesture(coordinateSpace: .global)
.onChanged({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
})
.onEnded({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
self.prevTranslation = self.gestureTranslation
})
)
Rectangle()
.fill(Color.green)
.frame(height: (UIScreen.main.bounds.height / 2) - self.gestureTranslation.height)
}
}
Heres the code for the UIViewRepresentable wrapper for the SCNView.
struct SceneView: UIViewRepresentable {
typealias UIViewType = SCNView
func makeUIView(context: Context) -> SCNView {
let scene = SCNScene(named: "SceneAssets.scnassets/Cube.scn")
let view = SCNView()
view.scene = scene
view.allowsCameraControl = true
view.defaultCameraController.interactionMode = .orbitTurntable
view.defaultCameraController.inertiaEnabled = true
return view
}
func updateUIView(_ uiView: SCNView, context: Context) {
}
}
Here's the full test project that I've made to describe the issue: https://github.com/vincentneo/swiftui-scnview-memory-issue
I'm exploring how to handle infinite animation. I learn how to start it and trying to stop it manually. The best solution I find is to calculate current value of animated property and set it while stopping animation. Well, it works nice except one moment. It looks like rotationEffect() uses built in self animation to stop rotation. if you try start and stop animation immediately, you will se something like velocity physics. Something like .spring animation, but I used .linear. I tried to disable it by using withAnimation(.none), but failed.
And if you stop rotation close to 90 degrees (but less then 90), it overshoot 90 degrees more.
The only way to fix this I see is to build my own rotation modifier using CG framework.
Any other ideas?
here is the code:
import SwiftUI
class AnimationHandler: ObservableObject{
#Published var isStarted: Bool = true
}
struct AnimationStopping: View {
#ObservedObject var animationHandler = AnimationHandler()
var body: some View {
VStack{
Spacer()
AnimatedRectObservedObject(animationHandler: self.animationHandler)
Spacer()
Button(action: {
self.animationHandler.isStarted.toggle()
}){
Text(self.animationHandler.isStarted ? "stop animation" : "start animation")
}
}
}
}
struct AnimatedRectObservedObject: View{
#State var startTime: Date = Date()
#State var angle: Double = 0
#ObservedObject var animationHandler: AnimationHandler
var animation: Animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
var body: some View{
VStack{
Rectangle()
.fill(Color.green)
.frame(width: 200, height: 200)
.rotationEffect(Angle(degrees: angle))
.animation(animationHandler.isStarted ? animation : .default)
Spacer()
}.onReceive(animationHandler.objectWillChange){
let newValue = self.animationHandler.isStarted
if newValue == false{
let timePassed = Date().timeIntervalSince(self.startTime)
let fullSecondsPassed = Double(Int(timePassed))
let currentStage = timePassed - fullSecondsPassed
let newAngle = self.angle - 90 + currentStage * 90
withAnimation(.none){//not working:(((
self.angle = newAngle
}
}else {
self.startTime = Date()
self.angle += 90
}
}
.onAppear{
self.angle += 90
self.startTime = Date()
}
}
}
update:
Actually, custom modifier will not help. I use it, put print on it, get back actual animation time of it through #ObservableObject and watched this:
current animation position 0.15000057220458984
current animation position 0.15833377838134766
current animation position 0.16666698455810547
current animation position 0.17500019073486328
current animation position 0.1833333969116211
current animation position 0.1916666030883789
current animation position 0.19999980926513672
current animation position 0.20833301544189453
animated from 0.0 to 1.0 stopped at 0.20833301544189453
current animation position 0.21662975207982527
current animation position 0.22470279080698674
current animation position 0.2323109631133775
current animation position 0.23920465996798157
current animation position 0.24513085941998725
current animation position 0.24984809499710536
current animation position 0.25314617272852047
current animation position 0.25487106971286266
current animation position 0.25495114064688096
current animation position 0.25341608746384736
current animation position 0.25040891469689086
current animation position 0.24617629182102974
current animation position 0.2410420152482402
current animation position 0.23537338342976
current animation position 0.2295457124710083
current animation position 0.2239141315640154
current animation position 0.21879699627515947
current animation position 0.2144686846222612
current animation position 0.2111600160587841
current animation position 0.20906241873944964
current animation position 0.20833301544189453
the answer was simple:
.animation(animationHandler.isStarted ? animation : .linear(duration: 0))