> For the complete documentation index, see [llms.txt](https://documentation.pushly.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://documentation.pushly.com/integration/implementation-steps/apple-ios/sdk-swift-obj-c.md).

# SDK: Swift / Obj-C

## Prerequisites

Before you can integrate the SDK with your app the following requirements must be met:

* An iOS 11+ device (iPhone, iPad, or iPod Touch) to develop and test on, or a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](#testing-on-a-simulator).
* A Mac OS device with XCode 14+ (Swift 5.7, if using Swift) installed.
* An iOS P12 Push Certificate or P8 Key. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup.md) to generate them.
* An App ID registered for your bundle id with the `Push Notifications` and `App Groups` capabilities enabled. See [<mark style="color:blue;">Register Your App ID</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup.md#register-your-app-id).
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Overview` > `Details` > `Domain Information` section.

## Step 1: Import the PushSDK Framework

{% tabs %}
{% tab title="Swift Package Manager" %}
The PushSDK Framework is compatible with both Swift and Objective-C.

* Select the root project and then your primary application target > `General`
* Click `+` > `Add Other` > `Add Package Dependency` under `Frameworks, Libraries, and Embedded Content`
* Search using the Package URL: [<mark style="color:blue;">https://github.com/pushly/push-sdk-ios</mark>](https://github.com/pushly/push-sdk-ios)
* Set the Dependency Rule to: `Range of Versions` and `1.0.0` < `2.0.0`
* Click `Add Package`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2Fur8SV1YwZVDqBNRbLvYW%2FScreen%20Shot%202022-12-16%20at%2012.50.27%20PM.png?alt=media&#x26;token=11c0170c-4375-4ce7-9f72-0077fd73e82f" alt=""><figcaption></figcaption></figure>

* Ensure the `Pushly` option is checked and click `Add Package`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FEXYitjbWrTVuLTouDMPQ%2FScreen%20Shot%202022-12-16%20at%2012.50.52%20PM.png?alt=media&#x26;token=f8eb701a-3b7c-47bc-9229-1259f38dd2bf" alt=""><figcaption></figcaption></figure>

* Finally, add the `Pushly` library to the Primary Application Target

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FsuHbbXOjXGKNlZobezvD%2FScreen%20Shot%202022-12-16%20at%2012.51.13%20PM.png?alt=media&#x26;token=c593ae8d-c92b-4dd7-9bc3-102bcd18fe10" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="CocoaPods" %}
The PushSDK Framework is compatible with both Swift and Objective-C.

CocoaPods Link: <https://cocoapods.org/pods/Pushly>

Add the following to your Podfile:

```
target 'MyApp' do
  pod 'Pushly', '~> 1.1'
end
```

Then run a `pod install` inside your terminal, or from CocoaPods.app.
{% endtab %}
{% endtabs %}

## Step 2: Add the Notification Service Extension

The Notification Service Extension is required to enable rich notifications that support images and custom actions.

Within your app's XCode project select `File > New > Target`. Select `Notification Service Extension` inside the iOS templates tab and click `Next`.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FIKOtBgXyBx78VJBxuNs9%2Fimage.png?alt=media&#x26;token=b0055514-7e75-4ce8-a0b3-bbba8f2543c6" alt=""><figcaption></figcaption></figure>

Enter "NotificationServiceExtension" for the `Product Name` and any other configuration details for your app extension and then click `Finish` but **do not click activate** on the subsequent dialog.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2F96yVKAthjykdmnXGXlem%2Fimage.png?alt=media&#x26;token=b8ed3424-44e0-4259-b97b-bb1457411164" alt=""><figcaption></figcaption></figure>

Click `Cancel` on the dialoging prompting you to activate the service extension so that you can set the deployment target once the modal is closed.

<img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FcGXECnSyGjGGEUBg5KDM%2Fimage.png?alt=media&#x26;token=bc91b955-baa5-490f-a32b-dd7367657f9f" alt="" data-size="original">

Make sure to set your deployment target to the **same as your primary application target**. Unless you have a specific reason not to, you should set the `Deployment Target` to be iOS 11 which is the lowest supported version of iOS in the PushSDK Framework and in the most recent releases of XCode.

Next, add the `Pushly` library to your Notification Service Extension:

Click `+` under `Frameworks and Libraries` and select the already imported `Pushly` library.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FK7zOKyVoMmLfbFB5SFif%2FScreen%20Shot%202022-12-16%20at%2012.51.32%20PM.png?alt=media&#x26;token=bf917f13-825a-4905-b13d-404d953a7c4a" alt=""><figcaption></figcaption></figure>

Now, open the newly created Notification Service Extension and replace the code with the following:

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly

class NotificationService: PNNotificationServiceExtension {
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// Make sure to keep the header file import
#import "NotificationService.h"

@import Pushly;

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    [PNNotificationServiceExtensionHandler didReceiveExtensionRequest:request content:self.bestAttemptContent withContentHandler:contentHandler];
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    [PNNotificationServiceExtensionHandler didRecieveExtensionTimeWillExpire:self.bestAttemptContent withContentHandler:self.contentHandler];

    self.contentHandler(self.bestAttemptContent);
}

@end
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can ignore dependency errors, such as `Module Pushly not found`, at this point as they will be resolved during the next build.
{% endhint %}

## Step 3: Add Capabilities to Primary Application Target

{% hint style="warning" %}
The Push Notifications capability should only be added to the **primary application target**.
{% endhint %}

Select the root project and then your primary application target > `Signing & Capabilities`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FPEKQiayxyjIphpjeqxB1%2Fimage.png?alt=media&#x26;token=84e69c08-d120-4a48-a193-a272b9e4f109" alt=""><figcaption></figcaption></figure>

Click the `+ Capability` and add `Push Notifications`.

Click the `+ Capability` and add `Background Modes`.

After adding the `Background Modes` capability ensure that `Remote Notifications` are enabled.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FMAfJ7lisFKGMxQvjpIaB%2Fimage.png?alt=media&#x26;token=7b5b4afb-0a24-4b0a-8ef6-594f763231ea" alt=""><figcaption></figcaption></figure>

Adding `Push Notifications` writes the `aps-environment` entitlement into the target's entitlements file — `development` for debug builds, and `production` for release, TestFlight, and App Store builds. Xcode manages this value; you should not set it by hand.

{% hint style="warning" %}
If the entitlement is missing, the app cannot register with APNs and registration fails at launch with:

```
no valid 'aps-environment' entitlement string found for application
```

That error means either the `Push Notifications` capability is not present on this target, or the provisioning profile predates the capability being enabled on your App ID. Confirm the capability is listed, then let Xcode regenerate the profile.
{% endhint %}

## Step 4: Configure the Shared App Group

Your app and its Notification Service Extension run as **two separate processes**. The shared App Group container is the only storage both can reach, and the PushSDK relies on it for everything that begins in one process and completes in the other.

The same App Group must be declared on **both** targets. If only one carries it, each process silently falls back to its own private storage and the sharing simply does not happen — with no error raised at the point of failure.

### Naming

The SDK derives the identifier from your app's bundle id rather than reading it from configuration, so the two targets cannot disagree about which container to use. It must be exactly:

```
group.{app-bundle-id}.push
```

For an app whose bundle id is `com.pushly.example`, the App Group is `group.com.pushly.example.push`. The Notification Service Extension uses the **app's** bundle id here, not the extension's own.

### Add it to the primary application target

With your primary application target selected under `Signing & Capabilities`, click the `+ Capability` and add `App Groups`.

Click the `+` symbol located inside the `App Groups` section to add a new named container, and name it as above.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FOR42sYi1kH4o1227pQpY%2Fimage.png?alt=media&#x26;token=c504305e-c703-4e7e-afff-834d9de68aac" alt=""><figcaption></figcaption></figure>

Check the box next to the newly created App Group in the primary application target.

### Add it to the Notification Service Extension

Select the root project and then the `NotificationServiceExtension > Signing & Capabilities`

Click the `+ Capability` and add `App Groups`.

Check the box next to the **same** App Group in the `NotificationServiceExtension`.

Adding the capability writes `com.apple.security.application-groups` into each target's entitlements file, containing the group identifier. Both files must list it.

{% hint style="info" %}
With `Automatically manage signing` enabled, Xcode creates the App Group identifier, enables the `App Groups` capability on your App ID, and regenerates the provisioning profiles for both targets. If Xcode complains about the new App Group, it is usually still provisioning — refresh the list once it has finished.

If you manage signing manually, creating the identifier is not enough on its own: enable `App Groups` on the App ID as well, then regenerate and download both profiles.
{% endhint %}

### What breaks without it

The SDK keeps running, but the features that depend on cross-process state stop working:

* **Notification impressions** — recorded by the extension and completed by the app, so a severed container costs every one of them.
* **Badge counts** — the shared count is no longer visible to both processes.
* **Service-extension attribution** — notifications handled by the extension are no longer attributed correctly.

The per-installation event sequence counter is also guarded by a cross-process lock that lives in the container. Without the container the SDK falls back to a best-effort in-process increment, so events still carry a sequence value, but it is no longer coordinated between the two processes.

### Confirming it is configured

The SDK reports the state of the shared container once per launch. When it is missing you will see:

```
[PNAppGroup] Shared App Group container is unavailable. Notification impressions, badge counts and service-extension attribution will not work. Add the App Group "group.com.pushly.example.push" to the entitlements of BOTH the app target and the Notification Service Extension target.
```

When it is configured correctly, the same check reports the resolved identifier at debug level:

```
[PNAppGroup] Shared App Group container available: group.com.pushly.example.push
```

## Step 5: SDK Initialization

First import the `Pushly` library in your AppDelegate

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Pushly;
```

{% endtab %}
{% endtabs %}

Add the following code to the AppDelegate's `didFinishLaunchingWithOptions` method of your application.

Replace the `REPLACE_WITH_SDK_KEY` in the `setConfiguration` method with the SDK Key from the platform settings page.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.logLevel = .info
PushSDK.setConfiguration(appKey: "REPLACE_WITH_SDK_KEY", withLaunchOptions: launchOptions)

PushSDK.PushNotifications.showPermissionPrompt() { granted, settings, error in
    // optional callback
    print("User accepted permissions: \(granted)")
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
PushSDK.logLevel = PNLogLevelInfo;
[PushSDK setConfigurationAppKey:@"REPLACE_WITH_SDK_KEY" withLaunchOptions:launchOptions];

[PushSDKPushNotifications showPermissionPrompt:^(BOOL granted, UNNotificationSettings * _Nonnull settings, NSError * _Nullable error) {
    NSLog(@"User accepted permissions: %@", granted ? @"YES" : @"NO")
}];
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**SwiftUI apps.** If your app supplies its delegate with `@UIApplicationDelegateAdaptor`, place the `setConfiguration` call in that delegate's `didFinishLaunchingWithOptions` exactly as shown above — the integration works normally.

Be aware when debugging that SwiftUI installs its own `SwiftUI.AppDelegate` as `UIApplication.shared.delegate` and forwards callbacks on to your adapted class. Inspecting `UIApplication.shared.delegate` will therefore not show your own delegate type. This is expected and does not indicate a broken integration.
{% endhint %}

## Step 6: Test the Implementation

Run your app on a physical iOS device, or on a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](#testing-on-a-simulator), to make sure it builds correctly.

The code you added in the previous step will show the push permission dialog upon app open. This can be customized using [<mark style="color:blue;">SDK methods</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-methods.md#manually-triggering-the-permission-dialog) to control when the dialog shows.

After accepting the dialog, log into the platform and navigate to `Notifications > Create Notification` and send your first notification, targeting the **Native: iOS** channel, to your device.

### Testing on a simulator

A simulator can register with APNs and receive real remote notifications, so it can be used to test subscription and end-to-end delivery. This requires **all** of the following:

* A Mac with Apple silicon or a T2 chip
* macOS 13 or later
* An iOS 16 or later simulator
* Xcode 14 or later

When those conditions are met the app registers normally and iOS issues a genuine device token. Two things differ from a physical device:

* **The token is sandbox-only.** Notifications must be sent through the APNs sandbox environment. A debug build carries `aps-environment: development`, which is what routes sends to sandbox, so this is handled for you.
* **The token is tied to that simulator on that Mac.** It is specific to the combination of the simulator and the Mac hardware it runs on, so it is not portable, and erasing the simulator issues a new one.

{% hint style="warning" %}
If any of the requirements above are not met — an Intel Mac without a T2 chip, macOS 12 or earlier, or a simulator older than iOS 16 — registration fails with APNs error `3010` and the SDK logs:

```
iOS Simulator does not support push notifications
```

On those setups token registration has to be exercised on a physical device.
{% endhint %}

### Delivering a notification without APNs

`xcrun simctl push` delivers a notification straight to a booted simulator with no APNs setup at all, and works on any simulator regardless of the requirements above. It is the quickest way to check notification **presentation and tap handling**.

Save a payload as `payload.apns`:

```json
{
  "aps": {
    "alert": {
      "title": "Test notification",
      "body": "Delivered locally to the simulator."
    },
    "sound": "default"
  },
  "piid": "00000000-0000-0000-0000-000000000000",
  "notification_id": 1,
  "landing_url": "https://example.com"
}
```

Then deliver it to a booted simulator, replacing the bundle id with your own:

```bash
xcrun simctl push booted com.pushly.example payload.apns
```

For the SDK to recognise the payload as a Pushly notification and process it, the payload must carry all of the following:

* `aps.alert` as a **dictionary** — a plain string alert is not recognised
* `piid` — string
* `notification_id` — **integer**
* `landing_url` — string

If any of these are missing or the wrong type, iOS still displays the notification but the SDK ignores it, and no SDK callbacks or events fire.

{% hint style="warning" %}
Notifications delivered with `simctl push` never travel through APNs or Pushly, so they produce no delivery, impression, or click reporting in the platform. Use them to verify presentation and tap handling only.
{% endhint %}

## Next Steps

Once you have confirmed the SDK is working properly you may continue to add additional optional functionality like:

* [<mark style="color:blue;">Handling notification opens / app links / deep linking</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/deep-linking.md)
* [<mark style="color:blue;">Attaching attributes to a subscriber's profile</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-methods.md#adding-attributes-to-a-subscribers-profile)
* [<mark style="color:blue;">Sending information about what content a subscriber interacts with</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-methods.md#activity-tracking-url-screen-visits)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://documentation.pushly.com/integration/implementation-steps/apple-ios/sdk-swift-obj-c.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
