172 lines
6.7 KiB
C#
172 lines
6.7 KiB
C#
#if UNITY_IOS
|
||
using UnityEditor;
|
||
using UnityEditor.Build;
|
||
using UnityEditor.Build.Reporting;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// iOS 出包后自动替换高清 App 图标,解决 Unity 压缩导致图标模糊的问题。
|
||
/// 使用 IPostprocessBuildWithReport 确保在所有 PostProcessBuild 之后执行。
|
||
/// 使用方式:将精确尺寸的 PNG 图标放入 Assets/Icons/iOS/ 目录下,打包时自动生效。
|
||
/// 命名规则:iPhone-App-180.png(设备-用途-像素尺寸)可被 Xcode Asset Catalog 自动识别。
|
||
/// </summary>
|
||
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<string>();
|
||
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<string> 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
|