flutter Notification Service Extension errors(Error output from CocoaPods,unknown ISA PBXFileSystemSynchronized)...

声明:作者声明此文章为原创,未经作者同意,请勿转载,若转载,务必注明本站出处,本平台保留追究侵权法律责任的权利。
全栈老韩
全栈工程师,擅长iOS App开发、前端(vue、react、nuxt、小程序&Taro)开发、Flutter、React Native、后端(midwayjs、golang、express、koa)开发、docker容器、seo优化等。

一、背景

在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).

issue-1 pod error

问题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'

issue-2 cycle inside runner

问题3(可能会遇到这样的问题,我也是在这个过程中碰到过):

RuntimeError - PBXGroup attempted to initialize an object with unknown ISA PBXFileSystemSynchronizedRootGroup

这个问题需要删除掉Notification Service Extension,然后重新新建.

三、步骤:

3.1 给iOS工程添加Notification Service Extension

添加Notification Service Extension
add Notification Service Extension

choose 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
convert to Group

这个解决方法,感谢链接:https://github.com/CocoaPods/CocoaPods/issues/12456
github fix

3.4 接着运行代码,遇到build的报错

issue-2 cycle inside runner
这个问题有点奇怪,我也查了好久,才从网上找到一个解决办法:

  • 选择主target,进入到Build Phrases的设置下
    build phrases setting
  • 将Embed Foundation Extensions这项,移动到CopyBundle Resources这项正下面.
    move position result
  • 然后运行,没有报错。
    run success

参考链接:https://stackoverflow.com/questions/77138968/handling-cycle-inside-runner-building-could-produce-unreliable-results-after-up

四、添加即时通知 - time sensitive notifications

4.1 设置push notification capability

按照下面截图中的步骤,添加push notifications
push notifications

会生成entitlements文件和capability
push notifications result

4.2 配置ios deployment target

因为Xcode默认会配置ios deployment target,所以我们要检查适配版本,修改为我们想要的版本.
截图是默认的18.2,但现阶段不可能配置到这个版本。
default ios deployment target
修改为15.0
set to ios15.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配置。

五、总结

其实也反反复复遇到过其他问题,但是解决了,这里篇幅有限,暂时记录这些,希望对有的同学有帮助.

暂无评论,快来发表第一条评论吧