Swift - 使用UIScrollView实现固定标题栏

4

我想开发一个带有scrollView的标题,类似于Revolut应用程序:

Revolut应用程序屏幕录制

我考虑将一个scrollView添加到另一个中,但我认为不会得到相同的结果。

这是我的代码:

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
        }
        
    }
topBar视图是一个小标题的view,在我向下滚动时替代了大标题(例如Revolut应用)。
我使用SnapKit根据scrollView的偏移量更新约束。
以下是我的效果: My app screen recording 您可以看到,我的名为chartView的白色viewscrollView的一部分)移动速度比我的标题头快,这消除了在Revolut上推动的效果。
此外,当我向下拖动scrollView时,我的白色view被卡住了:应该有一个向下拉的效果。
基本上我想做与Revolut相同的事情。App Store非特色应用程序具有相同的标题头。
我尝试过使用collectionView、tableView和scrollView(使用导航栏大标题),但它们无法实现我想要的效果。我像Revolut一样需要从头开始制作它。
顺便说一句,您可以轻松地执行我的代码,因为不需要故事板。

1
不要将您的标题添加为子视图,而是使用tableview的内置标题。https://developer.apple.com/documentation/uikit/views_and_controls/table_views/adding_headers_and_footers_to_table_sections - aheze
我能让它固定在顶部吗?我想使用子视图会给我更多的灵活性。 - Paul Bénéteau
可以的!这就是它的设计初衷。 - aheze
1
不是的。它被设计为粘附在属于它的第一个单元格上。 - Paul Bénéteau
抱歉,我以为表视图有一个粘性标题栏功能,因为集合视图有。也许改成集合视图?只需使用“sectionHeader.pinToVisibleBounds = true”就能做到像这样的效果。 - aheze
不,这样不好,我已经尝试使用现有的视图,但它没有起作用。我必须自己制作。 - Paul Bénéteau
2个回答

2
我已经实现了类似这样的东西几次了,你离成功很近。
响应scrollViewDidScroll是正确的做法,但我不建议更新约束。我认为自动布局并不是为实时更新而设计的,即使它能正常工作,你也可能会看到性能上的影响。
相反,使用视图的transform属性。
您需要调整值,但它应该长得像这样:
header.transform = CGAffineTransform(translateX: 0, y: max(0, scrollView.contentOffset.y - header.bounds.y))

这个数学推理比看起来简单:
我们希望当内容偏移量到达视图位置时,视图会向下移动。这发生在scrollView.contentOffset.y - header.bounds.y == 0时。在此之前,这个数字将低于0,因此我们使用max。这样,在此之前,我们将应用一个平移为0的变换,这样我们的视图将被正常拖动。
一旦我们的滚动视图被拖动到超过我们视图顶部的位置,那么我们就想让它保持粘性。scrollView.contentOffset.y - header.bounds.y告诉我们用户已经滚动了多远,所以将我们的视图向下移动恰好那个值。
如果您使用具有内容插入的视图,则可能需要调整数学运算。它可能看起来像这样,但如果不起作用,我会让您自己调整数值以查看哪些适用。
header.transform = CGAffineTransform(translateX: 0, y: max(0, scrollView.contentOffset.y - (header.bounds.y + scrollView.adjustedContentInsets.top)))

提示:在scrollViewDidScroll中使用带有内容偏移量等值的打印语句,以便确定您需要的确切数字。


你能解释一下为什么我不应该更新约束吗? - Paul Bénéteau
是否可能将其翻译为精确的y,而不是像这样发生:y +/- k? - Paul Bénéteau

0

您可以使用较大的导航栏来实现标题效果,使用UITableView / UICollectionView headers。

编辑:您还可以在导航栏上使用模糊效果,如下所示:

let statusBarHeight = UIApplication.shared.statusBarFrame.height
// iOS13 and later:
let statusBarHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
// the size of the blur effect
let bounds = navigationController?.navigationBar.bounds.insetBy(dx: 0, dy: -(statusBarHeight)).offsetBy(dx: 0, dy: -(statusBarHeight))
// Create blur effect.
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
blurEffectView.frame = bounds
// Set navigation bar up.
navigationController?.navigationBar.isTranslucent = true
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.addSubview(blurEffectView)
navigationController?.navigationBar.sendSubview(toBack: blurEffectView)

不过,我该如何实现模糊背景的菜单,并且在滚动时与默认导航栏“融合”? - Paul Bénéteau
@PaulBénéteau,你可以将模糊效果作为子视图添加到导航栏中,这样导航栏下方的项目就会被模糊处理。 - Zsolt Biró
已编辑我的帖子并附上了一个示例。 - Zsolt Biró

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接