iOS Setup Checklist
Verify your iOS integration end to end — dashboard values, Associated Domains, URL schemes, SDK logs — and test every deep link flow before you ship.
Use this page to confirm an iOS integration is correct before release, or to work out which piece is wrong when links misbehave. It runs in order from the dashboard to the device, and each step has a concrete check with an expected result. If a step fails, stop there: the later steps depend on it.
Run every check twice — once for the test environment and once for production. The dashboard gives you a separate link domain and URL scheme for each, and most broken setups have only one of the two configured.
1. Dashboard values match the app
Open your project in the Grovs dashboard and compare against Xcode:
| Dashboard field | Where to find the real value | Check |
|---|---|---|
| Bundle Identifier | Target → General → Bundle Identifier | Exact match, including case. See How to Get the Bundle Identifier. |
| Apple App Prefix (Team ID) | Target → Signing & Capabilities → Team, or developer.apple.com → Membership | Exact match. See How to Get the Apple App Prefix. |
| Link domains | Dashboard project setup, one per environment | Note both. You will add them to Xcode in step 2. |
| URL schemes | Dashboard project setup, one per environment | Note both. You will add them in step 3. |
Together these form TEAMID.bundle.identifier, which is what Apple's association file identifies your app by. A typo in either breaks Universal Links for every user, and only shows up once the app is installed from a build that has the wrong value.
2. Associated Domains are set up
In Xcode, under Signing & Capabilities, the Associated Domains capability must list an applinks: entry for each link domain from the dashboard:
applinks:yourapp.grovs.link
applinks:yourapp.test-sqd.linkUse the exact hostnames the dashboard shows you. If you use a custom domain, add that too. Follow How to Add an Associated Domain.
Check the association file is served. Open this in a browser for each domain:
https://<your-link-domain>/.well-known/apple-app-site-associationIt must return JSON that lists your TEAMID.bundle.identifier under applinks. If your app is missing from it, the dashboard values in step 1 are wrong. The Team ID and bundle ID are re-checked here because a mismatch is by far the most common cause.
Check the entitlement made it into the build. Associated Domains is an entitlement, so it must be present in the provisioning profile too. A build that shows the capability in Xcode but was signed with a profile created before you added it will silently lack it. Confirm with:
codesign -d --entitlements :- /path/to/YourApp.app | grep -A3 associated-domainsThe output should list the same applinks: entries.
iOS fetches the association file once, at install time, through Apple's CDN. After fixing anything in steps 1 or 2, delete the app from the device and reinstall it. The CDN can lag behind a corrected file for a while.
Check the association on the device
iOS keeps its own record of which domains it has verified for your app, and it can tell you directly. These tools live under Settings → Developer, which appears once the device has been used with Xcode. On iOS 16 and later you also need Settings → Privacy & Security → Developer Mode turned on.
Universal Links diagnostics. Go to Settings → Developer → Universal Links → Diagnostics, paste one of your Grovs links, and tap Diagnose. The result tells you what iOS would do with that URL:
| Result | Meaning |
|---|---|
Opens installed application, with your TEAMID.bundle.identifier | The association is verified. If tapping the link still opens Safari, the problem is in how you are testing (step 6) or in delegate forwarding (step 4), not the domain setup. |
| Opens in Safari, or "no matching apps" | iOS has no verified association for that domain. Recheck the applinks: entry and the association file in step 2, then reinstall the app. |
| An error fetching or parsing the association file | The file is not reachable over HTTPS with a valid certificate, or the JSON is malformed. Check the .well-known URL from step 2 in a browser. Custom domains most often fail here. |
Run it for the test link domain and for the production one.
Associated Domains Development. On the same screen, the Associated Domains Development switch makes iOS fetch the association file directly from your domain instead of Apple's CDN, so a fix you just made is visible on the next install without waiting for the CDN. Turn it off before testing a release candidate, since real users go through the CDN.
Reading the system's verification log. If diagnostics report a failure without saying why, the association daemon's own status is in a sysdiagnose. On the device, press and release both volume buttons and the side button together for about a second, wait a few minutes, then share the archive from Settings → Privacy & Security → Analytics & Improvements → Analytics Data (it is named sysdiagnose_...). Inside, swcutil_show.txt lists every app and domain pair with fields like these:
Service: applinks
App ID: TEAMID.com.example.app
App Version: 12
Domain: yourapp.grovs.link
Patterns: {"/":"*"}
User Approval: true
Site/Fmwk Approval: true
Flags:
Last Checked: 2026-09-14 10:21:33 +0000
Next Check: 2026-09-19 09:41:12 +0000Site/Fmwk Approval: true means the association file was fetched and lists your app. false means the fetch failed or the file does not list this App ID; an Error: line then names the cause, such as a certificate problem or a 404. User Approval: false means the user opted out of Universal Links for that domain on this device, which the long-press in step 6 restores. A missing entry for one of your domains means the applinks: entitlement for it is not in the installed build.
The same check runs on a Mac for a Mac Catalyst or Designed for iPad build with sudo swcutil show.
Watching it live. Connect the device to a Mac, open Console.app, select the device, and filter by process swcd. Reinstall the app and you will see the fetch for each associated domain and any error it hits.
3. URL schemes are registered
Under Info → URL Types, add one entry per scheme from the dashboard, with the Role set to Editor. Follow How to Add a New URL Scheme.
Check it from the SDK. Once the SDK authenticates it compares the scheme the dashboard expects for the current environment with your Info.plist. With info logging on (step 5) you will see one of:
URL Scheme properly configured.
There's a mismatch between the URL Scheme in the project and the one from the dashboard, deeplinking won't function properly!The mismatch line means the scheme for the environment you configured (useTestEnvironment) is missing from URL Types, or was added with a role other than Editor.
Check it from the device. Type the scheme into Safari's address bar:
yourscheme://openSafari asks to open your app. If it does not, the scheme is not registered in the installed build.
4. Delegate calls are forwarded
Universal Links and URL schemes both arrive through system callbacks. The SDK only sees them if you forward those callbacks:
- SceneDelegate apps: forward
scene(_:openURLContexts:),scene(_:continue:), andscene(_:willConnectTo:options:). - AppDelegate-only apps: forward
application(_:continue:restorationHandler:)andapplication(_:open:options:).
Copy them exactly from Delegate setup. The willConnectTo / options call is the one people forget, and it is the one that handles a link that launches the app from a cold start. Without it, links work only while the app is already running.
Also confirm Grovs.delegate is set — either passed to configure or assigned afterwards — and that the object you assign stays alive. The delegate is held weakly, so a delegate created inline and not stored anywhere is released immediately and never called.
SwiftUI apps using the App lifecycle without an AppDelegate need a UIApplicationDelegateAdaptor (and a scene delegate through application(_:configurationForConnecting:options:)) to receive these callbacks; onOpenURL alone does not reach the SDK.
5. The SDK authenticates
Turn on info logging before configure and run the app:
Grovs.setDebug(level: .info)
Grovs.configure(APIKey: "your-api-key", useTestEnvironment: true, delegate: self) { success in
print("Grovs configured: \(success)")
}Expected in the Xcode console:
Test environment enabled. // only when useTestEnvironment is true
Authenticate
URL Scheme properly configured.and Grovs configured: true. What the failures look like:
| Console line | Meaning |
|---|---|
[authentication_failed] Authentication failed. Check API key and bundle ID. followed by Can not initialize the SDK, the Bundle Key combo is invalid | Authentication failed. Either the API key does not belong to a project whose bundle ID matches this app, the key is for the other environment, or the request never reached the backend (no network). Check the key and step 1, then connectivity. The SDK retries on its own when the app becomes active. |
[authentication_failed] URI schemes are not configured. SDK won't work. | The app has no URL types at all. Step 3. |
baseURL is not a valid URL: ... The SDK was not configured. | A self-hosted baseURL that is not a URL with a host. configure reports false and does nothing else. |
The SDK is configured but disabled — skipping authentication. | You passed enabled: false to configure. Expected when gating on consent; see Privacy & Consent. |
In release builds the log content is redacted to <private> in Console.app. Use a debug build for this step.
Match useTestEnvironment to the API key: test key → true, production key → false. A production link opened by a test-mode build resolves to nothing, and vice versa. See Test vs production environment mismatch.
6. Test each flow
Create a test link in the dashboard for the environment you are checking, with a payload you can recognize, for example {"screen": "checklist", "id": "42"}. Then test the flows below on a physical device. Universal Links do not work in the Simulator; only the URL scheme flow can be exercised there.
For the Universal Link and deferred flows, the expected result is the same: grovsReceivedPayloadFromDeeplink is called on the main thread with link set to the URL and payload containing your test values.
Universal Link, app running
Paste the link into Notes and tap it. The app should come to the foreground and the delegate should fire.
Do not test from Safari's address bar: typing or pasting a URL there never triggers a Universal Link, by design. Tapping a link inside a Safari page, Messages, Mail, or Notes does.
Universal Link, cold start
Force-quit the app, then tap the link in Notes again. The app should launch and the delegate should fire. If this flow fails while the previous one works, scene(_:willConnectTo:options:) (or the AppDelegate equivalent) is not forwarded — step 4.
URL scheme
Open the environment's scheme from Notes, or from the Simulator:
xcrun simctl openurl booted "yourscheme://"The app should open, and with info logging on you will see the SDK handle the call. The SDK forwards every scheme URL it receives to the backend, so a payload only arrives when the URL identifies a Grovs link; opening the bare scheme is enough to prove the registration and the open:options: / openURLContexts forwarding. Universal Links fall back to this path when a user has opted out of them, so it must work too.
Restore a Universal Link that was switched off
If you ever tapped Open in Safari on the Universal Link banner, iOS remembers that choice for the domain and opens Safari from then on. Long-press the link and choose Open in "YourApp" to restore it. This is a per-device setting and a common reason a link "stopped working" on one tester's phone.
Deferred deep link
This flow is for users who click a link before the app is installed. Test it with a real install path, not Xcode:
- Delete the app from the device.
- Open the link on the device in Safari. It should land on the store page (or the preview page first, if enabled for the link).
- Install the app from TestFlight or the App Store, and open it.
The delegate fires on the first launch with the link's payload. Installing from Xcode instead breaks the match because it skips the click-to-install path the SDK correlates. See Deferred deep linking for the other pitfalls.
If copy-to-clipboard is enabled for the link or the project, iOS may show its paste confirmation on that first launch. Allow it. With info logging on, the Clipboard flow - lines show what the SDK found. See Clipboard-assisted deferred deep linking.
Confirm from the dashboard
Every resolved link produces an open, and a fresh install produces an install, in the project's analytics. After the tests above, check that the link's click and open counts moved in the environment you tested. If the device received the payload but the dashboard shows nothing, you are looking at the other environment.
7. Before you ship
- Repeat steps 2, 3, and 6 against a production build with the production API key,
useTestEnvironment: false, and a production link. TestFlight builds are the right place to do this. - Confirm the production
applinks:entry and URL scheme are in the target, not only the test ones. - Turn Associated Domains Development off on any device you use for the final check, so it goes through Apple's CDN like your users will.
- If you use a custom domain, confirm its association file is reachable over HTTPS with a valid certificate; iOS refuses the file otherwise. See Custom Domains.
- Leave
setDebugat the default.errorlevel, or remove the call. Logging is redacted in release builds either way.
See also
- Deep link doesn't open the app — symptom-by-symptom fixes for links that open Safari
- SDK configuration issues — initialization failures and environment mismatches
- iOS Deep Linking — the delegate, error callbacks, and past payloads
- iOS Quick Start — full configuration and delegate setup