Pattern · Auth and Onboarding

New

Email Link Sign In SwiftUI Block

Passwordless sign-in by email: send a link, check your inbox with Open Mail and a Resend countdown, or enter a code instead.

An email field that checks itself and Continue with email, which runs your closure through working, done and failed. The card then turns to Check your inbox with the address, Open Mail and Resend on a countdown, plus an optional six-digit code that fills from AutoFill and submits itself. Links that open your app sign the person in through `onLink`.

  • SwiftUI
  • iOS 17.0+
  • magic-link
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • An email field that checks on submit, then live, with AutoFill
  • Continue with email shows working, a check, or your error's message
  • Check your inbox with Open Mail and a Resend countdown
  • An optional six-digit code that fills from AutoFill and submits itself
  • Sign-in links handled on screen, plus .onSignInLink for your root

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

One card holds the email field, Continue with email in the accent and a line saying no password is needed. The email stays quiet until Continue is tapped, then says what's wrong under the field and keeps checking live as it's corrected; Return continues. Continue runs your closure: a spinner takes the label's place, a check follows, and a thrown error's message shows under the button. Once the link is on its way the card turns to the sent state: an open envelope, "Check your inbox" and the address, how long the link works, Open Mail in the accent, and "Didn't get it?" with Resend counting down from 0:30 before it can be tapped, then confirming "Sent". "Enter a code instead" opens six boxes over one real field, so the keyboard offers the code from Mail; the code submits itself once full, and a wrong one shows your error, clears the boxes and keeps focus. When the person taps the link and your app opens with it, the block runs onLink with the URL, shows progress on Open Mail, and checks. "Use a different email" under the card goes back to the field. Show the block redacted while a session restores and it draws grey shapes in its own layout.

Notes

  • MagicLinkSignIn(email:resendAfter:linkLifetime:onSendLink:onVerifyCode:onLink:linkMatches:onOpenMail:onSuccess:). Only onSendLink is required. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column, such as a ScrollView with .padding(.horizontal, 20), or a sheet.
  • onSendLink receives the trimmed email. It also runs for Resend. Return to show the sent state, throw to fail: the block shows a thrown LocalizedError's errorDescription, or "Couldn't send the link. Try again." A CancellationError goes back to rest quietly.
  • onVerifyCode receives MagicLinkSignIn.CodeAttempt (the address and the six digits). Passing it shows "Enter a code instead" in the sent state; throw to refuse the code.
  • onLink receives a URL the app was opened with while the block is on screen: a custom-scheme URL through onOpenURL, or a universal link through NSUserActivityTypeBrowsingWeb. linkMatches decides which URLs are sign-in links (every URL by default). Exchange the link's token with your server inside it; throw to show "That link didn't work. It may have expired, so send a new one."
  • For a link that opens a closed app, the block isn't on screen yet. Attach .onSignInLink(matching:perform:) at your root, which listens for both kinds of link and hands you the URL.
  • After a link or code succeeds the block holds its check for about a second, then calls onSuccess. Move to your app there.
  • onOpenMail replaces Open Mail's default, which opens the Mail app with openURL and message://. If no app takes that URL, the button folds away.
  • email is a Binding<String> to prefill a returning person's address. resendAfter sets the countdown (30 seconds by default). linkLifetime adds "It works for 15 minutes." to the sent line; nil leaves it out.
  • Loading: apply .redacted(reason: .placeholder) and the block draws its field and button as grey shapes, breathing unless Reduce Motion is on.
  • Fields: the email is .emailAddress with the email keyboard and Return reading Continue; the code is .oneTimeCode on the number pad.
  • Haptics: success when a link or code signs in, an error when anything fails, a warning when a submit is refused.
  • VoiceOver reads the email with its error as a hint, the code field as one field with its digits spoken one by one, and the countdown as text. It announces "Check your inbox" with the address, "Sent again", refusals, failures and "Signed in". Every control is at least 44 points tall. At accessibility sizes the sent line and errors wrap and the code boxes share the width.
  • Reduce Motion turns every change into a short fade.
  • MagicLinkSignInExample at the end of MagicLinkSignIn.swift is Fieldnote's sign-in screen from the preview: the app's name, "Sign in" and a line under it, then the block. Its stand-in auth accepts the code 246810 and refuses any other, and treats links whose path starts with /auth as sign-in links.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The card is color.surface with radius.lg and 16 point padding (space.lg).
  • The field and code boxes are 52 points tall on fill with radius.md and a 1 point separator hairline that turns to textPrimary when focused and to danger on an error. The label is type.subhead in textSecondary, input type.body in textPrimary, the prompt textTertiary. Code digits use the display face at 22 points, semibold at most, with tabular figures.
  • Continue with email and Open Mail are the accent, one at a time: accent with accentOn in type.headline, radius.md, 52 points.
  • The sent state's envelope is a 28 point symbol in textSecondary, its title type.title3 in textPrimary, its line type.subhead in textSecondary with the address in textPrimary. The countdown is textTertiary with tabular figures; Resend, "Enter a code instead" and "Use a different email" are textPrimary semibold. The hairline is SPHairline. Errors are type.footnote in danger.
  • States move with motion.snappy, the sent state with motion.smooth, the buttons' press with motion.standard.

Requirements

  • Associated Domains with an applinks: entry for a universal sign-in link, or a URL type for a custom scheme, so the link opens your app.