PresentViewController: анимированный: вид ДА не появится, пока пользователь не коснется снова

93

У меня странное поведение с presentViewController:animated:completion. То, что я делаю, - это, по сути, игра в угадывание.

У меня есть UIViewController(frequencyViewController), содержащий UITableView(frequencyTableView). Когда пользователь нажимает на строку в questionTableView, содержащую правильный ответ, должно быть создано представление (correViewController), и его представление должно скользить вверх из нижней части экрана, как модальное представление. Это сообщает пользователю, что у него есть правильный ответ, и сбрасывает стоящий за ним frequencyViewController, готовый к следующему вопросу. RightViewController отключается при нажатии кнопки, чтобы открыть следующий вопрос.

Все это работает правильно каждый раз, и правильное представление ViewController появляется мгновенно, пока оно presentViewController:animated:completionесть animated:NO.

Если я установил animated:YES, инициализируется правильноViewController и обращается к viewDidLoad. Однако viewWillAppear, viewDidAppearи блок завершения из presentViewController:animated:completionне вызываются. Приложение просто сидит и показывает frequencyViewController, пока я не сделаю второе касание. Теперь вызываются viewWillAppear, viewDidAppear и блок завершения.

Я исследовал еще немного, и это не просто еще одно нажатие, которое заставит его продолжить. Кажется, если я наклоню или встряхну свой iPhone, это также может привести к срабатыванию viewWillLoad и т. Д. Это похоже на ожидание любого другого бита пользовательского ввода, прежде чем он будет прогрессировать. Это происходит на реальном iPhone и в симуляторе, что я доказал, отправив на симулятор команду встряхивания.

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

Благодарность

Вот мой код. Это довольно просто ...

Это код в questionViewController, который действует как делегат для questionTableView.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

    if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
    {
        // If guess was wrong, then mark the selection as incorrect
        NSLog(@"Incorrect Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
        UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
        [cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];            
    }
    else
    {
        // If guess was correct, show correct view
        NSLog(@"Correct Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
        self.correctViewController = [[HFBCorrectViewController alloc] init];
        self.correctViewController.delegate = self;
        [self presentViewController:self.correctViewController animated:YES completion:^(void){
            NSLog(@"Completed Presenting correctViewController");
            [self setUpViewForNextQuestion];
        }];
    }
}

Это весь правильный ViewController

@implementation HFBCorrectViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self)
    {
        // Custom initialization
        NSLog(@"[HFBCorrectViewController initWithNibName:bundle:]");
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSLog(@"[HFBCorrectViewController viewDidLoad]");
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"[HFBCorrectViewController viewDidAppear]");
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)close:(id)sender
{
    NSLog(@"[HFBCorrectViewController close:sender:]");
    [self.delegate didDismissCorrectViewController];
}


@end

Редактировать:

Я нашел этот вопрос ранее: UITableView и PresentViewController требуют 2 щелчка для отображения

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

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

    if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
    {
        // If guess was wrong, then mark the selection as incorrect
        NSLog(@"Incorrect Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
        UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
        [cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];
        // [cell setAccessoryType:(UITableViewCellAccessoryType)]

    }
    else
    {
        // If guess was correct, show correct view
        NSLog(@"Correct Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);

        ////////////////////////////
        // BELOW HERE ARE THE CHANGES
        [self performSelector:@selector(showCorrectViewController:) withObject:nil afterDelay:0];
    }
}

-(void)showCorrectViewController:(id)sender
{
    self.correctViewController = [[HFBCorrectViewController alloc] init];
    self.correctViewController.delegate = self;
    self.correctViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
    [self presentViewController:self.correctViewController animated:YES completion:^(void){
        NSLog(@"Completed Presenting correctViewController");
        [self setUpViewForNextQuestion];
    }];
}
HalfNormalled
источник
1
У меня такая же проблема. Я проверил с помощью NSLogs, что я не вызываю никаких методов, выполнение которых требует много времени. Похоже, что именно анимация presentViewController:должна запускаться с большой задержкой. Кажется, это ошибка в iOS 7, которая также обсуждается на форумах Apple Dev.
Тео
У меня такая же проблема. Похоже на ошибку iOS7 - не происходит на iOS6. Кроме того, в моем случае это происходит только в первый раз после того, как я открываю контроллер представления представления. @ Тео, можешь дать ссылку на форумы Apple Dev?
AX

Ответы:

158

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

На самом деле это очень тонкая ошибка, потому что, если у вас есть малейшая анимация обратной связи, таймеры и т.д. в вашем коде, эта проблема не появится, потому что цикл выполнения будет поддерживаться этими источниками. Я обнаружил проблему, используя a, для UITableViewCellкоторого было selectionStyleустановлено значение UITableViewCellSelectionStyleNone, чтобы анимация выбора не запускала цикл выполнения после запуска обработчика выбора строки.

Чтобы исправить это (пока Apple что-то не сделает), вы можете запустить основной цикл выполнения несколькими способами:

Наименее назойливое решение - позвонить CFRunLoopWakeUp:

[self presentViewController:vc animated:YES completion:nil];
CFRunLoopWakeUp(CFRunLoopGetCurrent());

Или вы можете поставить пустой блок в основную очередь:

[self presentViewController:vc animated:YES completion:nil];
dispatch_async(dispatch_get_main_queue(), ^{});

Забавно, но если вы встряхнете устройство, он также запустит основной цикл (он должен обрабатывать события движения). То же самое с касаниями, но это включено в исходный вопрос :) Кроме того, если система обновляет строку состояния (например, обновляются часы, изменяется сила сигнала WiFi и т. Д.), Это также разбудит основной цикл и представит представление контроллер.

Для всех, кого это интересует, я написал минимальный демонстрационный проект проблемы, чтобы проверить гипотезу runloop: https://github.com/tzahola/present-bug

Я также сообщил об ошибке в Apple.

Тамаш Захола
источник
Спасибо, Тамаш. Это отличная информация. Это объясняет, почему иногда мне нужно было нажать, чтобы запустить цикл, а иногда просто ждать, и это сработает. Я отметил это как решение, так как не похоже, что мы можем что-то еще сделать, пока ошибка не будет исправлена.
HalfNormalled
Яблоко роет огромную яму, я падаю в нее и чувствую сильную боль.
OpenThread
7
OMG, это так весело! Кстати, эта ошибка все еще существует.
igrrik 02
1
У меня была точно такая же проблема, которая действительно раздражала, к счастью, это исправило ее.
Марк
1
Подтверждено, что эта проблема по-прежнему возникает в iOS 13
Эрик Горачек,
69

Проверьте это: https://devforums.apple.com/thread/201431 Если вы не хотите все это читать - решение для некоторых людей (включая меня) заключалось в том, чтобы сделать presentViewControllerвызов явно в основном потоке:

Swift 4.2:

DispatchQueue.main.async { 
    self.present(myVC, animated: true, completion: nil)
}

Цель-C:

dispatch_async(dispatch_get_main_queue(), ^{
    [self presentViewController:myVC animated:YES completion:nil];
});

Вероятно, iOS7 портит потоки didSelectRowAtIndexPath.

ТОПОР
источник
вау - это сработало для меня. Я тоже видел задержки. Я не понимаю, зачем это нужно. Спасибо, что разместили это.
drudru
4
Отправил в Apple радар об этой проблеме: rdar: // 19563577 openradar.appspot.com/19563577
mluisbrown
1
Я использую iOS 8, и это меня не исправляет - он всегда сообщает о том, что находится в основном потоке, но я все еще вижу описанную проблему.
Роберт
7

Я обошел его в Swift 3.0, используя следующий код:

DispatchQueue.main.async { 
    self.present(UIViewController(), animated: true, completion: nil)
}
отметка
источник
1
это решает проблему. У меня была такая же проблема, потому что я звонил presentв обратный вызов закрытия.
Джереми Пьедноэль
2

Вызов [viewController view]представленного контроллера представления помог мне.

ДжаганЫ
источник
У меня это сработало на iOS 8. Я думаю, что это лучше, чем dispatch_async.
Роберт
0

Было бы любопытно посмотреть, что [self setUpViewForNextQuestion];делает.

Вы можете попробовать позвонить [self.correctViewController.view setNeedsDisplay];в конце блока завершения в presentViewController.

Ник
источник
Прямо сейчас setUpViewForNextQuestion просто вызывает reloadData в табличном представлении (чтобы изменить все цвета фона обратно на белый, если какой-либо из них был установлен на красный из-за неправильного предположения). Но будут ли какие-то изменения иметь значение? Проблема в том, что правильныйViewController не появляется до тех пор, пока использование не коснется снова, поэтому этот блок завершения не вызывается до тех пор, пока после этого второго касания.
HalfNormalled
Я попробовал ваше предложение, но это не имело никакого значения. Как я уже сказал, представление не отображается, поэтому блок завершения не вызывается до второго касания или жеста встряхивания.
HalfNormalled
хм. для начала, я бы использовал точки останова, чтобы подтвердить, что ваш код презентации не вызывается до 2-го нажатия. (в отличие от вызова при первом касании, но не отображения на экране до некоторого времени). если последнее верно, попробуйте заставить вашу презентацию выполняться в основном потоке. Когда вы используете «performSelectorWithDelay: 0», это заставляет приложение ждать до следующей итерации цикла выполнения перед выполнением. Это могло быть связано с потоками.
Ник
Спасибо, Ник. Как мне проверить использование точек останова? В настоящий момент я использую NSLog, чтобы проверять, когда вызывается каждый метод. Вот что у меня сейчас: Tap 1 Correct Guess: 1 kHz 18:04:57.173 Frequency[641:60b] [HFBCorrectViewController initWithNibName:bundle:] 18:04:57.177 Frequency[641:60b] [HFBCorrectViewController viewDidLoad] Теперь ничего не происходит, пока: Tap 218:05:00.515 Frequency[641:60b] [HFBCorrectViewController viewDidAppear] 18:05:00.516 Frequency[641:60b] Completed Presenting correctViewController 18:05:00.517 Frequency[641:60b] [HFBFrequencyViewController setUpViewForNextQuestion]
HalfNormalled
Извините, надо было быть более откровенным. Я понимаю, как использовать точки останова. У меня та же история, что и с NSLog. Я помещаю точки останова на viewDidLoad и viewDidAppear в правильныйViewController. Он ломается на viewDidLoad, затем, когда я продолжаю, он сидит и ждет еще одного нажатия, затем ломается на viewDidAppear. Иногда кажется, что касание не требуется, и это зависит от времени, к тому времени, когда я прошел через вещи, глядя на то, что еще вызывается, он уже успел вызвать viewDidLoad.
HalfNormalled
0

Я написал расширение (категорию) с переключением методов для UIViewController, которое решает проблему. Спасибо AX и NSHipster за подсказки по реализации ( swift / objective-c ).

Swift

extension UIViewController {

 override public class func initialize() {
    struct DispatchToken {
      static var token: dispatch_once_t = 0
    }
    if self != UIViewController.self {
      return
    }
    dispatch_once(&DispatchToken.token) {
      let originalSelector = Selector("presentViewController:animated:completion:")
      let swizzledSelector = Selector("wrappedPresentViewController:animated:completion:")

      let originalMethod = class_getInstanceMethod(self, originalSelector)
      let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

      let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

      if didAddMethod {
        class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
      }
      else {
        method_exchangeImplementations(originalMethod, swizzledMethod)
      }
    }
  }

  func wrappedPresentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
    dispatch_async(dispatch_get_main_queue()) {
      self.wrappedPresentViewController(viewControllerToPresent, animated: flag, completion: completion)
    }
  }
}  

Цель-C

#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(presentViewController:animated:completion:);
        SEL swizzledSelector = @selector(wrappedPresentViewController:animated:completion:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)wrappedPresentViewController:(UIViewController *)viewControllerToPresent 
                             animated:(BOOL)flag 
                           completion:(void (^ __nullable)(void))completion {
    dispatch_async(dispatch_get_main_queue(),^{
        [self wrappedPresentViewController:viewControllerToPresent
                                  animated:flag 
                                completion:completion];
    });

}

@end
Gladkov_Art
источник
0

Проверьте, есть ли у вашей ячейки в раскадровке Selection = none

Если да, измените его на синий или серый, и он должен работать.

Mkey
источник
0

XCode Vesion: 9.4.1, Swift 4.1

В моем случае это происходит, когда я нажимаю на ячейку и перехожу к другому виду. Я отлаживаю глубже, и кажется, что это происходит внутри, viewDidAppearпотому что содержит следующий код

if let indexPath = tableView.indexPathForSelectedRow {
   tableView.deselectRow(at: indexPath, animated: true)
}

затем я добавил вышеупомянутый сегмент кода внутри prepare(for segue: UIStoryboardSegue, sender: Any?)и отлично работал.

По моему опыту, мое решение: если мы надеемся внести какие-либо новые изменения (например, перезагрузить таблицу, отменить выбор выбранной ячейки и т. Д.) Для табличного представления, когда снова вернемся из второго представления, тогда используйте делегат вместо viewDidAppearи с использованием tableView.deselectRowкода выше сегмент перед перемещением контроллера второго представления

Сачинта Удара
источник