Example applications are a great way to explore the headless capabilities of Adobe Experience Manager (AEM). This iOS application demonstrates how to query content using AEM’s GraphQL APIs using persisted queries.
View the source code on GitHub
The following tools should be installed locally:
The iOS application works with the following AEM deployment options. All deployments requires the WKND Site v2.0.0+ to be installed.
The iOS application is designed to connect to an AEM Publish environment, however it can source content from AEM Author if authentication is provided in the iOS application’s configuration.
Clone the adobe/aem-guides-wknd-graphql
repository:
$ git clone git@github.com:adobe/aem-guides-wknd-graphql.git
Launch Xcode and open the folder ios-app
Modify the file Config.xcconfig
file and update AEM_SCHEME
and AEM_HOST
to match your target AEM Publish service.
// The http/https protocol scheme used to access the AEM_HOST
AEM_SCHEME = http
// Target hostname for AEM environment, do not include http:// or https://
AEM_HOST = localhost:4503
If connecting to AEM Author, add the AEM_AUTH_TYPE
and supporting authentication properties to the Config.xcconfig
.
Basic authentication
The AEM_USERNAME
and AEM_PASSWORD
authenticate a local AEM user with access to WKND GraphQL content.
AEM_AUTH_TYPE = basic
AEM_USERNAME = admin
AEM_PASSWORD = admin
Token authentication
The AEM_TOKEN
is an access token that authenticates to an AEM user with access to WKND GraphQL content.
AEM_AUTH_TYPE = token
AEM_TOKEN = abcd...0123
Build the application using Xcode and deploy the app to iOS simulator
A list of adventures from the WKND site should be displayed on the application. Selecting an adventure opens the adventure details. On the adventures list view, pull to refresh the data from AEM.
Below is a summary of how the iOS application is built, how it connects to AEM Headless to retrieve content using GraphQL persisted queries, and how that data is presented. The full code can be found on GitHub.
Following AEM Headless best practices, the iOS application uses AEM GraphQL persisted queries to query adventure data. The application uses two persisted queries:
wknd/adventures-all
persisted query, which returns all adventures in AEM with an abridged set of properties. This persisted query drives the initial view’s adventure list.# Retrieves a list of all adventures
{
adventureList {
items {
_path
slug
title
price
tripLength
primaryImage {
... on ImageRef {
_path
mimeType
width
height
}
}
}
}
}
wknd/adventure-by-slug
persisted query, which returns a single adventure by slug
(a custom property that uniquely identifies an adventure) with a complete set of properties. This persisted query powers the adventure detail views.# Retrieves an adventure Content Fragment based on it's slug
# Example query variables:
# {"slug": "bali-surf-camp"}
# Technically returns an adventure list but since the the slug
# property is set to be unique in the CF Model, only a single CF is expected
query($slug: String!) {
adventureList(filter: {
slug: {
_expressions: [ { value: $slug } ]
}
}) {
items {
_path
title
slug
activity
adventureType
price
tripLength
groupSize
difficulty
price
primaryImage {
... on ImageRef {
_path
mimeType
width
height
}
}
description {
json
plaintext
}
itinerary {
json
plaintext
}
}
_references {
...on AdventureModel {
_path
slug
title
price
__typename
}
}
}
}
AEM’s persisted queries are executed over HTTP GET and thus, common GraphQL libraries that use HTTP POST such as Apollo, cannot be used. Instead, create a custom class that executes the persisted query HTTP GET requests to AEM.
AEM/Aem.swift
instantiates the Aem
class used for all interactions with AEM Headless. The pattern is:
Each persisted query has a corresponding public func (ex. getAdventures(..)
or getAdventureBySlug(..)
) the iOS application’s views invoke to get adventure data.
The public func calls a private func makeRequest(..)
that invokes an asynchronous HTTP GET request to AEM Headless, and returns the JSON data.
Each public func then decodes the JSON data, and performs any required checks or transformations, before returning the Adventure data to the view.
AEM/Models.swift
, which map to the JSON objects returned my AEM Headless. /// # getAdventures(..)
/// Returns all WKND adventures using the `wknd-shared/adventures-all` persisted query.
/// For this func call to work, the `wknd-shared/adventures-all` query must be deployed to the AEM environment/service specified by the host.
///
/// Since HTTP requests are async, the completion syntax is used.
func getAdventures(completion: @escaping ([Adventure]) -> ()) {
// Create the HTTP request object representing the persisted query to get all adventures
let request = makeRequest(persistedQueryName: "wknd-shared/adventures-all")
// Wait fo the HTTP request to return
URLSession.shared.dataTask(with: request) { (data, response, error) in
// Error check as needed
if ((error) != nil) {
print("Unable to connect to AEM GraphQL endpoint")
completion([])
}
if (!data!.isEmpty) {
// Decode the JSON data into Swift objects
let adventures = try! JSONDecoder().decode(Adventures.self, from: data!)
DispatchQueue.main.async {
// Return the array of Adventure objects
completion(adventures.data.adventureList.items)
}
}
}.resume();
}
...
/// #makeRequest(..)
/// Generic method for constructing and executing AEM GraphQL persisted queries
private func makeRequest(persistedQueryName: String, params: [String: String] = [:]) -> URLRequest {
// Encode optional parameters as required by AEM
let persistedQueryParams = params.map { (param) -> String in
encode(string: ";\(param.key)=\(param.value)")
}.joined(separator: "")
// Construct the AEM GraphQL persisted query URL, including optional query params
let url: String = "\(self.scheme)://\(self.host)/graphql/execute.json/" + persistedQueryName + persistedQueryParams;
var request = URLRequest(url: URL(string: url)!);
// Add authentication to the AEM GraphQL persisted query requests as defined by the iOS application's configuration
request = addAuthHeaders(request: request)
return request
}
...
iOS prefers mapping JSON objects to typed data models.
The src/AEM/Models.swift
defines the decodable Swift structs and classes that map to the AEM JSON responses returned by AEM’s JSON responses.
SwiftUI is used for the various views in the application. Apple provides a getting started tutorial for building lists and navigation with SwiftUI.
WKNDAdventuresApp.swift
The entry of the application and includes AdventureListView
whose .onAppear
event handler is used to fetch all adventures data via aem.getAdventures()
. The shared aem
object is initialized here, and exposed to other views as an EnvironmentObject.
Views/AdventureListView.swift
Displays a list of adventures (based on the data from aem.getAdventures()
) and displays a list item for each adventure using the AdventureListItemView
.
Views/AdventureListItemView.swift
Displays each item in the adventures list (Views/AdventureListView.swift
).
Views/AdventureDetailView.swift
Displays the details of an adventure including the title, description, price, activity type, and primary image. This view queries AEM for full adventure details using aem.getAdventureBySlug(slug: slug)
, where the slug
parameter is passed in based on the select list row.
Images referenced by adventure Content Fragments, are served by AEM. This iOS app uses the path _path
field in the GraphQL response, and prefixes the AEM_SCHEME
and AEM_HOST
to create a fully qualified URL.
If connecting to protected resources on AEM that requires authorization, credentials must also be added to image requests.
SDWebImageSwiftUI and SDWebImage are used to load the remote images from AEM that populate the Adventure image on the AdventureListItemView
and AdventureDetailView
views.
The aem
class (in AEM/Aem.swift
) facilitates the use of AEM images in two ways:
aem.imageUrl(path: String)
is used in views to prepend the AEM’s scheme and host to the image’s path, creating fully qualified URL.
// adventure.image() => /content/dam/path/to/an/image.png
let imageUrl = aem.imageUrl(path: adventure.image())
// imageUrl => http://localhost:4503/content/dam/path/to/an/image.png
The convenience init(..)
in Aem
set HTTP Authorization headers on the image HTTP request, based on the iOS applications configuration.
/// AEM/Aem.swift
///
/// # Basic authentication init
/// Used when authenticating to AEM using local accounts (basic auth)
convenience init(scheme: String, host: String, username: String, password: String) {
...
// Add basic auth headers to all Image requests, as they are (likely) protected as well
SDWebImageDownloader.shared.setValue("Basic \(encodeBasicAuth(username: username, password: password))", forHTTPHeaderField: "Authorization")
}
/// AEM/Aem.swift
///
/// # Token authentication init
/// Used when authenticating to AEM using token authentication (Dev Token or access token generated from Service Credentials)
convenience init(scheme: String, host: String, token: String) {
...
// Add token auth headers to all Image requests, as they are (likely) protected as well
SDWebImageDownloader.shared.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
A similar approach can be used with the SwiftUI-native AsyncImage. AsyncImage
is supported on iOS 15.0+.