[iOS] Push Notification – 앱 삭제 여부 판별이 가능한가?

‘앱이 Push Notification(푸쉬 알림)을 정상적으로 수신 가능한지 서버에서 판별할 수 있을까?’ 란 의문이 들었다. (‘앱에서 푸쉬 알림을 정상적으로 수신했느냐?’ 와는 다름)

개발 서버에서 APNS (Apple Push Notification Service)에 푸쉬 알림을 신청하기 전(또는 직후)에 디바이스로의 전달여부를 파악할 수 있다면, 실패시 사용자에게 다른 액션을 취할 수 있을 것이다.

APNS가 전송을 100% 보장하지 않음을 알고 있었지만, 혹시나 하는 맘에 iOS Developer Library를 다시 한번 들춰 보았다.

검토한 결론은 아래와 같다.

  • 앱이 삭제된 경우는 확인할 수 있다. (Feedback 서비스)
  • 앱에서 Push Notification 수신을 허가한 후, 사용자가 설정에서 알림 받기 거부로 설정한 경우는.. 명확치 않다. (Feedback 서비스를 통해 식별 가능한지 테스트가 필요하다. 안될 것 같긴 하다. )
  • 실제 디바이스에 푸쉬 알림이 도착했는지에 대한 여부는 알 수 없다.
  • 디바이스가 장시간 꺼져 있거나 잠시 꺼져있는 동안 여러 개의 푸쉬 알림이 온 경우,  알림이 유실될 수 있다.

이와 관련한 iOS Developer Library 문서 내용을 이번 포스팅에서 정리한다. (서버를 이용한 실제 테스트는 차후 해볼 예정이다.)

Push Notification

기본적으로 Push Notification 은 단방향 서비스이다. 개발 서버에서 APNS 에 푸쉬 알림을 요청하면 APNS가 알아서 디바이스에 전송한다. 개발 서버는 APNS에 요청하면 할 일은 끝이다.

 

< from iOS Developer Library >

개발 서버는 APNS 에 푸쉬 알림을 요청할 때, 데이터 format 등에 대한 정상 여부는 리턴 받지만 실제 도착여부에 대해서는 리턴을 받지 못한다.

앞에서도 언급했듯이 APNS 는 100% 전송을 보장하지 않는다. 개발문서에서 기술하고 있는 유실 케이스는 디바이스 전원이 꺼져있는 경우다. APNS의 QoS (Quality of Service) 컴포넌트는 디바이스 전원이 꺼져있는 경우, Notification을 잠시 저장했다가 다시 사용 가능해질 때 Notification이 전달되도록 하지만, 다음의 경우 저장된 Notification이 폐기된다.

  • 잠시 꺼져 있는 경우(얼마나 잠시인지 명시되지 않았음) , APNS는 Notification을 저장을 하는데 저장 가능한 Notificaiton은 하나다. 디바이스가 꺼졌있는 동안 하나의 앱에서 여러 개의 푸쉬 알림을 보낸다면, 제일 나중에 보내진 Notification만 저장되어 전달된다. 이전에 저장된 Notification 은 폐기된다.
  • 장시간 꺼져 있는 경우 (장시간이 얼마나인지는 명시되지 않았음), 저장된 Notification은 폐기된다.

개인적으로 아이폰 베터리가 다되어 충전 후 다시 켰을 때, 하나의 앱에서 여러 개의 푸쉬가 전달되는 경험을 한 것 같다. 애플 내부에서 개선이 되고 있는데 여전히 100% 장담이 안되어 개발 문서를 업데이트 하지 않은 건지, 아니면 본인의 착각인지 이것도 테스트 해봐야 겠다.

Feedback Service

The feedback service gives providers information about notifications that could not be delivered—for example, because the target app is no longer installed on that device. For more information, see “The Feedback Service.”

위 개발 문서의 내용과 같이 Feedback 서비스는 Notification 전달 실패 정보를 개발 서버에 알려주기 위한 서비스다. APNS는 Feedback 서비스를 이용한다. (Feedback 서비스를 통해 불필요한 푸쉬 요청을 피하고 전반적인 Performance 향상을 얻을 수 있다.)

푸쉬 알림이 앱이 삭제된 이유로 전달되지 못했다면 , APNS 는 이를 Feedback 서비스에 알리고 Feedback 서비스는 실패한 디바이스의 토큰을 리스트에 저장한다. APNS 에서 푸쉬 알림을 디바이스로 전송하기 전에 실패한 – data format error 등  – 경우에는 Feedback 서비스의 리스트에 등록되지 않는다. (APNS는 실제로 디바이스에 Notification을 전달해보고 앱의 삭제 여부를 파악하는 듯하다.)

몇 번이고 확인해봤지만, 사용자가 Push 접근을 허락한 이후 설정에서 이 기능을 disabled 한 케이스에 대해선 언급이 없다. 실제로 ‘알림센터에서 보기’를 끄더라도 알림 타입이 ‘None’이 아닌 다른 것 – 배너 등- 으로 선택되어 있다면 푸쉬 알림을 받는다. 앱의 푸쉬 알림에 대한 모든 설정을 전부 Off 로 해놔야 특정 앱에 대한 푸쉬 알림을 받지 못하는데, 이 경우에 Feedback 리스트에 등재가 될지 테스트를 해봐야겠다.

개발 서버는 APNS 연결과는 별개로 Feedback 서비스에 접속해서 디바이스 토큰 리스트를 요청해야 한다. Feedback 서비스는 리스트가 요청될 때마다 리스트를 전달한 후 리스트를 초기화(clear) 한다.

Feedback 서비스로부터 받은 리스트의 데이터에는 timestamp 정보가 포함되어 있다. 이를 이용하여 리스트에 있는 디바이스 토큰이 Push Service에 재등록되어 있는지를 확인할 수 있다. (푸쉬 알림을 보낸 시각과 디바이스 토큰의 timestamp 를 비교하면 디바이스 토큰의 재등록 여부를 확인할 수 있을 것 같다.)

< Binary format of a feedback, from iOS Developer Library >

Feedback 서비스로의 요청을 위한 url, port 및 format 등의 내용은 The Feedback Service를 참고하자.

개발 문서에는 적어도 하루에 한번 Feedback 서비스를 확인하라고 나왔는데, 서비스마다 Push Notification 이용처가 다르니 필요에 따라/용도에 맞게 확인 주기를 결정해야 할 것 같다.

삭제된 앱이 삭제되기 전 디바이스에 남은 최후의 push-enabled 앱이였다면, 앱이 삭제되는 순간 디바이스와 push service 간의 커넥션이 끊겨버려 Feedback 서비스에 추가되지 않는다고 한다. (아마 개발폰일 경우에나 간혹 발생할 일일 것 같다.)

마무으리

몰랐던 내용을 알아서 좋긴 한데, 찾고 있던 답은 아닌 것 같다. 좀 더 고민해보고 테스트해서 결론을 정리하자.

* Reference

 

[iOS] Permission Alert View 에 custom 메시지 추가하기

iOS App이 사용자의 사진, 캘린더, Push Notification, 연락처 등 개인정보에 접근 시도할 때,  System은 AlertView를 띄어 사용자에게 접근 권한을 부여할 것인지 묻는다.

이 때 보여지는 AlertView 메시지는 context 정보가 없기 때문에 사용자의 허가를 득하기가 쉽지 않다.

그래서 요즘은 권한 부여 AlertView를 띄우기 전에 접근이 필요한 이유를  사용자에게 먼저 알려주는 방법을 사용함으로써 이러한 장벽을 낮추려는 시도가 많다.

오늘 이와 관련한 글  The Right Way to Ask Users for iOS Permissions 을 읽다가 한가지 놀라운 – 나만 몰랐으니 나만 놀란걸 수도 – 사실을 발견하였다. (링크된 블로그 글도 꼭 읽어보길 바란다.)

글에 첨부된 이미지들을 살펴보니 System에서 관장하는 접근 권한 AlertView에 App에 특화된 접근 목적이 기술되고 있는 것 아닌가.

실제 구현 방법을 조사해보니 너무 간단했다. 간단히 방법을 알아보자. (너무 간단해서 개인 블로그 대기 리스트에 넣지 않고 바로 쓴다.)

구현 

iOS 는 info.plist의 특정 키값을 통해 접근 권한 AlertView에 메시지를 추가 할 수 있게 한다. 즉 개발자가 할 일은 info.plist에 키 값을 넣고 메시지를 넣으면 끝이다. (다국어를 지원한다면 Localized 작업 필요)

iOS Developer Library 의 Information Property List Key Reference 를 보면 접근 권한 메시지에 해당하는 키값을 확인할 수 있다. Xcode의 editor를 이용한다면 “Privacy – ” 로 시작하는 키값을 찾으면 된다. Key 값에 따라 지원하는 iOS 버전도 다르니 Refernce를 확인하자. (애석하게도 Push Notification 에 대한 키값은 보이질 않는다.)

< From : iOS Developer Library >

간단한 테스트 앱을 만들어 확인하였다. Info.plist 에 키값을 추가 하기 전의 접근 권한 알림창 – 기본 접근 권한 알림창 – 은 아래와 같다.

< 메시지 적용 전 접근 알림 >

아래와 같이 info.plist에 키값을  추가하였다. (사진과 연락처)

< info.plist 화면 in Xcode >

앱을 실행시키면 기존 접근 권한 알림과 달리 info.plist에 설정한 메시지가 AlertView에 추가된 것을 확인할 수 있다.

< 사진 라이브러리 접근 알림 창 >

< 연락처 접근 권한 알림 창 >

 

App에서 접근하려고 하는 사용자의 데이터가 App 동작의 핵심 역할을 한다면, 접근이 필요한 이유를 명시함으로써 사용자의 행동을 좀 더 쉽게 유도할 수 있을 것이다.

본인은 위 내용을 현재 작업 중인, 아니 현재 뿐 아니라 앞으로 작업할 서비스에도 적용할 생각이다.

iOS6, iOS7에서 사용자의 화면캡쳐(Screenshot) 감지하기

불현듯 Snapchat이 사용자의 화면 캡쳐를 어떻게 감지하는지 궁금해졌다.  구글링 및 간단한 데모를 통해 알아본 바를 정리한다.

iOS 디바이스는 홈 + 전원 버튼을 같이 누름으로써 디바이스에 보여지고 있는 화면을 사진으로 앨범에 저장할 수 있다.

사용자의 이러한 행동을 코드 내에서 감지할 수 있는 방법은 iOS 7 전후가 다르다.

iOS 7 이전 버전에서는 사용자가 스크린 캡쳐를 하면 화면의 touch 행위가 취소된다. touch가 취소되면 touchesCancelled:withEvent: 메소드가 호출되는데 이를 이용하면 사용자의 스크린 캡쳐를 감지할 수 있다.

Snapchat에서 사진을 보려면 화면을 계속 누르고 있어야 한다. 화면을 계속 누르는 행위를 하고 있기 때문에, 사진을 보는 중에 스크린 캡쳐가 발생하면 touch 취소 이벤트가 발생하게 된다.

iOS 7에서 사용자의 스크린 캡쳐는 touch에 아무런 영향을 미치지 않도록 바뀌었다. 따라서 위에서 설명한 방식으로는 스크린 캡쳐를 감지할 수 없다. 대신 Apple은 UIApplicationUserDidTakeScreenshotNotification을 별도로 정의하여 좀 더 명확하게 사용자의 스크린 캡쳐를 감지할 수 있도록 하였다.

UIApplicationUserDidTakeScreenshotNotification

Posted when the user presses the Home and Lock buttons to take a screenshot.

This notification does not contain a userInfo dictionary. This notification is posted after the screenshot is taken.

Availability

  • Available in iOS 7.0 and later.

Declared In

UIApplication.h

아래는 확인을 위해 작성한 데모 코드 중 일부다. (싱글뷰 프로젝트를 하나 만들었다. 기본으로 생성되는 UIViewController의 내부 코드다.)

- (void)viewDidLoad{

[super viewDidLoad];

// Do any additional setup after loading the view from its nib.

    // iOS7 판단
    if(floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1){

       // UIApplicationDidTakeScreenshotNotification을 등록, iOS7만 지원
       [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUserTakenScreenShot:) name:UIApplicationUserDidTakeScreenshotNotification object:nil];

    }

}

// NSNotificationCenter에 등록한 selector
// UIApplicationUserDidTakeScreenshotNotification이 발생하면 호출됨
-(void)didUserTakenScreenShot:(NSNotification*)noti{

    // 간단히 alert을 띄움
    [self showScreenShotAlert];

}

-(void)showScreenShotAlert{

    UIAlertView *alert = [[UIAlertView alloc]initWithTitle:nil message:@"User did take screen shot" delegate:nil cancelButtonTitle:@"ok" otherButtonTitles:nil, nil];

    [alert show];

}

// 화면을 누르면 색을 변화시킴
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

    self.view.backgroundColor = [UIColor yellowColor];

}

// iOS7 이전에서 화면을 누르고 있는 중에 스크린 캡쳐가 발생하면 호출됨
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{

    // 색상을 검은색으로 바꿈. 디바이스에 저장되는 사진도 막을 수 있는지 확인해보기 위해 배경 색상을 변경
    self.view.backgroundColor = [UIColor blackColor];

}

// 화면에서 손을 떼면 호출됨.
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{

    // 실수로 손을 뗀 후 스크린 캡쳐를 하게 되는 경우를 확인하기 위해 별도의 색상을 지정.
    self.view.backgroundColor = [UIColor whiteColor];

}

테스트 코드는 간단하다. (iOS6와 iOS7 모두 테스트)

iOS 7에서 스크린 캡쳐를 하면 didUserTakenScreenShot:이 호출되고 alert이 발생한다. touchesCancelled:withEvent:는 호출되지 않는다.

iOS 6에서는 화면을 누르면 배경색이 노란색으로 변경되고, 손을 떼지 않은 상태에서 스크린 캡쳐를 하면 배경색이 검정색으로 바뀌면서 검정색 화면이 앨범에 저장된다. 만약 UIImageView가 있다면, 배경색을 검정색으로 바꾸는 대신 removeFromSuperview를 통해 이미지가 저장되는 것을 막을 수 있다.

iOS 6에서는 이처럼 화면이 저장되기 전의 화면을 컨트롤 할 수 있지만 (테스트 결과 100%는 아닌 것 같다. 가끔 노란색 화면이 앨범에 저장되기도 하였다.) , iOS 7에서는 화면이 그대로 앨범에 저장된다. UIApplicationUserDidTakeScreenshotNotification이 스크린캡쳐 후에 호출되기 때문이다. iOS 7에서 캡쳐되는 화면을 변경하는 방법은 아직 없는 듯 하다. (아직 발견되지 않은 듯 하다.)

* Reference

[iOS] 능력자의 블로그를 통해 되돌아본 ARC

즐겨찾기 해놓은 iOS 개발 블로그를 둘러보다 ARC에 관한 좋은 글을 발견하였다. 글 내용 중 본인이 몰랐던 또는 잊고 있었던 내용 중심으로 일부를 정리한다.  (원문 블로그 링크 )

이전에 본인이 포스팅한  [iOS] Automatic Reference Counting보다 훨씬 실용적인 내용이 많이 담겨져 있으므로 꼭 원문 블로그 링크의 글을 읽어보길 권한다.

Toll-Free Bridging

ARC는 Objective-C의 Object만을 대상으로 한다. Core Foundation과 같은 C/C++ 기반의 코드는  ARC 범주에서 벗어나 있다.

어플리케이션 개발에 Objective-C 클래스만을 이용할 수도 있겠지만, 때에 따라 Core Graphics와 같은 Core Foundation이 필요한 경우도 있다.

앞서 언급했듯이 Core Foundation은 ARC의 범주가 아니므로 Core Foundation API를 사용할 때는 개발자가 수동으로 메모리 관리를 해야한다. 그러나 다행히(?)도 Core Foundation 중 일부 클래스는 Objective-C  클래스로의 타입 캐스팅을 통해 Objectiv-C 객체처럼 사용가능하며 ARC에게 메모리 관리 책임을 맡길 수 있다. (이런 호환 가능한 클래스를 toll-free bridged type이라 한다. 리스트 참고)

Objective- C → Core Foundation이든 그 반대든, toll-free bridged type이 타입 캐스팅을 할 때는 compiler에게 메모리 관리 책임에 관한 정보를 알려줄 필요가 있는데 이때 사용하는 지시어가 __bridge, __bridge_transfer, __bridge_retained 이다.

1) __bridge

Objective-C Object인 NSObject 타입을 잠시동안만 Core Foundation 타입으로 변환하고 싶을 때 사용한다. ARC가 메모리 해지의 책임을 갖고 있다.


-(void)testMethodWithString:(NSString*)text{
...
... CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)text, NULL,  .....);
...
}

위 코드의 CFURLCreateStringByAddingPercenctEscapes 메소드의 두번째 파라미터는 CFStringRef 타입이고 이는 NSString과 호환되는 toll-free bridged type이다.

text는 testMethodWithString: 함수의 파라미터로 strong 성격을 지닌다. 함수의 시작점부터 끝날 때까지는 text 객체는 해지되지 않는 것을 보장받는다. Core Foundation API인 CFURLCreateStringByAddingPercenctEscapes를 사용하면서도 이러한 속성은 유지가 되어야 하며 CFStringRef로 변환되더라도 ARC가 text 객체의 메모리 관리 책임을 갖는다.

2) __bridge_transfer

Core Foundation에서 NSObject로 타입 캐스팅 하는 경우 사용한다. 보통 생성된 Core Foundation 객체는 CFRelease()를 이용하여 메모리 해지를 해줘야 하는데, NSObject 타입으로 바뀌었으니 메모리 해지는 ARC가 담당해라 라는 의미다.


NSString *temp = (__bridge_transfer NSString*)CFURLCreateStringByAddingEscapes(...);

CFURLCreateStringByAddingEscapes(…); 메소드는  CFStringRef타입을 반환한다. CFStringRef는 NSString로 타입 변환이 가능하며, 보통 NSString으로 다루는 것이 개발자에게 편리하다. NSString 타입으로 변환하여 반환하는데 이때 ARC에게 메모리 관리 책임은 이제 너라고 알려주는 역할을 __bridge_transfer가 한다.

CFBridgingRelease()라는 helper 함수가 존재하는데 위 코드는  NSString *temp = CFBridgingRelease(CFURLCreateStringByAddingEscapes(…))과 동일하다.

CFBridgingRelease라는 이름은 CFURLCreate…로 Core Foundation 객체를 생성했으니 이와 쌍으로 release 한다는 의미를 가진다. 보통 Create, Copy, Retain으로 명명된 Core Foundation 함수를 호출할 때 생성 객체가 toll-free bridged type이면 CFBridgingRelease()를 사용하면 된다.

3) __bridge_retained

NSObject 타입을 Core Foundation타입으로 캐스팅하는 경우 사용한다.  __bridge와 다른 것은 메모리 해지의 책임이 ARC로 유지되는 것이 아니라 ARC의 책임이 아니게 된다. 즉 Core Foundation 타입으로 변환된 객체에 대한 메모리 해지의 책임은 개발자에게 있으며 CFRelease()를 통해 수동으로 관리를 해야 한다.

...
CFStringRef s2 = (__bridge_retained CFStringRef)s1; // s1은 NSString 타입
...

CFRelease(s2); //명시적으로 메모리 해지 필요

CFBridgingRetain()이라는 helper 함수가 존재하는데 위 코드는 CFStringRef s2 = CFBridgingRetain(s1); 과 동일한 의미를 가진다. CFRelease()와 쌍을 맞추기 위해 Retain()이 들어갔다고 이해하면 된다.

__bridge는 Core Foundation간의 변환에만 사용되지는 않는다. 어떤 API는 void* 를 파라미터로 갖는 경우도 있다. void* 파라미터에는 Objective-C object, Core Foundation object등 어떠한 타입도 들어갈 수 있다는 의미다. (Objective-C 내에서 사용되는 id 타입과 헷갈리지 말자.)

// objective-c => void*
MyClass *myObject = [[MyClass alloc] init];
[UIView beginAnimations:nil context:(__bridge void*)myObject];
...

// *void -> objective-c
-(void)animationDidStart:(NSString*)animationid context:(void*)context{
...
MyClass *myObject = (__bridge MyClass*)context;
...

}

Strong

  • strong 속성을 지닌 property와 Instance variable을 사용할 경우, 변수가 더 이상 필요하지 않는 시점에 변수를 nil로 할당하는 것이 좋다. (개인적으로 자주 까먹는다. nil처리를 하지 않았다고 어플이 쉽게 죽거나 하진 않기 때문).
  • UIViewController 안에 strong 변수가 있는 경우 viewDidUnload() 메소드에서 nil 처리를 하면 UIViewController가 deallocated 되기 전에 불필요한 메모리 사용을 막을 수 있다.

read-only 속성

Property로 선언된 변수인 경우 self.name  같이 항상 Property 방식으로 접근하는 것이 좋다. property로 선언된 변수를 property’s backing instance ( .m 파일에서 프로퍼티 변수를 _name과 같은 형식으로 접근할 수 있다.)로 접근하는 경우는  init 메소드 또는 custom getter/setter 메소드로 한정하라.

@property (nonatomic, strong, readonly) NSNumber *temperature;

헤더파일에 위와 같이 readonly로 정의된 property인 경우, @implentation 에서 _temperature를 통하여 값을 변경한다면 ARC는 이상한 버그를 낼 수도 있다.

이 경우 좋은 솔루션은 header 파일의 선언은 그대로 두고 .m 파일에서 class extension을 이용하여 @property(nonatomic, strong, readwrite) NSNumber *temperature 로 재정의 하여 사용하는 것이다.

IBOutlet 

  • IBOutlet property에 권장하는 속성은 weak이다.
  • nib 파일에서 최상위 view(보통 self.view)가 IBOutlet으로 정의한 view를 소유하고 있으므로 weak 지시어로 생성하여도 무방하다.
  • IBOutlet을 weak으로 선언하는 가장 큰 이점은 viewDidUnload()메소드에서 nil처리를 하지 않아도 된다는 점이다. (최상위 view가 파괴될 때 자동으로 nil 처리가 된다. viewDidUnload시점엔  nil 이 되어 있다.)
  • iOS 디바이스가 low-memory warning을 받으면 view가 unload되는데 이때 weak으로 처리해야 IBOutlet에 연결된 view도 자동으로 해제된다.

그 밖의 정보 & Tip

  • LLVM 3.0 컴파일러를 도입하면서 .m 파일 (@implementation 내부)에 instance variable 설정이 가능해졌다.
  • MRC 에서 개발자가 @property(nonatomic, retain)을 즐겨 사용하는 이유는 메모리 관리를 편하게 하기 위해서였다. 프로퍼티에 retain 키워드를 넣음으로써 변수 할당때마다 retain (or copy) 키워드를 넣을 필요가 없다. ARC에서 이는 더이상 특별한 이점이 아니다. ARC에선 다른 클래스에서 사용할 경우에만 property를 이용하는 것이 좋다.
  • copy property : NSString 또는 NSArray에 copy property를 사용하는 경우가 많다. 이는 프로퍼티에 할당하더라도 기존의 값이 변경되지 않길 바라는 경우에 사용한다. copy를 하면 할당된 객체와 동일한 객체를 새로 만들어서 변수에 할당한다. copy는 strong과 동일한 성격을 지닌다.

== updated 2013/7/25  ==

확인해보니 iOS 6.0부터 viewDidUnload 메소드가 deprecated 되었다. iOS 6 부터는 low-memory 상태에서 viewDidUnload가 호출되지 않는다.

< from iOS developer library >

위에서 정리된 내용 중 viewDidUnload()가 언급된 곳은 IBOutlet 과 strong property 두 곳이다.

  •  IBOutlet : 권장 프로퍼티가 weak라는 것에는 문제는 없어 보인다. low-memory warning과 별개로 nib파일의 최상위 view에서 이미 IBOutlet으로 선언한 view를 소유하고 있으므로, UIViewController에서 다시 strong 속성으로 view를 소유하는 것은 효율적이지 않아 보인다.
  • strong 변수의 nil 처리 : viewDidUnload는 low-memory warning에서만 호출되도록 되어 있다. 그러나 더이상 low-memory 상태에서 호출되지 않으므로 didReceiveMemoryWarning()에서 현재 뷰에 사용되지 않는 strong 변수에 대하여 nil처리를 하여야 한다. strong 변수를 property로 선언하고 getter메소드에 nil인 경우를 처리해 놓는다면, didReceiveMemoryWarning()에서 nil로 할당된 변수가 다음에 사용될 경우 간편하게 처리될 수 있을 것 같다.

UIScrollView 컨텐츠 영역 화면 캡쳐하기

Bitmap image context(이전 포스팅 참조)를 활용한 화면 캡쳐를 해보자. 단순 화면 캡쳐가 아닌 UIScrollView내의 컨텐츠를 이미지로 캡쳐하여 저장한다.

테스트를 위해 화면 아래로 스크롤이 가능하도록 20개의 row를 가진 UITableView 하나를 만든다. UITableView는 UIScrollView를 상속하고 있어 별도의 UIScrollView를 만들지 않았다.(구현 모습은 아래 그림 참고)

테이블 row 를 클릭하면 UIScrollView영역, 즉 20개의 row가 담긴 이미지가 앨범에 저장되도록 한다.

test project

바로 코드를 통해 확인해보자.

// import QuartzCore
#import <QuartzCore/QuartzCore.h>

- (void)viewDidLoad{

    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    _tableView.delegate = self;
    _tableView.dataSource = self;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

    // scroll이 생기도록 20개의 row를 생성
    return  20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    // 간단하게 row를 출력하는 UITableViewCell 생성
    UITableViewCell *cell = nil;
    cell = [tableView dequeueReusableCellWithIdentifier:@"test"];
    if(!cell){
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"test"];
    }

    cell.textLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];
    return cell;
}

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

     // capture 이미지 생성
    UIImage* captureImage = [self captureContentsInScrollView:indexPath.row];

    if(!captureImage) return;

    // 사진 앨범에 저장
    UIImageWriteToSavedPhotosAlbum(captureImage, nil, nil, nil);

}

-(UIImage*)captureContentsInScrollView:(int)selectRow{

    // 생성된 이미지를 저장할 변수
    UIImage *capture = nil;

    // UIScrollView의 기존 frame을 저장 (이 예제에선 UITableView가 UIScrollView의 역할)
    CGRect originTableViewFrame = _tableView.frame;

    // capture할 영역을 지정. UIScrollView의 컨텐츠 사이즈
    CGSize captureSize = CGSizeMake(_tableView.contentSize.width, _tableVie.contentSize.height);

    // bitmap graphic context 생성
    UIGraphicsBeginImageContextWithOptions(captureSize, YES, 0.0);

    // UIScrollView의 frame을 content 영역으로 변경
    _tableView.frame = CGRectMake(0, 0, captureSize.width, captureSize.height);

    // UIScrollView frame영역을 bitmap image context에 그림
    [_tableView.layer renderInContext:UIGraphicsGetCurrentContext()];

    //  bitmap graphic context로부터 이미지 획득
    capture = UIGraphicsGetImageFromCurrentImageContext();

    // UIScrollView의 frame을 원래대로 변경 (변경하지 않으면 스크롤이 안됨)
    _tableView.frame = originTableViewFrame;

     // bitmap image context 종료
     UIGraphicsEndImageContext();

    return capture;
}

Simulator로 실행하여 row를 선택하면 다음의 이미지가 앨범에 저장되는 것을 확인할 수 있다.

전체 캡쳐 결과

UIImageWriteToSavedPhotosAlbum()함수는  UIKit에서 지원하는 함수로 디바이스 앨범에 이미지를 저장한다. delegate를 구현하면 이미지 저장 여부를 어플리케이션에서 알 수 있다. (여기선 생략)

위 (UIImage*)captureContentsInScrollView:(int)selectRow 함수에 파라미터로 선택된 row를 전달하고 있다. 코드에서는 selectRow 파라미터가 사용되지 않았으나, 이 파라미터와 cell의 높이를 활용하여 captureSize를 조정하면 선택된 셀까지만 캡쳐되도록 할 수 있다. 아래 그림은 적용한 결과 화면이다.

선택한 row까지만 캡쳐

[iOS] Bitmap Image Context

대부분의 iOS 개발서적은 UIGraphicsBeginImageContext()를 이용한 화면 캡쳐 내용을 포함하고 있다. 그러나 아쉽게도 본인이 봤던 개발서에는 UIGraphicsBeginImageContext()의배경 설명 없이 스킬 위주로만 설명이 되어 있어 이를 활용하는데 어려움이 있었다. 이번 포스팅에서는 Bitmap Image Context에 대해 간단히 정리해본다.

iOS developer library의 Drawing and Creating Images 에 다음과 같은 설명이 있다.

Most of the time, it is fairly straightforward to display images using standard views. However, there are two situations in which you may need to do additional work:

두 번째 항목으로부터  bitmap image context의 용도를 파악할 수 있다.  화면에 바로 나타나지 않는 – offscreen – 이미지를 생성하고 싶을 때는 반드시 bitmap image context를 생성해야 한다.

bitmap image context를 생성하는 함수가 바로 UIGraphicsBeginImageContext()다. 화면 캡쳐를 생각해보면, 화면을 캡쳐한 이미지는 화면에 보이진 않고 내부적으로 생성되어 사진앨범에 저장된다.

UIKit에서 지원하는 UIGraphicsBeginImageContextWithOptions( CGSize size, BOOL opaque, CGFloat scale ) 함수는 iOS 4.0 이후부터 지원되며 각 옵션은 다음을 나타낸다.

  • size : bitmap context 사이즈. 생성될 이미지 사이즈와 연관있다. 
  • opaque : 생성될 이미지의 투명도 여부. 생성할 이미지가 투명도(alpha)를 가지고 있으면 NO, 불투명하면 YES. ( YES 의 퍼포먼스가 더 높다.)
  • scale : 0.0 이면 디바이스의 화면에 맞게 이미지가 결정된다. 개발자가 임의의 값을 넣을 수도 있다.

예를 들어 UIGraphicsBeginImageContextWithOptions(CGSizeMake(100.0,100.0), NO, 2.0) 으로 생성되는 이미지의 크기는 200 x 200으로 size 값에 scale 을 곱한 값이다.

iOS 4.0 이전에 지원했던 UIGraphicsBeginImageContext(CGSize size) 함수는 UIGraphicsBeginImageContextWithOptions 에서 scale = 1.0 인 것으로 레티나 디스플레이에서는 UIGraphicsBeginImageContext()로 생성된 이미지가 만족스럽지 않을 수도 있다.

UIGraphicsBeginImageContext를 이용한 이미지 생성은 보통 다음의 단계를 따른다.

-(void)createBitmapImage{

    // 1. bitmap context를 생성하고 이를 graphics stack에 넣는다.
    UIGraphicsBeginImageContextWithOptions(imageSize, NO, [UIScreen mainScreen].scale);

   // 2. UIKit 또는 Core Graphics 루틴을 활용하여 bitmap context에 이미지를 그린다.
   // ex) [UIImage drawInRect:], [CALayer renderInContext:] 등

    // 3. bitmap context로부터 이미지를 받아온다.
    UIImage *createdImage = UIGraphicsGetImageFromCurrentImageContext();

    // 4. bitmap context를 graphics stack으로부터 빼낸다.(bitmap context를 종료한다.)
    UIGraphicsEndImageContext();
}

(1)번에서 생성하는 bitmap context의 크기 – CGSize parameter 값 – 과 (2)에서 그릴 이미지 크기가 같아야(당연히 비율도 맞아야) 이미지 왜곡이 생기지 않는다. 만약 그릴 이미지 크기가 bitmap 크기보다 크다면 image가 짤린 것처럼 나올 수도 있다.

* Reference : iOS Developer Library – Drawing and Creating Images

순정 UITableView에서 Row개수만큼만 데이터 표시하기

UITableView에 데이터를 보여줄 때 데이터의 개수가 적으면 아래 그림처럼 보여지곤 한다. 데이터가 없음에도 화면에 빈 테이블뷰를 보여주는 것은 좀 구리다.

UITableView default

Row 개수만큼만 보여주고 싶으면 어떻게 해야 할까?

UITableView 및 UITableViewCell를 개발자가 별도로 재정의하여야만 할까? 물론 그래도 되지만 UITableView를 개발자가 구현한다면  Cell 재사용, Scroll View등 신경써야 할 부분이 많다.

UITableViewCell은 화면에 보여지는 데이터와 직접적인 관련이 있어 재정의하여 많이 사용된다. 그러나 UITableView는 UITableViewCell만큼 재정의해서 사용되진 않는다. 순정 UITableView를 이용해도 많은 부분 커버된다.

위 문제도 아주 간단한 방법으로 아래 그림처럼 구현이 가능하다.

UITableView after setting footerview

UITableView 하나로 구성된 테스트 프로젝트를 하나 만들었다. 코드는 아래와 같다.


@implementation ViewController{

    NSArray *_dataArray;
}

- (void)viewDidLoad
{

    [super viewDidLoad];
    // 테스트 데이터
    _dataArray = [NSArray arrayWithObjects:[NSString stringWithFormat:@"Row 1"],
                 [NSString stringWithFormat:@"Row 2"],
                 [NSString stringWithFormat:@"Row 3"], nil];

    _tableView.dataSource = self;
    _tableView.delegate = self;

    // tableFooterView를 정의. 사전에 tableFooterView가 기획되지 않았으므로 작은 영역으로 정의한다.
    UIView *footerView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 320, 10)];

    // footerView가 화면에 보이지 않도록 배경은 투명하게
    footerView.backgroundColor = [UIColor clearColor];

    // tableFooterView 설정
    _tableView.tableFooterView = footerView;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

    return _dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    static NSString *testIdentifer = @"testIdentifier";
    UITableViewCell *cell;

    cell = [tableView dequeueReusableCellWithIdentifier:testIdentifer];

    if(!cell){

       cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:testIdentifer];

    }

    cell.textLabel.text = [_dataArray objectAtIndex:indexPath.row];

    return  cell;
}

코드에서 보는 바와 같이 tableFooterView를 설정함으로써 간단히 구현된다.

UITableView의 배경색을 [UIColor clearColor] 로 설정하고, UITableViewCell을 재정의하여 Cell이 자체의 색상을 갖도록 하면(보통 Cell의 배경색은 UITableView의 배경색을 따름), 데이터가 없는 영역에는 UITableView 바로 아랫단의 뷰(위 예제의 경우 self.view)가 보여져 Row 개수만큼만 UITableView 영역이 설정된 것과 같은 효과를 얻을 수 있다.  (아래 그림)

UITableView with custom UITableViewCell

너무 간단한 방법이라 이렇게 포스팅하는게 우습기도 하지만, 본인이 젤 첨 UITableView를 공부할 때 위와 같은 구현법이 굉장히 궁금했으나 그 방법을 알아내기가 쉽지 않았다. UIView를 이용한 나만의 테이블뷰를 만들어야 하나 생각도 했었다.

UITableView의 tableHeaderView, tableFooterView, sectionHeaderView, sectionFooterView를 이용하면, UITableView만으로 다양한 형태의 화면 구성이 가능하다.

[iOS] Automatic Reference Counting (ARC)

iOS 5부터 Apple이 LLVM Compiler 를 채용함에 따라 ARC (Automatic Reference Counting)가 iOS 개발에 적용되었다.

Java나 C#만을 다룬 상태에서 iOS 4.x 후반대에 iOS 공부를 시작한 본인으로서는 개발 입문 후 곧 소개된 ARC가 큰 힘(?)이 되었다.

ARC 관련하여 개발 서적, iOS developer reference 등을 보았지만 가장 이해하기 쉬운 자료는 WWDC 2011 자료(애석하게도 WWDC 2011 Introduction Automatic Reference Counting 설명 자료는 Apple 개발자 계정이 있어야 볼 수 있음)였다. 이번 포스팅은 WWDC 자료를 바탕으로 ARC 에 대해 정리한다.

ARC 도입 이유

< from WWDC 2011 >

애플은 ARC 도입 이유는 다음과 같다.

  • 앱의 비정상 종료 원인 중 많은 부분이 메모리 문제. 메모리 관리는 애플의 앱 승인 거부(Rejection)의 대다수 원인 중 하나. 
  • 많은 개발자들이 수동적인 (retain/release) 메모리 관리로 힘들어함.
  •  retain/release 로 코드 복잡도가 증가.

결국 개발자가 좀 더 편하게 앱 개발을 할 수 있도록 해주겠단 의미다. 개발자는 객체 관계에 더 집중하면 된단다.

ARC 란?

< from WWDC 2011 >

ARC는 Automatic Reference Counting의 약자로 기존에 수동(MRC라고 함)으로 개발자가 직접 retain/release를 통해 reference counting을 관리해야 하는 부분을 자동으로 해준다.

위 자료를 보면 Object가 노란색으로 강조되어 있다. 실제로 ARC는 Objective-C의 Object만을 대상으로 하며 이로 인해 구조체나 C 기반의 Core Foundation 사용에는 제약이 존재한다. 이에 대해서는 아래에서 다시 언급하도록 하겠다.

< from WWDC 2011 >

ARC는 GC (Garbage Collector)와는 다르게 런타임이 아닌 컴파일 단에서 처리된다.

GC는 런타임에 메모리를 검사하기 때문에 앱 퍼포먼스에 악영향을 준다. 그러나  ARC는 개발자가 직접 코딩하던 retain/release를 컴파일러가 자동으로 코드에 삽입시키므로, 동작시에는 MRC와 동일하여 성능 저하를 유발하지 않는다.

Apple은 iOS SDK 문서에서 차후 Mac OSX에서도 ARC가 GC를 대체할 것이라 밝히고 있다. 애플에서 제공하는 ARC 퍼포먼스 향상 효과는 다음과 같다.

< from WWDC 2011 >

ARC를 위한 규칙

컴파일러가 자동으로 retain/release 코드를 삽입시키기 위해 몇 가지 규칙을 제안한다. 이를 알아보자.

< from WWDC 2011 >

[첫 번째]  retain/release/autorelease 를 이용하지 마라.

더 이상  retain/release/autorelease를 코드에 직접 삽입할 필요가 없다. 컴파일러가 알아서 다 해준다. 블록(blocks)도 Object이므로 기존 블록에 사용하던 copy, autorelease는 불필요하다. 메소드의 return에 대한 autorelease도 고려할 필요 없다. 그냥 짜면 컴파일러가 알아서 해준다.

dealloc 메소드는 사용할 수는 있지만 이는 인스턴스 변수들의 메모리 해제(release)가 아닌 자원 관리 차원에서 허용된다. 또한 개발자에 의해 재정의된 dealloc 메소드에서는 [super dealloc]을 호출해서는 안된다

< from WWDC 2011 >

[두 번째 규칙] C 구조체 내에 Object 타입을 사용하지 마라.

ARC는 Object만을 고려한다. 구조체 내의 Object는 ARC가 계산하기 어렵다. 그냥 구조체가 아닌 Object  (Objective-C Class)를 사용하라.

< from WWDC 2011 >

[세 번째 규칙] Core Foundation은 Objective-C의 Object 타입이 아니므로 Core Foundation과 Object 간의 명시적인 타입 캐스팅이 필요하다.

추가로 Core Foundation 스타일의 객체에는 CFRetain, CFRelease 와 같은 메모리 관리 함수를 사용할 수 있다. 다음을 참조하자 – “Managing Toll-Free Bridging”

< from WWDC 2011 >

[네 번째 규칙] NSAutoreleasePool 사용하지 마라. NSAutoreleasePool은 내부적으로 보면 실제로는 Object가 아니다. 대신 @autoreleasepool{}을 사용하라. 속도도 더 빠르다. @autoreleasepool{}은  loop에 쓰이는 경우가 많다. NSError에도 쓰이지만 자주 사용되지는 않는다. 그래도 경험상 거의 사용할 일이 없는 듯 하다.

새로운 지시어

1) 프로퍼티 속성

아래 예처럼 strong, weak 키워드가 프로퍼티에 사용된다.

// The following declaration is a synonym for: @property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;

/ The following declaration is similar to "@property(assign) MyClass *myObject;"
/ except that if the MyClass instance is deallocated,
// the property value is set to nil instead of remaining as a dangling pointer.

@property(weak) MyClass *myObject;

strong은 retain과 의미가 거의 같다.

weak는 assign 키워드처럼 참조 카운트를 증가시키지 않는다.  그러나 assign과는 다르게 참조하는 객체가 해제(deallocated)되면 weak로 선언된 프로퍼티는 자동으로 nil이 대입된다. assign의 경우엔 nil 이 대입되지 않아 의도치 않은 포인터를 가리킬 수 있었다. assign과 거의 동일한 의미의 지시어는 unsafe_unretained 이다.

ARC는 강한 참조 사이클(strong reference cycle)을 방지하지 못한다. 이를 위해 개발자가 강한 참조 사이클이 발생하지 않도록 약한 참조(weak)를 잘 사용해야 한다. (참고- “Practical Memory Management”)

2) 변수 지시자(variable qualifier)

  • __strong : 디폴트 지시자. 객체의 소유권을 획득한다. (retain count 증가)
  • __weak : 객체의 소유권을 가져오지 않고 참조한다. 참조한 객체가 소멸되면 nil 로 설정된다.
  • __unsafe_unretained : __weak 와 비슷하지만 참조 객체 해제되어도 nil 로 재설정되지 않는다. 변수의 값은 특정 포인터를 계속 가리키고 있으므로 변수 사용에 유의해야 한다. 오류가 발생할 수 있다.
  • __autoreleasing : 보통 메소드의 (id *) 타입 파라미터 인수에 사용하며 함수 리턴시 자동으로 해제된다. Cocoa Framework 의 간편 생성자(Convenience Initializer)역시 __autoreleasing 타입의 객체를 반환함.

__strong, __weak, __autoreleasing 변수는 생성시 자동으로 nil 로 초기화 된다. 변수 선언 형식은 다음과 같다.

ClassName * qualifier variableName;

__weak 를 사용할 경우 아래와 같은 실수를 하지 않도록 주의해야 한다.

NSString * __weak string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];
NSLog(@"string: %@", string);

결과적으로  string 변수는 바로 nil 로 설정된다.

단계적으로 다음과 같이 이해하면 쉽다.

1) 우측 구문이 샐행되어 NSString 객체 생성.

2) NSString 객체가 string 변수에 할당

3) string 변수는 __weak으로 선언. 따라서 기존의 참조 카운트를 그대로 갖는다. 따라서 초기화된 NSString의 참조카운트 0(NSString 객체를 강한 참조로 소유하는 다른 객체 없음)이 그대로 넘어가고, 참조 카운트가 0이므로 weak 특성에 따라 바로 nil 로 재설정.

__autorelease 는 일반적으로 잘 사용되지 않는다.

기타

1) 지원 OS

  • Xcode 4.2
  • Mac OS X v10.6 and v10.7 (64 – bit application)
  • iOS 4 and iOS 5
  • weak 참조는 OS X 10.6 및 iOS 4 에서 미지원. 따라서 weak 참조를 지원하는 완벽한 ARC 기능은 OS X 10.7 및 iOS 5 버전부터

2) NSAllocateObject , NSDeallocateObject 를 사용할 수 없다. alloc을 이용해서 객체를 생성하면, Runtime이 객체 해제(deallocate)를 알아서 처리한다.

3) ARC 는 다음과 같은 메소드 네이밍에 대한 제약을 갖는다.

  • 프로퍼티 접근자(accessor) 이름이 new 로 시작해서는 안된다. new 로 이름을 명명이 가능한 경우는 아래와 같이 getter 의 이름을 다른 이름으로 명시적으로 지정했을 경우이다.
// Won't work:
@property (strong, nonatomic) NSString *newTitle;
// Works:
@property (strong, nonatomic, getter=theNewTitle) NSString *newTitle;

* Reference

[iOS] 로컬 알림 취소 – UILocalNotification 저장 및 취소

이전 포스팅  [iOS] 로컬 알림 (Local Notifications)에 이어 로컬 알림 저장 및  취소에 대해 좀 더 정리해보려 한다.

이전 포스팅에서도 언급 했지만, 알림 취소의 의미는

  • 알림 대기 중인 알림의 취소
  • 이미 도착한 알림의 취소 (iOS 디바이스 알림 센터 목록에서의 삭제)

로 구분된다.

알림 등록에 사용한 UILocalNotification을 저장하지 않고 알림을 취소하는 방법은 아래와 같다. (로컬 알림과 연계된 application delegate 메소드 내용은 이전 포스팅 참고)

// (1) 모든 알림 취소
// 임의의 메소드에서 다음 코드로 가능

[[UIApplication sharedApplication] cancelAllLocalNotifications];

// (2) 특정 알림 취소. Application delegate 파라미터 이용

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

    UILocalNotification *localNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];

    if(localNotif){

        ...
        [application cancelLocalNotification:localNotif];
    }

    ...
    return YES;

}

// (2-2)
-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification{
    ...
    [application cancelLocalNotification:notification];
    ...
}

위 코드 (1) 방법은 앱에 등록된, 삭제되지 않은 모든 알림을 삭제한다. 알림센터에 보여지는 알림도 포함된다. (2) 방법은 도착한 알림만 삭제하여 알림센터에 보이지 않도록 한다.

간단한 알림의 경우 위 방법으로도 충분하다. 사용자가 알림 시각을 변경하면 기존에 등록된 알림을 cancelAllLocalNotifications로 지우고 새로 등록한다. 알림이 도착하면 application delegate에서 알림을 취소하여 확인된 알림이 알림센터에 보이지 않도록 한다.

그럼 어떠한 경우에 UILocalNotification을 저장해야 할까?

  • 어플리케이션에서 사용하는 로컬 알림의 종류가 다양하고 많을 경우, 
  • Application delegate 이외의 다른 이벤트에서 알림센터의 알림 항목을 삭제하고 싶은 경우

앱에서 사용하는 로컬 알림의 종류가 다양하고 – 특정 시간에 보내는 알림, 바로 보내는 알림등 앱 내에서 성격에 따라 구분 – 개수가 많다면, 하나의 알림을 취소하기 위해 cancelAllLocalNotifications으로 모든 알림을 지우고 다시 새로 등록하는 구조는 바람직하지 않다. 취소하고자 하는 알림만 취소하는 것이 효율적이다.

수신한 알림이 사용자가 원할 때까지 – 사용자가 확인한 경우에도 – 알림센터에 남아있길 원하는 경우에도 UILocalNotification을 저장하여 필요 시점에 취소해야 한다.

UILocalNotification 저장 시 NSUserDefaults를 이용하면 편하다. 직접 UILocalNotification 객체를 NSData로 변환 후 파일에 저장할 수 있지만 NSUserDefaults는 이를 쉽게 해준다.

(NSUserDefaults가 UILocalNotification 저장소로 적합한가에 대해서는 검토가 좀 더 필요하다. 본인도 스택오버플로우 검색을 통해 NSUserDefaults를 알았는데, 해당 클래스는 사용자의 앱 설정값 등 간단한 데이터를 저장하는 용도로 만들어졌다. 일단 지금까지 본인이 테스트한 바로는 문제가 발생하지 않았다.)

아래는 NSUserDefaults를 이용하여 UILocalNotification을 저장하고 반환하여 로컬 알림을 취소하는 예제 코드다.

//
// 로컬 알림 등록 메소드에서 UILocalNotification 저장
-(void)fireLocalNotification{

    // UILocalNotification 객체 생성
    UILocalNotification *noti = [[UILocalNotification alloc]init];
    noti.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];
    noti.timeZone = [NSTimeZone systemTimeZone];
    noti.alertBody = @"Just Do It";

    // UILocalNotification 을 NSData로 변환.
    // NSKeyedArchiver 이용
    NSData *notiData = [NSKeyedArchiver archivedDataWithRootObject:noti];

    // NSUserDefaults에 NSData 변환된 UILocalNotification 저장
    // 키값으로 알림메시지 사용 (테스트용)
    [[NSUserDefaults standardUserDefaults] setObject:notiData forKey:noti.alertBody];

    // 어플리케이션에 알림 등록
    [[UIApplication sharedApplication] scheduleLocalNotification:noti];
}

// 로컬 알림 취소
-(void)cancelLocalNotification:(NSString*)key{

    // key 값으로 NSData 타입의 UILocalNotification 획득
    // 파라미터 key는 저장시 등록한 값과 동일 가정
    NSData *dataNoti = [[NSUserDefaults standardUserDefaults] objectForKey:key]

    // NSData 타입을 UILocalNotification으로 변환
    // NSKeyedUnarchiver 이용
    UILocalNotification *notif = [NSKeyedUnarchiver unarchiveObjectWithData:dataNoti];

    // NSUserDefaults에 저장된 UILocalNotification 이용하여 알림 삭제
    [[UIApplication sharedApplication] cancelLocalNotification:notif];
}

NSUserDefaults에 저장할 수 있는 데이터 타입은 Property Lists에 사용되는 타입으로 NSArray, NSDictionary 등 다양하다. 위 코드에서는 UILocalNotification을 NSData 타입으로 저장하고 반환하였지만, 콜렉션 타입을 이용한다면 다른 방법으로도 가능하다. NSUserDefaults Class Reference를 확인하자.

NSObject와 NSData 간의 변환은 NSKeyedArchiver와 NSKeyedUnarchiver를 이용한다.

테스트 중 저장된 UILocalNotification을 이용한 알림 취소가 실패하는 경우를 발견하였다.

  • [UIApplication scheduleLocalNotification:]로 알림 등록 – 정상적으로 취소
  • [UIApplication presentLocalNotificationNow:]로 알림 등록 –  알림 취소 되지 않음 (디바이스 알림센터에서 알림이 삭제되지 않고 보임)

참고로 Application delegate 의 파라미터를 이용한 알림 취소는 위 두 메소드 모두 잘 된다.

iOS SDK를 찾아봤으나 정확한 원인을 밝힐 수  없었다. 두 메소드 간의 어떤 차이점이 이런 현상을 유발했는지 궁금하다.

%d bloggers like this: