Hide view item of NSStackView with animation - swift

I working with swift 4 for macOS and I would like to hide an stack view item with animation.
I tried this:
class ViewController: NSViewController {
#IBOutlet weak var box: NSBox!
#IBOutlet weak var stack: NSStackView!
var x = 0
#IBAction func action(_ sender: Any) {
if x == 0 {
NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true
self.stack.arrangedSubviews.last!.isHidden = true
self.view.layoutSubtreeIfNeeded()
x = 1
}, completionHandler: nil)
} else {
NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true
self.stack.arrangedSubviews.last!.isHidden = false
self.view.layoutSubtreeIfNeeded()
x = 0
}, completionHandler: nil)
}
}
}
The result will be:
It works!
But I am not happy with the animation style.
My wish is:
I press the button, the red view will be smaller to the right side
I press the button, the red view will be larger to the left side.
Like a sidebar or if you have an splitview controller and you will do splitviewItem.animator().isCollapsed = true
this animation of show/hide is perfect.
Is this wish possible?
UPDATE
self.stack.arrangedSubviews.last!.animator().frame = NSZeroRect
UPDATE 2
self.stack.arrangedSubviews.last!.animator().frame = NSRect(x: self.stack.arrangedSubviews.last!.frame.origin.x, y: self.stack.arrangedSubviews.last!.frame.origin.y, width: 0, height: self.stack.arrangedSubviews.last!.frame.size.height)

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

Have a look at Organize Your User Interface with a Stack View sample code.
I disliked the heightContraint on the view controller and the height calculation in the -viewDidLoad. The code below calculation the size before animating and only adds the contrain while animating.
Breakable Contraint
First you need to set the priority of the contraint to something lower than 1000 (required constraint) for the direction you want the animation. Here the bottom contraint.
While we animate the view in and out, we will add contraint to do so with a priority of 1000 (required constraint).
Code
#interface NSStackView (LOAnimation)
- (void)lo_toggleArrangedSubview:(NSView *)view;
#end
#implementation NSStackView (LOAnimation)
- (void)lo_toggleArrangedSubview:(NSView *)view
{
NSAssert([self detachesHiddenViews], #"toggleArrangedSubview requires detachesHiddenViews YES");
NSAssert([[self arrangedSubviews] containsObject:view], #"view not an arrangedSubview");
CGFloat postAnimationHeight = 0;
NSLayoutConstraint *animationContraint = nil;
if (view.hidden) {
view.hidden = NO; // NSStackView will re-add view to its subviews
[self layoutSubtreeIfNeeded]; // calucalte view size
postAnimationHeight = view.bounds.size.height;
animationContraint = [view.heightAnchor constraintEqualToConstant:0];
animationContraint.active = YES;
} else {
[self layoutSubtreeIfNeeded]; // calucalte view size
animationContraint = [view.heightAnchor constraintEqualToConstant:view.bounds.size.height];
animationContraint.active = YES;
}
[self layoutSubtreeIfNeeded]; // layout with animationContraint in place
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animationContraint.animator.constant = postAnimationHeight;
[self layoutSubtreeIfNeeded];
} completionHandler:^{
view.animator.hidden = (postAnimationHeight == 0);
[view removeConstraint:animationContraint];
}];
}
#end

Related

change UIPicker height programmatically

I would like to modify the height of UIPicker programmatically, I tried to do this but on the simulator the height remains as it was before, I don't see any changes.
#objc open func showPicker(title: String?, selected: String?, strings:[String], color: UIColor? = nil, completion:DPPickerValueIndexCompletion?) {
self.pickerValues = strings
let picker = UIPickerView()
picker.delegate = self
picker.dataSource = self
picker.transform = CGAffineTransformMakeScale(0.5, 0.5)
if let value = selected {
picker.reloadAllComponents()
if strings.count > 0 {
OperationQueue.current?.addOperation {
let index = strings.firstIndex(of: value) ?? 0
picker.selectRow(index, inComponent: 0, animated: false)
}
}
}
self.showPicker(title: title, view: picker, color: color) { (cancel) in
var index = -1
var value: String? = nil
if !cancel, strings.count > 0 {
index = picker.selectedRow(inComponent: 0)
if index >= 0 {
value = self.pickerValues?[index]
}
}
completion?(value, index, cancel || index < 0)
}
}
I don't why it's not resizing itself properly when you translate autoresizing mask into constraints, but it works properly when you use AutoLayout:
let pickerView = UIPickerView()
pickerView.translatesAutoresizingMaskIntoConstraints = false
pickerView.delegate = self
pickerView.dataSource = self
view.addSubview(pickerView)
NSLayoutConstraint.activate([
pickerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pickerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pickerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
pickerView.heightAnchor.constraint(equalToConstant: 500)
])

Trying to resize an UIView when text in a label is hidden

#IBOutlet weak var CompanyDetailsBottom: NSLayoutConstraint!
#IBOutlet weak var CompanyDetailsView: UIView!
#IBAction func toggleCollapisbleView(_ sender: UIButton) {
if(CompanyDetailsView.isHidden){
var _CompanyHeight: CGFloat {
get {
return CompanyDetailsBottom.constant.magnitude
}
set {
CompanyDetailsBottom.constant.magnitude = 0
}
}
CompanyDetailsView.isHidden = false
}else {
CompanyDetailsView.isHidden = true
}
}
}
I am trying to resize a view if the label is hidden, I get Cannot assign to property: 'magnitude' is a get-only property as an error.
What you can do, if you don't want to use constraints is to embed your CompanyDetailsView inside a vertical UIStackView and then when you hide it, the height of the stack view will automatically be 0.
Is the UILabel embed in the UIView ? If so you could change the UIView to be a UIStackView instead so the height will be 0 if the label is hidden
You can use the extension below to hide a view and its parent will adjust its height
extension UIView {
var isGone: Bool {
get {
return isHidden
}
set {
let constraints = self.constraints.filter({ $0.firstAttribute == .height && $0.constant == 0 && $0.secondItem == nil && ($0.firstItem as? UIView) == self })
self.isHidden = newValue
if newValue {
if let constraint = constraints.first {
constraint.isActive = true
} else {
let constraint = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 0)
// constraint.priority = UILayoutPriority(rawValue: 999)
self.addConstraint(constraint)
constraint.isActive = true
}
self.setNeedsLayout()
self.setNeedsUpdateConstraints()
} else {
constraints.first?.isActive = false
}
}
}
}
And all the constraint priorities of the child views must be less than 1000 (999 max)
Using:
childView.isGone = true

why NSLayoutConstraint animation works only one time?

in my project I have a someView(of type UIView), that inside holderView(of type UIView).
someView have 2 state.
in state 1 the someView become large and in the step 2 the someView become small.
when some condition where right, someView show in state 1 and when it's not someView show in state 2.
I want to do this with animation and I use NSLayoutConstraint in my project. I'm using this codes(someViewHeight and someViewWidth are of type NSLayoutConstraint):
func minimizeSomeView() {
someViewHeight.constant = holderView.frame.width
someViewWidth.constant = holderView.frame.height
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func maximizeSomeView() {
someViewHeight.constant = holderView.frame.width/4
someViewWidth.constant = holderView.frame.height/4
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
if someTextField.text != nil {
minimizeSomeView()
} else. {
maximizeSomeView()
}
I define someViewHeight and someViewWidth inside viewDidLayoutSubviews(), this is my codes:
class ViewController: UIViewController {
private var holderView, someView: UIView!
private var someViewHeight,someViewWidth: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
super.viewDidLoad()
view.backgroundColor = .white
// Holder View
holderView = UIView()
holderView.backgroundColor = .red
holderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holderView)
NSLayoutConstraint.activate([
holderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
holderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
holderView.topAnchor.constraint(equalTo: view.topAnchor, constant: 120),
holderView.heightAnchor.constraint(equalToConstant: view.frame.height * 40 / 100)
])
// Some View
someView = UIView()
someView.backgroundColor = .blue
someView.translatesAutoresizingMaskIntoConstraints = false
holderView.addSubview(someView)
someView.topAnchor.constraint(equalTo: holderView.topAnchor).isActive = true
someView.trailingAnchor.constraint(equalTo: holderView.trailingAnchor).isActive = true
// Some TextField
let someTextField = UITextField()
someTextField.backgroundColor = .yellow
someTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(someTextField)
NSLayoutConstraint.activate([
someTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
someTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
someTextField.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -100),
someTextField.heightAnchor.constraint(equalToConstant: 50)
])
someTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
}
override func viewDidLayoutSubviews() {
someViewHeight = someView.heightAnchor.constraint(equalToConstant: holderView.frame.width)
someViewHeight.isActive = true
someViewWidth = someView.widthAnchor.constraint(equalToConstant: holderView.frame.width)
someViewWidth.isActive = true
}
func minimizeSomeView() {
someViewHeight.constant = holderView.frame.width/4
someViewWidth.constant = holderView.frame.height/4
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func maximizeSomeView() {
someViewHeight.constant = holderView.frame.width
someViewWidth.constant = holderView.frame.height
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
#objc func textFieldDidChange(_ textfield: UITextField) {
if textfield.text!.count > 0 {
self.minimizeSomeView()
} else {
self.maximizeSomeView()
}
}
}
You should not update the constraints in viewDidLayoutSubviews, specifically when you are using auto layout. You don't need this block
When you add constraint you can initialize and make them active.
Here we can specify the constraint in proportion. i.e Height of someView is 0.4 of the holderView. Similarly, Width of someView is 0.4 of the holderView.
To fix the issue , you can perform below changes
// Your some View Constraints
someView.topAnchor.constraint(equalTo: holderView.topAnchor).isActive = true
someView.trailingAnchor.constraint(equalTo: holderView.trailingAnchor).isActive = true
maximizeSomeView()
Remove the viewDidLayouSubView Code
Update your maximize and minimize events. Here I have to remove the existing constraints as .multiplier is a readonly property.
func minimizeSomeView() {
removeExistingConstriant()
UIView.animate(withDuration: 1.0) { [unowned self] in
self.someViewWidth = self.someView.widthAnchor.constraint(equalTo: self.holderView.widthAnchor, multiplier: 1.0)
self.someViewWidth.isActive = true
self.someViewHeight = self.someView.heightAnchor.constraint(equalTo:
self.holderView.heightAnchor, multiplier: 1.0)
self.someViewHeight.isActive = true
self.view.layoutIfNeeded()
}
}
func maximizeSomeView() {
removeExistingConstriant()
UIView.animate(withDuration: 1.0) { [unowned self] in
self.someViewWidth = self.someView.widthAnchor.constraint(equalTo: self.holderView.widthAnchor, multiplier: 0.4)
self.someViewWidth.isActive = true
self.someViewHeight = self.someView.heightAnchor.constraint(equalTo:
self.holderView.heightAnchor, multiplier: 0.4)
self.someViewHeight.isActive = true
self.view.layoutIfNeeded()
}
}
func removeExistingConstriant(){
if self.someViewHeight != nil {
self.someViewHeight.isActive = false
}
if self.someViewWidth != nil {
self.someViewWidth.isActive = false
}
}

Animate UIView position on tableview scroll and again tableview drag

I have a list of items which a header menu at the top.
When the user scrolls up the menu should scroll off screen also and when they return to the top of the list the menu should be visible.
In much the same way as a tableViewHeader behaves.
However, should the header be off screen and the user ends a drag down on the list, this header view should animate down from the top. If the user then ends a drag up on the list, the header should animate back off screen.
I have achieved the first part of this below, however am struggling to achieve the animation effect.
I had considered using 2 views for the menu, one that scrolls and one that animates it's position, however this feels off, I'm sure there must be a better way without duplicating views.
class TableViewScene: UIViewController {
let data = Array(0...99)
var headerViewOneTopConstraint: NSLayoutConstraint!
var tableViewTopConstraint: NSLayoutConstraint!
lazy var headerViewOne: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .purple
return view
}()
lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.delegate = self
view.dataSource = self
view.tableFooterView = .init()
view.refreshControl = .init()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.isTranslucent = false
headerViewOneTopConstraint = headerViewOne.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 184)
[headerViewOne, tableView].forEach(view.addSubview)
NSLayoutConstraint.activate([
headerViewOneTopConstraint,
headerViewOne.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerViewOne.trailingAnchor.constraint(equalTo: view.trailingAnchor),
headerViewOne.heightAnchor.constraint(equalToConstant: 184),
tableViewTopConstraint,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
extension TableViewScene: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
headerViewOneTopConstraint.constant = max(-184, min(0, -offsetY))
tableViewTopConstraint.constant = 184 - max(0, offsetY)
}
}
I had also tried adding this to the view
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if translation.y > 0 {
headerViewOne.transform = .init(translationX: 0, y: 184)
} else if translation.y < 0 {
headerViewOne.transform = .identity
}
}
This does not work and instead just renders a black space the view used to fill on scroll to the top.
It's hard to say exactly what you are looking for but this might work for you. In this implementation when the user drags up the header will hide and when the user drags down the header will show. This is true regardless of the scroll position.
I made a change to one of your constraints so that the top of the tableView is pinned to the bottom of the header:
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: headerViewOne.bottomAnchor)
I'm also tracking the state of the header like so:
enum HeaderState {
case hidden
case revealed
case hiding
case revealing
}
var headerState: HeaderState = .revealed
Now for the scrolling logic. If the scroll position is near the top then we want to manually show/hide the header. However, if the scroll position is near the bottom or center then we want to animate the show/hide functionality.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if scrollView.contentOffset.y < 184 {
if translation.y > 0 && headerState != .revealed && headerState != .revealing {
headerViewOneTopConstraint.constant = max(-184, min(0, -scrollView.contentOffset.y))
} else if translation.y < 0 {
headerViewOneTopConstraint.constant = max(-184, min(0, -scrollView.contentOffset.y))
}
return
}
if translation.y > 0 && headerState == .hidden {
self.headerState = .revealing
UIView.animate(withDuration: 0.4, animations: {
self.headerViewOneTopConstraint.constant = 0
self.view.layoutIfNeeded()
}, completion: { _ in
self.headerState = .revealed
})
} else if translation.y < 0 && headerState == .revealed {
self.headerState = .hiding
UIView.animate(withDuration: 0.4, animations: {
self.headerViewOneTopConstraint.constant = -184
self.view.layoutIfNeeded()
}, completion: { _ in
self.headerState = .hidden
})
}
}
Give this a shot and see if it meets your needs.

textview shakes when resizing view

I'm resizing the view that a textview belongs to and the text shakes when the view either gets bigger or gets smaller.
Declaration of said text view:
lazy var textview: UITextView = {
let textView = UITextView()
textView.text = ""
textView.font = .systemFont(ofSize: 12, weight: UIFontWeightMedium)
textView.isScrollEnabled = false
textView.isEditable = false
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textAlignment = .center
textView.textColor = .lightGray
textView.dataDetectorTypes = .link
return textView
}()
I'm resizing the view that it's in to fit the full screen like this
if let window = UIApplication.shared.keyWindow {
let statusBarHeight = UIApplication.shared.statusBarFrame.size.height
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveLinear, animations: {
self.frame = CGRect(x: 0, y: statusBarHeight, width: window.frame.width, height: window.frame.height - statusBarHeight)
self.layer.cornerRadius = 0
self.layoutIfNeeded()
}, completion: nil)
}
Upon doing so, the view expands perfectly but the textviews text does a bounce effect that makes the animation look extremely unprofessional... any advice?
Edit: It seems like when I remove the center text alignment option it works fine. How do I make it work with the text center aligned?
edit: I took another look at this and attempted to use the technique based in UIScrollView animation of height and contentOffset "jumps" content from bottom.
Here's a minimal working example with text view with centered text alignment which is working for me!
I'd recommend managing animations either to be all constraint based, or all frame based. I attempted a version where the animation is driven by updating the container view frame but it was starting to take too long to left it at this constraint based approach.
Hope this points you in the right direction :)
import UIKit
class ViewController: UIViewController {
lazy var textView: UITextView = {
let textView = UITextView()
textView.text = "testing text view"
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var widthConstraint: NSLayoutConstraint!
var topAnchor: NSLayoutConstraint!
override func viewDidLoad() {
view.backgroundColor = .groupTableViewBackground
// add container view and constraints
view.addSubview(containerView)
containerView.frame = view.bounds.insetBy(dx: 100, dy: 200)
containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 100).isActive = true
// keep reference to topAnchor and width as properties to animate
topAnchor = containerView.topAnchor.constraint(lessThanOrEqualTo: view.topAnchor, constant: 100)
widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 300)
topAnchor.isActive = true
widthConstraint.isActive = true
// add text view to container view and set constraints
containerView.addSubview(textView)
textView.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
textView.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
textView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
}
#IBAction func toggleResize(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
view.layoutIfNeeded()
widthConstraint.constant = sender.isSelected ? view.bounds.width : 300
topAnchor.constant = sender.isSelected ? 20 : 100
// caculate the textView content offset for starting position based on
// expected end position at end of the animation
let xOffset = (textView.bounds.width - widthConstraint.constant) / 2
textView.contentOffset = CGPoint(x: -xOffset, y: textView.contentOffset.y)
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded()
}
}
}