[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

 

Advertisements

iOS App States

iOS 앱 개발시 경우 – 네트워크를 통한 다운로드/업로드, Music Player 등 – 에 따라 앱이 Background  – 앱 실행 중 홈버튼을 눌렀을 때와 같은 경우 – 에 있을 때도 코드를 수행시켜야 할 필요가 있다. iOS 4.0 부터는 Multitasking를 지원하여 Background 에서도 코드를 수행시킬 수 있다. 이 기능을 알아보기 전에  iOS 앱이 가지는 5가지 상태에 대해 알아보자.

참고로, iOS 3.2 이전 버전에서는  앱은 Background, Suspended 상태를 갖지 못한다. (iOS 4 라 할지라도 특정 기기 – 2009년 중반 이전의 아이폰, 아이팟 터치 – 들도 그렇다. ) 이러한 디바이스에서 동작하는 앱은 Foreground 를 벗어나면 종료된다. 따라서 Background 에서 수행되는 코드를 개발할 때, 디바이스의 Background 지원 여부를 확인할 필요가 있다. (참고 : Background Execution and Multitasking)

State Changes of iOS App < from iOS Developer Library >

iOS App 은 항상 아래의 5가지 중 하나의 상태를 가진다.

  • Not Running : 앱이 실행되기 전 상태, 또는 실행이 되었지만 시스템에 의해 종료된 상태
  • Inactive : Foreground 로 진입은 했으나 이벤트는 아직 받지 못한 상태. iOS 앱은 다른 두 상태 사이에서 상태변환을 할 때 중간  단계로  짧은 시간동안 InActive 에 머물게 된다.
  • Active :  앱이 화면에 보이고 이벤트도 받으며 실행되는 정상 상태. 일반적인 앱의 실행상태이다.
  • Background : 대부분의 앱은 홈버튼을 눌렀을 때 Suspended  상태가 되기 전 잠시 Background 상태에 머문다. 이 상태에서 앱은 일정 시간 일부 코드를 수행할 수 있지만 사용자의 이벤트를 받을 수는 없다. Background에서 추가적인 코드를 수행할 필요가 있는 경우 Background 상태로 남아 일정 시간동안 코드를 수행할 수 있다. 앱이 실행되자마자 Background 로 진입하는 앱은 Inactive 상태를 거치지 않고 바로 Background  상태로 진입한다.
  • Suspended : 앱 실행이 중단된 상태. Suspended 상태에서는 앱은 메모리에 존재하지만 아무런 코드를 수행하지 않는다. System은 앱을 아무런 통보 없이 Background상태에서 자동으로 Suspended 상태로 바꿀 수 있다. 메모리 부족현상이 발생하면 System은 별다른 통보 없이 Suspended 상태의 앱을 종료할 수 있다.

UIApplication Delegate 는 이러한 상태 전이에 반응할 수 있도록 메소드를 정의해 놓고 있다.

  • applicationDidBecomeActive : Active 상태로 전이 될 때 호출된다. Background 앱이 다시 호출되어 Active 상태가 되도 호출된다.
  • applicationWillResignActive : 앱이 Active 상태를 벗어날 것을 알려준다.(ex. 홈 버튼을 누르면 호출됨) 단, 앱이 Background 상태로 전이된다는 보장은 없다. 이 상황에 잠깐 머물다가 다시 바로 Active 상태로 바뀔 수도 있다.
  • applicationDidEnterBackground : Background로 진입. 언제든 System에 의해 Suspended 상태로 전이될 수 있다. 나중에 재생성할 수 있는 모든 리소스들을 제거하고 사용자 데이터를 저장한는 등의 작업을 수행하는 용도로 사용될 수 있다. 이 메소드 호출 이전에 applicationWillResignActive가 호출된다.
  • applicationWillEnterForeground : Background를 벗어나 Foreground로 진입될 것을 알려준다.(아직 Active 상태는 아님) applicationDidEnterBackground 에서 해제한 리소드들을 재생성하는 용도로 사용할 수 있다.이 메소드 호출 뒤에 applicationDidBecomeActive 가 호출된다.
  • applicationWillTerminate : 앱이 종료될 것임을 알려준다. 앱이 Suspended  상태에서 종료되는 것이라면 호출되지 않는다.

[iOS]2계층 UIViewController의 modalViewController 동시에 없애기(dismiss)

위 그림과 같은 상황을 가정해보자. (유저인터페이스가 적합한지는 별개의 문제다. 일단 가정하자.)

로그인뷰(Login View Controller)에서 사용자의 계정을 확인 후, Navigation Controller 를 Modal View 로서 화면에 나타낸다. Navigation Controller 의 설정 버튼을 통해 설정 화면(Settings View Controller)이 Modal View 로 나타나고 이곳에서 특정 이벤트(사용자 계정 log out )가 발생하면, 최초의 로그인 뷰로 돌아간다.

이 경우,  설정화면의 이벤트(ex. log out)에 의해 설정화면(Settings View Controller)과 Navigation Controller 가 함께 사라지는 효과를 구현해야 한다. 어떻게 하면 될까?

쉽게 생각할 수 있는 방은, 먼저 Settings View Controller 에서 dismissModalViewController 를 호출하고,  순차적으로 delegate 를 이용하여 Navigation Controller 에서 dismissModalViewController 를 호출하는 것이다. (참고로 iOS5 이후부터는 presentModalViewController 및 dismissModalViewController 는 deprecated 된다. 대안으로 presentViewController, dismissViewController 를 사용할 것을 권장한다. 정확한 메소드 사용법은 UIViewController Class Reference참조하라.)

이 방법이 제대로 동작할까? 실제로 해보면 쉽게 정상적으로 동작하지 않는 것을 확인할 수 있다. (본인의 개발환경은 OS X 10.6.8, iOS 4.3, XCode 3.2.6 이다.)

먼저 간단하게 위에서 언급한 방법을 구현한 코드를 보자.

Settings View Controller 내부 메소드

<pre>
// settings View controller 에서 사용자 이벤트가 전될되는 메소드
-(IBAction)didPressDone:(id)sender{</pre>
[self.parentViewController dismissModalViewControllerAnimated:YES]; // dismiss SettingsViewController
[delegate userAccountIsDisconnected]; // delegate 를 통하여 Navigation Controller 측에 이벤트를 알림

}
<pre>

코멘트에서와 같이 먼저 Settings View Controller 자신에 dismissModalViewController 를 적용하고, delegate 를 통하여 Navigation Controller 측에 이벤트를 전달한다. (여기서 delegate 사용법은 다루지 않겠다. 생각보다 어렵지 않고 간단하다.)

delegate 메소드가 구현된 Navigation Controller 측 내부 메소드

<pre>
// delegate 메소드
-(void)userAccountIsDisconnected{</pre>
NSLog(@"delegate method");
[self.naviController.parentViewController dismissModalViewControllerAnimated:YES]; //dismiss Navigation Controller

}
<pre>

위 코드는 Navigation Controller 를 dismiss 하여 로그인뷰를 나타나게 하는 것이 목적이다.

실제 위와 같은 코드로 동작을 시켜보면, Settings View Controller  는 정상적으로 사라지지만, Navigation Controller 화면은 사라지지 않아 로그인뷰가 화면에 나타나지 않는다.

디버그를 해보면 다음과 같은 에러메시지를 확인할 수 있다.

attempt to dismiss modal view controller whose view does not currently appear”

Settings View Controller 를 없애는 작업 (dismiss settings view controller)가 완전히 완료되지 않은 시점에서, 다른 뷰를 없애는 작업(dismiss navigation controller)를 하려고 하니 문제가 발생하는 것이다.

Settings View Controller 를 없애는 작업이 완전히 완료가 되고 화면이 안정적으로 구조화 된 후에 Navigation Controller 를 없앤다면 문제가 발생하지 않겠지만, 이는 화면에 별도의 이벤트(버튼)가 필요할 수도 있으므로 우리가 바라는 UI는 아니다.

그럼 해결책은 뭘까? 여러가지 방법이 있겠지만, Settings View Controller 를 없애는 작업을 수행하지 않은 채 바로 Navigation Controller 를 없애는 것이 하나의 해결책이다. 이렇게 되도 정상적으로 Settings View Controller 는 사라지고 메모리에서도 dealloc 된다.

하위층의 View Controller를 사라지게(dismiss)하면, 그 상위의 뷰들도 자연히 사라지는 듯 하다.

코드는 다음과 같이 Settings View Controller 에서의 설정뷰 자신을 없애는 부분을 생략하고 사용자 이벤트를 바로 Navigation Controller 측에 넘겨주면 된다.

<pre>
// settings View controller 에서 사용자 이벤트가 전될되는 메소드
-(IBAction)didPressDone:(id)sender{</pre>
//[self.parentViewController dismissModalViewControllerAnimated:YES]; // dismiss SettingsViewController
[delegate userAccountIsDisconnected]; // delegate 를 통하여 Navigation Controller 측에 이벤트를 알림

}
<pre>

실제 해결책은 생각보다 간단하였지만  UIViewController 의 계층 구조 및 라이프 사이클 등에 대해 좀 더 정확하게 파악할 필요가 있다.

Core Objects of iOS App

iOS App을 동작시키는 UIKit Framework의 핵심 객체들에 대해 알아보자.

앱이 구동되고 종료될 때 까지, UIKit Framework 은 앱의 핵심 동작을 관장한다. 앱의 심장격인 객체는 UIApplication 객체로, iOS로부터 이벤트를 받아 여러 custom 객체로 이벤트를 전달한다. UIApplication 외 다른 UIKit 클래스들도 custom code 를 호출하는 방식으로 앱의 동작에 관여한다.

아래 그림은 앱 동작에 있어 핵심적인 역할을 하는 객체들의 관계를 보여준다. – 기본적으로 iOS 앱은 MVC(model-view-controller) 디자인 패턴을 따른다. MVC 패턴은 데이터(model)를 뷰(view)와 분리시켜 코드 재활용성을 높이는 등 여러 이점들을 제공한다.

Key objects in an iOS app (from iOS Developer Library)

핵심 객체들의 역할은 아래와 같다.

UIApplication object

UIApplication 객체는 System 으로부터 이벤트를 받아 Custom Objects 로 이벤트를 전달하고 상위 레이어의 앱 동작을 조정한다. app delegate 객체 내의 custom logic 들은 UIApplication 객체와 상호작용한다.

App delegate object

App delegate  객체는 custom 객체로서 앱 런칭시점에 UIApplicationMain 함수에 의해 생성된다.

주로 앱의 상태 (Not running, Active, Inactive, Background, Suspended ) 전이를 담당하며, 앱 런칭 시점의 초기화 및 백그라운드 상태로의 전환등을 처리한다 (참고:  Managing App State Changes.)

iOS5 이후부터는 다른 앱의 이벤트를 App delegate 를 통해 핸들링 할 수 있다고 한다 . App delegate 는 UIResponder 클래스를 상속하므로 Responder Chain에 따라 UIApplication 이 처리하지 않은 이벤트를 처리할 수 있다.

Document and data model objects

Data model 객체는 앱의 컨텐트를 저장한다. 예를 들어 은행앱의 경우 거래트랜젝션을 저장할 수 있고, 그림그리기 앱의 경우 이미지 또는 이미지 조작 순서등을 저장할 수 있다.

앱은 Data model 객체를 Document 객체(custom subclasses of UIDocument)를 이용하여 처리할 수 있다. Document 객체 사용은 필수는 아니지만, 하나의 파일 또는 파일 패키지에 속해있는 데이터를 그룹화 하는데 편리한 방법을 제공한다. (참고: Defining a Document-Based Data Model.)

View controller objects

View controller 객체는 앱의 컨텐트가 화면에 보여지게 하는 역할을 담당한다. 하나의 View Controller 객체는 하나의 뷰와 뷰의 서브뷰를 관리한다. View Controller 객체는 자신이 관리하는 뷰를 앱의 윈도우에 위치시킴으로써 뷰가 화면에 보여지도록 한다.

UIViewController 클래스는 여러 View Controller 객체들의 부모클래스이다. UIViewController 는 기본적으로 뷰를 로딩하고, 화면에 뷰를 보여지게 하고, 뷰를 전환(디바이스 변화에 따른)하는 등의 기본적인 동작들을 지원한다.

UIKit 이나 다른 Framework들은 Image picker, Tab bar interface, Navigation interface 등의 추가적인 View Controller 클래스를 제공함으로써 기본적인 시스템 인터페이스를 수행할 수 있도록 한다. (참고: View Controller Programming Guide for iOS)

UIWindow object

UIWindow 객체는 화면에 보여지는 뷰 객체들을 조정하는 역할을 한다. 대부분의 앱은 컨텐트를 메인 스크린에 나타내는 윈도우 한 개를 소유하지만, 별도의 화면을 위한 UIWindow 객체 추가도 가능하다.

화면에 보여지는 컨텐트를 바꾸기 위해서는 View Controller 를 이용해야 한다. 윈도우 자체에서 컨텐트를 바꾸지 못한다.

뷰를 호스팅 하는 것 외에 UIWindow 객체는 UIApplication과의 상호작용을 통해 이벤트를 뷰와 View Controller에 전달하는 역할도 수행한다.

View, control and layer objects

View와 control은 컨텐트의 시각적 표현을 담당한다.

뷰는 직사각형 영역에 컨텐트를 표현하고, 영역 내의 이벤트를 감지하는 객체이다. 컨트롤은 뷰의 특정 타입으로서 버튼, 텍스트 필드, 토글 스위치 등이 있다.

UIKit Framework 는 다양한 타입의 컨텐트 (ex. 웹,이미지파일 등) 를 보여주기 위한 여러가지 기본 뷰를 제공한다. 기본 뷰 외에 개발자는 UIView를 직접 상속함으로써 자신만의 뷰를 만들 수 있다.

앱은 뷰와 컨트롤 계층(Hierarchy)에 Core Animation Layer 를 포함시킬 수 있다. Layer 객체는 정확히 말하면, 비쥬얼 컨텐트를 보여주는 data 객체이다. 뷰는 Layer 객체 또는 Custom layer 객체를 사용하여 복잡한 에니메이션 효과 및 시각적 효과를 표현할 수 있다.

* Reference : iOS Developer Library (iOS App Programming Guide – Core App Objects)
%d bloggers like this: