
Deep Linking
- 2 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates deep linking infrastructure with custom URL schemes, Universal Links via Associated Domains, and App Intents for Siri/Shortcuts routing.
About
Generates URL scheme handling, Universal Links, and App Intents so an app can route to specific content from external sources. A developer uses it to add deep links and Siri/Shortcuts navigation to an iOS/macOS app.
- Custom URL schemes and Universal Links via Associated Domains
- App Intents for Siri Shortcuts routing
Deep Linking by the numbers
- 2 all-time installs (skills.sh)
- Ranked #888 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill deep-linkingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates deep linking infrastructure with custom URL schemes, Universal Links via Associated Domains, and App Intents for Siri/Shortcuts routing.
Files
Deep Linking Generator
Generate deep linking infrastructure with URL schemes, Universal Links, and App Intents for Siri/Shortcuts.
When This Skill Activates
- User wants to handle custom URL schemes (myapp://)
- User mentions Universal Links or Associated Domains
- User wants Siri Shortcuts or App Intents
- User needs to navigate to specific content from external sources
Pre-Generation Checks
Before generating, verify:
1. Existing Deep Link Handling
# Check for existing URL handling
grep -r "onOpenURL\|open.*url\|handleOpen" --include="*.swift" | head -52. URL Scheme in Info.plist
# Check for CFBundleURLTypes
find . -name "Info.plist" -exec grep -l "CFBundleURLSchemes" {} \;3. Associated Domains Entitlement
find . -name "*.entitlements" -exec grep -l "associated-domains" {} \;Configuration Questions
1. URL Scheme
- What custom URL scheme? (e.g.,
myapp) - This enables
myapp://path/to/contentlinks
2. Universal Links
- Yes - Handle HTTPS links (requires AASA file on server)
- No - Custom URL scheme only
3. App Intents / Siri Shortcuts
- Yes - Enable voice commands and Shortcuts app
- No - URL-based deep linking only
4. Link Types
- Profile:
/users/{id} - Content:
/items/{id} - Actions:
/actions/share,/actions/create - Custom routes based on app needs
Generated Files
Core Infrastructure
Sources/DeepLinking/
├── DeepLinkRouter.swift # Central router
├── DeepLink.swift # Route definitions
└── UniversalLinkHandler.swift # Universal link processingApp Intents (Optional)
Sources/AppIntents/
├── OpenContentIntent.swift # Open specific content
├── AppShortcuts.swift # Shortcuts provider
└── ContentEntity.swift # Entities for Spotlight/SiriServer Files (Universal Links)
.well-known/
└── apple-app-site-association # AASA file templateKey Features
Route Definitions
enum DeepLink: Equatable {
case home
case profile(userId: String)
case item(itemId: String)
case settings
case action(ActionType)
enum ActionType {
case share(itemId: String)
case create
}
}URL Parsing
extension DeepLink {
init?(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return nil
}
let pathComponents = components.path.split(separator: "/").map(String.init)
switch pathComponents {
case ["users", let userId]:
self = .profile(userId: userId)
case ["items", let itemId]:
self = .item(itemId: itemId)
case ["settings"]:
self = .settings
default:
self = .home
}
}
}SwiftUI Integration
@main
struct MyApp: App {
@State private var router = DeepLinkRouter()
var body: some Scene {
WindowGroup {
ContentView()
.environment(router)
.onOpenURL { url in
router.handle(url)
}
}
}
}App Intents Integration
OpenIntent for Navigation
struct OpenItemIntent: OpenIntent {
static let title: LocalizedStringResource = "Open Item"
@Parameter(title: "Item")
var target: ItemEntity
func perform() async throws -> some IntentResult {
await router.navigate(to: .item(itemId: target.id))
return .result()
}
}App Shortcuts
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenItemIntent(),
phrases: [
"Open \(\.$target) in \(.applicationName)",
"Show \(\.$target)"
],
shortTitle: "Open Item",
systemImageName: "doc"
)
}
}Required Capabilities
URL Scheme (Info.plist)
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.yourcompany.myapp</string>
</dict>
</array>Universal Links (Entitlements)
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:yourapp.com</string>
<string>applinks:www.yourapp.com</string>
</array>Server Configuration (AASA)
Host at https://yourapp.com/.well-known/apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.yourcompany.yourapp",
"paths": [
"/items/*",
"/users/*",
"/share/*"
]
}
]
}
}Integration Steps
1. Basic URL Handling
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
handleDeepLink(url)
}
}
}
}2. Universal Links (UIKit)
// In SceneDelegate
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return
}
handleUniversalLink(url)
}3. App Intents Setup
1. Create entities for Spotlight indexing 2. Implement OpenIntent for each content type 3. Define AppShortcuts for Siri phrases 4. Index entities with CSSearchableIndex
Testing
URL Schemes
# Simulator
xcrun simctl openurl booted "myapp://items/123"
# Device
# Open Safari and navigate to myapp://items/123Universal Links
# Test AASA file
curl -I "https://yourapp.com/.well-known/apple-app-site-association"
# Should return Content-Type: application/json
# Validate with Apple
# Use Apple's tool or Branch.io validatorApp Intents
1. Build and run on device 2. Open Shortcuts app 3. Your app's shortcuts should appear 4. Test with Siri: "Hey Siri, [your phrase]"
References
Deep Linking Patterns
Best practices for implementing deep links, Universal Links, and App Intents in iOS/macOS apps.
URL Scheme Handling
Route Definition
/// Defines all deep link routes in the app.
enum DeepLink: Equatable, Hashable, Sendable {
// Navigation routes
case home
case profile(userId: String)
case item(itemId: String)
case category(categoryId: String)
case search(query: String)
case settings
case settingsSection(SettingsSection)
// Action routes
case share(itemId: String)
case create(type: ContentType)
case compose(to: String?, subject: String?)
// MARK: - Nested Types
enum SettingsSection: String, Sendable {
case account
case notifications
case appearance
case privacy
}
enum ContentType: String, Sendable {
case note
case task
case reminder
}
}URL Parsing
extension DeepLink {
/// Initialize from a URL (custom scheme or universal link).
init?(url: URL) {
// Handle both custom scheme (myapp://) and universal links (https://)
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return nil
}
// Parse path components
let pathComponents = components.path
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
.split(separator: "/")
.map(String.init)
// Parse query parameters
let queryItems = components.queryItems ?? []
let params = Dictionary(uniqueKeysWithValues: queryItems.compactMap { item in
item.value.map { (item.name, $0) }
})
// Route matching
switch pathComponents {
case []:
self = .home
case ["users", let userId]:
self = .profile(userId: userId)
case ["items", let itemId]:
self = .item(itemId: itemId)
case ["categories", let categoryId]:
self = .category(categoryId: categoryId)
case ["search"]:
let query = params["q"] ?? ""
self = .search(query: query)
case ["settings"]:
self = .settings
case ["settings", let section]:
if let section = SettingsSection(rawValue: section) {
self = .settingsSection(section)
} else {
self = .settings
}
case ["share", let itemId]:
self = .share(itemId: itemId)
case ["create", let type]:
if let contentType = ContentType(rawValue: type) {
self = .create(type: contentType)
} else {
return nil
}
case ["compose"]:
self = .compose(to: params["to"], subject: params["subject"])
default:
return nil
}
}
/// Convert back to URL for sharing or logging.
func toURL(scheme: String = "myapp") -> URL? {
var components = URLComponents()
components.scheme = scheme
switch self {
case .home:
components.path = "/"
case .profile(let userId):
components.path = "/users/\(userId)"
case .item(let itemId):
components.path = "/items/\(itemId)"
case .category(let categoryId):
components.path = "/categories/\(categoryId)"
case .search(let query):
components.path = "/search"
components.queryItems = [URLQueryItem(name: "q", value: query)]
case .settings:
components.path = "/settings"
case .settingsSection(let section):
components.path = "/settings/\(section.rawValue)"
case .share(let itemId):
components.path = "/share/\(itemId)"
case .create(let type):
components.path = "/create/\(type.rawValue)"
case .compose(let to, let subject):
components.path = "/compose"
var items: [URLQueryItem] = []
if let to { items.append(URLQueryItem(name: "to", value: to)) }
if let subject { items.append(URLQueryItem(name: "subject", value: subject)) }
if !items.isEmpty { components.queryItems = items }
}
return components.url
}
}Router Implementation
Observable Router
import SwiftUI
/// Central deep link router for navigation.
@MainActor
@Observable
final class DeepLinkRouter {
// MARK: - Properties
/// Current navigation path for NavigationStack.
var path = NavigationPath()
/// Pending deep link (for deferred handling).
private(set) var pendingDeepLink: DeepLink?
/// Whether the app is ready to handle deep links.
var isReady = false {
didSet {
if isReady, let pending = pendingDeepLink {
pendingDeepLink = nil
navigate(to: pending)
}
}
}
// MARK: - URL Handling
/// Handle an incoming URL.
func handle(_ url: URL) {
guard let deepLink = DeepLink(url: url) else {
print("⚠️ [DeepLink] Unrecognized URL: \(url)")
return
}
print("🔗 [DeepLink] Handling: \(deepLink)")
if isReady {
navigate(to: deepLink)
} else {
// Defer until app is ready
pendingDeepLink = deepLink
}
}
// MARK: - Navigation
/// Navigate to a deep link destination.
func navigate(to deepLink: DeepLink) {
// Reset navigation path
path = NavigationPath()
// Add appropriate destinations
switch deepLink {
case .home:
break // Already at root
case .profile(let userId):
path.append(ProfileDestination(userId: userId))
case .item(let itemId):
path.append(ItemDestination(itemId: itemId))
case .category(let categoryId):
path.append(CategoryDestination(categoryId: categoryId))
case .search(let query):
path.append(SearchDestination(query: query))
case .settings:
path.append(SettingsDestination())
case .settingsSection(let section):
path.append(SettingsDestination())
path.append(SettingsSectionDestination(section: section))
case .share(let itemId):
// Handle share action
NotificationCenter.default.post(
name: .shareItem,
object: nil,
userInfo: ["itemId": itemId]
)
case .create(let type):
NotificationCenter.default.post(
name: .createContent,
object: nil,
userInfo: ["type": type]
)
case .compose(let to, let subject):
NotificationCenter.default.post(
name: .compose,
object: nil,
userInfo: ["to": to as Any, "subject": subject as Any]
)
}
}
}
// MARK: - Navigation Destinations
struct ProfileDestination: Hashable {
let userId: String
}
struct ItemDestination: Hashable {
let itemId: String
}
struct CategoryDestination: Hashable {
let categoryId: String
}
struct SearchDestination: Hashable {
let query: String
}
struct SettingsDestination: Hashable {}
struct SettingsSectionDestination: Hashable {
let section: DeepLink.SettingsSection
}
// MARK: - Notification Names
extension Notification.Name {
static let shareItem = Notification.Name("shareItem")
static let createContent = Notification.Name("createContent")
static let compose = Notification.Name("compose")
}SwiftUI Integration
@main
struct MyApp: App {
@State private var router = DeepLinkRouter()
var body: some Scene {
WindowGroup {
ContentView()
.environment(router)
.onOpenURL { url in
router.handle(url)
}
.onAppear {
router.isReady = true
}
}
}
}
struct ContentView: View {
@Environment(DeepLinkRouter.self) private var router
var body: some View {
@Bindable var router = router
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: ProfileDestination.self) { dest in
ProfileView(userId: dest.userId)
}
.navigationDestination(for: ItemDestination.self) { dest in
ItemDetailView(itemId: dest.itemId)
}
.navigationDestination(for: SearchDestination.self) { dest in
SearchView(initialQuery: dest.query)
}
.navigationDestination(for: SettingsDestination.self) { _ in
SettingsView()
}
}
}
}Universal Links
Apple App Site Association (AASA)
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"TEAMID.com.yourcompany.yourapp"
],
"components": [
{
"/": "/items/*",
"comment": "Item detail pages"
},
{
"/": "/users/*",
"comment": "User profile pages"
},
{
"/": "/share/*",
"comment": "Share links"
},
{
"/": "/invite/*",
"?": { "code": "*" },
"comment": "Invite links with code"
}
]
}
]
},
"webcredentials": {
"apps": [
"TEAMID.com.yourcompany.yourapp"
]
}
}Server Requirements
1. Host AASA at /.well-known/apple-app-site-association 2. Serve with Content-Type: application/json 3. Use HTTPS with valid certificate 4. No redirects allowed 5. File size under 128 KB
Validation
/// Validate universal link before handling.
struct UniversalLinkValidator {
static let allowedHosts = [
"yourapp.com",
"www.yourapp.com",
"links.yourapp.com"
]
static func isValid(_ url: URL) -> Bool {
guard let host = url.host?.lowercased() else {
return false
}
return allowedHosts.contains(host)
}
}App Intents
Entity Definition
import AppIntents
import CoreSpotlight
struct ItemEntity: AppEntity, IndexedEntity {
// MARK: - AppEntity
static var typeDisplayRepresentation = TypeDisplayRepresentation(
name: "Item",
numericFormat: "\(placeholder: .int) items"
)
static var defaultQuery = ItemQuery()
var id: String
var name: String
var description: String
var category: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(category)",
image: .init(systemName: "doc.fill")
)
}
// MARK: - IndexedEntity
var searchableAttributes: CSSearchableItemAttributeSet {
let attributes = CSSearchableItemAttributeSet()
attributes.title = name
attributes.contentDescription = description
attributes.keywords = [category]
return attributes
}
}
struct ItemQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [ItemEntity] {
// Fetch items by IDs from your data source
await DataStore.shared.items(ids: identifiers)
}
func suggestedEntities() async throws -> [ItemEntity] {
// Return recently accessed items
await DataStore.shared.recentItems(limit: 5)
}
}Open Intent
import AppIntents
struct OpenItemIntent: OpenIntent {
static let title: LocalizedStringResource = "Open Item"
static let description = IntentDescription("Opens a specific item in the app")
@Parameter(title: "Item", requestValueDialog: "Which item would you like to open?")
var target: ItemEntity
@MainActor
func perform() async throws -> some IntentResult {
// Navigate to the item
let router = DeepLinkRouter.shared
router.navigate(to: .item(itemId: target.id))
return .result()
}
}
struct SearchItemsIntent: AppIntent {
static let title: LocalizedStringResource = "Search Items"
static let description = IntentDescription("Search for items in the app")
@Parameter(title: "Query")
var query: String
@MainActor
func perform() async throws -> some ReturnsValue<[ItemEntity]> {
let results = await DataStore.shared.search(query: query)
return .result(value: results)
}
}App Shortcuts Provider
import AppIntents
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenItemIntent(),
phrases: [
"Open \(\.$target) in \(.applicationName)",
"Show \(\.$target) in \(.applicationName)",
"Go to \(\.$target)"
],
shortTitle: "Open Item",
systemImageName: "doc.fill"
)
AppShortcut(
intent: SearchItemsIntent(),
phrases: [
"Search for \(\.$query) in \(.applicationName)",
"Find \(\.$query) in \(.applicationName)"
],
shortTitle: "Search",
systemImageName: "magnifyingglass"
)
AppShortcut(
intent: CreateItemIntent(),
phrases: [
"Create a new item in \(.applicationName)",
"Add item to \(.applicationName)"
],
shortTitle: "Create Item",
systemImageName: "plus"
)
}
}Spotlight Indexing
import CoreSpotlight
struct SpotlightIndexer {
static func indexItems(_ items: [ItemEntity]) async throws {
try await CSSearchableIndex.default().indexAppEntities(
items,
priority: .normal
)
}
static func removeItem(_ item: ItemEntity) async throws {
try await CSSearchableIndex.default().deleteAppEntities(
identifiedBy: [item.id],
ofType: ItemEntity.self
)
}
static func reindexAll() async throws {
// Delete all existing entries
try await CSSearchableIndex.default().deleteAllSearchableItems()
// Fetch and reindex
let items = await DataStore.shared.allItems()
try await indexItems(items)
}
}NSUserActivity
Handoff and State Restoration
extension DeepLink {
/// Activity type for NSUserActivity.
var activityType: String {
"com.yourcompany.yourapp.\(activityIdentifier)"
}
private var activityIdentifier: String {
switch self {
case .home: return "home"
case .profile: return "viewProfile"
case .item: return "viewItem"
case .category: return "viewCategory"
case .search: return "search"
case .settings, .settingsSection: return "settings"
case .share: return "share"
case .create: return "create"
case .compose: return "compose"
}
}
/// Create NSUserActivity for this deep link.
func userActivity() -> NSUserActivity {
let activity = NSUserActivity(activityType: activityType)
activity.isEligibleForHandoff = true
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
switch self {
case .item(let itemId):
activity.title = "View Item"
activity.userInfo = ["itemId": itemId]
activity.webpageURL = URL(string: "https://yourapp.com/items/\(itemId)")
case .profile(let userId):
activity.title = "View Profile"
activity.userInfo = ["userId": userId]
activity.webpageURL = URL(string: "https://yourapp.com/users/\(userId)")
case .search(let query):
activity.title = "Search: \(query)"
activity.userInfo = ["query": query]
default:
break
}
return activity
}
/// Parse from NSUserActivity.
init?(userActivity: NSUserActivity) {
// Try webpage URL first (Universal Links)
if let url = userActivity.webpageURL,
let deepLink = DeepLink(url: url) {
self = deepLink
return
}
// Try userInfo
let userInfo = userActivity.userInfo ?? [:]
switch userActivity.activityType {
case "com.yourcompany.yourapp.viewItem":
guard let itemId = userInfo["itemId"] as? String else { return nil }
self = .item(itemId: itemId)
case "com.yourcompany.yourapp.viewProfile":
guard let userId = userInfo["userId"] as? String else { return nil }
self = .profile(userId: userId)
case "com.yourcompany.yourapp.search":
let query = userInfo["query"] as? String ?? ""
self = .search(query: query)
default:
return nil
}
}
}SwiftUI UserActivity
struct ItemDetailView: View {
let itemId: String
@State private var item: Item?
var body: some View {
Group {
if let item {
ItemContent(item: item)
} else {
ProgressView()
}
}
.userActivity(DeepLink.item(itemId: itemId).activityType) { activity in
activity.title = item?.name ?? "Item"
activity.userInfo = ["itemId": itemId]
activity.webpageURL = URL(string: "https://yourapp.com/items/\(itemId)")
activity.isEligibleForHandoff = true
}
.task {
item = await DataStore.shared.item(id: itemId)
}
}
}Testing
URL Scheme Testing
#if DEBUG
struct DeepLinkTester: View {
@Environment(DeepLinkRouter.self) private var router
@State private var testURL = "myapp://items/123"
var body: some View {
VStack {
TextField("URL", text: $testURL)
.textFieldStyle(.roundedBorder)
Button("Test Deep Link") {
if let url = URL(string: testURL) {
router.handle(url)
}
}
}
.padding()
}
}
#endifUnit Tests
import XCTest
@testable import YourApp
final class DeepLinkTests: XCTestCase {
func testItemParsing() {
let url = URL(string: "myapp://items/123")!
let deepLink = DeepLink(url: url)
XCTAssertEqual(deepLink, .item(itemId: "123"))
}
func testSearchWithQuery() {
let url = URL(string: "myapp://search?q=hello%20world")!
let deepLink = DeepLink(url: url)
XCTAssertEqual(deepLink, .search(query: "hello world"))
}
func testUniversalLink() {
let url = URL(string: "https://yourapp.com/users/456")!
let deepLink = DeepLink(url: url)
XCTAssertEqual(deepLink, .profile(userId: "456"))
}
func testRoundTrip() {
let original = DeepLink.item(itemId: "789")
let url = original.toURL()!
let parsed = DeepLink(url: url)
XCTAssertEqual(parsed, original)
}
}{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"TEAMID.com.yourcompany.yourapp"
],
"components": [
{
"/": "/items/*",
"comment": "Item detail pages"
},
{
"/": "/users/*",
"comment": "User profile pages"
},
{
"/": "/categories/*",
"comment": "Category pages"
},
{
"/": "/share/*",
"comment": "Share links"
},
{
"/": "/search",
"?": {
"q": "*"
},
"comment": "Search with query"
},
{
"/": "/settings",
"comment": "Settings page"
},
{
"/": "/settings/*",
"comment": "Settings sections"
},
{
"/": "/invite",
"?": {
"code": "*"
},
"comment": "Invite links with referral code"
}
]
}
]
},
"webcredentials": {
"apps": [
"TEAMID.com.yourcompany.yourapp"
]
},
"appclips": {
"apps": [
"TEAMID.com.yourcompany.yourapp.Clip"
]
}
}
import AppIntents
/// App Shortcuts provider for Siri and Shortcuts app.
///
/// Defines the shortcuts that appear in the Shortcuts app
/// and the phrases Siri recognizes.
struct AppShortcuts: AppShortcutsProvider {
/// App shortcuts available to users.
static var appShortcuts: [AppShortcut] {
// Open item shortcut
AppShortcut(
intent: OpenItemIntent(),
phrases: [
"Open \(\.$target) in \(.applicationName)",
"Show \(\.$target) in \(.applicationName)",
"Go to \(\.$target)"
],
shortTitle: "Open Item",
systemImageName: "doc.fill"
)
// Search shortcut
AppShortcut(
intent: SearchIntent(),
phrases: [
"Search for \(\.$query) in \(.applicationName)",
"Find \(\.$query) in \(.applicationName)",
"Look up \(\.$query)"
],
shortTitle: "Search",
systemImageName: "magnifyingglass"
)
// Create item shortcut
AppShortcut(
intent: CreateItemIntent(),
phrases: [
"Create a new \(\.$type) in \(.applicationName)",
"Add \(\.$type) to \(.applicationName)",
"New \(\.$type)"
],
shortTitle: "Create",
systemImageName: "plus.circle.fill"
)
// Open settings shortcut
AppShortcut(
intent: OpenSettingsIntent(),
phrases: [
"Open \(.applicationName) settings",
"\(.applicationName) preferences"
],
shortTitle: "Settings",
systemImageName: "gear"
)
}
}
// MARK: - Open Item Intent
/// Intent to open a specific item.
struct OpenItemIntent: AppIntent {
static let title: LocalizedStringResource = "Open Item"
static let description = IntentDescription("Opens a specific item in the app")
static let openAppWhenRun: Bool = true
@Parameter(title: "Item", requestValueDialog: "Which item would you like to open?")
var target: ItemEntity
@MainActor
func perform() async throws -> some IntentResult {
let router = DeepLinkRouter.shared
router.navigate(to: .item(itemId: target.id))
return .result()
}
}
// MARK: - Search Intent
/// Intent to search for items.
struct SearchIntent: AppIntent {
static let title: LocalizedStringResource = "Search"
static let description = IntentDescription("Search for items in the app")
static let openAppWhenRun: Bool = true
@Parameter(title: "Query")
var query: String
@MainActor
func perform() async throws -> some IntentResult {
let router = DeepLinkRouter.shared
router.navigate(to: .search(query: query))
return .result()
}
}
// MARK: - Create Item Intent
/// Intent to create a new item.
struct CreateItemIntent: AppIntent {
static let title: LocalizedStringResource = "Create Item"
static let description = IntentDescription("Create a new item in the app")
static let openAppWhenRun: Bool = true
@Parameter(title: "Type", default: .note)
var type: ContentTypeEntity
@MainActor
func perform() async throws -> some IntentResult {
let router = DeepLinkRouter.shared
router.navigate(to: .create(type: type.deepLinkType))
return .result()
}
}
// MARK: - Open Settings Intent
/// Intent to open app settings.
struct OpenSettingsIntent: AppIntent {
static let title: LocalizedStringResource = "Open Settings"
static let description = IntentDescription("Opens the app settings")
static let openAppWhenRun: Bool = true
@MainActor
func perform() async throws -> some IntentResult {
let router = DeepLinkRouter.shared
router.navigate(to: .settings)
return .result()
}
}
// MARK: - Content Type Entity
/// App Intents entity for content types.
struct ContentTypeEntity: AppEnum {
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Content Type")
static let caseDisplayRepresentations: [ContentTypeEntity: DisplayRepresentation] = [
.note: DisplayRepresentation(title: "Note", image: .init(systemName: "note.text")),
.task: DisplayRepresentation(title: "Task", image: .init(systemName: "checkmark.circle")),
.reminder: DisplayRepresentation(title: "Reminder", image: .init(systemName: "bell")),
.folder: DisplayRepresentation(title: "Folder", image: .init(systemName: "folder"))
]
case note
case task
case reminder
case folder
/// Convert to DeepLink content type.
var deepLinkType: DeepLink.ContentType {
switch self {
case .note: return .note
case .task: return .task
case .reminder: return .reminder
case .folder: return .folder
}
}
}
// MARK: - Item Entity
/// App Intents entity for items.
///
/// This enables Siri to understand and reference items in your app.
struct ItemEntity: AppEntity {
// MARK: - AppEntity
static var typeDisplayRepresentation = TypeDisplayRepresentation(
name: "Item",
numericFormat: "\(placeholder: .int) items"
)
static var defaultQuery = ItemEntityQuery()
// MARK: - Properties
var id: String
var name: String
var category: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(category)",
image: .init(systemName: "doc.fill")
)
}
}
// MARK: - Item Entity Query
/// Query handler for ItemEntity.
struct ItemEntityQuery: EntityQuery {
/// Fetch items by their IDs.
func entities(for identifiers: [String]) async throws -> [ItemEntity] {
// TODO: Implement actual data fetching
// return await DataStore.shared.items(ids: identifiers)
// Placeholder implementation
return identifiers.map { id in
ItemEntity(id: id, name: "Item \(id)", category: "General")
}
}
/// Suggest items to the user.
func suggestedEntities() async throws -> [ItemEntity] {
// TODO: Return recently accessed or popular items
// return await DataStore.shared.recentItems(limit: 5)
// Placeholder implementation
return [
ItemEntity(id: "1", name: "Recent Item 1", category: "General"),
ItemEntity(id: "2", name: "Recent Item 2", category: "Work"),
ItemEntity(id: "3", name: "Recent Item 3", category: "Personal")
]
}
}
// MARK: - String Search (Optional)
extension ItemEntityQuery: EntityStringQuery {
/// Search items by string.
func entities(matching string: String) async throws -> [ItemEntity] {
// TODO: Implement search
// return await DataStore.shared.search(query: string)
// Placeholder implementation
return [
ItemEntity(id: "search-1", name: "Result for '\(string)'", category: "Search")
]
}
}
import Foundation
/// Defines all deep link routes in the app.
///
/// Usage:
/// ```swift
/// // Parse from URL
/// let deepLink = DeepLink(url: url)
///
/// // Create URL for sharing
/// let url = DeepLink.item(itemId: "123").toURL()
/// ```
enum DeepLink: Equatable, Hashable, Sendable {
// MARK: - Navigation Routes
/// Home screen
case home
/// User profile
case profile(userId: String)
/// Item detail
case item(itemId: String)
/// Category listing
case category(categoryId: String)
/// Search with optional query
case search(query: String)
/// Settings screen
case settings
/// Specific settings section
case settingsSection(SettingsSection)
// MARK: - Action Routes
/// Share an item
case share(itemId: String)
/// Create new content
case create(type: ContentType)
// MARK: - Nested Types
enum SettingsSection: String, Sendable, CaseIterable {
case account
case notifications
case appearance
case privacy
case about
}
enum ContentType: String, Sendable, CaseIterable {
case note
case task
case reminder
case folder
}
}
// MARK: - URL Parsing
extension DeepLink {
/// Your app's custom URL scheme.
static let scheme = "myapp" // TODO: Replace with your scheme
/// Allowed hosts for universal links.
static let allowedHosts = [
"yourapp.com", // TODO: Replace with your domain
"www.yourapp.com"
]
/// Initialize from a URL (custom scheme or universal link).
///
/// Supports:
/// - Custom scheme: `myapp://items/123`
/// - Universal link: `https://yourapp.com/items/123`
init?(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return nil
}
// Validate scheme for custom URLs or host for universal links
if let scheme = components.scheme?.lowercased() {
if scheme == Self.scheme {
// Custom URL scheme - continue parsing
} else if scheme == "https" || scheme == "http" {
// Universal link - validate host
guard let host = components.host?.lowercased(),
Self.allowedHosts.contains(host) else {
return nil
}
} else {
return nil
}
}
// Parse path components
let pathComponents = components.path
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
.split(separator: "/")
.map(String.init)
// Parse query parameters
let queryItems = components.queryItems ?? []
let params = Dictionary(uniqueKeysWithValues: queryItems.compactMap { item in
item.value.map { (item.name, $0) }
})
// Route matching
self.init(pathComponents: pathComponents, params: params)
}
private init?(pathComponents: [String], params: [String: String]) {
switch pathComponents {
case []:
self = .home
case ["users", let userId]:
self = .profile(userId: userId)
case ["profiles", let userId]: // Alternative path
self = .profile(userId: userId)
case ["items", let itemId]:
self = .item(itemId: itemId)
case ["categories", let categoryId]:
self = .category(categoryId: categoryId)
case ["search"]:
let query = params["q"] ?? params["query"] ?? ""
self = .search(query: query)
case ["settings"]:
self = .settings
case ["settings", let section]:
if let section = SettingsSection(rawValue: section) {
self = .settingsSection(section)
} else {
self = .settings
}
case ["share", let itemId]:
self = .share(itemId: itemId)
case ["create"]:
if let typeString = params["type"],
let contentType = ContentType(rawValue: typeString) {
self = .create(type: contentType)
} else {
return nil
}
case ["create", let typeString]:
if let contentType = ContentType(rawValue: typeString) {
self = .create(type: contentType)
} else {
return nil
}
default:
return nil
}
}
}
// MARK: - URL Generation
extension DeepLink {
/// Convert to URL with custom scheme.
func toURL() -> URL? {
toURL(scheme: Self.scheme)
}
/// Convert to universal link URL.
func toUniversalURL() -> URL? {
guard let host = Self.allowedHosts.first else { return nil }
return toURL(scheme: "https", host: host)
}
/// Convert to URL with specified scheme and optional host.
func toURL(scheme: String, host: String? = nil) -> URL? {
var components = URLComponents()
components.scheme = scheme
components.host = host
switch self {
case .home:
components.path = "/"
case .profile(let userId):
components.path = "/users/\(userId)"
case .item(let itemId):
components.path = "/items/\(itemId)"
case .category(let categoryId):
components.path = "/categories/\(categoryId)"
case .search(let query):
components.path = "/search"
if !query.isEmpty {
components.queryItems = [URLQueryItem(name: "q", value: query)]
}
case .settings:
components.path = "/settings"
case .settingsSection(let section):
components.path = "/settings/\(section.rawValue)"
case .share(let itemId):
components.path = "/share/\(itemId)"
case .create(let type):
components.path = "/create/\(type.rawValue)"
}
return components.url
}
}
// MARK: - Display
extension DeepLink: CustomStringConvertible {
var description: String {
switch self {
case .home:
return "Home"
case .profile(let userId):
return "Profile(\(userId))"
case .item(let itemId):
return "Item(\(itemId))"
case .category(let categoryId):
return "Category(\(categoryId))"
case .search(let query):
return "Search(\(query))"
case .settings:
return "Settings"
case .settingsSection(let section):
return "Settings/\(section.rawValue)"
case .share(let itemId):
return "Share(\(itemId))"
case .create(let type):
return "Create(\(type.rawValue))"
}
}
}
import SwiftUI
/// Central router for deep link navigation.
///
/// Setup in App:
/// ```swift
/// @main
/// struct MyApp: App {
/// @State private var router = DeepLinkRouter()
///
/// var body: some Scene {
/// WindowGroup {
/// ContentView()
/// .environment(router)
/// .onOpenURL { url in
/// router.handle(url)
/// }
/// }
/// }
/// }
/// ```
@MainActor
@Observable
final class DeepLinkRouter {
// MARK: - Singleton (optional)
/// Shared instance for use in App Intents.
static let shared = DeepLinkRouter()
// MARK: - Navigation State
/// Navigation path for NavigationStack.
var path = NavigationPath()
/// Currently selected tab (if using TabView).
var selectedTab: Tab = .home
/// Sheet presentation state.
var activeSheet: SheetDestination?
// MARK: - Deferred Handling
/// Pending deep link awaiting app readiness.
private(set) var pendingDeepLink: DeepLink?
/// Whether the app is ready to handle navigation.
var isReady = false {
didSet {
if isReady, let pending = pendingDeepLink {
pendingDeepLink = nil
navigate(to: pending)
}
}
}
// MARK: - Types
enum Tab: String, CaseIterable {
case home
case search
case profile
case settings
}
enum SheetDestination: Identifiable {
case share(itemId: String)
case create(type: DeepLink.ContentType)
var id: String {
switch self {
case .share(let itemId): return "share-\(itemId)"
case .create(let type): return "create-\(type.rawValue)"
}
}
}
// MARK: - URL Handling
/// Handle an incoming URL.
///
/// - Parameter url: The URL to handle (custom scheme or universal link).
func handle(_ url: URL) {
guard let deepLink = DeepLink(url: url) else {
#if DEBUG
print("⚠️ [DeepLink] Unrecognized URL: \(url)")
#endif
return
}
#if DEBUG
print("🔗 [DeepLink] Handling: \(deepLink)")
#endif
if isReady {
navigate(to: deepLink)
} else {
pendingDeepLink = deepLink
}
}
// MARK: - Navigation
/// Navigate to a deep link destination.
///
/// - Parameter deepLink: The destination to navigate to.
func navigate(to deepLink: DeepLink) {
// Dismiss any presented sheet
activeSheet = nil
switch deepLink {
case .home:
selectedTab = .home
path = NavigationPath()
case .profile(let userId):
selectedTab = .profile
path = NavigationPath()
path.append(ProfileDestination(userId: userId))
case .item(let itemId):
selectedTab = .home
path = NavigationPath()
path.append(ItemDestination(itemId: itemId))
case .category(let categoryId):
selectedTab = .home
path = NavigationPath()
path.append(CategoryDestination(categoryId: categoryId))
case .search(let query):
selectedTab = .search
path = NavigationPath()
if !query.isEmpty {
path.append(SearchDestination(query: query))
}
case .settings:
selectedTab = .settings
path = NavigationPath()
case .settingsSection(let section):
selectedTab = .settings
path = NavigationPath()
path.append(SettingsSectionDestination(section: section))
case .share(let itemId):
activeSheet = .share(itemId: itemId)
case .create(let type):
activeSheet = .create(type: type)
}
}
/// Pop to root of current navigation stack.
func popToRoot() {
path = NavigationPath()
}
/// Pop one level in navigation stack.
func pop() {
if !path.isEmpty {
path.removeLast()
}
}
}
// MARK: - Navigation Destinations
/// Profile screen destination.
struct ProfileDestination: Hashable {
let userId: String
}
/// Item detail destination.
struct ItemDestination: Hashable {
let itemId: String
}
/// Category listing destination.
struct CategoryDestination: Hashable {
let categoryId: String
}
/// Search results destination.
struct SearchDestination: Hashable {
let query: String
}
/// Settings section destination.
struct SettingsSectionDestination: Hashable {
let section: DeepLink.SettingsSection
}
// MARK: - SwiftUI Environment
/// Environment key for DeepLinkRouter.
private struct DeepLinkRouterKey: EnvironmentKey {
@MainActor static let defaultValue = DeepLinkRouter.shared
}
extension EnvironmentValues {
var deepLinkRouter: DeepLinkRouter {
get { self[DeepLinkRouterKey.self] }
set { self[DeepLinkRouterKey.self] = newValue }
}
}
// MARK: - View Extension
extension View {
/// Configure navigation destinations for deep linking.
func withDeepLinkDestinations() -> some View {
self
.navigationDestination(for: ProfileDestination.self) { dest in
// ProfileView(userId: dest.userId)
Text("Profile: \(dest.userId)")
}
.navigationDestination(for: ItemDestination.self) { dest in
// ItemDetailView(itemId: dest.itemId)
Text("Item: \(dest.itemId)")
}
.navigationDestination(for: CategoryDestination.self) { dest in
// CategoryView(categoryId: dest.categoryId)
Text("Category: \(dest.categoryId)")
}
.navigationDestination(for: SearchDestination.self) { dest in
// SearchResultsView(query: dest.query)
Text("Search: \(dest.query)")
}
.navigationDestination(for: SettingsSectionDestination.self) { dest in
// SettingsSectionView(section: dest.section)
Text("Settings: \(dest.section.rawValue)")
}
}
}