UIView на клавиатуре похож на iMessage App

В настоящее время я пытаюсь в основном реализовать и точную копию приложения Apple iMessage.

Это означает, что мне нужен UITextView, который состыкован в нижней части экрана и перемещается вверх, когда он становится первымResponder. - Это довольно легко. Там bazillion способы сделать это, и два из наиболее распространенных, конечно, оживляют представление вверх или вниз, если получено уведомление. Другой - сделать это через inputAccessoryView. К сожалению, некоторые из особенностей, которые есть у одного, другой нет. И они кажутся взаимоисключающими.

Большая проблема - вращение.

Я проработал примерно по крайней мере семь различных проектов github, все из которых перепрофилируют ту же функциональность/поведение, которую я пытаюсь достичь, но буквально все они терпят неудачу.

HPGrowingTextView, например, который использует официальное приложение Facebook/FacebookMessenger/(и, возможно, WhatsApp), является одним большим куском нежелательного кода. Возьмите iDevice, откройте приложение Facebook, зайдите в чат, нажмите клавиатуру и поверните устройство. Если вы обратите внимание, вы заметите, что входной бар слегка подпрыгивает и оставляет пустое пространство между рамкой клавиатуры и ее собственным. Затем взгляните на реализацию Apple в iMessage, когда отображается клавиатура. Это прекрасно.

Кроме того, что использование contentOffset и EdgeInset-взлома, которое использует библиотека HPGrowingTextView, дает мне кошмары.

Поэтому я хотел сделать это сам и начать с нуля.

Сейчас у меня очень тонкая, элегантная и безрадостная реализация растущего UITextView, но одна часть отсутствует.

Элегантное вращение.

Когда я просто настраиваю рамки на свои новые позиции в методе willRotateToInterfaceOrientation: duration:, все заканчивается отлично, но у меня та же проблема, что и HPGrowingTextView (см. приложение для Facebook). Небольшой пробел между входом и клавиатурой во время вращения.

Я узнал, что при повороте устройства в ландшафт, портретная клавиатура, которая в настоящее время отображается, не "морфирует", а скорее исчезает (отправляет уведомление "willHide" ), и появляется альбомная версия (отправка уведомления "willShow" ), Переход - очень тонкое исчезновение и, возможно, изменение размера.

Я снова реализовал свой проект, используя inputAccessoryView, чтобы узнать, что происходит потом, и я был приятно удивлен. InputAccessoryView вращается в идеальной синхронизации с клавиатурой. Между ними нет пробела/зазора.

К сожалению, мне еще предстоит придумать, как иметь док-станцию ​​inputAccessoryView в нижней части экрана и НЕ исчезнуть/выйти из нее вместе с клавиатурой...

То, что я не хочу, это hack-y-решения, например,... "немного опустив кадр в системе координат toInterfaceOrientation CoordinateSystem, а затем переместив его обратно, когда вызывается didRotateFrom..."

Я знаю одно другое приложение, которому удалось реализовать такое поведение, и это "Kik Messenger".

Есть ли у кого-нибудь идея, совет или ссылка, которую я еще не видел, покрывая эту тему?

Спасибо, куча!

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

Ответы

Ответ 1

Мне удалось решить проблему довольно элегантно (я думаю,...).

Код будет выпущен на Github на следующей неделе и связан с этим ответом.

-

Как это делается: я сделал работу по вращению, выбирая метод inputAccessoryView.

Обозначения:

  • 'MessageInputView' - это UIView, содержащий мой "GrowingUITextView" (он также содержит кнопку "Отправить" и фоновое изображение).
  • "ChatView" - это вид, который принадлежит ChatViewController, который отображает все Chatbubbles и имеет мой "MessageInputView", состыкованный внизу.
  • 'keyboardAccessoryView' - это пустой размер UIView: CGRect (0,0,0,0).

Мне нужно было выяснить, как включить MessageInputView на экране, когда клавиатура была уволена. Это была сложная часть. Я сделал это, создав другое представление (keyboardAccessoryView), и мой GrowingUITextView использовал его как свой входной экземпляр. Я сохранил клавиатуруAccessoryView, потому что мне понадобится ссылка на нее позже.

Затем я вспомнил некоторые вещи, которые я сделал в своей другой попытке (анимация кадров MessageInputView вокруг экрана всякий раз, когда пришло уведомление о клавиатуре).

Я добавил свой MessageInputView в качестве подзаголовка в свой ChatView (в самом низу). Всякий раз, когда он активируется, и методы willShow: вызывается с помощью уведомления клавиатуры, я вручную анимирую рамку MessageInputView, чтобы она обозначала позицию вверху. Когда анимация заканчивается и выполняется блок завершения, я удаляю subview из ChatView и добавляю его в клавиатуру AccessView. Это приводит к отключению другого уведомления, потому что клавиатура перезагружается КАЖДОЕ время, когда изменяются рамки/границы вводаAccessoryView!. Вы должны знать об этом и обращаться с ним соответствующим образом!

Когда клавиатура собирается уволить, я конвертирую свой кадр MessageInputView в мою систему координат ChatView и добавляю ее как подвью. Таким образом, он удаляется из моей клавиатурыAccessoryView. Затем я изменю размер рамки keyboardAccessoryView на CGRect (0,0,0,0), потому что иначе UIViewAnimationDuration не будет соответствовать! Затем я разрешаю клавиатуре отклоняться, и мой MessageInputView следует за ней сверху и в конечном итоге состыковывается в нижней части экрана.

Это довольно много работы для очень небольшого выигрыша.

-

Позаботьтесь.

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

Ответ 2

Недавно я столкнулся с одной и той же проблемой и должен был разработать собственное решение, поскольку я не был полностью доволен доступными сторонними библиотеками. Я отделил эту реализацию от своего проекта GitHub:

MessageComposerView

Из некоторого простого тестирования на iOS 6.1 7 и 8 симуляторов вращения, похоже, правильно следуют за клавиатурой. Представление также будет расти с текстом и автоматически изменяться при повороте.

MessageComposerView

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

self.messageComposerView = [[MessageComposerView alloc] init];
self.messageComposerView.delegate = self;
[self.view addSubview:self.messageComposerView];

Существует несколько других инициализаторов, которые также позволяют настраивать фрейм, смещение клавиатуры и максимальную высоту текста. Подробнее см. Readme!

Ответ 3

Здесь подкласс UITextView, который работает правильно на iOS 9.3.1 и 8.3.1. Он заботится о том, чтобы расти и сокращаться с ограничениями, сохраняя каретку всегда в нужном месте и плавно анимируя.

Наложение вида на клавиатуру тривиально, и многие решения можно легко найти, поэтому они не покрываются...

Я не смог найти готовые решения, готовые к выпуску, поэтому я закончил работу с нуля. На этом пути мне пришлось решить немало проблем.

Комментарии кодов должны дать вам представление о том, что происходит.

Я поделился этим с мой Github, взносы получили высокую оценку.

Примечания

  • Не тестируется для поддержки ландшафта
  • Не проверен на i6 +

Demo

введите описание изображения здесь

(после того, как элемент max height станет прокручиваемым. Забыл перетащить демоверсию, но это работает так же, как и ожидалось...)

Подкласс

class ruuiDynamicTextView: UITextView {
    var dynamicDelegate: ruuiDynamicTextViewDelegate?
    var minHeight: CGFloat!
    var maxHeight: CGFloat?
    private var contentOffsetCenterY: CGFloat!

    init(frame: CGRect, offset: CGFloat = 0.0) {
        super.init(frame: frame, textContainer: nil)
        minHeight = frame.size.height

        //center first line
        let size = self.sizeThatFits(CGSizeMake(self.bounds.size.width, CGFloat.max))
        contentOffsetCenterY = (-(frame.size.height - size.height * self.zoomScale) / 2.0) + offset

        //listen for text changes
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(textChanged), name: UITextViewTextDidChangeNotification, object: nil)

        //update offsets
        layoutSubviews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        //Use content size if more than min size, compensate for Y offset
        var height = max(self.contentSize.height - (contentOffsetCenterY * 2.0), minHeight)
        var updateContentOffsetY: CGFloat?
        //Max Height
        if maxHeight != nil && height > maxHeight {
            //Cap at maxHeight
            height = maxHeight!
        } else {
            //constrain Y to prevent odd skip and center content to view.
            updateContentOffsetY = contentOffsetCenterY
        }
        //update frame if needed & notify delegate
        if self.frame.size.height != height {
            self.frame.size.height = height
            dynamicDelegate?.dynamicTextViewDidResizeHeight(self, height: height)
        }
        //constrain Y must be done after setting frame
        if updateContentOffsetY != nil {
            self.contentOffset.y = updateContentOffsetY!
        }
    }

    func textChanged() {
        let caretRect = self.caretRectForPosition(self.selectedTextRange!.start)
        let overflow = caretRect.size.height + caretRect.origin.y - (self.contentOffset.y + self.bounds.size.height - self.contentInset.bottom - self.contentInset.top)
        if overflow > 0 {
            //Fix wrong offset when cursor jumps to next line un explisitly
            let seekEndY = self.contentSize.height - self.bounds.size.height
            if self.contentOffset.y != seekEndY {
                self.contentOffset.y = seekEndY
            }
        }
    }
}

protocol ruuiDynamicTextViewDelegate {
    func dynamicTextViewDidResizeHeight(textview: ruuiDynamicTextView, height: CGFloat)
}

Ответ 4

Недавно я написал сообщение в блоге об этой точной проблеме, которую вы описали, и о том, как ее решить коротким и элегантным способом, используя клавиатурные уведомления, но не используя inputAccessoryView. И хотя этот вопрос довольно старый, эта тема по-прежнему актуальна, поэтому вот ссылка на сообщение: Синхронизация анимации вращения между клавиатурой и прикрепленным представлением

Если вы не хотите погружаться в длинное объяснение, описанное в блоге, здесь приводится краткое описание с примером кода:

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

Пример вращения без отмены анимации при изменении ориентации интерфейса: 59YGJ.png

Пример поворота с отменой анимации при изменении ориентации интерфейса: ahbhD.png

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    [[NSNotificationCenter defaultCenter]
            addObserver:self selector:@selector(adjustViewForKeyboardNotification:)
            name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter]
            addObserver:self selector:@selector(adjustViewForKeyboardNotification:)
            name:UIKeyboardWillHideNotification object:nil];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];

    [[NSNotificationCenter defaultCenter]
            removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter]
            removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
    self.animatingRotation = YES;
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
    self.animatingRotation = NO;
}

- (void)adjustViewForKeyboardNotification:(NSNotification *)notification {
    NSDictionary *notificationInfo = [notification userInfo];

    // Get the end frame of the keyboard in screen coordinates.
    CGRect finalKeyboardFrame = [[notificationInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

    // Convert the finalKeyboardFrame to view coordinates to take into account any rotation
    // factors applied to the window’s contents as a result of interface orientation changes.
    finalKeyboardFrame = [self.view convertRect:finalKeyboardFrame fromView:self.view.window];

    // Calculate new position of the commentBar
    CGRect commentBarFrame = self.commentBar.frame;
    commentBarFrame.origin.y = finalKeyboardFrame.origin.y - commentBarFrame.size.height;

    // Update tableView height.
    CGRect tableViewFrame = self.tableView.frame;
    tableViewFrame.size.height = commentBarFrame.origin.y;

    if (!self.animatingRotation) {
        // Get the animation curve and duration
        UIViewAnimationCurve animationCurve = (UIViewAnimationCurve) [[notificationInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
        NSTimeInterval animationDuration = [[notificationInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

        // Animate view size synchronously with the appearance of the keyboard. 
        [UIView beginAnimations:nil context:nil];
        [UIView setAnimationDuration:animationDuration];
        [UIView setAnimationCurve:animationCurve];
        [UIView setAnimationBeginsFromCurrentState:YES];

        self.commentBar.frame = commentBarFrame;
        self.tableView.frame = tableViewFrame;

        [UIView commitAnimations];
    } else {
        self.commentBar.frame = commentBarFrame;
        self.tableView.frame = tableViewFrame;
    }
}

Ответ 5

Как я исправляю эту проблему для меня:

У меня ChatViewController и FooterViewController как UIContainerView. Кроме того, у меня есть выход contentView в FooterViewController. Тогда в ChatViewController у меня есть:

override func becomeFirstResponder() -> Bool {
    return true
}

override var inputAccessoryView: UIView? {
    if let childViewController = childViewControllers.first as? FooterViewController {
        childViewController.contentView.removeFromSuperview()
        return childViewController.contentView
    }
    return nil
}

введите описание изображения здесь

Другой способ - создать представление программно и вернуться в качестве inputAccessoryView.