{"id":2063,"date":"2026-08-18T09:00:00","date_gmt":"2026-08-18T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=2063"},"modified":"2026-03-07T03:50:31","modified_gmt":"2026-03-07T08:50:31","slug":"universal-links-xcode-configuration","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/universal-links-xcode-configuration\/","title":{"rendered":"Handling Universal Links in Xcode Previews and Tests"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Universal Links routing logic can be complex: parsing URLs, extracting parameters, mapping paths to screens, and handling edge cases. Testing this logic in Xcode ensures that your deep links work before they reach production. You can unit test URL parsing and routing, UI test the full launch-from-URL flow, and use Xcode&#39;s debugging tools to inspect Universal Link behavior on devices.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This article covers how to test Universal Links at each level, from unit tests to UI tests to on-device debugging.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For Universal Links fundamentals, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">universal links: everything you need to know<\/a>. For general testing approaches, see <a href=\"https:\/\/tolinku.com\/blog\/testing-universal-links\/\">testing Universal Links<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Unit Testing URL Routing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Your URL routing logic should be a pure function: give it a URL, get back a route or destination. This makes it easy to unit test without any iOS infrastructure.<\/p>\n\n\n\n<pre><code class=\"language-swift\">import XCTest\n@testable import YourApp\n\nclass UniversalLinkRouterTests: XCTestCase {\n\n    let router = DeepLinkRouter()\n\n    func testProductURL() {\n        let url = URL(string: &quot;https:\/\/example.com\/product\/abc123&quot;)!\n        let route = router.route(for: url)\n        XCTAssertEqual(route, .product(id: &quot;abc123&quot;))\n    }\n\n    func testCategoryURL() {\n        let url = URL(string: &quot;https:\/\/example.com\/category\/shoes&quot;)!\n        let route = router.route(for: url)\n        XCTAssertEqual(route, .category(slug: &quot;shoes&quot;))\n    }\n\n    func testUnknownPath() {\n        let url = URL(string: &quot;https:\/\/example.com\/unknown\/path&quot;)!\n        let route = router.route(for: url)\n        XCTAssertNil(route)\n    }\n\n    func testQueryParameters() {\n        let url = URL(string: &quot;https:\/\/example.com\/search?q=red+shoes&amp;page=2&quot;)!\n        let route = router.route(for: url)\n        XCTAssertEqual(route, .search(query: &quot;red shoes&quot;, page: 2))\n    }\n\n    func testMalformedURL() {\n        let url = URL(string: &quot;https:\/\/example.com\/product\/&quot;)!\n        let route = router.route(for: url)\n        XCTAssertNil(route, &quot;Empty product ID should not route&quot;)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Structuring Your Router for Testability<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The key to testable Universal Links is separating URL parsing from navigation. Your router should return a value (an enum case, a struct, a route identifier), not directly perform navigation:<\/p>\n\n\n\n<pre><code class=\"language-swift\">enum DeepLinkDestination: Equatable {\n    case product(id: String)\n    case category(slug: String)\n    case search(query: String, page: Int)\n    case profile(username: String)\n    case home\n}\n\nstruct DeepLinkRouter {\n\n    func route(for url: URL) -&gt; DeepLinkDestination? {\n        let path = url.pathComponents\n\n        guard path.count &gt;= 2 else { return nil }\n\n        switch path[1] {\n        case &quot;product&quot;:\n            guard path.count &gt;= 3, !path[2].isEmpty else { return nil }\n            return .product(id: path[2])\n        case &quot;category&quot;:\n            guard path.count &gt;= 3 else { return nil }\n            return .category(slug: path[2])\n        case &quot;search&quot;:\n            let components = URLComponents(url: url, resolvingAgainstBaseURL: false)\n            let query = components?.queryItems?.first(where: { $0.name == &quot;q&quot; })?.value ?? &quot;&quot;\n            let page = Int(components?.queryItems?.first(where: { $0.name == &quot;page&quot; })?.value ?? &quot;1&quot;) ?? 1\n            return .search(query: query, page: page)\n        case &quot;user&quot;:\n            guard path.count &gt;= 3 else { return nil }\n            return .profile(username: path[2])\n        default:\n            return nil\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This router is a pure function with no side effects, making it trivial to test with any URL you can construct.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">UI Testing Universal Links<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Xcode UI tests can launch your app with a Universal Link URL using the <code>XCUIApplication<\/code> launch arguments. This tests the full flow from URL to screen.<\/p>\n\n\n\n<pre><code class=\"language-swift\">import XCTest\n\nclass UniversalLinkUITests: XCTestCase {\n\n    func testProductDeepLink() {\n        let app = XCUIApplication()\n        app.launchEnvironment[&quot;TEST_DEEP_LINK&quot;] = &quot;https:\/\/example.com\/product\/abc123&quot;\n        app.launch()\n\n        \/\/ Assert the product screen is visible\n        XCTAssertTrue(app.navigationBars[&quot;Product Details&quot;].waitForExistence(timeout: 5))\n        XCTAssertTrue(app.staticTexts[&quot;abc123&quot;].exists)\n    }\n\n    func testCategoryDeepLink() {\n        let app = XCUIApplication()\n        app.launchEnvironment[&quot;TEST_DEEP_LINK&quot;] = &quot;https:\/\/example.com\/category\/shoes&quot;\n        app.launch()\n\n        XCTAssertTrue(app.navigationBars[&quot;Shoes&quot;].waitForExistence(timeout: 5))\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In your app code, handle the test deep link during launch:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n) -&gt; Bool {\n\n    #if DEBUG\n    if let testURL = ProcessInfo.processInfo.environment[&quot;TEST_DEEP_LINK&quot;],\n       let url = URL(string: testURL) {\n        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n            self.handleUniversalLink(url)\n        }\n    }\n    #endif\n\n    return true\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This approach simulates the deep link within your app&#39;s process. It does not test iOS&#39;s AASA validation or the system-level Universal Link dispatch, but it does test your app&#39;s URL handling end-to-end.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing With xcrun and Simctl<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For testing on the Simulator, you can open a Universal Link using the <code>xcrun simctl<\/code> command:<\/p>\n\n\n\n<pre><code class=\"language-bash\">xcrun simctl openurl booted &quot;https:\/\/example.com\/product\/abc123&quot;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This sends the URL to the Simulator&#39;s frontmost app as if the user tapped a Universal Link. If your app is installed and the AASA file is configured, the app will open and receive the URL through the standard delegate methods.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Limitations of Simulator Testing<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The Simulator does not always validate AASA files the same way as a physical device.<\/li>\n<li>Apple&#39;s CDN caching behavior is different (or absent) on the Simulator.<\/li>\n<li>For reliable Universal Link testing, always verify on a physical device.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For on-device testing strategies, see <a href=\"https:\/\/tolinku.com\/blog\/testing-universal-links\/\">testing Universal Links<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing the AASA File<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can validate your AASA file in Xcode without running the app:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Xcode Console Diagnostics<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On iOS 16+, connect a physical device and open the Console app (or Xcode&#39;s device logs). Filter for <code>swcd<\/code> (the Shared Web Credentials daemon) to see Universal Link validation messages:<\/p>\n\n\n\n<pre><code>swcd: Checking apple-app-site-association for domain example.com\nswcd: Found valid association for TEAMID.com.example.app\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If validation fails, you will see error messages indicating what went wrong (invalid JSON, missing Team ID, etc.).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Associated Domains Diagnostics<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In Xcode 14+, you can use the Associated Domains diagnostic tool:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Connect your device.<\/li>\n<li>Open Window &gt; Devices and Simulators.<\/li>\n<li>Select your device.<\/li>\n<li>Right-click and choose &quot;Show Associated Domains Diagnostics.&quot;<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This shows which domains are associated with which apps and whether validation succeeded.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For AASA debugging, see <a href=\"https:\/\/tolinku.com\/blog\/debugging-aasa-file\/\">debugging AASA files<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">SwiftUI Preview Testing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you use SwiftUI, you can preview screens that would be reached via Universal Links by passing the relevant data directly:<\/p>\n\n\n\n<pre><code class=\"language-swift\">struct ProductView: View {\n    let productID: String\n\n    var body: some View {\n        Text(&quot;Product: \\(productID)&quot;)\n    }\n}\n\n#Preview {\n    ProductView(productID: &quot;abc123&quot;)\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This does not test the URL routing itself, but it lets you verify that the destination view renders correctly with the parameters a Universal Link would provide.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For SwiftUI-specific deep link handling, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-with-swiftui\/\">Universal Links with SwiftUI<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Automated AASA Validation in CI<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can add AASA validation to your CI pipeline to catch configuration errors before they reach production:<\/p>\n\n\n\n<pre><code class=\"language-bash\">#!\/bin\/bash\n# validate-aasa.sh\n\nDOMAIN=&quot;yourdomain.com&quot;\nAASA_URL=&quot;https:\/\/${DOMAIN}\/.well-known\/apple-app-site-association&quot;\n\n# Fetch and validate JSON\nRESPONSE=$(curl -s -w &quot;\\n%{http_code}&quot; &quot;$AASA_URL&quot;)\nHTTP_CODE=$(echo &quot;$RESPONSE&quot; | tail -1)\nBODY=$(echo &quot;$RESPONSE&quot; | sed &#39;$d&#39;)\n\nif [ &quot;$HTTP_CODE&quot; != &quot;200&quot; ]; then\n    echo &quot;FAIL: AASA returned HTTP $HTTP_CODE&quot;\n    exit 1\nfi\n\n# Validate JSON syntax\necho &quot;$BODY&quot; | python3 -m json.tool &gt; \/dev\/null 2&gt;&amp;1\nif [ $? -ne 0 ]; then\n    echo &quot;FAIL: AASA is not valid JSON&quot;\n    exit 1\nfi\n\n# Check for applinks section\necho &quot;$BODY&quot; | python3 -c &quot;\nimport json, sys\ndata = json.load(sys.stdin)\nif &#39;applinks&#39; not in data:\n    print(&#39;FAIL: No applinks section&#39;)\n    sys.exit(1)\ndetails = data[&#39;applinks&#39;].get(&#39;details&#39;, [])\nif not details:\n    print(&#39;FAIL: No details in applinks&#39;)\n    sys.exit(1)\nfor d in details:\n    app_ids = d.get(&#39;appIDs&#39;, [d.get(&#39;appID&#39;, &#39;&#39;)])\n    if not any(app_ids):\n        print(&#39;FAIL: No appID in details entry&#39;)\n        sys.exit(1)\nprint(&#39;PASS: AASA is valid&#39;)\n&quot;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run this script as part of your CI build to catch AASA issues early.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Universal Link Testing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> manages your AASA file, so you can focus your testing on the app-side routing logic. The AASA file hosted by Tolinku is always valid JSON with correct formatting. Your tests can focus on verifying that your <code>DeepLinkRouter<\/code> correctly maps Tolinku-managed URLs to the right screens. See the <a href=\"https:\/\/tolinku.com\/docs\/developer\/universal-links\/\">Universal Links developer guide<\/a> for configuration details.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For Xcode-specific configuration of the Associated Domains entitlement, see <a href=\"https:\/\/tolinku.com\/blog\/xcode-universal-links-configuration\/\">xcode Universal Links configuration<\/a>. For the complete Universal Links guide, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">universal links: everything you need to know<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Test Universal Links in Xcode unit tests and UI tests. Learn to simulate deep links, assert routing, and automate Universal Link validation.<\/p>\n","protected":false},"author":2,"featured_media":2062,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Handling Universal Links in Xcode Previews and Tests","rank_math_description":"Test Universal Links in Xcode unit tests and UI tests. Learn to simulate deep links, assert routing, and automate Universal Link validation.","rank_math_focus_keyword":"universal links xcode tests","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-universal-links-xcode-configuration.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-universal-links-xcode-configuration.png","footnotes":""},"categories":[12],"tags":[648,165,20,24,31,80,660,22,81,659],"class_list":["post-2063","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ios","tag-app-development","tag-automation","tag-deep-linking","tag-ios","tag-swift","tag-testing","tag-ui-testing","tag-universal-links","tag-xcode","tag-xctest"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/2063","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=2063"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/2063\/revisions"}],"predecessor-version":[{"id":2064,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/2063\/revisions\/2064"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/2062"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=2063"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=2063"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=2063"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}