Animate UIView position on tableview scroll and again tableview drag - swift

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.

Related

add container to another container - problem with container.frame.origin

I am testing pan Gesture Recognizer.
I add a white Container to view and then add another red container to the white container.
When I am printing the whiteContainer.frame.orgin I get correct CGPoint numbers but when I print redContaner CGPoint numbers I get 0.0, 0.0
class ViewController6 : UIViewController {
let whiteContainer = UIView()
let redContainer = UIView()
var pointOrigin2: CGPoint?
var pointOrigin3: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
override func viewDidLayoutSubviews() {
pointOrigin2 = whiteContainer.frame.origin
print(pointOrigin2!)
pointOrigin3 = redContainer.frame.origin
print(pointOrigin3!)
}
private func setupView() {
view.backgroundColor = .black
configureContainer()
configureRedContainer()
addPanGestureRecognizare()
}
func configureContainer() {
view.addSubview(whiteContainer)
whiteContainer.translatesAutoresizingMaskIntoConstraints = false
whiteContainer.backgroundColor = .white
NSLayoutConstraint.activate([
whiteContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
whiteContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor),
whiteContainer.widthAnchor.constraint(equalToConstant: 200),
whiteContainer.heightAnchor.constraint(equalToConstant: 200)
])
}
func configureRedContainer() {
whiteContainer.addSubview(redContainer)
redContainer.translatesAutoresizingMaskIntoConstraints = false
redContainer.backgroundColor = .red
NSLayoutConstraint.activate([
redContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
redContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor),
redContainer.widthAnchor.constraint(equalToConstant: 50),
redContainer.heightAnchor.constraint(equalToConstant: 50)
])
}
When I am adding red Container to view (not whiteContainer.addSubview(red Container) ) I get the correct CGPoint number of the white and red container.
Why ?
Viewcontroller hasn't any calculation for the frames for subviews is not in own view if you add in viewDidLoad. If you add a subview to ViewController's view , then yes ViewController makes calculation for that view but if you add subview to view's subview , it doesnt. Thats why you can see the whiteContainer frame is not zero and redContainer is zero.
To make this you should say 'view , you should calculation it'. Thats why you must use view.layoutIfNeeded() after the constraints settings .
This gonna fix your problem
func configureRedContainer() {
whiteContainer.addSubview(redContainer)
redContainer.translatesAutoresizingMaskIntoConstraints = false
redContainer.backgroundColor = .red
NSLayoutConstraint.activate([
redContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
redContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor),
redContainer.widthAnchor.constraint(equalToConstant: 50),
redContainer.heightAnchor.constraint(equalToConstant: 50)
])
view.layoutIfNeeded() // must add after setting constraints
}
now my func PanGestute is working incorrectly. the condition "outside the white container" is triggered all the time
#OmerTekbiyik ?
func addPanGestureRecognizare() {
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture2(sender:)))
redContainer.addGestureRecognizer(pan)
}
#objc func handlePanGesture2(sender : UIPanGestureRecognizer) {
let fileView = sender.view!
switch sender.state {
case .began, .changed:
let translation = sender.translation(in: fileView)
fileView.center = CGPoint(x: fileView.center.x + translation.x, y: fileView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: fileView)
case .ended:
if fileView.frame.intersects(whiteContainer.frame) {
print("in white container")
} else {
print("outside the white container")
UIView.animate(withDuration: 0.3, animations: { fileView.frame.origin = self.pointOrigin3!})
}
default:
break
}
}

How can I animate this tableview header Y position change

I want to slide my table view header into view if the use scrolls up my feed and the header is off screen. If the user then scrolls down I want to hide it again.
I believe I have this working using the following:
final class AccountSettingsController: UITableViewController {
let items: [Int] = Array(0...500)
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: 120).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.tableHeaderViewWithAutolayout = menuView
tableView.tableFooterView = .init()
tableView.refreshControl = .init()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "This is setting #\(indexPath.row)"
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let transY = scrollView.panGestureRecognizer.translation(in: scrollView).y
if scrollView.contentOffset.y > 120 {
if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
} else if transY < 0 {
menuView.transform = .identity
}
return
}
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
}
}
}
}
I would like to animate the slide in / slide out of the header, so it slides down and up, rather than just appears.
I tried to add UIView.animate blocks such as
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .init(translationX: 0, y: contentOffset)
})
}
}
but this produces a random jumping effect on the header and does not achieve what I would like at all.
I've attached a gif that shows the current effect, as you can see it just appears and disappears. I'd like this to animate down and up for for show / hide.
I also have an extension to set the correct size for my table view header, which you can find here -
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
I agree with Claudio, using the tableHeaderView is probably complicating things as it is not really intended to be manipulated in this way.
Try the below and see if this gets you what you are looking for.
Instead of manipulating the offset of the header, manipulate the height. You can still give the impression of a scroll.
You could also introduce scrollView.panGestureRecognizer.velocity(in: scrollView) if you wanted to apply a threshold so the menu appears only after a particular scroll speed.
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let data: [Int] = Array(0...99)
private let menuView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .purple
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.dataSource = self
view.delegate = self
// view.refreshControl = .init()
view.contentInsetAdjustmentBehavior = .never
view.tableFooterView = .init()
return view
}()
var menuViewHeightAnchor: NSLayoutConstraint!
var tableViewTopAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
menuViewHeightAnchor = menuView.heightAnchor.constraint(equalToConstant: 120)
tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 120)
edgesForExtendedLayout = []
[tableView, menuView].forEach(view.addSubview)
NSLayoutConstraint.activate([
menuViewHeightAnchor,
menuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableViewTopAnchor,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: .zero)
cell.textLabel?.text = "Cell #\(indexPath.row)"
return cell
}
private var previousOffsetY: CGFloat = 0
private var velocityThreshold: CGFloat = 500
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = min(120, max(0, scrollView.contentOffset.y))
let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
let offsetYDiff = previousOffsetY - offsetY
previousOffsetY = offsetY
let adjustedOffset = menuViewHeightAnchor.constant + offsetYDiff
tableViewTopAnchor.constant = 120 - max(0, offsetY)
menuViewHeightAnchor.constant = min(120, adjustedOffset)
guard offsetY == 120 else { return }
if translation.y > 0 { // DRAGGED DOWN
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 120
self.view.layoutIfNeeded()
})
} else if translation.y < 0 { // DRAGGED UP
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 0
self.view.layoutIfNeeded()
})
}
}
}
I believe the best approach is by setting the menuView as a subview of the tableViewController, for example on the viewDidLoad method and also adding an inset on the tableView like this:
let menuHeight: CGFloat = 120
var scrollStartingYPoint: CGFloat = 0
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(menuView)
NSLayoutConstraint.activate([
menuView.leftAnchor.constraint(equalTo: view.leftAnchor),
menuView.topAnchor.constraint(equalTo: view.topAnchor)
])
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.contentInset.top = menuHeight
}
Then you can add the scrolling logic to show/hide the menu, I've done it a little different than you using the scrollViewWillBeginDragging method to store the initial scroll offset, to then determine the scroll direction and offset:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollStartingYPoint = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
if menuView.frame.origin.y >= 0 {
return
}
var translationY = -menuHeight + offset
if translationY > 0 {
translationY = 0
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}
If you want to animate the presentation of the menu, then you can change the previous code and use your animation code like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .identity
})
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}

Custom Tab Bar Menu with horizontal swipe

Just a quick question regarding implementation.
I am trying to recreate this:
https://i.imgur.com/lFXKFXl.mp4
I am wondering if I have to do it from scratch using a UICollectionView, there's a built-in Xcode method, or there is a library online somewhere.
Note: I am not trying to create a tab bar menu at the bottom, but rather at the top underneath the navigation controller navbar with horizontal swipe features. Instagram also has this underneath the search tab.
Thank you!
You can animate your blue bar in following way
func scrollViewDidScroll(_ scrollView: UIScrollView) {
blueBarViewLeadingConstraint.constant = scrollView.contentOffset.x / (scrollView.bounds.width / (TabBarView.frame.size.width / numberOfTab))
}
You can use a UIScrollView and add child viewcontrollers to it. I made an extension to UIViewController that easily allows you to create a swipe view.
extension UIViewController {
public func addViewControllerToContainer(viewController: UIViewController, count: CGFloat = 0, container: UIScrollView, heightDecrease: CGFloat = 0, startIndex: CGFloat = 0) {
let multiplier: CGFloat = count
addChildViewController(viewController)
container.addSubview(viewController.view)
container.contentSize.width += viewController.view.frame.width
container.contentSize.height = viewController.view.frame.height - heightDecrease
container.frame = CGRect(x: viewController.view.frame.origin.x, y: viewController.view.frame.origin.y, width: viewController.view.frame.width, height: viewController.view.frame.height - heightDecrease)
container.frame.origin.y += heightDecrease
container.contentOffset.x = container.frame.width * startIndex
viewController.view.frame.size.width = container.frame.width
viewController.view.frame.size.height = container.frame.height
viewController.view.frame.size.height -= heightDecrease
viewController.view.frame.origin.x = container.frame.width * multiplier
}
public func addViewControllersToContainer(viewControllers: [UIViewController], container: UIScrollView, heightDecrease: CGFloat = 0, startIndex: CGFloat) {
var count: CGFloat = 0
for viewController in viewControllers {
addViewControllerToContainer(viewController: viewController, count: count, container: container, heightDecrease: heightDecrease, startIndex: startIndex)
count += 1
}
}
}
class MainSwipeViewController: UIViewController {
let viewControllers: [UIViewController] = [VC1, VC2, VC3] // view controllers swiped between
let startIndex: CGFloat = 0 // which viewController to begin at (0 means first)
let mainScrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .lightGray
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.bounces = false
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.backgroundColor = UIColor.clear
return scrollView
}()
private func addViewControllers(_ vc: UIViewController, viewControllers: [UIViewController], startIndex: CGFloat = 0) {
view.addSubview(mainScrollView)
addViewControllersToContainer(viewControllers: viewControllers, container: mainScrollView, startIndex: startIndex)
}
override func viewDidLoad() {
super.viewDidLoad()
addViewControllers(self, viewControllers: viewControllers, startIndex: startIndex)
}
}
As for the tabbar menu, you can easily create that by implementing UIScrollViewDelegate in MainSwipeViewController
extension MainSwipeViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.x / view.frame.width
}
}
contentOffset is the interval of your swipe and increases by one everytime you swipe. This will help you animate the blue bar in your menu.

Hide view item of NSStackView with animation

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

Removing a UIView from UIStackView Changes its Size

I am trying to build a drag drop action and if I drag a view from stack view and drop it to somewhere and remove the view with "removeArrangedSubview" it changes the dragged items size and makes it bigger. I am using this piece of code to resize dropped item.
sender.view!.frame = CGRectMake(sender.view!.frame.origin.x, sender.view!.frame.origin.y, sender.view!.frame.width * 0.5, sender.view!.frame.height * 0.5)
Here is the image for the comparison.
You are setting the frame of the view you are moving but when you place it in a stack view the stack view will reset it based on the stack view's constraints and the intrinsic content size of the view that you are adding to it. The reason it is not doing that when you don't call removeArrangedSubview is because it has not triggered an auto layout pass. In general, when you're using auto layout you shouldn't set views frames but rather update constraints intrinsicContentSize and request auto layout updates.
You should play around with the distribution property of your stack view, the constraints you have placed on it, as well as the intrinsicContentSize of the views you are adding to it until you get your desired result.
For example:
class ViewController: UIViewController
{
let stackView = UIStackView()
let otherStackView = UIStackView()
override func viewDidLoad()
{
self.view.backgroundColor = UIColor.whiteColor()
stackView.alignment = .Center
stackView.axis = .Horizontal
stackView.spacing = 10.0
stackView.distribution = .FillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
otherStackView.alignment = .Center
otherStackView.axis = .Horizontal
otherStackView.spacing = 10.0
otherStackView.distribution = .FillEqually
otherStackView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stackView)
self.view.addSubview(otherStackView)
stackView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor).active = true
stackView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor).active = true
stackView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor).active = true
stackView.heightAnchor.constraintEqualToConstant(150).active = true
otherStackView.topAnchor.constraintEqualToAnchor(self.view.topAnchor).active = true
otherStackView.widthAnchor.constraintGreaterThanOrEqualToConstant(50).active = true
otherStackView.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor).active = true
otherStackView.heightAnchor.constraintEqualToConstant(150).active = true
self.addSubviews()
}
func addSubviews()
{
for _ in 0 ..< 5
{
let view = View(frame: CGRect(x: 0.0, y: 0.0, width: 100, height: 100))
view.userInteractionEnabled = true
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panHandler(_:)))
view.addGestureRecognizer(panGesture)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.redColor()
self.stackView.addArrangedSubview(view)
}
}
func panHandler(sender: UIPanGestureRecognizer)
{
guard let view = sender.view else{ return }
switch sender.state {
case .Began:
self.stackView.removeArrangedSubview(view)
self.view.addSubview(view)
let location = sender.locationInView(self.view)
view.center = location
case .Changed:
view.center.x += sender.translationInView(self.view).x
view.center.y += sender.translationInView(self.view).y
case .Ended:
if CGRectContainsPoint(self.otherStackView.frame, view.center)
{
self.otherStackView.addArrangedSubview(view)
self.otherStackView.layoutIfNeeded()
}
else
{
self.stackView.addArrangedSubview(view)
self.otherStackView.layoutIfNeeded()
}
default:
self.stackView.addArrangedSubview(view)
self.otherStackView.layoutIfNeeded()
}
sender.setTranslation(CGPointZero, inView: self.view)
}
}
class View: UIView
{
override func intrinsicContentSize() -> CGSize
{
return CGSize(width: 100, height: 100)
}
}
The above view controller and UIView subclass does something similar to what you're looking for, but it's not possible to tell from your question. Notice that pinning a stack view to the edges of its super view (like stackView) causes it to stretch subviews to fill all the available space while allowing the stack view to dynamically size based on the intrinsic size of its subviews (like otherStackView) does not.
Update
If you don't want to add the view to another stack view but you want them to retain their frame, you should remove any constraints the view's have on them and then set their translatesAutoresizingMaskIntoConstraints property to true. For example:
func panHandler(sender: UIPanGestureRecognizer)
{
guard let view = sender.view else{ return }
switch sender.state {
case .Began:
self.stackView.removeArrangedSubview(view)
self.view.addSubview(view)
let location = sender.locationInView(self.view)
view.center = location
case .Changed:
view.center.x += sender.translationInView(self.view).x
view.center.y += sender.translationInView(self.view).y
default:
view.removeConstraints(view.constraints)
view.translatesAutoresizingMaskIntoConstraints = true
// You can now set the view's frame or position however you want
view.center.x += sender.translationInView(self.view).x
view.center.y += sender.translationInView(self.view).y
}
sender.setTranslation(CGPointZero, inView: self.view)
}