iOS SDK
Add the PiedapConsent Swift package to an iOS or macOS app, create the client and look up every method, type and error.
PiedapConsent records consent from a native iOS or macOS app and keeps the answer on the device. It draws no banner: your app shows its own screen from the form the server sends.
Requirements#
- iOS 13 or macOS 12, or later.
- Swift 5.10 (Xcode 15.3) or later. The package depends on Foundation only.
- A publishable key,
pk_…. Its Allowed origins must listapp:followed by the bundle id, such asapp:in.example.kalpa. - A published form that a region rule sends your users to.
A test key answers any app. A live key answers only the apps on its list. See Mobile apps.
Install#
The package is not published to a public registry or served by the deployment. Your account contact provides the source.
In Xcode, choose File → Add Package Dependencies… → Add Local… and pick the consent-ios folder. Then link the PiedapConsent product. With a Package.swift, refer to the package by its folder name:
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "App",
platforms: [.iOS(.v13), .macOS(.v12)],
dependencies: [
.package(path: "../consent-ios"),
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "PiedapConsent", package: "consent-ios"),
]
),
]
)Create the client#
PiedapConsent()#
Creates the client. Create one and keep it for the life of the app.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
apiUrl |
String |
Required | The deployment address. |
publishableKey |
String |
Required | The publishable key, pk_…. |
applicationId |
String |
Required | The bundle id. It must be the id listed on the key after app:. |
store |
ConsentStore |
InMemoryConsentStore() |
Where state is kept. The default forgets everything when the app exits. |
timeout |
TimeInterval |
10 |
Request timeout in seconds. |
sessionConfiguration |
URLSessionConfiguration |
.ephemeral |
The configuration of the URLSession the client uses. |
Returns a PiedapConsent client.
Errors Throws ConsentAPIError with status 0 for a key that does not start with pk_, before any request is made.
import PiedapConsent
let consent = try PiedapConsent(
apiUrl: "https://console.dpconsent.com/consent",
publishableKey: "pk_…",
applicationId: Bundle.main.bundleIdentifier!,
store: UserDefaultsConsentStore()
)Each request carries the key in X-Piedap-Key and the bundle id in X-Piedap-App. Redirects are never followed.
Properties#
| Property | Type | Description |
|---|---|---|
decisions |
[String: Bool] |
The stored decisions by purpose key. Empty until the user decides. |
consentId |
String? |
The record's id, cns_…. nil before the first answer. |
onChange |
(([String: Bool]) -> Void)? |
Called with the decisions after every save and withdrawal, off the main thread. The client holds one handler. |
consent.onChange = { latest in applyConsent(latest) }Methods#
The methods that call the network are async and throw. There is no offline queue and no retry: a failed save throws, and the answer is not stored. Reading never needs the network.
loadForm(locale:countryCode:)#
Fetches the form for this device.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
locale |
String |
Required | A language tag, such as hi-IN. |
countryCode |
String? |
nil |
A two-letter country code. With nil, the server works it out from the request. |
Returns a ConsentForm. The country you pass picks the form only. The record takes its country and law from where the request comes from.
Errors Throws ConsentAPIError.
let form = try await consent.loadForm(
locale: Locale.preferredLanguages.first ?? "en"
)
let formForIndia = try await consent.loadForm(
locale: "hi-IN",
countryCode: "IN"
)shouldAsk(_:)#
Tells whether to show your consent screen for a form.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
form |
ConsentForm |
Required | The form from loadForm. |
Returns Bool. It is true:
- before the first decision
- when the published form has changed since the user answered, unless its
consentModelisNOTICE_ONLY - when the server asks for consent again
It is false when the form is not available.
Errors None.
if consent.shouldAsk(form) {
presentConsentScreen(form)
}acceptAll(_:)#
Records that the user accepted every purpose. The record's interaction is ACCEPT_ALL.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
form |
ConsentForm |
Required | The form the user answered. |
Returns [String: Bool], the stored decisions by purpose key.
Errors Throws ConsentAPIError. The status is 0 for a form that is not available.
try await consent.acceptAll(form)rejectAll(_:)#
Records that the user refused every optional purpose. The record's interaction is REJECT_ALL.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
form |
ConsentForm |
Required | The form the user answered. |
Returns [String: Bool], the stored decisions. Always-on purposes stay allowed.
Errors Throws ConsentAPIError. The status is 0 for a form that is not available.
try await consent.rejectAll(form)savePreferences(_:choices:)#
Records the user's own choices. The record's interaction is SAVE_PREFERENCES.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
form |
ConsentForm |
Required | The form the user answered. |
choices |
[String: Bool] |
Required | Purpose key to allowed. A purpose you leave out counts as declined. |
Returns [String: Bool], the stored decisions. Always-on purposes stay allowed.
Errors Throws ConsentAPIError. The status is 0 for a form that is not available.
try await consent.savePreferences(
form,
choices: ["analytics": true, "marketing": false]
)The three recording methods write records in the same way:
- The first answer creates a record. A change of mind on the same form version updates it.
- An answer to a new version, or to a request for fresh consent, creates a new record. So does a change to a record that was erased on the server.
- The record is kept under a random id made for this install, with the type
DEVICE_ID, unless you calledidentify.
granted(_:)#
Tells whether the stored decision allows a purpose.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
purposeKey |
String |
Required | The purpose key. |
Returns Bool. It is false for every purpose until the user decides. After loadForm finds no form for the user's location, it is true for every purpose, unless a decision is already stored.
Errors None.
if consent.granted("analytics") {
Analytics.enable()
}withdraw()#
Withdraws the record on the server and clears it from the device.
Takes no parameters.
Returns nothing. The next shouldAsk is true again.
Errors Throws ConsentAPIError. The stored decision then stays as it was.
try await consent.withdraw()identify(subjectId:subjectType:proof:)#
Records consent created from now on under the signed-in person instead of the device.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subjectId |
String |
Required | The person's identifier, exactly the one the proof was made for. |
subjectType |
SubjectType |
Required | .userId, .email or .phone. |
proof |
String |
Required | The identity proof your server computed: 64 hex characters. |
Returns nothing. The identity is kept in memory only, so call identify after sign-in on every launch.
Errors Throws ConsentAPIError with status 0 for an empty subjectId or a proof that is not 64 hex characters.
try consent.identify(
subjectId: session.userId,
subjectType: .userId,
proof: session.consentProof
)identify does not call the server. A proof the server cannot verify is refused when the next record is created, with status 403 and the code IDENTITY_PROOF_REQUIRED. A decision already recorded under the device id keeps being updated in its own record. Compute the proof with identityProof() or identity_proof().
clearIdentity()#
Records consent created from now on under the device again. Call it on sign-out.
Takes no parameters.
Returns nothing.
Errors None.
consent.clearIdentity()Types#
ConsentForm#
A text field the form does not have is nil.
| Field | Type | Description |
|---|---|---|
available |
Bool |
false when no form is targeted at the user's location. There is nothing to ask. |
title, body |
String? |
The wording of the notice, in the language asked for. |
acceptAllLabel, rejectAllLabel, savePreferencesLabel |
String? |
Button wording. |
privacyPolicyUrl |
String? |
The privacy policy link, if the form has one. |
purposes |
[Purpose] |
The purposes to show. |
consentModel |
String? |
OPT_IN, OPT_OUT or NOTICE_ONLY. |
law |
String? |
The law that applies, such as DPDP. |
key, contentHash |
String |
Which form this is. |
version |
Int |
The form's version. |
locale |
String? |
The language of the wording. |
reconsentRequired |
Bool |
true when the server asks for a fresh answer. |
Purpose#
| Field | Type | Description |
|---|---|---|
key |
String |
The purpose key. |
name, description, category |
String? |
What to show for the purpose. |
alwaysOn |
Bool |
The purpose cannot be switched off. |
defaultGranted |
Bool |
The form's default for the purpose. |
ConsentStore#
The protocol the client keeps its state through.
| Member | Description |
|---|---|
value(forKey:) |
Returns the stored string, or nil. |
set(_:forKey:) |
Stores a string. |
removeValue(forKey:) |
Removes a key. |
InMemoryConsentStore |
Built in. Keeps state in memory only. |
UserDefaultsConsentStore |
Built in. Keeps state between launches. Pass defaults: to share state with app extensions. |
let shared = UserDefaultsConsentStore(
defaults: UserDefaults(suiteName: "group.in.example.kalpa")!
)The store holds a random id for the install, the consent id, the latest decisions, the form and version they answer, and whether the last fetch found a form. Its keys start with piedap.consent..
SubjectType#
An enum with the cases .userId (USER_ID), .email (EMAIL) and .phone (PHONE).
Errors#
A failed request throws ConsentAPIError.
| Field | Type | Description |
|---|---|---|
status |
Int |
The HTTP status. 0 when the server was not reached, or when the SDK found the mistake before any request. |
message |
String |
The server's message. |
code |
String? |
The server's code, or nil. |
Match on the status and the code, not on the message. A 401 means the key was not accepted. It is wrong, revoked or turned off, or it is a live key and the bundle id is not in its allowed origins. Every code is listed in Errors.
do {
try await consent.acceptAll(form)
} catch let error as ConsentAPIError {
print(error.status, error.code ?? "no code", error.message)
}Not in this SDK#
- A ready-made consent screen. Draw your own from the form.
- Choices partner by partner. The SDK answers per purpose only.
- Linked identifiers. Link them from your server with
linkIdentifiers(). - An offline queue or retries.
- React Native and Flutter wrappers. Bridge the native SDK yourself, or call the API with the key in
X-Piedap-Keyand your app id inX-Piedap-App. - The complaint link, the rights request link, the privacy contact email and the list of languages. Read
form.copy.complaintUrl,runtime.rightsUrl,runtime.contactEmailandform.localesfromGET /sdk/v1/form. - Tracker blocking, Google Consent Mode, IAB TCF and GPP. Gate your own SDKs on
granted.