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.
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.
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"
Add your domain to the Associated Domains list:
applinks:yourdomain.com applinks:www.yourdomain.com
Note: Use applinks: prefix for Universal Links. You can add multiple domains if needed.
Ensure your app includes the proper URL handling configuration:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>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.
Create a JSON file without a file extension:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.myapp",
"paths": [
"/product/*",
"/user/*",
"/share/*"
]
}
]
}
}You need your Apple Team ID to create the appID. You can find it:
1. Go to developer.apple.com
2. Sign in with your Apple Developer account
3. Go to Account → Membership
4. Copy your Team ID (10-character code)
Host the file on your web server at the correct path:
https://yourdomain.com/.well-known/apple-app-site-association
Tip: Validate your AASA file using our AASA Validator tool to ensure it's correctly formatted.
Add the uni_links package to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
uni_links: ^0.9.0Implement a handler for incoming Universal Links:
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
class UniversalLinkHandler {
static StreamSubscription? _deepLinkSubscription;
static Future<void> 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();
}
}Call the handler in your main widget:
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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@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 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
Check if your AASA file is properly hosted:
curl -I https://yourdomain.com/.well-known/apple-app-site-association
Expected response: HTTP/1.1 200 OK
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.
Cause: AASA file not found or improperly configured
Solution:
/.well-known/apple-app-site-associationCause: Deep link handler not properly initialized
Solution:
Cause: Capability not added in Xcode
Solution:
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.