Skip to content

AI Implementation Guide - iOS

How to use this page: Copy everything below the horizontal line and paste it into your AI coding assistant (Claude, ChatGPT, GitHub Copilot, Cursor, etc.) to get accurate MovableInk SDK implementation help.

Sample Prompts

After pasting the context below into your AI assistant, try these prompts:

  • "Implement MovableInk deeplinking in my SwiftUI app using the context I provided"
  • "Add MovableInk deeplinking to my UIKit app"
  • "Help me configure Universal Links for MovableInk"
  • "Implement behavior events to capture when users view products"
  • "Add orderCompleted event tracking to my checkout flow"
  • "Help me test my MovableInk integration"
  • "Debug why my MovableInk deeplinks aren't working"

Context for AI Assistant

Copy everything inside the code block below and paste into your AI assistant:

# MovableInk iOS SDK Implementation Reference

## Overview

### What is MovableInk?
MovableInk is a marketing personalization platform that helps brands create dynamic, personalized content for email, mobile, and web. The iOS SDK enables two key capabilities:

1. **Deeplinking**: Users tap links in marketing emails → your app opens to specific content
2. **Behavior Events**: Capture user interactions (product views, purchases) to power personalization

### SDK Information
- **Current Version**: 2.3.0
- **Minimum iOS**: 13.0
- **Language**: Swift (Objective-C compatible)
- **GitHub**: https://github.com/movableink/ios-sdk

## Key Terminology

- **MIU (MovableInk User ID)**: Unique, non-PII identifier linking app users to marketing profiles (typically a UUID, NOT an email)
- **MovableInk Link**: URL format `https://mi.yourcompany.com/p/...` in marketing emails
- **AASA**: Apple App Site Association file at `https://mi.yourcompany.com/.well-known/apple-app-site-association` (configured by MovableInk team)
- **Universal Links**: iOS deep linking using https URLs (requires Associated Domains capability)

## Installation

### Swift Package Manager (Recommended)

1. In Xcode: File > Add Package Dependencies
2. Repository URL: `https://github.com/movableink/ios-sdk.git`
3. Version: `2.3.0` or later

### CocoaPods

```ruby
pod 'MovableInk', '~> 2.3.0'
```

Then run: `pod install`

## Configuration for Deeplinking

### Step 1: Add Associated Domains Capability

1. In Xcode, select your app target
2. Go to **Signing & Capabilities** tab
3. Click **+ Capability** → Add **Associated Domains**
4. Add entry: `applinks:mi.yourcompany.com`

**Important**: Replace `mi.yourcompany.com` with actual MovableInk subdomain (no `https://`)

### Step 2: Update Info.plist

Add this to your `Info.plist`:

```xml
<key>movable_ink_universal_link_domains</key>
<array>
    <string>mi.yourcompany.com</string>
</array>
```

**Important**: Replace `mi.yourcompany.com` with actual MovableInk subdomain

## Deeplinking Implementation

### SwiftUI Implementation

```swift
import SwiftUI
import MovableInk

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
                    guard let url = userActivity.webpageURL else { return }

                    MIClient.resolveURL(url) { result in
                        switch result {
                        case .success(let resolvedURL):
                            // Navigate to the resolved URL in your app
                            print("Deeplink resolved to: \(resolvedURL)")
                            handleDeeplink(resolvedURL)

                        case .failure(let error):
                            print("Failed to resolve link: \(error)")
                        }
                    }
                }
        }
    }

    private func handleDeeplink(_ url: URL) {
        // Parse URL and navigate to appropriate screen
        // Example:
        // if url.path.contains("/products/") {
        //     // Navigate to product detail
        // }
    }
}
```

### UIKit Implementation (AppDelegate)

```swift
import UIKit
import MovableInk

class AppDelegate: UIResponder, UIApplicationDelegate {

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

        MIClient.resolveURL(url) { result in
            switch result {
            case .success(let resolvedURL):
                print("Deeplink resolved to: \(resolvedURL)")
                self.handleDeeplink(resolvedURL)

            case .failure(let error):
                print("Failed to resolve link: \(error)")
            }
        }

        return true
    }

    private func handleDeeplink(_ url: URL) {
        // Navigate to appropriate screen based on URL
    }
}
```

### UIKit Implementation (SceneDelegate - iOS 13+)

```swift
import UIKit
import MovableInk

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

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

        MIClient.resolveURL(url) { result in
            switch result {
            case .success(let resolvedURL):
                print("Deeplink resolved to: \(resolvedURL)")
                self.handleDeeplink(resolvedURL)

            case .failure(let error):
                print("Failed to resolve link: \(error)")
            }
        }
    }

    private func handleDeeplink(_ url: URL) {
        // Navigate to appropriate screen based on URL
    }
}
```

## Behavior Events Implementation (Optional)

### Setup

Initialize the SDK with your API key (required only for behavior events, not deeplinking):

```swift
import MovableInk

// In AppDelegate.didFinishLaunchingWithOptions or App.init
MIClient.start(apiKey: "YOUR_API_KEY", region: .us)
```

**Regions**: `.us` or `.eu` (default is `.us` if not specified)

### Set User ID (MIU)

Set the MIU as soon as user is identified (typically after login):

```swift
MIClient.setMIU("USER_UNIQUE_ID")
```

**Important**: 
- MIU must be non-PII (use UUID, NOT email address)
- Must match what marketing team uses in email campaigns
- Should be URL-friendly string

### Product Searched

```swift
MIClient.productSearched(
    properties: .init(
        query: "running shoes",
        url: URL(string: "https://yourapp.com/search?q=running+shoes"),
        meta: [
            "resultsCount": 42
        ]
    )
)
```

### Product Viewed

```swift
MIClient.productViewed(
    properties: .init(
        id: "PROD-12345",
        title: "Running Shoes",
        price: "89.99",
        currency: .USD,
        url: URL(string: "https://yourapp.com/products/12345"),
        categories: [
            ProductCategory(id: "shoes", url: URL(string: "https://yourapp.com/categories/shoes")),
            ProductCategory(id: "running", url: URL(string: "https://yourapp.com/categories/running"))
        ],
        meta: [
            "inStock": true,
            "rating": 4.5
        ]
    )
)
```

**Important**: Product ID must not be empty string

### Product Added (to Cart)

```swift
MIClient.productAdded(
    properties: .init(
        id: "PROD-12345",
        title: "Running Shoes",
        price: "89.99",
        currency: .USD,
        url: URL(string: "https://yourapp.com/products/12345"),
        categories: [
            ProductCategory(id: "shoes"),
            ProductCategory(id: "running")
        ],
        meta: [
            "size": "10",
            "color": "blue"
        ]
    )
)
```

### Product Removed (from Cart)

```swift
MIClient.productRemoved(
    properties: .init(
        id: "PROD-12345",
        title: "Running Shoes",
        price: "89.99",
        currency: .USD,
        url: URL(string: "https://yourapp.com/products/12345")
    )
)
```

### Category Viewed

```swift
MIClient.categoryViewed(
    category: .init(
        id: "shoes",
        title: "Shoes",
        url: URL(string: "https://yourapp.com/categories/shoes"),
        meta: [
            "itemCount": 156
        ]
    )
)
```

### Order Completed

```swift
MIClient.orderCompleted(
    properties: .init(
        id: "ORDER-789",
        revenue: "179.98",
        currency: .USD,
        products: [
            .init(
                id: "PROD-12345",
                title: "Running Shoes",
                price: "89.99",
                url: URL(string: "https://yourapp.com/products/12345"),
                quantity: 2,
                meta: [
                    "size": "10",
                    "color": "blue"
                ]
            )
        ]
    )
)
```

**Important**: 
- Order ID and Product IDs must not be empty strings
- Revenue and price should be in dollars and cents (e.g., "15.99")
- Can include up to 100 products per order

### Currency Support

Available currency values:
- `.USD`, `.EUR`, `.GBP`, `.CAD`, `.AUD`, `.JPY`, etc.
- Default is `.USD` if not specified
- Introduced in SDK v2

## Testing

### Test Deeplinking

1. Build and run your app on device or simulator (iOS 13+)
2. Put app in background or close it
3. Open a MovableInk test link in Safari: `https://mi.yourcompany.com/p/rp/test`
4. App should open automatically
5. Check Xcode console for: `Deeplink resolved to: <final URL>`

### Test Behavior Events

1. Set MIU to test value: `MIClient.setMIU("00000000-0000-0000-0000-000000000000")`
2. Trigger an event (e.g., view a product)
3. Check Xcode console for event logs (filter by "MI SDK")
4. You should see success message with event name
5. Coordinate with MovableInk team to verify events are received

## Common Issues and Solutions

### Deeplinks Not Opening App

**Issue**: Clicking MovableInk links doesn't open the app

**Solutions**:
- Verify AASA file is accessible: `https://mi.yourcompany.com/.well-known/apple-app-site-association`
- Check Associated Domains match exactly (no wildcards, no `https://`)
- Ensure testing on iOS 13+
- Try uninstalling and reinstalling app (clears AASA cache)
- Verify Bundle ID matches what's in AASA file
- Wait 24-48 hours after AASA changes for Apple CDN to update

### Events Not Sending

**Issue**: Behavior events not appearing in MovableInk platform

**Solutions**:
- Verify API key is set: `MIClient.start(apiKey: "...", region: .us)`
- Ensure MIU is set before sending events: `MIClient.setMIU("...")`
- Check console for "MI SDK" logs showing success/failure
- Verify region is correct (`.us` or `.eu`)
- Confirm API key is for correct environment (staging/production)

### MIU Mismatch

**Issue**: Events not associating with correct users in campaigns

**Solutions**:
- Verify MIU matches exactly what marketing team uses in ESP
- Ensure MIU is set after user logs in
- MIU should be UUID format, not email address
- Call `MIClient.identifyUser()` after setting MIU for guest users who log in

### Build Errors

**Issue**: SDK not found or import errors

**Solutions**:
- For SPM: Ensure package is added in Xcode > Package Dependencies
- For CocoaPods: Run `pod install` and open `.xcworkspace` (not `.xcodeproj`)
- Clean build folder: Product > Clean Build Folder
- Check deployment target is iOS 13.0+

## Important Notes

- **Replace placeholders**: Always replace `mi.yourcompany.com` with actual MovableInk subdomain
- **MIU requirements**: Must be non-PII, URL-friendly, match marketing team's identifier
- **API Key**: Required only for behavior events, NOT for deeplinking
- **Deeplinking works without API key**: Basic deeplinking functionality requires only Universal Links configuration
- **Region compliance**: Use `.eu` for European users if required for data compliance
- **Price format**: Always use string format for prices ("15.99", not 15.99)
- **Product IDs**: Must not be empty strings - will cause event to fail
- **Testing**: Use UUID `00000000-0000-0000-0000-000000000000` for testing with MovableInk team
- **Guest users**: Call `identifyUser()` after setting MIU for users who made actions while logged out

## Full Example App

Minimal working example:

```swift
import SwiftUI
import MovableInk

@main
struct ShoppingApp: App {
    init() {
        // Optional: Only if using behavior events
        MIClient.start(apiKey: "YOUR_API_KEY", region: .us)
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
                    guard let url = userActivity.webpageURL else { return }

                    MIClient.resolveURL(url) { result in
                        if case .success(let resolvedURL) = result {
                            // Navigate based on resolved URL
                            print("Opening: \(resolvedURL)")
                        }
                    }
                }
                .onAppear {
                    // Set MIU when user is identified
                    if let userId = UserSession.shared.userId {
                        MIClient.setMIU(userId)
                    }
                }
        }
    }
}

struct ProductDetailView: View {
    let product: Product

    var body: some View {
        VStack {
            Text(product.name)
        }
        .onAppear {
            // Capture product view
            MIClient.productViewed(
                properties: .init(
                    id: product.id,
                    title: product.name,
                    price: product.price,
                    currency: .USD,
                    url: URL(string: "https://yourapp.com/products/\(product.id)")
                )
            )
        }
    }
}
```

## Push Notification Analytics (Optional)

### Overview

Track when users open MovableInk push notifications for campaign attribution and conversion measurement. This feature is particularly useful for DaVinci customers.

### Prerequisites

This assumes you have already set up push notifications in your app using the UserNotifications framework or UIApplicationDelegate.

### Using UserNotifications Framework (Recommended)

Implement the delegate method to track notification opens:

```swift
import UserNotifications
import MovableInk

class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        // Track the notification open with MovableInk
        MIClient.userNotificationCenter(center, didReceive: response)

        // Your app's notification handling logic here

        completionHandler()
    }
}
```

### Using UIApplicationDelegate

If not using UserNotifications framework:

```swift
import MovableInk

func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) {
    MIClient.handlePushNotificationOpened(userInfo)

    // Your app's notification handling logic here
}
```

### Manual Tracking Without SDK

Extract the tracking URL from the notification payload and make a request:

```swift
import UserNotifications

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    guard let userInfo = response.notification.request.content.userInfo as? [String: Any] else {
        completionHandler()
        return
    }

    var urlString: String?

    // Check for "mi" container format
    if let miContainer = userInfo["mi"] as? [String: Any] {
        urlString = miContainer["url"] as? String
    }

    // Check for "data" container format
    if urlString == nil, let dataContainer = userInfo["data"] as? [String: Any] {
        urlString = dataContainer["mi_url"] as? String
    }

    // Make tracking request
    if let urlString = urlString, let url = URL(string: urlString) {
        Task {
            let _ = try await URLSession.shared.data(for: URLRequest(url: url))
        }
    }

    completionHandler()
}
```

### Payload Formats

MovableInk push notifications will have one of these payload structures:

**Standard Format:**
```json
{
  "aps": {
    "alert": {
      "title": "Special Offer Just for You!",
      "body": "Open to unlock exclusive savings."
    },
    "sound": "default"
  },
  "mi": {
    "url": "https://mi.example.com/p/push/abcd12345"
  }
}
```

**DaVinci Format:**
```json
{
  "aps": {
    "alert": {
      "title": "Special Offer Just for You!",
      "body": "Open to unlock exclusive savings."
    },
    "sound": "default"
  },
  "data": {
    "mi_url": "https://mi.example.com/deeplink/abc123",
    "mi_source": "davinci"
  }
}
```

## Additional Resources

- Full Documentation: https://sdk-mobile.movableink.com/
- GitHub Repository: https://github.com/movableink/ios-sdk
- Contact MovableInk team for: API keys, AASA configuration, MIU strategy, testing support