Fix the AuthenticationStartScreen with large Dynamic Type sizes. (#5191)
* Allow AuthenticationStartLogo to be scaled and fix dark mode when not on a gradient. * Fix the layout of AuthenticationStartScreen with large Dynamic Type sizes.
This commit is contained in:
@@ -10,80 +10,132 @@ import SwiftUI
|
||||
|
||||
/// The app's logo styled to fit on various launch pages.
|
||||
struct AuthenticationStartLogo: View {
|
||||
/// Set to specify a custom size for the Logo, otherwise the default size of 158pt will be used.
|
||||
var size: CGFloat?
|
||||
/// Set to `true` to skip the brand chrome.
|
||||
let hideBrandChrome: Bool
|
||||
/// Set to `true` when using on top of `Asset.Images.launchBackground`.
|
||||
let isOnGradient: Bool
|
||||
|
||||
private let appLogoImage = Image(asset: Asset.Images.appLogo)
|
||||
|
||||
struct SizeMetrics {
|
||||
let scale: CGFloat
|
||||
let imageSize: CGFloat
|
||||
}
|
||||
|
||||
private var sizeMetrics: SizeMetrics? {
|
||||
size.map { customSize in
|
||||
let scale = customSize / 158
|
||||
return SizeMetrics(scale: scale,
|
||||
imageSize: hideBrandChrome ? customSize : 110 * scale)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let sizeMetrics {
|
||||
appLogoImage
|
||||
.resizable()
|
||||
.frame(width: sizeMetrics.imageSize, height: sizeMetrics.imageSize)
|
||||
.modifier(AuthenticationBrandLogoModifier(scale: sizeMetrics.scale,
|
||||
hideBrandChrome: hideBrandChrome,
|
||||
isOnGradient: isOnGradient))
|
||||
} else {
|
||||
appLogoImage
|
||||
.modifier(AuthenticationBrandLogoModifier(scale: 1,
|
||||
hideBrandChrome: hideBrandChrome,
|
||||
isOnGradient: isOnGradient))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the brand chrome styling (rounded card with shadows and border) to any image,
|
||||
/// as seen on the authentication start screen.
|
||||
private struct AuthenticationBrandLogoModifier: ViewModifier {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
/// Set to `true` when using on top of `Asset.Images.launchBackground`
|
||||
/// Scale factor relative to the original 158pt design.
|
||||
let scale: CGFloat
|
||||
/// Set to `true` to skip the brand chrome.
|
||||
let hideBrandChrome: Bool
|
||||
/// Set to `true` when using on top of `Asset.Images.launchBackground`.
|
||||
let isOnGradient: Bool
|
||||
|
||||
/// Extra padding needed to avoid cropping the shadows.
|
||||
private let extra: CGFloat = 64
|
||||
/// The shape that the logo is composed on top of.
|
||||
private let outerShape = RoundedRectangle(cornerRadius: 44)
|
||||
private let outerShapeShadowColor = Color(red: 0.11, green: 0.11, blue: 0.13)
|
||||
private var isLight: Bool {
|
||||
colorScheme == .light
|
||||
}
|
||||
|
||||
/// Extra padding needed to avoid cropping the shadows.
|
||||
private var extra: CGFloat {
|
||||
64 * scale
|
||||
}
|
||||
|
||||
/// The shape that the logo is composed on top of.
|
||||
private var outerShape: RoundedRectangle {
|
||||
RoundedRectangle(cornerRadius: 44 * scale)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
func body(content: Content) -> some View {
|
||||
if hideBrandChrome {
|
||||
Image(asset: Asset.Images.appLogo)
|
||||
content
|
||||
} else {
|
||||
brandLogo
|
||||
styledContent(content)
|
||||
}
|
||||
}
|
||||
|
||||
private var brandLogo: some View {
|
||||
Image(asset: Asset.Images.appLogo)
|
||||
private func styledContent(_ content: Content) -> some View {
|
||||
content
|
||||
.background {
|
||||
Circle()
|
||||
.inset(by: 1)
|
||||
.shadow(color: .black.opacity(!isLight ? 0.3 : 0.4),
|
||||
radius: 12.57143,
|
||||
y: 6.28571)
|
||||
.inset(by: 1 * scale)
|
||||
.shadow(color: .black.opacity(!isLight && isOnGradient ? 0.3 : 0.4),
|
||||
radius: 12.57143 * scale,
|
||||
y: 6.28571 * scale)
|
||||
|
||||
Circle()
|
||||
.inset(by: 1)
|
||||
.inset(by: 1 * scale)
|
||||
.shadow(color: .black.opacity(0.5),
|
||||
radius: 12.57143,
|
||||
y: 6.28571)
|
||||
radius: 12.57143 * scale,
|
||||
y: 6.28571 * scale)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
.padding(24)
|
||||
.padding(24 * scale)
|
||||
.background {
|
||||
Color.white
|
||||
.opacity(isLight ? 0.23 : 0.05)
|
||||
.opacity(isLight ? 0.23 : isOnGradient ? 0.05 : 0.13)
|
||||
}
|
||||
.clipShape(outerShape)
|
||||
.overlay {
|
||||
outerShape
|
||||
.inset(by: 0.25)
|
||||
.stroke(.white.opacity(isLight ? 1 : 0.9), lineWidth: 0.5)
|
||||
.inset(by: 0.25 * scale)
|
||||
.stroke(.white.opacity(isLight ? 1 : isOnGradient ? 0.9 : 0.25), lineWidth: 0.5 * scale)
|
||||
.blendMode(isLight ? .normal : .overlay)
|
||||
}
|
||||
.padding(extra)
|
||||
.background {
|
||||
ZStack {
|
||||
if !isLight {
|
||||
if !isLight, isOnGradient {
|
||||
outerShape
|
||||
.inset(by: 1)
|
||||
.inset(by: 1 * scale)
|
||||
.padding(extra)
|
||||
.shadow(color: .black.opacity(0.5),
|
||||
radius: 32.91666,
|
||||
y: 1.05333)
|
||||
radius: 32.91666 * scale,
|
||||
y: 1.05333 * scale)
|
||||
} else {
|
||||
outerShape
|
||||
.inset(by: 1)
|
||||
.inset(by: 1 * scale)
|
||||
.padding(extra)
|
||||
.shadow(color: outerShapeShadowColor.opacity(isLight ? 0.23 : 0.08),
|
||||
radius: 16,
|
||||
y: 8)
|
||||
radius: 16 * scale,
|
||||
y: 8 * scale)
|
||||
|
||||
outerShape
|
||||
.inset(by: 1)
|
||||
.inset(by: 1 * scale)
|
||||
.padding(extra)
|
||||
.shadow(color: outerShapeShadowColor.opacity(0.5),
|
||||
radius: 16,
|
||||
y: 8)
|
||||
radius: 16 * scale,
|
||||
y: 8 * scale)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
}
|
||||
@@ -98,3 +150,43 @@ struct AuthenticationStartLogo: View {
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: false)
|
||||
.padding()
|
||||
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: true)
|
||||
.padding()
|
||||
.background {
|
||||
AuthenticationStartScreenBackgroundImage().offset(y: 70)
|
||||
}
|
||||
.clipped()
|
||||
}
|
||||
.background(.compound.bgCanvasDefault)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: false)
|
||||
.padding()
|
||||
AuthenticationStartLogo(hideBrandChrome: false, isOnGradient: true)
|
||||
.padding()
|
||||
.background {
|
||||
AuthenticationStartScreenBackgroundImage().offset(y: 70)
|
||||
}
|
||||
.clipped()
|
||||
}
|
||||
.background(.compound.bgCanvasDefault)
|
||||
.colorScheme(.dark)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
AuthenticationStartLogo(size: 54, hideBrandChrome: false, isOnGradient: false)
|
||||
.padding()
|
||||
.background(.compound.bgCanvasDefault)
|
||||
AuthenticationStartLogo(size: 54, hideBrandChrome: false, isOnGradient: false)
|
||||
.padding()
|
||||
.background(.compound.bgCanvasDefault)
|
||||
.colorScheme(.dark)
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,36 +16,30 @@ struct AuthenticationStartScreen: View {
|
||||
@Bindable var context: AuthenticationStartScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
// This view uses a GeometryReader instead of FullscreenDialog so its content takes the full
|
||||
// height available (after taking the buttons out of the equation) in order for the logo
|
||||
// and title to appear vertically centred and equally spaced within this content area.
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.hidden)
|
||||
|
||||
buttons
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
versionText
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom)
|
||||
.onTapGesture(count: 7) {
|
||||
context.send(viewAction: .reportProblem)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.appVersion)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.hidden)
|
||||
|
||||
buttons
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
}
|
||||
.frame(minHeight: geometry.size.height)
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.background {
|
||||
@@ -64,7 +58,8 @@ struct AuthenticationStartScreen: View {
|
||||
if verticalSizeClass == .regular {
|
||||
Spacer()
|
||||
|
||||
AuthenticationStartLogo(hideBrandChrome: context.viewState.hideBrandChrome)
|
||||
AuthenticationStartLogo(hideBrandChrome: context.viewState.hideBrandChrome,
|
||||
isOnGradient: !context.viewState.hideBrandChrome)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -77,7 +72,7 @@ struct AuthenticationStartScreen: View {
|
||||
.multilineTextAlignment(.center)
|
||||
Text(L10n.screenOnboardingWelcomeMessage(InfoPlistReader.main.productionAppName))
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding()
|
||||
@@ -114,6 +109,16 @@ struct AuthenticationStartScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.tertiary))
|
||||
}
|
||||
|
||||
versionText
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 16)
|
||||
.onTapGesture(count: 7) {
|
||||
context.send(viewAction: .reportProblem)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.appVersion)
|
||||
}
|
||||
.padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
|
||||
.readableFrame()
|
||||
|
||||
@@ -28,7 +28,7 @@ struct PlaceholderScreen: View {
|
||||
let hideGradientBackground: Bool
|
||||
|
||||
var body: some View {
|
||||
AuthenticationStartLogo(hideBrandChrome: hideBrandChrome)
|
||||
AuthenticationStartLogo(hideBrandChrome: hideBrandChrome, isOnGradient: !hideGradientBackground)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background {
|
||||
if !hideGradientBackground {
|
||||
|
||||
Reference in New Issue
Block a user