0

Currently, I have a layout design as following

[Safe Area]

    [Scroll View (In green color)]
        
        [Custom View (In red color)]
            
            [Horizontal Stack View]
                [Button 1]
                [Button 2]
            
            [Text View]

    [Bottom Toolbar]

It is designed so that, as the content of [Text View] grow, user can scroll vertically [Text View] together with the buttons group ([Horizontal Stack View])

It suppose to look as the following

enter image description here

We think we have used the correct constraints, between [Custom View] and [Scroll View]

Custom View.top = Content Layout Guide.top
Custom View.trailing = Content Layout Guide.trailing
Custom View.leading = Content Layout Guide.leading
Custom View.bottom = Content Layout Guide.bottom
Custom View.width = Frame Layout Guide.width

One warning we are getting from Xcode is

Scrollable content size is ambiguous for "Scroll View".

Hence, to avoid such, we need to add

Custom View.centerX = Frame Layout Guide.centerX
Custom View.centerY = Frame Layout Guide.centerY

However, when we execute the app, only [Text View] is vertically scrollable, when the text content grows. The top buttons group remains static.

We try to disable scrolling behaviour in [Text View] itself. Again, the entire page is not scrollable, when the text content grows.

Do you have any idea how to fix this, so that when text content grow, the top button group can scroll together with text view?

The demo is located at https://github.com/yccheok/ios-tutorial/tree/learn-scroll-view/NavigationController

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875

2 Answers2

1

Looks like you're close...

The UITextView definitely should have scrolling disabled. That will cause it to grow/shrink vertically as the user types.

The key point you're missing, I think, is a bottom constraint for your text view.

Here is how it should be laid out - I've used 8-pt "padding" for the elements:

enter image description here

Here's the source for the storyboard:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="22Q-Va-uW9">
    <device id="retina6_1" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
        <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>
        <!--Text View Scroll View Controller-->
        <scene sceneID="cwD-Ry-Rln">
            <objects>
                <viewController id="22Q-Va-uW9" customClass="TextViewScrollViewController" customModule="PanZoom" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="FK3-2k-kFr">
                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9i4-uC-ifa">
                                <rect key="frame" x="0.0" y="88" width="414" height="725"/>
                                <subviews>
                                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cmb-lb-s1V" userLabel="GreenView">
                                        <rect key="frame" x="0.0" y="0.0" width="414" height="220.5"/>
                                        <subviews>
                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="dtt-fC-qBS" userLabel="ButtonsStack">
                                                <rect key="frame" x="8" y="8" width="398" height="30"/>
                                                <subviews>
                                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GFD-bo-TQG">
                                                        <rect key="frame" x="0.0" y="0.0" width="195" height="30"/>
                                                        <color key="backgroundColor" red="0.83741801979999997" green="0.83743780850000005" blue="0.83742713930000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                        <state key="normal" title="Button 1"/>
                                                    </button>
                                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G0H-uo-Hhv">
                                                        <rect key="frame" x="203" y="0.0" width="195" height="30"/>
                                                        <color key="backgroundColor" red="0.83741801979999997" green="0.83743780850000005" blue="0.83742713930000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                        <state key="normal" title="Button 2"/>
                                                    </button>
                                                </subviews>
                                            </stackView>
                                            <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="31s-od-JSj">
                                                <rect key="frame" x="8" y="46" width="398" height="166.5"/>
                                                <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                                                <string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
                                                <color key="textColor" systemColor="labelColor"/>
                                                <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                                <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
                                            </textView>
                                        </subviews>
                                        <color key="backgroundColor" red="0.97629755740000002" green="0.25518852469999997" blue="0.1867151558" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                        <constraints>
                                            <constraint firstAttribute="bottom" secondItem="31s-od-JSj" secondAttribute="bottom" constant="8" id="48L-4C-2qT"/>
                                            <constraint firstAttribute="trailing" secondItem="31s-od-JSj" secondAttribute="trailing" constant="8" id="8Fk-Hs-oE5"/>
                                            <constraint firstItem="31s-od-JSj" firstAttribute="top" secondItem="dtt-fC-qBS" secondAttribute="bottom" constant="8" id="FBX-A2-9r1"/>
                                            <constraint firstItem="dtt-fC-qBS" firstAttribute="top" secondItem="cmb-lb-s1V" secondAttribute="top" constant="8" id="ReQ-rG-pxE"/>
                                            <constraint firstAttribute="trailing" secondItem="dtt-fC-qBS" secondAttribute="trailing" constant="8" id="TNF-MC-1Fo"/>
                                            <constraint firstItem="dtt-fC-qBS" firstAttribute="leading" secondItem="cmb-lb-s1V" secondAttribute="leading" constant="8" id="cHU-IF-eqx"/>
                                            <constraint firstItem="31s-od-JSj" firstAttribute="leading" secondItem="cmb-lb-s1V" secondAttribute="leading" constant="8" id="vzb-fw-4rS"/>
                                        </constraints>
                                    </view>
                                </subviews>
                                <color key="backgroundColor" red="0.045027168950000002" green="0.85423937179999998" blue="0.076285673880000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                                <constraints>
                                    <constraint firstItem="cmb-lb-s1V" firstAttribute="trailing" secondItem="vj7-x9-CG7" secondAttribute="trailing" id="Ify-cz-Rri"/>
                                    <constraint firstItem="cmb-lb-s1V" firstAttribute="leading" secondItem="vj7-x9-CG7" secondAttribute="leading" id="Sup-Q0-buq"/>
                                    <constraint firstItem="cmb-lb-s1V" firstAttribute="top" secondItem="vj7-x9-CG7" secondAttribute="top" id="anR-gY-fDu"/>
                                    <constraint firstItem="cmb-lb-s1V" firstAttribute="width" secondItem="n5I-JV-9MK" secondAttribute="width" id="qXA-46-Ghy"/>
                                    <constraint firstItem="cmb-lb-s1V" firstAttribute="bottom" secondItem="vj7-x9-CG7" secondAttribute="bottom" id="xfn-l1-QHb"/>
                                </constraints>
                                <viewLayoutGuide key="contentLayoutGuide" id="vj7-x9-CG7"/>
                                <viewLayoutGuide key="frameLayoutGuide" id="n5I-JV-9MK"/>
                            </scrollView>
                        </subviews>
                        <viewLayoutGuide key="safeArea" id="0PY-SX-dfw"/>
                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                        <constraints>
                            <constraint firstItem="9i4-uC-ifa" firstAttribute="leading" secondItem="0PY-SX-dfw" secondAttribute="leading" id="VfX-uv-BcR"/>
                            <constraint firstItem="9i4-uC-ifa" firstAttribute="top" secondItem="0PY-SX-dfw" secondAttribute="top" id="nrt-pL-ZXy"/>
                            <constraint firstItem="9i4-uC-ifa" firstAttribute="bottom" secondItem="0PY-SX-dfw" secondAttribute="bottom" id="tLV-GX-hce"/>
                            <constraint firstItem="9i4-uC-ifa" firstAttribute="trailing" secondItem="0PY-SX-dfw" secondAttribute="trailing" id="v8d-TH-4oS"/>
                        </constraints>
                    </view>
                    <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
                    <simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
                    <connections>
                        <outlet property="theScrollView" destination="9i4-uC-ifa" id="Wq7-j8-nud"/>
                    </connections>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="QQ0-6m-9TV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="331.8840579710145" y="162.72321428571428"/>
        </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>

and a sample view controller with keyboard handling:

class TextViewScrollViewController: UIViewController {

    @IBOutlet var theScrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // button to end editing
        let btn = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.doneTapped(_:)))
        self.navigationItem.rightBarButtonItem = btn
        
        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)

    }
    
    @objc func doneTapped(_ sender: Any?) -> Void {
        view.endEditing(true)
    }
    
    @objc func adjustForKeyboard(notification: Notification) {
        guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
        
        let keyboardScreenEndFrame = keyboardValue.cgRectValue
        let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
        
        if notification.name == UIResponder.keyboardWillHideNotification {
            theScrollView.contentInset = .zero
        } else {
            // bottom padding to keep textView above keyboard - adjust as desired
            let padding: CGFloat = 16
            theScrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - (view.safeAreaInsets.bottom - padding), right: 0)
        }
        
        theScrollView.scrollIndicatorInsets = theScrollView.contentInset
        
    }
    
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Hey don, I've followed your answer here mostly, but I have a few more questions. I'm going to make a question. Could I link it to you here and have your help? – Cody Sep 18 '22 at 03:24
  • Have you tested the above codes when pasting to the textView? – Cody Sep 18 '22 at 03:40
  • @code - yes, it should work fine when pasting... What are your other questions? – DonMag Sep 18 '22 at 12:45
  • if I paste from the bottom of the UITextView, it grows (I have a growing textView) and keyboard moves down accordingly. Great work and thank you... but, the issue is if I paste from a line that is not the most recent line. The textview grows but the keyboard doesn't move down to show both the cursor, text pasted and the bottom of the textView which is just above the keyboard all in the same view. – Cody Sep 20 '22 at 15:04
  • I think what I am trying to do might be unrealistic and if it is achievable on one screen won't be on another, but perhaps I'm being cynical having tried for 3-4 days now. – Cody Sep 20 '22 at 15:05
  • @code - so... if the user pastes text, you want the new caret position automatically scrolled into view? If so, you probably want to create a new post explaining exactly what you want to do, and showing what you've tried so far. – DonMag Sep 21 '22 at 14:56
0

Scroll in scroll always been a tricky thing to handle. My suggestions -

Don't use UITextView if it's not editable, use UILabel.

OR

Calculate height of text and make text view equal to it and disable scrolling. this way you will ultimately have only one scrollable view. Helpful link.

Blind Ninja
  • 1,063
  • 13
  • 28