LimeLink iOS SDK

Target SDK Version: 0.4.0 | iOS 12.0+ | Swift 5.0 | Xcode 14.0+

LimeLink iOS SDK provides Universal Link resolution, deferred deep links, and explicit link event tracking through a binary XCFramework.

Version 0.4.0 moves SDK-owned networking to the Mobile API V2 contract and requires the public Project UUID for initialization. It no longer uses an Organization API credential as native SDK identity.

View previous iOS SDK releases

Before You Start

  1. Create or open a LimeLink Project.
  2. Register the iOS Application in that Project.
  3. Copy the Project ID shown in Project settings. It must be a canonical UUID and is not a secret.
  4. Decide which destination schemes and HTTPS hosts your app will allow before routing a resolved URI.

Installation

The supported consumer routes are binary Swift Package Manager and binary CocoaPods. Do not use the SDK source repository as an app dependency.

Swift Package Manager

Add this package in Xcode:

https://github.com/hellovelop/limelink-ios-sdk-binary.git

Select version 0.4.0, or add it to Package.swift:

dependencies: [
    .package(
        url: "https://github.com/hellovelop/limelink-ios-sdk-binary.git",
        from: "0.4.0"
    )
]

Add the LimelinkIOSSDK product to your app target.

CocoaPods

source 'https://cdn.cocoapods.org/'
platform :ios, '12.0'

target 'YourApp' do
  pod 'LimelinkIOSSDK', '~> 0.4.0'
end

The shipped XCFramework is dynamic and is verified without use_frameworks!, with use_frameworks!, and with use_frameworks! :linkage => :static.

Privacy Manifest

Version 0.4.0 includes an SDK-owned PrivacyInfo.xcprivacy in every XCFramework slice. It declares SDK-required UserDefaults access and deferred-link data practices with tracking disabled.

Your app remains responsible for its own required-reason APIs, collected data, tracking declarations, third-party SDK disclosures, and the final merged privacy report.

Initialize the SDK

Initialize once during app launch with the Project UUID:

import LimelinkIOSSDK

let config = LimeLinkConfig(
    projectId: "550e8400-e29b-41d4-a716-446655440000",
    loggingEnabled: false,
    deferredDeeplinkEnabled: true
)
LimeLinkSDK.initialize(config: config)

baseUrl defaults to https://api.limelink.org/. Production apps normally should keep the default. Duplicate initialization calls are ignored.

Add the domains your app handles to Associated Domains:

applinks:limelink.org
applinks:*.limelink.org
applinks:links.example.com

For a Project Custom Domain, publish the correct Apple App Site Association file for that hostname. Register every allowed custom callback scheme in CFBundleURLSchemes.

Forward browsing web activities to the SDK:

func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return false
    }

    LimeLinkSDK.shared.handleUniversalLink(url)
    return true
}

For scenes:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return }
    LimeLinkSDK.shared.handleUniversalLink(url)
}

Custom-scheme app-open events belong to your app router; do not send them to handleUniversalLink(_:) for network resolution.

The SDK accepts an absolute HTTPS inbound URL with a host and a supported Link path. It sends the complete OS-provided URL once as full_request_url, preserving ordered and duplicate query items.

The URI returned by LimeLink is final. The SDK does not append the inbound query again. resolvedUri preserves the final URI, while queryParams is a dictionary view in which the last duplicate value wins.

Receive Results

Listeners are weak. Retain a long-lived listener and register it early:

final class DeepLinkManager: NSObject, LimeLinkListener {
    static let shared = DeepLinkManager()

    private override init() {
        super.init()
        LimeLinkSDK.shared.addLinkListener(self)
    }

    func onDeeplinkReceived(result: LimeLinkResult) {
        guard isAllowedDestination(result.resolvedUri) else { return }
        navigateTo(result.resolvedUri)
    }

    func onDeferredDeepLinkNotFound() {
        // Normal terminal result: continue the default launch flow.
    }

    func onDeeplinkError(error: LimeLinkError) {
        // Show a safe fallback without exposing request details.
    }
}

Before opening a resolved URI, parse it and compare its scheme and host against an explicit app-owned allowlist. SDK validation does not make arbitrary destinations trusted.

An automatic launch outcome completed before registration is retained in process memory and replayed once to the first listener. Public callbacks and pending replay run on the main thread.

Automatic deferred resolution is enabled by default. The SDK sends the configured Project UUID and bounded screen, language, timezone, OS, and canonical phone/tablet evidence. It does not include raw IP in the request body and applies a five-second network timeout.

You can request a fresh manual check:

LimeLinkSDK.shared.handleDeferredDeepLink { result, error in
    switch (result, error) {
    case let (result?, nil):
        guard isAllowedDestination(result.resolvedUri) else { return }
        navigateTo(result.resolvedUri)
    case (nil, nil):
        // Normal not-found outcome.
        break
    case let (nil, error?):
        showError(error.message)
    default:
        break
    }
}

Manual matched, not-found, and failed outcomes are each delivered exactly once to both the completion and the current listener. Both channels run on the main thread; their relative order is unspecified. Automatic and manual checks can share one in-flight request. A transport, server, or validation failure remains eligible on a later launch, but the SDK does not immediately loop retries.

Explicit Stats

Successful Dynamic Lookup already records lookup attribution, so the SDK does not send a second automatic Stats request.

Use explicit tracking only when your app intentionally owns a separate view event. Pass the original inbound LimeLink URL, not the server-returned destination URI:

LimeLinkSDK.shared.trackLinkStatus(url: inboundLimeLinkURL)

Explicit Stats is best effort. Failure never changes Link delivery, and HTTP acceptance is not a guarantee that a metric incremented.

Objective-C

Import the generated Swift interface and initialize with Project ID:

#import <LimelinkIOSSDK/LimelinkIOSSDK-Swift.h>

LimeLinkConfig *config = [[LimeLinkConfig alloc]
    initWithProjectId:@"550e8400-e29b-41d4-a716-446655440000"
    baseUrl:@"https://api.limelink.org/"
    loggingEnabled:NO
    deferredDeeplinkEnabled:YES];
[LimeLinkSDK initialize:config];

[[LimeLinkSDK shared] handleUniversalLink:url];

Objective-C and Objective-C++ module imports are supported. For manual deferred handling use handleDeferredDeepLinkWithCompletion:; (result, nil) is matched, (nil, nil) is not found, and (nil, error) is failed.

Public API

APIPurpose
LimeLinkSDK.initialize(config:)Initialize once with Project ID
addLinkListener(_:)Register a weak result listener
removeLinkListener(_:)Remove a listener
handleUniversalLink(_:)Resolve an inbound HTTPS Link
handleDeferredDeepLink(completion:)Perform a manual deferred check
trackLinkStatus(url:)Send an explicit best-effort view event

Use the facade and public configuration/result models. Resolver services, URL parsers, request models, and state management are internal.

Migrating from 0.3.5

Troubleshooting

SDK not initialized

Call LimeLinkSDK.initialize(config:) with a valid Project UUID before forwarding links or requesting deferred resolution.

  1. Verify Associated Domains and the AASA file.
  2. Confirm the inbound URL is absolute HTTPS with a supported Link path.
  3. Forward only NSUserActivityTypeBrowsingWeb activities.
  4. Retain and register the listener early.
  5. Check the safe SDK error callback; do not log full inbound or resolved URLs.

CocoaPods cannot find the module

Open the .xcworkspace, clean Derived Data, confirm the selected pod version is 0.4.0 or later, and rerun pod install using the CocoaPods CDN.