LimeLink iOS SDK
SDK Version: 0.2.0 | iOS 12.0+ | Swift 5.0 | Xcode 14.0+
Requirements
| Item | Minimum Version |
|---|---|
| iOS Deployment Target | 12.0 |
| Swift | 5.0 |
| Xcode | 14.0+ |
| CocoaPods | 1.11.0+ |
Prerequisites:
- Register your app at limelink.org and obtain an API Key.
- Enable Associated Domains capability in your Apple Developer Account.
Installation
Swift Package Manager (Recommended)
- In Xcode, go to File > Add Package Dependencies...
- Enter the package URL:
https://github.com/hellovelope/limelink-ios-sdk.git - Select
Up to Next Major Versionand enter0.2.0 - 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 theLimelinkIOSSDKlibrary 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
.xcworkspacefile (not.xcodeproj).
Manual Installation
- Copy the
LimelinkIOSSDK/Classes/directory into your project. - Add the files to your target in Xcode.
- Set
DEFINES_MODULE = YESin 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
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey | String | (required) | API key from the limelink.org console |
baseUrl | String | "https://limelink.org/" | API server base URL |
loggingEnabled | Bool | false | Enable [LimeLinkSDK] prefixed console logs |
deferredDeeplinkEnabled | Bool | true | Auto-check deferred deep link on first launch |
baseUrlautomatically appends a trailing slash if missing.
isInitialized Property
You can check the SDK initialization status:
if LimeLinkSDK.shared.isInitialized {
// SDK is ready
}
Universal Link Setup
1. Associated Domains
- In Xcode, go to your target > Signing & Capabilities tab.
- Click + Capability > Associated Domains.
- 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, usestatic let sharedto keep it alive.
LimeLinkResult Properties
| Property | Type | Description |
|---|---|---|
originalUrl | String? | The original URL that was opened |
resolvedUri | String? | The final URI resolved by the API |
queryParams | [String: String] | URL query parameters |
pathParams | PathParamResponse | Path parameters (mainPath, subPath) |
isDeferred | Bool | Whether this is a deferred deep link |
LimeLinkError Properties
| Property | Type | Description |
|---|---|---|
code | Int | Error code |
message | String | Error message |
underlyingError | Error? | Underlying error (if any) |
Error Codes
| Code | Description |
|---|---|
-1 | SDK not initialized |
404 | Link resolution failed |
0 | Invalid URL or no deferred deep link match |
4xx/5xx | API server error |
Universal Link Flow
Subdomain Method (Recommended)
https://{subdomain}.limelink.org/link/{linkSuffix}
https://{subdomain}.limelink.org/link/{linkSuffix}?campaign=summer&source=email
Flow:
- User clicks the URL
- SDK extracts the subdomain and link suffix
- SDK fetches headers from
https://{subdomain}.limelink.org - SDK calls
GET /api/v1/app/dynamic_link/{linkSuffix}?full_request_url={encoded_url}with headers - API returns the resolved
uri - Listener receives a
LimeLinkResultwith 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.
Legacy Deeplink Pattern
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
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:
- User clicks a link on the web
- Server stores device information (width, height, user agent) with the link
- User is redirected to the App Store
- After installation, on first launch, SDK sends device information to the server
- Server matches the device and returns the original deep link
- 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
| Information | Description |
|---|---|
| Screen Width | Device screen width in points |
| Screen Height | Device screen height in points |
| User Agent | iOS 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
- Marketing Campaigns: User clicks a campaign link, installs the app, and is automatically directed to the promoted content.
- Product Sharing: User shares a product link. The recipient installs the app and is taken directly to the shared product.
- 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];
Universal Link Handling
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
| Method | Description |
|---|---|
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
| Class | Description |
|---|---|
LimeLinkConfig | SDK configuration (apiKey, baseUrl, loggingEnabled, deferredDeeplinkEnabled) |
LimeLinkResult | Deep link result (originalUrl, resolvedUri, queryParams, pathParams, isDeferred) |
LimeLinkError | Error model (code, message, underlyingError) |
PathParamResponse | Path 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.
Universal Link not delivered
- Associated Domains: Verify
applinks:domains are correctly registered in Xcode > Signing & Capabilities. - AASA file: Confirm
https://yourdomain.com/.well-known/apple-app-site-associationis served correctly. - Reinstall: On simulators, you may need to reinstall the app to refresh Associated Domains.
- Safari: Typing a URL directly in Safari's address bar does not trigger Universal Links. Tap the link from another app (Notes, Messages, etc.).
Deferred deep link not working
- First launch detection:
LinkStats.isFirstLaunch()usesUserDefaults. Deleting and reinstalling the app resets this. - Server matching: The server matches by screen size and OS version, so the click and install must happen on the same device.
- 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:
DEFINES_MODULE=YESSWIFT_OBJC_INTERFACE_HEADER_NAME=LimelinkIOSSDK-Swift.hCLANG_ENABLE_MODULES=YES
Migration from Previous Versions
API Mapping
| Old API | New 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.