Headers & Footers
All of the Authenticator views have a headerContent
and footerContent
argument in their initializers, which you can use to customize the UX.
Authenticator
Adding a header and/or footer to the Authenticator
component will display it in all of its views. By default, no header nor footer is provided by this component.
This example will result in the provided header (a logo) and footer (the copyright disclaimer) to be displayed in all views:
Authenticator(
headerContent: {
Image("amplify_logo")
},
footerContent: {
Text("© All Rights Reserved")
}
) { _ in
Text("You are logged in!")
}
The content that the Authenticator displays once the user has signed in, i.e. the Text("You are logged in!")
in the previous example, is not consider an Authenticator view and as such no header nor footer will be displayed for it.
Content Views
You can also provide a header and/or footer individually to each view that represents a step (e.g. SignInView
, SignUpView
, etc).
These views have the following default values:
- Header: A view that displays the step title, e.g. "Sign In"
- Footer: Any link buttons displayed by default (e.g. the "Forgot Password?" button in
SignInView
), otherwise empty
This example sets a custom header and footer in the SignInView
, which will only be displayed on that step:
Authenticator(
signInContent: { state in
SignInView(
state: state,
headerContent: {
VStack {
SignInHeader() // Re-use the default header
Divider() // Add a Divider
}
},
footerContent: {
VStack {
Divider() // Add a Divider
SignInFooter() // Re-use the default footer
}
}
)
}
) { _ in
Text("You are logged in!")
}
Internationalization (I18n)
The Authenticator ships with translations for English. These values can be customized simply by defining your own Localizable.strings
file in your app and overriding the desired keys.
For example, you can override the Sign In title and buttons texts:
"authenticator.signIn.title" = "Login";
"authenticator.signIn.button.forgotPassword" = "Reset password";
"authenticator.signIn.button.signIn" = "Submit";
"authenticator.signIn.button.createAccount" = "New user?";
Labels & Text
Using the same techniques as Internationalization (I18n), you can customize the labels and text of all views:
"authenticator.field.username.label" = "Enter your username";
"authenticator.field.username.placeholder" = "Your username is case-sensitive";
"authenticator.field.password.label" = "Enter your password";
"authenticator.field.password.placeholder" = "Your password is case-sensitive";
Sign Up Field Order
The Authenticator respects the order of the Sign Up fields that are provided in the signUpFields(_:)
call.
If the Authenticator automatically adds a required missing attribute (e.g. the verification mechanism), it will appear at the end.
Field Customization
When providing Sign Up Fields, you can use the default ones or define your own:
Authenticator { _ in
Text("You are logged in!")
}.signUpFields([
.username() // Default field for username
.password(isRequred: false) // Makes password optional
.text( // A customized text field for the Website attribute
key = .website, // A AuthUserAttributeKey
label = "Website", // Custom label
placeholder = "Enter your website" // Custom placeholder
isRequired: true, // Makes field mandatory
validator: { content in // Custom validator
guard content.contains("example.com") else {
return "Your website must have a domain of example.com"
}
return nil
}
),
.custom( // A fully custom field for a Custom attribute
attributeType: .custom(attributeKey: .custom("TOS")),
validator: { content in
guard let selected = Bool(content), selected else {
return "You must agree to the Terms of Service"
}
return nil
}
) { binding in
Toggle(isOn: binding.asBool()) {
Text("I agree to the terms of service")
}
}
])
// ...
// Custom extension to convert a Binding<String> into a Binding<Bool>
extension Binding where Value == String {
func asBool() -> Binding<Bool> {
return .init(
get: {
return Bool(wrappedValue) ?? false
},
set: { value in
wrappedValue = String(value)
}
)
}
}
Theming
By default, the Authenticator style matches those of the Amplify UI's default theme.
You can customize this look and feel by using the authenticatorTheme(_:)
view modifider and providing a customized AuthenticatorTheme
:
Authenticator { _ in
// ...
}.authenticatorTheme(myCustomTheme)
AuthenticatorTheme
is a class that allows you to set the style for various elements, which will be applied throughout the Authenticator views.
You can customize:
- Spacing (horizontal and vertical): For Authenticator views, buttons, alerts and fields.
- Colors: For backgrounds, borders and foreground elements.
- Fonts: For all views
The following example changes the colors and fonts used in the Authenticator:
struct MyView: View {
private let theme = AuthenticatorTheme()
init() {
// Colors
theme.colors.background.interactive = .pink
theme.colors.foreground.interactive = .red
theme.colors.border.interactive = .pink
// Fonts
theme.fonts.title = .custom("Impact", size: 40)
// Authenticator
theme.components.authenticator.spacing.vertical = 20
theme.components.authenticator.cornerRadius = 25
theme.components.authenticator.backgroundColor = Color(uiColor: .systemGray6)
theme.components.authenticator.padding = .init(
top: 20,
bottom: 20,
trailing: 50,
leading: 50
)
// Buttons
theme.components.button.primary.cornerRadius = 100
theme.components.button.primary.padding = 20
theme.components.button.link.font = .custom("Noteworthy-Bold", size: 15)
// Fields
theme.components.field.spacing.vertical = 20
theme.components.field.backgroundColor = .init(uiColor: .init(dynamicProvider: {
return $0.userInterfaceStyle == .dark ? .black : .white
})) // A dynamic color according to the color mode
// Alerts
theme.components.alert.cornerRadius = 30
theme.components.alert.padding = 50
}
var body: some View {
Authenticator { _ in
// ...
}
.authenticatorTheme(theme)
}
}
Full UI Customization
Customize the UI for the Authenticator flows
In addition to customizing fields and theming, you can also provide a custom UI for one or more of the Authenticator steps.
For simple use cases (such as adding additional information), you can use a combination of default views from the Authenticator library and views that you define yourself:
Authenticator(
signInContent: { state in
HStack {
Image("custom_image")
Divider()
SignInView(state: state) // Re-use the default SignInView
}
}
) { _ in
// ...
}
For more complex cases, you can completely replace the UI for any particular step.
Every view has an associated *State
class that they observe, and that can be used to implement a custom UI. These states have @Published
properties for their fields, which you can bind to your custom view.
This example shows how to fully replace the content displayed for the Sign In step, but a similar approach can be used for most steps:
Authenticator(
signInContent: { state in
CustomSignInView(state: state)
}
) { _ in
// ...
}
/// Custom Sign In view
struct CustomSignInView: View {
@ObservedObject var state: SignInState
var body: some View {
VStack {
TextField("Username", text: $state.username)
SecureField("Password", text: $state.password)
Divider()
Button("Sign in") {
Task {
try? await state.signIn()
}
}
if state.isBusy {
ProgressView()
}
}
}
}
Customize the UI for the Sign Up flow
Customizing the Sign Up step is slightly different, as SignUpState
does not individually publish each of its fields, but rather has one fields: [SignUpState.Field]
property.
A SignUpState.Field
is itself an @ObservableObject
that has the SignUpField
that it represents, and the @Published
property (called value
) that you need to bind to your view.
The following example shows how to fully replace the content displayed for the Sign Up step by iterating through the array of fields:
Authenticator(
signUpContent: { state in
CustomSignUpView(state: state)
}
) { _ in
// ...
}
/// Custom Sign Up view
struct CustomSignUpView: View {
@ObservedObject var state: SignUpState
var body: some View {
VStack {
ForEach(state.fields, id: \.self) { field in
createCustomField(for: field)
}
Button("Sign Up") {
Task {
try? await state.signUp()
}
}
}
}
/// Creates a view for a SignUpField according to its SignUpAttribute
@ViewBuilder
private func createCustomField(for signUpField: SignUpState.Field) -> some View {
@ObservedObject var observedField = signUpField
switch signUpField.field.attributeType {
case .username:
TextField("Username", text: $observedField.value)
case .password:
SecureField("Password", text: $observedField.value)
case .passwordConfirmation:
SecureField("Confirm password", text: $observedField.value)
case .phoneNumber:
TextField("Phone Number", text: $observedField.value)
case .email:
TextField("Email", text: $observedField.value)
/// ... An so on for all cases of SignUpAttribute
}
}
}
TOTP Setup
You can also customize the TOTP setup experience. We make available arguments in the ContinueSignInWithTOTPSetupView
i.e. qrCodeContent
and copyKeyContent
that can help you provide custom content for the TOTP Setup Experience. In the example below, examine how you could customize the setup screen.
Authenticator(
continueSignInWithTOTPSetupContent: { state in
ContinueSignInWithTOTPSetupView(
state: state,
// Example of how a customer can pass a custom QR code
qrCodeContent: { state in
// Your custom QR Code implementation goes here
},
copyKeyContent: { state in
// Your custom implementation goes here
}
)
}
) { _ in
// ...
}