Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify the layout of the onboarding splash screen #6320

Merged
merged 3 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ struct OnboardingSplashScreenPageContent {
let message: String
let image: ImageAsset
let darkImage: ImageAsset
let gradient: Gradient
}

// MARK: View model
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -40,75 +38,52 @@ struct OnboardingSplashScreen: View {

@ObservedObject var viewModel: OnboardingSplashScreenViewModel.Context

/// The main action buttons.
var buttons: some View {
VStack(spacing: 12) {
Button { viewModel.send(viewAction: .register) } label: {
Text(VectorL10n.onboardingSplashRegisterButtonTitle)
}
.buttonStyle(PrimaryActionButtonStyle())

Button { viewModel.send(viewAction: .login) } label: {
Text(VectorL10n.onboardingSplashLoginButtonTitle)
.font(theme.fonts.body)
.padding(12)
}
}
}

/// 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))
}
}

var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
VStack(alignment: .leading) {
Spacer()
.frame(height: OnboardingMetrics.spacerHeight(in: geometry))

// The main content of the carousel
HStack(spacing: 0) {
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],
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
OnboardingSplashScreenPage(content: viewModel.viewState.content[pageCount - 1])
.frame(width: geometry.size.width)
.tag(-1)

ForEach(0..<pageCount) { index in
OnboardingSplashScreenPage(content: viewModel.viewState.content[index],
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
ForEach(0..<pageCount, id: \.self) { index in
OnboardingSplashScreenPage(content: viewModel.viewState.content[index])
.frame(width: geometry.size.width)
.tag(index)
}

}
.offset(x: (CGFloat(viewModel.pageIndex + 1) * -geometry.size.width) + dragOffset)
.gesture(
DragGesture()
.onChanged(handleDragGestureChange)
.onEnded { handleDragGestureEnded($0, viewSize: geometry.size) }
)
.offset(x: pageOffset(in: geometry))

Spacer()

overlay
OnboardingSplashScreenPageIndicator(pageCount: pageCount,
pageIndex: viewModel.pageIndex)
.frame(width: geometry.size.width)
.padding(.bottom)

Spacer()

buttons
.frame(width: geometry.size.width)
.padding(.bottom, OnboardingMetrics.actionButtonBottomPadding)
.padding(.bottom, geometry.safeAreaInsets.bottom > 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) }
)
}
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
.navigationBarHidden(true)
.onAppear {
Expand All @@ -118,6 +93,37 @@ struct OnboardingSplashScreen: View {
.track(screen: .welcome)
}

/// The main action buttons.
var buttons: some View {
VStack(spacing: 12) {
Button { viewModel.send(viewAction: .register) } label: {
Text(VectorL10n.onboardingSplashRegisterButtonTitle)
}
.buttonStyle(PrimaryActionButtonStyle())

Button { viewModel.send(viewAction: .login) } label: {
Text(VectorL10n.onboardingSplashLoginButtonTitle)
.font(theme.fonts.body)
.padding(12)
}
}
.padding(.horizontal, 16)
.readableFrame()
}

@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
}
}

// MARK: - Animation

/// Starts the animation timer for an automatic carousel effect.
Expand Down Expand Up @@ -147,6 +153,11 @@ struct OnboardingSplashScreen: View {
pageTimer.invalidate()
}

/// The offset to apply to the `HStack` of pages.
private func pageOffset(in geometry: GeometryProxy) -> CGFloat {
(CGFloat(viewModel.pageIndex + 1) * -geometry.size.width) + dragOffset
}

// MARK: - Gestures

/// Whether or not a drag gesture is valid or not.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,59 +26,42 @@ 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()
}
}

struct OnboardingSplashScreenPage_Previews: PreviewProvider {
static let content = OnboardingSplashScreenViewState().content
static var previews: some View {
ForEach(0..<content.count, id:\.self) { index in
OnboardingSplashScreenPage(content: content[index], overlayHeight: 200)
OnboardingSplashScreenPage(content: content[index])
}
}
}
1 change: 1 addition & 0 deletions changelog.d/6319.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Authentication: Fix splash screen stuttering on some devices.