LimeLink iOS SDK

SDK Version: 0.2.0 | iOS 12.0+ | Swift 5.0 | Xcode 14.0+


Requirements

ItemMinimum Version
iOS Deployment Target12.0
Swift5.0
Xcode14.0+
CocoaPods1.11.0+

Prerequisites:


Installation

  1. In Xcode, go to File > Add Package Dependencies...
  2. Enter the package URL:
    https://github.com/hellovelope/limelink-ios-sdk.git
    
  3. Select Up to Next Major Version and enter 0.2.0
  4. Click Add Package

Or add it directly in Package.swift:

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

Add the dependency to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "LimelinkIOSSDK", package: "limelink-ios-sdk")
    ]
)

Note: The SPM package includes both a Swift target (LimelinkIOSSDK) and an ObjC Bridge target (LimelinkIOSSDKObjC). Adding the LimelinkIOSSDK library includes both targets.

CocoaPods

Add the following to your Podfile:

platform :ios, '12.0'
use_frameworks!

target 'YourApp' do
  pod 'LimelinkIOSSDK'
end

Run the install command:

pod install

After installation, open the .xcworkspace file (not .xcodeproj).

Manual Installation

  1. Copy the LimelinkIOSSDK/Classes/ directory into your project.
  2. Add the files to your target in Xcode.
  3. Set DEFINES_MODULE = YES in Build Settings.

SDK Initialization

AppDelegate

Initialize the SDK in AppDelegate.swift at app launch:

import UIKit
import LimelinkIOSSDK

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Initialize LimeLink SDK
        let config = LimeLinkConfig(
            apiKey: "YOUR_API_KEY",
            loggingEnabled: false,            // Set to true for debug builds
            deferredDeeplinkEnabled: true
        )
        LimeLinkSDK.initialize(config: config)

        return true
    }
}

Important: initialize(config:) must be called only once at app launch. Duplicate calls are ignored.

Config Parameters

ParameterTypeDefaultDescription
apiKeyString(required)API key from the limelink.org console
baseUrlString"https://limelink.org/"API server base URL
loggingEnabledBoolfalseEnable [LimeLinkSDK] prefixed console logs
deferredDeeplinkEnabledBooltrueAuto-check deferred deep link on first launch

baseUrl automatically appends a trailing slash if missing.

isInitialized Property

You can check the SDK initialization status:

if LimeLinkSDK.shared.isInitialized {
    // SDK is ready
}

1. Associated Domains

  1. In Xcode, go to your target > Signing & Capabilities tab.
  2. Click + Capability > Associated Domains.
  3. Add the following domains:
applinks:limelink.org
applinks:*.limelink.org

For custom domains:

applinks:yourdomain.com

2. Info.plist URL Scheme

Register a custom URL scheme in your Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.yourapp.deeplink</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourapp</string>
        </array>
    </dict>
</array>

3. AppDelegate URL Handling

Add URL handling methods to your AppDelegate:

// MARK: - Universal Link Handling

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

// MARK: - Custom URL Scheme Handling

func application(_ app: UIApplication,
                 open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    LimeLinkSDK.shared.handleUniversalLink(url)
    return true
}

4. SceneDelegate (iOS 13+)

If your app uses UISceneDelegate, add the following to SceneDelegate.swift:

import LimelinkIOSSDK

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    // Cold launch — app opened from terminated state via Universal Link
    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        if let userActivity = connectionOptions.userActivities.first,
           userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           let url = userActivity.webpageURL {
            LimeLinkSDK.shared.handleUniversalLink(url)
        }
    }

    // Warm launch — app already running, Universal Link received
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           let url = userActivity.webpageURL {
            LimeLinkSDK.shared.handleUniversalLink(url)
        }
    }

    // Custom URL Scheme handling
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        if let url = URLContexts.first?.url {
            LimeLinkSDK.shared.handleUniversalLink(url)
        }
    }
}

Listener Implementation

LimeLinkListener Protocol

Implement the LimeLinkListener protocol to receive deep link results:

@objc public protocol LimeLinkListener: AnyObject {
    func onDeeplinkReceived(result: LimeLinkResult)        // Required
    @objc optional func onDeeplinkError(error: LimeLinkError)  // Optional
}

ViewController Implementation

import UIKit
import LimelinkIOSSDK

class MainViewController: UIViewController, LimeLinkListener {

    override func viewDidLoad() {
        super.viewDidLoad()
        LimeLinkSDK.shared.addLinkListener(self)
    }

    // MARK: - LimeLinkListener (Required)

    func onDeeplinkReceived(result: LimeLinkResult) {
        guard let uri = result.resolvedUri else { return }

        if result.isDeferred {
            // Deferred Deep Link: received on first launch after install
            print("Deferred deep link: \(uri)")
        } else {
            // Universal Link: received while app is running
            print("Universal link resolved: \(uri)")
        }

        // Route based on URI
        navigateToContent(uri: uri)
    }

    // MARK: - LimeLinkListener (Optional)

    func onDeeplinkError(error: LimeLinkError) {
        switch error.code {
        case -1:
            print("SDK not initialized.")
        case 404:
            print("Link could not be resolved: \(error.message)")
        default:
            print("Deeplink error [\(error.code)]: \(error.message)")
        }
    }

    // MARK: - Navigation

    private func navigateToContent(uri: String) {
        guard let url = URL(string: uri) else { return }
        let pathComponents = url.pathComponents

        // Example: myapp://product/123 → navigate to ProductViewController
        if pathComponents.contains("product"), let id = pathComponents.last {
            let vc = ProductViewController(productId: id)
            navigationController?.pushViewController(vc, animated: true)
        }
    }
}

DeepLinkManager Singleton Pattern

For app-wide deep link handling, create a dedicated manager:

import LimelinkIOSSDK

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

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

    func onDeeplinkReceived(result: LimeLinkResult) {
        guard let uri = result.resolvedUri else { return }

        // Broadcast via NotificationCenter or handle routing directly
        NotificationCenter.default.post(
            name: .didReceiveDeepLink,
            object: nil,
            userInfo: ["uri": uri, "isDeferred": result.isDeferred]
        )
    }

    func onDeeplinkError(error: LimeLinkError) {
        print("[DeepLinkManager] Error: \(error.message)")
    }
}

extension Notification.Name {
    static let didReceiveDeepLink = Notification.Name("didReceiveDeepLink")
}

Activate the manager in AppDelegate:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Initialize SDK
    LimeLinkSDK.initialize(config: config)

    // Activate deep link manager
    _ = DeepLinkManager.shared

    return true
}

Note: Listeners are managed using NSHashTable.weakObjects(), so they are automatically removed when the listener object is deallocated. For the global manager pattern, use static let shared to keep it alive.

LimeLinkResult Properties

PropertyTypeDescription
originalUrlString?The original URL that was opened
resolvedUriString?The final URI resolved by the API
queryParams[String: String]URL query parameters
pathParamsPathParamResponsePath parameters (mainPath, subPath)
isDeferredBoolWhether this is a deferred deep link

LimeLinkError Properties

PropertyTypeDescription
codeIntError code
messageStringError message
underlyingErrorError?Underlying error (if any)

Error Codes

CodeDescription
-1SDK not initialized
404Link resolution failed
0Invalid URL or no deferred deep link match
4xx/5xxAPI server error

https://{subdomain}.limelink.org/link/{linkSuffix}
https://{subdomain}.limelink.org/link/{linkSuffix}?campaign=summer&source=email

Flow:

  1. User clicks the URL
  2. SDK extracts the subdomain and link suffix
  3. SDK fetches headers from https://{subdomain}.limelink.org
  4. SDK calls GET /api/v1/app/dynamic_link/{linkSuffix}?full_request_url={encoded_url} with headers
  5. API returns the resolved uri
  6. Listener receives a LimeLinkResult with the resolved URI

Query String Handling: All query parameters in the original URL are preserved and sent to the API. For example:

Original:  https://abc.limelink.org/link/test?campaign=summer&source=email
API call:  GET /api/v1/app/dynamic_link/test?full_request_url=https://abc.limelink.org/link/test?campaign=summer&source=email

Direct Access Method

https://limelink.org/api/v1/app/dynamic_link/{suffix}
https://limelink.org/universal-link/app/dynamic_link/{suffix}

When accessing the API URL directly, the SDK makes a direct API request and returns the resolved uri via the listener.

For URLs with hosts other than *.limelink.org, the SDK calls the legacy endpoint at https://deep.limelink.org/link with query parameters (subdomain, path, platform=ios).


Deferred Deep Link allows you to retrieve deep link information when the app is first launched after installation, even if the user clicked the link before the app was installed.

How It Works

The SDK uses device fingerprinting (screen size and OS version) to match users:

  1. User clicks a link on the web
  2. Server stores device information (width, height, user agent) with the link
  3. User is redirected to the App Store
  4. After installation, on first launch, SDK sends device information to the server
  5. Server matches the device and returns the original deep link
  6. Listener receives the result with isDeferred = true

Automatic Mode (Default)

When deferredDeeplinkEnabled = true (default), the SDK automatically checks for deferred deep links during initialize(config:) on first launch.

The result is delivered to registered listeners via onDeeplinkReceived(result:) with result.isDeferred == true.

Manual Mode

To disable automatic detection and control the timing yourself:

let config = LimeLinkConfig(
    apiKey: "YOUR_API_KEY",
    deferredDeeplinkEnabled: false  // Disable auto-check
)
LimeLinkSDK.initialize(config: config)

Invoke manually at the desired time:

LimeLinkSDK.shared.handleDeferredDeepLink { result, error in
    if let result = result {
        // result.isDeferred == true
        // result.resolvedUri contains the deep link URI
    }
    if let error = error {
        // No match found or network error
    }
}

API Flow

1. SDK collects device info (screen width, height, OS version)
       ↓
2. GET /api/v1/deferred-deep-link?width=414&height=896&user_agent=iOS 18_7
       ↓
3. Server returns: { "suffix": "testsub", "full_request_url": "https://example.com/link" }
       ↓
4. GET /api/v1/app/dynamic_link/testsub?full_request_url=https://example.com/link&event_type=setup
       ↓
5. Server returns: { "uri": "myapp://product/123" }
       ↓
6. Listener receives LimeLinkResult with isDeferred=true

Device Information Collected

InformationDescription
Screen WidthDevice screen width in points
Screen HeightDevice screen height in points
User AgentiOS version in format "iOS 18_7" (e.g., iOS 18.7)

No personally identifiable information (PII), IDFA, or IDFV is collected.

Event Tracking

When a deferred deep link is successfully retrieved, the SDK automatically sends an event with event_type=setup to the server for conversion tracking.

Use Cases

  1. Marketing Campaigns: User clicks a campaign link, installs the app, and is automatically directed to the promoted content.
  2. Product Sharing: User shares a product link. The recipient installs the app and is taken directly to the shared product.
  3. Referral Programs: Track referral sources and direct new users to specific content or rewards.

Stats Tracking

Automatic

When a deep link is resolved via handleUniversalLink(_:), the SDK automatically sends stats events to the server.

Manual

For additional tracking from other screens:

// Send stats event for a deep link URI
if let url = URL(string: resolvedUri) {
    LimeLinkSDK.shared.trackLinkStatus(url: url)
}

Objective-C Support

SDK Initialization

LimeLinkConfig *config = [[LimeLinkConfig alloc] initWithApiKey:@"YOUR_API_KEY"
                                                        baseUrl:@"https://limelink.org/"
                                                 loggingEnabled:YES
                                        deferredDeeplinkEnabled:YES];
[LimeLinkSDK initialize:config];

Method 1: Using SDK directly (recommended)

- (BOOL)application:(UIApplication *)application
    continueUserActivity:(NSUserActivity *)userActivity
      restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *))restorationHandler {

    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSURL *url = userActivity.webpageURL;
        if (url) {
            [[LimeLinkSDK shared] handleUniversalLink:url];
            return YES;
        }
    }
    return NO;
}

Method 2: Using UniversalLinkHandlerBridge

#import <LimelinkIOSSDK/UniversalLinkHandlerBridge.h>

- (BOOL)application:(UIApplication *)application
    continueUserActivity:(NSUserActivity *)userActivity
      restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *))restorationHandler {

    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSURL *url = userActivity.webpageURL;
        if (url) {
            [UniversalLinkHandlerBridge handleUniversalLink:url
                completion:^(NSString * _Nullable uri) {
                    if (uri) {
                        NSLog(@"Resolved URI: %@", uri);
                    }
                }];
            return YES;
        }
    }
    return NO;
}

Listener Implementation

// YourViewController.h
@import LimelinkIOSSDK;

@interface YourViewController : UIViewController <LimeLinkListener>
@end

// YourViewController.m
@implementation YourViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [[LimeLinkSDK shared] addLinkListener:self];
}

- (void)onDeeplinkReceivedWithResult:(LimeLinkResult *)result {
    NSLog(@"URI: %@", result.resolvedUri);
    NSLog(@"Is Deferred: %d", result.isDeferred);
}

- (void)onDeeplinkErrorWithError:(LimeLinkError *)error {
    NSLog(@"Error [%ld]: %@", (long)error.code, error.message);
}

@end

URL Parameter Parsing

The SDK automatically parses URL parameters and provides them in the LimeLinkResult:

func onDeeplinkReceived(result: LimeLinkResult) {
    // Query parameters: ?campaign=summer&source=email
    let campaign = result.queryParams["campaign"]  // "summer"
    let source = result.queryParams["source"]      // "email"

    // Path parameters: /product/detail
    let mainPath = result.pathParams.mainPath  // "product"
    let subPath = result.pathParams.subPath    // "detail"
}

API Reference

Public Methods

MethodDescription
LimeLinkSDK.initialize(config:)Initialize SDK (call once at app launch)
LimeLinkSDK.shared.addLinkListener(_:)Register a deep link listener (weak reference)
LimeLinkSDK.shared.removeLinkListener(_:)Remove a listener
LimeLinkSDK.shared.handleUniversalLink(_:)Process a Universal Link URL
LimeLinkSDK.shared.handleDeferredDeepLink(completion:)Manually check for deferred deep link
LimeLinkSDK.shared.trackLinkStatus(url:)Manually send a stats event

Models

ClassDescription
LimeLinkConfigSDK configuration (apiKey, baseUrl, loggingEnabled, deferredDeeplinkEnabled)
LimeLinkResultDeep link result (originalUrl, resolvedUri, queryParams, pathParams, isDeferred)
LimeLinkErrorError model (code, message, underlyingError)
PathParamResponsePath parameters (mainPath, subPath)

Troubleshooting

"SDK not initialized" error

LimeLinkError(code: -1, message: SDK not initialized)

Cause: handleUniversalLink(_:) was called before LimeLinkSDK.initialize(config:).

Solution: Ensure initialization in didFinishLaunchingWithOptions runs before any Universal Link handling code.

  1. Associated Domains: Verify applinks: domains are correctly registered in Xcode > Signing & Capabilities.
  2. AASA file: Confirm https://yourdomain.com/.well-known/apple-app-site-association is served correctly.
  3. Reinstall: On simulators, you may need to reinstall the app to refresh Associated Domains.
  4. Safari: Typing a URL directly in Safari's address bar does not trigger Universal Links. Tap the link from another app (Notes, Messages, etc.).
  1. First launch detection: LinkStats.isFirstLaunch() uses UserDefaults. Deleting and reinstalling the app resets this.
  2. Server matching: The server matches by screen size and OS version, so the click and install must happen on the same device.
  3. Timing: There may be a short delay between the web click and the server storing the fingerprint.

CocoaPods build error

Module 'LimelinkIOSSDK' not found

Solution:

pod deintegrate
pod install

If the issue persists, clear Derived Data:

rm -rf ~/Library/Developer/Xcode/DerivedData

Objective-C project: Swift header not found

'LimelinkIOSSDK-Swift.h' file not found

Solution: Verify these Build Settings:


Migration from Previous Versions

API Mapping

Old APINew API (v0.2.0)
saveLimeLinkStatus(url:, privateKey:)LimeLinkSDK.initialize(config:) — automatic
UniversalLink.shared.handleUniversalLink(url, completion:)LimeLinkSDK.shared.handleUniversalLink(url) + Listener
DeferredDeepLinkService.getDeferredDeepLink(completion:)LimeLinkSDK.shared.handleDeferredDeepLink(completion:) — or automatic
parsePathParams(from:)LimeLinkResult.pathParams

Before (Previous Versions)

// Manual stats saving
saveLimeLinkStatus(url: url, privateKey: "your_key")

// Direct UniversalLink handler
UniversalLink.shared.handleUniversalLink(url) { uri in
    if let uri = uri {
        // Handle URI
    }
}

// Manual deferred deep link check
if LinkStats.isFirstLaunch() {
    DeferredDeepLinkService.getDeferredDeepLink { result in
        switch result {
        case .success(let uri):
            // Handle URI
        case .failure(let error):
            // Handle error
        }
    }
}

After (v0.2.0)

// One-time initialization in AppDelegate
let config = LimeLinkConfig(apiKey: "your_key")
LimeLinkSDK.initialize(config: config)

// Register listener — handles all deep links including deferred
class MyViewController: UIViewController, LimeLinkListener {
    override func viewDidLoad() {
        super.viewDidLoad()
        LimeLinkSDK.shared.addLinkListener(self)
    }

    func onDeeplinkReceived(result: LimeLinkResult) {
        // All info in result:
        // result.resolvedUri, result.queryParams
        // result.pathParams, result.isDeferred
    }
}

// Universal Link handling via SDK singleton
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if let url = userActivity.webpageURL {
        LimeLinkSDK.shared.handleUniversalLink(url)
        return true
    }
    return false
}

Stats tracking and deferred deep link handling are now automatic. No manual calls needed.