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

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

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

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

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

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

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

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

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

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

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

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

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

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

Чего я не хочу, так это хакерских решений, таких как... "немного уменьшив фрейм в CoordinateSystem toInterfaceOrientation, а затем переместив его обратно вверх, когда был вызван didRotateFrom...".

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

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

Огромное спасибо!

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

5 ответов

Решение

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

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

-

Как это сделано: я сделал ротацию, выбрав inputAccessoryView-способ сделать это.

Номенклатура:

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

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

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

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

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

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

-

Береги себя.

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

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

MessageComposerView

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

MessageComposerView

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

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

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

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

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

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

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

Я поделился этим на своем Github.

Заметки

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

демонстрация

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

Подкласс

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)
}

Как мне решить эту проблему для меня:

я имею 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.

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

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

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

Пример поворота без пользовательской отмены анимации при изменении ориентации интерфейса:

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

- (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;
    }
}
Другие вопросы по тегам