#if UNITY_IOS using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using System.IO; using System.Text; using System.Collections.Generic; using UnityEngine; /// /// iOS 出包后自动替换高清 App 图标,解决 Unity 压缩导致图标模糊的问题。 /// 使用 IPostprocessBuildWithReport 确保在所有 PostProcessBuild 之后执行。 /// 使用方式:将精确尺寸的 PNG 图标放入 Assets/Icons/iOS/ 目录下,打包时自动生效。 /// 命名规则:iPhone-App-180.png(设备-用途-像素尺寸)可被 Xcode Asset Catalog 自动识别。 /// public class iOSIconPostProcessor : IPostprocessBuildWithReport { public int callbackOrder => int.MaxValue; private const string SOURCE_ICON_DIR = "Assets/Icons/iOS/"; // 图标配置: (idiom, scale, 点尺寸, 源文件名, 目标文件名) // 源文件在 Assets/Icons/iOS/ 下,目标文件写入 Xcode 的 AppIcon.appiconset private static readonly (string idiom, string scale, string size, string src, string dst)[] ICON_CONFIGS = { // --- iPhone --- ("iphone", "2x", "20x20", "iPhone-Notification-40.png", "Icon-iPhone-Notification-40.png"), ("iphone", "3x", "20x20", "iPhone-Notification-60.png", "Icon-iPhone-Notification-60.png"), ("iphone", "2x", "29x29", "iPhone-Settings-58.png", "Icon-iPhone-Settings-58.png"), ("iphone", "3x", "29x29", "iPhone-Settings-87.png", "Icon-iPhone-Settings-87.png"), ("iphone", "2x", "40x40", "iPhone-Spotlight-80.png", "Icon-iPhone-Spotlight-80.png"), ("iphone", "3x", "40x40", "iPhone-Spotlight-120.png", "Icon-iPhone-Spotlight-120.png"), ("iphone", "2x", "60x60", "iPhone-App-120.png", "Icon-iPhone-App-120.png"), ("iphone", "3x", "60x60", "iPhone-App-180.png", "Icon-iPhone-App-180.png"), // --- iPad(复用 iPhone 同名图标作为回退) --- ("ipad", "2x", "20x20", "iPhone-Notification-40.png", "Icon-iPad-Notification-40.png"), ("ipad", "2x", "29x29", "iPhone-Settings-58.png", "Icon-iPad-Settings-58.png"), ("ipad", "2x", "40x40", "iPhone-Spotlight-80.png", "Icon-iPad-Spotlight-80.png"), ("ipad", "2x", "76x76", "iPhone-Spotlight-80.png", "Icon-iPad-App-76.png"), ("ipad", "2x", "83.5x83.5", "iPhone-App-120.png", "Icon-iPad-Pro-App-167.png"), // --- App Store --- ("ios-marketing", "1x", "1024x1024", "AppStore-1024.png", "Icon-AppStore-1024.png"), }; public void OnPostprocessBuild(BuildReport report) { if (report.summary.platform != BuildTarget.iOS) return; string pathToBuiltProject = report.summary.outputPath; string iconSetPath = FindAppIconPath(pathToBuiltProject); if (string.IsNullOrEmpty(iconSetPath)) { Debug.LogWarning($"[iOSIconPostProcessor] 未找到 AppIcon.appiconset 目录,跳过图标替换"); return; } EditorApplication.delayCall += () => ReplaceIcons(iconSetPath); } private static void ReplaceIcons(string iconSetPath) { if (!Directory.Exists(iconSetPath)) { Debug.LogWarning($"[iOSIconPostProcessor] AppIcon.appiconset 目录已不存在: {iconSetPath}"); return; } string projectRoot = Path.GetDirectoryName(Application.dataPath); string sourceDir = Path.Combine(projectRoot, SOURCE_ICON_DIR); // 清空旧图标 foreach (string oldFile in Directory.GetFiles(iconSetPath, "*.png")) { File.Delete(oldFile); } Debug.Log($"[iOSIconPostProcessor] 已清空旧图标"); // 复制新图标(源文件 → 目标 Xcode 标准名称) var copiedDstFiles = new HashSet(); foreach (var cfg in ICON_CONFIGS) { string src = Path.Combine(sourceDir, cfg.src); if (File.Exists(src)) { string dst = Path.Combine(iconSetPath, cfg.dst); File.Copy(src, dst, overwrite: true); copiedDstFiles.Add(cfg.dst); } else { Debug.LogWarning($"[iOSIconPostProcessor] 缺少源图标: {cfg.src}"); } } if (copiedDstFiles.Count > 0) { WriteContentsJson(iconSetPath, copiedDstFiles); Debug.Log($"[iOSIconPostProcessor] 已替换 {copiedDstFiles.Count} 个 App 图标"); } else { Debug.LogError("[iOSIconPostProcessor] 未找到任何可用图标"); } } private static string FindAppIconPath(string buildPath) { string[] candidates = { Path.Combine(buildPath, "Unity-iPhone/Images.xcassets/AppIcon.appiconset"), Path.Combine(buildPath, "Images.xcassets/AppIcon.appiconset"), Path.Combine(buildPath, "Unity-iPhone/Unity-iPhone/Images.xcassets/AppIcon.appiconset"), }; foreach (string path in candidates) { if (Directory.Exists(path)) { Debug.Log($"[iOSIconPostProcessor] 找到 AppIcon 目录: {path}"); return path; } } string searchRoot = Path.Combine(buildPath, "Unity-iPhone"); if (!Directory.Exists(searchRoot)) searchRoot = buildPath; foreach (string dir in Directory.GetDirectories(searchRoot, "AppIcon.appiconset", SearchOption.AllDirectories)) { Debug.Log($"[iOSIconPostProcessor] 搜索找到 AppIcon 目录: {dir}"); return dir; } return null; } private static void WriteContentsJson(string iconSetPath, HashSet dstFiles) { var sb = new StringBuilder(); sb.AppendLine("{"); sb.AppendLine(" \"images\" : ["); bool first = true; foreach (var cfg in ICON_CONFIGS) { if (!dstFiles.Contains(cfg.dst)) continue; if (!first) sb.AppendLine(","); else sb.AppendLine(); first = false; sb.Append($" {{\"idiom\" : \"{cfg.idiom}\", "); sb.Append($"\"scale\" : \"{cfg.scale}\", "); sb.Append($"\"size\" : \"{cfg.size}\", "); sb.Append($"\"filename\" : \"{cfg.dst}\"}}"); } sb.AppendLine(); sb.AppendLine(" ],"); sb.AppendLine(" \"info\" : {"); sb.AppendLine(" \"version\" : 1,"); sb.AppendLine(" \"author\" : \"xcode\""); sb.AppendLine(" }"); sb.AppendLine("}"); string jsonPath = Path.Combine(iconSetPath, "Contents.json"); File.WriteAllText(jsonPath, sb.ToString()); } } #endif