Entwickler · API v1.0.0
iOS-Integration (Einrichtung und Objective-C)
Dieser Leitfaden beschreibt Schritt für Schritt die Einrichtung auf Apple-Seite für native iOS-Apps und enthält Objective-C-Beispielcode. Wenn Sie Swift verwenden, folgen Sie hier den Einrichtungsschritten und wechseln Sie für den Code zum Swift-Leitfaden. Die allgemeine API-Referenz finden Sie in der Überblick.
Unterstützte Versionen
| Komponente | Minimum | Empfohlen |
|---|---|---|
| iOS (Deployment Target) | 13.0 | 15.0+ |
| Xcode | 15 | aktuell |
| Objective-C | ARC aktiviert | – |
Voraussetzungen: Kostenpflichtige Mitgliedschaft im Apple Developer Program, physisches iPhone/iPad (Push-Tests im Simulator sind nicht zuverlässig).
Wie funktioniert es?
NotiPilot 1.0.0 stellt Benachrichtigungen über den Expo Push Service zu:
- Die App erhält von Apple den APNs Device Token.
- Dieser Token wird beim Expo-Token-Service gegen einen Expo Push Token eingetauscht.
- Der Expo Push Token wird per
register-devicebei NotiPilot registriert. - Eine aus dem Dashboard gesendete Benachrichtigung gelangt über Expo → APNs → Gerät zum Nutzer.
Ihre App muss nicht mit Expo geschrieben sein; Expo dient ausschließlich als Zustellinfrastruktur.
1. Apple-Developer-Einrichtung
1.1 App ID und Push Notifications
- Öffnen Sie unter developer.apple.com → Identifiers die App ID Ihrer App (oder legen Sie sie an).
- Aktivieren Sie in der Liste Capabilities das Kästchen Push Notifications und speichern Sie.
- Erstellen Sie Ihre Provisioning Profiles neu (bei „Automatically manage signing“ in Xcode geschieht das automatisch).
1.2 APNs Auth Key (.p8)
- Erstellen Sie unter developer.apple.com → Keys über + einen neuen Schlüssel.
- Aktivieren Sie die Option Apple Push Notifications service (APNs).
- Laden Sie die
.p8-Datei herunter (sie kann nur einmal heruntergeladen werden, bewahren Sie sie sicher auf). Notieren Sie die Key ID und die Team ID Ihres Accounts.
Ein einziger
.p8-Schlüssel kann für alle Apps Ihres Accounts sowie sowohl in der Sandbox- als auch in der Production-Umgebung verwendet werden. Die alten.p12-Zertifikate werden nicht benötigt.
1.3 Expo-Projekt
- Erstellen Sie auf expo.dev ein Projekt und notieren Sie die Projekt-ID (UUID).
- expo.dev → Projekt → Credentials → iOS → Bundle ID hinzufügen → im Bereich Push Key die
.p8-Datei zusammen mit Key ID und Team ID hochladen.
1.4 NotiPilot-Dashboard
Fügen Sie Ihre App hinzu: Expo Project ID = Expo-Projekt-ID, Expo Access Token = expo.dev → Access Tokens, iOS Bundle ID = Bundle Identifier Ihrer App.
2. Xcode-Einstellungen
Target → Signing & Capabilities:
- + Capability → Push Notifications
- + Capability → Background Modes → Remote notifications
Dadurch wird Ihrer .entitlements-Datei der Schlüssel aps-environment hinzugefügt. In Debug-Builds lautet der Wert development (APNs-Sandbox), in TestFlight-/App-Store-Builds production.
3. Objective-C-Code
3.1 NotiPilot.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NotiPilot : NSObject
+ (instancetype)shared;
/// In application:didRegisterForRemoteNotificationsWithDeviceToken: aufrufen.
- (void)registerDeviceWithAPNsToken:(NSData *)deviceToken
extraAttributes:(nullable NSDictionary<NSString *, id> *)extra
completion:(nullable void (^)(NSDictionary *_Nullable response, NSError *_Nullable error))completion;
/// Aufrufen, wenn sich der Nutzer anmeldet.
- (void)identifyWithExternalId:(NSString *)externalId
attributes:(nullable NSDictionary<NSString *, id> *)attributes
completion:(nullable void (^)(NSDictionary *_Nullable response, NSError *_Nullable error))completion;
@end
NS_ASSUME_NONNULL_END
3.2 NotiPilot.m
#import "NotiPilot.h"
#import <Security/Security.h>
#import <UIKit/UIKit.h>
static NSString *const kBaseURL = @"https://app.notipilot.com/api/v1";
static NSString *const kAppId = @"IHRE-EXPO-PROJEKT-ID"; // "Expo Project ID" im Dashboard
static NSString *const kUidKey = @"notipilot_device_uid";
@implementation NotiPilot
+ (instancetype)shared {
static NotiPilot *instance;
static dispatch_once_t once;
dispatch_once(&once, ^{ instance = [NotiPilot new]; });
return instance;
}
#pragma mark - device_uid (Keychain)
- (NSString *)deviceUid {
NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount: kUidKey,
(__bridge id)kSecReturnData: @YES,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne };
CFTypeRef result = NULL;
if (SecItemCopyMatching((__bridge CFDictionaryRef)query, &result) == errSecSuccess && result) {
NSData *data = (__bridge_transfer NSData *)result;
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
NSString *uid = [[NSUUID UUID].UUIDString lowercaseString];
NSDictionary *item = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount: kUidKey,
(__bridge id)kSecValueData: [uid dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly };
SecItemAdd((__bridge CFDictionaryRef)item, NULL);
return uid;
}
#pragma mark - Public API
- (void)registerDeviceWithAPNsToken:(NSData *)deviceToken
extraAttributes:(NSDictionary<NSString *, id> *)extra
completion:(void (^)(NSDictionary *, NSError *))completion {
NSMutableString *hex = [NSMutableString stringWithCapacity:deviceToken.length * 2];
const unsigned char *bytes = deviceToken.bytes;
for (NSUInteger i = 0; i < deviceToken.length; i++) [hex appendFormat:@"%02x", bytes[i]];
[self exchangeAPNsToken:hex completion:^(NSString *expoToken, NSError *error) {
if (!expoToken) { if (completion) completion(nil, error); return; }
NSMutableDictionary *attributes = [@{
@"locale": [NSLocale currentLocale].languageCode ?: [NSNull null],
@"country": [NSLocale currentLocale].countryCode ?: [NSNull null],
@"app_version": [NSBundle mainBundle].infoDictionary[@"CFBundleShortVersionString"] ?: [NSNull null],
@"os_version": [UIDevice currentDevice].systemVersion,
} mutableCopy];
if (extra) [attributes addEntriesFromDictionary:extra];
[self post:@"register-device" body:@{
@"app_id": kAppId,
@"device_uid": [self deviceUid],
@"token": expoToken,
@"platform": @"ios",
@"provider": @"expo",
@"attributes": attributes,
} attempt:0 completion:completion];
}];
}
- (void)identifyWithExternalId:(NSString *)externalId
attributes:(NSDictionary<NSString *, id> *)attributes
completion:(void (^)(NSDictionary *, NSError *))completion {
NSMutableDictionary *body = [@{ @"app_id": kAppId,
@"device_uid": [self deviceUid],
@"external_id": externalId } mutableCopy];
if (attributes) body[@"attributes"] = attributes;
[self post:@"identify-device" body:body attempt:0 completion:completion];
}
#pragma mark - Expo-Token-Austausch
- (void)exchangeAPNsToken:(NSString *)hexToken completion:(void (^)(NSString *, NSError *))completion {
#if DEBUG
BOOL development = YES; // Debug-Builds verwenden die APNs-Sandbox
#else
BOOL development = NO;
#endif
NSDictionary *body = @{ @"type": @"apns",
@"deviceId": [self deviceUid],
@"development": @(development),
@"appId": [NSBundle mainBundle].bundleIdentifier ?: @"",
@"deviceToken": hexToken,
@"projectId": kAppId };
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:
[NSURL URLWithString:@"https://exp.host/--/api/v2/push/getExpoPushToken"]];
req.HTTPMethod = @"POST";
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
req.HTTPBody = [NSJSONSerialization dataWithJSONObject:body options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:req
completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
NSInteger status = [(NSHTTPURLResponse *)resp statusCode];
NSDictionary *json = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
NSString *token = [json isKindOfClass:NSDictionary.class] ? json[@"data"][@"expoPushToken"] : nil;
if (status == 200 && [token isKindOfClass:NSString.class]) {
completion(token, nil);
} else {
completion(nil, err ?: [NSError errorWithDomain:@"NotiPilot" code:status userInfo:@{
NSLocalizedDescriptionKey: @"Expo token exchange failed" }]);
}
}] resume];
}
#pragma mark - HTTP
- (void)post:(NSString *)path body:(NSDictionary *)body attempt:(NSInteger)attempt
completion:(void (^)(NSDictionary *, NSError *))completion {
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:
[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", kBaseURL, path]]];
req.HTTPMethod = @"POST";
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[req setValue:@"application/json" forHTTPHeaderField:@"Accept"];
req.HTTPBody = [NSJSONSerialization dataWithJSONObject:body options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:req
completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
NSInteger status = [(NSHTTPURLResponse *)resp statusCode];
NSDictionary *json = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
if ((status == 429 || status >= 500) && attempt < 3) {
double wait = [json[@"retry_after"] doubleValue] ?: pow(2, attempt);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(wait * NSEC_PER_SEC)),
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[self post:path body:body attempt:attempt + 1 completion:completion];
});
return;
}
if (completion) completion(json, err);
}] resume];
}
@end
3.3 AppDelegate.m
#import "AppDelegate.h"
#import "NotiPilot.h"
#import <UserNotifications/UserNotifications.h>
@interface AppDelegate () <UNUserNotificationCenterDelegate>
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError *error) {
if (granted) {
dispatch_async(dispatch_get_main_queue(), ^{ [application registerForRemoteNotifications]; });
}
}];
return YES;
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[[NotiPilot shared] registerDeviceWithAPNsToken:deviceToken
extraAttributes:@{ @"city": @"Istanbul" }
completion:^(NSDictionary *res, NSError *err) {
if (err) NSLog(@"[NotiPilot] register failed: %@", err);
}];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSLog(@"[NotiPilot] APNs registration failed: %@", error);
}
// Benachrichtigung anzeigen, während die App im Vordergrund ist
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
if (@available(iOS 14.0, *)) {
completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionList | UNNotificationPresentationOptionSound);
} else {
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
}
}
// Benachrichtigung angetippt — aus dem Dashboard gesendete benutzerdefinierte Daten liegen unter dem Schlüssel `body`
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler {
id raw = response.notification.request.content.userInfo[@"body"];
NSDictionary *data = nil;
if ([raw isKindOfClass:NSDictionary.class]) {
data = raw;
} else if ([raw isKindOfClass:NSString.class]) {
data = [NSJSONSerialization JSONObjectWithData:[raw dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
}
NSString *screen = data[@"screen"];
// Anhand von screen navigieren
completionHandler();
}
@end
Wenn sich der Nutzer anmeldet:
[[NotiPilot shared] identifyWithExternalId:user.userId attributes:nil completion:nil];
Checkliste
- Push-Notifications-Capability ist in der App ID aktiviert
- APNs-Schlüssel (
.p8) mit Key ID + Team ID ins Expo-Projekt hochgeladen - Expo Project ID im Dashboard =
kAppId, iOS Bundle ID = Bundle Identifier der App - In Xcode sind Push Notifications + Background Modes → Remote notifications aktiviert
- Im Debug-Build
development = YES, in TestFlight/App StoreNO -
countryund nach Möglichkeitcitywerden als Attributes gesendet
Häufige Probleme
| Symptom | Lösung |
|---|---|
didRegisterForRemoteNotifications wird nie aufgerufen |
Push-Notifications-Capability und Provisioning Profile prüfen; ein physisches Gerät verwenden. |
Fehler no valid "aps-environment" entitlement |
Das Provisioning Profile wurde nach dem Hinzufügen der Capability nicht erneuert. |
Token-Austausch liefert 4xx |
Werte von projectId und appId (Bundle ID) prüfen. |
| In TestFlight kommen keine Benachrichtigungen an, im Debug schon (oder umgekehrt) | Der Wert development verweist auf die falsche Umgebung. |
404 unknown_app |
app_id ist im Dashboard nicht registriert. |