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 b5f571bfe4
commit bf2156b3df
11 changed files with 175 additions and 78 deletions
@@ -10,80 +10,132 @@ import SwiftUI
/// The app's logo styled to fit on various launch pages. /// The app's logo styled to fit on various launch pages.
struct AuthenticationStartLogo: View { 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 @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 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 let outerShapeShadowColor = Color(red: 0.11, green: 0.11, blue: 0.13)
private var isLight: Bool { private var isLight: Bool {
colorScheme == .light 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 { if hideBrandChrome {
Image(asset: Asset.Images.appLogo) content
} else { } else {
brandLogo styledContent(content)
} }
} }
private var brandLogo: some View { private func styledContent(_ content: Content) -> some View {
Image(asset: Asset.Images.appLogo) content
.background { .background {
Circle() Circle()
.inset(by: 1) .inset(by: 1 * scale)
.shadow(color: .black.opacity(!isLight ? 0.3 : 0.4), .shadow(color: .black.opacity(!isLight && isOnGradient ? 0.3 : 0.4),
radius: 12.57143, radius: 12.57143 * scale,
y: 6.28571) y: 6.28571 * scale)
Circle() Circle()
.inset(by: 1) .inset(by: 1 * scale)
.shadow(color: .black.opacity(0.5), .shadow(color: .black.opacity(0.5),
radius: 12.57143, radius: 12.57143 * scale,
y: 6.28571) y: 6.28571 * scale)
.blendMode(.overlay) .blendMode(.overlay)
} }
.padding(24) .padding(24 * scale)
.background { .background {
Color.white Color.white
.opacity(isLight ? 0.23 : 0.05) .opacity(isLight ? 0.23 : isOnGradient ? 0.05 : 0.13)
} }
.clipShape(outerShape) .clipShape(outerShape)
.overlay { .overlay {
outerShape outerShape
.inset(by: 0.25) .inset(by: 0.25 * scale)
.stroke(.white.opacity(isLight ? 1 : 0.9), lineWidth: 0.5) .stroke(.white.opacity(isLight ? 1 : isOnGradient ? 0.9 : 0.25), lineWidth: 0.5 * scale)
.blendMode(isLight ? .normal : .overlay) .blendMode(isLight ? .normal : .overlay)
} }
.padding(extra) .padding(extra)
.background { .background {
ZStack { ZStack {
if !isLight { if !isLight, isOnGradient {
outerShape outerShape
.inset(by: 1) .inset(by: 1 * scale)
.padding(extra) .padding(extra)
.shadow(color: .black.opacity(0.5), .shadow(color: .black.opacity(0.5),
radius: 32.91666, radius: 32.91666 * scale,
y: 1.05333) y: 1.05333 * scale)
} else { } else {
outerShape outerShape
.inset(by: 1) .inset(by: 1 * scale)
.padding(extra) .padding(extra)
.shadow(color: outerShapeShadowColor.opacity(isLight ? 0.23 : 0.08), .shadow(color: outerShapeShadowColor.opacity(isLight ? 0.23 : 0.08),
radius: 16, radius: 16 * scale,
y: 8) y: 8 * scale)
outerShape outerShape
.inset(by: 1) .inset(by: 1 * scale)
.padding(extra) .padding(extra)
.shadow(color: outerShapeShadowColor.opacity(0.5), .shadow(color: outerShapeShadowColor.opacity(0.5),
radius: 16, radius: 16 * scale,
y: 8) y: 8 * scale)
.blendMode(.overlay) .blendMode(.overlay)
} }
} }
@@ -98,3 +150,43 @@ struct AuthenticationStartLogo: View {
.accessibilityHidden(true) .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 @Bindable var context: AuthenticationStartScreenViewModel.Context
var body: some View { 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 GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0) { ScrollView {
Spacer() VStack(alignment: .leading, spacing: 0) {
.frame(height: UIConstants.spacerHeight(in: geometry)) Spacer()
.frame(height: UIConstants.spacerHeight(in: geometry))
content
.frame(width: geometry.size.width) content
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.hidden) .frame(width: geometry.size.width)
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.hidden)
buttons
.frame(width: geometry.size.width) buttons
.padding(.bottom, UIConstants.actionButtonBottomPadding) .frame(width: geometry.size.width)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
.padding(.top, 8) .padding(.top, 8)
Spacer() Spacer()
.frame(height: UIConstants.spacerHeight(in: geometry)) .frame(height: UIConstants.spacerHeight(in: geometry))
} }
.frame(maxHeight: .infinity) .frame(minHeight: geometry.size.height)
.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)
} }
.scrollBounceBehavior(.basedOnSize)
} }
.navigationBarHidden(true) .navigationBarHidden(true)
.background { .background {
@@ -64,7 +58,8 @@ struct AuthenticationStartScreen: View {
if verticalSizeClass == .regular { if verticalSizeClass == .regular {
Spacer() Spacer()
AuthenticationStartLogo(hideBrandChrome: context.viewState.hideBrandChrome) AuthenticationStartLogo(hideBrandChrome: context.viewState.hideBrandChrome,
isOnGradient: !context.viewState.hideBrandChrome)
} }
Spacer() Spacer()
@@ -77,7 +72,7 @@ struct AuthenticationStartScreen: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text(L10n.screenOnboardingWelcomeMessage(InfoPlistReader.main.productionAppName)) Text(L10n.screenOnboardingWelcomeMessage(InfoPlistReader.main.productionAppName))
.font(.compound.bodyLG) .font(.compound.bodyLG)
.foregroundColor(.compound.textSecondary) .foregroundColor(.compound.textPrimary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
.padding() .padding()
@@ -114,6 +109,16 @@ struct AuthenticationStartScreen: View {
} }
.buttonStyle(.compound(.tertiary)) .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) .padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
.readableFrame() .readableFrame()
@@ -28,7 +28,7 @@ struct PlaceholderScreen: View {
let hideGradientBackground: Bool let hideGradientBackground: Bool
var body: some View { var body: some View {
AuthenticationStartLogo(hideBrandChrome: hideBrandChrome) AuthenticationStartLogo(hideBrandChrome: hideBrandChrome, isOnGradient: !hideGradientBackground)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background { .background {
if !hideGradientBackground { if !hideGradientBackground {
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6158f690ffe1defeef9b583dd8267204ee733a45f2f5cf44d6696cc5af64789b oid sha256:540afca94168f6daa9ed51c761bd1ab271014898da893a39904d08e88872a263
size 111970 size 112308
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:514e4b542257ee77beb1c82896476935e1d8a1727d8a77384545cd94f8f87ea5 oid sha256:24320001ef26a722134e4ba829094cb8ee848079880f9f72c5284b1fb550bfe7
size 132189 size 132912
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:43a70f508336cb1d5fe0f3679b700b3d6cd8e64182b549e40f42c8f8aa0374e0 oid sha256:178b907d46a505230ef1d0530ff763789547c843ea828f7e3b4f18aae2be435f
size 510930 size 517884
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c7c51cd982fa0262784bf27f4a9bd295219a7733bc2ed4359d403bd81d2564f oid sha256:47a8832cc78842e8c471e603b88ea56b261fd9af821825246d55af8ab121e117
size 533667 size 541873
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a1a60cca5b53f6998cb6c2bf2cf1a8dd2fdd1343ea76fd72df32bba1d07fd011 oid sha256:b64fcfd41c482d6c7341d0b9a515c671175c10261f0fa5f412f943b1b895a655
size 99862 size 99485
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9ab408cd8e4aa4f955a03f68d845a5ac4d57e90ebbbab6b3fa0502aa6d0407c9 oid sha256:aea8fbf225d77a752f06612e76139a15e690d0be3dc51a56bb2189284a68d862
size 118992 size 119996
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:97d86af1a0f6eeb5d6933b13a6a5de3df8db1d2672d2e7cdf1f2fe83a0adbf64 oid sha256:b7eb4153207abeabf4f3217aae294603c8764bf28df93b901f0ab4c42a803527
size 531619 size 532886
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bda2bb728b9f9179ea211c5de083c8f4b67a6ffc4cc2f484c81c9ec4bb40d013 oid sha256:aadf9de9f2a5a4e6c2f0eb4a7becbed65e0ae24cdb9dd39ef1f25353d3309b3f
size 558660 size 561031