본문 바로가기
아이폰 개발/ios 개념&튜토리얼

iOS UNNotificationServiceExtension 예제 Push Sound

by 인생여희 2021. 2. 5.

iOS UNNotificationServiceExtension 예제 Push Sound

 

이번 포스팅에서는 백그라운드 모드에서 푸시가 왔을때 소리를 출력 하거나, 푸시 메시지를 미리 가로채서 수정하는법, 또는 push에 image를 넣어서 보내기 기능등을 할 수 있는 방법에 대해서 알아본다.

 

먼저 일반적인 푸시 보내는 예제부터 알아보자.

 

일반적인 푸시 보내기

 

작성 순서는 아래와 같다.

 

1. 라이브러리 추가

Xcode builde phases 에서 두개의 라이브러리를 추가해준다. UserNotification framework, PushKit framework

 

2.라이브러리 import

 AppDelegate.h에서 1번에서 추가한 라이브러리를 import 해준다. 그리고 파이어 베이스 라이브러리도 import 해준다.

#import <UserNotifications/UserNotifications.h>

 

3.UNUserNotificationCenterDelegate 채택

appDelegate.h에서 UNUserNotificationCenterDelegate 프로토콜을 채택한다

@interface AppDelegate : UIResponder <UIApplicationDelegate,UNUserNotificationCenterDelegate>

 

 

AppDelegate.h

 

#import <UIKit/UIKit.h>

//1.UserNotifications 라이브러리 임포트
#import <UserNotifications/UserNotifications.h>


@interface AppDelegate : UIResponder <UIApplicationDelegate,UNUserNotificationCenterDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

 

 

AppDelegate.m

 

1.didFinishLaunchingWithOptions  메소드에서 하는 일

 

- currentNotificationCenter delegate 설정

- UNAuthorizationOptions 설정

- currentNotificationCenter requestAuthorizationWithOptions 권한 설정

- APNS 서버에 기기 등록 [application registerForRemoteNotifications];

 

 

2.푸시 토근 관련 메소드

 

APNS 등록이 성공하고 DeviceToken을 전달받으면 아래 메서드가 실행된다.

- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken;

 

APNS 서버에 등록 실패

- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {

 

 

3. UNUserNotification Delegate 메소드

 

포그라운드에서 푸쉬 받기 

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler

 

사용자가 푸시 알림을 누른 후 알림 메시지를 처리

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse*)response withCompletionHandler:(void(^)(void))completionHandler

 

 

#import "AppDelegate.h"

@interface AppDelegate () 

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    NSLog(@"### didFinishLaunchingWithOptions 호출");
    
    
    //푸시 허용여부 질문
    if ([UNUserNotificationCenter class] != nil) {
        
      // iOS 10 or later
      // For iOS 10 display notification (sent via APNS)
      [UNUserNotificationCenter currentNotificationCenter].delegate = self;
     
        //푸쉬 옵션 - 소리 , 뱃지
        UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound ;
     
        //위의 푸시 옵션을 사용하겠냐 질문
        [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions
          completionHandler:^(BOOL granted, NSError * _Nullable error) {
           
            //앱 최초 실행시, 사용자에게 푸쉬 허용을 할건지 아닌지 권한 질문. 허용을 하든 , 거부를 하든 이곳이 실행된다.
            NSLog(@"APNS에 디바이스 알림 등록 시작");
            
            //허용 : 1, 허용안함 : 0
            NSLog(@"granted(푸시허용여부 1:허용 , 0: 거부) : %d" , granted);
            
            //9.APNS에 디바이스를 등록
            //앱 자신이 APNS를 사용하겠다고 iOS와 APNS에게 알림.
            [application registerForRemoteNotifications];
            
          }];
        
    }else {
      // iOS 10 notifications aren't available; fall back to iOS 8-9 notifications.
      UIUserNotificationType allNotificationTypes =
      (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge);
      UIUserNotificationSettings *settings =
      [UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil];
      [application registerUserNotificationSettings:settings];
    }

    return YES;
}



#pragma APPLE PUSH TOKEN DELEGATE METHOD


//APNS 등록이 성공하고 DeviceToken을 전달받으면 아래 메서드가 실행된다.
//NSData 형식의 deviceToken 인자가 전달된다.
- (void)application:(UIApplication *)app
        didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {

    //토큰형식 변경
    NSString *str = [self stringFromDeviceToken:devToken];
    NSLog(@"애플 APNS에서 받은 token: %@", str);
    
    //실제 서비스라면 이 토큰을 사용자 정보와 함께 프로바이더에게 전달하여 관리를 할 필요가 있음.
    //Provider에게 DeviceToken을 전송한다.
    //예) [self sendProviderTokenString:[deviceToken bytes]];
}
 
//APNS 서버에 등록 실패
- (void)application:(UIApplication *)app
        didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
   
    NSLog(@"APNS에 등록 실패 error: %@", err);
}



- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{
    NSLog(@"didReceiveRemoteNotification 2 :  userInfo: %@", userInfo);
}



#pragma UNUserNotification delegate method

//포그라운드에서 푸쉬 받기 - UNUserNotification delegate method
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
  NSDictionary *userInfo = notification.request.content.userInfo;

     NSLog(@"### willPresentNotification 호출 - 푸시 도착");

   //메시지 전체 내용 출력
   NSLog(@"## 메시지 전체 내용 : %@", userInfo);

  // 선호하는 프리젠 테이션 옵션으로 변경
  completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionAlert);
}


//포그라운드든 백그라운드든 유저가 푸쉬를 눌렀을때 발생
//사용자가 표시 알림을 누른 후 알림 메시지를 처리.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void(^)(void))completionHandler {

    NSLog(@"### didReceiveNotificationResponse 호출 - 유저 푸시 클릭");

    NSDictionary *userInfo = response.notification.request.content.userInfo;

    //메시지 전체 내용 출력
    NSLog(@"## 메시지 전체 내용 : %@", userInfo);

    completionHandler();
}



#pragma PUSH TOKEN HELPER METHOD

//ios 13에서 푸쉬 토큰 받는 형식 변경됨.
//참고 : http://blog.daum.net/creazier/15311563
//참고 : https://effectivecode.tistory.com/1211
- (NSString *)stringFromDeviceToken:(NSData *)deviceToken {

    NSUInteger length = deviceToken.length;
    if (length == 0) {
        return nil;
    }
    const unsigned char *buffer = deviceToken.bytes;

    NSMutableString *hexString  = [NSMutableString stringWithCapacity:(length * 2)];

    for (int i = 0; i < length; ++i) {

        [hexString appendFormat:@"%02x", buffer[i]];
    }

    return [hexString copy];

}

@end

 

 

Background PUSH

 

File -> New -> target -> notification service extension 클릭

 

1.UNNotificationServiceExtension 은 별도의 서브 앱이라고 생각하면 된다. (서로 데이터는 UserDefalt 등으로 공유 가능한데, 통신은 불가능하다. 객체는 참조 가능한데, 싱글톤 패턴이라도 각각의 메모리에 생성한다.)

 

2.빌드시 타겟을 기존앱으로 설정하고 실행하면 기존앱만 디버깅 되고, UNNotificationServiceExtension 앱을 타겟으로 놓고 빌드하면 UNNotificationServiceExtension 앱만 디버깅 된다.

 

 

 

UNNotificationServiceExtension

 

 

 

NotificationService.m

 

 

1.아래 didReceiveNotificationRequest 메소드를 직접 태우고 싶으면 빌드 타겟을 PushExtensionApp으로 한다.

 (대신 푸시가 왔을때 클릭하면 푸시 클릭 이벤트는 호출 안된다. 가 아니라 실은 디버깅이 안되고 있는것이다.)

 

2.인증서는 기존앱의 인증서를 사용하면 되고 프로파일만 새로 생성해주면 된다.

 

3.푸시 확장 서비스를 사용하려면 push payload 에서  "mutable-content": 1 값이 존재해야 한다.

 

4.아래 self.bestAttemptContent 객체에 title, subtitle, sound 같은 속성이 있다. request 객체에 접근해서 (request.content.userInfo) Payload 값을 읽어 온 후에 수정한 뒤 self.bestAttemptContent의 속성에 할당해 주면 된다. 

 

5. push sound 는 아래와 같이 구현 가능하다.

 

6.UNNotificationServiceExtension info plist 에 key를  UILocalNotificationDefaultSoundName 하고 value를 파일명 즉, sound.caf라고 작성해 줘야한다. 그리고 푸시 알림 설정할때 UNAuthorizationOptionSound 설정도 해주자.

 

UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound ;

 

#import "NotificationService.h"

@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];
    

    UNNotificationSound* sound = [UNNotificationSound soundNamed:@"sound.caf"];
    self.bestAttemptContent.sound = sound;
    
    
    self.contentHandler(self.bestAttemptContent);
    
   
}

- (void)serviceExtensionTimeWillExpire {

    self.contentHandler(self.bestAttemptContent);
    
}

@end

 

 

AudioSession

 

오디오 세션 카테고리 설정과 세션 활성화는 앱 시작할때 didFinishLaunchingWithOptions 에서 해주는 것이 좋다.

 

    //백그라운드 소리 안남
    //[[AVAudioSession sharedInstance] setCategory:AVAudioSessionModeDefault error:NULL];
    
    //백그라운드 소리 안남
    //[[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryAmbient error:nil];
    
    //백그라운드
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
                                withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDuckOthers
                                error:&error];
  
    if (error) {
        NSLog(@"ERROR: setCategory %@", [error localizedDescription]);
    }
               
    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
            NSLog(@"오디오 세션 활성화 할 수 없습니다. : %@" , error.localizedDescription);
    }else{
            NSLog(@"오디오 세션 활성화 성공");
    }

 

 

백그라운드 모드에서 소리를 재생할려면 Remote notifications 와 Audio, AirPlay, and Picture in Picture 체크 해주자.

 

 

 

 

 

자료

 

drive.google.com/file/d/1nfjc_XBsajpeVsDka1V9omq5X754kt56/view?usp=sharing

 

 

 푸시 확장 기능 참고

 

https://www.logisticinfotech.com/blog/ios-push-notification-with-notification-service-extension/

https://stackoverflow.com/questions/41845900/display-image-in-notification-bar-with-remote-notificationrich-media-push-notif

https://stackoverflow.com/questions/39673707/ios-10-rich-media-push-notification-media-attachment-in-objective-c