iOS使用觀察者模式實現推送訊息模組化

aron1992發表於2019-04-04

背景

前段時間做了一些專案解耦重構和一些元件化的工作,推送是很多app種涉及到的應用場景,所以把推送模組做了一些重構的工作,讓推送模組能夠獨立於業務適用於各種的業務場景。

本文的程式碼連結:PTNotificationManager

分析

推送訊息模組和其他模組從技術角度來看是屬於同一級別的模組,推送訊息模組為了能夠和其他業務元件之間既有通訊又能解耦,這複合設計中的控制反轉原則,依賴的雙方依賴於對方的抽象而不依賴於具體的實現,觀察者模式就是一種典型的控制反轉場景,觀察者模式很適合推送模組的獨立和解耦。

實現

觀察者模式簡析

觀察者模式類圖
觀察者模式類圖

這是一個簡單的觀察者的類圖結構

  • 抽象介面 Subject 有一個註冊觀察者的方法
  • 抽象介面 Observer 有一個獲取 Subject 資料更新的方法
  • SubjectObserver 具體的子類重寫對應的方法,處理資料
  • 有新的訊息,ConcreateSubject 會把訊息傳送給已註冊的ConcreateObserverConcreateObserver 負責接收訊息進行處理

程式碼實現

抽象介面

OC介面是使用 protocol 實現的,定義觀察者(Observer)和被觀察主題(Subject)如下:

@protocol PTNotificationObservable;

@protocol PTNotificationObserver <NSObject>

- (void)update:(id<PTNotificationObservable>)sender data:(id)data;

@end


@protocol PTNotificationObservable <NSObject>

- (void)addObserver:(id<PTNotificationObserver>)observer;

@end
複製程式碼

具體實現

推送模組管理類對應的是被觀察主題(Subject),定義瞭如下介面:

  • APP啟動,初始化推送配置
  • 處理註冊Token
  • 處理接收訊息
  • 新增觀察者

標頭檔案如下:

#import <Foundation/Foundation.h>
#import "PTNotificationProtocllDefine.h"

#undef    AS_SINGLETON
#define AS_SINGLETON \
+ (instancetype)sharedInstance;

#undef    DEF_SINGLETON
#define DEF_SINGLETON \
+ (instancetype)sharedInstance{ \
static dispatch_once_t once; \
static id __singleton__; \
dispatch_once( &once, ^{ __singleton__ = [[self alloc] init]; } ); \
return __singleton__; \
} \


@interface PTNotificationManager : NSObject <PTNotificationObservable>

AS_SINGLETON

// 處理APP啟動,配置推送
- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
// 處理註冊Token
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
// 處理接收訊息
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;

// 新增觀察者
- (void)addObserver:(id<PTNotificationObserver>)observer;

- (void)testSendNotification;

@end
複製程式碼

整合的是第三方的友盟訊息推送,所有裡面包含了友盟一些API的使用。
實現檔案:

#import "PTNotificationManager.h"
#import "UMessage.h"
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
#import <UserNotifications/UserNotifications.h>
#endif

#define UMengAppKey @"xxxxx"

@interface PTNotificationManager ()<UNUserNotificationCenterDelegate>
@property (nonatomic, strong) NSPointerArray* observers;
@end

@implementation PTNotificationManager

DEF_SINGLETON

- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //設定 AppKey 及 LaunchOptions
    [UMessage startWithAppkey:UMengAppKey launchOptions:launchOptions];
    //註冊通知
    [UMessage registerForRemoteNotifications];
    
    //iOS10必須加下面這段程式碼。
    if ([[[UIDevice currentDevice] systemVersion]intValue] >= 10) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate=self;
        UNAuthorizationOptions types10=UNAuthorizationOptionBadge|UNAuthorizationOptionAlert|UNAuthorizationOptionSound;
        [center requestAuthorizationWithOptions:types10 completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (granted) {
                //點選允許
            } else {
                //點選不允許
            }
        }];
    }
    
    //如果你期望使用互動式(只有iOS 8.0及以上有)的通知,請參考下面註釋部分的初始化程式碼
    if (([[[UIDevice currentDevice] systemVersion]intValue]>=8)&&([[[UIDevice currentDevice] systemVersion]intValue]<10)) {

//        UIMutableUserNotificationAction *action1 = [[UIMutableUserNotificationAction alloc] init];
//        action1.identifier = @"action1_identifier";
//        action1.title=@"開啟應用";
//        action1.activationMode = UIUserNotificationActivationModeForeground;當點選的時候啟動程式
//
//        UIMutableUserNotificationAction *action2 = [[UIMutableUserNotificationAction alloc] init];  第二按鈕
//        action2.identifier = @"action2_identifier";
//        action2.title=@"忽略";
//        action2.activationMode = UIUserNotificationActivationModeBackground;當點選的時候不啟動程式,在後臺處理
//        action2.authenticationRequired = YES;需要解鎖才能處理,如果action.activationMode = UIUserNotificationActivationModeForeground;則這個屬性被忽略;
//        action2.destructive = YES;
//        UIMutableUserNotificationCategory *actionCategory1 = [[UIMutableUserNotificationCategory alloc] init];
//        actionCategory1.identifier = @"category1";這組動作的唯一標示
//        [actionCategory1 setActions:@[action1,action2] forContext:(UIUserNotificationActionContextDefault)];
//        NSSet *categories = [NSSet setWithObjects:actionCategory1, nil];
//        [UMessage registerForRemoteNotifications:categories];
    }
    //如果要在iOS10顯示互動式的通知,必須注意實現以下程式碼
    if ([[[UIDevice currentDevice] systemVersion]intValue]>=10) {

//        UNNotificationAction *action1_ios10 = [UNNotificationAction actionWithIdentifier:@"action1_ios10_identifier" title:@"開啟應用" options:UNNotificationActionOptionForeground];
//        UNNotificationAction *action2_ios10 = [UNNotificationAction actionWithIdentifier:@"action2_ios10_identifier" title:@"忽略" options:UNNotificationActionOptionForeground];
        //UNNotificationCategoryOptionNone
        //UNNotificationCategoryOptionCustomDismissAction  清除通知被觸發會走通知的代理方法
        //UNNotificationCategoryOptionAllowInCarPlay       適用於行車模式
//        UNNotificationCategory *category1_ios10 = [UNNotificationCategory categoryWithIdentifier:@"category101" actions:@[action1_ios10,action2_ios10]   intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
//        NSSet *categories_ios10 = [NSSet setWithObjects:category1_ios10, nil];
//        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
//        [center setNotificationCategories:categories_ios10];

    }
    
    //如果對角標,文字和聲音的取捨,請用下面的方法
    //UIRemoteNotificationType types7 = UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeAlert|UIRemoteNotificationTypeSound;
    //UIUserNotificationType types8 = UIUserNotificationTypeAlert|UIUserNotificationTypeSound|UIUserNotificationTypeBadge;
    //[UMessage registerForRemoteNotifications:categories withTypesForIos7:types7 withTypesForIos8:types8];

    //for log
    [UMessage setLogEnabled:YES];
    
    // 應用從通知啟動
    NSDictionary* userInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (userInfo) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self handleNotificationUserInfo:userInfo];
        });
    }
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    //1.2.7版本開始不需要使用者再手動註冊devicetoken,SDK會自動註冊
    // [UMessage registerDeviceToken:deviceToken];
    
    NSString* tokenString = [self stringDevicetoken:deviceToken];
    NSLog(@"==tokenString = %@", tokenString);
    printf([[NSString stringWithFormat:@"\n\ntokenString = %@\n\n", tokenString] UTF8String]);
}

/**
 - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
 {
 //如果註冊不成功,列印錯誤資訊,可以在網上找到對應的解決方案
 //1.2.7版本開始自動捕獲這個方法,log以application:didFailToRegisterForRemoteNotificationsWithError開頭
 } */

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"userInfoNotification" object:self userInfo:@{@"userinfo":[NSString stringWithFormat:@"%@",userInfo]}];
    //關閉友盟自帶的彈出框
    [UMessage setAutoAlert:NO];
    [UMessage didReceiveRemoteNotification:userInfo];
    if (application.applicationState != UIApplicationStateActive) {
        [self handleNotificationUserInfo:userInfo];
    }
}

// 新增觀察者
- (void)addObserver:(id<PTNotificationObserver>)observer {
    [self.observers addPointer:(__bridge void * _Nullable)(observer)];
}

- (void)testSendNotification {
    [self handleNotificationUserInfo:@{@"kkk": @"kkk"}];
}

- (void)handleNotificationUserInfo:(NSDictionary *)userInfo {
    for (id<PTNotificationObserver> observer in self.observers.allObjects) {
        [observer update:self data:userInfo];
    }
}

#pragma mark - ......::::::: UNUserNotificationCenterDelegate :::::::......

//iOS10新增:處理前臺收到通知的代理方法
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    NSDictionary * userInfo = notification.request.content.userInfo;
    if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        //應用處於前臺時的遠端推送接受
        //必須加這句程式碼
        [UMessage setAutoAlert:NO];
        [UMessage didReceiveRemoteNotification:userInfo];
    }else{
        //應用處於前臺時的本地推送接受
    }
}

//iOS10新增:處理後臺點選通知的代理方法
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(nonnull void (^)(void))completionHandler {
    NSDictionary * userInfo = response.notification.request.content.userInfo;
    if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        //應用處於後臺時的遠端推送接受
        //必須加這句程式碼
        [UMessage didReceiveRemoteNotification:userInfo];
        [self handleNotificationUserInfo:userInfo];
    }else{
        //應用處於後臺時的本地推送接受
    }
}


#pragma mark - ......::::::: Helper :::::::......

-(NSString *)stringDevicetoken:(NSData *)deviceToken {
    NSString *token = [deviceToken description];
    NSString *pushToken = [[[token stringByReplacingOccurrencesOfString:@"<"withString:@""] stringByReplacingOccurrencesOfString:@">"withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""];
    return pushToken;
}


#pragma mark - ......::::::: Lazy Load :::::::......

- (NSPointerArray *)observers {
    if (nil == _observers) {
        _observers = [NSPointerArray weakObjectsPointerArray];
    }
    return _observers;
}

@end
複製程式碼

使用方法:

  • ViewController 實現了 PTNotificationObserver Protocol 作為觀察者
  • 重寫 - (void)update:(id<PTNotificationObservable>)sender data:(id)data 方法列印接收到的訊息
  • - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 模擬讓訊息模組傳送訊息
@interface PTViewController () <PTNotificationObserver>

@end

@implementation PTViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[PTNotificationManager sharedInstance] addObserver:self];

}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


// 接收到訊息
- (void)update:(id<PTNotificationObservable>)sender data:(id)data {
    NSLog(@"%@", data);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[PTNotificationManager sharedInstance] testSendNotification];
}
複製程式碼

使用cocopods整合

使用 pod lib create 命令建立私有庫

➜  DevPods pod lib create PTNotificationManager
Cloning `https://github.com/CocoaPods/pod-template.git` into `PTNotificationManager`.
Configuring PTNotificationManager template.

------------------------------

To get you started we need to ask a few questions, this should only take a minute.

If this is your first time we recommend running through with the guide: 
 - http://guides.cocoapods.org/making/using-pod-lib-create.html
 ( hold cmd and double click links to open in a browser. )


What language do you want to use?? [ Swift / ObjC ]
 > Objc

Would you like to include a demo application with your library? [ Yes / No ]
 > 
yes
Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > None

Would you like to do view based testing? [ Yes / No ]
 > No

What is your class prefix?
 > PT

Running pod install on your new library.

Analyzing dependencies
Fetching podspec for `PTNotificationManager` from `../`
Downloading dependencies
Installing PTNotificationManager (0.1.0)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `PTNotificationManager.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

[!] Automatically assigning platform ios with version 9.3 on target PTNotificationManager_Example because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

 Ace! you're ready to go!
 We will start you off by opening your project in Xcode
  open 'PTNotificationManager/Example/PTNotificationManager.xcworkspace'

To learn more about the template see `https://github.com/CocoaPods/pod-template.git`.
To learn more about creating a new pod, see `http://guides.cocoapods.org/making/making-a-cocoapod`.
➜  DevPods 
複製程式碼

修改 PTNotificationManager.podspec 檔案如下

Pod::Spec.new do |s|
  s.name             = 'PTNotificationManager'
  s.version          = '0.1.0'
  s.summary          = 'A short description of PTNotificationManager.'
  s.description      = <<-DESC
    Oh PTNotificationManager
                       DESC

  s.homepage         = 'https://github.com/flypigrmvb/PTNotificationManager'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'flypigrmvb' => '862709539@qq.com' }
  s.source           = { :git => 'https://github.com/flypigrmvb/PTNotificationManager.git', :tag => s.version.to_s }

  s.ios.deployment_target = '8.0'

  s.source_files = 'PTNotificationManager/Classes/**/*'

  # s.public_header_files = 'Pod/Classes/**/*.h'

    # 新增依賴的系統靜態庫
    s.libraries = 'xml2', 'z', 'c++', 'stdc++.6', 'sqlite3'
    # 新增系統的farme依賴庫
    s.frameworks = 'UIKit', 'MapKit'
    # 新增其他Pod依賴庫
    s.dependency 'UMessage'
end
複製程式碼

Example專案的 Podfile 新增如下內容


platform :ios, '8.0'

inhibit_all_warnings!

target 'PTNotificationManager_Example' do
  pod 'PTNotificationManager', :path => '../'
  pod 'UMessage', :podspec => 'https://raw.githubusercontent.com/kkme/UMessage/master/UMessage.podspec'

end
複製程式碼

以上步驟Example專案就可以跑起來了,完成了簡單的推送訊息模組的元件化和解耦,方便在不同的業主場景中使用。

相關文章