Animation doesn't start after reopening the app? - animation

Objective
I need my app to scale a UIImage.
Code
To do so, I use this method:
func animateStuff() {
println("Animate stuff called!")
let optionsAnimateStuff = UIViewAnimationOptions.Repeat | UIViewAnimationOptions.Autoreverse | UIViewAnimationOptions.AllowUserInteraction
let value : CGFloat = 1.045
UIView.animateWithDuration(0.9, delay: 0.0, options:
optionsAnimateStuff, animations: {
println("Let's scale this image!")
self.image.transform = CGAffineTransformMakeScale(value, value)
}, completion: { finished in })
}
When I close the app and open it again the method gets called again (due to the code inside applicationDidBecomeActivebut the UIImage does not animate anymore.
Question
Why doesn't the object animate?

The problem might be that the image still has the previous transform applied. You likely want to return it to its original when the animation completes (when the view goes away).
Try using this code in your completion:
{ finished in
self.image.transform = CGAffineTransformIdentity
}

Related

SwiftUI animation - toggled Boolean always ends up as true

I'm trying to create an animation in my app when a particular action happens which will essentially make the background of a given element change colour and back x number of times to create a kind of 'pulse' effect. The application itself is quite large, but I've managed to re-create the issue in a very basic app.
So the ContentView is as follows:
struct ContentView: View {
struct Constants {
static let animationDuration = 1.0
static let backgroundAlpha: CGFloat = 0.6
}
#State var isAnimating = false
#ObservedObject var viewModel = ContentViewViewModel()
private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(6, autoreverses: false)
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button(action: {
animate()
}) {
Text("Button")
.foregroundColor(Color.white)
}
}
.background(isAnimating ? Color.red : Color.blue)
.onReceive(viewModel.$shouldAnimate, perform: { _ in
if viewModel.shouldAnimate {
withAnimation(self.animation, {
self.isAnimating.toggle()
})
}
})
}
func animate() {
self.viewModel.isNew = true
}
}
And then my viewModel is:
import Combine
import SwiftUI
class ContentViewViewModel: ObservableObject {
#Published var shouldAnimate = false
#Published var isNew = false
var cancellables = Set<AnyCancellable>()
init() {
$isNew
.sink { result in
if result {
self.shouldAnimate = true
}
}
.store(in: &cancellables)
}
}
So the logic I am following is that when the button is tapped, we set 'isNew' to true. This in turn is a publisher which, when set to true, sets 'shouldAnimate' to true. In the ContentView, when shouldAnimate is received and is true, we toggle the background colour of the VStack x number of times.
The reason I am using this 'shouldAnimate' published property is because in the actual app, there are several different actions which may need to trigger the animation, and so it feels simpler to have this tied to one variable which we can listen for in the ContentView.
So in the code above, we should be toggling the isAnimating bool 6 times. So, we start with false then toggle as follows:
1: true, 2: false, 3: true, 4: false, 5: true, 6: false
So I would expect to end up on false and therefore have the background white. However, this is what I am getting:
I tried changing the repeatCount (in case I was misunderstanding how the count works):
private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(7, autoreverses: false)
And I get the following:
No matter the count, I always end on true.
Update:
I have now managed to get the effect I am looking for by using the following loop:
for i in 0...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i), execute: {
withAnimation(self.animation, {
self.isAnimating.toggle()
})
})
}
Not sure this is the best way to go though....
To understand what is going on, it would help to understand CALayer property animations.
When you define an animation the system captures the state of a Layer and watches for changes in the animatable properties of that layer. It records property changes for playback during the animation. To present the animation, it create a copy of the layer in its initial state (the presentationLayer). It then substitutes the copy in place of the actual layers on screen and runs the animation by manipulating the animatable properties of the presentation layer.
I this case, when you begin the animation, the system watches what happens to the CALayer that backs your view and captures the changes to any animatable properties (in this case the background color). It then creates a presentationLayer and replays those property changes repeatedly. It's not running your code repeatedly - it's changing the properties of the presentation Layer.
In other words the animation the system knows the layer's background color property should toggle back and forth because of the example you set in your animation block, but the animation toggles the background color back and forth without running your code again.

SwiftUI and excessive redrawing

TL;DR:
Applying visual effects to the contents of a ScrollView causes thousands of requests for the same (unchanging) image for each drag gesture. Can I reduce this? (In my real app, I have 50-odd images in the view, and the scrolling is correspondingly sluggish.)
Gist
To give a little life to a scrolling HStack of images, I applied a few transforms for a circular "carousel" effect. (Tips of the hat to sample code from John M. and Paul Hudson)
The code is copy-paste-runnable as given. (You do need to provide an image.) Without the two lines marked /* 1 */ and /* 2 */ the Slide object reports six image requests, no matter how much you drag and scroll. Enable the two lines, and watch the request count zoom to 1000 with a single flick of your finger.
Remarks
SwiftUI is predicated on the inexpensive re-drawing of lightweight Views based on current state. Careless management of state dependency can improperly invalidate parts of the view tree. And in this case, the constant rotation and scaling while scrolling makes the runtime re-render the content.
But... should this necessarily require the continual re-retrieval of static images? Casual dragging back-and-forth of my little finger will trigger tens of thousands of image requests. This seems excessive. Is there a way to reduce the overhead in this example?
Of course this is a primitive design, which lays out all its contents all of the time, instead of taking the cell-reuse approach of, say, UITableView. One might think to apply the transformations only on the three currently-visible views. There is some discussion about this online, but in my attempts, the compiler couldn't do the type inference.
Code
import SwiftUI
// Comment out lines marked 1 & 2 and watch the request count go down.
struct ContentView: View {
var body: some View {
GeometryReader { outerGeo in
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(Slide.all) { slide in
GeometryReader { innerGeo in
Image(uiImage: slide.image).resizable().scaledToFit()
/* 1 */ .rotation3DEffect(.degrees(Double(innerGeo.localOffset(in: outerGeo).width) / 10), axis: (x: 0, y: 1, z: 0))
/* 2 */ .scaleEffect(1.0 - abs(innerGeo.localOffset(in: outerGeo).width) / 800.0)
}
.frame(width:200)
}
}
}
}
.clipped()
.border(Color.red, width: 4)
.frame(width: 400, height: 200)
}
}
// Provides images for the ScrollView. Tracks and reports image requests.
struct Slide : Identifiable {
let id: Int
static let all = (1...6).map(Self.init)
static var requestCount = 0
var image: UIImage {
Self.requestCount += 1
print("Request # \(Self.requestCount)")
return UIImage(named: "blueSquare")! // Or whatever image
}
}
// Handy extension for finding local coords.
extension GeometryProxy {
func localOffset(in outerGeo: GeometryProxy) -> CGSize {
let innerFrame = self.frame(in: .global)
let outerFrame = outerGeo.frame(in: .global)
return CGSize(
width : innerFrame.midX - outerFrame.midX,
height: innerFrame.midY - outerFrame.midY
)
}
}
i think you could try it like this:
no, it is not a full solution, i just took one cached image (instead of an array of cached images, which you have to preload beforehand) but the concept should be clear and so this should be fast...i think
class ImageCache {
static let slides = Slide.all
// do prefetch your images here....
static let cachedImage = UIImage(named: "blueSquare")!
struct Slide : Identifiable {
let id: Int
static let all = (1...6).map(Self.init)
static var requestCount = 0
var image: UIImage {
Self.requestCount += 1
print("Request # \(Self.requestCount)")
// return ImageCache.image! // Or whatever image
return ImageCache.cachedImage // Or whatever image
}
}
}
// Comment out lines marked 1 & 2 and watch the request count go down.
struct ContentView: View {
var body: some View {
GeometryReader { outerGeo in
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(ImageCache.slides) { slide in
GeometryReader { innerGeo in
Image(uiImage: slide.image).resizable().scaledToFit()
/* 1 */ .rotation3DEffect(.degrees(Double(innerGeo.localOffset(in: outerGeo).width) / 10), axis: (x: 0, y: 1, z: 0))
/* 2 */ .scaleEffect(1.0 - abs(innerGeo.localOffset(in: outerGeo).width) / 800.0)
}
.frame(width:200)
}
}
}
}
.clipped()
.border(Color.red, width: 4)
.frame(width: 400, height: 200)
}
}
// Provides images for the ScrollView. Tracks and reports image requests.
// Handy extension for finding local coords.
extension GeometryProxy {
func localOffset(in outerGeo: GeometryProxy) -> CGSize {
let innerFrame = self.frame(in: .global)
let outerFrame = outerGeo.frame(in: .global)
return CGSize(
width : innerFrame.midX - outerFrame.midX,
height: innerFrame.midY - outerFrame.midY
)
}
}

Show current frame rate of a MTKView

I know that in SceneKit, you can enable a banner on the side of the SKView to look at real time frame rates and other useful debugging information. But what about MTKView? I don't seem to find such a property to enable, or how I can query the current frame rate. (Because I am rendering something that have a frame rate of 0.5fps or so)
I don't think there is simple flag for you. Because you control the complete rendering pipeline when creating a command buffer, Metal can't know where to inject a rendering pass with some custom text.
You could inject your own rendering pass (based on a flag like var showDebugInformation = true) in your pipeline, but that sounds like a bit of work.
I would probably monitor frame times manually in the draw method and update a label every draw. A rough outline could look like this:
var previousFrameAtTime: Date
let lastFrameTime = CurrentValueSubject<TimeInterval, Never>(.infinity)
func draw(in view: MTKView) {
lastFrameTime.send(Date().timeIntervalSince(previousFrameAtTime))
previousFrameAtTime = Date()
// ...
}
Then you can observe this value in your view, something like this:
import Combine
class MyViewController: UIViewController {
let label = UILabel()
var cancellables: [AnyCancellable] = []
func subscribeToFrameTime() {
renderer.lastFrameTime
.sink { label.text = "\($0 * 1000) ms." }
.store(in: &cancellables)
}
}

Animating Auto Layout constraints with NSView.layoutSubtreeIfNeeded() not working on macOS High Sierra

I have a basic Mac app with a view animation done through Auto Layout:
I add a new view to the right of the current view
I update the constraints so that the new view ends up filling the window
→ The animation will make it appear as if the view slides in from the right.
The recommended way for animating Auto Layout changes is:
Update the constraints
Use NSAnimationContext.runAnimationGroup()
Set allowsImplicitAnimation to true inside the animation block
Call view.layoutSubtreeIfNeeded() inside the animation block
I followed this recommendation and everything worked fine on macOS Sierra, but on macOS High Sierra, the animation does not take place anymore. Instead the view shows up at its final position without the animation.
I found a workaround: I schedule the animation on the next runloop cycle using DispatchQueue.main.async. However, that seems like a hack and I'm wondering if there is something else I'm missing here.
Here is my actual code:
private func appendSlideViewControllerAnimated(_ viewController:NSViewController, to viewToTheLeft:NSView)
{
// Insert the new view on the very right, just outside the parent:
viewController.view.frame = self.view.bounds
viewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(viewController.view)
viewController.view.topAnchor.constraint( equalTo: view.topAnchor ).isActive = true
viewController.view.bottomAnchor.constraint( equalTo: view.bottomAnchor ).isActive = true
viewController.view.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
viewController.view.leadingAnchor.constraint(equalTo: viewToTheLeft.trailingAnchor).isActive = true
// Update the layout after we just added the view on the right:
view.layoutSubtreeIfNeeded()
// Starting with macOS High Sierra, animating constraint changes for the newly inserted view
// only works if scheduled on the next runloop:
//DispatchQueue.main.async {
// Update the constraints to pin the view to the left:
self.view.removeConstraint(self.activeSlideLeadingConstraint!)
self.activeSlideLeadingConstraint = viewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor)
self.activeSlideLeadingConstraint?.constant = 0
self.activeSlideLeadingConstraint?.isActive = true
NSAnimationContext.runAnimationGroup( { context in
self.isAnimating = true
context.duration = self.slidingAnimationDuration
context.allowsImplicitAnimation = true
self.view.layoutSubtreeIfNeeded()
}, completionHandler: {
viewToTheLeft.removeFromSuperview()
self.clearUndoHistory()
self.updateFirstResponder()
self.isAnimating = false
})
//}
}
Enable Core Animation backing for root view you trying to animate. It can be done in Interface Builder or programmatically:
override func viewDidLoad()
{
super.viewDidLoad()
view.wantsLayer = true
}

transition scene sprite kit

Im having trouble transitioning to pre determined location on a different scene. (for example when Mario goes into the tunnel he returns to the original scene right where he left off) I was able to code a way to get to the next scene but node does not appear were I would like it to.
this is my code to transition to second Scene
func didBeginContact(contact: SKPhysicsContact) {
_ = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if (contact.bodyA.categoryBitMask == BodyType.cup.rawValue && contact.bodyB.categoryBitMask == BodyType.ball.rawValue) {
touchmain()
} else if (contact.bodyA.categoryBitMask == BodyType.ball.rawValue && contact.bodyB.categoryBitMask == BodyType.cup.rawValue) {
touchmain()
}
}
func touchmain() {
let second = GameScene(fileNamed:"GameScene")
second?.scaleMode = .AspectFill
self.view?.presentScene(second!, transition: SKTransition.fadeWithDuration(0.5))
}
I would really appreciate it if you guys can help a young developer out. much love!
There are many ways you could go about this. The general gist of it is, just remember "Mario's" location before you leave the scene and pass it along to a property in the next scene. (or you could use delegation to get the info)
If the scenario is that he starts in SceneA(main level) transfers to SceneB(small money tunnel) and then returns to SceneA(main level) you could pass the location to SceneB and have a property in SceneB that stores where Mario was. Then when you transfer back to SceneA just pass the property back to a property in SceneA, and position Mario and SceneA accordingly
func touchmain() {
let marioPos: CGPoint = Mario.position
let second = GameScene(fileNamed:"GameScene")
second.startingPos = marioPos
second?.scaleMode = .AspectFill
self.view?.presentScene(second!, transition: SKTransition.fadeWithDuration(0.5))
}
You can use the userData field on SKScene to remember the position:
Somewhere in the first scene init, do :
userData = [String : NSObject]()
Also, in your first scene, we want to override didMove(to:SKView) to set mario position based on your userdata :
override func didMove(to:SKView)
{
super.didMove(to:to)
if let position = self.userData["position"]
{
mario.position = position
}
}
We then want to actually assign the user data when transitioning away from the scene:
func touchmain() {
if let userData = self.userData
{
userData["marioPosition"] = mario.position
}
if let second = GameScene(fileNamed:"GameScene")
{
second.scaleMode = self.scaleMode //I assume we do not want to change scaleMode between scenes
second.userData = userData
self.view?.presentScene(second, transition: SKTransition.fadeWithDuration(0.5))
}
else
{
print("Error creating Second scene")
}
}
When we are coming back, we want to transfer the userData back to the first screen. This is where the didMove that I mentioned earlier comes into play, this will set the position of mario to previous location.
... I do not see this code, so try and apply it to how ever you are doing it
func touchBackToFirst() {
if let first = GameScene(fileNamed:"GameScene")
{
first.scaleMode = self.scaleMode
first.userData = userData
self.view?.presentScene(first, transition: SKTransition.fadeWithDuration(0.5))
}
else
{
print("Error creating Firstscene")
}
}

Resources