如何让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
相关产品推荐
相关产品推荐

