---
title: Swift – MomentPerks Prefetch Integration Guide
slug: swift-momentperks-prefetch-integration-guide
icon: {"faIcon":"fa-brands fa-app-store-ios"}
docTags: 
createdAt: 2025-07-26T11:13:22.159Z
---

# Overview

This guide explains how to integrate the MomentScience Moments solution into your iOS app using Swift and SwiftUI. The SDK offers two prefetching modes that help preload offers in advance, ensuring a fast, responsive checkout experience.

This guide covers two integration modes, both designed to preload and display offers in a WebView:

- SDK Prefetch Mode
- API Prefetch Mode

Both options enable a high-performing native experience while keeping the integration lightweight and flexible.

## Integration Modes

### SDK Prefetch Mode

This is the lightest-touch integration. A hidden 0×0 WKWebView is embedded before checkout (e.g., on the cart screen). It loads the Momentsand begins prefetching and caching offers in the background. Offers are later rendered instantly at checkout using this cached data.

### API Prefetch Mode

This approach gives your app full control over how and when offers are fetched. Your app sends a native HTTPS request to the Moments API before the checkout screen. The SDK is then initialized with the response payload at checkout.

**In both modes,&#x20;**&#x6F;ffers are rendered inside a fullscreen WebView at checkout. Because the SDK prefetches offers before rendering, the offer UI loads quickly and smooth. If no offers are found, you can simply skip showing the offer component.

:::hint{type="success"}
To see a full implementation, check out the [MomentScience iOS Demo on GitHub ](https://github.com/AdsPostX/examples/tree/main/ios-native/MSSDKDemoApp-iOS)which includes examples for offer fetching, event handling, and WebView integration.&#x20;
:::

***

## Requirements

To integrate the Moments solutioninto your iOS app, ensure the following prerequisites are met:

1. A vali&#x64;**&#x20;MomentScience SDK ID**, which you can [obtain by following these steps.](https://docs.momentscience.com/getting-your-sdk-id)
2. **Environment:**
   - **Swift version:** `5`
   - **UI Framework:** SwiftUI
   - **Minimum iOS Version:** `15`

***

# Integration Steps

## Step 1: Add Dependencies

### Add HTML Assets&#x20;

The Moments SDK renders offers inside a `WKWebView`using local HTML templates. These templates must be bundled with your app.

**Required Files:**

Add the following HTML files to your Xcode project:

- [`webpage_template.html`](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/webpage_template.html)
- [`prefetch_template.html`](https://prefetch_template.html)

You can place these files anywhere in your project. In the [MomentScience iOS demo app](https://github.com/AdsPostX/examples/tree/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp), they are located at the root of the Xcode project.

### Add and Install Dependencies&#x20;

No third-party libraries are required to use the Moments SDK. However, because offer experiences are rendered inside a WebView, you must import the **WebKit** framework:

```swift
import WebKit
```

This enables your app to use WKWebView for loading the offer templates and rendering personalized offers during checkout.

## Step 2:  Prefetch Offers (SDK or API)&#x20;

You can prefetch offers using one of two approaches:

- **SDK Prefetch**: Uses a hidden 0×0 WebView to silently fetch and cache offers before checkout.
- **API Prefetch**: Uses a native API call to fetch offers, which are then injected into the SDK display template.

In both modes, you may skip showing the offer screen if no offers are returned.

### Option 1: SDK Prefetch

This is the lightest integration path. It uses a hidden WebView to load the Moments SDK in prefetch mode. When offers are found, the SDK emits an `ads_found` event, which your app can use to determine whether to show the offer screen.

1. **Listen for SDK Events via JavaScript Handler:&#x20;**&#x55;se [`WKScriptMessageHandler`](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/Views/OffersView.swift)to listen for SDK events and capture results (e.g., whether offers were found).
   This callback is triggered when the SDK finishes prefetching. You can then decide whether to show the offers or skip it.

:::CodeblockTabs{indent="2"}
```swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    // Handle SDK messages such as "ads_found"
}
```
:::

2. **Load the HTML Template and Inject Data:&#x20;**&#x52;ead the`prefetch_template.html`by injecting values like `sdkId`, AdpxUser payload, and launcher script URL

:::CodeblockTabs{indent="2"}
```swift
func generatePrefetchHTML() throws -> String {
    guard let htmlPath = Bundle.main.path(forResource: "prefetch_template", ofType: "html") else {
        throw HTMLTemplateError.templateNotFound
    }

    var htmlContent = try String(contentsOfFile: htmlPath, encoding: .utf8)

    let escapedSdkId = sdkId.replacingOccurrences(of: "'", with: "\\'")
    htmlContent = htmlContent.replacingOccurrences(of: "{{SDK_ID}}", with: escapedSdkId)

    if let payload = userPayload, !payload.isEmpty {
        let payloadData = try JSONSerialization.data(withJSONObject: payload)
        let payloadJsonString = String(data: payloadData, encoding: .utf8) ?? "{}"
        let escapedPayload = payloadJsonString.replacingOccurrences(of: "'", with: "\\'")

        htmlContent = htmlContent.replacingOccurrences(
            of: "window.AdpxUser = {}",
            with: "window.AdpxUser = \(escapedPayload)"
        )
    }

    htmlContent = htmlContent.replacingOccurrences(
        of: "{{LAUNCHER_SCRIPT_URL}}",
        with: AppConfig.WebView.launcherScriptURL
    )

    return htmlContent
}

```
:::

:::hint{type="info"}
See [OfferView.swift ,](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/Views/OffersView.swift) [OffersViewModel.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/ViewModels/OffersViewModel.swift) and the [WKScriptMessageHandler ](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/Views/OffersView.swift)implementation in the demo app for a complete working example.
:::

### Option 2: Prefetch with API

In this method, your app fetches offers directly from the Moments API before checkout and injects the response into the SDK using JavaScript.

:::hint{type="success"}
For complete details on MomentPerks API, refer to the [MomentPerks API](docId\:zpOL4DX0Bohl2AOoyZrLQ) documentation.
:::

1. **Send a&#x20;**`POST`**request to the Moments API:**

:::CodeblockTabs{indent="2"}
Moments API Endpoint

```javascript
POST https://api.adspostx.com/native/v4/offers.json
```
:::

2. **Build the Request Parameters:&#x20;**&#x49;nclude the following:
   1. `api_key` as a query parameter
   2. A user payload in the request body
   3. Custom `User-Agent` in the request headers
3. **Validate Optional Parameters (if used):&#x20;**
   1. `loyaltyboost `must be "`0`", "`1`", or "`2`"
   2. `creative `must be "`0`" or "`1`"
4. **Prepare and Send the Request:** Use the following example code to construct and send the request:

:::CodeblockTabs{indent="2"}
fetchOffers

```swift
func fetchOffers(
    sdkId: String,
    isDevelopment: Bool = false,
    payload: [String: String]? = nil,
    loyaltyboost: String?,
    creative: String?,
    campaignId: String? = nil
) async throws -> [String: Any] {
    // Validate parameters
    if let loyaltyboost = loyaltyboost, !["0", "1", "2"].contains(loyaltyboost) {
        throw NetworkError.invalidParameter("loyaltyboost must be 0, 1, or 2")
    }
    if let creative = creative, !["0", "1"].contains(creative) {
        throw NetworkError.invalidParameter("creative must be 0 or 1")
    }

    // Build query params
    guard var urlComponents = URLComponents(string: baseURL) else {
        throw NetworkError.invalidURL
    }

    var queryItems = [URLQueryItem(name: "api_key", value: sdkId)]
    if let loyaltyboost = loyaltyboost {
        queryItems.append(URLQueryItem(name: "loyaltyboost", value: loyaltyboost))
    }
    if let creative = creative {
        queryItems.append(URLQueryItem(name: "creative", value: creative))
    }
    if let campaignId = campaignId {
        queryItems.append(URLQueryItem(name: "campaignId", value: campaignId))
    }
    urlComponents.queryItems = queryItems

    guard let url = urlComponents.url else {
        throw NetworkError.invalidURL
    }

    // Build request body
    var requestBody: [String: Any] = isDevelopment ? ["dev": "1"] : [:]
    payload?.forEach { key, value in requestBody[key] = value }

    // Create request
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    let userAgent = payload?["ua"] ?? UserAgentService.shared.userAgent
    request.addValue(userAgent, forHTTPHeaderField: "User-Agent")
    request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)

    // Perform request
    let (data, response) = try await URLSession.shared.data(for: request)
    guard let httpResponse = response as? HTTPURLResponse else {
        throw NetworkError.serverError("Invalid server response")
    }

    switch httpResponse.statusCode {
    case 200...299:
        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw NetworkError.decodingError("Invalid JSON format")
        }
        return json
    default:
        throw NetworkError.serverError("Unexpected error: \(httpResponse.statusCode)")
    }
}

```

fetchOffers usage

```swift
        do {
            let response = try await fetchOffers(
                sdkId: sdkId,
                isDevelopment: true,
                payload: userPayload,
                loyaltyboost: "0",
                creative: "0"
            )
        } catch {
            // Handle any errors from the API call
        }

```

payload

```swift
@Published var userPayload: [String: String]? = [
    "ua": "<user_agent_value>", // User-Agent string. Use system default or provide a custom one if needed.
    "placement": "checkout", //Page/section identifier where the offer was triggered.
    "pub_user_id": "<unique_value>", //A unique, non-PII identifier for the end user.
    "themeId": "demo", // pass valid themeId value here
    "adpx_fp": "<unique_value>"//Unique user identifier.
]
```
:::

5. **Save the API Response Locally:** Store the JSON response from the API to use it when initializing the SDK

:::hint{type="info"}
See [NetworkService.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/Services/NetworkService.swift) and [UserAgentService.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/Services/UserAgentService.swift) implementation in the demo app for a complete working example.
:::

***

## Step 3: Show Offers in Fullscreen WebView

Once you’ve prefetched offers using the Moments API or SDK, display them in a fullscreen WebView on your checkout screen.

1. **Navigate to the Offer WebView:** Use `NavigationLink` (or your app’s navigation method) to transition to a view that renders the offer experience. You’ll need to pass relevant properties such as the API response, load mode, and user payload.

:::CodeblockTabs{indent="2"}
```javascript
NavigationLink(
    destination: WebPageView(
        sdkId: viewModel.sdkId,
        momentsAPIResponse: viewModel.checkoutAPIResponse,
        loadMode: viewModel.prefetching ?? .prefetchAPI, // you may not need this parameter as you are going to use only one of the approach.
        offerCount: viewModel.offersCount, // Optional, require only if you want to show no of offers in UI.
        userPayload: viewModel.userPayload
    )
)
```
:::

2. **Render Offers in WKWebView:&#x20;**&#x49;nside `WebPageView`, use a fullscreen `WKWebView` to render HTML from a template. You'll need to:
   1. Load `webpage_template.html` from the app bundle.
   2. Replace placeholders (e.g., `{{SDK_ID}}`, `{{AUTO_CONFIG}}`) with actual values.
   3. Inject the modified HTML into the WebView.
3. **Load and Prepare HTML:&#x20;**&#x48;ere’s a simplified example of how the HTML template is loaded and populated:

```swift
private func loadHTMLTemplate() throws -> String {
    guard let htmlPath = Bundle.main.path(forResource: "webpage_template", ofType: "html") else {
        throw HTMLTemplateError.templateNotFound
    }
    
    do {
        return try String(contentsOfFile: htmlPath, encoding: .utf8)
    } catch {
        print("Error loading HTML template: \(error)")
        throw error
    }
}
```

```swift
private var htmlContent: String {
    let htmlTemplate: String
    do {
        htmlTemplate = try loadHTMLTemplate()
    } catch {
        return "<html><body><h1>Error</h1><p>\(error.localizedDescription)</p></body></html>"
    }

    // Serialize the Moments API response
    let responseJson = (try? JSONSerialization.data(withJSONObject: momentsAPIResponse ?? [:]))
        .flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
    let escapedResponse = WebPageViewModel.escapeForJSString(responseJson)

    // Serialize user payload
    let payloadJson = (try? JSONSerialization.data(withJSONObject: userPayload ?? [:]))
        .flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
    let escapedPayload = WebPageViewModel.escapeForJSString(payloadJson)

    // Determine WebView configuration based on prefetch method
    let (autoConfig, responseHandling, callSetResponse): (String, String, String) = {
        switch loadMode {
        case .prefetchAPI:
            return (
                "autoShow: true,\n              autoLoad: false",
                """
                try {
                    const responseData = JSON.parse('\(escapedResponse)');
                    if (responseData && typeof responseData === 'object') {
                        setTimeout(() => window.Adpx.setApiResponse(responseData), 100);
                    }
                } catch (error) {
                    console.error('Error parsing response data:', error.message);
                }
                """,
                "await setResponse();"
            )
        case .prefetchWebSDK:
            return (
                "autoShow: true,\n              autoLoad: true,\n              prefetch: true",
                "// WebSDK prefetch mode - no additional response handling needed",
                "// No need to call setResponse"
            )
        }
    }()

    let offerCountStr = String(offerCount ?? 0)
    let escapedSdkId = WebPageViewModel.escapeForJSString(sdkId)

    return htmlTemplate
        .replacingOccurrences(of: "{{SDK_ID}}", with: escapedSdkId)
        .replacingOccurrences(of: "{{LAUNCHER_SCRIPT_URL}}", with: AppConfig.WebView.launcherScriptURL)
        .replacingOccurrences(of: "{{OFFERS_COUNT}}", with: offerCountStr)
        .replacingOccurrences(of: "{{AUTO_CONFIG}}", with: autoConfig)
        .replacingOccurrences(of: "{{RESPONSE_HANDLING}}", with: responseHandling)
        .replacingOccurrences(of: "{{CALL_SET_RESPONSE}}", with: callSetResponse)
        .replacingOccurrences(of: "window.AdpxUser = {}", with: "window.AdpxUser = \(escapedPayload)")
}
```

:::hint{type="info"}
See [WebPageViewModel.swift ](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/ViewModels/WebPageViewModel.swift)and [webpage\_template.html](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/webpage_template.html) in the demo app for a complete example.
:::

***

## Step 4: Open Offer Links in an External Browser

To ensure a smooth user experience, all offer links (such as CTA clicks) should open in the user’s default browser, not inside the embedded WKWebView. This behavior requires customizing navigation handling using `WKNavigationDelegate`, `WKUIDelegate`, and `WKScriptMessageHandler`.&#x20;

1. **Intercept Navigation Requests:** Implement `WKNavigationDelegate` to inspect and control URL navigation events

:::CodeblockTabs{indent="2"}
```javascript
func webView(_ webView: WKWebView,
             decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let url = navigationAction.request.url {
        decisionHandler(viewModel.shouldOpenURL(url) ? .allow : .cancel)
    } else {
        decisionHandler(.allow)
    }
}
```
:::

2. **Handle New Window Requests:&#x20;**&#x55;se `WKUIDelegate` to intercept any links that attempt to open in a new window (e.g., using `target="_blank"`). Instead of opening in the WebView, redirect them externally:

:::CodeblockTabs{indent="2"}
```swift
func webView(_ webView: WKWebView,
             createWebViewWith configuration: WKWebViewConfiguration,
             for navigationAction: WKNavigationAction,
             windowFeatures: WKWindowFeatures) -> WKWebView? {
    if let url = navigationAction.request.url,
       viewModel.shouldOpenURL(url) {
        viewModel.openExternal(url: url)
    }
    return nil
}
```

openExternal

```swift
func openExternal(url: URL) {
  UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
```
:::

3. **Handle JavaScript Events (**`postMessage`**):** Some offer interactions are dispatched from the SDK using `window.webkit.messageHandlers.adpxCallback.postMessage(...)`. To respond to these events, implement `WKScriptMessageHandler`

:::CodeblockTabs{indent="2"}
WKScriptMessageHandler

```swift
func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
) {
    if message.name == "adpxCallback",
       let messageDict = message.body as? [String: Any],
       let event = messageDict["event"] as? String,
       let payload = messageDict["payload"] as? [String: Any] {
        viewModel.handleMessage(event: event, payload: payload)
    }
}
```
:::

4. Inside `handleMessage`, inspect for `url_clicked` event type and open the `target_url`using:

:::CodeblockTabs{indent="2"}
```javascript
if event == "url_clicked",
   let urlString = payload["target_url"] as? String,
   let url = URL(string: urlString) {
    UIApplication.shared.open(url)
}

```
:::

:::hint{type="info"}
See [WebPageViewModel.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/ViewModels/WebPageViewModel.swift) and [WebPageView.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MSSDKDemoApp-iOS/MSSDKDemoApp/Views/WebPageView.swift) in the demo app for implementation.
:::

***

# Conclusion

Congratulations! You've successfully integrated the Moments solution into your iOS app using Swift and SwiftUI.

Whether you’re using SDK Prefetch Mode or API Prefetch Mode, your app is now equipped to deliver personalized offers directly within the user experience. This setup enables dynamic monetization while preserving control over when and how offers are displayed.

***

📢 If you're running into any issues while going through the integration process, feel free to contact us at [help@momentscience.com](mailto\:help@Momentscience.com)
