Files
mobile-ios/Rosetta/Features/Onboarding/OnboardingPager.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)))
}
}