Search code examples
swiftanimationuistackview

UIStackView show/hide animation


In stack view I have UIPickerView and I want to collapse and expand it on button taps. I want to use a simple animation but don't know how to achieve it, I have tried many ways but none of it lead to the correct appearance I always get to this

iOS 10 animation

I want to make the picker also collapse which it does not. It only just disappears after the animation which does not look nice.

my code where self is the UIStackView:

UIView.animate(withDuration: 0.3, animations: { [unowned self] in
        self.picker.isHidden = !open
        self.layoutIfNeeded()
    })

Solution

  • Stack view's automatic show/hide animation works great --- for some things. For others, such as with a Picker View, not so much (as you've seen).

    One approach would be:

    • embed the picker view in a regular view
    • constrain it centered vertically
    • add a default height to the containing view (such as slightly taller than the picker view)
    • animate the view's height constraint

    Picker views will not "squeeze" on their own though, so you'll get a "disappearing" picker view. If you want it to "squeeze" as it animates, you'll also need to animate its transform

    Here is an example (I use contrasting colors to make it easy to see elements, and I've slowed the animation duration to make it obvious):

    enter image description here

    Here is sample code:

    class StackDemoViewController: UIViewController {
    
        @IBOutlet var pickerHolderView: UIView!
        @IBOutlet var pickerHolderHeightConstraint: NSLayoutConstraint!
    
        @IBOutlet var normalButton: UIButton!
        @IBOutlet var squeezeButton: UIButton!
    
        @IBOutlet var thePickerView: UIDatePicker!
    
        // this will be assigned in viewDidLoad
        var defaultPickerHolderViewHeight: CGFloat = 0.0
    
        // anim duration - change to something like 1.0 to see the effect in "slo-motion"
        let animDuration = 0.3
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // get the original picker holder view height constant
            defaultPickerHolderViewHeight = pickerHolderHeightConstraint.constant
        }
    
        @IBAction func normalAnim(_ sender: Any) {
    
            // local bool
            let bIsHidden = pickerHolderView.isHidden
    
            // if the picker holder view is currently hidden, show it
            if bIsHidden {
                pickerHolderView.isHidden = false
            }
    
            // if picker holder height constant is > 0 (it's open / showing)
            //      set it to 0
            // else
            //      set it to defaultPickerHolderViewHeight
            self.pickerHolderHeightConstraint.constant = self.pickerHolderHeightConstraint.constant > 0 ? 0 : defaultPickerHolderViewHeight
    
            // animate the change
            UIView.animate(withDuration: animDuration, animations: {
                self.view.layoutIfNeeded()
            }) { finished in
                // if the picker holder view was showing (NOT hidden)
                //  hide it
                if !bIsHidden {
                    self.pickerHolderView.isHidden = true
                    // disable squeeze button until view is showing again
                    self.squeezeButton.isEnabled = false
                } else {
                    // re-enable squeeze button
                    self.squeezeButton.isEnabled = true
                }
            }
        }
    
        @IBAction func squeezeAnim(_ sender: Any) {
    
            // local bool
            let bIsHidden = pickerHolderView.isHidden
    
            var t = CGAffineTransform.identity
    
            // if the picker holder view is currently hidden, show it
            if bIsHidden {
                pickerHolderView.isHidden = false
            } else {
                // we're going to hide it
                t = CGAffineTransform(scaleX: 1.0, y: 0.01)
            }
    
            // if picker holder height constant is > 0 (it's open / showing)
            //      set it to 0
            // else
            //      set it to defaultPickerHolderViewHeight
            self.pickerHolderHeightConstraint.constant = self.pickerHolderHeightConstraint.constant > 0 ? 0 : defaultPickerHolderViewHeight
    
            // animate the change
            UIView.animate(withDuration: animDuration, animations: {
                self.thePickerView.transform = t
                self.view.layoutIfNeeded()
            }) { finished in
                // if the picker holder view was showing (NOT hidden)
                //  hide it
                if !bIsHidden {
                    self.pickerHolderView.isHidden = true
                    // disable normal button until view is showing again
                    self.normalButton.isEnabled = false
                } else {
                    // re-enable normal button
                    self.normalButton.isEnabled = true
                }
            }
        }
    
    }
    

    Using this layout:

    enter image description here

    and, here is the source of the Storyboard (so you can quickly try it out yourself):

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Zg0-f1-bBK">
        <device id="retina4_7" orientation="portrait">
            <adaptation id="fullscreen"/>
        </device>
        <dependencies>
            <deployment identifier="iOS"/>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <scenes>
            <!--Stack Demo View Controller-->
            <scene sceneID="Itw-fL-6gO">
                <objects>
                    <viewController id="Zg0-f1-bBK" customClass="StackDemoViewController" customModule="TranslateTest" customModuleProvider="target" sceneMemberID="viewController">
                        <view key="view" contentMode="scaleToFill" id="rze-A8-JnC">
                            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                            <subviews>
                                <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="vDP-gh-oah">
                                    <rect key="frame" x="8" y="120" width="359" height="338"/>
                                    <subviews>
                                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="clh-vv-1e4">
                                            <rect key="frame" x="0.0" y="0.0" width="359" height="50"/>
                                            <subviews>
                                                <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="VMQ-JX-yNt">
                                                    <rect key="frame" x="8" y="8" width="343" height="34"/>
                                                    <subviews>
                                                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Zb9-rN-qPb">
                                                            <rect key="frame" x="0.0" y="0.0" width="163.5" height="34"/>
                                                            <color key="backgroundColor" red="0.99806135890000003" green="0.96808904409999996" blue="0.12760734560000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                            <state key="normal" title="Normal"/>
                                                            <connections>
                                                                <action selector="normalAnim:" destination="Zg0-f1-bBK" eventType="touchUpInside" id="zwU-Bs-ZlI"/>
                                                            </connections>
                                                        </button>
                                                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="v2b-2E-upp">
                                                            <rect key="frame" x="179.5" y="0.0" width="163.5" height="34"/>
                                                            <color key="backgroundColor" red="0.99806135890000003" green="0.96808904409999996" blue="0.12760734560000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                            <state key="normal" title="With Squeeze"/>
                                                            <connections>
                                                                <action selector="squeezeAnim:" destination="Zg0-f1-bBK" eventType="touchUpInside" id="ARc-fQ-XRE"/>
                                                            </connections>
                                                        </button>
                                                    </subviews>
                                                </stackView>
                                            </subviews>
                                            <color key="backgroundColor" red="1" green="0.14913141730000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                            <constraints>
                                                <constraint firstAttribute="trailing" secondItem="VMQ-JX-yNt" secondAttribute="trailing" constant="8" id="T0v-du-5Aj"/>
                                                <constraint firstItem="VMQ-JX-yNt" firstAttribute="top" secondItem="clh-vv-1e4" secondAttribute="top" constant="8" id="Y2j-KP-ylE"/>
                                                <constraint firstItem="VMQ-JX-yNt" firstAttribute="leading" secondItem="clh-vv-1e4" secondAttribute="leading" constant="8" id="mKK-5Q-IhS"/>
                                                <constraint firstAttribute="bottom" secondItem="VMQ-JX-yNt" secondAttribute="bottom" constant="8" id="uJf-Y8-Uun"/>
                                            </constraints>
                                        </view>
                                        <view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6L1-Bv-SxB">
                                            <rect key="frame" x="0.0" y="58" width="359" height="232"/>
                                            <subviews>
                                                <datePicker contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" datePickerMode="dateAndTime" minuteInterval="1" translatesAutoresizingMaskIntoConstraints="NO" id="0A6-0Z-m7u">
                                                    <rect key="frame" x="8" y="8" width="343" height="216"/>
                                                    <color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                    <date key="date" timeIntervalSinceReferenceDate="590598642.83352995">
                                                        <!--2019-09-19 15:10:42 +0000-->
                                                    </date>
                                                </datePicker>
                                            </subviews>
                                            <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                            <constraints>
                                                <constraint firstItem="0A6-0Z-m7u" firstAttribute="centerY" secondItem="6L1-Bv-SxB" secondAttribute="centerY" id="Eqi-Od-JBH"/>
                                                <constraint firstItem="0A6-0Z-m7u" firstAttribute="leading" secondItem="6L1-Bv-SxB" secondAttribute="leading" constant="8" id="IEp-7K-buG"/>
                                                <constraint firstAttribute="height" constant="232" id="e1y-wA-jqj"/>
                                                <constraint firstAttribute="trailing" secondItem="0A6-0Z-m7u" secondAttribute="trailing" constant="8" id="hLe-WM-Qnx"/>
                                            </constraints>
                                        </view>
                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Standard UILabel" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="X5m-RD-zx4">
                                            <rect key="frame" x="0.0" y="298" width="359" height="40"/>
                                            <color key="backgroundColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                            <constraints>
                                                <constraint firstAttribute="height" constant="40" id="4c2-X0-9Kb"/>
                                            </constraints>
                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                            <nil key="textColor"/>
                                            <nil key="highlightedColor"/>
                                        </label>
                                    </subviews>
                                </stackView>
                            </subviews>
                            <color key="backgroundColor" red="0.52747867609999999" green="1" blue="0.55622484120000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                            <constraints>
                                <constraint firstItem="k9S-Qf-yG1" firstAttribute="trailing" secondItem="vDP-gh-oah" secondAttribute="trailing" constant="8" id="5C9-Ef-syQ"/>
                                <constraint firstItem="vDP-gh-oah" firstAttribute="top" secondItem="k9S-Qf-yG1" secondAttribute="top" constant="100" id="cuG-HE-aDz"/>
                                <constraint firstItem="vDP-gh-oah" firstAttribute="leading" secondItem="rze-A8-JnC" secondAttribute="leading" constant="8" id="f5f-qW-BJ2"/>
                            </constraints>
                            <viewLayoutGuide key="safeArea" id="k9S-Qf-yG1"/>
                        </view>
                        <connections>
                            <outlet property="normalButton" destination="Zb9-rN-qPb" id="0sr-a2-wa9"/>
                            <outlet property="pickerHolderHeightConstraint" destination="e1y-wA-jqj" id="t7m-zQ-RwA"/>
                            <outlet property="pickerHolderView" destination="6L1-Bv-SxB" id="hkf-zy-GIS"/>
                            <outlet property="squeezeButton" destination="v2b-2E-upp" id="fFe-hm-qzd"/>
                            <outlet property="thePickerView" destination="0A6-0Z-m7u" id="ubt-fR-mx9"/>
                        </connections>
                    </viewController>
                    <placeholder placeholderIdentifier="IBFirstResponder" id="e1N-yd-USh" userLabel="First Responder" sceneMemberID="firstResponder"/>
                </objects>
                <point key="canvasLocation" x="2244" y="126.38680659670166"/>
            </scene>
        </scenes>
    </document>