# 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