将UITextView嵌入UITableViewCell中

在Workable,我们有一个称为评估的功能。 部分原因是可以填写面试套件,如下所示。

当我们开始研究如何实现此功能时,出现的一个想法是使用堆栈视图,但是用户创建了采访工具包,因此它们的大小可能会有所不同。 因此,我们选择使用UITableView作为此屏幕的主干。 毕竟它只是UITableViewCell中的UITextView,对吗?

在本文中,我将尝试列出我们面临的所有问题以及如何解决这些问题。 在演练结束时,将附上完整的解决方案。

在单元格内添加UITextView

我们创建了一个原型单元,并添加了UITextView,如下所示。

另外,不要忘记在UITextView的属性检查器中关闭 Scrolling Enabled属性。 这使UITextView具有取决于文本的固有内容大小,或者接近于我们稍后发现的文本大小

现在,我们希望能够编辑文本。 简单,只需启用UITextView的`Editable`属性即可。

但是我们有一个小问题,我们希望UITableViewCell跟随UITextView的高度。

我们可以从单元格创建一个回调,以通知我们任何文本更改,如下所示:

TextTableViewCell.swift中

  @IBOutlet弱var textView:UITextView! 
var textChanged:(((String)-> Void)?

覆盖func awakeFromNib(){
super.awakeFromNib()
textView.delegate =自我
}

func textChanged(action:@escaping(String)-> Void){
self.textChanged =操作
}

func textViewDidChange(_ textView:UITextView){
textChanged?(textView.text)
}

TableTableViewController.swift-> cellForRow中

  cell.textChanged {[weak tableView](newText:String)在 

}

注意:不要忘了在此闭包的捕获列表中使tableView变弱,因为您将得到一个保留周期。 tableView->单元格(UI),单元格-> tableView(关闭)

我们唯一要做的就是重新加载单元。

我们有多种方法可以做到这一点,因此我们将尝试最常见的一种方法tableView.reloadData()。

  cell.textChanged {[weak tableView](_)in 
//可能的解决方案:1
//不,单元格发生一次更改然后失去焦点
tableView?.reloadData()
}

使用reload数据,您只能对文本视图进行一次更改,因为reloadData()是异步工作的。 所以我们不能使用它。

好的,那么我们将尝试重新加载单元。

  cell.textChanged {[weak tableView](_)in 
//可能的解决方案:2
//不,重新加载单元格,文本甚至都没有变化
tableView?.reloadRows(at:[indexPath],带有:.none)
}

使用reloadRows,由于它具有同步特性,我们甚至都无法进行任何更改,只是失去了UITextView的关注。 所以我们也不能使用它。

我们可以尝试做的另一件事就是告诉tableView仅重新布局自身。

我们可以通过调用beginUpdates()和endUpdates()来实现。

  cell.textChanged {[weak tableView](_)in 
//可能的解决方案:3
//似乎可以工作
tableView?.beginUpdates()
tableView?.endUpdates()
}

该解决方案似乎确实有效 ! 具有使布局更改具有动画效果的附加好处!

注意:如果您不希望对布局更改进行动画处理,则可以在UIView.performWithoutAnimation()中调用beginUpdates()endUpdates()。

结果:

但是这个解决方案还没有结束。

UIScrollView和UITextView / UITextField

当UIScrollView内有UITextView或UITextField时,scrollview调整其contentOffset以使UITextView的光标可见。 在我们的情况下,当用户开始键入并且光标移到键盘下方时,您会注意到一种奇怪的滚动行为。

例:

当用户键入字母时,光标的焦点和tableView的布局会发生冲突。 我们唯一需要做的就是像这样将下一个运行循环分派到主队列,从而推迟其布局:

  cell.textChanged {[weak tableView](_)in 
DispatchQueue.main.async {
tableView?.beginUpdates()
tableView?.endUpdates()
}
}

现在就可以了!

是吗 取决于应用程序的部署目标。 如果您是从iOS 11或更高版本进行定位的,那就可以了! 如果没有,请继续阅读。


奇怪的滚动和布局问题

当您向下滚动UITableView并尝试编辑文本时,您会注意到奇怪的跳跃行为和布局错误的单元格。

经过一些实验,我们发现,奇怪的滚动行为具有更高的RowHeight estimatedRowHeight值“更奇怪”。 因此,我们怀疑这与UITableView如何计算行的高度有关。

所以我们决定去上学。

手动计算像元高度

首先关闭自动高度计算,并将estimatedRowHeight高度设置为0。

然后,我们将手动计算每个单元格的高度,以便UITableView可以正确布局单元格,并且高度会随着用户的输入而变化。

我们将需要此扩展名,该扩展名将为您提供String的高度,并提供将要呈现的可用宽度和字体。

 扩展字符串{ 
func heightWithConstrainedWidth(width:CGFloat,字体:UIFont)-> CGFloat {
让constraintRect = CGSize(宽度:宽度,高度:.greatestFiniteMagnitude)
让boundingBox = self.boundingRect(带有:constraintRect,选项:[。usesFontLeading,.usesLineFragmentOrigin],属性:[NSAttributedStringKey.font:font],上下文:nil)

返回boundingBox.height
}
}

现在我们可以使用它来计算每个单元的高度。

 公共重写func tableView(_ tableView:UITableView,heightForRowAt indexPath:IndexPath)-> CGFloat { 
返回模型[indexPath.row]
.heightWithConstrainedWidth(width:tableView.frame.width,字体:UIFont.systemFont(ofSize:14))
}

结果:

如您所见,该行的确改变了高度,但是UITextView被裁剪了,我们也没有奇怪的滚动行为! 我们越来越近了!

如果您仔细查看UITextViews,尽管与超级视图之间的距离为0,但仍会注意到某种填充。

为了使这一点更加清晰,我们使用了一个红色背景的半透明标签 ,我们将UITextView的背景设置为灰色,并且为了清晰起见,将单元格的高度增加了100点。 UILabel和UITextView具有相同的字体和字体大小的文本。

结果:

因此,我们的下一步是从UITextView中删除该填充。 为此,我们将UITextView子类化,删除了填充,并将其用作替代品。

  @IBDesignable类ZeroPaddingTextView:UITextView { 

覆盖func layoutSubviews(){
super.layoutSubviews()
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}

}

现在,UILabel和UITextView中的文本位置完全相同!

重要!

现在,我们手动计算了行的高度,我们不需要UITableViewCell内容视图和UITextView之间的底部约束,因为我们知道这两个在设计上具有相同的高度。

最后结果:

太好了,现在可以正常使用了! 🎉

示例项目在这里。

希望对您有所帮助! 另外, 欢迎反馈