CocoaPods trunk is moving to be read-only. Read more on the blog, there are 4 months to go.

Ferry 0.1.0

Ferry 0.1.0

Maintained by FerryLink.



Ferry 0.1.0

  • By
  • Ferry

Ferry for iOS

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

Requirements

Requirement Version
iOS 15.0 or later
Swift 5.9 or later
Ferry 0.1.0

Installation

Swift Package Manager

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")
    )
]

CocoaPods

Add Ferry to your Podfile:

platform :ios, '15.0'

target 'YourApp' do
  pod 'Ferry', '~> 0.1.0'
end

Then run:

pod install

Quick start

1. Configure Ferry

Call 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()
        }
    }
}

2. Register the link handler

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
    )
}

3. Forward incoming links

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)
}

4. Configure Associated Domains

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

Tracking events

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)")
}

Creating links in the app

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.

App Clips

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.

Debug logging

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.

Example and development

The example app runs against a bundled local mock server and requires no Ferry account.

Run the package tests with:

swift test

License

Ferry is available under the MIT License. See LICENSE.