storyboard design hierarchy like this
CodePudding user response:
The idea is to lock
the animation block from firing if it is already in progress.
The simplest way to do this is using a bool
to keep track of the status of the animation
First, use some variables to help you keep track of the animation and top view's status
// Persist the top view height constraint
var topViewHeightConstraint: NSLayoutConstraint?
// Original height of the top view
var viewHeight: CGFloat = 100
// Keep track of the
private var isAnimationInProgress = false
Then use these variables when performing the animation
extension ScrollViewAnimateVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Check if the animation is locked or not
if !isAnimationInProgress {
guard let topViewHeightConstraint = topViewHeightConstraint
else { return }
// Check if an animation is required
if scrollView.contentOffset.y > .zero &&
topViewHeightConstraint.constant > .zero {
topViewHeightConstraint.constant = .zero
animateTopViewHeight()
}
else if scrollView.contentOffset.y <= .zero
&& topViewHeightConstraint.constant <= .zero {
topViewHeightConstraint.constant = viewHeight
animateTopViewHeight()
}
}
}
// Animate the top view
private func animateTopViewHeight() {
// Lock the animation functionality
isAnimationInProgress = true
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
} completion: { [weak self] (_) in
// Unlock the animation functionality
self?.isAnimationInProgress = false
}
}
}
This will give you something smoother like this
CodePudding user response:
Assuming you don't really need to hide the yellow view - you only need to cover it with the green view...
This can be done with constraints only -- no need for any scrollViewDidScroll
code.
What we'll do is constrain the yellow view to the scroll view's Frame Layout Guide so it doesn't move at all.
Then we'll constrain the green view's Top greater-than-or-equal to the Frame Layout Guide, and constrain it to the bottom of the yellow view with a less-than-required Priority.
Then we'll constrain the Bottom of the green view to the Top of the red view, so the red view will "push it up / pull it down" when we scroll. Again, though, we'll use a less-than-required Priority so the red view can slide up underneath.
Finally, we'll constrain the red view to the scroll view's Content Layout Guide to control the scrollable area. Since the yellow and green views each have a constant height of 100-pts, we'll constrain the Top of the red view 200-pts from the Top of the content guide.
Here's a complete example (no @IBOutlet
connections needed):
class ExampleVC: UIViewController {
let yellowView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let greenView: UIView = {
let v = UIView()
v.backgroundColor = .green
return v
}()
let redView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemBlue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// create a vertical stack view
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
// let's add some labels to the stack view
// so we have something to scroll
(1...30).forEach { n in
let v = UILabel()
v.backgroundColor = .yellow
v.text = "Label \(n)"
v.textAlignment = .center
stack.addArrangedSubview(v)
}
// add the stack view to the red view
redView.addSubview(stack)
// add these views to scroll view in this order
[yellowView, redView, greenView].forEach { v in
scrollView.addSubview(v)
}
// add scroll view to view
view.addSubview(scrollView)
// they will all use auto-layout
[stack, yellowView, redView, greenView, scrollView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// always respect safe area
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view to safe area
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
// we need yellow view to
// fill width of scroll view FRAME
// height: 100-pts
// "stick" to top of scroll view FRAME
yellowView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor),
yellowView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor),
yellowView.heightAnchor.constraint(equalToConstant: 100.0),
yellowView.topAnchor.constraint(equalTo: frameG.topAnchor),
// we need green view to
// fill width of scroll view FRAME
// height: 100-pts
// start at bottom of yellow view
// "stick" to top of scroll view FRAME when scrolled up
greenView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor),
// we'll use a constant of -40 here to leave a "gap" on the right, so it's
// easy to see what's happening...
greenView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: -40),
greenView.heightAnchor.constraint(equalToConstant: 100.0),
greenView.topAnchor.constraint(greaterThanOrEqualTo: frameG.topAnchor),
// we need red view to
// fill width of scroll view FRAME
// dynamic height (determined by its contents - the stack view)
// start at bottom of green view
// "push / pull" green view when scrolled
// go under green view when green view is at top
// red view will be controlling the scrollable area
redView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
redView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
redView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
redView.widthAnchor.constraint(equalTo: frameG.widthAnchor),
// let's inset the stack view 16-pts on all 4 sides
stack.topAnchor.constraint(equalTo: redView.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 16.0),
stack.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -16.0),
stack.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -16.0),
])
var c: NSLayoutConstraint!
// these constraints need Priority adjustments
c = greenView.topAnchor.constraint(equalTo: yellowView.bottomAnchor)
c.priority = .defaultHigh - 1
c.isActive = true
c = redView.topAnchor.constraint(equalTo: greenView.bottomAnchor)
c.priority = .defaultHigh
c.isActive = true
// since yellow and green view Heights are constant 100-pts each
c = redView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 200.0)
c.isActive = true
}
}
Here's how it looks - I set the green view to be 40-pts narrower than the full width to make it easy to see what's happening:
Now, if you do want to actually hide the yellow view, instead of just covering it, add this extension:
extension ExampleVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
yellowView.isHidden = scrollView.contentOffset.y >= 100
}
}
and add this to the view controller:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scrollView.delegate = self
}
We do this in viewDidAppear
to avoid erroneous frame positioning that occurs if we set the delegate in viewDidLoad