Appearance
iOS Integration
Display Falcon Perks in your iOS app using WKWebView. No native SDK download required.
Requirements
- iOS 15.0+
- Xcode 15+
- A Falcon API key and placement ID
Quick Start
1. Create the WebView
Set up a WKWebView with the iosNativeListener message handler:
swift
import WebKit
class FalconPerksViewController: UIViewController, WKScriptMessageHandler {
private var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "iosNativeListener")
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
// Enable Safari Web Inspector for debugging (iOS 16.4+)
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
}
deinit {
// Break the retain cycle: WKUserContentController retains its message handlers
webView.configuration.userContentController
.removeScriptMessageHandler(forName: "iosNativeListener")
}
}2. Load the Falcon WebView URL
swift
func loadPerks(apiKey: String, placementId: String) {
let sessionId = UUID().uuidString
let urlString = "https://promo.falconlabs.us/ui/webview"
+ "?placement=\(placementId)"
+ "&apiKey=\(apiKey)"
+ "&sessionId=\(sessionId)"
if let url = URL(string: urlString) {
webView.load(URLRequest(url: url))
}
}3. Handle bridge messages
Implement WKScriptMessageHandler to receive events from the WebView:
swift
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard message.name == "iosNativeListener",
let body = message.body as? [String: Any],
let type = body["type"] as? String,
let name = body["name"] as? String else { return }
if type == "event" && name == "click" {
if let data = body["data"] as? [String: Any],
let clickUrl = data["clickUrl"] as? String,
let url = URL(string: clickUrl) {
// Open in SFSafariViewController or system browser
UIApplication.shared.open(url)
}
}
if type == "event" && name == "close" {
// User closed the perks view
dismiss(animated: true)
}
}Custom Attributes
Pass user and order attributes to improve offer targeting. Build the URL using URLComponents to safely encode all parameters:
swift
func loadPerks(apiKey: String, placementId: String, email: String?, firstName: String?, orderId: String?) {
let sessionId = UUID().uuidString
var components = URLComponents(string: "https://promo.falconlabs.us/ui/webview")!
components.queryItems = [
URLQueryItem(name: "placement", value: placementId),
URLQueryItem(name: "apiKey", value: apiKey),
URLQueryItem(name: "sessionId", value: sessionId),
]
if let email = email {
components.queryItems?.append(URLQueryItem(name: "at.email", value: email))
}
if let firstName = firstName {
components.queryItems?.append(URLQueryItem(name: "at.firstname", value: firstName))
}
if let orderId = orderId {
components.queryItems?.append(URLQueryItem(name: "at.orderId", value: orderId))
}
if let url = components.url {
webView.load(URLRequest(url: url))
}
}See the overview for the full list of supported at.* attributes.
Complete Example
Here is a complete, self-contained view controller with attribute passing and click handling via SFSafariViewController:
swift
import UIKit
import WebKit
import SafariServices
class FalconPerksViewController: UIViewController,
WKScriptMessageHandler, WKNavigationDelegate {
private var webView: WKWebView!
private let apiKey: String
private let placementId: String
private let email: String?
private let firstName: String?
private let orderId: String?
init(apiKey: String, placementId: String,
email: String? = nil, firstName: String? = nil, orderId: String? = nil) {
self.apiKey = apiKey
self.placementId = placementId
self.email = email
self.firstName = firstName
self.orderId = orderId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Configure WebView with bridge
let contentController = WKUserContentController()
contentController.add(self, name: "iosNativeListener")
let config = WKWebViewConfiguration()
config.userContentController = contentController
config.allowsInlineMediaPlayback = true
webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.navigationDelegate = self
view.addSubview(webView)
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
// Build URL with attributes
let sessionId = UUID().uuidString
var components = URLComponents(string: "https://promo.falconlabs.us/ui/webview")!
components.queryItems = [
URLQueryItem(name: "placement", value: placementId),
URLQueryItem(name: "apiKey", value: apiKey),
URLQueryItem(name: "sessionId", value: sessionId),
]
if let email = email {
components.queryItems?.append(URLQueryItem(name: "at.email", value: email))
}
if let firstName = firstName {
components.queryItems?.append(URLQueryItem(name: "at.firstname", value: firstName))
}
if let orderId = orderId {
components.queryItems?.append(URLQueryItem(name: "at.orderId", value: orderId))
}
if let url = components.url {
webView.load(URLRequest(url: url))
}
}
// MARK: - WKScriptMessageHandler
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard message.name == "iosNativeListener",
let body = message.body as? [String: Any],
let type = body["type"] as? String,
let name = body["name"] as? String,
type == "event" else { return }
let data = body["data"] as? [String: Any]
switch name {
case "click":
if let clickUrl = data?["clickUrl"] as? String,
let url = URL(string: clickUrl) {
// Open in SFSafariViewController (outside the WebView)
let safari = SFSafariViewController(url: url)
present(safari, animated: true)
}
case "close":
dismiss(animated: true)
default:
break
}
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!,
withError error: Error) {
print("WebView error: \(error.localizedDescription)")
}
deinit {
webView.configuration.userContentController
.removeScriptMessageHandler(forName: "iosNativeListener")
}
}Usage from your app:
swift
let perksVC = FalconPerksViewController(
apiKey: "YOUR_API_KEY",
placementId: "YOUR_PLACEMENT_ID",
email: "user@example.com",
firstName: "John",
orderId: "ORD-123"
)
present(perksVC, animated: true)Presentation Options
You can present the perks view controller however you like:
- Modal (full screen):
present(perksVC, animated: true) - Push:
navigationController?.pushViewController(perksVC, animated: true) - Sheet (iOS 15+): Set
perksVC.sheetPresentationControllerdetents - Child view controller: Add as a child VC in a portion of your screen
SwiftUI
Wrap the view controller in a UIViewControllerRepresentable:
swift
import SwiftUI
struct FalconPerksView: UIViewControllerRepresentable {
let apiKey: String
let placementId: String
func makeUIViewController(context: Context) -> FalconPerksViewController {
FalconPerksViewController(apiKey: apiKey, placementId: placementId)
}
func updateUIViewController(_ uiViewController: FalconPerksViewController,
context: Context) {}
}
// Usage in SwiftUI:
// FalconPerksView(apiKey: "YOUR_API_KEY", placementId: "YOUR_PLACEMENT_ID")Debugging
- Enable Safari Web Inspector: the code above sets
webView.isInspectable = trueon iOS 16.4+ - Open Safari on your Mac, go to Develop menu, select your simulator/device
- You can inspect the WebView DOM, console logs, and network requests
Info.plist (for local development)
If testing against a local development server over HTTP, add this to your Info.plist:
xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>