Why sign-up emails are awkward to test on iOS
- There’s no inbox on the device. The simulator has no Mail app, and signing a real email account into a test iPhone means one shared inbox full of codes from every run.
- Links have to open the app, not Safari. A verify or magic link is usually an
httpsUniversal Link. It opens your app only when Associated Domains is set up correctly; otherwise it opens in Safari and the sign-in finishes in the wrong place, or not at all. - Custom-scheme links can’t be clicked from a laptop. A
myapp://verify?token=…link does nothing in a desktop browser, so a tester reading mail on their Mac can’t just click it. - Single-use links break if anything opens them first. Link scanners and previews in some mail setups use them up. Proxy Mail QA never opens or prefetches a link.
Test it by hand in the simulator
- Create an address in Proxy Mail QA. Turn on a persona and a generated password if your sign-up form asks for a name or a strong password.
- Sign up in the simulator with it. The simulator shares your Mac’s clipboard, so copy the address on the Mac and paste it into the app.
- Get the code. It appears on the address’s row with a Copy button within seconds of arriving. Paste it into the app.
- Or open the link in the app. Copy the link and run
xcrun simctl openurl booted '<link>'in Terminal. For amyapp://link the inbox shows that command next to the link, ready to copy. iOS asks “Open in ‘YourApp’?” the first time a custom scheme opens.
Automate it with XCUITest
Add the small Swift helper from the XCUITest guide to your UI test target, set PROXYMAIL_QA_API_KEY in the scheme or test plan, and the test reads the code itself:
let qa = try ProxyMailQA()
let account = try await qa.createAddress()
let app = XCUIApplication()
app.launch()
app.textFields["Email"].tap()
app.textFields["Email"].typeText(account.address)
let submitted = Date()
app.buttons["Create account"].tap()
let code = try await qa.waitForCode(account.address, since: submitted)
app.textFields["Code"].typeText(code)
For a link, app.open(try await qa.waitForLink(account.address, since: submitted)) opens it the way tapping it in an email would, without the “Open in” prompt. If the email has both a web link and a myapp:// one, pass app: true to get the app’s.
The same helper checks the email itself (checkEmail with your Universal Link domain, and countEmails for duplicates), signs in as a saved test account, and adds each test to the pull request’s report: see the XCUITest guide.
Or let a coding agent do it
Claude Code, Codex and Cursor can drive the simulator, create the address, type it in and read the code over Proxy Mail QA’s MCP server. See iOS apps with a coding agent.
What to check in SwiftUI and UIKit
- The email field doesn’t change what’s typed. Without
.textInputAutocapitalization(.never)and.autocorrectionDisabled()in SwiftUI (orautocapitalizationType = .noneandautocorrectionType = .noin UIKit), iOS can capitalise or “correct” an address. Use.keyboardType(.emailAddress)and.textContentType(.emailAddress)too. - The code field offers AutoFill.
.textContentType(.oneTimeCode)in SwiftUI, ortextContentType = .oneTimeCodein UIKit, lets iOS suggest the code above the keyboard on a real device. - The link works from a cold start. This is the most common bug. In SwiftUI,
.onOpenURLreceives both Universal Links and custom-scheme links whether the app was running or not. In UIKit, a link that launches the app arrives inscene(_:willConnectTo:options:)(inconnectionOptions.userActivitiesorurlContexts), not inscene(_:continue:)orscene(_:openURLContexts:). Test with the app closed as well as open. - The link is still single-use. Open it twice: the second time should explain that it has been used, not crash or sign in again.
- It doesn’t point at your Mac. A link to
localhostopens in the simulator, which shares your Mac’s network, but nowhere else. Proxy Mail QA flags local links; see links that point at localhost.
Using Firebase email-link sign-in? It has its own setup on iOS: see Firebase email-link sign-in on mobile.