iOS 应用在 PushViewController 上冻结
iOS App Freezes on PushViewController
我的导航控制器间歇性地在推送时冻结。似乎将新的视图控制器添加到堆栈中,但动画从未发生过。我还有另外两个容器在屏幕上保存视图控制器,在导航控制器冻结后我可以与它们进行交互。真正有趣的是,如果我尝试将另一个视图控制器推送到导航控制器的堆栈上,我注意到堆栈顶部有一个额外的视图控制器(我最初推送的视图控制器冻结了导航控制器)。因此,如果我在主屏幕上(我们称之为 VC-Home)并且我尝试推送一个新视图(VC-1)并且它冻结了,那么我尝试推送一个新视图(VC-2),这是我在推送之前在当前堆栈中看到的:
{ [VC-Home, VC-1] }
和调用pushViewController后,还是一样; VC-2 没有加入堆栈。
据我所知,导航控制器通过在动画开始之前使前一个视图控制器处于非活动状态来启动动画,但随后动画永远不会发生,使导航控制器处于冻结状态。
我正在通过调用从故事板创建新的视图控制器
UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController")
所以我不认为那里有任何问题。我也没有覆盖导航栏上的 pushViewController 。我的应用程序的一些独特之处在于它的高分辨率图像很重(使用 SDWebImage 来管理它)并且我总是在屏幕上同时显示三个容器(一个导航控制器,一个用于搜索的视图控制器和一个交互式 gutter/side/slideout 菜单)。
CPU 使用率低,内存使用率正常(发生冻结时设备上稳定在 60-70MB 左右)。
是否有关于可能导致此问题的任何想法或任何可以帮助我发现真正问题的调试技巧?
更新
UINavigationController 没有唯一代码,因为我只是使用 pushViewController() 进行推送。这是调用它的代码:
func didSelectItem(profile: SimpleProfile) {
let vc = UIStoryboard.profileViewController()
vc.profile = profile
navigationController?.pushViewController(vc, animated: true)
}
我推送的ViewController在viewDidLoad中有如下代码:
override func viewDidLoad() {
super.viewDidLoad()
button.roundView()
if let type = profile?.profileType {
//load multiple view controllers into a view pager based on type
let viewControllers = ProfileTypeTabAdapter.produceViewControllersBasedOnType(type)
loadViewPagerViews(viewControllers)
let topInset = headerView.bounds.height + tabScrollView.contentSize.height
if let viewPager = viewPager {
for view in viewPager.views {
if let tempView = view as? PagingChildViewController {
tempView.profile = fullProfile
tempView.parentVCDelegate = self
tempView.topInset = topInset
}
}
}
}
}
func loadViewPagerViews(viewControllers: [UIViewController]) {
viewPager?.views = viewControllers
viewPager?.delegate = self
//loading views into paging scroll view (using PureLayout to create constraints)
let _ = subviews.map { [=12=].removeFromSuperview() }
var i = 0
for item in views {
addSubview(item.view)
item.view.autoSetDimensionsToSize(CGSize(width: tabWidth, height: tabHeight))
if i == 0 {
item.view.autoPinEdgeToSuperviewEdge(.Leading)
} else if let previousView = views[i-1].view {
item.view.autoPinEdge(.Leading, toEdge: .Trailing, ofView: previousView)
}
if i == views.count {
item.view.autoPinEdgeToSuperviewEdge(.Trailing)
}
i += 1
}
contentSize = CGSize(width: Double(i)*Double(tabWidth), height: Double(tabHeight))
}
更新 2
我终于让它再次冻结了。该应用程序在后台,我将其取回并尝试在它冻结时将视图控制器推到堆栈上。我注意到正在播放动画。我在页面顶部有一个滚动视图,每 10 秒翻阅一次其内容(想想应用程序商店的顶部横幅)。在此冻结中,我注意到横幅正在播放动画。
这是我的 UIScrollView 中每 10 秒调用一次的滚动函数:
func moveToNextItem() {
let pageWidth: CGFloat = CGRectGetWidth(frame)
let maxWidth: CGFloat = pageWidth * CGFloat(max(images.count, profileImages.count))
let contentOffset: CGFloat = self.contentOffset.x
let slideToX = contentOffset + pageWidth
//if this is the end of the line, stop the timer
if contentOffset + pageWidth == maxWidth {
timer?.invalidate()
timer = nil
return
}
scrollRectToVisible(CGRectMake(slideToX, 0, pageWidth, CGRectGetHeight(frame)), animated: true)
}
我不记得曾经因为 animation/scroll 发生过推进停止,但我可能是错的。
我也重新检查了堆栈,与上述相同的情况仍然是 [VC-Home,VC-1] 是堆栈,VC -2 未被推上。我还检查了 VC-1 的变量,所有内容都已加载(数据调用和图像加载)。
更新 3
这越来越陌生了。我已经覆盖了 pushViewController,所以我可以在其中放置一个断点并根据 Alessandro Ornano 的响应进行一些调试。如果我推送视图控制器不成功,然后将我的应用程序发送到后台,在 pushViewController 调用中放置一个断点,然后将应用程序带回来,断点会立即命中多次。如果我然后继续通过所有点击,下一个视图控制器突然变得可见并且我尝试推送的最后一个视图控制器现在作为最后一个视图控制器在堆栈上。这意味着我看到的那个仍然是禁用的,这基本上使我处于与以前相同的位置。
我在考虑间歇性冻结、主线程和SDWebImage.
假设您使用从 downloadImageWithURL:options:progress:completed: 的已完成块下载的图像.. 如果是这样,请确保在使用之前分派到主队列图片。
如果您直接使用 SDWebImageDownloader,完成块(如您所述)将在后台队列上调用,您可以在主队列上使用 dispatch_async 从完成中修复它。
否则你可以使用:
SDWebImageManager downloadImageWithURL:options:progress:completed:(调用主队列上的完成块的方法)。
如果问题仍然存在(只是因为您谈到“..我的应用程序的一些独特之处在于它非常重图像..”)还请查看 Common problems 特别是 Handle图片刷新 知道问题
在您的检查中也添加这个漂亮的代码片段:
import UIKit.UINavigationController
public typealias VoidBlock = (Void -> Void)
public extension UINavigationController
{
public func pushViewController(viewController: UIViewController, animated: Bool, completion: VoidBlock) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
也许可以帮助理解 pushViewController 是否完成,是否完成所有预期的 viewControllers ..
我尝试做的另一个测试是用 iOS 8.x 和 iPhone 6+ 启动应用程序,因为周围的 pureLayout 项目中有一些 issues iOS 9. 你能就这个测试发送反馈吗?
我对pushview动作之前的真实scrollview维度也有些怀疑,你能通过examing the view hierarchy分析当前视图吗?
几周前我们遇到了同样的问题。对于我们的问题,我们将其缩小到 left-edge pop gesture recogniser
。您可以尝试检查是否可以使用以下步骤重现此问题
- 尝试在其下方没有视图控制器时使用左边缘弹出手势(即在根视图控制器上,您的
VC-Home
控制器)
- 尝试点击此后的任何 UI 个元素。
如果您能够重现冻结,请尝试在视图控制器堆栈只有一个视图控制器时禁用 interactivePopGestureRecognizer
。
有关详细信息,请参阅 this 问题。下面是来自 link 的代码,以便于参考。
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animate
{
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
{
if (self.viewControllers.count > 1)
{
self.interactivePopGestureRecognizer.enabled = YES;
}
else
{
self.interactivePopGestureRecognizer.enabled = NO;
}
}
}
@Penkey Suresh 的 太棒了!拯救了我的一天!这是一个 SWIFT 3 版本,其中添加了一些对我来说有所不同的内容:
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if (navigationController.viewControllers.count > 1)
{
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController.interactivePopGestureRecognizer?.isEnabled = true;
}
else
{
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
navigationController.interactivePopGestureRecognizer?.isEnabled = false;
}
}
只是不要忘记添加 UINavigationControllerDelegate
并设置 navigationController?.delegate = self
另一个重要的部分是将 interactivePopGestureRecognizer
相应地分配给 self 或 nil。
请检查您的 BaseNavigationViewController 中是否有任何不必要的代码,如下所示:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
这里我有一个重现此问题的示例代码(在推送时冻结应用程序 VC),您必须通过以下步骤重现它:
- 当下方没有视图控制器时尝试使用(左|右)边缘弹出手势(即在根视图控制器上,您的 VC-Home 控制器)
- 尝试在此之后单击任何 UI 个元素(这会将您带到下一个 ViewController)。
我的问题是通过重新启动 Xcode 和模拟器解决的,然后从模拟器列表中选择不同的设备。
@Penkey Suresh 的回答! @Tim Friedland 帮了我很多,我必须为我的用例做额外的一件事,也可能对其他人有帮助。
用例:
我有标签栏控制器,我想在屏幕上显示 swipe-back-gesture 假设我在 tabA 上,然后从那里打开 VC1、VC2。我的滑动手势在 VC1 和 VC2 上工作正常,但由于某种原因,当返回到 tabA 时它没有被禁用,这就是为什么当它试图从那里向左滑动并试图点击某处时它冻结了(如@Pankey Suresh 所述)
解决方案 SWIFT 5
我实现了这个自定义 class:
class BaseSwipeBack: UIViewController, UIGestureRecognizerDelegate {
func swipeToPop(enable: Bool) {
if enable && (navigationController?.viewControllers.count ?? 0 > 1){
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController?.interactivePopGestureRecognizer?.isEnabled = true;
}
else {
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
navigationController?.interactivePopGestureRecognizer?.isEnabled = false;
}
}
}
用法
VC1 class - 使其成为我们刚刚创建的 BaseSwipeBack 的子 class,以便您可以访问函数
class VC1: BaseSwipeBack {
override func viewDidLoad() {
}
override func viewDidAppear(_ animated: Bool) {
self.swipeToPop(enable: true)
}
deinit {
self.swipeToPop(enable: false)
}
}
tabAController class - 虽然我在取消 VC1 时禁用了手势,但它工作不正常所以我不得不再次禁用它 tabAController
class tabAController: BaseSwipeBack {
override func viewDidLoad() {
}
override func viewDidAppear(_ animated: Bool) {
self.swipeToPop(enable: false)
}
}
我的导航控制器间歇性地在推送时冻结。似乎将新的视图控制器添加到堆栈中,但动画从未发生过。我还有另外两个容器在屏幕上保存视图控制器,在导航控制器冻结后我可以与它们进行交互。真正有趣的是,如果我尝试将另一个视图控制器推送到导航控制器的堆栈上,我注意到堆栈顶部有一个额外的视图控制器(我最初推送的视图控制器冻结了导航控制器)。因此,如果我在主屏幕上(我们称之为 VC-Home)并且我尝试推送一个新视图(VC-1)并且它冻结了,那么我尝试推送一个新视图(VC-2),这是我在推送之前在当前堆栈中看到的:
{ [VC-Home, VC-1] }
和调用pushViewController后,还是一样; VC-2 没有加入堆栈。
据我所知,导航控制器通过在动画开始之前使前一个视图控制器处于非活动状态来启动动画,但随后动画永远不会发生,使导航控制器处于冻结状态。
我正在通过调用从故事板创建新的视图控制器
UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController")
所以我不认为那里有任何问题。我也没有覆盖导航栏上的 pushViewController 。我的应用程序的一些独特之处在于它的高分辨率图像很重(使用 SDWebImage 来管理它)并且我总是在屏幕上同时显示三个容器(一个导航控制器,一个用于搜索的视图控制器和一个交互式 gutter/side/slideout 菜单)。
CPU 使用率低,内存使用率正常(发生冻结时设备上稳定在 60-70MB 左右)。
是否有关于可能导致此问题的任何想法或任何可以帮助我发现真正问题的调试技巧?
更新
UINavigationController 没有唯一代码,因为我只是使用 pushViewController() 进行推送。这是调用它的代码:
func didSelectItem(profile: SimpleProfile) {
let vc = UIStoryboard.profileViewController()
vc.profile = profile
navigationController?.pushViewController(vc, animated: true)
}
我推送的ViewController在viewDidLoad中有如下代码:
override func viewDidLoad() {
super.viewDidLoad()
button.roundView()
if let type = profile?.profileType {
//load multiple view controllers into a view pager based on type
let viewControllers = ProfileTypeTabAdapter.produceViewControllersBasedOnType(type)
loadViewPagerViews(viewControllers)
let topInset = headerView.bounds.height + tabScrollView.contentSize.height
if let viewPager = viewPager {
for view in viewPager.views {
if let tempView = view as? PagingChildViewController {
tempView.profile = fullProfile
tempView.parentVCDelegate = self
tempView.topInset = topInset
}
}
}
}
}
func loadViewPagerViews(viewControllers: [UIViewController]) {
viewPager?.views = viewControllers
viewPager?.delegate = self
//loading views into paging scroll view (using PureLayout to create constraints)
let _ = subviews.map { [=12=].removeFromSuperview() }
var i = 0
for item in views {
addSubview(item.view)
item.view.autoSetDimensionsToSize(CGSize(width: tabWidth, height: tabHeight))
if i == 0 {
item.view.autoPinEdgeToSuperviewEdge(.Leading)
} else if let previousView = views[i-1].view {
item.view.autoPinEdge(.Leading, toEdge: .Trailing, ofView: previousView)
}
if i == views.count {
item.view.autoPinEdgeToSuperviewEdge(.Trailing)
}
i += 1
}
contentSize = CGSize(width: Double(i)*Double(tabWidth), height: Double(tabHeight))
}
更新 2
我终于让它再次冻结了。该应用程序在后台,我将其取回并尝试在它冻结时将视图控制器推到堆栈上。我注意到正在播放动画。我在页面顶部有一个滚动视图,每 10 秒翻阅一次其内容(想想应用程序商店的顶部横幅)。在此冻结中,我注意到横幅正在播放动画。
这是我的 UIScrollView 中每 10 秒调用一次的滚动函数:
func moveToNextItem() {
let pageWidth: CGFloat = CGRectGetWidth(frame)
let maxWidth: CGFloat = pageWidth * CGFloat(max(images.count, profileImages.count))
let contentOffset: CGFloat = self.contentOffset.x
let slideToX = contentOffset + pageWidth
//if this is the end of the line, stop the timer
if contentOffset + pageWidth == maxWidth {
timer?.invalidate()
timer = nil
return
}
scrollRectToVisible(CGRectMake(slideToX, 0, pageWidth, CGRectGetHeight(frame)), animated: true)
}
我不记得曾经因为 animation/scroll 发生过推进停止,但我可能是错的。
我也重新检查了堆栈,与上述相同的情况仍然是 [VC-Home,VC-1] 是堆栈,VC -2 未被推上。我还检查了 VC-1 的变量,所有内容都已加载(数据调用和图像加载)。
更新 3
这越来越陌生了。我已经覆盖了 pushViewController,所以我可以在其中放置一个断点并根据 Alessandro Ornano 的响应进行一些调试。如果我推送视图控制器不成功,然后将我的应用程序发送到后台,在 pushViewController 调用中放置一个断点,然后将应用程序带回来,断点会立即命中多次。如果我然后继续通过所有点击,下一个视图控制器突然变得可见并且我尝试推送的最后一个视图控制器现在作为最后一个视图控制器在堆栈上。这意味着我看到的那个仍然是禁用的,这基本上使我处于与以前相同的位置。
我在考虑间歇性冻结、主线程和SDWebImage.
假设您使用从 downloadImageWithURL:options:progress:completed: 的已完成块下载的图像.. 如果是这样,请确保在使用之前分派到主队列图片。
如果您直接使用 SDWebImageDownloader,完成块(如您所述)将在后台队列上调用,您可以在主队列上使用 dispatch_async 从完成中修复它。
否则你可以使用: SDWebImageManager downloadImageWithURL:options:progress:completed:(调用主队列上的完成块的方法)。
如果问题仍然存在(只是因为您谈到“..我的应用程序的一些独特之处在于它非常重图像..”)还请查看 Common problems 特别是 Handle图片刷新 知道问题
在您的检查中也添加这个漂亮的代码片段:
import UIKit.UINavigationController
public typealias VoidBlock = (Void -> Void)
public extension UINavigationController
{
public func pushViewController(viewController: UIViewController, animated: Bool, completion: VoidBlock) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
也许可以帮助理解 pushViewController 是否完成,是否完成所有预期的 viewControllers ..
我尝试做的另一个测试是用 iOS 8.x 和 iPhone 6+ 启动应用程序,因为周围的 pureLayout 项目中有一些 issues iOS 9. 你能就这个测试发送反馈吗?
我对pushview动作之前的真实scrollview维度也有些怀疑,你能通过examing the view hierarchy分析当前视图吗?
几周前我们遇到了同样的问题。对于我们的问题,我们将其缩小到 left-edge pop gesture recogniser
。您可以尝试检查是否可以使用以下步骤重现此问题
- 尝试在其下方没有视图控制器时使用左边缘弹出手势(即在根视图控制器上,您的
VC-Home
控制器) - 尝试点击此后的任何 UI 个元素。
如果您能够重现冻结,请尝试在视图控制器堆栈只有一个视图控制器时禁用 interactivePopGestureRecognizer
。
有关详细信息,请参阅 this 问题。下面是来自 link 的代码,以便于参考。
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animate
{
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
{
if (self.viewControllers.count > 1)
{
self.interactivePopGestureRecognizer.enabled = YES;
}
else
{
self.interactivePopGestureRecognizer.enabled = NO;
}
}
}
@Penkey Suresh 的
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if (navigationController.viewControllers.count > 1)
{
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController.interactivePopGestureRecognizer?.isEnabled = true;
}
else
{
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
navigationController.interactivePopGestureRecognizer?.isEnabled = false;
}
}
只是不要忘记添加 UINavigationControllerDelegate
并设置 navigationController?.delegate = self
另一个重要的部分是将 interactivePopGestureRecognizer
相应地分配给 self 或 nil。
请检查您的 BaseNavigationViewController 中是否有任何不必要的代码,如下所示:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
这里我有一个重现此问题的示例代码(在推送时冻结应用程序 VC),您必须通过以下步骤重现它:
- 当下方没有视图控制器时尝试使用(左|右)边缘弹出手势(即在根视图控制器上,您的 VC-Home 控制器)
- 尝试在此之后单击任何 UI 个元素(这会将您带到下一个 ViewController)。
我的问题是通过重新启动 Xcode 和模拟器解决的,然后从模拟器列表中选择不同的设备。
@Penkey Suresh 的回答! @Tim Friedland 帮了我很多,我必须为我的用例做额外的一件事,也可能对其他人有帮助。
用例: 我有标签栏控制器,我想在屏幕上显示 swipe-back-gesture 假设我在 tabA 上,然后从那里打开 VC1、VC2。我的滑动手势在 VC1 和 VC2 上工作正常,但由于某种原因,当返回到 tabA 时它没有被禁用,这就是为什么当它试图从那里向左滑动并试图点击某处时它冻结了(如@Pankey Suresh 所述)
解决方案 SWIFT 5 我实现了这个自定义 class:
class BaseSwipeBack: UIViewController, UIGestureRecognizerDelegate {
func swipeToPop(enable: Bool) {
if enable && (navigationController?.viewControllers.count ?? 0 > 1){
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController?.interactivePopGestureRecognizer?.isEnabled = true;
}
else {
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
navigationController?.interactivePopGestureRecognizer?.isEnabled = false;
}
}
}
用法
VC1 class - 使其成为我们刚刚创建的 BaseSwipeBack 的子 class,以便您可以访问函数
class VC1: BaseSwipeBack { override func viewDidLoad() { } override func viewDidAppear(_ animated: Bool) { self.swipeToPop(enable: true) } deinit { self.swipeToPop(enable: false) } }
tabAController class - 虽然我在取消 VC1 时禁用了手势,但它工作不正常所以我不得不再次禁用它 tabAController
class tabAController: BaseSwipeBack { override func viewDidLoad() { } override func viewDidAppear(_ animated: Bool) { self.swipeToPop(enable: false) } }