SwiftUI safeAreaBar-based scroll-edge bars, made easy to use from UIKit.
As of iOS 26, there is no public UIKit API that allows custom views to blend with the blur of the navigation bar or tab bar. UIScrollEdgeElementContainerInteraction exists but does not integrate with the system bar blur — it renders its own separate background, resulting in two distinct blur effects that don't fluently merge.
I originally wanted to bring this effect to React Native, but it wasn't even solved at the UIKit level. By investigating SwiftUI's internals, I found that SwiftUI coordinates between the scroll edge bar and the navigation/tab bar without requiring them to be in the same view hierarchy. Unable to recreate this in pure UIKit, I built this wrapper that bridges through SwiftUI to achieve the effect.
Note: The library locates rendered bar frames by walking the view hierarchy. It does not call any private methods or import private frameworks, but it does rely on an internal implementation detail that could theoretically change in a future iOS release. I will actively monitor for changes, and hope Apple exposes a native UIKit API that makes this library unnecessary.
- Seamless glass blur — extends navigation bar and tab bar Liquid Glass (iOS 26+)
- Graceful fallback — uses
safeAreaInseton iOS 16–25, same layout without the blur - Top & bottom bars — attach a bar to either scroll edge, or both
- UIKit-native — works with any
UIScrollView,UITableView, orUICollectionView - UIView-based content — pass any
UIViewas bar content - Automatic insets — content insets and scroll indicators managed for you
- iOS 16.0+
- Swift 6.2+
- Xcode 26.0+
Add to Package.swift:
dependencies: [
.package(url: "https://github.com/jensvansteen/ScrollEdgeBar.git", from: "1.2.0")
]Or via Xcode: File → Add Package Dependencies and enter the repository URL.
pod 'ScrollEdgeBar', '~> 1.2'import ScrollEdgeBar
class MyViewController: UIViewController {
private let tableView = UITableView()
private var edgeBarController: ScrollEdgeBarController?
override func viewDidLoad() {
super.viewDidLoad()
// Set up your scroll view as usual
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
// ... constraints, dataSource, etc.
// Wrap it with ScrollEdgeBarController
let controller = ScrollEdgeBarController(scrollView: tableView)
let segmented = UISegmentedControl(items: ["First", "Second"])
segmented.selectedSegmentIndex = 0
controller.setTopBar(segmented)
addChild(controller)
view.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
controller.didMove(toParent: self)
edgeBarController = controller
}
}let controller = ScrollEdgeBarController(scrollView: scrollView)
controller.setTopBar(myFilterView)
controller.setBottomBar(myToolbarView)controller.removeTopBar()
controller.removeBottomBar()By default, the glass blur effect is used on iOS 26+. Pass prefersGlassEffect: false to use plain safeAreaInset bars on all OS versions:
let controller = ScrollEdgeBarController(scrollView: scrollView, prefersGlassEffect: false)Provide estimated bar heights to prevent a brief layout flicker on first appearance:
controller.estimatedTopBarHeight = 48
controller.estimatedBottomBarHeight = 44safeAreaBar works best when the UIView identity it hosts remains stable across updates. If you are integrating from a framework like React Native that manages its own view lifecycle and may reparent or replace views, use ScrollEdgeBarSlotHostView as a fixed container:
let slot = ScrollEdgeBarSlotHostView()
controller.setTopBar(slot)
// Later, swap content without touching the bar itself
slot.setContentView(myReactNativeView)The slot's identity stays fixed in the SwiftUI hierarchy while the content inside can be replaced freely.
| Member | Description |
|---|---|
init(scrollView:prefersGlassEffect:) |
Creates a controller wrapping the given scroll view. prefersGlassEffect defaults to true |
scrollView |
The wrapped scroll view (read-only) |
prefersGlassEffect |
When true (default), uses iOS 26 glass blur if available. Set to false for plain bars on all OS versions |
setTopBar(_:) |
Sets a UIView as the top edge bar |
setBottomBar(_:) |
Sets a UIView as the bottom edge bar |
removeTopBar() |
Removes the top bar |
removeBottomBar() |
Removes the bottom bar |
estimatedTopBarHeight |
Estimated top bar height used before layout (default: 60) |
estimatedBottomBarHeight |
Estimated bottom bar height used before layout (default: 60) |
| Member | Description |
|---|---|
setContentView(_:) |
Mounts a view inside the slot, replacing any previous content |
contentView |
The currently mounted view (read-only) |
The Example/ directory contains a full demo app. Open Example/ScrollEdgeBarExampleApp/ScrollEdgeBarExampleApp.xcodeproj in Xcode 26 to run it.
Segmented control as top bar above a ranked app list.
219_1440x30_shots_so.mp4
Same screen with prefersGlassEffect: false, showing the plain safeAreaInset bar.
159_1440x30_shots_so.mp4
Horizontally scrolling filter chips with large title navigation.
849_1440x30_shots_so.mp4
Note: When a
safeAreaBaris present alongside a large title navigation bar, SwiftUI applies the scroll edge blur effect to the navigation bar even when the content is at rest, causing it to appear blurry on first appearance. This is a known SwiftUI behavior (FB21613303).
Glass-effect review banner (top) and action buttons (bottom) using UIGlassEffect.
139_1440x30_shots_so.mp4
Large colored blocks demonstrating how the glass blur color transitions as you scroll.
46_1440x30_shots_so.mp4
Bottom edge bar positioned above the system UIToolbar.
235_1440x30_shots_so.mp4
UISearchController in the navigation bar with a segmented control edge bar below it.
62_1440x30_shots_so.mp4
Week day selector top bar with strong edge blur, simulating the Calendar app. The strength of the blur as content scrolls behind the bar is controlled via UIScrollView.topEdgeEffect:
scrollView.topEdgeEffect.style = .automatic // default
scrollView.topEdgeEffect.style = .soft
scrollView.topEdgeEffect.style = .hard // strongest, matches Calendar app
// Also available for other edges
scrollView.bottomEdgeEffect.style = .hard773_1440x30_shots_so.mp4
Created by Jens Van Steen · GitHub
MIT License. See LICENSE file.