143 lines
5.0 KiB
Swift
143 lines
5.0 KiB
Swift
import SwiftUI
|
|
|
|
/// UIPageViewController wrapper that handles paging entirely in UIKit.
|
|
/// Exposes both the settled page index AND a continuous drag progress for smooth dot tracking.
|
|
struct OnboardingPager<Page: View>: UIViewControllerRepresentable {
|
|
@Binding var currentIndex: Int
|
|
/// Continuous progress: 0.0 = page 0, 1.0 = page 1, etc. Tracks the finger in real time.
|
|
@Binding var continuousProgress: CGFloat
|
|
let count: Int
|
|
let buildPage: (Int) -> Page
|
|
|
|
func makeCoordinator() -> OnboardingPagerCoordinator {
|
|
OnboardingPagerCoordinator(
|
|
currentIndex: currentIndex,
|
|
count: count,
|
|
currentIndexSetter: { [self] idx in self._currentIndex.wrappedValue = idx },
|
|
continuousProgressSetter: { [self] val in self._continuousProgress.wrappedValue = val },
|
|
buildControllers: { (0..<count).map { i in
|
|
let hc = UIHostingController(rootView: buildPage(i))
|
|
hc.view.backgroundColor = .clear
|
|
return hc
|
|
}}
|
|
)
|
|
}
|
|
|
|
func makeUIViewController(context: Context) -> UIPageViewController {
|
|
let vc = UIPageViewController(
|
|
transitionStyle: .scroll,
|
|
navigationOrientation: .horizontal
|
|
)
|
|
vc.view.backgroundColor = .clear
|
|
vc.dataSource = context.coordinator
|
|
vc.delegate = context.coordinator
|
|
vc.setViewControllers(
|
|
[context.coordinator.controllers[currentIndex]],
|
|
direction: .forward,
|
|
animated: false
|
|
)
|
|
// Hook into the inner scroll view for real-time offset tracking
|
|
for sub in vc.view.subviews {
|
|
if let scrollView = sub as? UIScrollView {
|
|
scrollView.backgroundColor = .clear
|
|
scrollView.delegate = context.coordinator
|
|
context.coordinator.pageWidth = scrollView.frame.width
|
|
}
|
|
}
|
|
return vc
|
|
}
|
|
|
|
func updateUIViewController(_ vc: UIPageViewController, context: Context) {
|
|
context.coordinator.count = count
|
|
context.coordinator.currentIndex = currentIndex
|
|
context.coordinator.currentIndexSetter = { [self] idx in self._currentIndex.wrappedValue = idx }
|
|
context.coordinator.continuousProgressSetter = { [self] val in self._continuousProgress.wrappedValue = val }
|
|
}
|
|
}
|
|
|
|
// MARK: - Coordinator (non-generic to avoid Swift compiler crash in Release optimiser)
|
|
|
|
final class OnboardingPagerCoordinator: NSObject,
|
|
UIPageViewControllerDataSource,
|
|
UIPageViewControllerDelegate,
|
|
UIScrollViewDelegate
|
|
{
|
|
let controllers: [UIViewController]
|
|
var count: Int
|
|
var currentIndex: Int
|
|
var currentIndexSetter: (Int) -> Void
|
|
var continuousProgressSetter: (CGFloat) -> Void
|
|
var pageWidth: CGFloat = 0
|
|
private var pendingIndex: Int = 0
|
|
|
|
init(
|
|
currentIndex: Int,
|
|
count: Int,
|
|
currentIndexSetter: @escaping (Int) -> Void,
|
|
continuousProgressSetter: @escaping (CGFloat) -> Void,
|
|
buildControllers: () -> [UIViewController]
|
|
) {
|
|
self.currentIndex = currentIndex
|
|
self.count = count
|
|
self.currentIndexSetter = currentIndexSetter
|
|
self.continuousProgressSetter = continuousProgressSetter
|
|
self.pendingIndex = currentIndex
|
|
self.controllers = buildControllers()
|
|
}
|
|
|
|
// MARK: DataSource
|
|
|
|
func pageViewController(
|
|
_ pvc: UIPageViewController,
|
|
viewControllerBefore vc: UIViewController
|
|
) -> UIViewController? {
|
|
guard let idx = controllers.firstIndex(where: { $0 === vc }), idx > 0 else { return nil }
|
|
return controllers[idx - 1]
|
|
}
|
|
|
|
func pageViewController(
|
|
_ pvc: UIPageViewController,
|
|
viewControllerAfter vc: UIViewController
|
|
) -> UIViewController? {
|
|
guard let idx = controllers.firstIndex(where: { $0 === vc }),
|
|
idx < count - 1 else { return nil }
|
|
return controllers[idx + 1]
|
|
}
|
|
|
|
// MARK: Delegate
|
|
|
|
func pageViewController(
|
|
_ pvc: UIPageViewController,
|
|
willTransitionTo pendingVCs: [UIViewController]
|
|
) {
|
|
if let vc = pendingVCs.first,
|
|
let idx = controllers.firstIndex(where: { $0 === vc }) {
|
|
pendingIndex = idx
|
|
}
|
|
}
|
|
|
|
func pageViewController(
|
|
_ pvc: UIPageViewController,
|
|
didFinishAnimating finished: Bool,
|
|
previousViewControllers: [UIViewController],
|
|
transitionCompleted completed: Bool
|
|
) {
|
|
guard completed,
|
|
let current = pvc.viewControllers?.first,
|
|
let idx = controllers.firstIndex(where: { $0 === current }) else { return }
|
|
currentIndex = idx
|
|
currentIndexSetter(idx)
|
|
}
|
|
|
|
// MARK: ScrollView — real-time progress
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
let w = scrollView.frame.width
|
|
guard w > 0 else { return }
|
|
let offsetFromCenter = scrollView.contentOffset.x - w
|
|
let fraction = offsetFromCenter / w
|
|
let progress = CGFloat(currentIndex) + fraction
|
|
continuousProgressSetter(max(0, min(CGFloat(count - 1), progress)))
|
|
}
|
|
}
|