Skip to main content
The Spam Control SDK blocks and identifies spam calls through Apple’s Call Directory Extensions. Everything goes through SpamControl.shared. Current version: 3.0.0.
  • iOS 14 or later. Releases are built with Xcode 16.4 and import in apps on Xcode 16 or 26.
  • Test on a physical device: call blocking does not run on the Simulator.
  • Phone numbers are Brazilian, 55 + area code + number (e.g. 5511987654321).

Setup

1. Add the framework

Add FeatureSpamControl.xcframework (provided by Bemobi) as Embed & Sign in the app and as Do Not Embed in each extension. Every slice carries its own PrivacyManifest.xcprivacy, declaring the data the SDK collects and the APIs it uses. Your app still declares its own.

2. Create the extensions and App Groups

Create one Call Directory Extension target per feature. Their bundle identifiers must start with the app’s. Add the App Groups capability to the app and to each extension. Spam blocking and identification must share one App Group, where the database lives. Manual blocking can use the same group or its own. Use the same identifiers everywhere below.

3. Implement the extension handlers

Spam blocking and identification complete the request in a callback:
Manual blocking is synchronous and throws:
hintIdentifier is the label iOS shows on the call. Each handler also has a failable initializer without appGroup: that reads the SpamControlAppGroup key from the extension’s Info.plist. Manual blocking never blocks one of the user’s contacts: the SDK asks for contacts access on the first manual block, so add NSContactsUsageDescription to the app’s Info.plist.

4. On every launch

In application(_:didFinishLaunchingWithOptions:), or in your SwiftUI App’s init, register the extensions and then call refresh(). Both are required on every launch. Never call the SDK from the extensions.
refresh() keeps the SDK up to date and starts its automatic refresh, see Refresh.

Declaring the extensions in the Info.plist

Instead of configureExtensions(with:), declare them in the app’s Info.plist under SpamControlExtensions:
Type is blockSpams, callIdentifier or blockManual; an entry missing a key is ignored. The code call wins when both exist. This replaces only configureExtensions(with:): refresh() is still required on every launch.

Activation

The user first turns the extensions on in Settings > Apps > Phone > Call Blocking & Identification (Settings > Phone before iOS 18); openSettings() opens Settings. Then call enable once:
enable downloads and applies the whole database, so it can take a few seconds. It saves the credentials, so later launches don’t need them. To switch account, environment or mode, call disable() and then enable again.

State and deactivation

isEnabled tells whether the SDK is on. Re-read it when your screen appears, because the SDK turns itself off when the user turns the spam blocking extension off in Settings, or when the subscription is cancelled. Call enable again to turn it back on. disable() turns the SDK off until the next enable, across launches. Manually blocked numbers and category choices are kept.

Refresh

After the first refresh(), the SDK also refreshes whenever the app returns to the foreground. A refresh downloads the database at most once every 24 hours and applies it, together with the subscription state. Pass force: true only on an explicit user action, to ignore the 24-hour window. refresh never throws, the result is discardable, and calling it again while one is running is safe.

Failure reasons

EnableResult.FailureReason and SpamDatabaseRefreshOutcome.FailureReason are separate enums with overlapping cases, so a switch over one does not accept the other.

Subscription mode

The mode is set at activation and ignored by an enable on an SDK already on: to change it, disable() first. subscriptionMode returns the mode in force, saved across launches. The state comes with the daily database download, so right after enable there is none yet. To apply a change sooner, call refreshSubscriptionState() when the user opens your Spam Control screen; it returns nil in .free mode. subscriptionState() reads the last known state locally, e.g. to show pending-payment UI. cancelSubscription(reason:) throws if the request fails, e.g. when the backend has no subscription for the number.

API reference

Everything is on SpamControl.shared, except the last two rows. A number is either blocked by the user or trusted: the latest action wins. Removing a trusted number puts it back where it came from.

Types

CXCallDirectoryPhoneNumber is CallKit’s typealias for Int64. BlockManuallyErrorType cases you are most likely to handle: phoneNumberAlreadyBlocked, phoneNumberNotBlocked, phoneNumberInContacts, invalidPhoneNumberFormat, emptyPhoneNumber, phoneNumberInsufficientSignificantDigits, permissionDenied (contacts), callDirectoryIsNotEnabled and networkUnavailable; it has more cases, and most carry associated values with the number and the context. TrustedNumbersErrorType covers an invalid number, a disabled extension and storage or database failures. Both are LocalizedError.

Migrating from 2.x

  1. Keep configureExtensions(with:) on every launch, or move it to the Info.plist.
  2. Add refresh() on every launch, right after it.
  3. Replace the 2.x calls:
  1. Keep the App Group of .blockSpams, where your users’ database is. If identification used its own group, move it into the .blockSpams group, not the other way round.
Users activated with 2.x keep blocking after the update without calling enable. Calling it later is optional: it returns .alreadyOn and only saves the values it receives, e.g. to switch those users to .paid.