adjoe Engineers’ Blog
 /  iOS  /  Consent Management Platform
green decorative image with snippets of code across it
iOS

Building a Consent Management Platform on iOS

Our adjoe WAVE team builds an SDK that helps publishers to monetize their apps by displaying ads. 

WAVE prioritizes compliance with essential regulatory and data privacy frameworks, including the IAB Europe Transparency and Consent Framework (TCF). We recently integrated a consent management platform (CMP) solution into our WAVE SDK, which enhances the transparency, experience, and privacy for publishers, advertisers, and end users. 

In this article, I will address the main challenges we encountered while building our own CMP. I’ll cover two topics: the CMP UI and CMP API. Together, these will give you a general overview of what it takes to implement a CMP on iOS.

What Is a Consent Management Platform?

A CMP is software that develops notices to inform users and capture their preferences regarding how their personal data is processed. This transparency allows for a more positive and sustainable experience in the apps they interact with. CMP has become a key part of IAB Europe’s TCF. 

The primary goal of a TCF is to ensure compliance with data protection regulations, such as the General Data Protection Regulation (GDPR) in the European Union, by providing users with transparency and control over their personal data.

The main responsibilities of a CMP are to

  1. Handle user permission requests: a CMP provides a user interface to establish transparency for users and obtain consent or register objections from end users. A CMP presents users with clear and concise information about the types of data the app intends to access or process.
  2. Share user preferences: A CMP provides a standardized means for parties, such as publishers or advertisers, to access these permissions via a CMP API.

Building the CMP UI

Our CMP UI consists of three main parts:

  1. Layer 1: This is the first screen with which the user interacts when the host app asks the SDK to present a consent screen. It gives a user an overview of what data needs to be used and for which reasons. There are two main ways to go from here: The user either agrees to everything or they can dive deeper into the details by tapping “Change choices.”
  2. Layer 2 (ad preferences): Here, users can adjust their preferences more precisely. They can give their consent for specific purposes of data usage and reject it from being used for other purposes.
  3. Layer 2 (partners): This screen gives users an opportunity to adjust their preferences individually per advertising vendor.
screenshots of consent management platform CMP

Becoming TCF-compliant is a rigorous process that ensures every aspect, from UI to CMP implementation, is achieved to the highest standard by data privacy vendors. It has many requirements regarding the CTA button text and styles, font contrast rate, and, mainly, the transparency of the information that you need to disclose to users. This allows the IAB to ensure the highest level of quality for users interacting with the platform and guarantees data security for those who have successfully implemented the measures.

We’ve chosen SwiftUI framework to build our CMP UI and be able to change our UI fast and flexibly. It’s a relatively new UI framework developed by Apple, and its main feature is that it uses declarative syntax.

The main benefits we gained from SwiftUI are as follows:

1. CMP screens are mainly collection views 

With the old UI framework, UIKit, it takes tons of boilerplate code (mainly because of imperative code and delegate pattern) to implement complex collection views that display heterogeneous data.

 With SwiftUI, building a collection view is as simple as iterating over an array and mapping each element to a cell view instance. Here’s a comparison between UIKit and SwiftUI implementation for a simple vendor list:

// UIKit implemenation
​
class VendorListTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
   let vendors = ["Vendor 1", "Vendor 2", "Vendor 3"]
   let tableView = UITableView()
   override func viewDidLoad() {
       super.viewDidLoad()
       tableView.dataSource = self
       tableView.delegate = self
       tableView.register(UITableViewCell.self, forCellReuseIdentifier: "VendorCell")
       view.addSubview(tableView)
       tableView.translatesAutoresizingMaskIntoConstraints = false
       NSLayoutConstraint.activate([
           tableView.topAnchor.constraint(equalTo: view.topAnchor),
           tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
           tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
           tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
       ])
   }
   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       return vendors.count
   }
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let cell = tableView.dequeueReusableCell(withIdentifier: "VendorCell", for: indexPath)
       cell.textLabel?.text = vendors[indexPath.row]
       return cell
   }
}
​
​
// SwiftUI implementation
import SwiftUI
struct VendorListView: View {
   let vendors = ["Vendor 1", "Vendor 2", "Vendor 3"]
   var body: some View {
       NavigationView {
           List(vendors, id: \.self) { item in
               Text(item)
           }
       }
   }

2. Toggle is a key control on these screens 

It’s the main control for users to set up their preferences. There are also toggles that are automatically disabled when another toggle is disabled, such as “Enable all.”

Screenshot of enable all toggle for CMP solution

SwiftUI views reflect the state of your UI model. To code this “Enable all” functionality, all we need to do is to create a Binding<Bool> structure. This is where the getter reads if all other toggles are enabled in the model, and the setter sets all model toggles to a new value. 

Here’s a code example of this:

Section(
   content: {
       let allEnabledToggleBinding = Binding<Bool>(get: {
           return model.isAllEnabled()
       }, set: { newValue in
           model.setEnableAll(newValue)
       })
       Toggle("Enable All", isOn: allEnabledToggleBinding)
   }
)

You can see how easy it’s to manage this logic with the power of reactive UI and bindings in SwiftUI.

3. Details inside expandable collection cells are hidden

We hide a lot of details inside expandable list rows to make our CMP screens look neat. Users still see the most important info on the top level of the screen. And if they need to go deeper, we let them expand each row. 

SwiftUI has an out-of-the-box expandable list row feature:

Screenshot showing expandable row feature in SwiftUI

In code, all we need to do to make the row expandable is to wrap the cell content into a DisclosureGroup view. 

Let’s look at the purpose rows example. This is where we attach detailed info to one of the purpose toggles:

ForEach($model.purposesList) { $purpose in
   DisclosureGroup(
       content: {
           Text(purpose.detailedInfo)
       }, label: {
           Toggle($purpose.name, isOn: $purpose.enabled)
       }
   )
}

SwiftUI lets us add this complex interactive functionality with just a few lines of codes. 

This MVP laid the foundation for the CMP later used in production. The declarative nature of SwiftUI also lets us adjust our UI really quickly for TCF compliance needs as we continued to develop the software.

One of the main challenges of using SwiftUI, however, is that it’s quite new and could still have some bugs and cause crashes – especially, on old platforms, like iOS 14. But after releasing the CMP feature, we found SwiftUI to be stable and have not encountered any bugs or crashes from it.

Implementing an CMP API

Building the UI part is just a half of the story. 

We then needed to collect user preferences, convert them to standardized format — TC String – and share it with the hosting app or advertisement services (vendors). 

A TC String’s primary purpose is to encapsulate and encode all the information disclosed to a user. According to TCF guidelines, the host app should provide users with an opportunity to change these choices. 

Every time a user is about to see the consent screen again, we need to take previously generated TC String and build our UI based on its value. This means that we need to have support for both encoding and decoding in our SDK.

screenshot showing the coding format for TC String

The coding format for TC String is well described in the IAB TCF docs. TC String is a base64 encoded list of bits; most of the time each bit tells us if the user has given us permission to use their data for a specific reason or vendor.

There is currently no open-source library available for generating TC String V2 in Swift. However, among the forks of the official repo, we found one that supports the encoding and decoding of TC String v1. We used this as a basis to create our own fork with support for TCF 2.2. We are currently working on opening a merge request on GitHub to merge our changes back to IAB’s repository.

The differences between TC String v1 and v2 are:

  • v2 has new fields with static size: TcfPolicyVersion, IsServiceSpecific, UseNonStandardTexts, PurposeOneTreatment, PublisherCC
  • v2 has new dynamic ranges of values: SpecialFeatureOptIns, PurposesLITransparency, Vendor Legitimate Interest Section

We added those new fields to the TC String coding algorithms of our fork. The public API provides you with two main classes: ConsentStringBuilderV2 and ConsentStringV2.

ConsentStringBuilderV2, as the name implies, is used for building TCString from plain values.

let tcString = try ConsentStringBuilderV2().build(
   cmpId: 0,
   cmpVersion: 0,
   consentScreenId: 0,
   consentLanguage: "EN",
   vendorListVersion: 153,
   tcfPolicyVersion: 4,
   useNonStandardTexts: 0,
   specialFeatureOptIns: [],
   purposeConsents: [1, 2, 3, 4],
   purposeLegitimateInterests: [2, 3],
   purposeOneTreatment: 1,
   publisherCC: "DE",
   allowedVendorIds: [1, 2, 3, 4],
   legitimateInterestsForVendorIds: [1, 2]
)
// CAAB9ZAAAB9ZALZAAAENCZCgAPAAAGAAAAYgACNAAJgAAA
print(tcString)

ConsentStringV2 is a class for reading plain values from TC String. In the example below, we use the same TC String value we generated:

let consentString = try ConsentStringV2(consentString: tcString)
       // 0
       print(consentString.cmpId)
       // 1970-01-01 00:00:00 +0000
       print(consentString.dateCreated)
       // 1970-01-01 00:00:00 +0000
       print(consentString.dateUpdated)
       // 50
       print(consentString.consentScreen)
       // EN
       print(consentString.consentLanguage)
       // 153
       print(consentString.vendorListVersion)
       // 0
       print(consentString.tfcPolicyVersion)
       // 1
       print(consentString.isServiceSpecific)
       // 0
       print(consentString.useNonStandardTexts)
       // []
       print(consentString.specialFeatureOptIns)
       // [4, 1 ,2 ,3]
       print(consentString.purposeConsents)
       // [3, 2]
       print(consentString.purposeLegitimateInterests)
       // 1
       print(consentString.purposeOneTreatment)
       // EN
       print(consentString.publisherCC)
       // 667
       print(consentString.maxVendorIdForConsents)
       // true
       print(consentString.isVendorAllowed(vendorId: 1))
       // true
       print(consentString.isVendorAllowed(vendorId: 2))
       // true
       print(consentString.isVendorAllowed(vendorId: 3))
       // false
       print(consentString.isVendorAllowed(vendorId: 4))
       // false
       print(consentString.isVendorAllowed(vendorId: 5))
       // false   
       print(consentString.isLegitimateInterestForVendorAllowed(vendorId: 1))
       // true
       print(consentString.isLegitimateInterestForVendorAllowed(vendorId: 2))
       // true
       print(consentString.isLegitimateInterestForVendorAllowed(vendorId: 3))

If you are looking for a TC String v2 solution, feel free to try out our TC String V2 coding implementation

The base64-encoded list of bits is not something that you can easily verify and test. Fortunately, the IAB provides developers with a comprehensive tool that helps with the encoding and decoding of TC String value.

screenshot of using TC String decoding tool

Once TC String was generated, we needed to notify the host application or another ad SDK integrated within the app. This was necessary for the CMP API we had to implement. 

An iOS way to support the CMP API is to set the UserDefaults variables. With this approach, third parties are able to read TC String and other values from UserDefaults directly. Or they can even subscribe to be notified of UserDefaults changes as soon as the user has selected their choices on the CMP consent screens. 

Besides saving TC String to UserDefaults, we also had to save some other information about our CMP, as well as raw “1” and “0” strings that represent user consent. You can find the whole list of required values here

Here’s a code snippet:

UserDefaults.standard.setValue(0, forKey: "IABTCF_CmpSdkID")
UserDefaults.standard.setValue(0, forKey: "IABTCF_CmpSdkVersion")
UserDefaults.standard.setValue05, forKey: "IABTCF_PolicyVersion")
UserDefaults.standard.setValue(1, forKey: "IABTCF_gdprApplies")
UserDefaults.standard.setValue("CAAB9ZAAAB9ZALZAAAENCZCgAPAAAGAAAAYgACNAAJgAAA", forKey: "IABTCF_TCString")
UserDefaults.standard.setValue("DE", forKey: "IABTCF_PublisherCC")
UserDefaults.standard.setValue(0, forKey: "IABTCF_PurposeOneTreatment")
UserDefaults.standard.setValue(0, forKey: "IABTCF_UseNonStandardTexts")
UserDefaults.standard.setValue("1101", forKey: "IABTCF_VendorConsents")
UserDefaults.standard.setValue("011", forKey: "IABTCF_VendorLegitimateInterests")
UserDefaults.standard.setValue("111111111110000000000000", forKey: "IABTCF_PurposeConsents")
UserDefaults.standard.setValue("010000111100000000000000", forKey: "IABTCF_PurposeLegitimateInterests")
UserDefaults.standard.setValue("110000000000", forKey: "IABTCF_SpecialFeaturesOptIns")

After all these steps, the only thing left for us to do was to send TC String information to the WAVE mediation platform when our SDK makes an ad request. This way, the vendors can decode TC String value and see if they are allowed to use user data. 

The overall process looks like this:

diagram showing TC String generation flow

Lessons Learned about Implementing Our CMP Solution

The documentation provided by the IAB means that implementing a CMP API is straightforward. These resources offer clear guidelines, making it simple for developers to integrate and manage user consent within their digital platforms. 

Implementing a CMP solution presents a challenge; however, it ensures you have the flexibility to meet user needs while maintaining the highest standards of user data privacy. 

And while SwiftUI is relatively new and may potentially have bugs, it’s ready for production. Our SwiftUI CMP screens have been deployed across millions of devices and ensure a smooth crash-free user experience. Introducing them to your application or SDK is a good idea if you’re looking to expedite development.

Tech Lead (f/m/d)

  • adjoe
  • Programmatic Supply
  • Full-time
adjoe is a leading mobile ad platform developing cutting-edge advertising and monetization solutions that take its app partners’ business to the next level. Part of the applike group ecosystem, adjoe is home to an advanced tech stack, powerful financial backing from Bertelsmann, and a highly motivated workforce to be reckoned with.

Meet Your Team: WAVE Supply Services
In this competitive adtech market, adjoe stands for greater transparency and fairness for app publishers and advertisers – and a more relevant and enjoyable experience for users.
It’s exactly for this that adjoe has built its own programmatic mobile ad platform WAVE which connects app publishers with advertisers. We are working with dozens of advertising networks, measurement providers and other external services with whom we exchange millions of data points every minute. The WAVE Supply Services team is responsible for developing tools through which app publishers can manage their WAVE integration, analyze their ad monetisation performance and assess the ads’ UX through dashboards and APIs.

Join our discussions, explore implementation, and put your problem-solving skills to the test in our cross-functional Programmatic team!

As a part of the WAVE Supply Services team, you’ll be responsible for developing the face of the product: services to set up an SDK, analyze ad revenue and apps’ UX – from gathering the necessary data from our SDK to visualizing it on the dashboard or providing it via APIs.
What You Will Do
  • Build and manage a cross-functional team together with a Product Lead: assist in hiring, provide feedback, set up software development guidelines (code style, best practices).
  • Define the architecture of the software that gives engineers in the team a framework in that they can act and develop.
  • Understand business requirements to come up with solutions and tech specifications for the features.
  • Be a mentor and train developers in the team to make them better in programming, communication, and planning.
  • Be hands-on by developing features on your own (at least 50% of the working time, mostly web frontend).
  • Align with other tech teams on common technologies and tools to be used.
  • Work with statistics from different sources on a regular basis to define data use cases and issues.
  • Who You Are
  • You have a tech degree (computer science, engineering, mathematics or similar). Alternatively 5 years of professional experience in software development.
  • You have 2+ years’ of experience working as a Lead, preferably developing web applications.
  • Full-stack development experience: frontend (React, Angular, or Vue.js), backend (experience in Go or you’re ready to learn it).
  • You have experience in mobile development, e.g. writing an app or working in a cross-functional team.
  • You have gained knowledge of working in Typescript, Redux, Emotion-js, JSS, Tailwind, or a similar framework, Git, and (ideally) Docker.
  • You’re excited to work with data and have experience performing basic data analysis.
  • You have experience hiring people, doing regular 1-1s, creating career plans for developers.
  • Heard of Our Perks?
  • Tech Package: Create game-changing technologies and work with the newest technologies out there.
  • Wealth Building: Benefit from virtual stock options.
  • Work–Life Package: Work remotely for 2 days per week, enjoy flexible working hours and 30 vacation days, work remotely for 3 weeks per year, modern office in the city center, dog-friendly.
  • Relocation Package: Receive visa and legal support, a generous relocation subsidy, and free German classes in the office.
  • Never-Go-Hungry Package: Graze on regular company breakfasts and events, and a selection of free snacks and drinks.
  • Physical & Mental Health Package: In-house gym with a personal trainer, various classes like Yoga with expert teachers & free of charge access to our EAP (Employee Assistance Program) to support your mental health and well-being
  • Activity Package: Enjoy a host of team events, hackathons, and company trips.
  • Career Growth Package: Benefit from a dedicated growth budget to attend relevant conferences and online seminars of your choosing.
  • We’re programmed to succeed

    See vacancies