3

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()
    })
eja08
  • 4,600
  • 3
  • 13
  • 19
  • 1
    Dynamically adding/removing a row in a `UITableView` might be a better option than a `UIStackView`. – Sweeper Sep 19 '19 at 09:27
  • I thought that UIStackView was designed to dynamically change content on the go. – eja08 Sep 19 '19 at 09:35
  • No, that's one of the purposes of `UITableView`. `UIStackView` is used for easily laying out views horizontally or vertically in a straight line. – Sweeper Sep 19 '19 at 09:38

3 Answers3

11

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>
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thank you very much for your answer! It gave me an insight that I couldn't find anywhere on the internet! – eja08 Sep 20 '19 at 09:03
0

You can use usingSpringWithDamping along with the animation code

D. Schreier
  • 1,700
  • 1
  • 22
  • 34
Vishnu Hari
  • 111
  • 1
  • 10
0

Swift 5: Slide up animation

1- Set hight to 216 (PickerView Standard).

2- Set leading an trailing to safe Area.

3- Set Bottom to "SuperViewBottom" as -216.

4- Make an IBOutlet from line 3 as NSLayoutConstraint optional.

and then:

import UIKit

class ViewController: UIViewController {


@IBOutlet weak var bottom: NSLayoutConstraint!


 override func viewDidLoad() {

 super.viewDidLoad()

 bottom.constant = -216

}


-(IBAction)button:(id)sender
{
   UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations:
        {
            self.bottom.constant = 0
        self.view.layoutIfNeeded()
        }) { (AnimationComplete ) in }
}

 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
       

        UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations:
        {
            self.bottom.constant = -216
        self.view.layoutIfNeeded()
        }) { (AnimationComplete ) in }
   
    }


}

it should working like Apple standard animation.

Good luck