iOS Universal Links Guide
Complete step-by-step guide to implement, configure, and test iOS Universal Links in your mobile application
Table of Contents
1. Overview & Prerequisites
What are iOS Universal Links?
iOS Universal Links are HTTPS URLs that allow users to intelligently follow links to content inside your app or to your website. When a Universal Link is opened, iOS checks if any installed app can handle that link, and if so, opens the app instead of a web browser.
Types of iOS Deep Links
Custom URL Schemes
Basic app-specific URL schemes
myapp://product/123
Standard Web Links
HTTPS URLs that open in Safari
https://example.com/product/123
Universal Links
Verified HTTPS URLs that open directly in app
https://verified.com/product/123
- • Seamless transition from web to app
- • Works even if your app isn't installed (falls back to website)
- • No custom URL scheme registration needed
- • Better user experience and engagement
- • Secure domain verification process
Prerequisites
- • Xcode 12.0 or later
- • iOS 14.0+ deployment target
- • Swift 5.3+ knowledge
- • Apple Developer Account
- • Domain ownership
- • HTTPS enabled
- • Web server access
- • SSL certificate
2. Configure Associated Domains
Add Associated Domains Capability
Configure your Xcode project to declare which domains your app can handle Universal Links for.
1. Open your Xcode project
2. Select your app target
3. Go to Signing & Capabilities tab
4. Click + Capability
5. Add "Associated Domains"
6. Add your domains with applinks: prefix
Domain Configuration Examples
applinks:yourdomain.com applinks:www.yourdomain.com applinks:api.yourdomain.com applinks:subdomain.yourdomain.com // For development/testing applinks:dev.yourdomain.com applinks:staging.yourdomain.com
Important Notes
- • Always use
applinks:
prefix (nothttps://
) - • Don't include paths in the domain entry
- • Add both www and non-www versions if needed
- • Each domain requires a separate entry
3. Setup App Site Association
Create apple-app-site-association File
This file tells iOS which app should handle Universal Links for your domain. Create this JSON file and host it on your web server.
This example uses the modern components format (iOS 13+). For legacy support, you can also use the paths format, but components provides more flexibility and better fragment handling.
{ "applinks": { "details": [ { "appIDs": ["TEAMID.com.yourcompany.yourapp"], "components": [ { "/": "/product/*", "comment": "Handles product pages" }, { "/": "/dashboard", "comment": "Handles dashboard" }, { "/": "/profile/*", "comment": "Handles profile pages" }, { "/": "/admin/*", "exclude": true, "comment": "Excludes admin pages" } ] } ] }, "webcredentials": { "apps": ["TEAMID.com.yourcompany.yourapp"] } }
Components Pattern Examples
Include Patterns
{ "/": "/product/*" }
All product pages
{ "/": "/dashboard" }
Exact path match
{ "/": "/*" }
All paths (use carefully)
Exclusion Patterns
{ "/": "/admin/*", "exclude": true }
Exclude admin pages
{ "#": "no_universal_links", "exclude": true }
Exclude URLs with fragment
{ "/": "*.pdf", "exclude": true }
Exclude PDF files
Find Your Team ID
Method 1: Apple Developer Portal
Go to developer.apple.com/account → Membership → Team ID
Format: 10 alphanumeric characters (e.g., TEAM123456)
Method 2: Xcode
Project → Target → Signing & Capabilities → Team dropdown shows Team ID in parentheses
Look for text like "Team Name (TEAM123456)"
Method 3: App Store Connect
App Store Connect → App Information → General Information → Bundle ID (shows full App ID)
The Team ID is the prefix before the bundle identifier
Host the File
- • Host at:
https://yourdomain.com/.well-known/apple-app-site-association
- • Also accessible at:
https://yourdomain.com/apple-app-site-association
- • Serve with
Content-Type: application/json
- • Must be accessible via HTTPS
- • No file extension (.json) needed
- • No redirects allowed
- • Include proper caching headers (max-age=3600)
Web Credentials (Optional)
Web credentials enable password AutoFill between your website and iOS app, creating a seamless authentication experience.
- • Automatic password suggestions from Keychain
- • Seamless login experience across web and app
- • Shared authentication state
- • Enhanced security with verified domains
4. Handle URLs in SwiftUI
Basic URL Handling (SwiftUI)
import SwiftUI struct ContentView: View { @StateObject private var navigationManager = NavigationManager() var body: some View { NavigationStack(path: $navigationManager.path) { HomeView() .navigationDestination(for: String.self) { route in destinationView(for: route) } } .onOpenURL { url in handleUniversalLink(url) } } private func handleUniversalLink(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } let path = components.path let queryItems = components.queryItems switch path { case let p where p.hasPrefix("/product"): if let productId = queryItems?.first(where: { $0.name == "id" })?.value { navigationManager.navigateToProduct(id: productId) } case "/dashboard": navigationManager.navigateToDashboard() case let p where p.hasPrefix("/profile"): if let userId = queryItems?.first(where: { $0.name == "user" })?.value { navigationManager.navigateToProfile(userId: userId) } default: print("Unknown path: \(path)") } } @ViewBuilder private func destinationView(for route: String) -> some View { let components = route.components(separatedBy: ":") let destination = components[0] switch destination { case "product": if components.count > 1 { ProductDetailView(productId: components[1]) } case "dashboard": DashboardView() case "profile": if components.count > 1 { ProfileView(userId: components[1]) } default: Text("Unknown destination") } } }
Navigation Manager
import SwiftUI @MainActor class NavigationManager: ObservableObject { @Published var path = NavigationPath() func navigateToProduct(id: String) { // Validate product ID guard isValidProductId(id) else { print("Invalid product ID: \(id)") return } path.append("product:\(id)") } func navigateToDashboard() { path.append("dashboard") } func navigateToProfile(userId: String) { // Validate user ID guard isValidUserId(userId) else { print("Invalid user ID: \(userId)") return } path.append("profile:\(userId)") } func popToRoot() { path.removeLast(path.count) } // MARK: - Validation private func isValidProductId(_ id: String) -> Bool { // Validate product ID format let regex = "^[a-zA-Z0-9_-]{1,50}$" return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: id) } private func isValidUserId(_ id: String) -> Bool { // Validate user ID format let regex = "^[a-zA-Z0-9_-]{1,30}$" return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: id) } }
UIKit App Delegate Approach
import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } // Handle the Universal Link return handleUniversalLink(url) } private func handleUniversalLink(_ url: URL) -> Bool { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } let path = components.path // Post notification for SwiftUI to handle NotificationCenter.default.post( name: .universalLinkReceived, object: url ) return true } } extension Notification.Name { static let universalLinkReceived = Notification.Name("universalLinkReceived") }
🔒 Security Considerations
Input Validation
Always validate incoming URL parameters to prevent security vulnerabilities.
private func handleUniversalLink(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } let path = components.path // ❌ Bad: Direct usage without validation // let productId = queryItems?.first(where: { $0.name == "id" })?.value // navigationManager.navigateToProduct(id: productId!) // ✅ Good: Validate input first guard isValidPath(path) else { print("Invalid path: \(path)") return } if let productId = queryItems?.first(where: { $0.name == "id" })?.value { guard isValidProductId(productId) else { print("Invalid product ID: \(productId)") return } navigationManager.navigateToProduct(id: productId) } } private func isValidPath(_ path: String) -> Bool { let allowedPaths = ["/product", "/dashboard", "/profile"] return allowedPaths.contains { path.hasPrefix($0) } } private func isValidProductId(_ id: String) -> Bool { let regex = "^[a-zA-Z0-9_-]{1,50}$" return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: id) }
Authentication & Authorization
- • Always verify user authentication before accessing sensitive content
- • Validate user permissions for accessed resources
- • Don't expose sensitive data through URL parameters
- • Use secure tokens for authenticated deep links
- • Implement rate limiting for URL processing
Best Security Practices
HTTPS Only
Universal Links only work with HTTPS, providing built-in security against man-in-the-middle attacks.
Domain Verification
iOS automatically verifies domain ownership through the apple-app-site-association file.
Graceful Fallbacks
Always handle invalid URLs gracefully without exposing sensitive information or crashing.
Path Restrictions
Use specific path patterns in apple-app-site-association to limit which URLs open your app.
6. Testing & Debugging
Testing Methods
1. Safari Testing
Open Safari on your device and navigate to your Universal Link URL.
https://yourdomain.com/product?id=123
2. Notes App Testing
Paste your Universal Link in the Notes app and tap it.
Open Notes → Type URL → Tap the link
3. Messages Testing
Send yourself a message with the Universal Link and tap it.
iMessage → Send link to yourself → Tap
Debugging Tools
Apple App Site Association Validator
Use Apple's official validator:
search.developer.apple.com/appsearch-validation-tool/Console Debugging
Add logging to track URL handling:
print("Received URL: \\(url)")
Device Console
Connect device to Xcode and check console logs for Universal Link processing.
Network Inspector
Verify iOS can fetch your apple-app-site-association file over HTTPS.
Testing Checklist
- ✓ apple-app-site-association file is accessible via HTTPS
- ✓ Associated Domains capability is properly configured
- ✓ App ID matches the one in apple-app-site-association
- ✓ URL handling code is implemented and tested
- ✓ Links work on physical device (not simulator)
- ✓ Links work from different apps (Safari, Messages, Notes)
- ✓ Fallback to website works when app isn't installed
7. Common Issues & Solutions
❌ Universal Links open Safari instead of app
- • Check apple-app-site-association file is accessible and valid
- • Verify Team ID and Bundle ID match exactly
- • Ensure Associated Domains capability is properly configured
- • Test on physical device (Universal Links don't work in Simulator)
- • Check that the URL path is included in the "paths" array
⚠️ Apple-app-site-association file not found
- • Ensure file is at /.well-known/apple-app-site-association
- • Check file has no .json extension
- • Verify Content-Type: application/json header
- • Test file accessibility in browser
- • Ensure no redirects are happening
ℹ️ Universal Links work inconsistently
- • iOS caches Universal Link settings - uninstall and reinstall app
- • Universal Links don't work when tapped within your own app
- • Long-pressing a link gives option to open in app or Safari
- • User may have manually chosen to open in Safari (check Settings)
✅ Best Practices
- • Always test on physical devices, not simulators
- • Use Apple's validation tool for apple-app-site-association
- • Implement proper fallback handling for unrecognized URLs
- • Keep apple-app-site-association file simple and focused
- • Test Universal Links from multiple sources (Safari, Messages, etc.)
📱 Example iOS App
Want to see a working implementation? Check out our complete iOS Universal Links example application with full source code.
iOS Universal Links Example
Complete working iOS application demonstrating Universal Links implementation with SwiftUI and proper fallback handling.
1. Clone the repository: git clone https://github.com/TedyHub/ios_universal_links.git
2. Open in Xcode and configure your Team ID and Bundle Identifier
3. Build and run on a physical device to test Universal Links