Зачекайте, поки два блоки асинхронізації будуть виконані перед запуском іншого блоку


192

Використовуючи GCD, ми хочемо почекати, поки два блоки асинхронізації будуть виконані та виконані, перш ніж перейти до наступних кроків виконання. Який найкращий спосіб зробити це?

Ми спробували наступне, але, схоже, це не працює:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
});


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
});

// wait until both the block1 and block2 are done before start block3
// how to do that?

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
});

Дивіться мою відповідь на Swift 5, який пропонує до шести різних способів вирішити вашу проблему.
Іману Петі

Відповіді:


301

Використовуйте групи диспетчеризації: див. Тут приклад "Очікування груп завдань у черзі" у розділі "Черги відправки" керівництва з програмування програми конверторії Apple iOS для бібліотеки розробників iOS

Ваш приклад може виглядати приблизно так:

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
    NSLog(@"Block1");
    [NSThread sleepForTimeInterval:5.0];
    NSLog(@"Block1 End");
});


dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
    NSLog(@"Block2");
    [NSThread sleepForTimeInterval:8.0];
    NSLog(@"Block2 End");
});

dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
    NSLog(@"Block3");
});

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group);

і може отримати такий випуск:

2012-08-11 16:10:18.049 Dispatch[11858:1e03] Block1
2012-08-11 16:10:18.052 Dispatch[11858:1d03] Block2
2012-08-11 16:10:23.051 Dispatch[11858:1e03] Block1 End
2012-08-11 16:10:26.053 Dispatch[11858:1d03] Block2 End
2012-08-11 16:10:26.054 Dispatch[11858:1d03] Block3

3
Класно. чи будуть завдання / блоки асинхронізації, щойно пов'язані з групою, виконуватись послідовно або одночасно? Я маю на увазі, припустимо, що block1 та block2 пов'язані з групою зараз, буде block2 чекати, поки block1 буде виконано, перш ніж він може почати виконання?
tom

9
Це залежить від вас. dispatch_group_asyncтак само, як dispatch_asyncдодано параметр групи. Отже, якщо ви використовуєте різні черги для block1 і block2 або плануєте їх на одній паралельній черзі, вони можуть працювати одночасно; якщо ви плануєте їх на одній послідовній черзі, вони запустяться послідовно. Це не відрізняється від планування блоків без груп.
Йорн Ейріх

1
Чи це стосується також виконання посади веб-служби?
SleepNot

Ви помічаєте, що час не дорівнює часу сну, встановленому у вашому блоці? чому це було б так?
Деймон Юан

2
В ARC просто видаліть dispatch_release (group);
loretoparisi

272

Розширюючи відповідь Йорна Ейріха (підсиліть його відповідь, якщо ви схвалюєте цей), якщо ви не маєте контролю над dispatch_asyncдзвінками для своїх блоків, як це може бути для блоків завершення асинхронізації, ви можете використовувати групи GCD безпосередньо dispatch_group_enterта dispatch_group_leaveбезпосередньо.

У цьому прикладі ми робимо вигляд computeInBackground, що ми не можемо змінити (уявіть, що це зворотний виклик делегата, NSURLConnection завершенняHandler чи будь-що інше), і тому ми не маємо доступу до відправки.

// create a group
dispatch_group_t group = dispatch_group_create();

// pair a dispatch_group_enter for each dispatch_group_leave
dispatch_group_enter(group);     // pair 1 enter
[self computeInBackground:1 completion:^{
    NSLog(@"1 done");
    dispatch_group_leave(group); // pair 1 leave
}];

// again... (and again...)
dispatch_group_enter(group);     // pair 2 enter
[self computeInBackground:2 completion:^{
    NSLog(@"2 done");
    dispatch_group_leave(group); // pair 2 leave
}];

// Next, setup the code to execute after all the paired enter/leave calls.
//
// Option 1: Get a notification on a block that will be scheduled on the specified queue:
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"finally!");
});

// Option 2: Block an wait for the calls to complete in code already running
// (as cbartel points out, be careful with running this on the main/UI queue!):
//
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // blocks current thread
// NSLog(@"finally!");

У цьому прикладі computeInBackground: завершення: реалізується як:

- (void)computeInBackground:(int)no completion:(void (^)(void))block {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"%d starting", no);
        sleep(no*2);
        block();
    });
}

Вихід (із часовими позначками від пробігу):

12:57:02.574  2 starting
12:57:02.574  1 starting
12:57:04.590  1 done
12:57:06.590  2 done
12:57:06.591  finally!

1
@ ɲeuroburɳ Наведений вище код чекає на основній темі. Я вважаю, що це заблокує основний потік і спричинить невідповідальність інтерфейсу користувача, поки вся група не буде завершена. Я рекомендую перенести очікування на фонову нитку. Наприклад, dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_HIGH, 0)
cbartel

2
@cbartel, хороший улов! Я оновив приклад коду, щоб відобразити ваш коментар. Багато разів вам потрібно, щоб зворотний виклик знаходився на головній черзі - в цьому випадку dispatch_queue_notify, мабуть, краще (якщо тільки час блокування гарантовано не буде коротким).
ɲeuroburɳ

Де я можу випустити групу (тобто dispatch_release (група))? Я не впевнений, чи безпечно це випустити в dispatch_group_notify. Але оскільки це код, який виконується після завершення групи, я не знаю, де її випустити.
GingerBreadMane

Якщо ви використовуєте ARC , то вам не потрібно dispatch_release виклику: stackoverflow.com/questions/8618632 / ...
ɲeuroburɳ

3
Хороший пост , який далі пояснює , що: commandshift.co.uk/blog/2014/03/19 / ...
Rizon

98

Завдяки Swift 5.1, Grand Central Dispatch пропонує безліч способів вирішити вашу проблему. Відповідно до ваших потреб, ви можете обрати один із семи візерунків, показаних у наступних фрагментах дитячої площадки.


№1. Використання DispatchGroup, DispatchGroup's notify(qos:flags:queue:execute:)і DispatchQueue' sasync(group:qos:flags:execute:)

Посібник з програмування кон'юнктурних програм Apple Developer говорить проDispatchGroup :

Групи диспетчеризації - це спосіб блокувати потік, поки не завершиться виконання однієї чи декількох задач. Ви можете використовувати цю поведінку в тих місцях, де ви не можете досягти успіху, поки всі визначені завдання не будуть виконані. Наприклад, після відправлення декількох завдань для обчислення деяких даних, ви можете використовувати групу для очікування цих завдань, а потім обробляти результати, коли вони будуть виконані.

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

queue.async(group: group) {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async(group: group) {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

group.notify(queue: queue) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

№2. Використовуючи DispatchGroup, DispatchGroup'S wait(), DispatchGroup' s enter()і DispatchGroup«Sleave()

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

group.enter()
queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    group.leave()
}

group.enter()
queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    group.leave()
}

queue.async {
    group.wait()
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

Зауважте, що ви також можете змішувати DispatchGroup wait()з DispatchQueue async(group:qos:flags:execute:)або змішувати DispatchGroup enter()і DispatchGroup leave()з DispatchGroup notify(qos:flags:queue:execute:).


№3. Використання і 'sDispatch​Work​Item​Flags barrierDispatchQueueasync(group:qos:flags:execute:)

Grand Central Dispatch Tutorial for Swift 4: Стаття 1/2 статті від Raywenderlich.com дає визначення бар'єрів :

Диспетчерські перешкоди - це група функцій, що виконують роль вузького місця в серійному стилі при роботі з паралельними чергами. Під час подання DispatchWorkItemдо черги відправлення ви можете встановити прапори, які вказують, що це повинен бути єдиний елемент, виконаний у зазначеній черзі на цей конкретний час. Це означає, що всі пункти, подані до черги перед бар'єром відправки, повинні заповнитись перед виконанням DispatchWorkItemзаповіту.

Використання:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

queue.async(flags: .barrier) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

№4. Використання DispatchWorkItem, Dispatch​Work​Item​Flags's barrierі DispatchQueue' sasync(execute:)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let dispatchWorkItem = DispatchWorkItem(qos: .default, flags: .barrier) {
    print("#3 finished")
}

queue.async(execute: dispatchWorkItem)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

№5. Використання DispatchSemaphore, DispatchSemaphore's wait()і DispatchSemaphore' ssignal()

Сорош Ханлу написав наступні рядки в публікації блогу " Довідник ГДЧ" :

Використовуючи семафор, ми можемо заблокувати потік протягом довільної кількості часу, поки не буде надісланий сигнал з іншої нитки. Семафори, як і решта GCD, безпечні для ниток, і їх можна спрацьовувати з будь-якого місця. Семафори можна використовувати, коли є асинхронний API, який потрібно зробити синхронним, але ви не можете його змінити.

Довідник API розробника Apple також дає наступне обговорення для DispatchSemaphore init(value:​)ініціалізатора:

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

Використання:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 0)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    semaphore.signal()
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    semaphore.signal()
}

queue.async {
    semaphore.wait()
    semaphore.wait()    
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

№6. Використання OperationQueueі Operation'saddDependency(_:)

Довідник API розробника Apple говорить про Operation​Queue:

Черги операцій використовують libdispatchбібліотеку (також відому як Grand Central Dispatch) для ініціювання виконання своїх операцій.

Використання:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let blockThree = BlockOperation {
    print("#3 finished")
}

blockThree.addDependency(blockOne)
blockThree.addDependency(blockTwo)

operationQueue.addOperations([blockThree, blockTwo, blockOne], waitUntilFinished: false)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

№7. Використання OperationQueueта OperationQueue's addBarrierBlock(_:)(вимагає iOS 13)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

operationQueue.addOperations([blockTwo, blockOne], waitUntilFinished: false)
operationQueue.addBarrierBlock {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

Чи є рішення для асинхронних дзвінків без використання group.enter () та group.leave () для кожного (і без семафорів)? Як якщо мені потрібно чекати запиту на асинхронізацію до сервера, то після цього чекайте другого запиту на асинхронізацію тощо. Я читав цю статтю avanderlee.com/swift/asynchronous-operations але я не подивимося , просте використання його по порівнянні з BlockOperation
Гав

58

Ще одна альтернатива GCD - бар'єр:

dispatch_queue_t queue = dispatch_queue_create("com.company.app.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{ 
    NSLog(@"start one!\n");  
    sleep(4);  
    NSLog(@"end one!\n");
});

dispatch_async(queue, ^{  
    NSLog(@"start two!\n");  
    sleep(2);  
    NSLog(@"end two!\n"); 
});

dispatch_barrier_async(queue, ^{  
    NSLog(@"Hi, I'm the final block!\n");  
});

Просто створіть паралельну чергу, відправте два блоки та відправте остаточний блок із бар'єром, що змусить зачекати, поки інші два завершаться.


Чи є якась проблема, якщо я не використовував сон (4);
Хіманта

Ні, звичайно, з цим проблем немає. Насправді ви практично ніколи цього не бажаєте sleep()! Я лише додав ці sleep()заклики з педагогічних причин, щоб змусити блоки працювати досить довго, щоб ви могли бачити, що вони працюють одночасно. У цьому тривіальному прикладі, за відсутності sleep(), ці два блоки можуть працювати так швидко, що відправлений блок може початись і закінчитися, перш ніж у вас є шанс емпірично спостерігати за одночасним виконанням. Але не sleep()майте власного коду.
Роб

39

Я знаю, що ви запитували про GCD, але якщо ви хочете, NSOperationQueueтакож обробляєте подібні речі дуже витончено, наприклад:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 3");
}];

NSOperation *operation;

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 1");
    sleep(7);
    NSLog(@"Finishing 1");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 2");
    sleep(5);
    NSLog(@"Finishing 2");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

[queue addOperation:completionOperation];

3
Це добре, коли код всередині вашого NSBlockOperation синхронний. Але що робити, якщо це не так, і ви хочете запустити завершення, коли буде виконана операція асинхронізації?
Грег Малетич

3
@GregMaletic У такому випадку я створюю NSOperationпідклас, який є одночасним, і встановлюю, isFinishedколи асинхронний процес завершується. Тоді залежності працює нормально.
Роб


1
@GregMaletic Так, ви можете також використовувати це (доки dispatch_semaphore_waitце не відбудеться на головній черзі і поки ваші сигнали та очікування будуть врівноважені). Поки ваш режим не блокує основну чергу, семафорний підхід буде нормальним, якщо вам не потрібна гнучкість операцій (наприклад, можливість їх скасувати, можливість контролювати ступінь одночасності тощо).
Роб

1
@ Reza.Ab - Якщо вам потрібно виконати завдання, перш ніж розпочати завдання два, додайте залежність між цими завданнями. Або якщо черга завжди виконує лише одне завдання за один раз, зробіть це послідовною чергою, встановивши maxConcurrentOperationCountв 1. Ви також можете встановити пріоритет операцій як qualityOfServiceі queuePriority, але вони мають набагато тонший вплив на пріоритет завдання, ніж залежності та / або ступінь одночасності черги.
Роб

4

Відповіді вище - це круто, але всі вони пропустили одне. група виконує завдання (блоки) у потоці, куди вона введена під час використання dispatch_group_enter/ dispatch_group_leave.

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      dispatch_async(demoQueue, ^{
        dispatch_group_t demoGroup = dispatch_group_create();
        for(int i = 0; i < 10; i++) {
          dispatch_group_enter(demoGroup);
          [self testMethod:i
                     block:^{
                       dispatch_group_leave(demoGroup);
                     }];
        }

        dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
          NSLog(@"All group tasks are done!");
        });
      });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

це працює у створеній паралельній черзі demoQueue. Якщо я не створюю жодної черги, вона працює в основній темі .

- (IBAction)buttonAction:(id)sender {
    dispatch_group_t demoGroup = dispatch_group_create();
    for(int i = 0; i < 10; i++) {
      dispatch_group_enter(demoGroup);
      [self testMethod:i
                 block:^{
                   dispatch_group_leave(demoGroup);
                 }];
    }

    dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
      NSLog(@"All group tasks are done!");
    });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

і є третій спосіб зробити завдання, виконані в іншому потоці:

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      //  dispatch_async(demoQueue, ^{
      __weak ViewController* weakSelf = self;
      dispatch_group_t demoGroup = dispatch_group_create();
      for(int i = 0; i < 10; i++) {
        dispatch_group_enter(demoGroup);
        dispatch_async(demoQueue, ^{
          [weakSelf testMethod:i
                         block:^{
                           dispatch_group_leave(demoGroup);
                         }];
        });
      }

      dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
        NSLog(@"All group tasks are done!");
      });
      //  });
    }

Звичайно, як було сказано, ви можете використовувати dispatch_group_asyncте, що хочете.


3

Перша відповідь по суті правильна, але якщо ви хочете, щоб найпростішим способом досягти бажаного результату, ось окремий приклад коду, який демонструє, як це зробити за допомогою семафору (це також те, як диспетчерські групи працюють за лаштунками, JFYI) :

#include <dispatch/dispatch.h>
#include <stdio.h>

main()
{
        dispatch_queue_t myQ = dispatch_queue_create("my.conQ", DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t mySem = dispatch_semaphore_create(0);

        dispatch_async(myQ, ^{ printf("Hi I'm block one!\n"); sleep(2); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ printf("Hi I'm block two!\n"); sleep(4); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ dispatch_semaphore_wait(mySem, DISPATCH_TIME_FOREVER); printf("Hi, I'm the final block!\n"); });
        dispatch_main();
}

7
Два спостереження: 1. Ви пропускаєте a dispatch_semaphore_wait. У вас два сигнали, тому вам потрібно два очікування. Так, ваш блок "завершення" розпочнеться, як тільки перший блок подасть сигнал семафору, але до того, як закінчиться другий блок; 2. Враховуючи, що це питання iOS, я б відмовив від використання dispatch_main.
Роб

1
Я згоден з Робом. Це неправильне рішення. dispatch_semaphore_waitРозблокують , як тільки будь-який з dispatch_semaphore_signalметодів називаються. Причина, по якій це може здатися, полягає в тому, що printfдля блоків "один" і "два" виникають негайно, а printfдля "нарешті" відбувається після очікування - таким чином, після того, як блок спав протягом 2 секунд. Якщо ви поставите printf після sleepдзвінків, ви отримаєте висновок для "один", потім 2 секунди для "нарешті", потім 2 секунди пізніше для "дві".
ɲeuroburɳ

1

Прийнята відповідь швидко:

let group = DispatchGroup()

group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block1
    print("Block1")
    Thread.sleep(forTimeInterval: 5.0)
    print("Block1 End")
})


group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block2
    print("Block2")
    Thread.sleep(forTimeInterval: 8.0)
    print("Block2 End")
})

dispatch_group_notify(group, DispatchQueue.global(qos: .default), {
    // block3
    print("Block3")
})

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group)

0

Приклад Swift 4.2:

let group = DispatchGroup.group(count: 2)
group.notify(queue: DispatchQueue.main) {
     self.renderingLine = false
     // all groups are done
}
DispatchQueue.main.async {
    self.renderTargetNode(floorPosition: targetPosition, animated: closedContour) {
        group.leave()
        // first done
    }
    self.renderCenterLine(position: targetPosition, animated: closedContour) {
        group.leave()
        // second done
    }
 }

group.leave()спричинив крах
Бен

-3

Не кажучи про те, що інші відповіді не певні для певних обставин, але це один фрагмент, який я завжди користувач Google:

- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {


    if (signInDoneSel) {
        [self performSelector:signInDoneSel];
    }

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.