Как сделать "серийную" анимацию с GCD?

Я пытаюсь создать пользовательские UIView дисплеи на экране в течение 5 секунд, когда приходит удаленное уведомление.

Код, подобный этому:

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

Проблема и то, что мне нужно

Но есть случаи, когда несколько уведомлений могут появляться через короткий промежуток времени, в котором несколько customView могут одновременно анимироваться, а другие могут быть закрыты другими.

Я хочу, чтобы эти анимации выполнялись один за другим, чтобы они не конфликтуют.

Предполагаемый, но неудачный

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

После создания анимации в очереди GCD я получил тот же результат, что и исходный код, который я использовал, который не использовал GCD. Анимация по-прежнему противоречива.

BTW, я слышал, что анимации или задачи с участием UI всегда должны запускаться в основном потоке, но в моем втором коде анимация казалась сглаженной. Почему?

Ответ 1

Если это одна и та же анимация, которая запускается каждый раз, тогда вы можете просто сохранить количество раз, которое должна выполняться анимацией (не такая же, как свойство повторения счетчика анимации).

Когда вы получаете удаленное уведомление, вы увеличиваете счетчик и вызываете метод, который оживляет, если счетчик точно равен одному. Затем в методеThatAnimates вы рекурсивно называете себя в блоке завершения при каждом уменьшении счетчика. Он будет выглядеть примерно так (с именами псевдокода):

- (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];
                         }];
    }
}

Ответ 2

Использование очередей для представления анимаций в последовательности не будет работать, потому что метод, который начинает анимацию, немедленно возвращается, а анимация добавляется в дерево анимации, которое будет выполнено позже. Каждая запись в вашей очереди будет завершена за небольшую долю секунды.

Если каждая из ваших анимаций работает на одном и том же представлении, то по умолчанию система должна позволить каждой анимации завершить работу до того, как она запустит следующий.

Чтобы процитировать документы для значения опций UIViewAnimationOptionBeginFromCurrentState:

UIViewAnimationOptionBeginFromCurrentState

Запустите анимацию с текущая настройка, связанная с уже находящейся в полете анимацией. Если этого ключа нет, любые анимации в полете можно закончить до начала новой анимации. Если другая анимация не в полете, этот ключ не влияет.

Если вы хотите связать серию анимаций, вот что я буду делать:

Создайте изменяемый массив блоков анимации. (кодовые блоки являются объектами и могут быть добавлены в массив). Напишите метод, который вытащил верхний блок анимации из массива (и удалит его из массива) и отправит его с помощью анимации: анимация: завершение, где метод завершения просто снова вызывает метод. Сделайте код, объявляющий блокировку, прежде чем вытащить элемент из массива и освободите блокировку после удаления элемента.

Затем вы можете написать код, который отвечает на входящее уведомление, утвердив блокировку массива анимации, добавив блок блокировки к блокировке и освободив блокировку.

Ответ 3

Вы можете использовать (не) параллельное NSOperationQueue для выполнения анимации "шаг за шагом"

Класс NSOperationQueue регулирует выполнение набора объектов NSOperation. После добавления в очередь в этой очереди остается операция до тех пор, пока она не будет явно отменена или не завершит выполнение своей задачи. Операции в очереди (но еще не выполняющиеся) сами организованы в соответствии с уровнями приоритета и зависимостями между операционными объектами и выполняются соответствующим образом. Приложение может создавать несколько очередей операций и отправлять операции любому из них.

Зависимости между операциями обеспечивают абсолютный порядок выполнения для операций, даже если эти операции расположены в разных рабочих очередей. Объект операции не считается готовым к выполняются до тех пор, пока все его зависимые операции не закончатся. Для операций, которые готовы выполнить, очередь операций всегда выполняет тот, который имеет наивысший приоритет относительно другого готового операции.

Ответ 4

Я предлагаю отправить сообщение в блок завершения любому объекту, запускающему анимацию. Тогда вы можете заставить этот объект поставить очередь на уведомления и начать следующий каждый раз, когда он получит сообщение.

Ответ 5

ProcedureKit (на основе NSOperation) является примером готового решения, но он достаточно тяжелый, чтобы использовать его только для анимаций.

Мой Operation подкласс, который я использую для размещения анимированных всплывающих окон и других вещей:

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()
    }
}

Пример использования:

// 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()
        }
    }
}

В принципе, просто добавьте эту операцию в последовательную очередь и не забудьте позвонить finish(), когда вы закончите делать асинхронные вещи. Также вы можете отменить все операции в последовательной очереди с одним вызовом, и они будут отклонены изящно.