presentViewController:animated:YES视图控制器不会出现直到用户再次点击。

94

我正在使用 presentViewController:animated:completion 时遇到一些奇怪的行为。我正在制作一个猜谜游戏。

我有一个包含 UITableView (frequencyTableView) 的 UIViewController (frequencyViewController)。当用户点击包含正确答案的 questionTableView 行时,应该实例化一个视图(correctViewController),并将其视图从屏幕底部向上滑动,作为模态视图。这告诉用户他们有一个正确的答案,并准备好下一个问题的 frequencyViewController。按下按钮来解除 correctViewController,以显示下一个问题。

每次都能正常工作,只要 presentViewController:animated:completion 具有 animated:NO,correctViewController 的视图就会立即出现。

如果我设置 animated:YES,correctViewController 就会被初始化并调用 viewDidLoad。但是,viewWillAppear viewDidAppearpresentViewController: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];
        }];
    }
}

这是 correctViewController 的全部内容。

@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需要点击两次才能显示

如果我将我的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];
    }];
}

1
我有同样的问题。我通过NSLogs检查了一下,没有调用任何执行时间长的方法。看起来只是presentViewController:应该触发的动画开始时有很大的延迟。这似乎是iOS 7中的一个bug,也在Apple开发者论坛中讨论过。 - Theo
我有同样的问题。似乎是iOS7的错误 - 在iOS6上不会发生。而且,在我的情况下,它只会在我打开呈现视图控制器后第一次发生。 @Theo,你能提供苹果开发者论坛的链接吗? - AXE
8个回答

162

今天我遇到了同样的问题,我深入研究了一下发现它与主运行循环处于休眠状态有关。

实际上,这是一个非常微妙的 bug,因为如果你的代码中有任何反馈动画、定时器等等,这个问题就不会浮出水面,因为这些源会让运行循环保持活跃。我通过使用一个 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信号强度改变等),也会唤醒主循环并呈现视图控制器。

对于任何感兴趣的人,我编写了一个最小化的演示项目来验证运行循环假设:https://github.com/tzahola/present-bug

我也向苹果报告了这个错误。


谢谢Tamás。这是很好的信息。它解释了为什么有时我需要点击来启动运行循环,或者有时只需等待就可以工作。我已将其标记为解决方案,因为在修复错误之前似乎没有其他可做的事情了。 - HalfNormalled
苹果挖了一个大坑,我掉进去了,感觉很受伤。 - OpenThread
很好地探索了一下...它像魔法一样运行,救了我的一天 :) 但 bug 仍然存在。 - Bhavin_m
1
我曾经遇到过完全相同的问题,真的很烦人,幸运的是这个方法解决了它。 - Mark
1
确认该问题在iOS 13中仍然存在。 - Eric Horacek
显示剩余8条评论

69

看看这个:https://devforums.apple.com/thread/201431 如果你不想全部阅读,一些人(包括我)的解决方案是在主线程上显式调用presentViewController

Swift 4.2:

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

Objective-C:

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

可能是iOS7在didSelectRowAtIndexPath中搞乱了线程。


哇 - 这对我有用。我也看到了延迟。我不明白为什么这是必要的。感谢您发布这个。 - drudru
4
我已经向 Apple 提交了有关这个问题的雷达反馈:rdar://19563577 http://openradar.appspot.com/19563577 - mluisbrown
1
我正在使用iOS 8,这个解决方法对我无效 - 它始终报告在主线程上运行,但我仍然看到所描述的问题。 - Robert

8

我在Swift 3.0中通过以下代码绕过了它:

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

1
它解决了这个问题。我遇到了同样的问题,因为我在闭包回调中调用了 present - Jeremy Piednoel

2

对于我而言,调用被呈现视图控制器的[viewController view]即可解决问题。


这在iOS 8上对我有效。我认为它比dispatch_async更好。 - Robert

0

XCode版本: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代码段。


0

我编写了一个扩展(类别),使用方法交换技术解决了UIViewController的问题。感谢AXE和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)
    }
  }
}  

Objective-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

0

检查一下你在Storyboard中的单元格是否选择了“无”选项

如果是这样,请将其更改为蓝色或灰色,然后它应该可以正常工作。


0

我很想知道 [self setUpViewForNextQuestion]; 这一行的作用。

你可以在 presentViewController 完成块的末尾调用 [self.correctViewController.view setNeedsDisplay];


现在,setUpViewForNextQuestion只是在tableview上调用reloadData(以将所有背景颜色都改回白色,如果有任何一个因不正确的猜测而被设置为红色)。但是那里的任何更改会有任何区别吗?问题是correctViewController直到用户再次点击才出现,因此该完成块直到第二次点击后才被调用。 - HalfNormalled
我确实尝试了您的建议,但没有任何改变。就像我所说的那样,视图没有出现,因此完成块直到第二次点击或摇晃手势后才被调用。 - HalfNormalled
嗯。首先,我会使用断点来确认您的演示代码是否在第二次轻按之前没有被调用。(相对于在第一次轻按时就被调用,但直到稍后的某个时间才绘制在屏幕上)。如果是后者,请尝试强制在主线程上进行演示。当您使用“performSelectorWithDelay:0”时,这会强制应用程序等待直到运行循环的下一次迭代才执行。它可能与线程有关。 - Nick
谢谢,Nick。如何使用断点检查?目前我正在使用NSLog来检查每个方法被调用的时间。这是我现在拥有的: 轻按1 正确答案: 1 kHz 18:04:57.173 Frequency[641:60b] [HFBCorrectViewController initWithNibName:bundle:] 18:04:57.177 Frequency[641:60b] [HFBCorrectViewController viewDidLoad]现在什么都不会发生,直到轻按2 18:05:00.515 Frequency[641:60b] [HFBCorrectViewController viewDidAppear] 18:05:00.516 Frequency[641:60b] 完成呈现correctViewController 18:05:00.517 Frequency[641:60b] [HFBFrequencyViewController setUpViewForNextQuestion] - HalfNormalled
抱歉,我应该更明确一些。我知道如何使用断点。我和NSLog的情况一样。我在正确的视图控制器中的viewDidLoad和viewDidAppear上设置了断点。它会在viewDidLoad上中断,然后当我继续时,它会等待另一个点击,然后在viewDidAppear上中断。有时它似乎不需要点击,而是基于时间的,在我查看调用了什么其他内容的同时,它已经开始调用viewDidLoad了。 - HalfNormalled

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接