1
This commit is contained in:
@@ -133,25 +133,36 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain
|
||||
code:KBSkinBridgeErrorContainerUnavailable
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"App Group container unavailable"}];
|
||||
// 皮肤根目录:优先 App Group,若不可写则退回当前进程的 Caches 目录。
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSString *baseRoot = nil;
|
||||
NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (containerURL.path.length > 0) {
|
||||
// 探测写权限:在 Skins/.kb_write_test 下创建临时目录
|
||||
NSString *testDir = [[containerURL.path stringByAppendingPathComponent:@"Skins"]
|
||||
stringByAppendingPathComponent:@".kb_write_test"];
|
||||
NSError *probeError = nil;
|
||||
BOOL canWrite = [fm createDirectoryAtPath:testDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:&probeError];
|
||||
if (canWrite) {
|
||||
baseRoot = containerURL.path;
|
||||
[fm removeItemAtPath:testDir error:NULL];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
if (baseRoot.length == 0) {
|
||||
NSArray<NSString *> *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
baseRoot = dirs.firstObject ?: NSTemporaryDirectory();
|
||||
}
|
||||
|
||||
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||||
NSString *skinsRoot = [baseRoot stringByAppendingPathComponent:@"Skins"];
|
||||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||||
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:iconsDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
[fm createDirectoryAtPath:iconsDir
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
BOOL isDir = NO;
|
||||
BOOL hasIconsDir = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
|
||||
NSArray *contents = hasIconsDir ? [fm contentsOfDirectoryAtPath:iconsDir error:NULL] : nil;
|
||||
|
||||
@@ -59,18 +59,38 @@ static NSString * const kKBSkinThemeStoreKey = @"KBSkinThemeCurrent";
|
||||
|
||||
@implementation KBSkinManager
|
||||
|
||||
/// 判断指定 skinId 在 App Group 中是否存在资源目录(Skins/<skinId>/...)。
|
||||
/// 返回所有可能的皮肤根目录(优先 App Group,其次当前进程的 Caches)。
|
||||
+ (NSArray<NSString *> *)kb_candidateBaseRoots {
|
||||
NSMutableArray<NSString *> *roots = [NSMutableArray array];
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (containerURL.path.length > 0) {
|
||||
[roots addObject:containerURL.path];
|
||||
}
|
||||
NSArray<NSString *> *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *caches = dirs.firstObject;
|
||||
if (caches.length > 0) {
|
||||
[roots addObject:caches];
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
/// 判断指定 skinId 是否有可用资源目录(任一根目录下存在 Skins/<skinId>/...)。
|
||||
/// 默认皮肤(nil / @"default")始终视为存在。
|
||||
+ (BOOL)kb_hasAssetsForSkinId:(NSString *)skinId {
|
||||
if (skinId.length == 0 || [skinId isEqualToString:@"default"]) {
|
||||
return YES;
|
||||
}
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) return NO;
|
||||
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||||
BOOL isDir = NO;
|
||||
return [[NSFileManager defaultManager] fileExistsAtPath:skinRoot isDirectory:&isDir] && isDir;
|
||||
NSArray<NSString *> *roots = [self kb_candidateBaseRoots];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
for (NSString *base in roots) {
|
||||
NSString *skinsRoot = [base stringByAppendingPathComponent:@"Skins"];
|
||||
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||||
BOOL isDir = NO;
|
||||
if ([fm fileExistsAtPath:skinRoot isDirectory:&isDir] && isDir) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (instancetype)shared {
|
||||
@@ -176,27 +196,25 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
}
|
||||
|
||||
- (UIImage *)currentBackgroundImage {
|
||||
// 新策略:始终从 App Group 容器按 skinId 读取背景图文件,
|
||||
// 确保当 AppGroup 目录被系统清理时,背景图与按键图标一起消失,
|
||||
// 不再依赖 Keychain 中可能残留的 backgroundImageData。
|
||||
NSString *skinId = self.current.skinId;
|
||||
if (skinId.length == 0) return nil;
|
||||
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) return nil;
|
||||
|
||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId];
|
||||
NSString *bgPath = [[containerURL.path stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
|
||||
NSArray<NSString *> *roots = [self.class kb_candidateBaseRoots];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
BOOL isDir = NO;
|
||||
if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) {
|
||||
return nil;
|
||||
}
|
||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId];
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfFile:bgPath];
|
||||
if (data.length == 0) return nil;
|
||||
return [UIImage imageWithData:data scale:[UIScreen mainScreen].scale] ?: nil;
|
||||
for (NSString *base in roots) {
|
||||
NSString *bgPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) {
|
||||
continue;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfFile:bgPath];
|
||||
if (data.length == 0) continue;
|
||||
UIImage *img = [UIImage imageWithData:data scale:[UIScreen mainScreen].scale];
|
||||
if (img) return img;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideKeyTextForIdentifier:(NSString *)identifier {
|
||||
@@ -257,12 +275,19 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
// 若在 keyIconMap 中找到了 value,按约定加载
|
||||
if (value.length > 0) {
|
||||
if ([value containsString:@"/"]) {
|
||||
// 视为相对 App Group 根目录的文件路径
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) return nil;
|
||||
NSString *fullPath = [[containerURL.path stringByAppendingPathComponent:value] stringByStandardizingPath];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) return nil;
|
||||
return [UIImage imageWithContentsOfFile:fullPath];
|
||||
// 视为相对皮肤根目录(App Group / Caches)的文件路径。
|
||||
NSArray<NSString *> *roots = [self.class kb_candidateBaseRoots];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
for (NSString *base in roots) {
|
||||
NSString *fullPath = [[base stringByAppendingPathComponent:value] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || isDir) {
|
||||
continue;
|
||||
}
|
||||
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
if (img) return img;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
// 否则按本地 Assets 名称加载(兼容旧实现)
|
||||
return [UIImage imageNamed:value];
|
||||
@@ -272,28 +297,34 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
// Skins/<skinId>/icons/(identifier[_upper/_lower]).png
|
||||
NSString *skinId = self.current.skinId;
|
||||
if (skinId.length == 0 || identifier.length == 0) return nil;
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) return nil;
|
||||
NSArray<NSString *> *roots = [self.class kb_candidateBaseRoots];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
|
||||
// 先尝试大小写后缀
|
||||
if (caseVariant == 2) {
|
||||
NSString *relativeUpper = [NSString stringWithFormat:@"Skins/%@/icons/%@_upper.png", skinId, identifier];
|
||||
NSString *fullUpper = [[containerURL.path stringByAppendingPathComponent:relativeUpper] stringByStandardizingPath];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:fullUpper]) {
|
||||
return [UIImage imageWithContentsOfFile:fullUpper];
|
||||
}
|
||||
} else if (caseVariant == 1) {
|
||||
NSString *relativeLower = [NSString stringWithFormat:@"Skins/%@/icons/%@_lower.png", skinId, identifier];
|
||||
NSString *fullLower = [[containerURL.path stringByAppendingPathComponent:relativeLower] stringByStandardizingPath];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:fullLower]) {
|
||||
return [UIImage imageWithContentsOfFile:fullLower];
|
||||
if (caseVariant == 2 || caseVariant == 1) {
|
||||
NSString *suffix = (caseVariant == 2) ? @"_upper" : @"_lower";
|
||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@%@.png", skinId, identifier, suffix];
|
||||
for (NSString *base in roots) {
|
||||
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) {
|
||||
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
if (img) return img;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最后回退到基础 id:Skins/<skinId>/icons/<identifier>.png
|
||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@.png", skinId, identifier];
|
||||
NSString *fullPath = [[containerURL.path stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) return nil;
|
||||
return [UIImage imageWithContentsOfFile:fullPath];
|
||||
for (NSString *base in roots) {
|
||||
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) {
|
||||
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
if (img) return img;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback {
|
||||
|
||||
Reference in New Issue
Block a user