제목: Service-oriented AppDelegate


보통 앱델리게이트(AppDelegate)는 거대한 클래스이다. 이것은 여러분의 앱에 대해 너무 많이 알고 있고 점점 더 커지게 된다. 이 글에서는 깔끔한 플러그인 기반 아키텍처를 만들어서 그 세부 기능으로부터 어떻게 분리해낼 수 있는지 보여줄 것이다.

우리는 올바르지 않은 방법으로 문제를 직면하고 있다. 당신의 앱델리게이트를 어떻게 관리하고 있는지 생각해보자. 내 생각엔 굉장히 거대하거나, 아니면 적어도 거기서 코딩한 것들에 대해 자신감이 있지는 않을 것 같아 보인다.

앱델리게이트의 문제는 바로 앱델리게이트가 여러분의 앱에대해 너무 많이 알고 있다는 점이다. 이것은 여러분의 의존성들을 알고있는데, 어떻게 초기화하는지, 어떻게 푸시 노티피케이션을 파싱하는지 등 많은 것을 알고 있다.

요약을 해보자면
ApplicationSercvices를 만들어낼 수 있다. 이것들은 AppDelegate 라이프사이클을 공유하고 해당 단계에 작업을 실행하는 오브젝트이다. 여러분의 앱델리게이트는 피관찰자(Observable)이고 여러분의 서비스들은 관찰자(Observer)이다. 이런 접근법은 그 서비스들로부터 앱델리게이트를 분리시켜주고 단일 책임 오브젝트를 생성해준다.

물론 많은 양의 코드를 작성하게 되므로 이 문제를 다루기위해 CocoaPod을 만들었다. PluggableAppDelegate라 부르며 여기에서 확인할 수 있다: http://github.com/fm091/PluggableApplicationDelegate

서비스를 생성해서 AppDelegate에 등록만 하면 된다. 그 결과는 여러분이 봐왔던 것 중에 가장 작고 깔끔한 AppDelegate가 될것이다.

일반적인 앱델리게이트
일반적으로, 앱델리게이트는 많은 역할을 가지고 있다.
  1. 여러분 앱의 모든 의존성을 초기화한다.
  2. 전역의 UIAppearance 값들을 초기설정한다.
  3. 푸시 노티피케이션을 다룬다.
  4. 푸시 노티피케이션을 등록한다.

그밖에 더 있을 것이다.

여기서 문제는 여러분의 앱델리게이트가 정확하게 모든 컴포넌트가 어떻게 동작하는지, 그리고 어떻게 인스턴스화하는지 알고 있기 때문에 생긴 것이다. 또한 이것은 여러분의 앱이 어떻게 생겼는지도 알 수도 있다. 그리고 푸시 노티피케이션 포멧도 알고있다. 결과적으로 생각했던것보다 너무 커져버린다.

다른 접근법
ApplicationService라 부르는 그 고유의 컴포넌트에 여러분의 의존성들을 캡슐화하여 들고 있다고 생각해보자.

모든 ApplicationService는 하위-앱델리게이트(sub-AppDelegate)이다. 이것은 앱델리게이트의 라이프사이클을 공유하고, 요청된 이벤트에서 행동을 취한다.

여러분 AppDelegate는 이벤트의 발생를 관찰하는 관찰자(observable)이 되어, 어떤 일이 일어나면 서비스에게 알려주기만 하면 된다.



어플리케이션 서비스들
어플리케이션 서비스들은 AppDelegate 라이프사이클을 공유하는 오브젝트들이다. ApplicationService는 그냥 태그한 프로토콜이다. 만약 PluggableApplicationDelegate를 사용한다면 이렇게 생겼을 것이다.
public protocol ApplicationService: UIApplicationDelegate {}
위에서 본 것처럼, UIApplicationDelegate만이지만, 더 전달력있는 이름이다.

이것을 구현할때, AppDelegate를 구현하듯이 하면 된다. 그러나 여기서 다른점은 단일 책임 요소를 코딩한다는 점이다.

아래에 구현에대한 예시가 있다.
import Foundation
import PluggableApplicationDelegate

final class LoggerApplicationService: NSObject, ApplicationService {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
        print("It has started!") return true
    }
    func applicationDidEnterBackground(_ application: UIApplication) {
        print("It has entered background”)
    }
}

새로운 앱델리게이트
PluggableApplicationDelegate를 상속받고 이것을 서비스에 등록함으로서, 이제 여러분의 앱델리게이트는 더 작아졌다.

아래를 확인해보자
import UIKit 
import PluggableApplicationDelegate 

@UIApplicationMain 
class AppDelegate: PluggableApplicationDelegate { 

    override var services: [ApplicationService] { 
        return [ LoggerApplicationService() ] 
    } 
}

PluggableApplicationDelegate는 너무 크다. 뭔가 일어났을때만 그 서비스에 알려준다. 그것에대한 코드를 여기서 확인할 수 있을 것이다.

의존성과 기능을 점점 더 앱에 추가하더래도 앱델리게이트는 아래처럼 깨끗하게 유지될 것이다.
import UIKit 
import PluggableApplicationDelegate 

@UIApplicationMain 
class AppDelegate: PluggableApplicationDelegate { 

    override var services: [ApplicationService] { 
        return [ 
            PushNotificationsService(), 
            AnalyticsService(), 
            CustomAppearanceService(),
            FirebaseService(), 
            FacebookSDKService() 
        ] 
    } 
}

결론
이게 다다. 여러분의 앱델리게이트를 분리시키는 간단한 방법이며, 그것이 커다란 덩어리로 되지 않게 막아준다.

내 깃헙 저장소를 꼭 확인해보기 바란다:

내가 도움이 됐던 만큼 여러분에게도 도움이 되길 바라며, 이것에대한 피드백을 꼭 받고싶다. 아래 댓글로 남겨달라

고맙다!


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
이번 포스팅에서는 iOS에서 Objective-C로 푸시를 구현하는 방법에 대해 설명하겠다. 그렇지만 단순히 푸시를 받는것이 다가 아닌 푸시를 받았을 때 다음 이벤트 처리도 앱에서 어떻게 처리할지에대한 고민도 함께할 것이다. 이 부분은 애플에서 제공하는 방법이 생각보다 복잡하게 되있는것 같으므로 포스팅에 남기기로 하였다.

먼저 푸시를 받는 방법에 대해 설명하..  검색해보니 바로 원하는 링크가 안나와서 푸시 받는법부터 설명해보겠다.



1. 푸시 아이디를 얻어내는법

푸시 아이디를 얻고자 하는 시점에서 다음을 호출하면 된다. 
// 현재 푸시가 On인지 Off인지 알아내는 함수
BOOL pushEnable = NO;
if ([[UIApplication sharedApplication] respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    pushEnable = [[UIApplication sharedApplication] isRegisteredForRemoteNotifications];
} else {
    UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
    pushEnable = types & UIRemoteNotificationTypeAlert;
}

// 푸시 아이디를 달라고 폰에다가 요청하는 함수
UIApplication *application = [UIApplication sharedApplication];
if ([application respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    NSLog(@"upper ios8");
    // iOS 8 Notifications
    [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
    [application registerForRemoteNotifications];
} else {
    NSLog(@"down ios8");
    // iOS < 8 Notifications
    [application registerForRemoteNotificationTypes:
     (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound)];
}


// AppDelegate.m 파일에서 아래 함수를 추가한다. 아래 함수는 푸시 아이디를 받아내는 함수이고, 푸시아이디는 아래 함수를 통해서만 받을 수 있다.
// AppDelegate.m
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
     NSString* newToken = [[[NSString stringWithFormat:@"%@",deviceToken]
                           stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""];
     NSLog(@"DeviceToken : %@", newToken );
}


위 코드를 보면 이것저것 복잡해보인다. 여기서 중요한 라인만 설명을 하자면

UIApplication *application = [UIApplication sharedApplication];
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
[application registerForRemoteNotifications];
위 세 라인이다.
1. 앱 싱글톤 델리게이트를 받아와서 2. 푸시받을 타입을 정한다. 그리고 3. 실제로 푸시 아이디를 달라고 요청한다.
registerForRemoteNotifications가 호출되는 순간 appdelegate.m에 
첫째, 앱을 설치하고 처음으로 registerForRemoteNotifications를 호출하는 상황이면 사용자에게 푸시를 허용할지 말지에 대한 경고창이 뜬다. 여기서 허용을 하면 AppDelegate.m에 구현된application:didRegisterForRemoteNotificationsWithDeviceToken: 함수가 호출되면서 deviceToken파라미터에 푸시아이디가 담기게 된다. 반대로 사용자가 푸시 허용을 하지 않는다면 application:didRegisterForRemoteNotificationsWithDeviceToken: 함수는 호출되지 않는다. 그게 다다. 사용자가 푸시알림 허용 안했다고해서 뭐 어떻게 더 바로 할 수 있는게 없다. 푸시알림 무한 유도를 막기위함의 애플의 생각이 아닐까 싶은데, 이 부분때문에 고민을 좀 많이했다. 그래서 준비한게 BOOL pushEnable;이고 푸시가 현재 허용되있는지 아닌지 대강 판단해준다. 푸시가 꼭 필요한 어플일 경우는 pushEnable를 받아내서 확인할 수 있다. (하지만 이것도 사용자가 한번도 푸시 설정을 안했는지, 실제 푸시 거부를 한건지 알 방법은 없다. 단지 현재 푸시 설정 On/Off 여부만 알 수 있다.)
둘째, 앱을 설치하고 푸시 허용을 해놓은 상태이며(최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 AppDelegate.m에 application:didRegisterForRemoteNotificationsWithDeviceToken:함수가 실행되면서 푸시아이디를 받아올 수 있다. 푸시 아이디를 서버에 등록할 때 사실상 푸시 아이디는 바뀔 수가 있다. 그렇기에 나같은 경우는 푸시아이디와 디바이스아이디를 함께 서버에 보내서 디바이스 아이디 기준으로 푸시 아이디를 저장한다. 아무튼 두번째의 경우는 무조건 푸시아이디를 받아올 수 있는 상황이다.

세번째, 앱을 설치하고 푸시 허용을 해제해놓은 상태이며(마찬가지로 최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 아무일도 일어나지 않는다. 그렇기때문에 이 경우는 미리 pushEnable를 받아내서 푸시가 가능한지 아닌지를 판단해놓을 필요가 있다. pushEnableNO이면 푸시 허용 Off인 상태인거다. 그러면 푸시 설정을 해라고 경고를 띄우면 된다. 


2. 서버에서 푸시를 쏘았을때 폰에서 받은 위 호출되는 함수
서버에서 푸시를 쏘자마자 알아내는 방법은 하나밖에 없다. -> 앱이 실행되는 중일때이다.
앱이 만약 꺼저있거나, 백그라운드에서 돌고있으면 푸시가 왔는지 안왔는지 앱에서는 모른다. 앱이 다시 포그라운드로 왔을때 푸시가 왔으면 ‘푸시받는함수'가 호출된다. 하지만 웃긴것이, 앱이 백그라운드에도 없고 메모리에 올라가있지 않을때 푸시가 왔을때, 바탕화면에서 푸시를 받아서 푸시를 눌러 앱에 들어가면 ‘푸시받는함수’가 호출되지 않는다. 이 예외적인 부분을 처리하는 방법에 대해 설명하겠다.
 
// AppDelegate.m
// 앱이 런칭되서 메모리에 올라갈때 실행되는 함수이다.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    UILocalNotification *notificationUserInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (notificationUserInfo) {
//        NSLog(@"app recieved notification from remote%@",notification);
        NSMutableDictionary *mNotificationUserInfo = [NSMutableDictionary dictionaryWithDictionary:(id)notificationUserInfo];
        mNotificationUserInfo[@"appLaunch"] = @"yes";
        [self application:application didReceiveRemoteNotification:mNotificationUserInfo];
    }else{
//        NSLog(@"app did not recieve notification");
    }
    return YES;
}

여기서
// AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

    [PushController shared].userInfo = userInfo;

    if (userInfo[@"appLaunch"]) {
        [PushController shared].waitingViewDidLoad = YES;
    } else {
        if (application.applicationState==UIApplicationStateActive) {
            [[PushController shared] pushInActiveStatus];
        } else {
            // if (application.applicationState==UIApplicationStateInactive) {
            [[PushController shared] presentPushPostTableViewController];
        }
    }
}
위의 PushController는 일단 신경쓰지 말고 푸시를 받았을 때 라이프 사이클만 신경쓰자. 
저기서 특이한 점은 
if (userInfo[@"appLaunch”])
를 이용하여 첫째 상황인지 먼저 구분하고
if (application.applicationState==UIApplicationStateActive) ;
를 이용하여 둘째상황과 셋째상황을 구분했다는 점이다.

첫째 상황에서는 아직 ViewController가 생성이 되기 전의 라이프사이클이다. 그렇기때문에 ViewControllerviewDidLoad를 호출하기 전까지 푸시정보를 잠시 가지고 있어야한다. 
둘째 상황에서는 application.applicationState==UIApplicationStateInactive에 해당하는 상태인데, 현재 앱이 백그라운드에서 포그라운드로 가고 있다는 뜻으로 사용자가 앱을 켜놓고 바탕화면에서 푸시를 눌렀을 때를 말한다. 이때는 바로 푸시 행위를 진행하면 된다.
셋째상황은 application.applicationState==UIApplicationStateActive의 상황으로써 앱이 이미 포그라운드에서 돌고 있었다는 뜻이고 이때는 자동으로 푸시 기능을 실행시켜버리는게 아니라 상단에 팝업창이 떠서 푸시가 온것처럼 만들어주면 사용자가 인지하기 쉬울 것이다. (안드로이드는 기본으로 제공되고, 아이폰은 카톡에서 볼 수 있는 기능이다) 상단에서 팝업이 잠시 내려왔을때 팝업을 누르면 푸시 기능을 수행하게 만들었는데, 이 기능은 PushController에 있다. PushController는 UI작업을 좀 했기 때문에 라인이 좀 되서 첨부해두겠다.

아무튼 위와같이 처리하면 푸시를 받는 일을 놓히지 않고 처리할 수 있다. 
 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,