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

Posted

技术标签:

【中文标题】iOS 7:自定义容器视图控制器和内容插入【英文标题】:iOS 7: Custom container view controller and content inset 【发布时间】:2013-09-28 08:55:00 【问题描述】:

我有一个封装在导航控制器中的表格视图控制器。当通过presentViewController:animated:completion: 呈现时,导航控制器似乎会自动将正确的内容插入应用到表格视图控制器。 (谁能向我解释一下这是如何工作的?)

但是,只要我将组合包装在自定义容器视图控制器中并呈现它,表格视图内容的最顶部就会隐藏在导航栏后面。为了在此配置中保留自动内容插入行为,我能做些什么吗?我是否必须“通过”容器视图控制器中的某些内容才能使其正常工作?

我想避免手动或通过自动布局调整内容插入,因为我想继续支持 ios 5。

【问题讨论】:

【参考方案1】:

我也遇到过类似的问题。所以在 iOS 7 上,UIViewController 上有一个新属性,称为automaticallyAdjustsScrollViewInsets。默认情况下,它设置为YES。当您的视图层次结构比导航或标签栏控制器中的滚动/表格视图更复杂时,此属性似乎对我不起作用。

我所做的是:我创建了一个基本视图控制器类,我的所有其他视图控制器都继承自该类。然后该视图控制器负责显式设置插图:

标题:

#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

这也适用于网络视图:

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

如果有更优雅但更可靠的方法来做到这一点,请告诉我!硬编码的插入值闻起来很糟糕,但是当您想要保持 iOS 5 兼容性时,我真的没有看到其他方法。

【讨论】:

我在类似的情况下正在做这样的事情。不幸的是,如果您有一个用于 tableview 的 UIRefreshControl 并在刷新期间旋转设备,则它不起作用。这似乎会自行更新插图 - 在这种情况下是错误的。 你说得对,我也不喜欢硬编码的插入值。这是否可以使用自动布局来完成,因为它仅适用于 iOS 7?约束是什么样的,它会进入哪个控制器类的哪个方法? @tajmahal 我绝对同意:我也真的不喜欢硬编码的值。就像我说的,当想要保持 iOS 5 兼容性时,我找不到其他方法。如果您找到更清洁的解决方案,请务必分享。自动布局不适用于插图,因为不能使用自动布局调整插图。【参考方案2】:

仅供参考,以防有人遇到类似问题:似乎automaticallyAdjustsScrollViewInsets 仅在您的滚动视图(或 tableview/collectionview/webview)是其视图控制器层次结构中的第一个视图时才应用。

我经常在我的层次结构中首先添加一个 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;


【讨论】:

【参考方案3】:

感谢 Johannes Fahrenkrug 的提示,我发现了以下内容:automaticallyAdjustsScrollViewInsets 确实似乎只有在子视图控制器的根视图是 UIScrollView 时才能像宣传的那样工作。对我来说,这意味着以下安排有效:

内容视图控制器导航控制器自定义容器视图控制器内。

虽然不是这样:

内容视图控制器自定义容器视图控制器导航控制器

然而,从逻辑的角度来看,第二种选择似乎更明智。第一个选项可能只起作用,因为自定义容器视图控制器用于将视图附加到内容的底部。如果我想把视图放在导航栏和内容之间,那是行不通的。

【讨论】:

【参考方案4】:

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 的一个问题。这个解决方案对我来说非常有效。对于每个也有这个问题的人。请为该苹果提交一份雷达文件,公开此方法。 这对我来说适用于 iOS 8.4。目前在 iOS 9B5 上,这已停止工作。任何猜测为什么?该方法确实存在,如此处所述github.com/JaviSoto/iOS9-Runtime-Headers/blob/master/Frameworks/… 使用 - (void)_computeAndApplyScrollContentInsetDeltaForViewController:(id)arg1; 对我来说适用于 iOS 8 和 9。【参考方案5】:

您不需要调用任何未记录的方法。您需要做的就是在您的UINavigationController 上拨打setNeedsLayout。查看其他答案:https://***.com/a/33344516/5488931

【讨论】:

【参考方案6】:

迅捷解决方案

此解决方案针对 Swift 中的 iOS 8 及更高版本。

我的应用的根视图控制器是一个导航控制器。最初,这个导航控制器的堆栈上有一个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 方法...

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)
    

请注意,上面的所有代码都出现在父视图控制器中。

我在Christopher Pickslay's answer 的帮助下解决了这个问题“iOS 7 Table view failed to autoadjust content inset”。

【讨论】:

你有“Christopher Pickslay 的帖子”的链接吗? 嗨@Koen。我在答案底部添加了指向 Christoper Pickslay 答案的链接。

以上是关于iOS 7:自定义容器视图控制器和内容插入的主要内容,如果未能解决你的问题,请参考以下文章

使用自定义标签栏自动调整视图的内容插入

iOS - 创建自定义过渡动画

如何为 iOS 应用创建自定义容器视图?

iOS 自定义容器 VC 和需要特定方向的子级

模态视图控制器从自定义单元格选择问题中分离 - iOS 7

如何使用 iOS 7 自定义转换在顶部呈现半模态视图控制器