首页 > 解决方案 > 通过点击按钮动态根据 UITextView 的 isScrollEnabled 展开和折叠 UITableView 行

问题描述

我已经实现了一个 UITableView。我在表格视图的单元格内容视图中有一个 UITextView 和 UIButton 。启用文本视图的滚动并给出高度约束。表格视图的行高是自动的,取决于文本视图的高度加上按钮的高度。

现在我要实现的是单击按钮切换文本视图的 isScrollEnabled 属性并删除高度约束。再次单击按钮时,切换文本视图的 isScrollEnabled 属性并添加高度约束。

在此过程中,文本视图不会在按钮单击时自动更改其框架(类似的事情可以通过使用 UILabel 代替 UITextView 并切换标签的行数属性而不是文本视图的启用滚动来实现),因此行高也不是更新。

预期结果是在第一次单击按钮时将文本视图的 isScrollEnabled 属性设置为 false 并删除文本视图的高度约束将使文本视图根据其包含的文本展开。在第二次单击按钮时,将文本视图的 isScrollEnabled 属性设置为 true 并添加文本视图的高度约束将使文本视图折叠到高度约束的常量值。因此,当我使用自动布局时,表格视图的行高也会发生变化。

标签: iosswift

解决方案


当我们动态更改单元格的高度时, 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>

样本输出:

在此处输入图像描述


推荐阅读