#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