带有动态页脚的 UIScrollView

UIScrollView with dynamic Footer

我在 ScrollView 中有一个 Label 和一个 ContainerView(带有一个 Label 和 Switch 作为孩子)。 我希望我的 ContainerView 在标签底部有一个填充,但是如果标签 高度太大(需要滚动)页脚应该贴在屏幕底部(不应该滚动)。是否可以使用 AutoLayout 来实现?

我在之前的项目中做过,这是代码

@IBOutlet weak var scrollViewHeightConstraints: NSLayoutConstraint!
override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        let labelHeight = self.textLabel.frame.size.height + 60
        let screenHeight = UIScreen.main.bounds.size.height - 100
        self.scrollViewHeightConstraints.constant = min(screenHeight, labelHeight)
    }

常量 60 和 100 可以根据您的看法进行更改。

有几种方法可以做到这一点...

我建议您将“切换容器视图”放在滚动视图之外,固定到滚动视图的底部.当滚动视图的内容(多行标签)改变高度时,我们将改变滚动视图的高度--直到切换容器到达视图底部(安全区底部)。

这是它在 Storyboard 中的样子:

多行标签为青色背景,切换容器视图为黄色背景。

请注意,我们已将滚动视图的高度限制为多行标签的高度,但将优先级设置为750 (High)。通过将其从 Required 更改为 High,我们告诉自动布局在需要时打破该约束。也就是说,当可用 space.

变得太高时

另请注意,开关容器视图的底部约束设置为 >= 0。当我们向多行标签添加文本时,它会导致滚动视图的高度增加将容器视图向下推 ...但只会直到它到达视图的底部(安全区)。

我们在这里唯一看不到的 - 但它很重要 - 是我们将多行标签和开关的 Vertical Content Compression Resistance 设置为 1000 (Required)

这是故事板的来源,您可以查看它:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="XCt-Hc-wQm">
    <device id="retina4_7" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="System colors in document resources" minToolsVersion="11.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--Scroll Footer View Controller-->
        <scene sceneID="Caf-Eb-sIu">
            <objects>
                <viewController id="XCt-Hc-wQm" customClass="ScrollFooterViewController" customModule="DelMe" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="dUm-Xz-ZMW">
                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="USX-Ck-yG1">
                                <rect key="frame" x="0.0" y="0.0" width="375" height="21.5"/>
                                <subviews>
                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lwg-yg-39D">
                                        <rect key="frame" x="0.0" y="0.0" width="375" height="21.5"/>
                                        <color key="backgroundColor" red="0.45138680930000002" green="0.99309605359999997" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="18"/>
                                        <nil key="textColor"/>
                                        <nil key="highlightedColor"/>
                                    </label>
                                </subviews>
                                <color key="backgroundColor" red="1" green="0.14913141730000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <constraints>
                                    <constraint firstAttribute="height" secondItem="lwg-yg-39D" secondAttribute="height" priority="750" id="76J-30-uTn"/>
                                    <constraint firstItem="lwg-yg-39D" firstAttribute="trailing" secondItem="ZIz-K9-t2b" secondAttribute="trailing" id="GCE-Kl-AnT"/>
                                    <constraint firstItem="lwg-yg-39D" firstAttribute="leading" secondItem="ZIz-K9-t2b" secondAttribute="leading" id="KKY-k0-5P6"/>
                                    <constraint firstItem="lwg-yg-39D" firstAttribute="top" secondItem="ZIz-K9-t2b" secondAttribute="top" id="MY4-LC-VHJ"/>
                                    <constraint firstItem="lwg-yg-39D" firstAttribute="bottom" secondItem="ZIz-K9-t2b" secondAttribute="bottom" id="dnk-39-RlU"/>
                                    <constraint firstItem="lwg-yg-39D" firstAttribute="width" secondItem="PfO-IK-xkv" secondAttribute="width" id="zrQ-RH-rly"/>
                                </constraints>
                                <viewLayoutGuide key="contentLayoutGuide" id="ZIz-K9-t2b"/>
                                <viewLayoutGuide key="frameLayoutGuide" id="PfO-IK-xkv"/>
                            </scrollView>
                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Njo-Q9-KiY" userLabel="ContainerView">
                                <rect key="frame" x="0.0" y="21.5" width="375" height="47"/>
                                <subviews>
                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vUU-Ps-Crk" userLabel="SwitchLabel">
                                        <rect key="frame" x="8" y="13" width="42" height="21"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                        <nil key="textColor"/>
                                        <nil key="highlightedColor"/>
                                    </label>
                                    <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="24L-2e-H7p">
                                        <rect key="frame" x="318" y="8" width="51" height="31"/>
                                    </switch>
                                </subviews>
                                <color key="backgroundColor" red="0.99953407049999998" green="0.98835557699999999" blue="0.47265523669999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <constraints>
                                    <constraint firstItem="vUU-Ps-Crk" firstAttribute="centerY" secondItem="24L-2e-H7p" secondAttribute="centerY" id="Dwk-0c-XGq"/>
                                    <constraint firstAttribute="trailing" secondItem="24L-2e-H7p" secondAttribute="trailing" constant="8" id="Vuj-Vf-Ubm"/>
                                    <constraint firstItem="vUU-Ps-Crk" firstAttribute="leading" secondItem="Njo-Q9-KiY" secondAttribute="leading" constant="8" id="VzC-He-6eA"/>
                                    <constraint firstItem="24L-2e-H7p" firstAttribute="top" secondItem="Njo-Q9-KiY" secondAttribute="top" constant="8" id="xVS-9o-J0n"/>
                                    <constraint firstAttribute="bottom" secondItem="24L-2e-H7p" secondAttribute="bottom" constant="8" id="yR9-XP-FQq"/>
                                </constraints>
                            </view>
                        </subviews>
                        <viewLayoutGuide key="safeArea" id="y57-Pg-ijO"/>
                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                        <constraints>
                            <constraint firstItem="y57-Pg-ijO" firstAttribute="trailing" secondItem="Njo-Q9-KiY" secondAttribute="trailing" id="7PH-bB-Xu1"/>
                            <constraint firstItem="USX-Ck-yG1" firstAttribute="top" secondItem="y57-Pg-ijO" secondAttribute="top" id="RRh-cW-cdn"/>
                            <constraint firstItem="y57-Pg-ijO" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="Njo-Q9-KiY" secondAttribute="bottom" id="ZhG-3Y-WiX"/>
                            <constraint firstItem="Njo-Q9-KiY" firstAttribute="leading" secondItem="y57-Pg-ijO" secondAttribute="leading" id="aj2-fS-rab"/>
                            <constraint firstItem="Njo-Q9-KiY" firstAttribute="top" secondItem="USX-Ck-yG1" secondAttribute="bottom" id="eeo-Zi-yar"/>
                            <constraint firstItem="USX-Ck-yG1" firstAttribute="trailing" secondItem="y57-Pg-ijO" secondAttribute="trailing" id="iK0-ax-YU6"/>
                            <constraint firstItem="USX-Ck-yG1" firstAttribute="leading" secondItem="y57-Pg-ijO" secondAttribute="leading" id="lMU-A2-QsG"/>
                        </constraints>
                    </view>
                    <connections>
                        <outlet property="multiLineLabel" destination="lwg-yg-39D" id="huD-0B-amT"/>
                        <outlet property="scrollView" destination="USX-Ck-yG1" id="2c5-7h-7Ii"/>
                    </connections>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="cjI-ra-ksU" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="1586.4000000000001" y="266.71664167916043"/>
        </scene>
    </scenes>
    <resources>
        <systemColor name="systemBackgroundColor">
            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
        </systemColor>
    </resources>
</document>

和一些示例代码来演示:

class ScrollFooterViewController: UIViewController {
    
    @IBOutlet var scrollView: UIScrollView!
    @IBOutlet var multiLineLabel: UILabel!
    
    var numLines: Int = 0
    var addLine: Bool = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add a tap recognizer...
        //  each time we tap the view, we'll add text from the sample text array
        //  until we reach the end of the array
        //  then, each tap will remove text until we reach 1
        let t = UITapGestureRecognizer(target: self, action: #selector(self.updateMultilineLabel))
        view.addGestureRecognizer(t)
        
        // init the multiline label text
        updateMultilineLabel()
    }
    
    @objc func updateMultilineLabel() -> Void {
        // are we adding or removing text?
        if addLine == true {
            numLines += 1
        } else {
            numLines -= 1
            // keep at least 1 line of text
            numLines = max(numLines, 1)
        }
        
        // generate string with numLines of text from sample text array
        let s = sampleText[0..<numLines].joined(separator: "\n\n")
        
        // set text of multiline label
        multiLineLabel.text = s
        
        // auto-scroll to the bottom
        scrollToBottom(scrollView)
        
        // if we're at the end of the sample text array, switch from adding to removing
        if numLines == sampleText.count {
            addLine = false
        }
        // if we're at 1, switch from removing to adding
        if numLines == 1 {
            addLine = true
        }
    }
    
    func scrollToBottom(_ scrollView: UIScrollView) -> Void {
        // set content offset after 0.01 seconds (to make sure it runs after the label has updated its frame)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01, execute: {
            let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom)
            if(bottomOffset.y > 0) {
                scrollView.setContentOffset(bottomOffset, animated: true)
            }
        })
    }
    
    let sampleText: [String] = [
        "UILabel\n\nA label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.",
        "UIButton\n\nYou can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state.",
        "UISegmentedControl\n\nThe segments can represent single or multiple selection, or a list of commands.\n\nEach segment can display text or an image, but not both.",
        "UITextField\n\nDisplays a rounded rectangle that can contain editable text. When a user taps a text field, a keyboard appears; when a user taps Return in the keyboard, the keyboard disappears and the text field can handle the input in an application-specific way. UITextField supports overlay views to display additional information, such as a bookmarks icon. UITextField also provides a clear text control a user taps to erase the contents of the text field.",
        "UISlider\n\nUISlider displays a horizontal bar, called a track, that represents a range of values. The current value is shown by the position of an indicator, or thumb. A user selects a value by sliding the thumb along the track. You can customize the appearance of both the track and the thumb.",
        "UISwitch\n\nDisplays an element that shows the user the boolean state of a given value.  By tapping the control, the state can be toggled.",
    ]
    
}

该代码有一个“示例文本”数组...每次点击屏幕时,都会向多行标签添加更多文本,直到我们到达数组的末尾,此时每次点击将删除一些文本。

结果如下所示:

多一点文字,但不足以需要滚动:

以及我们需要滚动的足够文本: