iOS SDK
The iOS SDK (TolinkuSDK) provides Universal Link handling, deferred deep linking, event tracking, referrals, and in-app messages for iOS 15+ and macOS 13+.
Installation
Section titled “Installation”Add the SDK via Swift Package Manager in Xcode:
- Go to File > Add Package Dependencies.
- Enter the repository URL:
https://github.com/tolinku/ios-sdk - Select the latest version and add the
TolinkuSDKlibrary to your target.
Configure the SDK as early as possible in your app lifecycle:
import TolinkuSDK
// In your App init or AppDelegate:do { let tolinku = try Tolinku.configure(apiKey: "tolk_pub_your_key")} catch { print("Tolinku configuration failed: \(error)")}Optionally specify a custom base URL:
try Tolinku.configure( apiKey: "tolk_pub_your_key", baseURL: "https://your-app.tolinku.com")Access the shared instance anywhere in your app:
// Optional access (nil before configure)Tolinku.shared?.track("custom.screen_view")
// Throwing access (throws TolinkuError.notConfigured)let tolinku = try Tolinku.requireShared()User identification
Section titled “User identification”Tolinku.shared?.setUserId("user_123")
// Clear on logoutTolinku.shared?.setUserId(nil)Event tracking
Section titled “Event tracking”// Simple eventawait Tolinku.shared?.track("custom.app_open")
// Event with propertiesawait Tolinku.shared?.track("custom.purchase", properties: [ "amount": .string("29.99"), "currency": .string("USD")])
// Force flushawait Tolinku.shared?.flush()Events are batched (10 events or 5-second timer) and auto-flushed when the app enters the background.
Ecommerce tracking
Section titled “Ecommerce tracking”Track purchases, cart activity, and product events via ecommerce:
let tolinku = try Tolinku.requireShared()tolinku.setUserId("user_123")
// Track a purchaseawait tolinku.ecommerce.purchase( transactionId: "order_456", revenue: 49.99, currency: "USD", items: [TolinkuItem(itemId: "sku_1", itemName: "T-Shirt", price: 24.99, quantity: 2)])
// Track product views and cart eventsawait tolinku.ecommerce.viewItem( items: [TolinkuItem(itemId: "sku_1", itemName: "T-Shirt", price: 24.99)])await tolinku.ecommerce.addToCart( items: [TolinkuItem(itemId: "sku_1", quantity: 1)])await tolinku.ecommerce.beginCheckout()
// Search and ratingsawait tolinku.ecommerce.search(searchTerm: "shoes")await tolinku.ecommerce.rate(itemId: "sku_1", rating: 4.5, maxRating: 5)
// Force flushawait tolinku.ecommerce.flush()Ecommerce events are batched (10 events or 5-second timer) and auto-flushed when the app enters the background. The SDK manages cart IDs automatically via UserDefaults, clearing them after purchase. All money values use Swift Decimal for precision.
Handling Universal Links
Section titled “Handling Universal Links”Parse incoming Universal Links using the static helper:
// UIKit AppDelegatefunc application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL, let result = Tolinku.handleUniversalLink(url) else { return false }
// result.path - e.g. "/merchant/abc123" // result.queryItems - any query parameters navigateToDeepLink(path: result.path) return true}For SwiftUI:
@mainstruct MyApp: App { init() { try? Tolinku.configure(apiKey: "tolk_pub_your_key") }
var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in if let result = Tolinku.handleUniversalLink(url) { // Navigate to result.path } } } }}Resolving an incoming link
Section titled “Resolving an incoming link”A link arrives as the URL that was tapped, exactly as it was written. That is enough while the URL is readable, but every route also has a short link, and a short link is the same route written as a code:
https://links.example.com/s7k2p9q/4821Nothing in that URL says which route it is, and nothing on the device can work it out. Short links are what the dashboard’s copy button gives you and what a QR code carries, so your app will receive them whether or not you chose to share them.
links.resolve asks Tolinku and answers with the route, the token and the
canonical path. A readable URL resolves to itself, so resolve every incoming
link rather than trying to spot the short ones. It returns nothing rather than
throwing when it cannot reach us, so your own handling stays the fallback.
func handle(_ url: URL) async { let link = await Tolinku.shared.links.resolve(url) let path = link?.deepLinkPath ?? url.path // path -> "/merchant/abc123", and link?.token -> "abc123" route(to: path)}token saves you working out which segment it is, which the URL alone does not
tell you when a route’s prefix places its token mid-path.
Deferred deep linking
Section titled “Deferred deep linking”Recover the link a user tapped before they had your app, and route them to it on first launch.
if let link = try await Tolinku.shared?.deferred.claimDeferredLink( appspaceId: "64f0a1b2c3d4e5f60718") { // Route to link.deepLinkPath}Call it once, on the first launch after install.
How matching works on iOS
Section titled “How matching works on iOS”There is no Play Install Referrer on iOS, so this is device signal matching: timezone, language, screen size, pixel ratio and OS version are compared against what the landing page recorded when the link was tapped. Matching is probabilistic and the window is short, which is why the claim has to happen on first launch rather than later.
On Android the same call tries the Play referrer first, which is deterministic. That difference is in the SDKs, not in your code: the call is the same name with the same arguments on every platform.
Calling it once
Section titled “Calling it once”A claim is consumed the first time it succeeds. claimDeferredLink remembers
that it asked, so calling it again costs nothing.
Only a settled answer is remembered. “Nothing waiting for this device” counts,
because no amount of asking will change it. A thrown error does not, so a bad
connection, or an appspaceId you are about to correct, leaves the next launch
free to try again.
The lower-level calls
Section titled “The lower-level calls”Both are still available and unchanged. They ask every time they are called and do no remembering.
let byToken = try await Tolinku.shared?.deferred.claimByToken(token)let bySignals = try await Tolinku.shared?.deferred.claimBySignals( appspaceId: "64f0a1b2c3d4e5f60718")Counting taps that open your app directly
Section titled “Counting taps that open your app directly”A link that opens your app directly never reaches Tolinku, so the tap is not counted. Those taps are the ones from people who already have your app, so leaving them out makes a campaign aimed at existing customers look like it got no traffic.
trackLinkOpen reports one. Call it wherever your app receives an incoming link.
A link arrives in two places and both need it. One that launches your app cold arrives somewhere different from one tapped while the app is already running, and instrumenting only the second misses the more common case while appearing to work.
// Covers both a launch and a resume in one place.func application(_ app: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { if let url = userActivity.webpageURL { Task { await Tolinku.shared?.trackLinkOpen(url.absoluteString) } } return true}Wiring both is safe: some link plugins hand the launching link to the listener as well, and the same link inside a few seconds is reported once rather than counted twice.
Only http and https links are reported. A custom scheme means Tolinku’s own
hand-off page opened your app, and that tap was counted when the page was served.
The call never throws and never blocks.
Whether these are recorded is an Appspace setting, and it decides the bill. See Attributing app opens.
Referrals
Section titled “Referrals”let referrals = Tolinku.shared!.referrals
// Create a referral codelet result = try await referrals.create(userId: "user_123", userName: "Jane")print(result.referral_code) // "ABC123"print(result.referral_url) // "https://myapp.tolinku.com/ref/ABC123"
// Look up a referrallet info = try await referrals.get(code: "ABC123")
// Link a referred user (status stays pending until reward milestone is reached)try await referrals.complete(code: "ABC123", referredUserId: "user_456")
// Update milestone (completes the referral if it matches the reward milestone)try await referrals.milestone(code: "ABC123", milestone: "first_purchase")
// Claim reward (after granting it in your system)try await referrals.claimReward(code: "ABC123")
// Get leaderboardlet leaders = try await referrals.leaderboard(limit: 10)In-app messages
Section titled “In-app messages”Fetch and display messages:
// UIKit: show highest-priority messageawait Tolinku.shared?.messages.show( trigger: "on_open", from: viewController, onAction: { action in // Handle CTA action URL }, onDismiss: { // Message dismissed })Messages are rendered in a WKWebView dialog.
Teardown
Section titled “Teardown”Clean up when the app terminates or you need to reconfigure:
await Tolinku.destroy()await Tolinku.shutdown() is the same call under the name this SDK shipped
with. It still works and is not deprecated. destroy() is the name every
Tolinku SDK uses, so prefer it in new code.