I'm looking for the best way to make the following animation. I've tried some solutions, but none of them seem to tackle my problem as every single solution seems to fail at some point. The animation I need to do is the following:
User swipes his finger up and the tableView & 1 & 2 scroll up (it's like tableView & 1 & 2 are one scrollable element). Then, when 2 becomes invisible upon the scroll, 3 & 1 & tableView become scrollable (again, as if it were one scrollable element). Then, when 3 becomes invisible (as it is scrolled) the tableView is then the only scrollable element.
What I tried:
I initially tried with simple animations like changing height constraints for every 1/2/3 element based on the scroll offset and although it seemed fine to me, it didn't to the reviewer as he wanted more precise scrolling in between hiding elements animations
I then tried to combine panGesture with scroll. I embedded 1 & 2 and tableView into one scroll View and set panGesture recognizer for it with delegate function shouldRecognizeSimultaneouslyWith returning true while having tableView scrolling disabled. Then, upon intersecting 3 I was disabling the panGesture and tried to enable tableView scrolling, but failed to recognize which panGesture/scrolling works or not, which one to disable, which one to fail or work alone/simultaneously.
How would you dear developers tackle it so that the animations are smooth and as described? Perhaps, you have an awesome idea :)
I think you'll be fighting a losing battle trying to toggle scrolling.
Here's another approach...
UIView
contentInset.top
of the height of the three views plus vertical spacing3
will stick to the top, until it is pushed up by 1
2
till slide under 3
when 1
pushes it up1
when the tableView scrollsYou can try it with this example code (no @IBOutlet
connections needed):
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let tableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
v.separatorInset = .zero
return v
}()
let view1: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemRed
v.text = "1"
return v
}()
let view2: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemGreen
v.text = "2"
return v
}()
let view3: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemBlue
v.text = "3"
return v
}()
// this will hold the tableView and the
// three other views
let containerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
// clip to bounds to prevent the "top" views from showing
// as they are "pushed up" out of bounds
v.clipsToBounds = true
return v
}()
// this constraint constant will be changed
// in scrollViewDidScroll
@IBOutlet var view1TopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// add our container view
view.addSubview(containerView)
// add our tableView and three "top" views
containerView.addSubview(tableView)
containerView.addSubview(view2)
containerView.addSubview(view3)
containerView.addSubview(view1)
for v in [view1, view2, view3] {
// all three "top" views should be
// equal width to tableView
// horizontally centered to tableView
// 40-pts tall
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalTo: tableView.widthAnchor),
v.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
v.heightAnchor.constraint(equalToConstant: 40.0),
])
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain container view with 20-pts "padding"
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// constrain all 4 sides of tableView ot container view
tableView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
tableView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),
tableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0),
])
// view3 should stick to the top of the table,
// unless it's being pushed up by view1
let v3Top = view3.topAnchor.constraint(equalTo: tableView.topAnchor)
v3Top.priority = .defaultHigh + 1
// view2 should stick to the bottom of view3,
// unless it's being pushed up by view1
let v2Top = view2.topAnchor.constraint(equalTo: view3.bottomAnchor, constant: 4.0)
v2Top.priority = .defaultHigh
// view1 should ALWAYS be 4-pts from bottom of view2
let v1TopA = view1.topAnchor.constraint(equalTo: view2.bottomAnchor, constant: 4.0)
v1TopA.priority = .required
// view1 should ALWAYS be greater-than-or-equal 4-pts from bottom of view3
let v1TopB = view1.topAnchor.constraint(greaterThanOrEqualTo: view3.bottomAnchor, constant: 4.0)
v1TopB.priority = .required
// view1 top should ALWAYS be greater-than-or-equal top of tableView
let v1TopC = view1.topAnchor.constraint(greaterThanOrEqualTo: tableView.topAnchor)
v1TopC.priority = .required
// 88-pts is 40-pts for view3 and view2 plus 4-pts vertical spacing
// view1 top should NEVER be more-than 88-pts from top of tableView
let v1TopD = view1.topAnchor.constraint(lessThanOrEqualTo: tableView.topAnchor, constant: 88.0)
v1TopD.priority = .required
// view1 top will start at 88-pts from top of tableView
view1TopConstraint = view1.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 88.0)
view1TopConstraint.priority = .defaultHigh + 2
// activate those constraints
NSLayoutConstraint.activate([
v3Top,
v2Top,
v1TopA,
v1TopB,
v1TopC,
v1TopD,
view1TopConstraint,
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
// top inset is
// three 40-pt tall views
// plus 4-pts vertical spacing between each
// and 4-pts vertical spacing below view1
tableView.contentInset.top = 132
tableView.contentOffset.y = -132
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// we're getting called when the tableView scrolls
// invert the contentOffset y
let y = -scrollView.contentOffset.y
// subtract 44-pts (40-pt tall view plus 4-pts vertical spacing)
view1TopConstraint.constant = y - 44.0
}
}
extension ExampleViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "Row \(indexPath.row)"
return c
}
}
Here's how it looks to start:
then, after scrolling up just a bit (2
is sliding under 3
):
scrolling a bit more (1
is pushing 3
up and out of view):
and then table scrolling while 1
remains at the top: