iOS 7:自定义容器视图控制器和内容插入

24
我有一个包含在导航控制器中的表视图控制器。当通过presentViewController:animated:completion:呈现时,导航控制器似乎会自动向表视图控制器应用正确的内容插入。(有人能否准确解释一下这是如何工作的?)
然而,一旦我将它们组合在自定义容器视图控制器中并进行演示,表视图内容的顶部部分就会隐藏在导航栏后面。在这种配置中,我是否可以做些什么来保留自动内容插入行为?我必须在容器视图控制器中“透过”某些东西才能正常工作吗?
我想避免手动调整内容插入或通过自动布局来实现,因为我希望继续支持iOS 5。
6个回答

12

我遇到了一个类似的问题。在 iOS 7 上,UIViewController 有一个名为 automaticallyAdjustsScrollViewInsets 的新属性。默认情况下,它被设置为 YES。但是当你的视图层次结构比滚动/表格视图在导航或选项卡栏控制器中更复杂时,这个属性似乎对我不起作用。

我所做的是:创建一个基本的视图控制器类,所有其他视图控制器都从该类继承。那个视图控制器会明确地设置插图:

Header:

#import <UIKit/UIKit.h>

@interface SPWKBaseCollectionViewController : UICollectionViewController

@end

实现:

#import "SPWKBaseCollectionViewController.h"

@implementation SPWKBaseCollectionViewController


- (void)viewDidLoad
{
    [super viewDidLoad];

    [self updateContentInsetsForInterfaceOrientation:self.interfaceOrientation];
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [self updateContentInsetsForInterfaceOrientation:toInterfaceOrientation];

    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}

- (void)updateContentInsetsForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
        UIEdgeInsets insets;

        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
            insets = UIEdgeInsetsMake(64, 0, 56, 0);
        } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
            if (UIInterfaceOrientationIsPortrait(orientation)) {
                insets = UIEdgeInsetsMake(64, 0, 49, 0);
            } else {
                insets = UIEdgeInsetsMake(52, 0, 49, 0);
            }
        }

        self.collectionView.contentInset = insets;
        self.collectionView.scrollIndicatorInsets = insets;
    }
}

@end

这也适用于Web视图:

self.webView.scrollView.contentInset = insets;
self.webView.scrollView.scrollIndicatorInsets = insets;

如果有更加优雅可靠的方法,请告诉我!硬编码的插入值很不好,但是当您想要保持iOS 5的兼容性时,我真的看不到其他方法。


我在类似的情况下做了类似的事情。不幸的是,如果您在刷新期间为tableview使用UIRefreshControl并旋转设备,则它将无法正常工作。这似乎会自行更新插图 - 在这种情况下是错误的。 - Stefan
你说得对,我也不喜欢硬编码插入的值。这个问题能否使用自动布局来解决呢?因为它仅适用于iOS 7版本以上。约束看起来会是什么样子?在哪个控制器类中使用哪种方法? - tajmahal
@tajmahal 我完全同意:我也不喜欢硬编码的值。就像我说的,当想要保持iOS 5兼容性时,我找不到其他方法。如果你找到了更干净的解决方案,请一定分享。自动布局对于插图不起作用,因为插图不能通过自动布局进行调整。 - Johannes Fahrenkrug

5

提供参考,如果有人遇到类似问题:看起来只有在滚动视图(或表格视图/集合视图/网页视图)是其视图控制器层次结构中的第一个视图时,automaticallyAdjustsScrollViewInsets 才会生效。

通常我会先在层次结构中添加UIImageView以获得背景图片。 如果您这样做,您必须在 viewDidLayoutSubviews: 中手动设置滚动视图的边缘插图:

- (void) viewDidLayoutSubviews {
    CGFloat top = self.topLayoutGuide.length;
    CGFloat bottom = self.bottomLayoutGuide.length;
    UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
    self.collectionView.contentInset = newInsets;

}

4
感谢Johannes Fahrenkrug的提示,我找到了以下解决方法:automaticallyAdjustsScrollViewInsets只有当子视图控制器的根视图是UIScrollView时才能正常工作。对于我来说,这意味着以下布置可行: 内容视图控制器内部包含导航控制器内部包含自定义容器视图控制器
但是这个却不行: 内容视图控制器内部包含自定义容器视图控制器内部包含导航控制器
然而,从逻辑角度来看,第二种选择似乎更合理。第一种选择可能仅适用于自定义容器视图控制器用于将视图附加到内容底部的情况。如果我想把视图放在导航栏和内容之间,就无法这样做。

3

在控制器之间的过渡中,UINavigationController调用未公开的方法_updateScrollViewFromViewController:toViewController来更新内容插入。这是我的解决方案:

UINavigationController+ContentInset.h

#import <UIKit/UIKit.h>
@interface UINavigationController (ContentInset)
- (void) updateScrollViewFromViewController:(UIViewController*) from toViewController:(UIViewController*) to;
@end

UINavigationController+ContentInset.m

#import "UINavigationController+ContentInset.h"
@interface UINavigationController()
- (void) _updateScrollViewFromViewController:(UIViewController*) from toViewController:(UIViewController*) to;
@end

@implementation UINavigationController (ContentInset)
- (void) updateScrollViewFromViewController:(UIViewController*) from toViewController:(UIViewController*) to {
    if ([UINavigationController instancesRespondToSelector:@selector(_updateScrollViewFromViewController:toViewController:)])
        [self _updateScrollViewFromViewController:from toViewController:to];
}
@end

在您的自定义容器视图控制器中的某处调用updateScrollViewFromViewController:toViewController

[self addChildViewController:newContentViewController];
[self transitionFromViewController:previewsContentViewController
                  toViewController:newContentViewController
                          duration:0.5f
                           options:0
                        animations:^{
                            [self.navigationController updateScrollViewFromViewController:self toViewController:newContentViewController];

                    }
                    completion:^(BOOL finished) {
                        [newContentViewController didMoveToParentViewController:self];
                    }];

这在IOS9beta版本中仍然是一个问题。这个解决方案对我来说完美地解决了问题。对于所有遇到此问题的人,请为此提交一个雷达,以便苹果公司公开此方法。 - berbie
这对我来说在iOS 8.4上运行良好。目前在iOS 9B5上,这已经停止工作了。有什么猜测为什么?该方法确实存在,如此处所述 https://github.com/JaviSoto/iOS9-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UINavigationController.h#L408 - dezinezync
使用“- (void)_computeAndApplyScrollContentInsetDeltaForViewController:(id)arg1;”对我来说在iOS 8和9上都有效。 - dezinezync

3

2

Swift解决方案

这个解决方案针对iOS 8及以上版本的Swift。

我的应用程序根视图控制器是一个导航控制器。最初,这个导航控制器有一个UIViewController在其堆栈上,我称之为父视图控制器。

当用户点击导航栏按钮时,父视图控制器在其视图中使用容器API在两个子视图控制器之间切换(在映射结果和列表结果之间切换)。父视图控制器持有对这两个子视图控制器的引用。第二个子视图控制器直到用户第一次点击切换按钮才会被创建。第二个子视图控制器是一个表视图控制器,存在以下问题:无论其 automaticallyAdjustsScrollViewInsets 属性设置如何,都会低于导航栏。

为了解决这个问题,在子表视图控制器创建后以及在父视图控制器的 viewDidLayoutSubviews 方法中,我调用 adjustChild:tableView:insetTop 方法(如下所示)。

像这样将子视图控制器的表视图和父视图控制器的 topLayoutGuide.length 传递给 adjustChild:tableView:insetTop 方法...

// called right after childViewController is created
adjustChild(childViewController.tableView, insetTop: topLayoutGuide.length)

调整子元素方法(adjustChild method)...
private func adjustChild(tableView: UITableView, insetTop: CGFloat) {
    tableView.contentInset.top = insetTop
    tableView.scrollIndicatorInsets.top = insetTop

    // make sure the tableview is scrolled at least as far as insetTop
    if (tableView.contentOffset.y > -insetTop) {
        tableView.contentOffset.y = -insetTop
    }
}

语句 tableView.scrollIndicatorInsets.top = insetTop 调整了表视图右侧的滚动指示器,使其从导航栏下方开始。这是微妙的,很容易被忽略,直到你意识到它。

以下是 viewDidLayoutSubviews 的内容...

override func viewDidLayoutSubviews() {
    if let childViewController = childViewController {
        adjustChild(childViewController.tableView, insetTop: topLayoutGuide.length)
    }
}

请注意,上述所有代码均出现在父视图控制器中。
我是在“iOS 7 Table view fail to auto adjust content inset”问题的Christopher Pickslay's answer帮助下解决了这个问题。

您有“Christopher Pickslay的帖子”的链接吗? - koen
嗨@Koen。我在我的答案底部添加了一个链接,指向Christopher Pickslay的答案。 - Mobile Dan

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