Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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()
    })
like image 484
eja08 Avatar asked Oct 27 '25 01:10

eja08


1 Answers

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>
like image 132
DonMag Avatar answered Oct 29 '25 19:10

DonMag