【问题标题】:UIScrollView with sticky footer UIView and dynamic height content带有粘性页脚 UIView 和动态高度内容的 UIScrollView
【发布时间】:2014-03-08 17:01:36
【问题描述】:

挑战时间!

假设我们有 2 个内容视图:

  1. 具有动态高度内容的 UIView(可扩展 UITextView)= RED
  2. 作为页脚的 UIView = 蓝色

此内容在 UIScrollView = GEEN 内

我应该如何构建和处理具有自动布局的约束以归档以下所有情况?

我正在考虑下一个基本结构:

- UIScrollView (with always bounce vertically)
    - UIView - Container
       - UIView - DynamicHeightContent
       - UIView - Sticky Footer

键盘处理应通过代码监视通知UIKeyboardWillShowNotificationUIKeyboardWillHideNotification 来完成。我们可以选择将键盘的结束帧高度设置为 Container UIView 底部引脚约束或 UIScrollView 底部 contentInset。

现在,棘手的部分是粘性页脚。

  • 如果可用屏幕多于整个容器视图,我们如何确保粘性页脚 UIView 保持在底部?
  • 当键盘显示/隐藏时,我们如何知道可用的屏幕空间?我们肯定会需要它。
  • 我想要的这个结构对吗?

谢谢。

【问题讨论】:

  • 2014 年末更新:很难在 iOS 中做到这一点。在 Android 中,您无需一个 loc 即可自动获得此设置。

标签: ios uiview uiscrollview autolayout sticky-footer


【解决方案1】:

UITextView 的文本内容相对较短时,内容视图的子视图(即文本视图和页脚)将无法通过约束来决定其内容视图的大小。这是因为当文本内容很短时,内容视图的大小将需要由滚动视图的大小决定。

更新:后一段是不真实的。您可以在内容视图本身或内容视图的视图层次结构中的某处安装固定高度约束。固定高度约束的常量可以在代码中设置以反映滚动视图的高度。后一段也反映了思想上的谬误。在纯自动布局方法中,内容视图的子视图不需要指定滚动视图的contentSize;相反,内容视图本身最终必须决定contentSize

无论如何,我决定采用 Apple 所谓的“混合方法”来使用 UIScrollView 的自动布局(请参阅 Apple 的技术说明:https://developer.apple.com/library/ios/technotes/tn2154/_index.html

一些 iOS 技术作家,例如 Erica Sadun,喜欢在几乎所有情况下使用混合方法(“iOS Auto Layout Demystified”,第 2 版)。

在混合方法中,内容视图的框架和滚动视图的内容大小在代码中明确设置。

这是我为此挑战创建的 GitHub 存储库:https://github.com/bilobatum/StickyFooterAutoLayoutChallenge。这是一个有效的解决方案,带有布局更改的动画。它适用于不同尺寸的设备。为简单起见,我禁用了横向旋转。

对于那些不想下载和运行 GitHub 项目的人,我在下面列出了一些亮点(要完整的实现,你必须查看 GitHub 项目):

内容视图为橙色,文本视图为灰色,粘性页脚为蓝色。滚动时,文本在状态栏后面可见。我实际上不喜欢那样,但它可以作为演示。

故事板中唯一实例化的视图是滚动视图,它是全屏的(即,下重叠状态栏)。

出于测试目的,我将双击手势识别器附加到蓝色页脚以关闭键盘。

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.scrollView.alwaysBounceVertical = YES;

    [self.scrollView addSubview:self.contentView];
    [self.contentView addSubview:self.textView];
    [self.contentView addSubview:self.stickyFooterView];

    [self configureConstraintsForContentViewSubviews];

    // Apple's mixed (a.k.a. hybrid) approach to laying out a scroll view with Auto Layout: explicitly set content view's frame and scroll view's contentSize (see Apple's Technical Note TN2154: https://developer.apple.com/library/ios/technotes/tn2154/_index.html)
    CGFloat textViewHeight = [self calculateHeightForTextViewWithString:self.textView.text];
    CGFloat contentViewHeight = [self calculateHeightForContentViewWithTextViewHeight:textViewHeight];
    // scroll view is fullscreen in storyboard; i.e., it's final on-screen geometries will be the same as the view controller's main view; unfortunately, the scroll view's final on-screen geometries are not available in viewDidLoad
    CGSize scrollViewSize = self.view.bounds.size;

    if (contentViewHeight < scrollViewSize.height) {
        self.contentView.frame = CGRectMake(0, 0, scrollViewSize.width, scrollViewSize.height);
    } else {
        self.contentView.frame = CGRectMake(0, 0, scrollViewSize.width, contentViewHeight);
    }

    self.scrollView.contentSize = self.contentView.bounds.size;
}

- (void)configureConstraintsForContentViewSubviews
{
    assert(_textView && _stickyFooterView); // for debugging

    // note: there is no constraint between the subviews along the vertical axis; the amount of vertical space between the subviews is determined by the content view's height

    NSString *format = @"H:|-(space)-[textView]-(space)-|";
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:@{@"space": @(SIDE_MARGIN)} views:@{@"textView": _textView}]];

    format = @"H:|-(space)-[footer]-(space)-|";
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:@{@"space": @(SIDE_MARGIN)} views:@{@"footer": _stickyFooterView}]];

    format = @"V:|-(space)-[textView]";
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:@{@"space": @(TOP_MARGIN)} views:@{@"textView": _textView}]];

    format = @"V:[footer(height)]-(space)-|";
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:@{@"space": @(BOTTOM_MARGIN), @"height": @(FOOTER_HEIGHT)} views:@{@"footer": _stickyFooterView}]];

    // a UITextView does not have an intrinsic content size; will need to install an explicit height constraint based on the size of the text; when the text is modified, this height constraint's constant will need to be updated
    CGFloat textViewHeight = [self calculateHeightForTextViewWithString:self.textView.text];

    self.textViewHeightConstraint = [NSLayoutConstraint constraintWithItem:self.textView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0f constant:textViewHeight];

    [self.textView addConstraint:self.textViewHeightConstraint];
}

- (void)keyboardUp:(NSNotification *)notification
{
    // when the keyboard appears, extraneous vertical space between the subviews is eliminated–if necessary; i.e., vertical space between the subviews is reduced to the minimum if this space is not already at the minimum

    NSDictionary *info = [notification userInfo];
    CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
    double duration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

    CGFloat contentViewHeight = [self calculateHeightForContentViewWithTextViewHeight:self.textView.bounds.size.height];
    CGSize scrollViewSize = self.scrollView.bounds.size;

    [UIView animateWithDuration:duration animations:^{

        self.contentView.frame = CGRectMake(0, 0, scrollViewSize.width, contentViewHeight);
        self.scrollView.contentSize = self.contentView.bounds.size;
        UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, keyboardRect.size.height, 0);
        self.scrollView.contentInset = insets;
        self.scrollView.scrollIndicatorInsets = insets;

        [self.view layoutIfNeeded];

    } completion:^(BOOL finished) {

        [self scrollToCaret];
    }];
}

虽然这个演示应用的自动布局组件花费了一些时间,但我在与 UITextView 嵌套在 UIScrollView 中相关的滚动问题上花费的时间几乎一样多。

【讨论】:

  • 哇,谢谢@bilobatum!我同意没有代码就不可能解决这个挑战,但是我确实找到了一种方法来强制自动布局计算并在有更多可用屏幕时扩展内容视图。当你知道有更多屏幕时,我认为的技巧是添加垂直对齐约束。再次感谢!我也会很快在回购中发布我的解决方案。
  • @GastonM 你让我重新思考了我对使用纯自动布局方法的可能性的假设。我更新了我的答案。我渴望看到您的解决方案。
  • 我在 iOS 6 上遇到了同样的问题。在 iOS 7 的情况下,我的页脚视图在滚动视图上完美地粘在底部,但在 iOS 6 上却不是这样。请帮忙。
【解决方案2】:

如果我了解整个任务,我的解决方案是将“红色”和“蓝色”视图放在一个容器视图中,当您知道动态内容(红色)的大小时,您可以计算容器的大小并设置滚动视图内容尺寸。 稍后,在键盘事件中,您可以调整内容和页脚视图之间的空白

【讨论】:

  • 但是(>=25)之间的空格应该和键盘无关。它应该根据是否可以扩展或收缩(有更多的屏幕高度可用)。
  • 看来我缺了点什么。主屏幕大小总是已知的值,键盘框架也是,所以你只需要在知道动态内容大小的那一刻计算白色调料,为此调用setNeedLayout 用于容器视图并在layoutSubviews 中调整所有视图的框架。在 viewDidLayoutSubviews 的视图控制器中,将 scrollView 内容大小设置为 container.bounds。
  • 好吧,也许我不够清楚。我不想自己计算。我想设置适当的约束并让自动布局计算它。这不是它的用途吗?
  • 哦,看来我忽略了主要问题:)
  • 没问题!最后,我可以说我做到了!使用自动布局和约束。我不得不说这是可能的,但它真的很复杂。在我发布我的做法之前,我会等待一些时间来鼓励其他人发布他们的想法。谢谢!
【解决方案3】:

与其使用UIScrollView,不如使用UITableView。不使用自动布局也可能更好。至少,我发现最好不要将它用于这些操作。

查看以下内容:

  • UITextView textViewDidChange
    • 使用sizeThatFits 更改文本视图的大小(限制宽度并使用FLT_MAX 作为高度)。更改框架,而不是 contentSize。
    • 调用UITableViewbeginUpdates/endUpdates 更新表格视图
    • 滚动到光标
  • UIKeyboardWillShowNotification通知
    • 在通过的NSNotification 上,您可以调用userInfo(字典)和密钥UIKeyboardFrameBeginUserInfoKey。根据键盘大小的高度缩小表格视图的框架。
    • 再次滚动到光标(因为布局将全部更改)
  • UIKeyboardWillHideNotification 通知
    • 同显示通知,正好相反(增加表格视图高度)

要将页脚视图固定在底部,您可以在表格视图中添加一个中间单元格,并根据文本大小和键盘是否可见来更改大小。

上述内容肯定需要您进行一些额外的操作 - 我并不完全了解您的所有案例,但它绝对可以帮助您入门。

【讨论】:

    猜你喜欢
    • 2014-02-13
    • 2015-05-12
    • 1970-01-01
    • 1970-01-01
    • 2016-01-04
    • 1970-01-01
    • 2018-02-05
    • 1970-01-01
    相关资源
    最近更新 更多