Build a Blog in Swift with Headless Oracle Content Management

Introduction

An iOS development environment utilizing Swift and SwiftUI can be a powerful tool for building applications that consume content from Oracle Content Management. Armed with the right content model, you can quickly build the user interface that makes up a typical blog app.

In this tutorial, we’ll build a simple iOS blog application in Swift by leveraging Oracle Content Management as a headless CMS as well as its software development kit (SDK) for content delivery. This iOS sample is available on GitHub.

The tutorial consists of three steps:

  1. Prepare Oracle Content Management
  2. Build the Blog Application in Xcode
  3. Run your application

Prerequisites

Before proceeding with this tutorial, we recommend that you read the following information first:

To follow this tutorial, you’ll need:

What We’re Building

Our blog application consists of three separate pages that allow visitors to explore blog articles organized into topics.

This animated image shows the navigation through a blog sample app, starting with topics, navigating to a list of articles and finally viewing a blog article in detail.

The first page, the home page, consists of branding (company name and logo), some links, and a list of blog topics.

The second page, the article listing page, shows previews of each blog article that belongs to the topic.

Lastly, the article page renders the final blog article, including information about the blog’s author.

To proceed, you’ll need to have an active subscription to Oracle Content Management and be logged in with the Content Administrator role.

Step 1: Prepare Oracle Content Management

If you don’t already have an Oracle Content Management instance, see the Quick Start to learn how to register for Oracle Cloud, provision an Oracle Content Management instance, and configure Oracle Content Management as a headless CMS.

For this tutorial, you’ll need to create a content model in either of two ways. There’s a downloadable asset pack available that will fill your empty repository with content types and associated content, or you can create your own content model and content.

To prepare Oracle Content Management:

  1. Create a channel and asset repository.
  2. Create a content model using either of two methods:

Create a Channel and Asset Repository

You first need to create a channel and an asset repository in Oracle Content Management so you can publish content.

To create a channel and an asset repository in Oracle Content Management:

  1. Log in to the Oracle Content Management web interface as an administrator.

  2. Choose Content in the left navigation menu and then choose Publishing Channels from the selection list in the page header.

    This image shows the Publishing Channels option selected in the dropdown menu in the Content page header.

  3. In the upper right corner, click Create to create a new channel. Name the channel ‘OCEGettingStartedChannel’ for the purpose of this tutorial, and keep the access public. Click Save to create the channel.

    This image shows the publishing channel definition panel, with ‘OCEGettingStartedChannel’ in the channel name field.

  4. Choose Content in the left navigation menu and then choose Repositories from the selection list in the page header.

    This image shows the Repositories option selected in the dropdown menu in the Content page header.

  5. In the upper right corner, click Create to create a new asset repository. Name the asset repository ‘OCEGettingStartedRepository’ for the purpose of this tutorial.

    This image shows the repository definition panel, with ‘OCEGettingStartedRepository’ in the repository name field.

  6. In the Publishing Channels field, select the OCEGettingStartedChannel channel to indicate to Oracle Content Management that content in the OCEGettingStartedRepository repository can be published to the OCEGettingStartedChannel channel. Click Save when you’re done.

    This image shows the repository definition panel, with ‘OCEGettingStartedChannel’ in the Publishing Channels field.

Create a Content Model

The next step is to create a content model. You can use either of two methods:

Import the Oracle Content Management Samples Asset Pack

You can download a preconfigured Oracle Content Management sample assets pack that contains all required content types and assets for this tutorial. If you prefer, you can also create your own content model rather than download the sample assets pack.

You can upload a copy of the content we’re using in this tutorial from the Oracle Content Management Samples Asset Pack. This will let you experiment with the content types and modify the content. If you want to import the Oracle Content Management Samples Asset Pack, download the asset pack archive, OCESamplesAssetPack.zip, and extract it to a directory of your choice:

  1. Download the Oracle Content Management Samples Asset Pack (OCESamplesAssetPack.zip) from the Oracle Content Management downloads page. Extract the downloaded zip file to a location on your computer. After extraction, this location will include a file called OCEGettingStarted_data.zip.

  2. Log in to the Oracle Content Management web interface as an administrator.

  3. Choose Content in the left navigation menu and then choose Repositories from the selection list in the page header. Now select OCEGettingStartedRepository and click the Import Content button in the top action bar.

    This image shows the Repositories page, with the OCEGettingStartedRepository item selected.

  4. Upload OCEGettingStarted_data.zip from your local computer to the Documents folder.

    This image shows the upload confirmation screen for the OCEGettingStarted_data.zip file.

  5. Once it’s uploaded, select OCEGettingStarted_data.zip and click OK to import the contents into your asset repository.

    This image shows the selected OCEGettingStarted_data.zip file with the OK button enabled.

  6. After the content has been imported successfully, navigate to the Assets page and open the OCEGettingStartedRepository repository. You’ll see that all the related images and content items have now been added to the asset repository.

    This image shows the OCEGettingStartedRepository repository, with all assets that were just imported.

  7. Click Select All on the top left and then Publish to add all the imported assets to the publishing channel that you created earlier, OCEGettingStartedChannel.

    This image shows the OCEGettingStartedRepository repository, with all assets selected and the Publish option in the action bar visible.

  8. Before publishing, you need to validate all the assets. First add OCEGettingStartedChannel as a selected channel, and then click the Validate button.

    This image shows the Validation Results page, with the OCEGettingStartedChannel channel added in the Channels field, all assets to be validated, and the Validate button enabled.

  9. After the assets have been validated, you can publish all the assets to the selected channel by clicking the Publish button in the top right corner.

    This image shows the Validation Results page, with the OCEGettingStartedChannel channel added in the Channels field, all assets validated, and the Publish button enabled.

Once that’s done, you can see on the Assets page that all assets have been published. (You can tell by the icon above the asset name.)

This image shows the Assets page, with all assets pubished.

After importing the Oracle Content Management Samples Asset Pack, you can start building the blog in Xcode.

Create Your Own Content Model

Instead of importing the Oracle Content Management Samples Asset Pack, you can also create your own content model.

For this tutorial, we’re using a content type called ‘OCEGettingStartedHomePage’ to build the home page for our blog. This home page consists of branding (company name and logo), some URLs for links, and a list of blog topics that should be included on the page.

This image shows the home page for the Cafe Supremo demo site.

To create content types for the content model:

  1. Log in to the Oracle Content Management web interface as an administrator.
  2. Choose Content in the left navigation menu and then choose Asset Types from the selection list in the page header.
  3. Click Create in the top right corner.
  4. Choose to create a content type (not a digital asset type). Repeat this for all required content types.

This image shows the Create Asset Type dialog in the Oracle Content Management web interface.

We’ll create four content types, each with its own set of fields:

The first content type, OCEGettingStartedHomePage, should have the following fields:

Display Name Field Type Required Machine Name
Company Name Single-value text field X company_name
Company Logo Single-value text field X company_logo
Topics Multiple-value reference field X topics
Contact URL Single-value text field X contact_url
About URL Single-value text field X about_url

This is what your OCEGettingStartedHomePage content type definition should look like:

This image shows the definition for the content type ‘OCEGettingStartedHomePage’. It includes these data fields: Company Name, Company Logo, Topics, Contact URL, and About URL.

The second content type, OCEGettingStartedTopic, should have the following field:

Display Name Field Type Required Machine Name
Thumbnail Single-value image field X thumbnail

This is what your OCEGettingStartedTopic content type should look like:

This image shows the definition for the content type ‘OCEGettingStartedTopic’. It includes this data field: Thumbnail.

The third content type, OCEGettingStartedAuthor, should have the following fields:

Display Name Field Type Required Machine Name
Avatar Single-value image field X avatar

This is what your OCEGettingStartedAuthor content type should look like:

This image shows the definition for the content type ‘OCEGettingStartedAuthor’. It includes this data field: Avatar.

The fourth and final content type, OCEGettingStartedArticle, should have the following fields:

Display Name Field Type Required Machine Name
Published Date Single-value date field X published_name
Author Single-value reference field X author
Image Single-value image field X image
Image Caption Single-value text field X image_caption
Article Content Single-value large-text field X article_content
Topic Single-value reference field X topic

This is what your OCEGettingStartedArticle content type should look like:

This image shows the definition for the content type ‘OCEGettingStartedArticlePage’. It includes these data fields: Published Date, Author, Image, Image Caption, Article Content, and Topic.

Once you’ve created your content types, you can add these content types to the repository that you created earlier, OCEGettingStartedRepository:

  1. Log in to the Oracle Content Management web interface as an administrator.
  2. Navigate to OCEGettingStartedRepository.
  3. Edit the repository and, under Asset Types, specify all four newly created content types. Click the Save button to save the changes.

This image shows the Edit Repository page in Oracle Content Management, with the four newly created content types associated with the OCEGettingStartedRepository repository.

After adding the content types to the repository, you can open the OCEGettingStartedRepository repository on the Assets page and start creating your content items for all the content types.

This image shows content items on the Assets page in the Oracle Content Management web interface, with options on the left for collections, channels, languages, types, content item selection, and status.

Step 2: Build the Blog Application in Xcode

To consume our Oracle Content Management content in an iOS application, we can use the iOS blog sample, which is available as an open-source repository on GitHub.

Note: Remember that using the iOS sample is optional, and we use it in this tutorial to get you started quickly. You can also build your own iOS application.

To build the blog in Swift:

  1. Clone the sample repository
  2. Configure the iOS application
  3. Use the Content SDK to Fetch Content

Clone the Sample Repository

The iOS blog sample is available as an open-source repository on GitHub.

You’ll first need to clone the sample from GitHub to your local computer:

git clone https://github.com/oracle-samples/oce-ios-blog-sample.git

Once you’ve cloned the sample, open the Xcode project file, BlogDemo.xcodeproj.

When you open the sample project in Xcode, it will automatically pull in the dependency for content-management-swift, the Swift package that implements the Oracle Content Delivery SDK.

There are no other third-party dependencies for this application, so no other manual installs are required. However, before running the application, there is some additional configuration that is required.

Configure the iOS Application

In this iOS blog sample, you need to configure a few pieces of information so that the Oracle Content Management Content SDK can target the correct instance URL with the correct channel token. These values are used each time your application requests data from your Oracle Content Management instance.

Open the file credentials.json and change both key-value pairs to reflect your instance URL and the channel token associated with your publishing channel. The channel for this tutorial is OCEGettingStartedChannel.

{
    "url": "https://samples.mycontentdemo.com",
    "channelToken": "47c9fb78774d4485bc7090bf7b955632"
}

Use the Content SDK to Fetch Content

Oracle Content Management offers a Swift package (content-management-swift) consisting of the OracleContentCore and OracleContentDelivery libraries to help discover and use content in your applications. The package is hosted on GitHub.

Learn more about the Content SDK for Swift in the Oracle Content Management documentation library:

You can leverage these Content SDK libraries to fetch content so that we can render it in our iOS application.

Onboard the Application

In order to request data, you need to provide some required information to your library, which is referred to as onboarding the application. You assign particular pieces of information so that requests are properly formed and point to the correct instance URL.

In this demo, when the application first starts, Onboarding.urlProvider is assigned to be an instance of your own class, MyURLProvider. This establishes the instance URL and channel token that are used by the content library for each request made.

While specifying a URL provider isn’t strictly necessary (as URL and token information may be specified on a per-request basis), assigning them reduces the amount of code required for each call site.

@main
struct BlogDemo: App {
    init() {
        // ... 

        // The sample code expects the URL and channel token to be provided by ``OracleContentCore.Onboarding``
        // Assign your ``OracleContentCore.URLProvider`` implementation to the ``OracleContentCore.Onboarding.urlProvider`` property
        Onboarding.urlProvider = MyURLProvider()
        
        // ...
        
    }
}

The MyURLProvider implementation reads data from credentials.json to obtain the URL and channel token.

{
    "url": "https://samples.mycontentdemo.com",
    "channelToken": "47c9fb78774d4485bc7090bf7b955632"
}

Each time the Oracle content library needs to build a request, it retrieves the following property:

/// This function provides the URL to be used for each OracleContentCore request
///
/// Services which implement ``OracleContentCore.ImplementsOverrides`` may provide a different URL and
/// authorization headers (if required) on a call-by-call basis
public var url: () -> URL? = {
  return URL(string: MyURLProvider.credentials.url)
}

Each time the library needs to build a request, it will also retrieve the delivery token:

/// This function provides the delivery channel token to be used for each OracleContentCore request
///
/// Services which implement ``OracleContentCore.ImplementsChannelToken`` may override this value
/// on a call-by-call basis
public var deliveryChannelToken: () -> String? = {
  
  return MyURLProvider.credentials.channelToken
}

Additionally, assigning to Onboarding.logger provides the opportunity to define your own logging implementation. For this demo, the MyLogger implementation consists of simply “printing” to the console. In production environments, your logger could utilize universal logging, core data, or any technology you choose.

@main
struct BlogDemo: App {
    init() {
        // ... 

         Onboarding.logger = MyLogger()
        
        // ...
        
    }
}

Request Data Using the Oracle Content Library

Note: All network request code for this demo can be found in BlogNetworking.swift.

We can now leverage the Content SDK to fetch content so that we can render it in the iOS application.

The BlogNetworking.swift file contains all the code to get data for the application. The model object associated with each page will call into various methods in order to retrieve Oracle Content Management data.

Home Page Data

The home page of our application is BlogDemoMain.swift, a SwiftUI file with an associated model object. The model object is reponsible for maintaining the state of the page and issuing the function calls necessary to start the data retrieval process. As data is received, the state of the page changes and SwiftUI will refresh the UI accordingly.

Initially, the main page requests two different pieces of data:

  1. First, we query for the content item representing the home page of the blog.
  2. Then we download the logo referenced by the content item.

Open BlogNetworking.swift and find the fetchHomePage() function, which performs the initial request.

/// Retrieve the content item which represents the home page of the blog
/// - returns: Asset
public func fetchHomePage() async throws -> Asset {
    let typeNode = QueryNode.equal(field: "type", value: "OCEGettingStartedHomePage")
    let nameNode = QueryNode.equal(field: "name", value: "HomePage")
    let q = QueryBuilder(node: typeNode).and(nameNode)
    
    let result = try await DeliveryAPI
        .listAssets()
        .query(q)
        .fields(.all)
        .limit(1)
        .fetchNextAsync()
        .items
        .first
       
    guard let foundResult = result else {
        throw BlogNetworkingError.homePageNotFound
    }
    
    return foundResult
}

This function uses Swift concurrency to request data. Note that all request objects (or services) are namespaced to DeliveryAPI. In this example, our request object is listAssets().

Following the request object are a series of builder components which allow us to fine-tune our request. Here we have specified some query details, asked to retrieve all fields, and have limited our response data to only a single object.

Our request to Oracle Content Management is submitted using the invocation verb fetchNextAsync().

Note: Since our request object starts with “list”, the invocation verb will start with “fetchNext”.

“List” requests will return data indicating how many records were returned, whether more data exists, and a collection of results (in the “items” property).

Our function grabs the first value from the “items” property and returns it. This is the asset representing our blog and it contains the logo ID, the company name, the about and contact URLs, and a list of topics.

With the logo ID available, we can issue our second request to download the logo:

/// Download the logo for display on the home page
/// - parameter logoId: The identifier of the logo to download
/// - returns: BlogImageState 
public func fetchLogo(logoId: String?) async throws -> BlogImageState {
    
    do {
        guard let logoID = logoId else { 
          throw BlogNetworkingError.missingLogoId 
        }
        
        let result = try await DeliveryAPI
            .downloadNative(identifier: logoID)
            .downloadAsync(progress: nil)
        
        guard let image = UIImage(contentsOfFile: result.result.path) else {
            throw OracleContentError.couldNotCreateImageFromURL(URL(string: result.result.path))
        }
        
        return .image(image)
        
    } catch {
        return .error(error)
    }

}

Our request and invocation are much simpler in this function. We create the request object using downloadNative(identifier: logoID) and submit it using the invocation verb downloadAsync.

When the object has been downloaded, we convert the data to a UIImage and return it as a BlogImageState enumeration with the image itself as the associated value.

Note: This sample uses the BlogImageState enum because it allows us to represent the various states of image without having to utilize optional values. This makes uptake in SwiftUI very easy.

Topic Data

When our model requested the home page data, it executed a call that looked like this:

self.home = try await self.networking.fetchHomePage()   

With the home page successfully retrieved, we need to find the collection of asset values that represent the topics in our blog. To do that, we ask for the field named “topics” whose type is an array of assets.

self.topics = try self.home.customField("topics") as [Asset]

BlogDemoMain.swift will iterate over this collection of topics and represent each as an element in a grid view. That UI representation is defined in Topic.swift and its associated model object, TopicModel.swift.

In order to display information about an individual topic, we must perform two tasks:

  1. We need to retrieve the detailed information about each topic.
  2. We must download the image referenced by each topic.

In TopicModel.swift, we call into the networking code to obtain the full asset:

let fullAsset = try await self.networking.readAsset(assetId: self.topic.identifier)

Obtaining detailed information about topic is a simple process. Construct a read request using the given identifier and then fetch the data.

/// Obtain detailed information about an Asset
/// - parameter assetId: The identifier of the asset to read
/// - returns: Asset
public func readAsset(assetId: String) async throws -> Asset {
    let result = try await DeliveryAPI
        .readAsset(assetId: assetId)
        .fetchAsync()
    
    return result
}

Note: All read requests return a single object and are submitted using an invocation verb that begins with “fetch”.

Once we have retrieved the detailed information about our topic, we need to extract the data that defines the thumbnail and then download it.

In TopicModel.swift, we obtain the thumbnail information by asking for the custom field that is named “thumbnail”. Because custom fields can be one of many different types, we need to explicitly specify that this field is an asset type. Once we have successfully found the asset, we can grab its identifier.

imageIdentifier = (try fullAsset.customField("thumbnail") as Asset).identifier

With the identifier available, TopicModel.swift can now ask the networking code to retrieve the medium rendition of the thumbnail image.

let imageState = await self.networking.downloadMediumRendition(identifier: imageIdentifier)
return imageState

The networking code to obtain the image creates a “downloadRendition” request object to retrieve a rendition named ‘Medium’ in jpg format. The invocation verb used to submit the request is “downloadAsync”.

When the object has been downloaded, we convert the data to a UIImage and return it as a BlogImageState enumeration with the image itself as the associated value.

/// Downloads the "Medium" rendition of an asset and returns the value as a `BlogImageState`
/// Note that any error while downloading the image will result in a placeholder image
public func downloadMediumRendition(identifier: String) async -> BlogImageState {
    
    do {
        let result = try await DeliveryAPI
                                .downloadRendition(identifier: identifier,
                                                   renditionName: "Medium",
                                                   format: "jpg")
                                .downloadAsync(progress: nil)
        
        guard let uiImage = UIImage(contentsOfFile: result.result.path()) else {
            throw OracleContentError.couldNotCreateImageFromURL(result.result)
        }
        
        return .image(uiImage)
        
    } catch {
        return .image(UIImage(systemName: "questionmark.circle")!)
    }

}

Now that we have both the detailed topic information and the thumbnail image, SwiftUI can construct the visual representation of a topic.

This image shows the UI representing a topic. It contains a title of ‘How To’, a thumbnail image of coffee cups with containg latte and a description defining the topic as learning to create beautiful latte art and pouring just the right cup.

Article Listing Data

Tapping on a topic navigates the user to a new page containing a listing of articles assigned to the selected topic.

Building the articles pages is very similar to the process used to build the topics listing. Given a topic identifier, we need to:

  1. Retrieve the listing of articles for that topic, and
  2. For each article, retrieve the thumbnail rendition of the asset contained in its “image” field.

When retrieving the list of assets, our request object is .listAssets().

Absent any other builder components, this would retrieve all published assets from the publishing channel specified in credentials.json. What we want, however, is to retrieve only those assets whose type is OCEGettingStartedArticle and whose topic property matches the topic identifier passed in.

/// Obtain the collection of articles for a given topic. Limited to a maximum of 50 articles for demo purposes.
/// - parameter topic: Asset
/// - returns: [Asset] representing the articles for the specified topic
public func fetchArticles(for topic: Asset) async throws -> [Asset] {

    let typeNode = QueryNode.equal(field: "type", value: "OCEGettingStartedArticle")
    let fieldsTopicNode = QueryNode.equal(field: "fields.topic", value: topic.identifier)
    let fullQuery = QueryBuilder(node: typeNode).and(fieldsTopicNode)
    
    let result = try await DeliveryAPI
        .listAssets()
        .query(fullQuery)
        .order(.field("fields.published_date", .desc))
        .limit(50)
        .fetchNextAsync()
    
    return result.items
}

For each of the articles retrieved, we need to determine what image to download. We get the identifier of the asset referenced in the “image” field and use that to submit a request to download its thumbnail.

let identifier = (try article.customField("image") as Asset).identifier
let image = await self.networking.downloadThumbnail(identifier: identifier, fileGroup: article.fileGroup)

The networking code to request the download looks like this:

/// Downloads the thumbnail rendition of an asset and returns the values as a ``BlogImageState``
/// Note that any error while downloading the image will result in a placeholder image
/// - parameter identifier: The identifier of the asset
/// - parameter fileGroup: The file group of the asset - used to differentiate thumbnails for digital assets, videos and "advanced videos"
public func downloadThumbnail(identifier: String, fileGroup: String) async -> BlogImageState {
    do {
        let result = try await DeliveryAPI
            .downloadThumbnail(identifier: identifier, fileGroup: fileGroup)
            .downloadAsync(progress: nil)
        
        guard let uiImage = UIImage(contentsOfFile: result.result.path()) else {
           throw OracleContentError.couldNotCreateImageFromURL(result.result)
        }
        
        return .image(uiImage)
    } catch {
        Onboarding.logError(error.localizedDescription)
        return .image(UIImage(systemName: "questionmark.circle")!)
    }
}

We can now display a preview of each article:

This image shows a preview of an article. It contains a title of ‘Create Beautiful Latte Art’, a formatted date indicating when the article was published, a thumbnail image of coffee cups with containg latte and a some descriptive text about the article - that ‘With a little practice, you can create designs with steamed milk.’

Article Data

Tapping on an article preview causes the final page of the sample application to display—that is, the article itself. As before, several data requests need to be performed:

  1. Given the identifier of the article selected, we need to retrieve its detailed information.
  2. Download the author’s avatar.
  3. Download the article image to display.
@MainActor
func fetchArticle() async throws {

    self.article = try await self.networking.readArticle(assetId: article.identifier)
    
    let author: Asset = try self.article.customField("author")
    let authorAvatar: Asset = try author.customField("avatar")

    Task {
        self.avatar = await self.networking.downloadNative(identifier: authorAvatar.identifier)
    }
    
    Task {
        let hero: Asset = try self.article.customField("image_16x9")
        self.heroImage = await self.networking.downloadNative(identifier: hero.identifier)
    }
}

Reading the detailed information for an asset is a straightforward process:

/// Obtain detailed information about an Asset
/// - parameter assetId: The identifier of the asset to read
/// - returns: Asset
public func readAsset(assetId: String) async throws -> Asset {
    let result = try await DeliveryAPI
        .readAsset(assetId: assetId)
        .fetchAsync()
    
    return result
}

Downloading the avatar and article images both follow the same general pattern that was used to obtain other images:

/// Downloads the native rendition of an asset and returns the values as a ``BlogImageState``
/// Note that any error while downloading the image will result in a placeholder image
public func downloadNative(identifier: String) async -> BlogImageState {
    do {
        let result = try await DeliveryAPI
            .downloadNative(identifier: identifier)
            .downloadAsync(progress: nil)
        
        guard let uiImage = UIImage(contentsOfFile: result.result.path()) else {
           throw OracleContentError.couldNotCreateImageFromURL(result.result)
        }
        
        return .image(uiImage)
    } catch {
        Onboarding.logError(error.localizedDescription)
        return .image(UIImage(systemName: "questionmark.circle")!)
    }
    
}

The result is a fully-formatted blog article:

This image shows a full blog article. It contains information about the author (including an avatar image), the image used to represent the article and the HTML formatted text of the article.

Step 3: Run Your Application

With your application complete, you can run it in the simulator or deploy it to any iPhone or iPad running iOS or iPadOS 16.0 or greater.

Conclusion

In this tutorial, we created an iOS blog application site, which can be found on GitHub. This application uses Oracle Content Management as a headless CMS. After setting up and configuring Oracle Content Management with a channel of published content for the blog tutorial, we ran the application to fetch the required content.

For more information on Swift, go to the Swift website.

Learn about important Oracle Content Management concepts in the documentation.

You can find more samples like this on the Oracle Content Management Samples page in the Oracle Help Center.