mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Simplify the layout of the onboarding splash screen (#6320)
* Simplify the layout of the onboarding splash screen * Re-organise OnboardingSplashScreen. * Fix frame drops for real this time.
This commit is contained in:
parent
03d37b5923
commit
2d1f6f88b7
5 changed files with 114 additions and 123 deletions
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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..<pageCount, id: \.self) { index in
|
||||
OnboardingSplashScreenPage(content: viewModel.viewState.content[index])
|
||||
.frame(width: geometry.size.width)
|
||||
}
|
||||
|
||||
}
|
||||
.offset(x: pageOffset(in: geometry))
|
||||
|
||||
Spacer()
|
||||
|
||||
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) }
|
||||
)
|
||||
}
|
||||
.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..<pageCount) { index in
|
||||
OnboardingSplashScreenPage(content: viewModel.viewState.content[index],
|
||||
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(handleDragGestureChange)
|
||||
.onEnded { handleDragGestureEnded($0, viewSize: geometry.size) }
|
||||
)
|
||||
|
||||
overlay
|
||||
.frame(width: geometry.size.width)
|
||||
}
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.accentColor(theme.colors.accent)
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
startTimer()
|
||||
}
|
||||
.onDisappear { stopTimer() }
|
||||
.track(screen: .welcome)
|
||||
}
|
||||
|
||||
// MARK: - Animation
|
||||
|
||||
/// Starts the animation timer for an automatic carousel effect.
|
||||
|
@ -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.
|
||||
|
|
|
@ -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..<content.count, id:\.self) { index in
|
||||
OnboardingSplashScreenPage(content: content[index], overlayHeight: 200)
|
||||
OnboardingSplashScreenPage(content: content[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1
changelog.d/6319.bugfix
Normal file
1
changelog.d/6319.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Authentication: Fix splash screen stuttering on some devices.
|
Loading…
Reference in a new issue