Docs

iOS setup

Claim your Lander domain with Associated Domains, turn off Flutter's own deep link handler and add an optional custom scheme.

Universal Links open your app instead of Safari when two things agree: your app says it handles a domain (the Associated Domains entitlement), and the domain says it trusts your app (the apple-app-site-association file). Lander hosts the file for you. You add the entitlement.

Before you start

  • You need a paid Apple Developer Program membership. Associated Domains is not available to free personal teams.
  • Know your link domain. Open Build > Domains in the dashboard. Hosted domains are named after your project and organization, for example acme-shop-acme.golander.link. The examples below use acme.golander.link, the domain of Lander's demo project.

Add your iOS app in the dashboard

Open Build > Platforms and fill in the iOS app card:

FieldWhere to find it
Team ID10 characters, for example ABCDE12345. Membership details at developer.apple.com.
Bundle IDYour Runner target's bundle identifier, for example com.acme.shop.
App Store ID (optional)The numeric ID in your App Store URL. Used to send people who do not have the app to the store.
URI scheme (optional)Your custom scheme without ://, for example acmeshop. See step 5.

Save. Lander now serves this file on your link domain:

https://acme.golander.link/.well-known/apple-app-site-association
{
  "applinks": {
    "details": [{ "appIDs": ["ABCDE12345.com.acme.shop"], "components": [{ "/": "/*" }] }]
  }
}

Every path on the domain opens your app. Check it is live:

curl -i https://acme.golander.link/.well-known/apple-app-site-association

You should get 200 with Content-Type: application/json. A 404 means no iOS app is saved for this domain yet. More detail on this card is in Dashboard: Apps.

Add the Associated Domains capability

In Xcode, open ios/Runner.xcworkspace, select the Runner target, then Signing & Capabilities. Click + Capability, add Associated Domains, and add an entry with the applinks: prefix and no https://:

applinks:acme.golander.link

Xcode creates (or updates) ios/Runner/Runner.entitlements:

ios/Runner/Runner.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.developer.associated-domains</key>
	<array>
		<string>applinks:acme.golander.link</string>
	</array>
</dict>
</plist>

All build configurations

Flutter projects have Debug, Release and Profile configurations. Make sure Code Signing Entitlements (CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements) is set for all three in the Runner target's build settings, or links will work in debug and silently fail in your release build.

If you also use a custom domain, add one applinks: entry per domain. Lander serves the file on every verified domain of the project.

Turn off Flutter's built in deep linking

Flutter has its own deep link handler that pushes the URL's path onto your router. With Lander delivering links too, every link would navigate twice. Turn Flutter's off in Info.plist:

ios/Runner/Info.plist
<key>FlutterDeepLinkingEnabled</key>
<false/>

Check your app lifecycle (nothing to change)

Both iOS app lifecycles work with no code changes:

  • Apps with a FlutterSceneDelegate (the default for new Flutter projects).
  • Older apps that only have a FlutterAppDelegate.

The plugin registers for scene and app delegate callbacks and handles Universal Links (NSUserActivity) and URL opens on cold start (scene(_:willConnectTo:options:)) and warm start. Leave your AppDelegate.swift and SceneDelegate.swift as Flutter generated them.

Every callback returns false, so other plugins (sign in redirects, for example) still receive their URLs. Those URLs also reach Lander.onDeepLink, so ignore routes you do not handle.

If you use multiple scenes with a custom root view controller, register your engine with the scene lifecycle as described in Flutter's FlutterSceneLifeCycleEngineRegistration docs.

A custom scheme such as acmeshop:// is a fallback that always opens the app. It matters most if you use Lander's Continue in app page: iOS keeps a Universal Link tapped on a page of the same domain inside Safari, so that page's "open it" button uses your scheme when you have one.

ios/Runner/Info.plist
<key>CFBundleURLTypes</key>
<array>
	<dict>
		<key>CFBundleURLName</key>
		<string>com.acme.shop</string>
		<key>CFBundleURLSchemes</key>
		<array>
			<string>acmeshop</string>
		</array>
	</dict>
</array>

Then enter the same scheme (acmeshop) as the URI scheme of your iOS app in Build > Platforms. Lowercase letters, digits, +, . and - only, starting with a letter.

Custom scheme URLs reach your app without a network call. acmeshop://product/123 routes to /product/123 (the host counts as the first path segment).

Privacy manifest

The plugin bundles its own PrivacyInfo.xcprivacy, so you do not need to add anything for Lander. It declares:

Data typeLinked to userTrackingPurposes
Device ID (the random install ID)NoNoAnalytics, App Functionality
Product Interaction (opens and events)NoNoAnalytics

NSPrivacyTracking is false, there are no tracking domains, and no required reason APIs are used. Xcode merges this into your app's privacy report. See Privacy for exactly what the SDK sends.

Local development over HTTP

Only needed if you point the SDK at a Lander API running on your machine (http://localhost:8787). App Transport Security blocks plain HTTP by default, so allow local networking:

ios/Runner/Info.plist
<key>NSAppTransportSecurity</key>
<dict>
	<key>NSAllowsLocalNetworking</key>
	<true/>
</dict>

Try it

Build and run on a device or simulator, then open a link from the terminal:

xcrun simctl openurl booted "https://acme.golander.link/p-123"

A Universal Link only opens the app on the simulator when iOS has fetched the domain's apple-app-site-association file. If Safari opens instead, try the custom scheme (xcrun simctl openurl booted "acmeshop://product/123") to check your routing, and see Troubleshooting for the domain side.

On this page