Hide View Item of Nsstackview with Animation

Hide view item of NSStackView with animation

I just create a simple testing code which can animate the red view, instead of using button, I just used touchup, please have a look at the code:

class ViewController: NSViewController {

let view1 = NSView()
let view2 = NSView()
let view3 = NSView()
var x = 0

var constraint: NSLayoutConstraint!

override func viewDidLoad() {
super.viewDidLoad()

// Do any additional setup after loading the view.
view1.wantsLayer = true
view2.wantsLayer = true
view3.wantsLayer = true

view1.layer?.backgroundColor = NSColor.orange.cgColor
view2.layer?.backgroundColor = NSColor.green.cgColor
view3.layer?.backgroundColor = NSColor.red.cgColor

view1.translatesAutoresizingMaskIntoConstraints = false
view2.translatesAutoresizingMaskIntoConstraints = false
view3.translatesAutoresizingMaskIntoConstraints = false

self.view.addSubview(view1)
self.view.addSubview(view2)
self.view.addSubview(view3)

self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view1]|", options: [], metrics: nil, views: ["view1": view1]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view2]|", options: [], metrics: nil, views: ["view2": view2]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view3]|", options: [], metrics: nil, views: ["view3": view3]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view1(==view2)][view2(==view1)][view3]|", options: [], metrics: nil, views: ["view1": view1, "view2": view2, "view3": view3]))

constraint = NSLayoutConstraint(item: view3, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
self.view.addConstraint(constraint)
}

override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}

override func mouseUp(with event: NSEvent) {
if x == 0 {

NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true

constraint.constant = 0
self.view.layoutSubtreeIfNeeded()
x = 1
}, completionHandler: nil)

} else {

NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true

constraint.constant = 100
self.view.layoutSubtreeIfNeeded()
x = 0
}, completionHandler: nil)

}
}
}

Sample Image

UIStackView show/hide animation

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):

Sample Image

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:

Sample Image

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>

Not all content is animated inside a stack view when hiding it

I kept doing some research on the subject, and it seems like most articles were suggesting that using stacks to perform animation would work fine. However I have also found that animations would only work with animatable properties, isHidden not being one of them.

In the end after some trial and errors I have found that isHidden can be animated with stack views, but you can expect children to misbehave. So far the only workaround I have found is like so:

        let duration = 0.5;
let delay = 0;
UIView.animate(withDuration: duration, delay: delay, animations: {
self.bottomStack.isHidden = self.hideBottomStack;
})
UIView.animate(withDuration: duration/2, delay: delay, animations: {
self.bottomStack.alpha = 0;
})

You'll note here that I basically "turn" the alpha property down to 0 in half the time I take to hide the stack. This has the effect to hide the text before it overlaps with the upper stack. Also note that I could also have decided to do something like this:

       UIView.animate(withDuration: duration, delay: delay, animations: {
self.bottomStack.alpha = 0;
}, completion: { (_) in
self.bottomStack.isHidden = true;
})

This would also hide the bottom stack, but you lose the hiding motion in favor of a fading motion and hide the stack once the fading is done.

Unhiding subviews in stack view causing weird animation

It turns out it was something to do with animating the update of layouts of views pinned to the edge of the UIStackView. Removing the animation make it work fine.

How to inject a NSStackView into the view hierarchy?

[_window setContentView:st] calls removeFromSuperview on the old content view and removeFromSuperview releases the view. [firstView removeFromSuperview] and [_window setContentView:st] will both release firstView.

Solution: replace [firstView removeFromSuperview] by [_window setContentView:nil].

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// I cannot modify this procedure:
[self createFirstView];

// I can modify that:
NSTextField *label2 = [NSTextField labelWithString:@"Second view."];

NSView *firstView = [_window contentView];
[_window setContentView:nil];
NSStackView *st = [NSStackView stackViewWithViews:@[firstView, label2]];
[_window setContentView:st];
}


Related Topics



Leave a reply



Submit