Use offsets for the carousel instead of a page view.

Fix carousel for RTL layouts.
This commit is contained in:
Doug
2022-01-07 12:19:39 +00:00
parent b8e9179bbf
commit 0c649183ee
7 changed files with 104 additions and 37 deletions
@@ -23,10 +23,15 @@ struct OnboardingSplashScreen: View {
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
@Environment(\.theme) private var theme
@Environment(\.layoutDirection) private var layoutDirection
private var isLeftToRight: Bool { layoutDirection == .leftToRight }
private var pageCount: Int { viewModel.viewState.content.count }
@State private var overlayFrame: CGRect = .zero
@State private var pageTimer: Timer?
@State private var dragOffset: CGFloat = .zero
// MARK: Public
@@ -48,7 +53,7 @@ struct OnboardingSplashScreen: View {
var overlay: some View {
VStack {
OnboardingSplashScreenPageIndicator(pageCount: viewModel.viewState.content.count,
OnboardingSplashScreenPageIndicator(pageCount: pageCount,
pageIndex: viewModel.pageIndex)
.padding(.vertical, 20)
@@ -59,44 +64,90 @@ struct OnboardingSplashScreen: View {
var body: some View {
GeometryReader { geometry in
// FIXME: The PageTabViewStyle breaks the safe area - replace with ScrollView or custom offsets
TabView(selection: $viewModel.pageIndex) {
OnboardingSplashScreenPage(content: viewModel.viewState.content[viewModel.viewState.content.count - 1],
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
.tag(-1)
ForEach(0..<viewModel.viewState.content.count, id:\.self) { index in
let pageContent = viewModel.viewState.content[index]
OnboardingSplashScreenPage(content: pageContent,
ZStack(alignment: .bottomLeading) {
HStack(spacing: 0) {
OnboardingSplashScreenPage(content: viewModel.viewState.content[pageCount - 1],
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.ignoresSafeArea()
.overlay(overlay
.background(ViewFrameReader(frame: $overlayFrame))
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16),
alignment: .bottom)
.accentColor(theme.colors.accent)
.onAppear {
pageTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
if viewModel.pageIndex == viewModel.viewState.content.count - 1 {
viewModel.send(viewAction: .hiddenPage)
withAnimation {
viewModel.send(viewAction: .nextPage)
}
} else {
withAnimation {
viewModel.send(viewAction: .nextPage)
}
.frame(width: geometry.size.width)
.tag(-1)
ForEach(0..<pageCount, id:\.self) { index in
let pageContent = viewModel.viewState.content[index]
OnboardingSplashScreenPage(content: pageContent,
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
.frame(width: geometry.size.width)
.tag(index)
}
}
.offset(x: (CGFloat(viewModel.pageIndex + 1) * -geometry.size.width) + dragOffset)
.gesture(
DragGesture()
.onChanged {
guard shouldSwipeForTranslation($0.translation.width) else { return }
stopTimer()
dragOffset = isLeftToRight ? $0.translation.width : -$0.translation.width
}
.onEnded { value in
withAnimation(.easeInOut(duration: 0.2)) {
if dragOffset < -geometry.size.width / 3 {
viewModel.send(viewAction: .nextPage)
} else if dragOffset > geometry.size.width / 3 {
viewModel.send(viewAction: .previousPage)
}
dragOffset = 0
startTimer()
}
}
)
overlay
.frame(width: geometry.size.width)
.background(ViewFrameReader(frame: $overlayFrame))
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
}
.background(theme.colors.background.ignoresSafeArea()) // whilst gradients are transparent
.accentColor(theme.colors.accent)
.navigationBarHidden(true)
.onAppear { startTimer() }
.onDisappear { stopTimer() }
}
private func shouldSwipeForTranslation(_ width: CGFloat) -> Bool {
if viewModel.pageIndex == 0 {
return isLeftToRight ? width < 0 : width > 0
} else if viewModel.pageIndex == pageCount - 1 {
return isLeftToRight ? width > 0 : width < 0
}
return true
}
private func startTimer() {
guard pageTimer == nil else { return }
pageTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
if viewModel.pageIndex == pageCount - 1 {
viewModel.send(viewAction: .hiddenPage)
withAnimation(.easeInOut(duration: 0.7)) {
viewModel.send(viewAction: .nextPage)
}
} else {
withAnimation(.easeInOut(duration: 0.7)) {
viewModel.send(viewAction: .nextPage)
}
}
}
.navigationBarHidden(true)
.background(theme.colors.background.ignoresSafeArea())
}
private func stopTimer() {
guard let pageTimer = pageTimer else { return }
self.pageTimer = nil
pageTimer.invalidate()
}
}
@@ -107,5 +158,6 @@ struct OnboardingSplashScreen_Previews: PreviewProvider {
static let stateRenderer = MockOnboardingSplashScreenScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
// .environment(\.layoutDirection, .rightToLeft)
}
}