diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift index fb17fbaad..6eb697ed3 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift @@ -18,7 +18,6 @@ import SwiftUI /// Metrics used across the entire onboarding flow. struct OnboardingMetrics { - static let maxContentWidth: CGFloat = 600 static let maxContentHeight: CGFloat = 750 /// The padding used between the top of the main content and the navigation bar. diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift index 6b6967962..6bd22223f 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift @@ -24,7 +24,6 @@ struct OnboardingSplashScreenPageContent { let message: String let image: ImageAsset let darkImage: ImageAsset - let gradient: Gradient } // MARK: View model @@ -38,19 +37,14 @@ enum OnboardingSplashScreenViewModelResult { struct OnboardingSplashScreenViewState: BindableState, CustomDebugStringConvertible { - // MARK: - Constants - - private enum Constants { - static let gradientColors = [ - Color(red: 0.95, green: 0.98, blue: 0.96), - Color(red: 0.89, green: 0.96, blue: 0.97), - Color(red: 0.95, green: 0.89, blue: 0.97), - Color(red: 0.81, green: 0.95, blue: 0.91), - Color(red: 0.95, green: 0.98, blue: 0.96) - ] - } - - // MARK: - Properties + /// The colours of the background gradient shown behind the 4 pages. + private let gradientColors = [ + Color(red: 0.95, green: 0.98, blue: 0.96), + Color(red: 0.89, green: 0.96, blue: 0.97), + Color(red: 0.95, green: 0.89, blue: 0.97), + Color(red: 0.81, green: 0.95, blue: 0.91), + Color(red: 0.95, green: 0.98, blue: 0.96) + ] /// An array containing all content of the carousel pages let content: [OnboardingSplashScreenPageContent] @@ -61,6 +55,13 @@ struct OnboardingSplashScreenViewState: BindableState, CustomDebugStringConverti "OnboardingSplashScreenViewState at page \(bindings.pageIndex)." } + /// The background gradient for all 4 pages and the hidden page at the start of the carousel. + var backgroundGradient: Gradient { + // Include the extra stop for the hidden page at the start of the carousel. + let hiddenPageColor = gradientColors[gradientColors.count - 2] + return Gradient(colors: [hiddenPageColor] + gradientColors) + } + init() { // The pun doesn't translate, so we only use it for English. let locale = Locale.current @@ -70,23 +71,19 @@ struct OnboardingSplashScreenViewState: BindableState, CustomDebugStringConverti OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage1Title, message: VectorL10n.onboardingSplashPage1Message, image: Asset.Images.onboardingSplashScreenPage1, - darkImage: Asset.Images.onboardingSplashScreenPage1Dark, - gradient: Gradient(colors: [Constants.gradientColors[0], Constants.gradientColors[1]])), + darkImage: Asset.Images.onboardingSplashScreenPage1Dark), OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage2Title, message: VectorL10n.onboardingSplashPage2Message, image: Asset.Images.onboardingSplashScreenPage2, - darkImage: Asset.Images.onboardingSplashScreenPage2Dark, - gradient: Gradient(colors: [Constants.gradientColors[1], Constants.gradientColors[2]])), + darkImage: Asset.Images.onboardingSplashScreenPage2Dark), OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage3Title, message: VectorL10n.onboardingSplashPage3Message, image: Asset.Images.onboardingSplashScreenPage3, - darkImage: Asset.Images.onboardingSplashScreenPage3Dark, - gradient: Gradient(colors: [Constants.gradientColors[2], Constants.gradientColors[3]])), + darkImage: Asset.Images.onboardingSplashScreenPage3Dark), OnboardingSplashScreenPageContent(title: page4Title, message: VectorL10n.onboardingSplashPage4Message, image: Asset.Images.onboardingSplashScreenPage4, - darkImage: Asset.Images.onboardingSplashScreenPage4Dark, - gradient: Gradient(colors: [Constants.gradientColors[3], Constants.gradientColors[4]])), + darkImage: Asset.Images.onboardingSplashScreenPage4Dark), ] self.bindings = OnboardingSplashScreenBindings() } diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift index ca8bd8974..6afe46726 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift @@ -29,8 +29,6 @@ struct OnboardingSplashScreen: View { private var isLeftToRight: Bool { layoutDirection == .leftToRight } private var pageCount: Int { viewModel.viewState.content.count } - /// The dimensions of the stack with the action buttons and page indicator. - @State private var overlayFrame: CGRect = .zero /// A timer to automatically animate the pages. @State private var pageTimer: Timer? /// The amount of offset to apply when a drag gesture is in progress. @@ -40,6 +38,61 @@ struct OnboardingSplashScreen: View { @ObservedObject var viewModel: OnboardingSplashScreenViewModel.Context + var body: some View { + GeometryReader { geometry in + VStack(alignment: .leading) { + Spacer() + .frame(height: OnboardingMetrics.spacerHeight(in: geometry)) + + // The main content of the carousel + HStack(alignment: .top, spacing: 0) { + + // Add a hidden page at the start of the carousel duplicating the content of the last page + OnboardingSplashScreenPage(content: viewModel.viewState.content[pageCount - 1]) + .frame(width: geometry.size.width) + + ForEach(0.. 0 ? 0 : 16) + + Spacer() + .frame(height: OnboardingMetrics.spacerHeight(in: geometry)) + } + .frame(maxHeight: .infinity) + .background(background.ignoresSafeArea().offset(x: pageOffset(in: geometry))) + .gesture( + DragGesture() + .onChanged(handleDragGestureChange) + .onEnded { handleDragGestureEnded($0, viewSize: geometry.size) } + ) + } + .accentColor(theme.colors.accent) + .navigationBarHidden(true) + .onAppear { + startTimer() + } + .onDisappear { stopTimer() } + .track(screen: .welcome) + } + /// The main action buttons. var buttons: some View { VStack(spacing: 12) { @@ -54,70 +107,23 @@ struct OnboardingSplashScreen: View { .padding(12) } } + .padding(.horizontal, 16) + .readableFrame() } - /// The only part of the UI that isn't inside of the carousel. - var overlay: some View { - VStack(spacing: 50) { - Color.clear - Color.clear - - VStack { - OnboardingSplashScreenPageIndicator(pageCount: pageCount, - pageIndex: viewModel.pageIndex) - Spacer() - - buttons - .padding(.horizontal, 16) - .frame(maxWidth: OnboardingMetrics.maxContentWidth) - Spacer() - } - .background(ViewFrameReader(frame: $overlayFrame)) + @ViewBuilder + /// The view's background, showing a gradient in light mode and a solid colour in dark mode. + var background: some View { + if !theme.isDark { + LinearGradient(gradient: viewModel.viewState.backgroundGradient, + startPoint: .leading, + endPoint: .trailing) + .flipsForRightToLeftLayoutDirection(true) + } else { + theme.colors.background } } - var body: some View { - GeometryReader { geometry in - ZStack(alignment: .leading) { - - // The main content of the carousel - HStack(spacing: 0) { - - // Add a hidden page at the start of the carousel duplicating the content of the last page - OnboardingSplashScreenPage(content: viewModel.viewState.content[pageCount - 1], - overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom) - .frame(width: geometry.size.width) - .tag(-1) - - ForEach(0.. CGFloat { + (CGFloat(viewModel.pageIndex + 1) * -geometry.size.width) + dragOffset + } + // MARK: - Gestures /// Whether or not a drag gesture is valid or not. diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift index 0be9e6891..e8e66d773 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift @@ -26,51 +26,34 @@ struct OnboardingSplashScreenPage: View { // MARK: Public /// The content that this page should display. let content: OnboardingSplashScreenPageContent - /// The height of the non-scrollable content in the splash screen. - let overlayHeight: CGFloat // MARK: - Views - @ViewBuilder - var backgroundGradient: some View { - if !theme.isDark { - LinearGradient(gradient: content.gradient, startPoint: .leading, endPoint: .trailing) - .flipsForRightToLeftLayoutDirection(true) - } - } - var body: some View { VStack { - VStack { - Image(theme.isDark ? content.darkImage.name : content.image.name) - .resizable() - .scaledToFit() - .frame(maxWidth: 300) - .padding(20) - .accessibilityHidden(true) - - VStack(spacing: 8) { - OnboardingTintedFullStopText(content.title) - .font(theme.fonts.title2B) - .foregroundColor(theme.colors.primaryContent) - Text(content.message) - .font(theme.fonts.body) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.center) - } - .padding(.bottom) - - Spacer() - - // Prevent the content from clashing with the overlay content. - Spacer().frame(maxHeight: overlayHeight) + Image(theme.isDark ? content.darkImage.name : content.image.name) + .resizable() + .scaledToFit() + .frame(maxWidth: 310) // This value is problematic. 300 results in dropped frames + // on iPhone 12/13 Mini. 305 the same on iPhone 12/13. As of + // iOS 15, 310 seems fine on all supported screen widths 🤞. + .padding(20) + .accessibilityHidden(true) + + VStack(spacing: 8) { + OnboardingTintedFullStopText(content.title) + .font(theme.fonts.title2B) + .foregroundColor(theme.colors.primaryContent) + Text(content.message) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) } - .padding(.horizontal, 16) - .frame(maxWidth: OnboardingMetrics.maxContentWidth, - maxHeight: OnboardingMetrics.maxContentHeight) + .fixedSize(horizontal: false, vertical: true) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(backgroundGradient.ignoresSafeArea()) + .padding(.bottom) + .padding(.horizontal, 16) + .readableFrame() } } @@ -78,7 +61,7 @@ struct OnboardingSplashScreenPage_Previews: PreviewProvider { static let content = OnboardingSplashScreenViewState().content static var previews: some View { ForEach(0..