I'm trying to develop a header with a scrollView
a bit like the Revolut app:
I was thinking of adding one scrollview
into another but I guess wouldn't get the same result.
Here is my code:
import UIKit
import SnapKit
class ItemDetailViewController: UIViewController, UIScrollViewDelegate {
lazy var topBar = CryptoDetailTopBarView()
lazy var header = UIView()
lazy var chartView = UIView()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.contentSize = CGSize(width: 414, height: 2000)
view.backgroundColor = .systemGreen
view.bounces = true
return view
}()
lazy var name = UILabel()
lazy var symbol = UILabel()
lazy var type = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGray
view.addSubview(topBar)
view.addSubview(header)
view.addSubview(scrollView)
header.addSubview(name)
header.addSubview(symbol)
header.addSubview(type)
scrollView.addSubview(chartView)
topBar.layer.zPosition = 20
topBar.backgroundColor = .blue
header.backgroundColor = .systemRed
name.textColor = .label
name.font = .boldSystemFont(ofSize: 34)
symbol.textColor = .label
type.textColor = .systemGray
chartView.backgroundColor = .white
chartView.isUserInteractionEnabled = true
self.topBar.title.text = "Title"
self.name.text = "Title"
self.symbol.text = "Text"
self.type.text = "Type"
scrollView.delegate = self
updateViewConstraints()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if header.frame.minY < topBar.frame.maxY - name.frame.minY - name.frame.height/2 {
topBar.title.alpha = 1
} else {
topBar.title.alpha = 0
}
let y: CGFloat = scrollView.contentOffset.y
header.snp.remakeConstraints { make in
make.left.equalToSuperview()
make.top.equalTo(topBar.snp.bottom).inset(y)
make.width.equalToSuperview()
make.height.equalTo(150)
}
}
override func updateViewConstraints() {
topBar.snp.makeConstraints { make in
make.left.top.equalToSuperview()
make.width.equalToSuperview()
make.height.equalTo(90)
}
header.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalTo(topBar.snp.bottom)
make.width.equalToSuperview()
make.height.equalTo(150)
}
scrollView.snp.makeConstraints { make in
make.top.equalTo(header.snp.bottom)
make.centerX.bottom.width.equalToSuperview()
}
name.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.left.equalToSuperview().offset(16)
make.width.equalTo(200)
make.height.equalTo(41)
}
symbol.snp.makeConstraints { make in
make.top.equalTo(name.snp.bottom).offset(10)
make.left.equalTo(name)
make.width.equalTo(50)
make.height.equalTo(20)
}
type.snp.makeConstraints { make in
make.top.equalTo(symbol)
make.left.equalTo(symbol.snp.right).offset(10)
make.width.equalTo(50)
make.height.equalTo(20)
}
chartView.snp.makeConstraints { make in
make.left.equalTo(scrollView).offset(16)
make.width.equalTo(scrollView).offset(-32)
make.top.equalTo(15)
make.height.equalTo(100)
}
super.updateViewConstraints()
}
fileprivate func getHeightOfHeaderView() -> CGFloat {
return header.frame.height
}
}
The topBar
view is the view
with the smaller title that replace the big title when I scroll down (like Revolut).
I'm updating constraints using SnapKit
depending on my scrollView
offset.
Here is what I get:
You can see that my white view
called chartView
(part of the scrollView
) is moving faster than my header which remove the "pushing" effect there is on Revolut.
Also, my white view
is stuck when I drag down the scrollView
: there should be a "pulling down" effect.
Basically I want to make the same thing as Revolut. The App Store has the pretty same header for non featured app.
I tried with collectionView, tableView and scrollView (using navigation bar big title) but it's not doing what I want. I need to make it myself from scratch like Revolut did I think.
Btw, you can easily execute my code because there is no storyboard needed.
I've implemented something like this a couple of times and you're really close.
Reacting to scrollViewDidScroll
is the right thing to do, but I wouldn't recommend updating the constraints. I would't say that Auto Layout was designed to be updated in real time, and even if it worked correctly, you'll probably see a performance impact.
Instead, use the transform
property of your view.
You'll have to tweak the values but it should look a bit like this:
header.transform = CGAffineTransform(translateX: 0, y: max(0, scrollView.contentOffset.y - header.bounds.y))
The reasoning for this math is simpler than it seems:
We want our view to be moved downwards as soon as the content offset reaches our view's position. This happens when scrollView.contentOffset.y - header.bounds.y == 0
. Before this happens, this number will be lower than 0, that's why we use max
. This way, before this happens, we'll apply a transform with a translation of 0, so our view will be dragged normally.
Once our scroll view has been dragged beyond the point where our view is at the top, that's when we want to make it sticky. scrollView.contentOffset.y - header.bounds.y
tells us how far beyond our view the user has scrolled, so move our view down by exactly that value.
If you are using a view with content insets, you might need to tweak your math, it might look something like this, but if it doesn't work I'll leave it up to you to play with the values to see what works.
header.transform = CGAffineTransform(translateX: 0, y: max(0, scrollView.contentOffset.y - (header.bounds.y + scrollView.adjustedContentInsets.top)))
Tip: Use a print statement with values like the content offset on scrollViewDidScroll
in order to figure out what numbers you want exactly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With