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:
Doug
2026-03-12 14:18:19 +00:00
committed by GitHub
parent 0bd72114b8
commit 67162381a8
11 changed files with 175 additions and 78 deletions

View File

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

View File

@@ -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()

View File

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