You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让UINavigationBar覆盖App内容并修复CollectionView单元格下移问题

问题描述

我希望UINavigationBar能覆盖App中的所有内容,为此编写了如下代码:

SceneDelegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
                    
        if let windowScene = (scene as? UIWindowScene) {
            
            let window = UIWindow(windowScene: windowScene)
            
            let nav = UINavigationController()
            let mainView = StoreViewController()
            nav.viewControllers = [mainView]
            nav.navigationBar.backgroundColor = .green.withAlphaComponent(0.5)
            window.rootViewController = nav
            
            self.window = window
            window.makeKeyAndVisible()
            
        }
    }

}

StoreViewController

class StoreViewController: UIViewController {
    
    let backgroundView = BackgroundView()
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
    let sections = Bundle.main.decode([Section].self, from: "section.json")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        createCollectionView()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false)
    }
    
    func createDataSource() {
        
        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
            switch self.sections[indexPath.section].identifier {
                
            case "featuredCell": return self.configure(FeaturedCell.self, with: item, for: indexPath)
                
            default: return self.configure(CarouselCell.self, with: item, for: indexPath)
            }
        }
        
        dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
            guard let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeader.reuseIdentifier, for: indexPath) as? SectionHeader
            else { return nil }
            guard let firstApp = self?.dataSource?.itemIdentifier(for: indexPath) else { return nil }
            guard let section = self?.dataSource?.snapshot().sectionIdentifier(containingItem: firstApp)
            else { return nil }
            if section.title.isEmpty { return nil }
            sectionHeader.title.text = section.title
            sectionHeader.subtitle.text = section.subtitle
            return sectionHeader
        }
    }

    func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with item: Item, for indexPath: IndexPath) -> T {
        guard let cell = collectionView.dequeueReusableCell(
            withReuseIdentifier: cellType.reuseIdentifier,
            for: indexPath) as? T else {
            fatalError("Unable to dequeue \(cellType)")
        }
        cell.configure(with: item)
        return cell
    }
    
    func reloadData() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(sections)
        for section in sections { snapshot.appendItems(section.item, toSection: section) }
        dataSource?.apply(snapshot)
    }
    
    func createCollectionView() {
        
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundView = backgroundView
        collectionView.isScrollEnabled = false
        view.addSubview(collectionView)
        
        collectionView.register(SectionHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: SectionHeader.reuseIdentifier)
        
        collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
        
        createDataSource()
        reloadData()
    }

    func createCompositionalLayout() -> UICollectionViewLayout {
        
        let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
            let section = self.sections[sectionIndex]
            switch section.identifier {
                
            case "featuredCell": return self.featuredSection(using: section)
                
            default: return self.carouselSection(using: section)
            }
        }
        
        return layout
    }
    
    func sectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
        
        let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(80))
        let layoutSectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: UICollectionElementKindSectionHeader, alignment: .top)
        return layoutSectionHeader
    }
    
    func carouselSection(using section: Section) -> NSCollectionLayoutSection {
        
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
        let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
        
        var layoutGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(1), heightDimension: .absolute(1))
        var layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
        
        switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            
            let a:CGFloat = UIScreen.main.bounds.size.height/8
            let b:CGFloat = UIScreen.main.bounds.size.height
            
            layoutGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(b - (a) ), heightDimension: .absolute(b))
            layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
            layoutGroup.contentInsets = NSDirectionalEdgeInsets(top: a, leading: a/2, bottom: a, trailing: a/2)
            
        case .pad:
            
            let a:CGFloat = UIScreen.main.bounds.size.height*0.25
            let h:CGFloat = a * 1.8
            let b:CGFloat = UIScreen.main.bounds.size.height
            
            layoutGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(b - (h)), heightDimension: .absolute(b - (a)))
            layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
            layoutGroup.contentInsets = NSDirectionalEdgeInsets(top: a, leading: a/10, bottom: 0, trailing: a/10)
            
        default:
            
            print(UIScreen.main.bounds.size.height)
        }

        let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
        layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
        
        layoutSection.visibleItemsInvalidationHandler = { (items, offset, environment) in
            
            items.forEach { item in
                let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0)
                let minScale: CGFloat = 0.7
                let maxScale: CGFloat = 1.1
                let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width), minScale)
                item.transform = CGAffineTransform(scaleX: scale, y: scale)
            }
            
            // no apparent built-in property of "scrollable content size"
            // so let's jump through some hoops to calculate it
            // we could probably make these class properties, and only update them
            // on frame change, but that's for another time...
                        
            let n: Int = self.dataSource!.snapshot().numberOfItems

            let cvw: CGFloat = self.collectionView.frame.width
            let cw: CGFloat = environment.container.contentSize.width

            let groupWidth: CGFloat = layoutGroupSize.widthDimension.dimension
                        
            // because layoutSection.orthogonalScrollingBehavior  is set to .groupPagingCentered
            // the actual scrollable width is center of item 0 to center of last item
            let scrollableWidth: CGFloat = groupWidth * CGFloat(n - 1)

            // get the group midX
            let groupMidX = CGRect(origin: .zero, size: environment.container.contentSize).midX
                        
            // calculate x-offset for first centered group
            let baseOffsetX: CGFloat = (groupMidX - (groupWidth * 0.5)) - ((cvw - cw) * 0.5)
                        
            // percent we are currently scrolled, of total scrollable width
            let pctScrolled: CGFloat = (offset.x + baseOffsetX) / scrollableWidth

            // tell the background view to update its image view x-offset
            self.backgroundView.xPercent = pctScrolled
        }
        
        return layoutSection
    }

无导航栏时的效果

无导航栏效果

添加导航栏后的效果

添加导航栏效果

目前导航栏虽覆盖了背景和CollectionView,但CollectionView的单元格出现下移,请问该如何修复这个问题?


解决方案

单元格下移是因为UIViewController默认会自动调整滚动视图的内边距以适配导航栏,导致CollectionView内容被向下推。修复方法很简单:

在StoreViewController的viewDidLoad方法中添加一行代码,禁用自动调整滚动视图Insets的行为:

override func viewDidLoad() {
    super.viewDidLoad()
    // 禁用自动调整滚动视图内边距
    automaticallyAdjustsScrollViewInsets = false
    createCollectionView()
}

这一设置会让CollectionView保持原本的布局位置,不会因为导航栏的存在而被向下偏移,同时导航栏依然可以覆盖在内容上方。

另外,你已经设置了导航栏的半透明背景,且控制器默认的edgesForExtendedLayout属性为.all,这会让视图自然延伸到导航栏下方,配合上述设置就能实现你想要的“导航栏覆盖所有内容”的效果。


内容的提问来源于stack exchange,提问作者user1342352532

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 00:35:56