3

I am making an app where I have a button who is moving from one side of the screen to the other. I have created a pause button which pauses the animation once that button is selected. This button and function works fine. I also added a function which determines if the user has exited the application. In this function, I added the pause button function. However, when the user returns to the app after exiting, it shows the pause screen, but the animation is completed. Here is the code:

@IBAction func pauseButton(sender: UIButton) {
    let button = self.view.viewWithTag(1) as? UIButton!
    let layer = button!.layer
    pauseLayer(layer) // Pausing the animation
    view2.hidden = false // Showing pause screen
}
func pauseLayer(layer: CALayer) {
    let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
    layer.speed = 0.0
    layer.timeOffset = pausedTime
}
@IBAction func startButton(sender: UIButton) {
    let button = UIButton()
    button.tag = 1 // so I can retrieve it in the pause function
    // Extra code to make the button look nice
    button.translatesAutoresizingMaskIntoConstraints = false // Allow constraints
    let topAnchorForButton = button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 400)
    NSLayoutConstraint.activateConstraints([ // Adding constraints
        topAnchorForButton,
        button.leadingAnchor.constraintEqualToAnchor(view.topAnchor, constant: 120),
        button.widthAnchor.constraintEqualToConstant(65),
        button.heightAnchor.constraintEqualToConstant(65)
        ])
    self.view.layoutIfNeeded()
    [UIView.animateWithDuration(5.0, delay: 0.0, options: [.CurveLinear, .AllowUserInteraction], animations: {
        topAnchorForButton.constant = 0
        self.view.layoutIfNeeded()
        }, completion: nil )] // the animation

override func viewDidLoad() {
    super.viewDidLoad()
         NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseButton:", name: UIApplicationWillResignActiveNotification, object: nil) // When exists app
         NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseButton:", name: UIApplicationDidEnterBackgroundNotification, object: nil) // when goes to home screen
}

When I re-enter the app, the animation is completed.

Here's is a code I've tried which only plays and stops the animation. I've also added something in the App Delegate but it still doesn't work:

ViewController:

class Home: UIViewController {

func pauseAnimation(){
let button = self.view.viewWithTag(1) as? UIButton!
let layer = button!.layer
pauseLayer(layer) // Pausing the animation
}  
@IBAction func pauseButton(sender: UIButton) {
   pauseAnimation()
}
func pauseLayer(layer: CALayer) {
    let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
    layer.speed = 0.0
    layer.timeOffset = pausedTime
}
@IBAction func startButton(sender: UIButton) {
    let button = UIButton()
    button.tag = 1 // so I can retrieve it in the pause function
    // Extra code to make the button look nice
    button.setTitle("Moving", forState: .Normal)
    button.setTitleColor(UIColor.redColor(), forState: .Normal)
    view.addSubview(button)
    button.translatesAutoresizingMaskIntoConstraints = false // Allow constraints
    let topAnchorForButton = button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 400)
    NSLayoutConstraint.activateConstraints([ // Adding constraints
        topAnchorForButton,
        button.leadingAnchor.constraintEqualToAnchor(view.topAnchor, constant: 120),
        button.widthAnchor.constraintEqualToConstant(65),
        button.heightAnchor.constraintEqualToConstant(65)
        ])
    self.view.layoutIfNeeded()
    [UIView.animateWithDuration(10.0, delay: 0.0, options: [.CurveLinear, .AllowUserInteraction], animations: {
        topAnchorForButton.constant = 0
        self.view.layoutIfNeeded()
        }, completion: nil )] // the animation
}

App Delegate:

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
var vc = Home()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
}
func applicationWillResignActive(application: UIApplication) {
   vc.pauseAnimation()
}

Just in case you want to know, here is the code for resuming the animation:

@IBAction func resume(sender: UIButton) {
    let button = self.view.viewWithTag(100) as? UIButton!
    let layer = button!.layer 
    resumeLayer(layer)
}
func resumeLayer(layer: CALayer) {
    let pausedTime: CFTimeInterval = layer.timeOffset
    layer.speed = 1.0
    layer.timeOffset = 0.0
    layer.beginTime = 0.0
    let timeSincePause: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
    layer.beginTime = timeSincePause
}

When I exit the app, it says that button or/and layer is nil (fatal error... optional value is nil), so it crashes.

Please help. Thanks in advance... Anton

Anton O.
  • 653
  • 7
  • 27

3 Answers3

2

You need to know a little about how animations work under the hood. This is very well explained in WWDC session 236 'Building Interruptible and Responsive Interactions' (the part you are interested in starts at 17:45). In the meantime, read about CABasicAnimation, and checkout the presentationLayer property on CALayer (you will need to drop down to CALayer for what you want to achieve, and maybe you will need to implement the animations using CA(Basic)Animation, although maybe UIKit's animation methods might still be able to do the trick, if you've structured your code well).

Basically, a view or layer will get the end-values that you set as part of an animation immediately. Then the animation starts, from begin to end. If the animation gets interrupted, say by leaving the app, upon return the view gets drawn with the values it has (which are the end-values, as the animation is gone).

What you want is that you capture the value of the view at the point in the animation when you leave the app, save it, and start a new animation from that point to the original endpoint when your app comes back to the foreground. You can get the current position of a CALayer (not UIView) by asking for the frame, or transform, whatever you need, from the CALayer's presentationLayer property.

I had this problem when building a custom refreshControl for use in any `UIScrollView``. This code is on Github, the relevant code for you is here (its Objective-C, but it is not very complicated, so you should be able to follow along and re-implement what you need in Swift): JRTRefreshControlView.m, specifically:

-(void)willMoveToWindow:(UIWindow *)newWindow This gets called when the user moves the app to the background, or when the view is removed from the superView (newWindow will be nil).

And the actual implementation of an animation in a subclass is here: JRTRotatingRefreshControlView.m, specifically the

- (void) resumeRefreshingState method is what you want to look at.

(there is also saveRefreshingState, which is where you can save the presentation-values from the presentationLayer, but it is not implemented for my animations).

Joride
  • 3,722
  • 18
  • 22
1

Try putting this code in:

View controller:

pauseAnimation(){

    let button = self.view.viewWithTag(1) as? UIButton!
let layer = button!.layer
pauseLayer(layer) // Pausing the animation
view2.hidden = false // Showing pause screen

}

AppDelegate:

func applicationWillResignActive(application: UIApplication) {

    vc.pauseAnimation()

}

To be able to access functions from view controllers in the appDelegate go here: link

Apple Geek
  • 1,121
  • 1
  • 10
  • 16
  • Do you mean ViewDidLoad? – Anton O. Jan 16 '16 at 01:25
  • @AntonO. No I mean viewDidAppear() try it whenever the view appears it runs where as viewdidload runs whenever the view controller is reloaded – Apple Geek Jan 16 '16 at 01:26
  • One problem: you cant call function or put IBActions inside of viewdidAppear – Anton O. Jan 16 '16 at 01:38
  • make a start animation button and copy and paste the startButton() code into it then run startAnimation in viewDidAppear. – Apple Geek Jan 16 '16 at 01:42
  • 1
    Sorry I'm a little bit confused. Can you provide code if possible? It would be really appreciated – Anton O. Jan 16 '16 at 01:51
  • @AntonO. I updated my answer a bit let me know if it helps I gotta go now. – Apple Geek Jan 16 '16 at 02:15
  • What I did was that I added a function called startAnimation with code of start button. I called the function startAnimation when I click the start button and in viewdidappear, but it didn't work – Anton O. Jan 16 '16 at 03:08
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/100862/discussion-between-apple-geek-and-anton-o). – Apple Geek Jan 16 '16 at 19:36
  • Thanks for the update! When I exit the app, it crashes because it says that button in let layer = button!.layer is equal to nil. What do you think is should do? Do you want me to send you the code? – Anton O. Jan 16 '16 at 21:57
1

Have a look here. Listing 5-4 shows an example of how to pause and resume animation. You should move the code out of AppDelegate, and instead the view controller (Home) should handle going into and out of the background. Within Home's viewDidLoad or viewWillAppear:

NSNotificationCenter.defaultCenter().addObserver(
    self,
    selector: "resigningActive",
    name: UIApplicationWillResignActiveNotification,
    object: nil
)
NSNotificationCenter.defaultCenter().addObserver(
    self,
    selector: "becomeActive",
    name: UIApplicationDidBecomeActiveNotification,
    object: nil
)

You will need to removeObserver, probably in Home's deinit. Then you have:

func resigningActive() {
    pauseLayer(button!.layer)
}
func becomeActive() {
    resumeLayer(button!.layer)
}

This code doesn't need another button to pause/resume, but you could also do it that way if you want. You will probably need the observers to be defined in viewWillAppear, since it looks like your code will reference outlets than don't yet exist in viewDidLoad. This will mean you must code to only hook up the observers the first time viewWillAppear is called, or you will be adding multiple observers for the same event.

Your pauseLayer should also write pausedTime to NSUserDefaults, and resumeLayer should read it back, along the lines of (untested):

// Write
NSUserDefaults.standarduserDefaults().setDouble(Double(pausedTime), forKey: "pausedTime")
// Read
pausedTime = CFTimeInterval(NSUserDefaults.standarduserDefaults().doubleForKey("pausedTime"))
Michael
  • 8,891
  • 3
  • 29
  • 42
  • Thanks for the answer. I see that this is in Objective C and not swift... Do you know where this information could be in Swift? – Anton O. Jan 19 '16 at 01:04
  • Unless you can find it elsewhere, you would need to convert it to Swift. You already have the pauseLayer converted to Swift, you just need to do resumeLayer. You haven't included the cost showing how you're resuming the animation. – Michael Jan 19 '16 at 02:00
  • Ok I'll add it later... I will tell you when I add it – Anton O. Jan 19 '16 at 02:02
  • I posted the code. I believe its the same as the one from your site(I was using it). Any other ideas on what may be the problem? As i said, when I exit the app, it says that button or/and layer is nil (fatal error... optional value is nil), so it crashes. – Anton O. Jan 19 '16 at 02:23
  • So the animation pause/resume code is probably okay. I don't think you should put the `pauseAnimation` in the AppDelegate's `applicationWillResignActive`, as the ViewController is probably in the process of being destroyed. Similarly it is creating a new Home instance during start-up. Unless you are handling this yourself, it will be a different instance to the one showing. – Michael Jan 19 '16 at 05:00
  • I will reword my answer to make it clearer (hopefully) – Michael Jan 19 '16 at 08:19
  • Thanks for the update. I have tried your solution before and I have re tried it right now. Whats happening is when I'm leaving the app and then returning, the animation is completed and doesn't resume from where it left of. Any ideas why? (If you need more information about my code, just ask) – Anton O. Jan 19 '16 at 12:06
  • Are you storing pausedTime in NSUserDefaults? You will need to store it in resigningActive, and reread it in becomeActive. – Michael Jan 19 '16 at 22:49
  • Ya, PauseLayer is in resignActive if thats what you mean – Anton O. Jan 19 '16 at 22:50
  • No, I mean resignActive needs to write pausedTime to NSUserDefaults, and it needs to be read again when the app starts again. You can confirm this needs to be done by printing out pausedTime in resumeLayer. It is probably zero, and certainly will be if your app gets thrown out of memory. – Michael Jan 19 '16 at 23:05
  • Ohh, ok. I'm kind of new to NSUserDefaults. Do you think you can provide the code because I think this may be the problem. – Anton O. Jan 19 '16 at 23:09
  • For the read line, it says that I can't assign value of type AnyObject? to CFTimeInterval. – Anton O. Jan 20 '16 at 00:40
  • I've updated the answer, but it's getting way off track of the question. You need to read the documentation on NSUserDefaults to understand how they work. – Michael Jan 20 '16 at 01:23
  • Thanks for all the updates, I really appreciate it. Sadly it did not work though, but thanks for trying :) – Anton O. Jan 20 '16 at 01:27