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


Вызов времени!

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

  1. UIView с динамически изменяемым содержимым высоты (расширяемый UITextView) = RED
  2. UIView как нижний колонтитул = синий

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

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

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

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

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

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

Спасибо.

Случай отдыха

3 10

3 ответа:

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

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

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

Некоторые технические авторы iOS, такие как Эрика Садун, предпочитают использовать смешанный подход практически во всех ситуациях ("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.

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

Обратите внимание на следующее:

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

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

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

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