简体   繁体   中英

How to do 'serial' animation with GCD?

I'm trying to make a custom UIView displays on screen for 5s when a remote notification comes.

Code like this:

//customView.alpha = 1.0 here
[UIView animateWithDuration:1 animations:^{
                                  customView.alpha = 0.3;
                              } 
                              completion:^(BOOL finished){
                                  // remove customView from super view.
                              }];

Problem and What I Need

But there're cases that a couple of notification may come at short time interval, in which several customView may be animating at the same time and one may cover others.

I want these animations perform one after another, so that they won't conflict.

Assumed but Failed

//(dispatch_queue_t)queue was created in other parts of the code
dispatch_sync(queue, ^{
    [UIView animationWithDuration:animations:...];
});

After making the animation in GCD queue, I got the same result as the origin code I used, which didn't use GCD. Animations are still conflicting.

BTW , I heard that animations or tasks involving UI should always be run on main thread, but in my second code the animation seemed done smoothy. Why?

If it's the same animation that runs every time then you could just have store the number of times that the animation should run (not the same as the repeat count property of the animation).

When you receive the remote notification you increment the counter and call the method that animates if the counter is exactly one. Then in the methodThatAnimates you recursively call yourself in the completion block while decreasing the counter every time. It would look something like this (with pseudocode method names):

- (void)methodThatIsRunWhenTheNotificationIsReceived {
    // Do other stuff here I assume...
    self.numberOfTimesToRunAnimation = self.numberOfTimesToRunAnimation + 1;
    if ([self.numberOfTimesToRunAnimation == 1]) {
        [self methodThatAnimates];
    }
}

- (void)methodThatAnimates {
    if (self.numberOfTimesToRunAnimation > 0) {
        // Animation preparations ...
        [UIView animateWithDuration:1 
                         animations:^{
                                  customView.alpha = 0.3;
                         } 
                         completion:^(BOOL finished){
                                  // Animation clean up ...
                                  self.numberOfTimesToRunAnimation = self.numberOfTimesToRunAnimation - 1;
                                  [self methodThatAnimates];
                         }];
    }
}

Using queues to submit animations in sequence won't work, because the method that begins the animation returns immediately, and the animation is added to the animation tree to be performed later. Each entry in your queue will complete in a tiny fraction of a second.

If each of your animations operates on the same view then by default the system should let each animation finish running before it starts the next one.

To quote the docs for the UIViewAnimationOptionBeginFromCurrentState options value:

UIViewAnimationOptionBeginFromCurrentState

Start the animation from the current setting associated with an already in-flight animation. If this key is not present, any in-flight animations are allowed to finish before the new animation is started. If another animation is not in flight, this key has no effect.

If you want to chain a series of animations, here's what I would do:

Create a mutable array of animation blocks. (code blocks are objects, and can be added to an array.) Write a method that pulls the top animation block off the array (and removes it from the array) and submits it using animateWithDuration:animations:completion, where the completion method simply invokes the method again. Make the code assert a lock before pulling an item off the array, and free the lock after deleting the item.

Then you can write code that responds to an incoming notification by asserting your animation array lock, adding an animation block to the lock, and releasing the lock.

You could use a (non) concurrent NSOperationQueue to perform the animations "step by step"

The NSOperationQueue class regulates the execution of a set of NSOperation objects. After being added to a queue, an operation remains in that queue until it is explicitly canceled or finishes executing its task. Operations within the queue (but not yet executing) are themselves organized according to priority levels and inter-operation object dependencies and are executed accordingly. An application may create multiple operation queues and submit operations to any of them.

Inter-operation dependencies provide an absolute execution order for operations, even if those operations are located in different operation queues. An operation object is not considered ready to execute until all of its dependent operations have finished executing. For operations that are ready to execute, the operation queue always executes the one with the highest priority relative to the other ready operations.

I would suggest sending a message in the completion block to whatever object is triggering the animation. Then you could have that object queue up the notifications itself and start the next one every time it receives the message.

ProcedureKit (based on NSOperation ) is an example of ready-made solution, but it's quite heavyweight to use it only for animations.

My Operation subclass which I use to queue animated pop-ups and other stuff:

class SerialAsyncOperation: Operation {

    private var _started = false

    private var _finished = false {
        willSet {
            guard _started, newValue != _finished else {
                return
            }
            willChangeValue(forKey: "isFinished")
        }
        didSet {
            guard _started, oldValue != _finished else {
                return
            }
            didChangeValue(forKey: "isFinished")
        }
    }

    private var _executing = false {
        willSet {
            guard newValue != _executing else {
                return
            }
            willChangeValue(forKey: "isExecuting")
        }
        didSet {
            guard oldValue != _executing else {
                return
            }
            didChangeValue(forKey: "isExecuting")
        }
    }

    override var isAsynchronous: Bool {
        return true
    }

    override var isFinished: Bool {
        return _finished
    }

    override var isExecuting: Bool {
        return _executing
    }

    override func start() {
        guard !isCancelled else {
            return
        }
        _executing = true
        _started = true
        main()
    }

    func finish() {
        _executing = false
        _finished = true
    }

    override func cancel() {
        _executing = false
        _finished = true
        super.cancel()
    }
}

Example of usage:

// Setup a serial queue
private lazy var serialQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 1
    queue.name = String(describing: type(of: self))
    return queue
}()

// subclass SerialAsyncOperation
private class MessageOperation: SerialAsyncOperation {

    // ...

    override func main() {
        DispatchQueue.main.async { [weak self] in
            // do UI stuff

            self?.present(completion: {
                self?.finish()  
            })
        }
    }

    func present(completion: @escaping () -> Void) {
        // do async animated presentation, calling completion() in its completion
    }

    func dismiss(completion: @escaping () -> Void) {
        // do async animated dismissal, calling completion() in its completion
    }

    // animated cancellation support
    override func cancel() {
        if isExecuting {
            dismiss(completion: {
                super.cancel()
            })
        } else {
            super.cancel()
        }
    }
}

Basically, just add this operation to a serial queue, and remember to call finish() when you finish doing your asynchronous stuff. Also you can cancel all operations on a serial queue with one call, and these would be dismissed gracefully.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM