Geliştiriciler · API v1.0.0

iOS Entegrasyonu (Kurulum ve Objective-C)

Bu rehber native iOS uygulamaları için Apple tarafındaki kurulumu adım adım anlatır ve Objective-C örnek kodu içerir. Swift kullanıyorsanız kurulum adımlarını buradan takip edip kod için Swift rehberine geçin. Genel API referansı için Genel bakış.

Desteklenen sürümler

Bileşen Minimum Önerilen
iOS (deployment target) 13.0 15.0+
Xcode 15 güncel
Objective-C ARC etkin –

Gereksinimler: Ücretli Apple Developer Program üyeliği, fiziksel iPhone/iPad (simülatörde push testi güvenilir değildir).

Nasıl çalışır?

NotiPilot 1.0.0 bildirimleri Expo Push Service üzerinden teslim eder:

  1. Uygulama Apple'dan APNs device token'ı alır.
  2. Bu token Expo token servisinde bir Expo Push Token'a dönüştürülür.
  3. Expo Push Token NotiPilot'a register-device ile kaydedilir.
  4. Panelden gönderilen bildirim Expo → APNs → cihaz yoluyla ulaşır.

Uygulamanızın Expo ile yazılmış olması gerekmez; Expo yalnızca teslimat altyapısı olarak kullanılır.

1. Apple Developer kurulumu

1.1 App ID ve Push Notifications

  1. developer.apple.com → Identifiers sayfasında uygulamanızın App ID'sini açın (yoksa oluşturun).
  2. Capabilities listesinde Push Notifications kutusunu işaretleyip kaydedin.
  3. Provisioning profile'larınızı yeniden oluşturun (Xcode "Automatically manage signing" kullanıyorsanız otomatik yapılır).

1.2 APNs Auth Key (.p8)

  1. developer.apple.com → Keys → + ile yeni anahtar oluşturun.
  2. Apple Push Notifications service (APNs) seçeneğini işaretleyin.
  3. .p8 dosyasını indirin (yalnızca bir kez indirilebilir, güvenli saklayın). Key ID ve hesabınızın Team ID değerini not edin.

Tek bir .p8 anahtarı hesabınızdaki tüm uygulamalar için ve hem sandbox hem production ortamında kullanılabilir. Eski .p12 sertifikalarına gerek yoktur.

1.3 Expo projesi

  1. expo.dev üzerinde bir proje oluşturun ve proje ID'sini (UUID) not edin.
  2. expo.dev → Proje → Credentials → iOS → Bundle ID'nizi ekleyin → Push Key bölümüne .p8 dosyasını, Key ID ve Team ID ile yükleyin.

1.4 NotiPilot paneli

Uygulamanızı ekleyin: Expo Project ID = Expo proje ID'si, Expo Access Token = expo.dev → Access Tokens, iOS Bundle ID = uygulamanızın bundle identifier'ı.

2. Xcode ayarları

Target → Signing & Capabilities:

  1. + Capability → Push Notifications
  2. + Capability → Background Modes → Remote notifications

Bu işlem .entitlements dosyanıza aps-environment anahtarını ekler. Debug build'lerde değer development (APNs sandbox), TestFlight/App Store build'lerinde production olur.

3. Objective-C kodu

3.1 NotiPilot.h

Objective-C
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NotiPilot : NSObject

+ (instancetype)shared;

/// application:didRegisterForRemoteNotificationsWithDeviceToken: içinde çağırın.
- (void)registerDeviceWithAPNsToken:(NSData *)deviceToken
                    extraAttributes:(nullable NSDictionary<NSString *, id> *)extra
                         completion:(nullable void (^)(NSDictionary *_Nullable response, NSError *_Nullable error))completion;

/// Kullanıcı giriş yaptığında çağırın.
- (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

Objective-C
#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   = @"EXPO-PROJE-ID-NIZ"; // Paneldeki "Expo Project ID"
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 dönüşümü

- (void)exchangeAPNsToken:(NSString *)hexToken completion:(void (^)(NSString *, NSError *))completion {
#if DEBUG
    BOOL development = YES;  // Debug build'ler APNs sandbox kullanır
#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

Objective-C
#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);
}

// Uygulama ön plandayken bildirimi göster
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    if (@available(iOS 14.0, *)) {
        completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionList | UNNotificationPresentationOptionSound);
    } else {
        completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
    }
}

// Bildirime tıklandı — panelden gönderilen özel veri `body` anahtarı altında gelir
- (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"];
    // screen'e göre yönlendirme yapın
    completionHandler();
}

@end

Kullanıcı giriş yaptığında:

Objective-C
[[NotiPilot shared] identifyWithExternalId:user.userId attributes:nil completion:nil];

Kontrol listesi

  • App ID'de Push Notifications capability'si açık
  • APNs .p8 anahtarı Key ID + Team ID ile Expo projesine yüklendi
  • Paneldeki Expo Project ID = kAppId, iOS Bundle ID = uygulamanın bundle identifier'ı
  • Xcode'da Push Notifications + Background Modes → Remote notifications açık
  • Debug build'de development = YES, TestFlight/App Store'da NO
  • country ve mümkünse city attributes olarak gönderiliyor

Sık karşılaşılan sorunlar

Belirti Çözüm
didRegisterForRemoteNotifications hiç çağrılmıyor Push Notifications capability'si ve provisioning profile'ı kontrol edin; fiziksel cihaz kullanın.
no valid "aps-environment" entitlement hatası Capability eklendikten sonra provisioning profile yenilenmemiş.
Token dönüşümü 4xx dönüyor projectId ve appId (bundle ID) değerlerini kontrol edin.
TestFlight'ta bildirim gelmiyor, debug'da geliyor (veya tersi) development değeri yanlış ortamı gösteriyor.
404 unknown_app app_id panelde kayıtlı değil.