3
This commit is contained in:
Binary file not shown.
@@ -10,6 +10,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 跨进程通知:主 App 请求键盘扩展安装皮肤。
|
/// 跨进程通知:主 App 请求键盘扩展安装皮肤。
|
||||||
extern NSString * const KBDarwinSkinInstallRequestNotification;
|
extern NSString * const KBDarwinSkinInstallRequestNotification;
|
||||||
|
|
||||||
|
/// 皮肤安装桥接错误域。
|
||||||
|
extern NSErrorDomain const KBSkinBridgeErrorDomain;
|
||||||
|
|
||||||
|
/// 皮肤安装桥接错误码。
|
||||||
|
typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
||||||
|
KBSkinBridgeErrorInvalidPayload = 1,
|
||||||
|
KBSkinBridgeErrorContainerUnavailable,
|
||||||
|
KBSkinBridgeErrorZipMissing,
|
||||||
|
KBSkinBridgeErrorUnzipFailed,
|
||||||
|
KBSkinBridgeErrorApplyFailed,
|
||||||
|
};
|
||||||
|
|
||||||
typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable error);
|
typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable error);
|
||||||
|
|
||||||
@interface KBSkinInstallBridge : NSObject
|
@interface KBSkinInstallBridge : NSObject
|
||||||
@@ -17,6 +29,17 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable
|
|||||||
/// 默认图标短文件名映射(从 KBSkinIconMap.strings 读取)。
|
/// 默认图标短文件名映射(从 KBSkinIconMap.strings 读取)。
|
||||||
+ (NSDictionary<NSString *, NSString *> *)defaultIconShortNames;
|
+ (NSDictionary<NSString *, NSString *> *)defaultIconShortNames;
|
||||||
|
|
||||||
|
/// 主 App / 键盘扩展:通过远程 zip_url 下载并安装一套皮肤。
|
||||||
|
/// - skinJSON 结构与后端约定一致,至少包含:
|
||||||
|
/// * id: 皮肤唯一标识
|
||||||
|
/// * name: 展示名称(可选,缺省为 id)
|
||||||
|
/// * zip_url: 远程 Zip 地址(http/https)
|
||||||
|
/// * key_icons: 按键 -> 图标“短文件名”映射(可选,不传则使用 defaultIconShortNames)
|
||||||
|
/// - 内部会将 Zip 解压到 App Group/Skins/<id>/...,并使用 KBSkinManager 应用主题与背景图。
|
||||||
|
/// - 应用成功后,KBSkinManager 会广播皮肤变更通知,键盘扩展可立即感知。
|
||||||
|
+ (void)installRemoteSkinWithJSON:(NSDictionary *)skinJSON
|
||||||
|
completion:(nullable KBSkinInstallConsumeCompletion)completion;
|
||||||
|
|
||||||
/// 主 App 侧:记录一个“从 bundle 解压皮肤”的请求,写入 App Group 并广播 Darwin 通知。
|
/// 主 App 侧:记录一个“从 bundle 解压皮肤”的请求,写入 App Group 并广播 Darwin 通知。
|
||||||
+ (void)publishBundleSkinRequestWithId:(NSString *)skinId
|
+ (void)publishBundleSkinRequestWithId:(NSString *)skinId
|
||||||
name:(NSString *)name
|
name:(NSString *)name
|
||||||
@@ -36,4 +59,3 @@ typedef void (^KBSkinInstallConsumeCompletion)(BOOL success, NSError * _Nullable
|
|||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
|
|
||||||
#import "KBConfig.h"
|
#import "KBConfig.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
|
#if __has_include("KBNetworkManager.h")
|
||||||
|
#import "KBNetworkManager.h"
|
||||||
|
#endif
|
||||||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||||||
#import <SSZipArchive/SSZipArchive.h>
|
#import <SSZipArchive/SSZipArchive.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
NSString * const KBDarwinSkinInstallRequestNotification = @"com.loveKey.nyx.skin.install.request";
|
NSString * const KBDarwinSkinInstallRequestNotification = @"com.loveKey.nyx.skin.install.request";
|
||||||
|
NSErrorDomain const KBSkinBridgeErrorDomain = @"com.loveKey.nyx.skin.bridge";
|
||||||
|
|
||||||
static NSString * const kKBSkinPendingRequestKey = @"com.loveKey.nyx.skin.pending";
|
static NSString * const kKBSkinPendingRequestKey = @"com.loveKey.nyx.skin.pending";
|
||||||
static NSString * const kKBSkinPendingSkinIdKey = @"skinId";
|
static NSString * const kKBSkinPendingSkinIdKey = @"skinId";
|
||||||
@@ -20,16 +24,6 @@ static NSString * const kKBSkinPendingKindKey = @"kind";
|
|||||||
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
|
static NSString * const kKBSkinPendingTimestampKey = @"timestamp";
|
||||||
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
|
static NSString * const kKBSkinPendingIconShortKey = @"iconShortNames";
|
||||||
|
|
||||||
static NSString * const kKBSkinBridgeErrorDomain = @"com.loveKey.nyx.skin.bridge";
|
|
||||||
|
|
||||||
typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|
||||||
KBSkinBridgeErrorInvalidPayload = 1,
|
|
||||||
KBSkinBridgeErrorContainerUnavailable,
|
|
||||||
KBSkinBridgeErrorZipMissing,
|
|
||||||
KBSkinBridgeErrorUnzipFailed,
|
|
||||||
KBSkinBridgeErrorApplyFailed,
|
|
||||||
};
|
|
||||||
|
|
||||||
@implementation KBSkinInstallBridge
|
@implementation KBSkinInstallBridge
|
||||||
|
|
||||||
+ (NSDictionary<NSString *,NSString *> *)defaultIconShortNames {
|
+ (NSDictionary<NSString *,NSString *> *)defaultIconShortNames {
|
||||||
@@ -54,6 +48,245 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|||||||
return [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
return [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (void)installRemoteSkinWithJSON:(NSDictionary *)skinJSON
|
||||||
|
completion:(KBSkinInstallConsumeCompletion)completion {
|
||||||
|
if (![skinJSON isKindOfClass:NSDictionary.class] || skinJSON.count == 0) {
|
||||||
|
if (completion) {
|
||||||
|
NSError *err = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorInvalidPayload
|
||||||
|
userInfo:nil];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{ completion(NO, err); });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *skinId = skinJSON[@"id"] ?: @"remote";
|
||||||
|
NSString *name = skinJSON[@"name"] ?: skinId;
|
||||||
|
NSString *zipURL = skinJSON[@"zip_url"] ?: @"";
|
||||||
|
|
||||||
|
// key_icons 可选:
|
||||||
|
// - 若后端提供 key_icons,则优先使用服务端映射;
|
||||||
|
// - 若未提供,则回退到本地默认映射,这样后端只需返回 id/name/zip_url。
|
||||||
|
NSDictionary *iconShortNames = nil;
|
||||||
|
if ([skinJSON[@"key_icons"] isKindOfClass:NSDictionary.class]) {
|
||||||
|
iconShortNames = skinJSON[@"key_icons"];
|
||||||
|
} else {
|
||||||
|
iconShortNames = [self defaultIconShortNames];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||||
|
if (!containerURL) {
|
||||||
|
if (completion) {
|
||||||
|
NSError *err = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorContainerUnavailable
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Shared container unavailable"}];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{ completion(NO, err); });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *skinsRoot = [containerURL.path stringByAppendingPathComponent:@"Skins"];
|
||||||
|
NSString *skinRoot = [skinsRoot stringByAppendingPathComponent:skinId];
|
||||||
|
NSString *iconsDir = [skinRoot stringByAppendingPathComponent:@"icons"];
|
||||||
|
[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;
|
||||||
|
BOOL hasCachedAssets = (contents.count > 0);
|
||||||
|
|
||||||
|
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
||||||
|
|
||||||
|
dispatch_group_t group = dispatch_group_create();
|
||||||
|
__block BOOL zipOK = YES;
|
||||||
|
__block NSError *innerError = nil;
|
||||||
|
|
||||||
|
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
||||||
|
// 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。
|
||||||
|
if (!hasCachedAssets && zipURL.length > 0) {
|
||||||
|
dispatch_group_enter(group);
|
||||||
|
|
||||||
|
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
||||||
|
if (data.length == 0) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorZipMissing
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Zip data is empty"}];
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 将 Zip 写入临时路径再解压
|
||||||
|
[fm createDirectoryAtPath:skinRoot
|
||||||
|
withIntermediateDirectories:YES
|
||||||
|
attributes:nil
|
||||||
|
error:NULL];
|
||||||
|
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
|
||||||
|
if (![data writeToFile:zipPath atomically:YES]) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Failed to write zip file"}];
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *unzipError = nil;
|
||||||
|
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
||||||
|
toDestination:skinRoot
|
||||||
|
overwrite:YES
|
||||||
|
password:nil
|
||||||
|
error:&unzipError];
|
||||||
|
[fm removeItemAtPath:zipPath error:nil];
|
||||||
|
if (!ok || unzipError) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = unzipError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
|
userInfo:nil];
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容“额外包一层目录”的压缩结构:
|
||||||
|
// 若 Skins/<skinId>/icons 为空,但存在 Skins/<skinId>/<子目录>/icons,
|
||||||
|
// 则将实际 icons 与 background.png 上移到预期位置。
|
||||||
|
BOOL isDir2 = NO;
|
||||||
|
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL];
|
||||||
|
BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
|
||||||
|
if (!iconsValid) {
|
||||||
|
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
|
||||||
|
for (NSString *subName in subItems) {
|
||||||
|
if ([subName isEqualToString:@"icons"] || [subName isEqualToString:@"__MACOSX"]) continue;
|
||||||
|
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:subName];
|
||||||
|
BOOL isDirNested = NO;
|
||||||
|
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
||||||
|
|
||||||
|
NSString *nestedIcons = [nestedRoot stringByAppendingPathComponent:@"icons"];
|
||||||
|
BOOL isDirNestedIcons = NO;
|
||||||
|
if ([fm fileExistsAtPath:nestedIcons isDirectory:&isDirNestedIcons] && isDirNestedIcons) {
|
||||||
|
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
||||||
|
if (nestedFiles.count > 0) {
|
||||||
|
// 确保目标 icons 目录存在
|
||||||
|
[fm createDirectoryAtPath:iconsDir
|
||||||
|
withIntermediateDirectories:YES
|
||||||
|
attributes:nil
|
||||||
|
error:NULL];
|
||||||
|
// 将 icons 下所有文件上移一层
|
||||||
|
for (NSString *fn in nestedFiles) {
|
||||||
|
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
||||||
|
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
|
||||||
|
[fm removeItemAtPath:to error:nil];
|
||||||
|
[fm moveItemAtPath:from toPath:to error:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 background.png:若在子目录下存在,则上移到 skinRoot
|
||||||
|
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
|
||||||
|
if ([fm fileExistsAtPath:nestedBg]) {
|
||||||
|
[fm removeItemAtPath:bgPath error:nil];
|
||||||
|
[fm moveItemAtPath:nestedBg toPath:bgPath error:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
#if __has_include("KBNetworkManager.h")
|
||||||
|
// 远程下载(http/https)
|
||||||
|
[[KBNetworkManager shared] GET:zipURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
||||||
|
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
||||||
|
if (error || data.length == 0) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = error ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorZipMissing
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Failed to download zip"}];
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleZipData(data);
|
||||||
|
}];
|
||||||
|
#else
|
||||||
|
// 无 KBNetworkManager 时,退回到简单的 dataWithContentsOfURL 下载(阻塞当前线程)
|
||||||
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||||
|
NSURL *url = [NSURL URLWithString:zipURL];
|
||||||
|
NSData *data = url ? [NSData dataWithContentsOfURL:url] : nil;
|
||||||
|
if (!data) {
|
||||||
|
zipOK = NO;
|
||||||
|
if (!innerError) {
|
||||||
|
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorZipMissing
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"Failed to download zip"}];
|
||||||
|
}
|
||||||
|
dispatch_group_leave(group);
|
||||||
|
} else {
|
||||||
|
handleZipData(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
zipOK = NO;
|
||||||
|
innerError = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"SSZipArchive not available"}];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 解压与下载完成后,构造主题并应用
|
||||||
|
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||||
|
// 构造 key_icons -> App Group 相对路径 映射
|
||||||
|
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
||||||
|
[iconShortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) {
|
||||||
|
if (![shortName isKindOfClass:NSString.class] || shortName.length == 0) return;
|
||||||
|
NSString *fileName = shortName;
|
||||||
|
// 若未带扩展名,默认按 .png 处理
|
||||||
|
if (fileName.pathExtension.length == 0) {
|
||||||
|
fileName = [fileName stringByAppendingPathExtension:@"png"];
|
||||||
|
}
|
||||||
|
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
||||||
|
iconPathMap[identifier] = relative;
|
||||||
|
}];
|
||||||
|
|
||||||
|
NSMutableDictionary *themeJSON = [skinJSON mutableCopy];
|
||||||
|
themeJSON[@"id"] = skinId;
|
||||||
|
if (iconPathMap.count > 0) {
|
||||||
|
themeJSON[@"key_icons"] = iconPathMap.copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
||||||
|
|
||||||
|
// 背景图优先从 Zip 解压出的 background.png 读取
|
||||||
|
NSData *bgData = [NSData dataWithContentsOfFile:bgPath];
|
||||||
|
BOOL ok = themeOK;
|
||||||
|
if (bgData.length > 0) {
|
||||||
|
ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!zipOK && !hasCachedAssets) {
|
||||||
|
ok = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *finalError = nil;
|
||||||
|
if (!ok) {
|
||||||
|
finalError = innerError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
|
code:KBSkinBridgeErrorApplyFailed
|
||||||
|
userInfo:nil];
|
||||||
|
}
|
||||||
|
if (completion) completion(ok, finalError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
+ (void)publishBundleSkinRequestWithId:(NSString *)skinId
|
+ (void)publishBundleSkinRequestWithId:(NSString *)skinId
|
||||||
name:(NSString *)name
|
name:(NSString *)name
|
||||||
zipName:(NSString *)zipName
|
zipName:(NSString *)zipName
|
||||||
@@ -115,7 +348,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|||||||
error:(NSError * __autoreleasing *)error {
|
error:(NSError * __autoreleasing *)error {
|
||||||
#if !__has_include(<SSZipArchive/SSZipArchive.h>)
|
#if !__has_include(<SSZipArchive/SSZipArchive.h>)
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain
|
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
code:KBSkinBridgeErrorUnzipFailed
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"SSZipArchive not available"}];
|
userInfo:@{NSLocalizedDescriptionKey: @"SSZipArchive not available"}];
|
||||||
}
|
}
|
||||||
@@ -126,7 +359,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|||||||
NSString *zipName = payload[kKBSkinPendingZipKey];
|
NSString *zipName = payload[kKBSkinPendingZipKey];
|
||||||
if (skinId.length == 0 || zipName.length == 0) {
|
if (skinId.length == 0 || zipName.length == 0) {
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain
|
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
code:KBSkinBridgeErrorInvalidPayload
|
code:KBSkinBridgeErrorInvalidPayload
|
||||||
userInfo:nil];
|
userInfo:nil];
|
||||||
}
|
}
|
||||||
@@ -192,9 +425,9 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|||||||
}
|
}
|
||||||
if (zipPath.length == 0) {
|
if (zipPath.length == 0) {
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain
|
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
code:KBSkinBridgeErrorZipMissing
|
code:KBSkinBridgeErrorZipMissing
|
||||||
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not found"}];
|
userInfo:@{NSLocalizedDescriptionKey: @"Zip resource not found"}];
|
||||||
}
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
@@ -207,7 +440,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|||||||
error:&unzipError];
|
error:&unzipError];
|
||||||
if (!ok || unzipError) {
|
if (!ok || unzipError) {
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = unzipError ?: [NSError errorWithDomain:kKBSkinBridgeErrorDomain
|
*error = unzipError ?: [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
code:KBSkinBridgeErrorUnzipFailed
|
code:KBSkinBridgeErrorUnzipFailed
|
||||||
userInfo:nil];
|
userInfo:nil];
|
||||||
}
|
}
|
||||||
@@ -283,7 +516,7 @@ typedef NS_ENUM(NSUInteger, KBSkinBridgeErrorCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ok && error) {
|
if (!ok && error) {
|
||||||
*error = [NSError errorWithDomain:kKBSkinBridgeErrorDomain
|
*error = [NSError errorWithDomain:KBSkinBridgeErrorDomain
|
||||||
code:KBSkinBridgeErrorApplyFailed
|
code:KBSkinBridgeErrorApplyFailed
|
||||||
userInfo:nil];
|
userInfo:nil];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,9 @@
|
|||||||
"Notification Setting" = "Notification Setting";
|
"Notification Setting" = "Notification Setting";
|
||||||
"Please Enter The Content" = "Please Enter The Content";
|
"Please Enter The Content" = "Please Enter The Content";
|
||||||
"Commit" = "Commit";
|
"Commit" = "Commit";
|
||||||
|
"Nickname" = "Nickname";
|
||||||
|
"Gender" = "Gender";
|
||||||
|
"User ID" = "User ID";
|
||||||
|
|
||||||
|
|
||||||
// Search & history
|
// Search & history
|
||||||
|
|||||||
@@ -107,6 +107,9 @@
|
|||||||
"Notification Setting" = "通知设置";
|
"Notification Setting" = "通知设置";
|
||||||
"Please Enter The Content" = "请输入反馈内容";
|
"Please Enter The Content" = "请输入反馈内容";
|
||||||
"Commit" = "提交";
|
"Commit" = "提交";
|
||||||
|
"Nickname" = "用户名";
|
||||||
|
"Gender" = "性别";
|
||||||
|
"User ID" = "用户ID";
|
||||||
|
|
||||||
// 搜索与历史(英文 key)
|
// 搜索与历史(英文 key)
|
||||||
"Clear history" = "清空历史";
|
"Clear history" = "清空历史";
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
04791F952ED48028004E8522 /* KBFeedBackVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F942ED48028004E8522 /* KBFeedBackVC.m */; };
|
04791F952ED48028004E8522 /* KBFeedBackVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F942ED48028004E8522 /* KBFeedBackVC.m */; };
|
||||||
04791F982ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
04791F982ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
||||||
04791F992ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
04791F992ED49CE7004E8522 /* KBFont.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F972ED49CE7004E8522 /* KBFont.m */; };
|
||||||
04791FF52ED5A487004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF42ED5A487004E8522 /* Christmas.zip */; };
|
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */ = {isa = PBXBuildFile; fileRef = 04791FF62ED5B985004E8522 /* Christmas.zip */; };
|
||||||
047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; };
|
047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; };
|
||||||
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; };
|
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; };
|
||||||
047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; };
|
047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; };
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
04791F942ED48028004E8522 /* KBFeedBackVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFeedBackVC.m; sourceTree = "<group>"; };
|
04791F942ED48028004E8522 /* KBFeedBackVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFeedBackVC.m; sourceTree = "<group>"; };
|
||||||
04791F962ED49CE7004E8522 /* KBFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFont.h; sourceTree = "<group>"; };
|
04791F962ED49CE7004E8522 /* KBFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFont.h; sourceTree = "<group>"; };
|
||||||
04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = "<group>"; };
|
04791F972ED49CE7004E8522 /* KBFont.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFont.m; sourceTree = "<group>"; };
|
||||||
04791FF42ED5A487004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = "<group>"; };
|
04791FF62ED5B985004E8522 /* Christmas.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Christmas.zip; sourceTree = "<group>"; };
|
||||||
047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = "<group>"; };
|
047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = "<group>"; };
|
||||||
047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = "<group>"; };
|
047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = "<group>"; };
|
||||||
047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = "<group>"; };
|
047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = "<group>"; };
|
||||||
@@ -501,7 +501,7 @@
|
|||||||
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
041007D12ECE012000D203BB /* KBSkinIconMap.strings */,
|
||||||
041007D32ECE012500D203BB /* 002.zip */,
|
041007D32ECE012500D203BB /* 002.zip */,
|
||||||
046131102ECF3A6E00A6FADF /* fense.zip */,
|
046131102ECF3A6E00A6FADF /* fense.zip */,
|
||||||
04791FF42ED5A487004E8522 /* Christmas.zip */,
|
04791FF62ED5B985004E8522 /* Christmas.zip */,
|
||||||
);
|
);
|
||||||
path = Resource;
|
path = Resource;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1453,7 +1453,7 @@
|
|||||||
046131112ECF3A6E00A6FADF /* fense.zip in Resources */,
|
046131112ECF3A6E00A6FADF /* fense.zip in Resources */,
|
||||||
041007D42ECE012500D203BB /* 002.zip in Resources */,
|
041007D42ECE012500D203BB /* 002.zip in Resources */,
|
||||||
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
041007D22ECE012000D203BB /* KBSkinIconMap.strings in Resources */,
|
||||||
04791FF52ED5A487004E8522 /* Christmas.zip in Resources */,
|
04791FF72ED5B985004E8522 /* Christmas.zip in Resources */,
|
||||||
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */,
|
04286A0B2ECD88B400CE730C /* KeyboardAssets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
@@ -77,178 +77,25 @@
|
|||||||
/// - skinJSON.key_icons 的 value 填写 Zip 内的图标“短文件名”(不含路径,可不含扩展名),例如 "key_a"
|
/// - skinJSON.key_icons 的 value 填写 Zip 内的图标“短文件名”(不含路径,可不含扩展名),例如 "key_a"
|
||||||
/// 应用时会被转换为相对 App Group 根目录的路径:Skins/<skinId>/icons/<shortName>.png
|
/// 应用时会被转换为相对 App Group 根目录的路径:Skins/<skinId>/icons/<shortName>.png
|
||||||
- (void)kb_applySkinUsingRemoteIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
- (void)kb_applySkinUsingRemoteIcons:(NSDictionary *)skin completion:(KBSkinApplyCompletion)completion {
|
||||||
NSString *skinId = skin[@"id"] ?: @"remote";
|
[KBSkinInstallBridge installRemoteSkinWithJSON:skin
|
||||||
NSString *name = skin[@"name"] ?: skinId;
|
completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
NSString *zipURL = skin[@"zip_url"] ?: @""; // 新协议:远程 Zip 包地址
|
if (completion) {
|
||||||
|
completion(success);
|
||||||
// key_icons 可选:
|
|
||||||
// - 若后端提供 key_icons,则优先使用服务端映射;
|
|
||||||
// - 若未提供,则回退到本地默认映射(KBSkinInstallBridge.defaultIconShortNames),这样后端只需返回 id/name/zip_url。
|
|
||||||
NSDictionary *iconShortNames = nil;
|
|
||||||
if ([skin[@"key_icons"] isKindOfClass:NSDictionary.class]) {
|
|
||||||
iconShortNames = skin[@"key_icons"];
|
|
||||||
} else {
|
|
||||||
iconShortNames = [KBSkinInstallBridge defaultIconShortNames];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
|
||||||
if (!containerURL) {
|
|
||||||
if (completion) completion(NO);
|
|
||||||
[KBHUD showInfo:KBLocalized(@"无法访问共享容器,应用皮肤失败")];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *skinsRoot = [containerURL.path 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];
|
|
||||||
BOOL isDir = NO;
|
|
||||||
BOOL hasIconsDir = [fm fileExistsAtPath:iconsDir isDirectory:&isDir] && isDir;
|
|
||||||
NSArray *contents = hasIconsDir ? [fm contentsOfDirectoryAtPath:iconsDir error:NULL] : nil;
|
|
||||||
BOOL hasCachedAssets = (contents.count > 0);
|
|
||||||
|
|
||||||
NSString *bgPath = [skinRoot stringByAppendingPathComponent:@"background.png"];
|
|
||||||
|
|
||||||
dispatch_group_t group = dispatch_group_create();
|
|
||||||
__block BOOL zipOK = YES;
|
|
||||||
|
|
||||||
#if __has_include(<SSZipArchive/SSZipArchive.h>)
|
|
||||||
// 若本地尚未缓存该皮肤资源且提供了 zip_url,则通过网络下载并解压 Zip 包。
|
|
||||||
if (!hasCachedAssets && zipURL.length > 0) {
|
|
||||||
dispatch_group_enter(group);
|
|
||||||
|
|
||||||
void (^handleZipData)(NSData *) = ^(NSData *data) {
|
|
||||||
if (data.length == 0) {
|
|
||||||
zipOK = NO;
|
|
||||||
dispatch_group_leave(group);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 将 Zip 写入临时路径再解压
|
|
||||||
[[NSFileManager defaultManager] createDirectoryAtPath:skinRoot
|
|
||||||
withIntermediateDirectories:YES
|
|
||||||
attributes:nil
|
|
||||||
error:NULL];
|
|
||||||
NSString *zipPath = [skinRoot stringByAppendingPathComponent:@"skin.zip"];
|
|
||||||
if (![data writeToFile:zipPath atomically:YES]) {
|
|
||||||
zipOK = NO;
|
|
||||||
dispatch_group_leave(group);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *unzipError = nil;
|
|
||||||
BOOL ok = [SSZipArchive unzipFileAtPath:zipPath
|
|
||||||
toDestination:skinRoot
|
|
||||||
overwrite:YES
|
|
||||||
password:nil
|
|
||||||
error:&unzipError];
|
|
||||||
[fm removeItemAtPath:zipPath error:nil];
|
|
||||||
if (!ok || unzipError) {
|
|
||||||
zipOK = NO;
|
|
||||||
dispatch_group_leave(group);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容“额外包一层目录”的压缩结构:
|
|
||||||
// 若 Skins/<skinId>/icons 为空,但存在 Skins/<skinId>/<子目录>/icons,
|
|
||||||
// 则将实际 icons 与 background.png 上移到预期位置。
|
|
||||||
BOOL isDir2 = NO;
|
|
||||||
NSArray *iconsContent = [fm contentsOfDirectoryAtPath:iconsDir error:NULL];
|
|
||||||
BOOL iconsValid = ([fm fileExistsAtPath:iconsDir isDirectory:&isDir2] && isDir2 && iconsContent.count > 0);
|
|
||||||
if (!iconsValid) {
|
|
||||||
NSArray<NSString *> *subItems = [fm contentsOfDirectoryAtPath:skinRoot error:NULL];
|
|
||||||
for (NSString *name in subItems) {
|
|
||||||
if ([name isEqualToString:@"icons"] || [name isEqualToString:@"__MACOSX"]) continue;
|
|
||||||
NSString *nestedRoot = [skinRoot stringByAppendingPathComponent:name];
|
|
||||||
BOOL isDirNested = NO;
|
|
||||||
if (![fm fileExistsAtPath:nestedRoot isDirectory:&isDirNested] || !isDirNested) continue;
|
|
||||||
|
|
||||||
NSString *nestedIcons = [nestedRoot stringByAppendingPathComponent:@"icons"];
|
|
||||||
BOOL isDirNestedIcons = NO;
|
|
||||||
if ([fm fileExistsAtPath:nestedIcons isDirectory:&isDirNestedIcons] && isDirNestedIcons) {
|
|
||||||
NSArray *nestedFiles = [fm contentsOfDirectoryAtPath:nestedIcons error:NULL];
|
|
||||||
if (nestedFiles.count > 0) {
|
|
||||||
// 确保目标 icons 目录存在
|
|
||||||
[fm createDirectoryAtPath:iconsDir
|
|
||||||
withIntermediateDirectories:YES
|
|
||||||
attributes:nil
|
|
||||||
error:NULL];
|
|
||||||
// 将 icons 下所有文件上移一层
|
|
||||||
for (NSString *fn in nestedFiles) {
|
|
||||||
NSString *from = [nestedIcons stringByAppendingPathComponent:fn];
|
|
||||||
NSString *to = [iconsDir stringByAppendingPathComponent:fn];
|
|
||||||
[fm removeItemAtPath:to error:nil];
|
|
||||||
[fm moveItemAtPath:from toPath:to error:nil];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 background.png:若在子目录下存在,则上移到 skinRoot
|
|
||||||
NSString *nestedBg = [nestedRoot stringByAppendingPathComponent:@"background.png"];
|
|
||||||
if ([fm fileExistsAtPath:nestedBg]) {
|
|
||||||
[fm removeItemAtPath:bgPath error:nil];
|
|
||||||
[fm moveItemAtPath:nestedBg toPath:bgPath error:nil];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch_group_leave(group);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 远程下载(http/https)
|
|
||||||
[[KBNetworkManager shared] GET:zipURL parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
|
|
||||||
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
|
|
||||||
if (error || data.length == 0) {
|
|
||||||
zipOK = NO;
|
|
||||||
dispatch_group_leave(group);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleZipData(data);
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
zipOK = NO;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
|
||||||
// 构造 key_icons -> App Group 相对路径 映射
|
|
||||||
NSMutableDictionary<NSString *, NSString *> *iconPathMap = [NSMutableDictionary dictionary];
|
|
||||||
[iconShortNames enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSString *shortName, BOOL *stop) {
|
|
||||||
if (![shortName isKindOfClass:NSString.class] || shortName.length == 0) return;
|
|
||||||
NSString *fileName = shortName;
|
|
||||||
// 若未带扩展名,默认按 .png 处理
|
|
||||||
if (fileName.pathExtension.length == 0) {
|
|
||||||
fileName = [fileName stringByAppendingPathExtension:@"png"];
|
|
||||||
}
|
|
||||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/icons/%@", skinId, fileName];
|
|
||||||
iconPathMap[identifier] = relative;
|
|
||||||
}];
|
|
||||||
|
|
||||||
NSMutableDictionary *themeJSON = [skin mutableCopy];
|
|
||||||
themeJSON[@"id"] = skinId;
|
|
||||||
if (iconPathMap.count > 0) {
|
|
||||||
themeJSON[@"key_icons"] = iconPathMap.copy;
|
|
||||||
}
|
}
|
||||||
|
if (!success && error) {
|
||||||
BOOL themeOK = [[KBSkinManager shared] applyThemeFromJSON:themeJSON];
|
NSLog(@"[KBSkinService] remote skin install failed: %@", error);
|
||||||
|
|
||||||
// 背景图优先从 Zip 解压出的 background.png 读取
|
|
||||||
NSData *bgData = [NSData dataWithContentsOfFile:bgPath];
|
|
||||||
BOOL ok = themeOK;
|
|
||||||
if (bgData.length > 0) {
|
|
||||||
ok = [[KBSkinManager shared] applyImageSkinWithData:bgData skinId:skinId name:name];
|
|
||||||
}
|
}
|
||||||
|
NSString *message = nil;
|
||||||
if (!zipOK && !hasCachedAssets) {
|
if (success) {
|
||||||
ok = NO;
|
message = KBLocalized(@"已应用,切到键盘查看");
|
||||||
|
} else if ([error.domain isEqualToString:KBSkinBridgeErrorDomain] &&
|
||||||
|
error.code == KBSkinBridgeErrorContainerUnavailable) {
|
||||||
|
message = KBLocalized(@"无法访问共享容器,应用皮肤失败");
|
||||||
|
} else {
|
||||||
|
message = KBLocalized(@"应用皮肤失败");
|
||||||
}
|
}
|
||||||
|
[KBHUD showInfo:message];
|
||||||
if (completion) completion(ok);
|
}];
|
||||||
[KBHUD showInfo:(ok ? KBLocalized(@"已应用,切到键盘查看") : KBLocalized(@"应用皮肤失败"))];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 本地 bundle 模式:不走网络,skin[@"zip_url"] 直接为 bundle 内 zip 文件名(可带/不带扩展名)。
|
/// 本地 bundle 模式:不走网络,skin[@"zip_url"] 直接为 bundle 内 zip 文件名(可带/不带扩展名)。
|
||||||
|
|||||||
@@ -43,15 +43,15 @@
|
|||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
self.kb_titleLabel.text = @"Settings"; // 导航标题
|
self.kb_titleLabel.text = KBLocalized(@"Settings"); // 导航标题
|
||||||
self.kb_navView.backgroundColor = [UIColor clearColor];
|
self.kb_navView.backgroundColor = [UIColor clearColor];
|
||||||
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
|
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
|
||||||
|
|
||||||
// 构造数据
|
// 构造数据
|
||||||
self.items = @[
|
self.items = @[
|
||||||
@{ @"title": @"Nickname", @"value": @"Nickname", @"arrow": @YES, @"copy": @NO },
|
@{ @"title": KBLocalized(@"Nickname"), @"value": @"Nickname", @"arrow": @YES, @"copy": @NO },
|
||||||
@{ @"title": @"Gender", @"value": @"Choose", @"arrow": @YES, @"copy": @NO },
|
@{ @"title": KBLocalized(@"Gender"), @"value": @"Choose", @"arrow": @YES, @"copy": @NO },
|
||||||
@{ @"title": @"User ID", @"value": @"8888888", @"arrow": @NO, @"copy": @YES },
|
@{ @"title": KBLocalized(@"User ID"), @"value": @"8888888", @"arrow": @NO, @"copy": @YES },
|
||||||
];
|
];
|
||||||
|
|
||||||
[self.view addSubview:self.tableView];
|
[self.view addSubview:self.tableView];
|
||||||
|
|||||||
Reference in New Issue
Block a user