You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS通知拦截与shared_preferences存储问题及Laravel-Flutter通信优化

iOS Notification Service Extension 异常排查与Flutter-Laravel通信同步问题

我正在开发Flutter聊天应用,核心需求是将所有消息和聊天记录本地存储在Android/iOS设备,应用启动时从本地加载数据。最初计划通过Laravel后端发送静默通知(数据消息),同步新消息接收、消息删除、群组成员变更等操作——Android设备不受严格限流和电池优化影响,消息能稳定送达;但iOS设备因系统电池优化,静默通知被限流,后台唤醒应用的次数受限,无法依赖该方案更新本地数据。

经咨询苹果工程师,调整方案为使用Notification Service Extension(NSE):该扩展会处理每条可见推送通知,只要用户未禁用通知可见性即可触发,但不会处理无视觉展示的静默通知。NSE有30秒运行时间、24MB内存限制,无法直接与主应用通信,只能通过共享容器读写数据。

基于此,我设计的流程是:发送带唯一ID的普通通知避免折叠,确保送达可靠性;通过NSE拦截通知,根据payload中的suppress字段决定是否展示通知,同时将业务数据存储到共享容器;Flutter应用启动后读取共享容器数据,转换后同步到本地数据库。目前已按要求创建并配置App Group到NSE和主应用目标,但NSE代码无法正常工作。


现有代码

Notification Service Extension 代码

import UserNotifications

class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    print("NSE: didReceive triggered")
    
    self.contentHandler = contentHandler
    bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    
    guard let bestAttemptContent = bestAttemptContent else {
        print("NSE: bestAttemptContent is nil")
        contentHandler(request.content)
        return
    }

    print("NSE: Notification Payload - \(request.content.userInfo)")
    
    if let userInfo = request.content.userInfo as? [String: Any],
       let suppressFlag = userInfo["suppress"] as? Bool,
       let cleanData = userInfo["data"] as? [String: Any] {
        print("NSE: Suppress flag = \(suppressFlag)")
        
        if suppressFlag {
            print("NSE: Suppressing notification")
            bestAttemptContent.title = ""
            bestAttemptContent.body = ""
            bestAttemptContent.sound = nil
        } else {
            print("NSE: Displaying notification")
            bestAttemptContent.title = cleanData["title"] as? String ?? "New Message"
            bestAttemptContent.body = cleanData["body"] as? String ?? "You have a new message."
        }

        print("NSE: Saving notification to shared container")
        saveNotificationToSharedContainer(data: cleanData)
    }

    print("NSE: Passing modified content to handler")
    contentHandler(bestAttemptContent)
}

override func serviceExtensionTimeWillExpire() {
    // Called when time runs out
    if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
        contentHandler(bestAttemptContent)
    }
}

private func saveNotificationToSharedContainer(data: [String: Any]) {
    // Access shared container
    let sharedDefaults = UserDefaults(suiteName: "group.com.egensolve.demo")
    var notifications = sharedDefaults?.array(forKey: "stored_notifications") as? [[String: Any]] ?? []

    // Append the new notification data
    notifications.append(data)
    sharedDefaults?.set(notifications, forKey: "stored_notifications")
    sharedDefaults?.synchronize()
}
}

Laravel 端发送通知代码

public function sendDataMessage($deviceTokens, $data = [], $priority = "high")
{
    $messaging = app('firebase.messaging');
    $clean_data = $data;
    if (isset($data["type"]) && $data["type"] == "chat" && $data["action"] == "receiveMessage") {
        unset($clean_data["fcm_message"]);
    }
    $messages = [];
    foreach ($deviceTokens as $token) {
        $aps = [];
        try {
            if (isset($data["type"]) && $data["type"] == "chat") {
                $chat_message = $data["fcm_message"];
                $clean_data["title"] = Chats::select("name")->find($data["chat_id"])->name;
                if ($data["action"] == "receiveMessage") {
                    $clean_data["body"] = $chat_message["sender"]["name"] . ": " . ($chat_message["content"]["type"] == "Text" ? $chat_message["content"]["body"] : $chat_message["content"]["type"]);
                    $aps = [
                        "alert" => [
                            "title" => $clean_data["title"],
                            "body" => $clean_data["body"]
                        ],
                        "content-available" => 1,
                        "mutable-content" => 1
                    ];
                    $clean_data["suppress"] = true;
                    $aps["sound"] = "default";
                } else {
                    $clean_data["suppress"] = true;
                }
            }
            $messages[] = CloudMessage::withTarget('token', $token)
                ->withData($clean_data)
                ->withAndroidConfig(AndroidConfig::fromArray([
                    'priority' => 'normal',  // Use high priority
                    'ttl' => 86400 * 28, // 4 weeks in seconds
                ]))
                ->withApnsConfig(ApnsConfig::fromArray([
                    'headers' => [
                        'apns-collapse-id' => uniqid(),
                        'apns-expiration' => strval(time() + 86400 * 28), // Expiry timestamp
                        'apns-priority' => '10',
                    ],
                    'payload' => [
                        'aps' => $aps,
                    ],
                ]));
        } catch (Exception $e) {
            return $e;
        }
    }
    try {
        $report = $messaging->sendAll($messages);
    } catch (NotFound $e) {
        return $e;
    } catch (Exception $e) {
        return $e;
    }

    return $report->successes()->count();
}

Flutter 读取共享容器代码

final notifications = prefs?.getStringList('stored_notifications') ?? [];
print(notifications);

求助需求

需要排查以下问题,确保iOS端能稳定拦截通知并将数据存储到共享容器,实现Laravel与Flutter的可靠通信:

  • NSE未触发或无法正确解析处理通知payload
  • 数据无法写入共享容器,导致Flutter端读取不到内容
  • 验证suppress字段逻辑是否正常生效(值为true时隐藏通知,false时展示)

内容的提问来源于stack exchange,提问作者OKKO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 12:17:03