# How to Set Up Universal Links in Flutter (iOS)
> Set up Universal Links in Flutter for iOS with step-by-step Xcode configuration, AASA file creation, Dart implementation, and troubleshooting.
- Canonical: https://redirectly.app/blog/flutter-universal-links-ios-setup
- Site: [Redirectly](https://redirectly.app) — deferred deep linking for Flutter & React Native
- Published: 2024-02-10
- Author: Redirectly
- Category: Flutter · 8 min read
Master Universal Links for iOS with this complete step-by-step guide. Learn Xcode configuration, AASA file setup, and Dart implementation for seamless deep linking.
## Table of Contents
1. What are Universal Links?
2. Xcode Configuration
3. AASA File Setup
4. Dart Code Implementation
5. Testing Universal Links
6. Common Issues & Fixes
## What are Universal Links?
**Definition**
Universal Links are standard HTTPS URLs that iOS recognizes and routes directly to your app when it's installed. They provide a seamless experience where users never see an app chooser dialog and can be opened on both web and mobile without app-specific URL schemes.
### Key Advantages
- No app chooser dialog
- Works as web links too
- Native iOS integration
- Better user experience
- SEO friendly
- Fallback to web version
### How They Work
- User clicks an HTTPS link
- iOS checks Associated Domains
- iOS validates AASA file
- App opens if installed
- Fallback to web if not
- No manual routing needed
## Xcode Configuration
### Step 1: Add Associated Domains Entitlement
Open your Flutter project in Xcode and configure the Associated Domains capability:
1. Open `ios/Runner.xcworkspace`
2. Select the Runner project in the navigator
3. Select the Runner target
4. Go to Signing & Capabilities tab
5. Click + Capability button
6. Search for and add "Associated Domains"
### Step 2: Configure Domain Entries
Add your domain to the Associated Domains list:
```text
applinks:yourdomain.com
applinks:www.yourdomain.com
```
**Note:** Use `applinks:` prefix for Universal Links. You can add multiple domains if needed.
### Step 3: Update Info.plist
Ensure your app includes the proper URL handling configuration:
```xml
CFBundleURLTypes
CFBundleURLName
com.example.myapp
CFBundleURLSchemes
myapp
```
## AASA File Setup
### What is AASA?
The Apple App Site Association (AASA) file is a JSON file that tells iOS which URLs should be handled by your app. It must be hosted at `/.well-known/apple-app-site-association` on your domain.
### Step 1: Create the AASA File
Create a JSON file without a file extension:
```json
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.myapp",
"paths": [
"/product/*",
"/user/*",
"/share/*"
]
}
]
}
}
```
### Step 2: Find Your Team ID
You need your Apple Team ID to create the appID. You can find it:
1. Go to [developer.apple.com](https://developer.apple.com)
2. Sign in with your Apple Developer account
3. Go to Account → Membership
4. Copy your Team ID (10-character code)
### Step 3: Upload AASA File
Host the file on your web server at the correct path:
```text
https://yourdomain.com/.well-known/apple-app-site-association
```
**Tip:** Validate your AASA file using our [AASA Validator](https://redirectly.app/aasa-validator.md) tool to ensure it's correctly formatted.
## Dart Code Implementation
### Step 1: Add uni_links Package
Add the uni_links package to your pubspec.yaml:
```yaml
dependencies:
flutter:
sdk: flutter
uni_links: ^0.9.0
```
### Step 2: Create Deep Link Handler
Implement a handler for incoming Universal Links:
```dart
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
class UniversalLinkHandler {
static StreamSubscription? _deepLinkSubscription;
static Future initialize(BuildContext context) async {
// Handle link when app is opened from terminated state
final String? initialLink = await getInitialLink();
if (initialLink != null) {
_handleDeepLink(initialLink, context);
}
// Listen for deep links when app is running
_deepLinkSubscription = linkStream.listen(
(String link) {
_handleDeepLink(link, context);
},
onError: (err) {
debugPrint('Deep link error: $err');
},
);
}
static void _handleDeepLink(String link, BuildContext context) {
final Uri uri = Uri.parse(link);
debugPrint('Received deep link: $link');
debugPrint('Scheme: ${uri.scheme}');
debugPrint('Host: ${uri.host}');
debugPrint('Path: ${uri.path}');
// Route based on path
if (uri.pathSegments.isNotEmpty) {
final String firstSegment = uri.pathSegments[0];
switch (firstSegment) {
case 'product':
if (uri.pathSegments.length > 1) {
final String productId = uri.pathSegments[1];
_navigateToProduct(context, productId);
}
break;
case 'user':
if (uri.pathSegments.length > 1) {
final String userId = uri.pathSegments[1];
_navigateToUser(context, userId);
}
break;
case 'share':
if (uri.pathSegments.length > 1) {
final String shareCode = uri.pathSegments[1];
_handleShare(context, shareCode);
}
break;
default:
_navigateToHome(context);
}
}
}
static void _navigateToProduct(BuildContext context, String productId) {
debugPrint('Navigate to product: $productId');
// Implementation: Navigate to product screen with productId
// Example: Navigator.of(context).pushNamed('/product', arguments: productId);
}
static void _navigateToUser(BuildContext context, String userId) {
debugPrint('Navigate to user: $userId');
// Implementation: Navigate to user profile screen
}
static void _handleShare(BuildContext context, String shareCode) {
debugPrint('Handle share code: $shareCode');
// Implementation: Handle referral or share code
}
static void _navigateToHome(BuildContext context) {
debugPrint('Navigate to home');
// Implementation: Navigate to home screen
}
static void dispose() {
_deepLinkSubscription?.cancel();
}
}
```
### Step 3: Initialize in Main App
Call the handler in your main widget:
```dart
import 'package:flutter/material.dart';
import 'deep_link_handler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void initState() {
super.initState();
// Initialize deep link handler after widget binding
WidgetsBinding.instance.addPostFrameCallback((_) {
UniversalLinkHandler.initialize(context);
});
}
@override
void dispose() {
UniversalLinkHandler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: const HomeScreen(),
);
}
}
```
## Testing Universal Links
### Test on Real Device
Testing on a physical iOS device is essential since the simulator has limitations:
1. Build your app to a real iOS device
2. Open Notes or Safari app
3. Type or paste your Universal Link: `https://yourdomain.com/product/123`
4. Long-press the link and tap "Open in App"
5. App should open and navigate to the correct screen
### Using Apple's Validation Tool
Check if your AASA file is properly hosted:
```bash
curl -I https://yourdomain.com/.well-known/apple-app-site-association
```
Expected response: `HTTP/1.1 200 OK`
### Simulator Limitations
The iOS Simulator cannot properly test Universal Links due to sandbox restrictions. Always test on a real device with a valid Apple Developer account and properly signed build.
## Common Issues & Fixes
### Issue: Link Opens in Safari Instead of App
**Cause:** AASA file not found or improperly configured
**Solution:**
- Verify AASA file exists at `/.well-known/apple-app-site-association`
- Check AASA JSON syntax using our [validator](https://redirectly.app/aasa-validator.md)
- Ensure appID matches your bundle ID and Team ID
- Check domain is listed in Associated Domains entitlement
### Issue: Deep Link Not Received by App
**Cause:** Deep link handler not properly initialized
**Solution:**
- Initialize handler in main.dart with proper context
- Add debug prints to verify handler is called
- Check that uni_links package is properly added
- Ensure app is fully launched before testing
### Issue: Associated Domains Entitlement Missing
**Cause:** Capability not added in Xcode
**Solution:**
- Re-open Xcode project
- Go to Signing & Capabilities tab
- Verify Associated Domains is present
- Re-add if missing and rebuild
## Ready for Android?
Once you've mastered Universal Links for iOS, implement the Android equivalent using App Links. Learn how to set up App Links in our complete Android guide.
- [Android App Links Setup](https://redirectly.app/blog/flutter-android-app-links-setup.md)
- [Deferred Deep Linking](https://redirectly.app/flutter-deferred-deep-linking.md)
## Related Articles
- [Android App Links Setup](https://redirectly.app/blog/flutter-android-app-links-setup.md) — Learn how to configure App Links for Android with complete setup and Dart implementation guide.
- [Deep Linking with go_router](https://redirectly.app/blog/flutter-deep-linking-go-router.md) — Master go_router for advanced deep linking with route guards and deferred link handling.
---
Full site index for AI agents: https://redirectly.app/llms.txt