UIScrollView с липким нижним колонтитулом UIView и динамической высотой содержимого

Время вызова!

Представьте, что у нас есть 2 просмотра контента:

  • UIView с динамически высотным содержимым (расширяемый UITextView) = RED
  • UIView как нижний колонтитул = ГОЛУБОЙ

Этот контент находится внутри UIScrollView = GEEN

Как мне структурировать и обрабатывать ограничения с помощью автоматического макета для архивирования всех следующих случаев?

Я думаю следующую базовую структуру, чтобы начать с:

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

Работа с клавиатурой должна выполняться с помощью уведомлений с кодом просмотра UIKeyboardWillShowNotification и UIKeyboardWillHideNotification. Мы можем выбрать, чтобы высота конечной рамы клавиатуры была ограничена нижним ограничителем контейнера UIView или нижним содержимым содержимого UIScrollView.

Теперь сложная часть - липкий нижний колонтитул.

  • Как мы убеждаемся, что липкий нижний колонтитул UIView остается внизу, если доступно больше экрана, чем весь вид контейнера?
  • Как узнать доступное пространство экрана, когда клавиатура отображается/скрыта? нам это обязательно понадобится.
  • Является ли это правильной этой структурой?

Спасибо.

Case recreation

Ответ 1

Когда текстовое содержимое UITextView относительно короткое, подвид представлений содержимого (то есть текстовый вид и нижний колонтитул) не сможет определять размер их содержимого через ограничения. Это связано с тем, что, когда текстовый контент короток, размер представления содержимого должен определяться размером прокрутки.

Обновление: Последний абзац не соответствует действительности. Вы можете установить ограничение с фиксированной высотой либо в самом представлении контента, либо где-нибудь в иерархии представлений представления содержимого. Константа ограничения фиксированной высоты может быть установлена ​​в коде, чтобы отображать высоту представления прокрутки. Последний абзац также отражает ошибочность мышления. В подходе с автоматическим макетом в представлении просмотра содержимого нет необходимости прокручивать вид прокрутки contentSize; вместо этого это представление контента, которое в конечном счете должно диктовать contentSize.

Несмотря на это, я решил пойти с Apple так называемым "смешанным подходом" для использования Auto Layout с UIScrollView (см. Техническое примечание Apple: https://developer.apple.com/library/ios/technotes/tn2154/_index.html)

Некоторые технические писатели iOS, такие как Эрика Садун, предпочитают использовать смешанный подход практически во всех ситуациях ( "iOS Auto Layout Demystified", 2-е изд.).

В смешанном подходе кадр представления контента и размер содержимого представления прокрутки явно задаются в коде.

Здесь репозиторий GitHub, созданный для этой задачи: https://github.com/bilobatum/StickyFooterAutoLayoutChallenge. Это рабочее решение в комплекте с анимацией изменений макета. Он работает на устройствах различного размера. Для простоты я отключил поворот к пейзажу.

Для тех, кто не хочет загружать и запускать проект GitHub, я включил некоторые основные моменты ниже (для полной реализации вам нужно будет посмотреть проект GitHub):

enter image description hereenter image description here

enter image description hereenter image description here

enter image description here

Представление контента оранжевое, текстовый вид серый, а липкий нижний колонтитул - синий. При прокрутке текст отображается за строкой. Мне это не нравится, но это хорошо для демонстрации.

Единственным представлением, созданным в раскадровке, является просмотр прокрутки, который является полноэкранным (т.е. строка состояния подкрылья).

Для целей тестирования я прикрепил двойной распознаватель жестов к голубому нижнему колонтитулу с целью отклонения клавиатуры.

- (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 mixed (a.k.a. hybrid) approach to laying out a scroll view with Auto Layout: explicitly set content view frame and scroll view contentSize (see Apple 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 final on-screen geometries will be the same as the view controller main view; unfortunately, the scroll view 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 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 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];
    }];
}

Хотя компонент Auto Layout этого демонстрационного приложения занял некоторое время, я потратил почти столько же времени на прокрутку проблем, связанных с UITextView, вложенными внутри UIScrollView.

Ответ 2

Вместо использования UIScrollView вам, скорее всего, будет лучше с UITableView. Также лучше не использовать автоматическую компоновку. По крайней мере, я нашел, что лучше не использовать его для такого рода манипуляций.

Посмотрите на следующее:

  • UITextView textViewDidChange
    • Измените размер текстового вида с помощью sizeThatFits (ограничение ширины и использование FLT_MAX для высоты). Измените кадр, а не contentSize.
    • Вызов UITableView beginUpdates/endUpdates для обновления вида таблицы
    • Прокрутите до курсора
  • UIKeyboardWillShowNotification уведомление
    • В NSNotification, который появляется, вы можете вызвать userInfo (словарь) и ключ UIKeyboardFrameBeginUserInfoKey. Уменьшите рамку представления таблицы в зависимости от высоты клавиатуры.
    • Прокрутите до курсора еще раз (так как все макеты будут изменены)
  • UIKeyboardWillHideNotification уведомление
    • То же, что и уведомление о шоу, прямо напротив (увеличение высоты таблицы)

Чтобы нижний колонтитул был обращен вниз, вы можете добавить промежуточную ячейку в представление таблицы и изменить ее размер в зависимости от размера текста и видимости клавиатуры.

Вышеизложенное, безусловно, потребует некоторых дополнительных манипуляций с вашей стороны - я не полностью понимаю все ваши дела, но это определенно поможет вам начать.

Ответ 3

Если я понимаю всю задачу, мое решение ставит "один" и "синий" виды в один вид контейнера, а в тот момент, когда вы знаете размер динамического контента (красный), вы можете рассчитать размер контейнера и установить содержимое прокрутки размер. Позже, на событиях клавиатуры вы можете настроить пробел между представлениями контента и нижнего колонтитула