Swift/iOS Frontend Integration

This page describes a simple example Froomle frontend integration using Swift for iOS app integration. We’ll show how to send different types of events and how to request recommendations. This will be the bulk of the integration work, leaving only the synchronization of your item metadata, which should be handled on the backend for security reasons.

You can find a Swift Playground file containing all code on this page here.

Setup

When you sign up with Froomle you will be assigned your own subdomain and one or more environments (for example one for production and development). These determine the API url, which we can set once at startup:

let subdomain = "<subdomain>"
let environment = "<your-environment>"
let baseUrl = "https://\(subdomain).froomle.com/api/\(environment)/"

Each API call expects some contextual information, namely what type of page the user is currently on, how it is being served (desktop, app, ..) and who the user is. We assign it once at page startup and reuse it for all API calls:

struct Context {
    var channel: String
    var page_type: String
    var context_item: String
    var context_item_type: String
    var device_id: String
    var user_id: String
}

let context = Context(
    channel: "www-desktop",
    page_type: "article_detail",
    context_item: "item-id-123",
    context_item_type: "article",
    device_id: "device-id-123",
    user_id: "user-id-123"
)

The channel is how the page is being served. Should be set to www-desktop, www-mobile, mobile-app or test. The page_type identify a unique location where you’ve integrated one or more lists with Froomle recommendations that is distinctly different from other locations and in addition, this page-type is useful to differentiate on in our reporting. If the current page is a detail page for one of your items then include the item id as the context_item. If not, omit this field. The device_id is a unique identifier for the user that persists between sessions. It’s typically stored in the local storage on the device using e.g. CoreData or Realm. If the user does not consent to tracking a value of anonymous can be used here. The user_id is an identifier for logged in users that is stable across devices. If the user is not logged in the field can be omitted here.

Requesting recommendations

Below is an example on how to request one or more lists of recommendations from the Froomle recommendations API. If multiple lists of recommendations are placed on the same page please give them unique names. When you sign up with Froomle you can specify what type of recommendations you’d like to appear in each list.

Example:

struct RequestList: Codable {
    var list_name: String
    var list_size: Int
    var limit: Int
}

struct RecommendationRequest: Codable {
    var channel: String
    var device_id: String
    var page_type: String
    var lists: [RequestList]
}

struct Item: Codable {
    var rank: Int
    var item_id: String
    var uri: String
    var images: [String]
}

struct ResponseList: Codable {
    var list_name: String
    var list_size: Int
    var limit: Int
    var list_key: String
    var items: [Item]
}

struct Recommendations: Codable {
    var device_id: String
    var request_id: String
    var user_group: String
    var user_id: String
    var lists: [ResponseList]

}

let exampleRecommendationsRequest = RecommendationRequest(
    channel: context.channel,
    device_id: context.device_id,
    page_type: context.page_type,
    lists: [
        RequestList(list_name: "reco_list_001", list_size: 5, limit: 2)
    ]
)

// Send recommendations request
Task {
    guard let recommendationsUrl = URL(string: baseUrl + "recommendations/requests") else { fatalError("Missing URL for Recommendations") }
    let payload = try JSONEncoder().encode(exampleRecommendationsRequest)

    var recommendationRequest = URLRequest(url: recommendationsUrl)
    recommendationRequest.httpMethod = "POST"
    recommendationRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")

    let (data, response) = try await URLSession.shared.upload(for: recommendationRequest, from: payload)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { print("Error \((response as! HTTPURLResponse).statusCode) while fetching recommendations: \(String(data:data, encoding: .utf8) ?? ""))") }
    print("Successfully fetched recommendations")

    let recommendations = try JSONDecoder().decode(Recommendations.self, from: data)

    print("Example recommendations:", String(data: try JSONEncoder().encode(recommendations), encoding: .utf8) ?? "No recommendations...")
}

Rendering recommendations

To render the recommended items you can either fetch relevant display information such as title and thumbnail from your own backend, or ask Froomle to include this data in the recommendation response. To read the extra fields from the response remember to add them to the Swift class:

struct Item: Codable {
    var rank: Int
    var item_id: String
    var your_custom_field: String
    // ...
}

Sending events

Using our events API is very similar. In this case we chose to store the request body in a single Event data class that includes fields for multiple types of events, but you could split the class up by event type for stricter static typing if desired.

For an overview of different types of events and fields please refer to the events concept page. The news and e-commerce example pages have an overview of required events and how they are used in those settings.

Note that is important to use the data from the fetched recommendations when sending impression and click_on_recommendation events for viewed and clicked items.

Example:

struct Event: Codable {
    var event_type: String
    var page_type: String
    var device_id: String
    var user_id: String
    var action_item: String
    var action_item_type: String
    var channel: String
    var list_name: String? = nil
    var user_group: String? = nil
    var request_id: String? = nil
}

struct Events: Codable {
    var events: [Event]
}

// This is an example of recommendations that you have fetched earlier
let fetchedRecommendations = Recommendations(device_id: context.device_id, request_id: "6461", user_group: "froomle", user_id: context.user_id,
                                              lists: [
                                                ResponseList(list_name: "reco_list_001", list_size: 2, limit: 2, list_key: "reco_list_001",
                                                             items: [
                                                                Item(rank: 1, item_id: "item-id-123", uri: "https://link.com/123", images: ["https://via.placeholder.com/150"]),
                                                                Item(rank: 2, item_id: "item-id-321", uri: "https://link.com/321", images: ["https://via.placeholder.com/150"])
                                                             ])
                                              ]
)

let viewedItem = fetchedRecommendations.lists[0].items[0]
let clickedOnItem = fetchedRecommendations.lists[0].items[1]

let exampleEvents = Events(events: [
    Event(event_type: "detail_pageview", page_type: context.page_type, device_id: context.device_id, user_id: context.user_id, action_item: context.context_item, action_item_type: context.context_item_type, channel: context.channel),
    Event(event_type: "impression", page_type: context.page_type, device_id: context.device_id, user_id: context.user_id, action_item: viewedItem.item_id, action_item_type: viewedItem.item_type, channel: context.channel, list_name: fetchedRecommendations.lists[0].list_name, user_group: fetchedRecommendations.user_group, request_id: fetchedRecommendations.request_id),
    Event(event_type: "click_on_recommendation", page_type: context.page_type, device_id: context.device_id, user_id: context.user_id, action_item: clickedOnItem.item_id, action_item_type: clickedOnItem.item_type, channel: context.channel, list_name: fetchedRecommendations.lists[0].list_name, user_group: fetchedRecommendations.user_group, request_id: fetchedRecommendations.request_id),
])

Task {
    guard let eventsUrl = URL(string: baseUrl + "events") else { fatalError("Missing URL for Events") }
    let payload = try JSONEncoder().encode(exampleEvents)

    var eventsRequest = URLRequest(url: eventsUrl)
    eventsRequest.httpMethod = "POST"
    eventsRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")

    let (data, response) = try await URLSession.shared.upload(for: eventsRequest, from: payload)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { print("Error \((response as! HTTPURLResponse).statusCode) while sending events: \(String(data:data, encoding: .utf8) ?? ""))") }

    print("\nEvents are sent successfully!")
}

Impression events

To report on how well its recommendations are performing Froomle needs to know which of the recommendations the user has seen and which have been clicked. Ideally impressions are only sent when the recommendations become visible to the user. If they’re far down on the page or you’re using a carousel widget the user may need to scroll to actually see them. In UIKIt and SwiftUI you can use respectively the viewDidAppear() and onAppear() functions in the view of the recommendations.