ios - 通过点击按钮动态根据 UITextView 的 isScrollEnabled 展开和折叠 UITableView 行
问题描述
我已经实现了一个 UITableView。我在表格视图的单元格内容视图中有一个 UITextView 和 UIButton 。启用文本视图的滚动并给出高度约束。表格视图的行高是自动的,取决于文本视图的高度加上按钮的高度。
现在我要实现的是单击按钮切换文本视图的 isScrollEnabled 属性并删除高度约束。再次单击按钮时,切换文本视图的 isScrollEnabled 属性并添加高度约束。
在此过程中,文本视图不会在按钮单击时自动更改其框架(类似的事情可以通过使用 UILabel 代替 UITextView 并切换标签的行数属性而不是文本视图的启用滚动来实现),因此行高也不是更新。
预期结果是在第一次单击按钮时将文本视图的 isScrollEnabled 属性设置为 false 并删除文本视图的高度约束将使文本视图根据其包含的文本展开。在第二次单击按钮时,将文本视图的 isScrollEnabled 属性设置为 true 并添加文本视图的高度约束将使文本视图折叠到高度约束的常量值。因此,当我使用自动布局时,表格视图的行高也会发生变化。
解决方案
当我们动态更改单元格的高度时, AUITableView
不会自动重新计算行高 - 例如更改约束或更改 UI 元素的固有高度(例如,在文本视图中键入)。
因此,单元格需要告诉控制器其内容已更改。
大概,当用户在文本视图中键入时,您正在使用协议/委托模式或闭包来更新您的数据?如果是这样,这就是我们可以通知控制器并让它更新行高的地方。(如果你还没有这样做,你需要实现它。)
在不重新加载数据(我们不想这样做)的情况下更新表的最简单方法是调用:
tableView.performBatchUpdates(nil, completion: nil)
如果我们已经在使用闭包来更新用户输入的数据,我们可以同时进行调用......并且,我们可以包含“展开/折叠”状态(这样我们就可以在我们的数据源),当用户点击按钮在文本视图中切换滚动时,我们可以使用相同的闭包。
这是我的单元格及其约束的布局方式:
Scrolling Height Constraint
是,它64
是我们要在活动/非活动之间切换的约束。请注意,文本字段上有第二个高度约束:height >= 64
. 这使文本视图在编辑时保持在其“最小高度”。没有它,如果我们删除了足够多的文本,那么我们只有一两行,而不是这样:
我们最终得到一个如下所示的单元格:
这是一些示例代码...
首先,我们的数据的简单结构:
struct MyDataItem {
var text: String = ""
var isScrollEnabled: Bool = false
}
我们的单元类将声明一个闭包,当我们有更改时,我们将使用它来通知控制器:
var callback: ((String, Bool) -> ())?
接下来,一个示例滚动视图控制器,它实现了该闭包cellForRowAt
:
class ExpandTableViewController: UITableViewController {
var myData: [MyDataItem] = []
override func viewDidLoad() {
super.viewDidLoad()
// start with 20 sample strings (so we have table view scrolling)
for i in 0..<20 {
myData.append(MyDataItem(text: "Sample string \(i)", isScrollEnabled: true))
}
// start with 5 lines of text in Second row
myData[1].text = "Sample string 1\nLine 2\nLine 3\nLine 4\nLine 5"
// start with long text in Fourth row (for word wrapping)
myData[3].text = "Sample string 3 - with plenty of text to demonstrate word wrapping in the text view, and how it will affect the scrolling and expand / collapse feature of the cell."
// add a right nav bar "Done" button to stop editing and dismiss the keyboard
let btn = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.doneTapped))
self.navigationItem.rightBarButtonItem = btn
}
// to dismiss keyboard
@objc func doneTapped() -> Void {
view.endEditing(true)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as! TextViewCell
let item = myData[indexPath.row]
cell.fillData(item)
cell.callback = { [weak self] str, scroll in
guard let self = self else { return }
// update our data
self.myData[indexPath.row].text = str
self.myData[indexPath.row].isScrollEnabled = scroll
// tell the table view to re-layout cells where needed
// this will change row heights when editing and/or when
// tapping Expand/Collapse button
self.tableView.performBatchUpdates(nil, completion: nil)
}
return cell
}
}
这是单元格类 - 大多数“魔术”发生的地方:) ...我已经包含.isEnabled
了根据文本视图中的文本数量切换展开/折叠按钮的逻辑:
class TextViewCell: UITableViewCell, UITextViewDelegate {
@IBOutlet var theTextView: UITextView!
@IBOutlet var theButton: UIButton!
@IBOutlet var scrollingHeightConstraint: NSLayoutConstraint!
var callback: ((String, Bool) -> ())?
func textViewDidChange(_ textView: UITextView) {
let t = textView.text ?? ""
// inform the controller that our text changed
callback?(t, textView.isScrollEnabled)
updateTheButton()
}
@IBAction func didTap(_ sender: Any) {
// toggle scrolling on the text view
theTextView.isScrollEnabled.toggle()
updateTheConstrints()
updateTheButton()
let t = theTextView.text ?? ""
// inform the controller the expand / collapse button was tapped
callback?(t, theTextView.isScrollEnabled)
// if we're editing and just tapped Collapse
if theTextView.isFirstResponder && theTextView.isScrollEnabled {
//scroll the text view so the Cursor is visible *after* the view has resized
DispatchQueue.main.async {
self.scrollToCursorPosition()
}
}
}
private func scrollToCursorPosition() {
if let r = theTextView.selectedTextRange?.start {
let c = theTextView.caretRect(for: r)
theTextView.scrollRectToVisible(c, animated: true)
}
}
func updateTheButton() -> Void {
// set button title appropriately
theButton.setTitle(theTextView.isScrollEnabled ? "Expand" : "Collapse", for: [])
DispatchQueue.main.async {
// enable / disablbe button based on amount of text
// have to do this async, so it runs after the text has changed
self.theButton.isEnabled = self.theTextView.contentSize.height > self.scrollingHeightConstraint.constant
}
}
func updateTheConstrints() -> Void {
// activate or deactivate text view's height constraint
scrollingHeightConstraint.isActive = theTextView.isScrollEnabled
}
func fillData(_ item: MyDataItem) -> Void {
theTextView.text = item.text
theTextView.isScrollEnabled = item.isScrollEnabled
updateTheConstrints()
updateTheButton()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
func commonInit() -> Void {
if theTextView != nil {
// mke sure the text view's delegate is set
theTextView.delegate = self
// anything else we may want to do on init
}
}
}
最后,我正在使用的情节提要的来源:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="VjW-oA-FRf">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Expand Table View Controller-->
<scene sceneID="PM6-ph-sPi">
<objects>
<tableViewController id="VjW-oA-FRf" customClass="ExpandTableViewController" customModule="DelMe" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="xK8-0I-ylV">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="textViewCell" rowHeight="101" id="MWI-1y-UOV" customClass="TextViewCell" customModule="DelMe" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="101"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MWI-1y-UOV" id="ZYw-rr-lnO">
<rect key="frame" x="0.0" y="0.0" width="375" height="101"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="Lorem ipsum dolor sit er elit lamet." textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="pCr-bW-XWY">
<rect key="frame" x="16" y="11" width="251" height="64"/>
<color key="backgroundColor" red="0.55634254220000001" green="0.97934550050000002" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="64" id="4SV-lM-57L"/>
<constraint firstAttribute="height" constant="64" id="jBQ-hv-SBY"/>
</constraints>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Bex-xS-WMG">
<rect key="frame" x="279" y="11" width="80" height="30"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="80" id="CG1-AO-HEE"/>
</constraints>
<state key="normal" title="Expand"/>
<connections>
<action selector="didTap:" destination="MWI-1y-UOV" eventType="touchUpInside" id="KlW-my-Tna"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="Bex-xS-WMG" firstAttribute="top" secondItem="ZYw-rr-lnO" secondAttribute="topMargin" id="3de-fi-JG9"/>
<constraint firstItem="pCr-bW-XWY" firstAttribute="top" secondItem="ZYw-rr-lnO" secondAttribute="topMargin" id="Xuc-OZ-KQh"/>
<constraint firstItem="Bex-xS-WMG" firstAttribute="trailing" secondItem="ZYw-rr-lnO" secondAttribute="trailingMargin" id="Ypd-BU-eNg"/>
<constraint firstItem="Bex-xS-WMG" firstAttribute="leading" secondItem="pCr-bW-XWY" secondAttribute="trailing" constant="12" id="ihY-xL-anh"/>
<constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="pCr-bW-XWY" secondAttribute="bottom" id="iu3-Sw-yey"/>
<constraint firstItem="pCr-bW-XWY" firstAttribute="leading" secondItem="ZYw-rr-lnO" secondAttribute="leadingMargin" id="o85-kl-ee4"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="scrollingHeightConstraint" destination="jBQ-hv-SBY" id="AQQ-LK-f5R"/>
<outlet property="theButton" destination="Bex-xS-WMG" id="Dpg-wY-06G"/>
<outlet property="theTextView" destination="pCr-bW-XWY" id="vzw-GP-Eav"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="VjW-oA-FRf" id="g9q-N9-GPB"/>
<outlet property="delegate" destination="VjW-oA-FRf" id="9Ff-xj-zPJ"/>
</connections>
</tableView>
<navigationItem key="navigationItem" id="ztb-ky-WPU"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="5rS-VR-5Ht" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-124" y="1613"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
样本输出:
推荐阅读
- javascript - 如何在提交时使用 emailjs 将表单信息发送到我的电子邮件?
- reactjs - React 应用程序未安装在我的系统上
- python - 如何使用窗口函数使用动态选择查询来选择列
- amazon-web-services - S3 公共 URL 只能从 VPC 内的实例访问
- reactjs - 显示绝对页码
- c++ - 默认 Visual Studio C++ 控制台应用程序有 409 错误
- javascript - 从 API 获取数据并将字符串转换为数字
- python - 替换列数据框python中的数值
- vue.js - 将 Agora.io 与 nuxt.js 集成时出错 创建钩子时出错:“ReferenceError: AgoraRTC is not defined”
- javascript - 如何以 3 个不同的百分比为剑道甘特图任务的背景着色