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

Expo托管工作流下iOS通知显示图片问题求助

解决方案:Expo SDK 49 EAS托管工作流实现iOS通知图片展示

1. 使用社区封装插件(推荐)

无需手动操作Xcode,借助社区插件即可快速添加Notification Service Extension:

  • 安装依赖:
npx expo install expo-notifications @bam.tech/expo-notification-service-extension
  • 配置app.json/app.config.js:
{
  "expo": {
    "plugins": [
      [
        "@bam.tech/expo-notification-service-extension",
        {
          "name": "NotificationService",
          "appleTeamId": "你的Apple Team ID"
        }
      ],
      "expo-notifications"
    ]
  }
}
  • 调整FCM推送Payload:
    必须在apns字段中开启mutable-content,并指定图片URL:
{
  "to": "目标设备Token",
  "notification": {
    "title": "测试通知",
    "body": "带图片的通知内容"
  },
  "apns": {
    "payload": {
      "aps": {
        "mutable-content": 1
      },
      "attachment": "https://example.com/your-image.jpg"
    }
  }
}
  • 构建测试:
eas build --platform ios

构建完成后安装到测试设备,发送符合格式的推送即可看到图片展示。

2. 自定义Config Plugin(按需使用)

如果社区插件不符合需求,可自行编写插件添加Extension:

  1. 在项目中创建plugins/notification-service-extension.js,内容如下:
const { withXcodeProject, withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');

module.exports = function withNotificationServiceExtension(config) {
  config = withXcodeProject(config, async (config) => {
    // 自动修改Xcode项目添加Extension Target,逻辑可参考Expo官方文档扩展
    return config;
  });

  config = withDangerousMod(config, [
    'ios',
    async (config) => {
      const extensionPath = path.join(config.modRequest.platformProjectRoot, 'NotificationService');
      if (!fs.existsSync(extensionPath)) {
        fs.mkdirSync(extensionPath);
        // 写入NotificationService核心逻辑
        fs.writeFileSync(path.join(extensionPath, 'NotificationService.swift'), `
import UserNotifications

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

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

        // 解析并下载图片附件
        if let attachmentUrlStr = bestAttemptContent.userInfo["attachment"] as? String, let url = URL(string: attachmentUrlStr) {
            downloadAttachment(url: url) { attachment in
                if let attachment = attachment {
                    bestAttemptContent.attachments = [attachment]
                }
                contentHandler(bestAttemptContent)
            }
        } else {
            contentHandler(bestAttemptContent)
        }
    }
    
    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
    
    private func downloadAttachment(url: URL, completion: @escaping (UNNotificationAttachment?) -> Void) {
        let task = URLSession.shared.downloadTask(with: url) { tmpUrl, _, error in
            guard let tmpUrl = tmpUrl, error == nil else {
                completion(nil)
                return
            }
            let fileManager = FileManager.default
            let tmpDir = NSTemporaryDirectory()
            let destUrl = URL(fileURLWithPath: tmpDir).appendingPathComponent(url.lastPathComponent)
            
            do {
                if fileManager.fileExists(atPath: destUrl.path) {
                    try fileManager.removeItem(at: destUrl)
                }
                try fileManager.moveItem(at: tmpUrl, to: destUrl)
                let attachment = try UNNotificationAttachment(identifier: "", url: destUrl)
                completion(attachment)
            } catch {
                completion(nil)
            }
        }
        task.resume()
    }
}
        `);
        // 写入Extension的Info.plist
        fs.writeFileSync(path.join(extensionPath, 'Info.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>CFBundleDisplayName</key>
    <string>NotificationService</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).notification-service</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>NotificationService</string>
    <key>CFBundlePackageType</key>
    <string>XPC!</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>NSExtension</key>
    <dict>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.usernotifications.service</string>
        <key>NSExtensionPrincipalClass</key>
        <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
    </dict>
</dict>
</plist>
        `);
      }
      return config;
    },
  ]);

  return config;
};
  1. 在app.json中引入自定义插件:
{
  "expo": {
    "plugins": [
      "./plugins/notification-service-extension.js",
      "expo-notifications"
    ]
  }
}
  1. 运行eas build --platform ios完成构建测试。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 23:47:18