UIView在类似于iMessage App的键盘上

目前我正在尝试基本实施和苹果iMessage应用程序的确切副本。

这意味着我需要一个停靠在屏幕底部的UITextView,并在它成为firstResponder时向上移动。 – 其实很简单 有很多方法可以做到这一点,其中最常见的两种方法当然是在收到通知时向上或向下animation。 另一个是通过inputAccessoryView来完成的。 可悲的是,有一些function,另一个没有。 他们似乎是相互排斥的。

最大的问题是轮换

我大概至less挖掘了七个不同的github项目,所有这些项目都重新实现了我试图实现的相同的function/行为,但是实际上所有这些项目都失败了。

例如HPGrowingTextView,其中Facebook / FacebookMes​​senger /(可能WhatsApp)官方应用程序使用,是垃圾代码的一大块。 把你的iDevice,打开Facebook应用程序,去聊天,popup键盘和旋转你的设备。 如果你注意,你会注意到input栏略微跳跃,在键盘框架和它自己之间留下一些空白空间。 然后在显示键盘时看看iMessage中的Apples实现。 这是完美的。

除此之外,HPGrowingTextView库使用的contentOffset和EdgeInset-hacking给了我恶梦。

所以我想自己做,从头开始。

现在我已经有了一个非常漂亮,优雅和不太成熟的UITextView实现,但是有一部分缺失。

优雅的旋转。

当我在willRotateToInterfaceOrientation:duration:方法中简单地调整框架到它们各自的新位置时,所有事情都完成了,但是我也遇到了与HPGrowingTextView(见Facebook应用程序)相同的问题。 在旋转发生时,input视图和键盘之间有一点空间。

我发现当将设备旋转到横向时,当前显示的肖像键盘不会“变形”,而是消失(发送'willHide'通知),并再次出现横向版本(发送“willShow”通知)。 过渡是一个非常微妙的褪色,可能有一些调整。

我使用inputAccessoryView重新实现了我的项目,看看会发生什么,然后我感到惊喜。 inputAccessoryView旋转与键盘完美同步。 两者之间没有空间/差距。

可悲的是,我还没有想出如何让inputAccessoryView停靠在屏幕的底部,而不是随着键盘消失/移出…

我不想要的就是像hack-y这样的解决scheme,“在toInterfaceOrientation的CoordinateSystem中稍微降低框架,然后在调用didRotateFrom …时将其移回。

我知道另一个已经设法实现这种行为的应用程序,它是“Kik Messenger”。

有没有人有一个想法,build议或链接,我还没有看到涵盖该主题?

谢谢一堆!

注意:一旦这个问题解决了,我将开源项目,让每个人都能获利,因为几乎每一个在过去几天我都能find的实现是一团糟。

我最近遇到了同样的问题,不得不build立一个自定义的解决scheme,因为我对可用的第三方库并不满意。 我把这个实现分解到它自己的GitHub项目中:

MessageComposerView

从iOS 6.1 7&8模拟器的一些简单testing来看,旋转似乎正确地遵循键盘。 该视图也将随文本一起增长,并在旋转时自动resize。

MessageComposerView

你可以像这样使用一个非常基本的init函数来创build屏幕宽度和默认高度,例如:

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

还有其他几种初始化工具可供您自定义框架,键盘偏移和textview最大高度。 查看更多的自述文件!

我已经以相当优雅的方式成功地解决了这个问题(我想,…)。

该代码将在下周在Github上发布,并链接到这个答案。

它是如何完成的:我通过selectinputAccessoryView来完成旋转工作。

命名:

  1. 'MessageInputView'是一个包含我的'GrowingUITextView' (它还包含一个“发送”button和背景图像)的UIView。
  2. “ChatView”是属于ChatViewController的视图,它显示所有Chatbubbles,并将我的“MessageInputView”停靠在底部。
  3. 'keyboardAccessoryView'是一个空的UIView大小:CGRect(0,0,0,0)。

我需要弄清楚当键盘被解散时如何在屏幕上留下MessageInputView 。 那是棘手的部分。 我通过创build另一个视图( keyboardAccessoryView ),并有我的GrowingUITextView使用它作为inputAccessoryView。 我保留了keyboardAccessoryView,因为我以后需要引用它。

然后我想起了我在其他尝试中做的一些事情(每当键盘通知到达时,在屏幕上围绕MessageInputView的框架设置animation)。

我添加我的MessageInputView作为子视图到我的ChatView (在最底部)。 每当它被激活,willShow:方法被键盘通知调用时,我都会手动将MessageInputView的框架设置到它指定的位置上方。 当animation结束并执行完成块时,我从ChatView中删除子视图并将其添加到keyboardAccessoryView 。 这将导致另一个通知被触发,因为键盘被重新加载每次inputAccessoryView的框架/边界被改变! 你需要意识到这一点,并妥善处理!

当键盘即将解散时,我将MessageInputView的框架转换为我的ChatView的坐标系,并将其添加为子视图。 因此,它从我的keyboardAccessoryView中删除。 然后,我调整了keyboardAccessoryView的框架回到CGRect(0,0,0,0),否则UIViewAnimationDuration将不匹配! 然后,我允许键盘被解散,我有我的MessageInputView从上面跟着它,最终停靠在屏幕的底部。

尽pipe如此,这是相当多的工作。

保重。

PS:如果有人想出一个更简单的方法(完全)让我知道。

这是一个在iOS 9.3.1和8.3.1上正常工作的UITextView子类。 它把注意力放在长大和缩小的同时,始终保持在正确的地方,顺利地进行animation。

坚持键盘上的视图是微不足道的,许多解决scheme很容易find,所以它不包括…

我找不到任何制作完成的解决scheme,所以我最终从头开始研究这个解决scheme。 一路上我不得不解决很多小问题。

代码评论应该让你知道发生了什么。

我已经在我的Github上分享了这个,贡献非常感谢。

笔记

  • 未经testing支持景观
  • 未在i6 +上testing

演示

在这里输入图像说明

(在最大高度元素变成可滚动之后,忘了拖动演示,但是这也如预期那样工作…)

子类

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

最近我写了一篇关于这个确切问题的博客文章,以及如何通过使用键盘通知,但不使用inputAccessoryView ,以简洁优雅的方式解决这个问题。 虽然这个问题很老,但这个主题仍然是相关的,所以这里是链接到post: 在键盘和附加视图之间同步旋转animation

如果您不想深入博客文章中描述的长篇解释,请使用代码示例进行简短描述:

基本的原则是使用每个人都使用的相同方法 – 观察键盘通知来上下移动连接的视图。 但除此之外,由于接口方向更改而导致键盘通知被触发时,您必须取消这些animation。

不带animation取消的旋转示例自定义界面方向更改:

在界面方向更改上带有animation取消的旋转示例:

 - (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; } } 

我如何为我解决这个问题:

我有ChatViewControllerFooterViewController作为UIContainerView 。 另外,我有FooterViewController contentViewsockets。 然后在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 } 

在这里输入图像说明

另一种方法是以编程方式创build视图并作为inputAccessoryView返回。