Push Notification

이 문서는 ChannelIO iOS SDK(이하 SDK)의 Push Notification 에 대해 서술한 문서입니다.

APNs 인증 정보 설정하기

Step 1. Push Notification 키 생성 (Key, Key ID)

📘

이미 앱에서 푸시 알림을 제공하고 있다면, 아래 과정을 생략합니다.

  1. Certificates, Identifiers & Profiles > Keys 페이지에서, Apple Push Notifications service 가 활성화된 키를 발급합니다.

  1. Key ID를 확인합니다. 발급된 인증 키는 한 번만 다운로드할 수 있으므로 안전한 위치에 저장합니다.

  1. Account > Membership 페이지에서 Team ID를 확인합니다.

Step 2. 채널톡에 APNs Auth Key(.p8) 등록하기

  1. 채널톡(PC)를 실행하고, 설정 > 보안 및 개발 > 모바일 SDK 푸시 섹션으로 이동합니다.

  1. 위에서 다운로드한 Key file을 업로드하고, Key ID, Bundle ID, Team ID를 입력합니다.

Step 3. 채널톡에 Device token 등록하기

initPushToken을 이용하여 채널톡에 푸시를 받을 기기의 device token을 등록합니다.

아래 예시를 참고하여, AppDelegate.swiftapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:) 메서드에 ChannelIO.initPushToken(deviceToken: deviceToken) 를 추가합니다.

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  ...
  ChannelIO.initPushToken(deviceToken: deviceToken)
  ...
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [ChannelIO initPushTokenWithDeviceToken:deviceToken];
}

채널톡 알림 이용하기

채널톡 알림 저장하기

백그라운드 상태에서 채널톡 푸시가 도착한 경우, 알림 내용을 저장하여 이후 앱이 포그라운드 상태가 될 때 저장된 내용을 보여줄 수 있습니다.

아래는, 푸시 알림을 탭했을 때, 채널톡 알림인 경우 푸시를 받았음을 알리고, 알림 내용을 저장하는 예시입니다.

func userNotificationCenter(_ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
) {
    let userInfo = response.notification.request.content.userInfo
    if ChannelIO.isChannelPushNotification(userInfo) {
        ChannelIO.receivePushNotification(userInfo)
        ChannelIO.storePushNotification(userInfo)
    }
    completionHandler()
}
//iOS 10 and above
- (void)userNotificationCenter:(UNUserNotificationCenter *)center 
  didReceiveNotificationResponse:(UNNotificationResponse *)response  
  withCompletionHandler:(void (^)())completionHandler {
  NSDictionary *userInfo = response.notification.request.content.userInfo;
	if ([ChannelIO isChannelPushNotification:userInfo]) {
		[ChannelIO receivePushNotification:userInfo completion: nil];
		[ChannelIO storePushNotification: userInfo];
	}
	completionHandler();
}

채널톡 채팅 열기

storePushNotification 을 통해 채널톡 푸시 알림 저장에 성공한 경우, openStoredPushNotification 함수를 통해 저장된 푸시 메시지를 담은 채팅을 열 수 있습니다.

이어지는 예시를 채널톡 채팅을 열기를 원하는 ViewController 에 추가해 주세요.

class ViewController : UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    if ChannelIO.hasStoredPushNotification() {
      ChannelIO.openStoredPushNotification()
    }
  }
}
@implementation ViewController

- (void)viewDidLoad { 
	[super viewDidLoad]; 

	if ([ChannelIO hasStoredPushNotification]) {
		[ChannelIO openStoredPushNotification]
	}
}

@end

팔로업 기능과 결합하기

푸시 알림과 SMS가 모두 도착하는 경우를 방지하기 위해, 채널에 푸시가 정상적으로 도착함을 알려야 합니다.

Signing & Capabilities 탭으로 이동하여, +Capability 버튼을 눌러 Background Mode를 추가합니다.
Background fetchRemote notification을 체크합니다. 아래 사진을 참고합니다.

이후, 아래의 예시와 같이 application:didReceiveRemoteNotification delegate 메서드에 아래와 같이 receivePushNotification, storePushNotification 를 추가합니다.

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable : Any],
  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
  ) {
    if ChannelIO.isChannelPushNotification(userInfo) {
        // This line
        ChannelIO.receivePushNotification(userInfo)
        ChannelIO.storePushNotification(userInfo)
    }
    completionHandler()
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
	if ([ChannelIO isChannelPushNotification:userInfo]) {
		[ChannelIO receivePushNotification:userInfo completion: ^{
			completionHandler(UIBackgroundFetchResultNoData);
		}];
		[ChannelIO storePushNotification: userInfo];
	} else {
		completionHandler(UIBackgroundFetchResultNoData);
	}
}

백그라운드 또는 앱 종료 상태에서도 정상적으로 receivePushNotification을 보낼 수 있도록 Notification Extension을 참고하여 Extension을 추가합니다.

Notification Extension

이 단락은 SDK의 Notification Extension에 대하여 서술합니다. 이 Extension은 앱의 background 또는 terminated 상태에서도 채널톡 푸시에 대한 응답을 정상적으로 보낼 수 있도록 합니다.

채널톡은 이 Extension을 추가하기를 권장합니다. 만약 이를 추가하지 않은 경우에는, 팔로업 문자와 푸시 알림이 모두 도착할 수 있습니다.

Step 1. Extension Setup

  1. Xcode의 메뉴에서, File > New > Target… 탭을 선택합니다.
  2. 새로운 타겟을 고르는 창에서, Notification Service Extension을 선택합니다.

새로운 Target이 생성되고, Swift의 경우 두 개의 파일이 새로 생성됩니다. Objective-C의 경우 세 개의 파일이 새로 생성됩니다.

새로 생성된 파일에 대한 설명은 다음과 같습니다.

  • NotificationService.swift : extension에서 사용하는 코드를 작성합니다.
  • Info.plist : extension Target의 세부 설정을 포함합니다.

Step 2. 패키지 설정

1. CocoaPods

새로 만든 Extension 타겟에 SDK 의존성을 추가합니다. Podfile을 다음과 같이 수정합니다.

target 'your-notification-extention-target-name' do
  pod 'ChannelIOSDK' ~

end

2. Carthage, Swift Package Manager

  1. 새로 만든 타겟의 프로젝트 설정으로 이동합니다.
  2. General 탭으로 이동합니다.
  3. Frameworks and Libraries 항목에 ChannelIOFront 패키지를 추가합니다.

Step 3. NotificationService 추가

아래 예시를 프로젝트 언어 설정에 맞게 추가합니다.

import UserNotifications
import ChannelIOFront

class NotificationService: UNNotificationServiceExtension {
  var contentHandler: ((UNNotificationContent) -> Void)?
  var content: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    self.content        = (request.content.mutableCopy() as? UNMutableNotificationContent)
      
    // this line need to combine sms feature during app terminated
    ChannelIO.receivePushNotification(request.content.userInfo)
      
    if let bca = self.content {
      func save(_ identifier: String, data: Data, options: [AnyHashable: Any]?) -> UNNotificationAttachment? {
        let directory = URL(fileURLWithPath: NSTemporaryDirectory())
          .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true)
        
        do {
          try FileManager.default.createDirectory(
            at: directory, withIntermediateDirectories: true, attributes: nil
          )
          let fileURL = directory.appendingPathComponent(identifier)
          try data.write(to: fileURL, options: [])
          return try UNNotificationAttachment.init(
            identifier: identifier, url: fileURL, options: options
          )
        } catch {
          
        }
        
        return nil
      }
      
      func exitGracefully(_ reason: String = "") {
        let bca = request.content.mutableCopy() as? UNMutableNotificationContent
        contentHandler(bca!)
      }
      
      guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
        return exitGracefully()
      }
      
      let userInfo : [AnyHashable: Any] = request.content.userInfo
      
      guard let attachmentURL = (userInfo["thumbUrl"] ?? userInfo["avatarUrl"]) as? String else {
          return exitGracefully()
        }
      
      guard let imageData = try? Data(contentsOf: URL(string: attachmentURL)!) else {
        return exitGracefully()
      }
      
      guard let attachment = save("\(attachmentURL.hashValue).png", data: imageData, options: nil) else {
        return exitGracefully()
      }
      
      content.attachments = [attachment]
      contentHandler(content.copy() as! UNNotificationContent)
    }
  }
  
  override func serviceExtensionTimeWillExpire() {
    if let contentHandler = contentHandler, let bac = self.content {
        contentHandler(bac)
    }
  }
}
@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];
    
    if (self.bestAttemptContent != nil) {
        UNMutableNotificationContent *content = request.content.mutableCopy;
        NSDictionary *userInfo = request.content.userInfo;
      
        NSString *attachmentURL = userInfo[@"avatarUrl"];
        if (attachmentURL == nil) {
            return [self exitGracefully:request withContentHandler:contentHandler];
        }
      
        NSURL *url = [[NSURL alloc] initWithString:attachmentURL];
        NSData *imageData = [[NSData alloc] initWithContentsOfURL:url];
      
        if (imageData == nil) {
            return [self exitGracefully:request withContentHandler:contentHandler];
        }
      
        UNNotificationAttachment *attachment = [self save:[NSString stringWithFormat:@"%ld.png", attachmentURL.hash] data:imageData options:nil];
        if (attachmentURL == nil) {
            return [self exitGracefully:request withContentHandler:contentHandler];
        }
      
        content.attachments = @[attachment];
        self.contentHandler(content.copy);
    }
}

- (void)exitGracefully:(UNNotificationRequest* )request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    UNMutableNotificationContent *bca = request.content.mutableCopy;
    contentHandler(bca);
}

- (UNNotificationAttachment *)save:(NSString *)identifier data:(NSData *)data options:(NSDictionary *)options {
    NSURL *directory = [[[NSURL alloc] initFileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
                        URLByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString];
  
    NSError *error = [[NSError alloc] init];
    [[NSFileManager defaultManager] createDirectoryAtURL:directory withIntermediateDirectories:YES attributes:nil error:&error];
  
    if (error != nil) {
        return nil;
    }
  
    NSURL *fileURL = [directory URLByAppendingPathComponent:identifier];
    [data writeToURL:fileURL atomically:YES];
    UNNotificationAttachment *ret = [UNNotificationAttachment attachmentWithIdentifier:identifier URL:fileURL options:options error:&error];
    if (error != nil) {
        return nil;
    }
    return ret;
}

- (void)serviceExtensionTimeWillExpire {
    self.contentHandler(self.bestAttemptContent);
}

@end