MASA MAUI Plugin (十)iOS消息推送(原生APNS方式)

語言: CN / TW / HK

背景

MAUI的出現,賦予了廣大.Net開發者開發多平台應用的能力,MAUI 是Xamarin.Forms演變而來,但是相比Xamarin性能更好,可擴展性更強,結構更簡單。但是MAUI對於平台相關的實現並不完整。所以MASA團隊開展了一個實驗性項目,意在對微軟MAUI的補充和擴展

項目地址https://github.com/BlazorComponent/MASA.Blazor/tree/feature/Maui/src/Masa.Blazor.Maui.Plugin

每個功能都有單獨的demo演示項目,考慮到app安裝文件體積(雖然MAUI已經集成裁剪功能,但是該功能對於代碼本身有影響),屆時每一個功能都會以單獨的nuget包的形式提供,方便測試,現在項目才剛剛開始,但是相信很快就會有可以交付的內容啦。

前言

本系列文章面向移動開發小白,從零開始進行平台相關功能開發,演示如何參考平台的官方文檔使用MAUI技術來開發相應功能。

介紹

Apple 推送通知服務(Apple Push Notification service),簡稱 APNs。與之前Android使用個推不同,由於APNs國內可用,所以我們可以直接使用APNs來實現遠程消息推送,不依賴其他第三方組件和服務。我們這裏推送使用的是p8證書,p8證書相對p12證書來講,更靈活,而且沒有p12證書有效期1年的限制。

一、實現方式

一、申請p8證書

https://developer.apple.com/

1、登錄開發者中心,點擊右上角Account,找到Keys管理。

在這裏插入圖片描述 2、在頂部點擊+號。 在這裏插入圖片描述 3、勾選APNs服務,並輸入Key名稱,下一步Continue。 在這裏插入圖片描述 4、點擊Register。 在這裏插入圖片描述 5、記錄Key ID,並下載證書,得到AuthKey_xxxxxxxxxx.p8證書文件。 在這裏插入圖片描述 6、獲取Team ID,Account界面點擊Membership details 在這裏插入圖片描述 在這裏插入圖片描述

二、編寫MAUI實現代碼

參考官方文檔:https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns

1、首先需要先開啟App消息推送的能力

我們新建一個iOSPush 文件夾,並在文件夾下面新建MauiBlazor項目iOSPushSample (由於受打包長度影響,項目名稱和文件夾名稱,我這裏儘量簡短。路徑長度超過255會導致編譯時提示部分文件找不到。) 我們找到Platforms->iOS->Info.plist文件,雙擊用默認的iOS清單編輯器打開,勾選“啟用後台模式”和“遠程通知”。這項操作會在Info.plist文件中添加如下信息:

	<key>UIBackgroundModes</key>
	<array>
		<string>remote-notification</string>
	</array>

在這裏插入圖片描述 在開發環境,你需要以下的額外配置。

如果你的項目已經使用了Entitlements.plist文件,雙擊打開改文件,並勾選“推送通知”。 在這裏插入圖片描述 如果沒有這個文件,那麼新建這個文本文件,內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>aps-environment</key>
	<string>development</string>
</dict>
</plist>

注意:這項配置的目的是在development環境支持推送,如果在項目發佈的時候,MAUI不會自動清除,需要手動關閉或註釋掉這項配置,否則會報錯。

2、編寫實現代碼

參考文檔 https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns

我們首先需要將App註冊到APNs並獲取唯一的device token。在iOS中需要調用UIApplication 中通過registerForRemoteNotifications() 方法,實現註冊,如果註冊成功,就可以在delegateapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:) 方法中獲取device token。如果註冊失敗,會觸發application(_:didFailToRegisterForRemoteNotificationsWithError:) 方法。

Swift代碼
func application(_ application: UIApplication,
           didFinishLaunchingWithOptions launchOptions:
           [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
   // Override point for customization after application launch.you’re         
   UIApplication.shared.registerForRemoteNotifications()
   return true
}

func application(_ application: UIApplication,
            didRegisterForRemoteNotificationsWithDeviceToken 
                deviceToken: Data) {
   self.sendDeviceTokenToServer(data: deviceToken)
}

func application(_ application: UIApplication,
            didFailToRegisterForRemoteNotificationsWithError 
                error: Error) {
   // Try again later.
}

我們開始編寫代碼

向APNs註冊設備

首先在iOS->AppDelegate.cs 類中,重寫FinishedLaunching 方法,在應用啟動之後進行註冊。

    [Register("AppDelegate")]
    public class AppDelegate : MauiUIApplicationDelegate
    {
        protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
        public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
        {
            UNUserNotificationCenter center = UNUserNotificationCenter.Current;

            var options = UNAuthorizationOptions.Alert | UNAuthorizationOptions.Sound | UNAuthorizationOptions.CriticalAlert;
             // Request notification permissions from the user
            center.RequestAuthorization(options,
                (bool success, NSError error) =>
                {
                // Handle approval
                }
            );
            UIApplication.SharedApplication.RegisterForRemoteNotifications();
            return base.FinishedLaunching(application, launchOptions);
        }
    }

應用啟動時應立即請求通知權限,方法是將以下代碼添加到FinishedLaunchingAppDelegate 所需通知類型 (UNAuthorizationOptions) 的方法:

UNUserNotificationCenter 僅適用於 iOS 10+,但是考慮到基本沒人用低於10的版本了,這裏我們就不做版本檢查了

用户可以批准應用的三個不同級別的通知請求: 橫幅顯示-Alert 。 聲音警報-Sound 。 對應用圖標進行錯誤設置-CriticalAlert。

請求權限結束後我們通過 UIApplication.SharedApplication.RegisterForRemoteNotifications(); 向APNs註冊。 註冊成功後我們通過application:didRegisterForRemoteNotificationsWithDeviceToken: 方法獲取device token,但是由於這個方法是在UIApplication下,但是我們的AppDelegate是繼承自 MauiUIApplicationDelegate ,默認沒有這個方法,我們可以通過Export特性,導出我們需要的方法,繼續在AppDelegate中添加

        [Export("application:didFailToRegisterForRemoteNotificationsWithError:")]
        public void FailedToRegisterForRemoteNotifications(UIKit.UIApplication application, NSError error)
        {
            Console.WriteLine("FailedToRegisterForRemoteNotifications");
        }
        
        [Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")]
        public void RegisteredForRemoteNotifications(UIKit.UIApplication application,
            Foundation.NSData deviceToken)
        {
            var token = ExtractToken(deviceToken);
            Preferences.Default.Set("PushToken", token);
            Console.WriteLine(token);
        }

        private string ExtractToken(NSData deviceToken)
        {
            if (deviceToken.Length == 0)
                return null;
            var result = new byte[deviceToken.Length];
            System.Runtime.InteropServices.Marshal.Copy(deviceToken.Bytes, result, 0, (int)deviceToken.Length);
            return BitConverter.ToString(result).Replace("-", "");
        }

ExtractToken是將返回的deviceToken解析為推送可用的字符串。 我們通過Preferences.Default.Set將token存儲起來,方便在登錄等業務中使用。

接收遠程推送

接收推送的消息是通過**application:didReceiveRemoteNotification:fetchCompletionHandler:**實現的

        [Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")]
        public void DidReceiveRemoteNotification(UIKit.UIApplication application, NSDictionary userInfo, Action<UIKit.UIBackgroundFetchResult> completionHandler)
        {
            foreach (var item in userInfo)
            {
                var alertMsg = ((NSDictionary)item.Value)["alert"];
                Console.WriteLine($"alertMsg:{alertMsg}");
            }
            Console.WriteLine("DidReceiveRemoteNotification");
        }

其實我們在方法內部不需要寫任何代碼,就可以實現基本的推送功能。但如果想處理推送過來的消息,可以通過NSDictionary類型userInfo中拿到。這裏示例從alert中拿到具體的消息內容,並打印。

三、編寫演示代碼

我們修改Index.razor,通過點擊按鈕獲取設備Token

@page "/"

<button @onclick="GetDeviceToken">Get Device Token</button>
<text>@deviceToken</text>
@code
{
    private string deviceToken { get; set; }
    private void GetDeviceToken()
    {
        deviceToken= Preferences.Default.Get("PushToken",string.Empty);
    }
}

四、服務端測試

我們可以通過個推的在線測試工具,配置好p8證書和其他參數。 在這裏插入圖片描述 我這裏寫了一個命令行的測試程序,這裏使用了第三方的PushNotifications.Server包

using PushNotifications.Server.Apple;

Console.WriteLine("Hello, World!");
IApnsClient apnsClient = new ApnsClient(new ApnsJwtOptions
{
    CertContent = "-----BEGIN PRIVATE KEY-----\r\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\r\n-----END PRIVATE KEY-----",
    KeyId = "LUxxxxxxxx",
    TeamId = "V4xxxxxxxx",
    BundleId = "com.iOSPush.Sample",
    UseSandbox = true
});

var apnsRequest = new ApnsRequest(ApplePushType.Alert)
    .AddToken("47006118F8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
    .AddSound()
    .AddAlert("MASA", $"Message @ {DateTime.Now}", $"Message @ {DateTime.Now}");
var apnsResponse = await apnsClient.SendAsync(apnsRequest);
Console.ReadKey();

五、演示效果

請添加圖片描述

可以看出,應用打開的狀態不會收到推送,後台運行或者劃掉關閉都可以收到通知。

如果你對我們的 MASA Framework 感興趣,無論是代碼貢獻、使用、提 Issue,歡迎聯繫我們