一、背景
在flutter工程中为iOS添加Notification Service Extension时,遇到了一系列的编译和运行问题,在此做一个记录。
本地环境environment
:
- Xcode: 16.2
- Simulator: iOS 17.5
- Flutter 3.27.0-0.1.pre • channel beta • https://github.com/flutter/flutter.git
Framework • revision 2e2c358c9b (4 months ago) • 2024-10-22 11:02:13 -0400
Engine • revision af0f0d559c
Tools • Dart 3.6.0 (build 3.6.0-334.3.beta) • DevTools 2.40.1 - ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-darwin23]
- CocoaPods : 1.15.2
二、问题
问题1:
Error output from CocoaPods:
↳
Searching for inspections failed: undefined method `map' for nil
Error running pod install
Error launching application on iPhone SE (3rd generation).

问题2:
Launching lib/main.dart on iPhone SE (3rd generation) in debug mode...
Running pod install...
Running Xcode build...
Xcode build done. 304.5s
Failed to build iOS app
Error (Xcode): Cycle inside Runner; building could produce unreliable results.
Cycle details:
→ That command depends on command in Target 'Runner': script phase “[CP] Copy Pods Resources”
○ That command depends on command in Target 'Runner': script phase “[CP] Embed Pods Frameworks”
○ That command depends on command in Target 'Runner': script phase “Thin Binary”
○ Target 'Runner' has process command with output '/Users/edy/Desktop/xxxproject/build/ios/Debug-iphonesimulator/Runner.app/Info.plist'
○ Target 'Runner' has copy command from '/Users/edy/Desktop/xxxproject/build/ios/Debug-iphonesimulator/xxxPushExtension.appex' to '/Users/edy/Desktop/xxxproject/build/ios/Debug-iphonesimulator/Runner.app/PlugIns/xxxPushExtension.appex'

问题3(可能会遇到这样的问题,我也是在这个过程中碰到过):
RuntimeError -
PBXGroup
attempted to initialize an object with unknown ISAPBXFileSystemSynchronizedRootGroup
这个问题需要删除掉Notification Service Extension,然后重新新建.
三、步骤:
3.1 给iOS工程添加Notification Service Extension
添加Notification Service Extension

添加完之后的状态folder结构
3.2 编译flutter运行,出现报错:
Launching lib/main.dart on iPhone SE (3rd generation) in debug mode...
Updating project for Xcode compatibility.
Upgrading project.pbxproj
Upgrading Runner.xcscheme
Running pod install...
CocoaPods' output:
↳
Preparing
Analyzing dependencies
Inspecting targets to integrate
CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
――― MARKDOWN TEMPLATE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
### Command
```
/Users/edy/.rvm/rubies/ruby-3.3.5/bin/pod install --verbose
```
### Report
* What did you do?
* What did you expect to happen?
* What happened instead?
### Stack
```
CocoaPods : 1.15.2
Ruby : ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-darwin23]
RubyGems : 3.5.16
Host : macOS 14.6.1 (23G93)
Xcode : 16.2 (16C5032a)
Git : git version 2.39.5 (Apple Git-154)
Ruby lib dir : /Users/edy/.rvm/rubies/ruby-3.3.5/lib
Repositories : cocoapods - git - https://github.com/CocoaPods/Specs.git @ 3574d4d220177427a4fa77c221b01e55996aa84d
trunk - CDN - https://cdn.cocoapods.org/
```
### Plugins
```
cocoapods-deintegrate : 1.0.5
cocoapods-plugins : 1.0.0
cocoapods-search : 1.0.1
cocoapods-trunk : 1.6.0
cocoapods-try : 1.2.0
```
### Podfile
```ruby
# Uncomment this line to define a global platform for your project
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
pod 'KeychainAccess'
pod 'FBAudienceNetwork','= 6.15.0'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '5.0' # required by simple_permission
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
```
### Error
```
RuntimeError - `PBXGroup` attempted to initialize an object with unknown ISA `PBXFileSystemSynchronizedRootGroup` from attributes: `{"isa"=>"PBXFileSystemSynchronizedRootGroup", "exceptions"=>["0453440D2D5DA235003E9BF2"], "explicitFileTypes"=>{}, "explicitFolders"=>[], "path"=>"xxxPushExtension", "sourceTree"=>"<group>"}`
If this ISA was generated by Xcode please file an issue: https://github.com/CocoaPods/Xcodeproj/issues/new
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:359:in `rescue in object_with_uuid'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:349:in `object_with_uuid'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:300:in `block (2 levels) in configure_with_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:299:in `each'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:299:in `block in configure_with_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:296:in `each'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:296:in `configure_with_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:272:in `new_from_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:350:in `object_with_uuid'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:290:in `block in configure_with_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:287:in `each'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:287:in `configure_with_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:272:in `new_from_plist'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:213:in `initialize_from_file'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:113:in `open'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1194:in `block (2 levels) in inspect_targets_to_integrate'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1193:in `each'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1193:in `block in inspect_targets_to_integrate'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/user_interface.rb:64:in `section'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1188:in `inspect_targets_to_integrate'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:107:in `analyze'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:422:in `analyze'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:244:in `block in resolve_dependencies'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/user_interface.rb:64:in `section'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:243:in `resolve_dependencies'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:162:in `install!'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/command/install.rb:52:in `run'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/claide-1.1.0/lib/claide/command.rb:334:in `run'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/command.rb:52:in `run'
/Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/bin/pod:55:in `<top (required)>'
/Users/edy/.rvm/rubies/ruby-3.3.5/bin/pod:25:in `load'
/Users/edy/.rvm/rubies/ruby-3.3.5/bin/pod:25:in `<main>'
/Users/edy/.rvm/rubies/ruby-3.3.5/bin/ruby_executable_hooks:22:in `eval'
/Users/edy/.rvm/rubies/ruby-3.3.5/bin/ruby_executable_hooks:22:in `<main>'
```
――― TEMPLATE END ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
[!] Oh no, an error occurred.
Search for existing GitHub issues similar to yours:
https://github.com/CocoaPods/CocoaPods/search?q=%60PBXGroup%60+attempted+to+initialize+an+object+with+unknown+ISA+%60PBXFileSystemSynchronizedRootGroup%60+from+attributes%3A+%60%7B%22isa%22%3D%3E%22PBXFileSystemSynchronizedRootGroup%22%2C+%22exceptions%22%3D%3E%5B%220453440D2D5DA235003E9BF2%22%5D%2C+%22explicitFileTypes%22%3D%3E%7B%7D%2C+%22explicitFolders%22%3D%3E%5B%5D%2C+%22path%22%3D%3E%22xxxPushExtension%22%2C+%22sourceTree%22%3D%3E%22%3Cgroup%3E%22%7D%60%0AIf+this+ISA+was+generated+by+Xcode+please+file+an+issue%3A+https%3A%2F%2Fgithub.com%2FCocoaPods%2FXcodeproj%2Fissues%2Fnew&type=Issues
If none exists, create a ticket, with the template displayed above, on:
https://github.com/CocoaPods/CocoaPods/issues/new
Be sure to first read the contributing guide for details on how to properly submit a ticket:
https://github.com/CocoaPods/CocoaPods/blob/master/CONTRIBUTING.md
Don't forget to anonymize any private data!
Looking for related issues on cocoapods/cocoapods...
Error output from CocoaPods:
↳
Searching for inspections failed: undefined method `map' for nil
Error running pod install
Error launching application on iPhone SE (3rd generation).
3.3 解决pod install失败的问题
从问题3.1的folder结构中可以看出,文件夹的icon标识不是灰色gray的,而是淡蓝色blue的icon,这个应该是Xcode的bug,需要将新建的Extension文件夹改成group。
右键Extension文件夹,选择Convert to Group
这个解决方法,感谢链接:https://github.com/CocoaPods/CocoaPods/issues/12456
3.4 接着运行代码,遇到build的报错
这个问题有点奇怪,我也查了好久,才从网上找到一个解决办法:
- 选择主target,进入到Build Phrases的设置下
- 将Embed Foundation Extensions这项,移动到CopyBundle Resources这项正下面.
- 然后运行,没有报错。
四、添加即时通知 - time sensitive notifications
4.1 设置push notification capability
按照下面截图中的步骤,添加push notifications
会生成entitlements文件和capability
4.2 配置ios deployment target
因为Xcode默认会配置ios deployment target,所以我们要检查适配版本,修改为我们想要的版本.
截图是默认的18.2,但现阶段不可能配置到这个版本。
修改为15.0
4.3 配置NotificationService方法
配置如下:
//
// NotificationService.swift
// xxxPushExtension
//
// Created by Hamry on 2/13/25.
//
import UserNotifications
import UIKit
import Intents
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)
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
// 获取通信消息
if let options = bestAttemptContent.userInfo["fcm_options"] as? [String: Any], let senderImageURLString = options["image"] as? String
{
// Here you need to download the image data from the URL.
self.getMediaAttachment(for: senderImageURLString) { image in
guard let groupIcon = image else {
contentHandler(bestAttemptContent)
return
}
let avatar = INImage(imageData: groupIcon.pngData()!)
// 消息发送方
let messageSender = INPerson(
personHandle: INPersonHandle(value: nil, type: .unknown),
nameComponents: try? PersonNameComponents(bestAttemptContent.title),
displayName: bestAttemptContent.title,
image: avatar,
contactIdentifier: nil,
customIdentifier: nil,
isMe: false,
suggestionType: .none
)
// 消息接收方
let mePerson = INPerson(
personHandle: INPersonHandle(value: "", type: .unknown),
nameComponents: nil,
displayName: nil,
image: nil,
contactIdentifier: nil,
customIdentifier: nil,
isMe: true,
suggestionType: .none
)
let intent = INSendMessageIntent(recipients: [mePerson],
outgoingMessageType: .outgoingMessageText,
content: bestAttemptContent.body,
speakableGroupName: INSpeakableString(spokenPhrase: bestAttemptContent.title),
conversationIdentifier: nil,
serviceName: nil,
sender: messageSender,
attachments: nil)
// sender
intent.setImage(avatar, forParameterNamed: \.speakableGroupName)
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)
do {
let messageContent = try request.content.updating(from: intent)
contentHandler(messageContent)
} catch {
print(error.localizedDescription)
contentHandler(bestAttemptContent)
}
}
}else {
contentHandler(bestAttemptContent)
}
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
extension NotificationService {
// 通过本地 url 地址 获取图片资源
private func getMediaAttachment(for urlString: String, completion: @escaping (UIImage?) -> Void) {
// 1
guard let url = URL(string: urlString) else {
completion(nil)
return
}
// 2 通过远程图片URL下载图片
downloadImage(forURL: url) { result in
// 3
guard let image = try? result.get() else {
completion(nil)
return
}
// 4
completion(image)
}
}
// 通过远程图片url地址 下载图片文件
public enum DownloadError: Error {
case emptyData
case invalidImage
}
private func downloadImage(forURL url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(DownloadError.emptyData))
return
}
guard let image = UIImage(data: data) else {
completion(.failure(DownloadError.invalidImage))
return
}
completion(.success(image))
}
task.resume()
}
}
4.4 运行,没有问题
注意NotificationService.swift
中的INPerson
等是iOS 15.0才开始使用的特性,所以需要4.2种的ios deployment target配置。
五、总结
其实也反反复复遇到过其他问题,但是解决了,这里篇幅有限,暂时记录这些,希望对有的同学有帮助.