iOS β Moments Prefetch Integration Guide
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, offers 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.
To see a full implementation, check out the MomentScience iOS Demo on GitHub which includes examples for offer fetching, event handling, and WebView integration.
Requirements
To integrate the Moments solutioninto your iOS app, ensure the following prerequisites are met:
- A valid MomentScience SDK ID, which you can obtain by following these steps.ο»Ώ
- Environment:
- Swift version: 5
- UI Framework: SwiftUI
- Minimum iOS Version: 15
Integration Steps
Step 1: Add Dependencies
Add HTML Assets
The Moments SDK renders offers inside a WKWebViewusing 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ο»Ώ
- ο»Ώprefetch_template.htmlο»Ώ
You can place these files anywhere in your project. In the MomentScience iOS demo app, they are located at the root of the Xcode project.
Add and Install Dependencies
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:
import WebKitThis enables your app to use WKWebView for loading the offer templates and rendering personalized offers during checkout.
Step 2: Prefetch Offers (SDK or API)
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.
- Listen for SDK Events via JavaScript Handler: Use WKScriptMessageHandlerto 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.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Handle SDK messages such as "ads_found"
}- Load the HTML Template and Inject Data: Read theprefetch_template.htmlby injecting values like sdkId, AdpxUser payload, and launcher script URL
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
}
See OfferView.swift , OffersViewModel.swift and the WKScriptMessageHandler 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.
For complete details on Moments API, refer to the Moments API documentation.
- Send a POSTrequest to the Moments API:
POST https://api.adspostx.com/native/v4/offers.json- Build the Request Parameters: Include the following:
- api_key as a query parameter
- A user payload in the request body
- Custom User-Agent in the request headers
- Validate Optional Parameters (if used):
- loyaltyboost must be "0", "1", or "2"
- creative must be "0" or "1"
- Prepare and Send the Request: Use the following example code to construct and send the request:
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)")
}
}
- Save the API Response Locally: Store the JSON response from the API to use it when initializing the SDK
See NetworkService.swift and 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.
- 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.
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
)
)- Render Offers in WKWebView: Inside WebPageView, use a fullscreen WKWebView to render HTML from a template. You'll need to:
- Load webpage_template.html from the app bundle.
- Replace placeholders (e.g., {{SDK_ID}}, {{AUTO_CONFIG}}) with actual values.
- Inject the modified HTML into the WebView.
- Load and Prepare HTML: Hereβs a simplified example of how the HTML template is loaded and populated:
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
}
}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)")
}See WebPageViewModel.swift and 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.
- Intercept Navigation Requests: Implement WKNavigationDelegate to inspect and control URL navigation events
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)
}
}- Handle New Window Requests: Use 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:
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
}- 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
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)
}
}- Inside handleMessage, inspect for url_clicked event type and open the target_urlusing:
if event == "url_clicked",
let urlString = payload["target_url"] as? String,
let url = URL(string: urlString) {
UIApplication.shared.open(url)
}
See WebPageViewModel.swift and 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ο»Ώ