Ferry provides deep linking and link-level attribution for iOS applications. It handles direct Universal Link opens, deferred deep linking after install, attribution events, and optional client-side link creation.
- One callback for direct and deferred links
- Swift Package Manager and CocoaPods support
- iOS 15 and later
- No third-party runtime dependencies
- No advertising identifier or tracking permission
| Requirement | Version |
|---|---|
| iOS | 15.0 or later |
| Swift | 5.9 or later |
| Ferry | 0.1.0 |
In Xcode, select File > Add Package Dependencies and enter:
https://github.com/FerryLink/ferry-ios.git
Choose Up to Next Minor Version starting at 0.1.0, then add the Ferry
library to your app target.
For a package manifest:
dependencies: [
.package(
url: "https://github.com/FerryLink/ferry-ios.git",
.upToNextMinor(from: "0.1.0")
)
]Add Ferry to your Podfile:
platform :ios, '15.0'
target 'YourApp' do
pod 'Ferry', '~> 0.1.0'
endThen run:
pod installCall configure once during app launch with the project's pk_ public key.
Configuration itself does not perform network activity.
import Ferry
import SwiftUI
@main
struct YourApp: App {
init() {
Ferry.configure(publicKey: "pk_test_xxx")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Register one handler during launch. It receives both direct opens and the deferred first-install match.
Ferry.onLink { link in
switch link.data["screen"] {
case "product":
router.showProduct(id: link.data["id"])
case "checkout":
router.showCheckout()
default:
router.showHome()
}
}Use link.isDeferred when the app needs to distinguish a deferred match from a
direct open. Registering onLink also starts the deferred matching flow.
To observe attributed, organic, and repeated first-open outcomes:
Ferry.onInstallation { result in
analytics.record(
attribution: result.attribution,
isNewInstallation: result.isNewInstallation
)
}With SwiftUI:
ContentView()
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
Ferry.handle(activity)
}
.onOpenURL { url in
Ferry.handle(url)
}With UIKit, forward Universal Links from the application or scene delegate:
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
Ferry.handle(userActivity)
}Enable the Associated Domains capability for the app target and add one entry for each Ferry link domain:
applinks:example.feryl.io
applinks:go.example.com
Each domain must serve a matching Apple App Site Association file at:
https://<domain>/.well-known/apple-app-site-association
Ferry supports login, signup, and purchase attribution events:
Ferry.track(.login)
Ferry.track(.signup, customerID: "customer_123")
Ferry.track(
.purchase(
transactionId: "order_123",
value: 29.99,
currency: "USD",
productId: "pro_monthly"
),
customerID: "customer_123"
)Events are queued and delivered automatically. Customer, transaction, and product identifiers must be opaque. Do not provide names, email addresses, phone numbers, or advertising identifiers.
Register onEventDelivery if the app needs to reconcile accepted and rejected
events:
Ferry.onEventDelivery { result in
print("Accepted: \(result.accepted)")
print("Rejected: \(result.rejected.count)")
}Client-side link creation is optional and must be enabled for the Ferry project. The hostname must belong to that project.
do {
let link = try await Ferry.createLink(
domain: "go.example.com",
data: ["screen": "profile", "user": "u_42"]
)
share(link.url)
} catch {
report(error)
}Ferry generates the slug and applies the server-defined expiration. Client apps cannot choose a redirect URL through this endpoint.
Add the Ferry library to both the full app and App Clip targets. Configure the
App Clip with the same public key and app group used by the full app:
Ferry.configureAppClip(
publicKey: "pk_test_xxx",
appGroup: "group.com.example.ferry"
)The full app must provide the same app group:
Ferry.configure(
publicKey: "pk_test_xxx",
options: FerryOptions(appGroupIdentifier: "group.com.example.ferry")
)Enable App Groups on both targets and add the appropriate appclips: Associated
Domains entry.
Diagnostic logging is disabled by default:
Ferry.configure(
publicKey: "pk_test_xxx",
options: FerryOptions(
debugLogging: true,
debugNetworkLogging: true
)
)Network diagnostics redact authorization, payloads, URLs, and stable identifiers. Leave debug logging disabled in production builds unless it is needed for support.
The example app runs against a bundled local mock server and requires no Ferry account.
Run the package tests with:
swift testFerry is available under the MIT License. See LICENSE.