using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEditor.iOS.Xcode.Custom; using UnityEngine; public class XCodeProjectMod { // Channel 插件存放目录(BuildIpa 前由 ClientPackage.BuildIpa 从 _sdkPath 拷贝到 Assets/Plugins/iOS/Channel) private static readonly string iOSPluginPath = Application.dataPath + "/Plugins/iOS/Channel"; // ===== 渠道标识:与 ClientPackage 中 VersionConfig.appId / sdkFileName 对应 ===== private const string CHANNEL_SGHY = "sghy"; // 原有渠道(fimpfunSDK + Firebase/Facebook/Adjust) private const string CHANNEL_HYYH = "hyyh"; // 欢游硬核(HGameSDK) private static string s_Channel = null; /// 当前打包渠道,未识别到时按 sghy 处理,保证原有打包行为不变 private static string Channel { get { if (s_Channel == null) { s_Channel = DetectChannel(); } return s_Channel; } } private static bool IsSghy { get { return Channel == CHANNEL_SGHY; } } private static bool IsHyyh { get { return Channel == CHANNEL_HYYH; } } private static string DetectChannel() { return VersionConfig.Get().appId;; } [UnityEditor.Callbacks.PostProcessBuild(999)] public static void OnPostprocessBuild(BuildTarget buildTarget, string path) { if (buildTarget != BuildTarget.iOS) { return; } // 一次 Editor 会话可能连续打多个渠道的包,这里重新识别渠道 s_Channel = null; Debug.Log("[XCodeProjectMod] 当前 iOS 渠道: " + Channel); DoPBXProject(path); BuildPlist(path); ModifyFile(path); // SPM 依赖(Firebase / Facebook / Adjust)为 sghy 渠道使用,需要通过 .xcworkspace 管理 if (IsSghy) { CreateWorkspaceWithSPM(path); } } static void DoPBXProject(string path) { string _projectPath = PBXProject.GetPBXProjectPath(path); PBXProject project = new PBXProject(); project.ReadFromString(File.ReadAllText(_projectPath)); string targetGUID = project.TargetGuidByName(PBXProject.GetUnityTargetName()); string fwTargetGUID = project.TargetGuidByName("UnityFramework"); // 1.1 环境要求:iOS 13.0 及以上 project.SetBuildProperty(targetGUID, "IPHONEOS_DEPLOYMENT_TARGET", "13.0"); project.SetBuildProperty(fwTargetGUID, "IPHONEOS_DEPLOYMENT_TARGET", "13.0"); project.AddBuildProperty(targetGUID, "OTHER_LDFLAGS", "-ObjC"); //UnityFramework project.AddBuildProperty(fwTargetGUID, "OTHER_LDFLAGS", "-ObjC"); // sghy 渠道:Firebase 调试日志参数 if (IsSghy) { project.AddBuildProperty(targetGUID, "OTHER_LDFLAGS", "-FIRAnalyticsDebugEnabled"); project.AddBuildProperty(targetGUID, "OTHER_LDFLAGS", "-FIRDebugEnabled"); project.AddBuildProperty(targetGUID, "OTHER_LDFLAGS", "-FIRAnalyticsVerboseLoggingEnabled"); // WebKit(文档要求) project.AddFrameworkToProject(fwTargetGUID, "WebKit.framework", true); // 添加 GoogleService-Info.plist 和 funshine.plist 到工程 string[] configPlists = { "GoogleService-Info.plist", "funshine.plist" }; foreach (var plistName in configPlists) { string srcPath = iOSPluginPath + "/" + plistName; if (File.Exists(srcPath)) { string destPath = path + "/" + plistName; File.Copy(srcPath, destPath, true); string fileGuid = project.AddFile(plistName, plistName, PBXSourceTree.Source); project.AddFileToBuild(targetGUID, fileGuid); } else { Debug.LogWarning("[XCodeProjectMod] " + plistName + " 未找到: " + srcPath); } } // hfsdk.xcframework 添加到工程并设置为 Embed & Sign(sghy 渠道) string hfFrameworkPath = path + "/Frameworks/hfsdk.xcframework"; if (Directory.Exists(hfFrameworkPath)) { string fileGuid = project.AddFile(hfFrameworkPath, "Frameworks/hfsdk.xcframework", PBXSourceTree.Source); project.AddFileToBuild(fwTargetGUID, fileGuid); // Embed & Sign: 添加到 main target 的 Embed Frameworks project.AddFileToBuild(targetGUID, fileGuid); } project.AddFrameworkToProject(fwTargetGUID, "AdServices.framework", true); project.AddFrameworkToProject(fwTargetGUID, "AdSupport.framework", true); project.AddFrameworkToProject(fwTargetGUID, "AppTrackingTransparency.framework", true); project.AddFrameworkToProject(fwTargetGUID, "StoreKit.framework", true); } // hyyh 渠道:HGameSDK.xcframework / HGameChannelSdk.xcframework 设置为 Embed & Sign if (IsHyyh) { AddEmbedFramework(project, path, fwTargetGUID, targetGUID, "HGameSDK.xcframework"); AddEmbedFramework(project, path, fwTargetGUID, targetGUID, "HGameChannelSdk.xcframework"); } File.WriteAllText(_projectPath, project.WriteToString()); } /// /// 将导出工程 Frameworks 目录下的 xcframework 加入工程并设置 Embed & Sign。 /// Unity 会把 Assets/Plugins/iOS 下的 .xcframework 拷贝到 <导出工程>/Frameworks 下。 /// private static void AddEmbedFramework(PBXProject project, string projectPath, string fwTargetGUID, string targetGUID, string frameworkName) { string relPath = "Frameworks/" + frameworkName; string frameworkPath = projectPath + "/" + relPath; if (!Directory.Exists(frameworkPath)) { // Unity 不同版本放置位置可能不同(Frameworks 下或插件子目录),这里递归兜底查找 string[] found = Directory.GetDirectories(projectPath, frameworkName, SearchOption.AllDirectories); if (found == null || found.Length == 0) { Debug.LogWarning("[XCodeProjectMod] " + frameworkName + " 未找到: " + frameworkPath); return; } frameworkPath = found[0]; relPath = frameworkPath.Substring(projectPath.Length).TrimStart('/', '\\').Replace("\\", "/"); } string fileGuid = project.AddFile(frameworkPath, relPath, PBXSourceTree.Source); project.AddFileToBuild(fwTargetGUID, fileGuid); // Link Binary With Libraries project.AddFileToBuild(targetGUID, fileGuid); // Embed Frameworks Debug.Log("[XCodeProjectMod] 已添加(Embed & Sign): " + relPath); } /// /// 生成 Unity-iPhone.xcworkspace 结构,仅包含 contents.xcworkspacedata。 /// SPM 依赖由开发者在 Xcode 中手动 File → Add Package Dependency 添加并选择所需产品包: /// - Firebase: https://github.com/firebase/firebase-ios-sdk /// - Facebook: https://github.com/facebook/facebook-ios-sdk /// - Adjust: https://github.com/adjust/ios_sdk /// Package.resolved 由 Xcode 在 resolve 后自动生成,代码不做处理。 /// private static void CreateWorkspaceWithSPM(string projectPath) { string workspacePath = projectPath + "/Unity-iPhone.xcworkspace"; string xcodeprojAbsPath = projectPath + "/Unity-iPhone.xcodeproj"; if (!Directory.Exists(xcodeprojAbsPath)) { Debug.LogWarning("[XCodeProjectMod] xcodeproj not found, skip creating workspace: " + xcodeprojAbsPath); return; } Directory.CreateDirectory(workspacePath); string contentsXml = @" "; File.WriteAllText(workspacePath + "/contents.xcworkspacedata", contentsXml); Debug.Log("[XCodeProjectMod] Unity-iPhone.xcworkspace created at: " + workspacePath); Debug.Log("[XCodeProjectMod] 请在 Xcode 中打开 .xcworkspace,手动 File → Add Package Dependency 添加 SPM 仓库:"); Debug.Log("[XCodeProjectMod] - Firebase: https://github.com/firebase/firebase-ios-sdk"); Debug.Log("[XCodeProjectMod] - Facebook: https://github.com/facebook/facebook-ios-sdk"); Debug.Log("[XCodeProjectMod] - Adjust: https://github.com/adjust/ios_sdk"); } private static void BuildPlist(string path) { string _plistPath = path + "/Info.plist"; PlistDocument _plist = new PlistDocument(); _plist.ReadFromString(File.ReadAllText(_plistPath)); PlistElementDict _rootDict = _plist.root; _rootDict.SetString("NSUserTrackingUsageDescription", "Please rest assured, granting permission will not access your private information on other websites. This permission is only used to identify the device and ensure service security and enhance browsing experience"); // sghy 渠道:Facebook(funshine.plist: kFacebookAppID / kFacebookAppSecret) if (IsSghy) { _rootDict.SetString("FacebookAppID", "1445747540428317"); _rootDict.SetString("FacebookClientToken", "1103765897fb67ffc898e6a8ee2d9aec"); _rootDict.SetString("FacebookDisplayName", "EZ PZ: Byte Heroes"); PlistElementArray nsURLTypes = _rootDict.CreateArray("CFBundleURLTypes"); var dict0 = nsURLTypes.AddDict(); dict0.SetString("CFBundleTypeRole", "Editor"); dict0.SetString("CFBundleURLName", "fb"); var schemesArray = dict0.CreateArray("CFBundleURLSchemes"); schemesArray.AddString("fb1445747540428317"); var array1 = _rootDict.CreateArray("LSApplicationQueriesSchemes"); array1.AddString("fbapi"); array1.AddString("fb-messenger-api"); array1.AddString("fbauth2"); array1.AddString("fbshareextension"); // 适配欧盟地区政策法规 - Google Analytics 默认允许 _rootDict.SetBoolean("GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE", true); _rootDict.SetBoolean("GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE", true); _rootDict.SetBoolean("GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA", true); _rootDict.SetBoolean("GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS", true); } // App 名称本地化(需要 InfoPlist.strings 中存在 CFBundleDisplayName,见 CreateInfoPlistStrings) if (HasDisplayName()) { _rootDict.SetBoolean("Application has localized display name", true); _rootDict.SetString("CFBundleDevelopmentRegion", "english"); _rootDict.SetString("Localization native development region", "english"); } // ===== 启动屏白底(Info.plist 方式:UILaunchScreen -> Background color)===== // iOS 13+ 支持用 Info.plist 的 UILaunchScreen 字典描述启动屏,无需 storyboard。 // 注意:UILaunchStoryboardName 优先级高于 UILaunchScreen,必须移除 Unity 生成的 // LaunchScreen storyboard 引用,否则下面的配置不会生效。 if (_rootDict.values.ContainsKey("UILaunchStoryboardName")) { _rootDict.values.Remove("UILaunchStoryboardName"); } // UIColorName 只能引用 asset catalog 中的「命名颜色」,这里在 Unity 生成的 // Images.xcassets 中创建一个纯白的 Color Set(folder reference 会自动被编译) string launchScreenColorName = "LaunchScreenBackground"; CreateLaunchScreenColorSet(path, launchScreenColorName); PlistElementDict launchScreenDict = _rootDict.CreateDict("UILaunchScreen"); launchScreenDict.SetString("UIColorName", launchScreenColorName); // ===================================================================== File.WriteAllText(_plistPath, _plist.WriteToString()); } /// /// 在 Unity 生成的 Images.xcassets 中创建纯白的 Color Set, /// 供 Info.plist 的 UILaunchScreen.UIColorName 引用。 /// private static void CreateLaunchScreenColorSet(string projectPath, string colorName) { string[] xcassetsDirs = Directory.GetDirectories(projectPath, "*.xcassets", SearchOption.AllDirectories); if (xcassetsDirs == null || xcassetsDirs.Length == 0) { Debug.LogWarning("[XCodeProjectMod] 未找到 *.xcassets,无法创建启动屏白色 Color Set"); return; } string contentsJson = "{\n" + " \"colors\" : [\n" + " {\n" + " \"color\" : {\n" + " \"color-space\" : \"srgb\",\n" + " \"components\" : {\n" + " \"alpha\" : \"1.000\",\n" + " \"blue\" : \"1.000\",\n" + " \"green\" : \"1.000\",\n" + " \"red\" : \"1.000\"\n" + " }\n" + " },\n" + " \"idiom\" : \"universal\"\n" + " }\n" + " ],\n" + " \"info\" : {\n" + " \"author\" : \"xcode\",\n" + " \"version\" : 1\n" + " }\n" + "}\n"; foreach (var xcassets in xcassetsDirs) { // 跳过 Pods 等第三方目录 if (xcassets.Replace("\\", "/").Contains("/Pods/")) { continue; } string colorSetDir = Path.Combine(xcassets, colorName + ".colorset"); Directory.CreateDirectory(colorSetDir); File.WriteAllText(Path.Combine(colorSetDir, "Contents.json"), contentsJson); Debug.Log("[XCodeProjectMod] LaunchScreen 白色 Color Set 已生成: " + colorSetDir); } } private static void ModifyFile(string projectPath) { if (IsHyyh) { ModifyFileForHyyh(projectPath); } else { ModifyFileForSghy(projectPath); } // 创建 InfoPlist.strings(App 名称本地化) CreateInfoPlistStrings(projectPath); // Sign In with Apple entitlements SetupEntitlements(projectPath); } /// /// sghy 渠道(fimpfunSDK + Firebase/Facebook/Adjust)的 AppController 注入 /// private static void ModifyFileForSghy(string projectPath) { //修改UnityAppController.mm 文件 var _xclass = new XClass(projectPath + "/Classes/UnityAppController.mm"); //引入fimpfunSDK _xclass.WriteBelow("#include \"PluginBase/AppDelegateListener.h\"", "#import \"fimpfunSDK.h\"\n#import "); //unity交互调用的方法 string newCode = "\n" + "extern \"C\" void IOSUniyMessageHandle(const char* jsonString) {\n" + " [GetAppController().fimpfunBridge HandleGameMessage:[NSString stringWithUTF8String:jsonString]];\n" + "}"; _xclass.WriteBelow("extern \"C\" ScreenOrientation UnityCurrentOrientation() { return GetAppController().unityView.contentOrientation; }", newCode); //初始化fimpfunSDK newCode = "\n" + " _fimpfunBridge = [[FimpfunBridge alloc] init];\n [_fimpfunBridge YngameSDKInit:application didFinishLaunchingWithOptions:launchOptions];"; _xclass.WriteBelow("::printf(\"-> applicationDidFinishLaunching()\\n\");", newCode); // fimpfunSDK 生命周期 _xclass.WriteBelow("- (BOOL)application:(UIApplication*)app openURL:(NSURL*)url options:(NSDictionary*)options\n{", "\n [_fimpfunBridge application:app openURL:url options:options];"); _xclass.WriteBelow("::printf(\"-> applicationDidBecomeActive()\\n\");", " [_fimpfunBridge applicationDidBecomeActive:application];\n if (@available(iOS 14, *)) {\n static dispatch_once_t onceToken;\n dispatch_once(&onceToken, ^{\n [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {\n NSLog(@\"[ATT] Authorization status: %ld\", (long)status);\n }];\n });\n }"); //修改UnityAppController.h 文件 _xclass = new XClass(projectPath + "/Classes/UnityAppController.h"); _xclass.WriteBelow("@class DisplayConnection;", "@class FimpfunBridge;"); _xclass.WriteBelow("DisplayConnection* _mainDisplay;", "FimpfunBridge* _fimpfunBridge;"); _xclass.WriteBelow("@property (readonly, copy, nonatomic) DisplayConnection* mainDisplay;", "@property (readonly, copy, nonatomic) FimpfunBridge* fimpfunBridge;"); // 3.1 适配欧盟政策:在 viewDidAppear 中调用 adaptingEUGDPR(文档要求) // UnityViewControllerBase.mm 在较新 Unity 版本(如 2022.3+)中可能不存在 string viewControllerPath = projectPath + "/Classes/UI/UnityViewControllerBase.mm"; if (System.IO.File.Exists(viewControllerPath)) { var _viewController = new XClass(viewControllerPath); _viewController.WriteBelow("@implementation UnityViewControllerBase", "\n- (void)viewDidAppear:(BOOL)animated {\n [super viewDidAppear:animated];\n [GetAppController().fimpfunBridge viewDidAppear];\n}\n"); } else { Debug.LogError("[XCodeProjectMod] UnityViewControllerBase.mm 文件不存在,跳过 viewDidAppear 注入。此 Unity 版本可能已移除该文件。"); } } /// /// hyyh 渠道(HGameSDK)的 AppController 注入: /// 引入 UniversalSDK、暴露 IOSUniyMessageHandle 给 C#(AotSdkUtility)、初始化并转发 AppDelegate 生命周期。 /// private static void ModifyFileForHyyh(string projectPath) { string mmPath = projectPath + "/Classes/UnityAppController.mm"; string hPath = projectPath + "/Classes/UnityAppController.h"; if (!File.Exists(mmPath) || !File.Exists(hPath)) { Debug.LogError("[XCodeProjectMod][hyyh] UnityAppController 文件不存在: " + projectPath + "/Classes"); return; } // 引入 UniversalSDK(渠道插件 wgyx/UniversalSDK.h) InsertBelowPattern(mmPath, @"#include\s+""PluginBase/AppDelegateListener\.h""", "#import \"UniversalSDK.h\"", "import UniversalSDK"); // C# 调用入口,与 AotSdkUtility.IOSUniyMessageHandle 对应 string entryCode = "\n" + "extern \"C\" void IOSUniyMessageHandle(const char* jsonString) {\n" + " [GetAppController().universalSDK HandleUnityMessage:[NSString stringWithUTF8String:jsonString]];\n" + "}"; InsertBelowPattern(mmPath, @"extern\s+""C""\s+ScreenOrientation\s+UnityCurrentOrientation\s*\(\s*\)", entryCode, "IOSUniyMessageHandle"); // 初始化 HGameSDK(必须在主线程、在 delegate 设置之后) string initCode = "\n" + " _universalSDK = [[UniversalSDK alloc] init];\n" + " [_universalSDK HGameSDKInit:application didFinishLaunchingWithOptions:launchOptions];"; InsertBelowPattern(mmPath, @"::printf\(""-> applicationDidFinishLaunching\(\\n""\);", initCode, "initSdk"); // ===== UIApplicationDelegate 生命周期转发(HGameSDK 必接)===== InsertBelowPattern(mmPath, @"- \(BOOL\)application:[^\n]*openURL:[^\n]*\n\s*\{", " [_universalSDK application:[UIApplication sharedApplication] openURL:url options:options];", "openURL"); InsertBelowPattern(mmPath, @"- \(void\)applicationWillResignActive:[^\n]*\n\s*\{", " [_universalSDK applicationWillResignActive:[UIApplication sharedApplication]];", "willResignActive"); InsertBelowPattern(mmPath, @"- \(void\)applicationDidEnterBackground:[^\n]*\n\s*\{", " [_universalSDK applicationDidEnterBackground:[UIApplication sharedApplication]];", "didEnterBackground"); InsertBelowPattern(mmPath, @"- \(void\)applicationWillEnterForeground:[^\n]*\n\s*\{", " [_universalSDK applicationWillEnterForeground:[UIApplication sharedApplication]];", "willEnterForeground"); InsertBelowPattern(mmPath, @"- \(void\)applicationDidBecomeActive:[^\n]*\n\s*\{", " [_universalSDK applicationDidBecomeActive:[UIApplication sharedApplication]];", "didBecomeActive"); InsertBelowPattern(mmPath, @"- \(void\)applicationWillTerminate:[^\n]*\n\s*\{", " [_universalSDK applicationWillTerminate:[UIApplication sharedApplication]];", "willTerminate"); InsertBelowPattern(mmPath, @"- \(void\)application:[^\n]*didRegisterForRemoteNotificationsWithDeviceToken:[^\n]*\n\s*\{", " [_universalSDK application:[UIApplication sharedApplication] didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];", "didRegisterToken"); InsertBelowPattern(mmPath, @"- \(void\)application:[^\n]*didFailToRegisterForRemoteNotificationsWithError:[^\n]*\n\s*\{", " [_universalSDK application:[UIApplication sharedApplication] didFailToRegisterForRemoteNotificationsWithError:error];", "didFailRegister"); // 静默远程通知:优先复用 Unity 已有的现代回调;Unity 未声明该方法时补一个完整实现(文档必接) string remotePattern = @"- \(void\)application:[^\n]*didReceiveRemoteNotification:[^\n]*fetchCompletionHandler:[^\n]*\n\s*\{"; string remoteForward = " [_universalSDK application:[UIApplication sharedApplication] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];"; if (!InsertBelowPattern(mmPath, remotePattern, remoteForward, "didReceiveRemoteNotification")) { AppendMethod(mmPath, "- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler\n{\n" + " [_universalSDK application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];\n}", "didReceiveRemoteNotification"); } // 屏幕方向:渠道返回 0 表示不接管,继续走 Unity 原有逻辑 InsertBelowPattern(mmPath, @"- \(NSUInteger\)application:[^\n]*supportedInterfaceOrientationsForWindow:[^\n]*\n\s*\{", " NSUInteger sdkOrientations = [_universalSDK application:[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:window];\n" + " if (sdkOrientations != 0) {\n return sdkOrientations;\n }", "supportedInterfaceOrientations"); // Universal Link:优先复用 Unity 已有回调;没有则补一个完整实现(文档必接) string continuePattern = @"- \(BOOL\)application:[^\n]*continueUserActivity:[^\n]*\n\s*\{"; string continueForward = " if ([_universalSDK application:[UIApplication sharedApplication] continueUserActivity:userActivity restorationHandler:restorationHandler]) {\n return YES;\n }"; if (!InsertBelowPattern(mmPath, continuePattern, continueForward, "continueUserActivity")) { AppendMethod(mmPath, "- (BOOL)application:(UIApplication*)application continueUserActivity:(NSUserActivity*)userActivity restorationHandler:(void (^)(NSArray*))restorationHandler\n{\n" + " if ([_universalSDK application:application continueUserActivity:userActivity restorationHandler:restorationHandler]) {\n return YES;\n }\n" + " return NO;\n}", "continueUserActivity"); } // 修改 UnityAppController.h var _xclass = new XClass(hPath); _xclass.WriteBelow("@class DisplayConnection;", "@class UniversalSDK;"); _xclass.WriteBelow("DisplayConnection* _mainDisplay;", "UniversalSDK* _universalSDK;"); _xclass.WriteBelow("@property (readonly, copy, nonatomic) DisplayConnection* mainDisplay;", "@property (readonly, copy, nonatomic) UniversalSDK* universalSDK;"); } /// /// 按正则查找注入点,并在该行之后插入代码。找不到时只告警,不修改文件。 /// private static bool InsertBelowPattern(string filePath, string pattern, string text, string tag) { if (!File.Exists(filePath)) { Debug.LogWarning("[XCodeProjectMod] 文件不存在,跳过注入[" + tag + "]: " + filePath); return false; } string content = File.ReadAllText(filePath); Match match = Regex.Match(content, pattern); if (!match.Success) { Debug.LogWarning("[XCodeProjectMod] 未找到注入点[" + tag + "],跳过: " + pattern); return false; } int lineEnd = content.IndexOf('\n', match.Index + match.Length - 1); if (lineEnd < 0) { lineEnd = content.Length; } content = content.Substring(0, lineEnd) + "\n" + text + content.Substring(lineEnd); File.WriteAllText(filePath, content); return true; } /// /// 在 UnityAppController.mm 的 @implementation UnityAppController 块末尾追加一个完整的 delegate 方法。 /// 用于 Unity 未声明该方法、但渠道 SDK 要求必须转发的场景(文档必接项)。 /// private static bool AppendMethod(string filePath, string methodText, string tag) { if (!File.Exists(filePath)) { Debug.LogWarning("[XCodeProjectMod] 文件不存在,跳过追加[" + tag + "]: " + filePath); return false; } string content = File.ReadAllText(filePath); int implIndex = content.IndexOf("@implementation UnityAppController", StringComparison.Ordinal); if (implIndex < 0) { Debug.LogError("[XCodeProjectMod] 未找到 @implementation UnityAppController,无法追加[" + tag + "]"); return false; } int endIndex = content.IndexOf("@end", implIndex, StringComparison.Ordinal); if (endIndex < 0) { Debug.LogError("[XCodeProjectMod] 未找到 @end,无法追加[" + tag + "]"); return false; } content = content.Substring(0, endIndex) + "\n" + methodText + "\n\n" + content.Substring(endIndex); File.WriteAllText(filePath, content); Debug.Log("[XCodeProjectMod] 已追加 delegate 方法[" + tag + "]"); return true; } /// /// 各渠道的 App 显示名称:[渠道] = { 英文, 简体中文 } /// 未配置的渠道不会生成本地化名称,直接使用 PlayerSettings 中的 Product Name。 /// private static readonly Dictionary DisplayNames = new Dictionary { { CHANNEL_SGHY, new[] { "EZ PZ", "懒人三国" } }, // TODO hyyh 渠道确认 App 名称后在这里补充:{ CHANNEL_HYYH, new[] { "英文名", "中文名" } }, }; private static bool HasDisplayName() { return DisplayNames.ContainsKey(Channel); } /// /// 创建 InfoPlist.strings 文件用于 App 名称本地化 /// private static void CreateInfoPlistStrings(string projectPath) { if (!HasDisplayName()) { Debug.LogWarning("[XCodeProjectMod] 渠道 " + Channel + " 未配置 App 显示名称,跳过 InfoPlist.strings 本地化"); return; } string[] names = DisplayNames[Channel]; // 英文(默认) string enDir = projectPath + "/English.lproj"; if (!Directory.Exists(enDir)) { Directory.CreateDirectory(enDir); } string enStringsPath = enDir + "/InfoPlist.strings"; if (!File.Exists(enStringsPath)) { File.WriteAllText(enStringsPath, "CFBundleDisplayName = \"" + names[0] + "\";\n"); } // 中文 string zhDir = projectPath + "/zh-Hans.lproj"; if (!Directory.Exists(zhDir)) { Directory.CreateDirectory(zhDir); } string zhStringsPath = zhDir + "/InfoPlist.strings"; if (!File.Exists(zhStringsPath)) { File.WriteAllText(zhStringsPath, "CFBundleDisplayName = \"" + names[1] + "\";\n"); } // 将 InfoPlist.strings 添加到 Xcode 工程 string _projectPath = PBXProject.GetPBXProjectPath(projectPath); try { PBXProject project = new PBXProject(); project.ReadFromString(File.ReadAllText(_projectPath)); string targetGUID = project.TargetGuidByName(PBXProject.GetUnityTargetName()); // 英文 string enRelPath = "English.lproj/InfoPlist.strings"; string enFileGuid = project.AddFile(enRelPath, enRelPath, PBXSourceTree.Source); project.AddFileToBuild(targetGUID, enFileGuid); // 中文 string zhRelPath = "zh-Hans.lproj/InfoPlist.strings"; string zhFileGuid = project.AddFile(zhRelPath, zhRelPath, PBXSourceTree.Source); project.AddFileToBuild(targetGUID, zhFileGuid); File.WriteAllText(_projectPath, project.WriteToString()); } catch (System.Exception e) { Debug.LogError("[XCodeProjectMod] 将 InfoPlist.strings 添加到 Xcode 工程失败: " + e.Message); } } /// /// Sign In with Apple entitlements(与渠道无关,所有渠道都需要) /// private static void SetupEntitlements(string projectPath) { string _projectPath = PBXProject.GetPBXProjectPath(projectPath); try { PBXProject project = new PBXProject(); project.ReadFromString(File.ReadAllText(_projectPath)); string targetGUID = project.TargetGuidByName(PBXProject.GetUnityTargetName()); AddEntitlements(projectPath, project, targetGUID); File.WriteAllText(_projectPath, project.WriteToString()); } catch (System.Exception e) { Debug.LogError("[XCodeProjectMod] 添加 entitlements 失败: " + e.Message); } } /// /// 添加 Sign In with Apple 等 entitlements /// private static void AddEntitlements(string projectPath, PBXProject project, string targetGUID) { string entPath = projectPath + "/" + PBXProject.GetUnityTargetName() + ".entitlements"; // 如果 entitlements 文件不存在,创建 if (!File.Exists(entPath)) { string entContent = "\n" + "\n" + "\n" + "\n" + "\tcom.apple.developer.applesignin\n" + "\t\n" + "\t\tDefault\n" + "\t\n" + "\n" + "\n"; File.WriteAllText(entPath, entContent); } else { // 已有 entitlements 文件,检查是否已有 Sign In with Apple string existingContent = File.ReadAllText(entPath); if (!existingContent.Contains("com.apple.developer.applesignin")) { existingContent = existingContent.Replace("", "\tcom.apple.developer.applesignin\n" + "\t\n" + "\t\tDefault\n" + "\t\n" + ""); File.WriteAllText(entPath, existingContent); } } // 设置 CODE_SIGN_ENTITLEMENTS build property(不加入 Copy Bundle Resources) // entitlements 文件只需要在 Build Settings 中引用,不需要加入任何 Build Phase string entFileName = PBXProject.GetUnityTargetName() + ".entitlements"; project.SetBuildProperty(targetGUID, "CODE_SIGN_ENTITLEMENTS", entFileName); } private static string GetBuildPath() { string _buildPath = Application.dataPath + "/../IOS_BUILD"; if (!Directory.Exists(_buildPath)) { Directory.CreateDirectory(_buildPath); } return new DirectoryInfo(_buildPath).FullName; } public static string[] GetBuildLevels() { List _sceneNames = new List(); foreach (var _scene in EditorBuildSettings.scenes) { if (_scene == null) { continue; } if (_scene.enabled) { _sceneNames.Add(_scene.path); } } return _sceneNames.ToArray(); } private static void CopyDirectory(string sourceDir, string destDir) { Directory.CreateDirectory(destDir); foreach (var file in Directory.GetFiles(sourceDir)) { string destFile = Path.Combine(destDir, Path.GetFileName(file)); File.Copy(file, destFile, true); } foreach (var dir in Directory.GetDirectories(sourceDir)) { string destSubDir = Path.Combine(destDir, Path.GetFileName(dir)); CopyDirectory(dir, destSubDir); } } private static bool s_IsAppend = false; public static void BuildIOSProject_Append() { string _buildPath = GetBuildPath(); UnityEngine.Debug.Log(_buildPath); if (string.IsNullOrEmpty(_buildPath)) { return; } s_IsAppend = true; PlayerSettings.SetScriptingBackend(BuildTargetGroup.iOS, ScriptingImplementation.IL2CPP); BuildPipeline.BuildPlayer(ClientPackage.baseLevels, _buildPath, BuildTarget.iOS, BuildOptions.AcceptExternalModificationsToPlayer); } public static void BuildIOSProject_Replace() { s_IsAppend = false; PlayerSettings.SetScriptingBackend(BuildTargetGroup.iOS, ScriptingImplementation.IL2CPP); BuildPipeline.BuildPlayer(ClientPackage.baseLevels, GetBuildPath(), BuildTarget.iOS, BuildOptions.None); } [MenuItem("Build/ipa")] public static void BuildIPA() { var p = new System.Diagnostics.Process(); p.StartInfo.FileName = "osascript"; p.StartInfo.Arguments = string.Format("-e 'tell application \"Terminal\" \n activate \n do script \"cd {0} && sh {1} {2}\" \n end tell'", Application.dataPath + "/../", "buildipa.sh", GetBuildPath()); p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = false; p.Start(); p.WaitForExit(); } }