Files
keyboard/keyBoard/Class/Me/VC/KBPersonInfoVC.m
2026-03-04 21:57:37 +08:00

1187 lines
51 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// KBPersonInfoVC.m
// keyBoard
//
// Created by Mac on 2025/11/11.
// 个人资料
//
#import "KBPersonInfoVC.h"
#import <Masonry/Masonry.h>
#import "KBPersonInfoItemCell.h"
#import <PhotosUI/PhotosUI.h>
#import "LSTPopView.h"
#import "KBChangeNicknamePopView.h"
#import "KBGenderPickerPopView.h"
#import "KBMyVM.h"
#import "KBAlert.h"
#import "KBCancelAccountVC.h"
#import "KBLocalizationManager.h"
#import "KBConfig.h"
#import "KBSkinInstallBridge.h"
#import "KBInputProfileManager.h"
static NSInteger const kKBPersonInfoRowNickname = 0;
static NSInteger const kKBPersonInfoRowGender = 1;
static NSInteger const kKBPersonInfoRowLanguage = 2;
static NSInteger const kKBPersonInfoRowUserID = 3;
typedef void(^KBInputProfileSelectHandler)(NSString *languageCode, NSString *layoutVariant, NSString *profileId);
@interface KBKeyboardLayoutSelectVC : BaseViewController <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) BaseTableView *tableView;
@property (nonatomic, strong) UIButton *confirmButton;
@property (nonatomic, copy) NSDictionary *languageConfig;
@property (nonatomic, copy) NSString *selectedLayoutVariant;
@property (nonatomic, copy) NSString *selectedProfileId;
@property (nonatomic, copy) NSString *pendingLayoutVariant;
@property (nonatomic, copy) NSString *pendingProfileId;
@property (nonatomic, copy) KBInputProfileSelectHandler onSelect;
@end
@implementation KBKeyboardLayoutSelectVC
- (void)viewDidLoad {
[super viewDidLoad];
/// 1控件初始化
[self setupUI];
/// 2初始化选中态
[self prepareDefaultSelection];
}
- (void)setupUI {
self.kb_titleLabel.text = KBLocalized(@"Choose Layout");
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
[self.view addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-72);
}];
[self.view addSubview:self.confirmButton];
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
make.height.mas_equalTo(50);
}];
}
- (void)prepareDefaultSelection {
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
NSDictionary *matched = nil;
for (NSDictionary *layout in layouts) {
NSString *variant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
if (self.selectedLayoutVariant.length > 0 && [variant isEqualToString:self.selectedLayoutVariant]) {
matched = layout;
break;
}
}
if (!matched) {
matched = layouts.firstObject;
}
self.pendingLayoutVariant = [matched[@"variant"] isKindOfClass:NSString.class] ? matched[@"variant"] : @"";
self.pendingProfileId = [matched[@"profileId"] isKindOfClass:NSString.class] ? matched[@"profileId"] : @"";
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
return layouts.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 56.0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cid = @"KBKeyboardLayoutCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid];
cell.textLabel.font = [KBFont medium:16];
cell.detailTextLabel.font = [KBFont regular:12];
cell.detailTextLabel.textColor = [UIColor colorWithHex:0x999999];
}
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
NSDictionary *layout = (indexPath.row < layouts.count) ? layouts[indexPath.row] : @{};
NSString *title = [layout[@"title"] isKindOfClass:NSString.class] ? layout[@"title"] : @"";
NSString *variant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
NSString *profileId = [layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"";
cell.textLabel.text = title;
cell.detailTextLabel.text = profileId;
BOOL selected = (self.pendingLayoutVariant.length > 0 && [self.pendingLayoutVariant isEqualToString:variant]);
cell.accessoryType = selected ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
NSArray *layouts = [self.languageConfig[@"layouts"] isKindOfClass:NSArray.class] ? self.languageConfig[@"layouts"] : @[];
NSDictionary *layout = (indexPath.row < layouts.count) ? layouts[indexPath.row] : nil;
if (![layout isKindOfClass:NSDictionary.class]) { return; }
self.pendingLayoutVariant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
self.pendingProfileId = [layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"";
[self.tableView reloadData];
}
- (void)onTapConfirm {
NSString *code = [self.languageConfig[@"code"] isKindOfClass:NSString.class] ? self.languageConfig[@"code"] : KBLanguageCodeEnglish;
if (self.onSelect && code.length > 0 && self.pendingLayoutVariant.length > 0 && self.pendingProfileId.length > 0) {
self.onSelect(code, self.pendingLayoutVariant, self.pendingProfileId);
}
}
- (BaseTableView *)tableView {
if (!_tableView) {
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.backgroundColor = UIColor.clearColor;
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.separatorInset = UIEdgeInsetsMake(0, 16, 0, 16);
}
return _tableView;
}
- (UIButton *)confirmButton {
if (!_confirmButton) {
_confirmButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_confirmButton setTitle:KBLocalized(@"Confirm") forState:UIControlStateNormal];
[_confirmButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
_confirmButton.titleLabel.font = [KBFont medium:16];
_confirmButton.backgroundColor = [UIColor colorWithHex:0x111111];
_confirmButton.layer.cornerRadius = 12;
_confirmButton.layer.masksToBounds = YES;
[_confirmButton addTarget:self action:@selector(onTapConfirm) forControlEvents:UIControlEventTouchUpInside];
}
return _confirmButton;
}
@end
@interface KBKeyboardLanguageSelectVC : BaseViewController <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) BaseTableView *tableView;
@property (nonatomic, strong) UIButton *confirmButton;
@property (nonatomic, copy) NSArray<NSDictionary *> *languageConfigs;
@property (nonatomic, copy) NSString *selectedLanguageCode;
@property (nonatomic, copy) NSString *selectedLayoutVariant;
@property (nonatomic, copy) NSString *pendingLanguageCode;
@property (nonatomic, copy) NSString *pendingLayoutVariant;
@property (nonatomic, copy) NSString *pendingProfileId;
@property (nonatomic, copy) KBInputProfileSelectHandler onSelect;
@end
@implementation KBKeyboardLanguageSelectVC
- (void)viewDidLoad {
[super viewDidLoad];
/// 1控件初始化
[self setupUI];
/// 2初始化选中态
[self prepareDefaultSelection];
[self updateConfirmVisibility];
}
- (void)setupUI {
self.kb_titleLabel.text = KBLocalized(@"Input Language");
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
[self.view addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-72);
}];
[self.view addSubview:self.confirmButton];
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
make.height.mas_equalTo(50);
}];
}
- (void)prepareDefaultSelection {
NSDictionary *config = [self languageConfigForCode:self.selectedLanguageCode];
if (!config) {
config = self.languageConfigs.firstObject;
}
NSString *code = [config[@"code"] isKindOfClass:NSString.class] ? config[@"code"] : KBLanguageCodeEnglish;
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
NSDictionary *layoutMatched = nil;
for (NSDictionary *layout in layouts) {
NSString *variant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
if (self.selectedLayoutVariant.length > 0 && [variant isEqualToString:self.selectedLayoutVariant]) {
layoutMatched = layout;
break;
}
}
if (!layoutMatched) {
layoutMatched = layouts.firstObject;
}
self.pendingLanguageCode = code;
self.pendingLayoutVariant = [layoutMatched[@"variant"] isKindOfClass:NSString.class] ? layoutMatched[@"variant"] : @"qwerty";
self.pendingProfileId = [layoutMatched[@"profileId"] isKindOfClass:NSString.class] ? layoutMatched[@"profileId"] : @"en_US_qwerty";
}
- (nullable NSDictionary *)languageConfigForCode:(NSString *)code {
for (NSDictionary *item in self.languageConfigs) {
NSString *langCode = [item[@"code"] isKindOfClass:NSString.class] ? item[@"code"] : @"";
if ([langCode isEqualToString:code]) {
return item;
}
}
return nil;
}
- (void)updateConfirmVisibility {
NSDictionary *config = [self languageConfigForCode:self.pendingLanguageCode];
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
BOOL hasMultiLayout = (layouts.count > 1);
self.confirmButton.hidden = hasMultiLayout;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.languageConfigs.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 56.0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cid = @"KBKeyboardLanguageCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid];
cell.textLabel.font = [KBFont medium:16];
cell.detailTextLabel.font = [KBFont regular:12];
cell.detailTextLabel.textColor = [UIColor colorWithHex:0x999999];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSDictionary *lang = (indexPath.row < self.languageConfigs.count) ? self.languageConfigs[indexPath.row] : @{};
NSString *name = [lang[@"name"] isKindOfClass:NSString.class] ? lang[@"name"] : @"";
NSString *code = [lang[@"code"] isKindOfClass:NSString.class] ? lang[@"code"] : @"";
NSArray *layouts = [lang[@"layouts"] isKindOfClass:NSArray.class] ? lang[@"layouts"] : @[];
BOOL multi = layouts.count > 1;
BOOL isSelected = (self.pendingLanguageCode.length > 0 && [self.pendingLanguageCode isEqualToString:code]);
cell.textLabel.text = name;
cell.detailTextLabel.text = multi ? KBLocalized(@"Multiple Keyboard Layouts") : @"QWERTY";
cell.accessoryType = multi ? UITableViewCellAccessoryDisclosureIndicator : (isSelected ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone);
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
NSDictionary *lang = (indexPath.row < self.languageConfigs.count) ? self.languageConfigs[indexPath.row] : nil;
if (![lang isKindOfClass:NSDictionary.class]) { return; }
NSString *code = [lang[@"code"] isKindOfClass:NSString.class] ? lang[@"code"] : KBLanguageCodeEnglish;
NSArray *layouts = [lang[@"layouts"] isKindOfClass:NSArray.class] ? lang[@"layouts"] : @[];
if (layouts.count > 1) {
self.pendingLanguageCode = code;
[self updateConfirmVisibility];
[self.tableView reloadData];
KBKeyboardLayoutSelectVC *layoutVC = [[KBKeyboardLayoutSelectVC alloc] init];
layoutVC.languageConfig = lang;
layoutVC.selectedLayoutVariant = self.selectedLanguageCode.length > 0 && [self.selectedLanguageCode isEqualToString:code] ? self.selectedLayoutVariant : @"";
__weak typeof(self) weakSelf = self;
layoutVC.onSelect = ^(NSString *languageCode, NSString *layoutVariant, NSString *profileId) {
weakSelf.pendingLanguageCode = languageCode;
weakSelf.pendingLayoutVariant = layoutVariant;
weakSelf.pendingProfileId = profileId;
if (weakSelf.onSelect) {
weakSelf.onSelect(languageCode, layoutVariant, profileId);
}
};
[self.navigationController pushViewController:layoutVC animated:YES];
return;
}
NSDictionary *layout = layouts.firstObject;
self.pendingLanguageCode = code;
self.pendingLayoutVariant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"qwerty";
self.pendingProfileId = [layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"en_US_qwerty";
[self updateConfirmVisibility];
[self.tableView reloadData];
}
- (BaseTableView *)tableView {
if (!_tableView) {
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.backgroundColor = UIColor.clearColor;
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.separatorInset = UIEdgeInsetsMake(0, 16, 0, 16);
}
return _tableView;
}
- (void)onTapConfirm {
if (self.confirmButton.hidden) {
return;
}
if (self.onSelect && self.pendingLanguageCode.length > 0 && self.pendingLayoutVariant.length > 0 && self.pendingProfileId.length > 0) {
self.onSelect(self.pendingLanguageCode, self.pendingLayoutVariant, self.pendingProfileId);
}
}
- (UIButton *)confirmButton {
if (!_confirmButton) {
_confirmButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_confirmButton setTitle:KBLocalized(@"Confirm") forState:UIControlStateNormal];
[_confirmButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
_confirmButton.titleLabel.font = [KBFont medium:16];
_confirmButton.backgroundColor = [UIColor colorWithHex:0x111111];
_confirmButton.layer.cornerRadius = 12;
_confirmButton.layer.masksToBounds = YES;
[_confirmButton addTarget:self action:@selector(onTapConfirm) forControlEvents:UIControlEventTouchUpInside];
}
return _confirmButton;
}
@end
@interface KBPersonInfoVC () <UITableViewDelegate, UITableViewDataSource, PHPickerViewControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>
// 列表
@property (nonatomic, strong) BaseTableView *tableView; // 懒加载
// 头像区(表头)
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIImageView *avatarView; // 头像
@property (nonatomic, strong) UIButton *editBadge; // 头像右下角的小铅笔
@property (nonatomic, strong) UILabel *modifyLabel; // “Modify” 文案
@property (nonatomic, strong) UILabel *userIdLabel; // 用户 ID
@property (nonatomic, strong) UIButton *userIdCopyButton; // 复制用户 ID
// 底部退出登录按钮
@property (nonatomic, strong) UIButton *logoutBtn;
// 数据
@property (nonatomic, copy) NSArray<NSDictionary *> *items; // {title,value,arrow,copy}
// 压缩后的头像 JPEG 数据(可用于上传)
@property (nonatomic, strong) NSData *avatarJPEGData;
@property (nonatomic, strong) KBMyVM *myVM;
@property (nonatomic, strong) KBMyVM *viewModel; // 我的页面 VM
@property (nonatomic, strong) KBUser *userModel;
@property (nonatomic, copy) NSString *selectedLanguageCode;
@property (nonatomic, copy) NSString *selectedLayoutVariant;
@property (nonatomic, copy) NSString *selectedProfileId;
@end
@implementation KBPersonInfoVC
- (void)viewDidLoad {
[super viewDidLoad];
/// 1控件初始化
[self setupUI];
/// 2恢复输入配置
[self restoreInputProfile];
/// 3加载用户资料
[self loadData];
/// 4监听语言变化
[self bindNotifications];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setupUI {
self.kb_titleLabel.text = KBLocalized(@"Settings"); // 导航标题
self.kb_navView.backgroundColor = [UIColor clearColor];
self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8];
[self.view addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self.view);
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10);
}];
// 表头
self.tableView.tableHeaderView = self.headerView;
// 底部退出登录按钮
[self.view addSubview:self.logoutBtn];
[self.logoutBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12);
make.height.mas_equalTo(56);
}];
// 列表底部腾出空间,避免被按钮挡住
UIEdgeInsets inset = self.tableView.contentInset;
inset.bottom = 56 + 24; // 按钮高度 + 额外间距
self.tableView.contentInset = inset;
}
- (void)loadData {
self.viewModel = [[KBMyVM alloc] init];
__weak typeof(self) weakSelf = self;
[self.viewModel fetchUserDetailWithCompletion:^(KBUser * _Nullable user, NSError * _Nullable error) {
if (user) {
weakSelf.userModel = user;
[weakSelf.avatarView kb_setAvatarURL:weakSelf.userModel.avatarUrl placeholder:KBAvatarPlaceholderImage];
weakSelf.modifyLabel.text = weakSelf.userModel.nickName;
weakSelf.userIdLabel.text = weakSelf.userModel.userId ?: @"";
}
[weakSelf rebuildItems];
}];
}
- (void)bindNotifications {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleLanguageChanged)
name:KBLocalizationDidChangeNotification
object:nil];
}
- (void)handleLanguageChanged {
self.kb_titleLabel.text = KBLocalized(@"Settings");
[self.logoutBtn setTitle:KBLocalized(@"Log Out") forState:UIControlStateNormal];
[self rebuildItems];
}
- (void)rebuildItems {
NSString *nickname = self.userModel.nickName ?: @"";
NSString *genderText = [self kb_genderDisplayText];
NSString *languageText = [self currentInputProfileDisplayText];
self.items = @[
@{ @"title": KBLocalized(@"Nickname"), @"value": nickname, @"arrow": @YES, @"copy": @NO },
@{ @"title": KBLocalized(@"Gender"), @"value": genderText, @"arrow": @YES, @"copy": @NO },
@{ @"title": KBLocalized(@"Input Language"), @"value": languageText, @"arrow": @YES, @"copy": @NO },
];
[self.tableView reloadData];
}
/// 根据 userModel.gender 生成展示用的性别文案(带多语言)
- (NSString *)kb_genderDisplayText {
if (!self.userModel) {
return KBLocalized(@"Choose");
}
switch (self.userModel.gender) {
case UserSexMan:
return KBLocalized(@"Male");
case UserSexWeman:
return KBLocalized(@"Female");
case UserSexTwoSex:
return KBLocalized(@"The Third Gender");
default:
return KBLocalized(@"Choose");
}
}
- (void)restoreInputProfile {
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *savedCode = [shared stringForKey:AppGroup_SelectedKeyboardLanguageCode];
NSString *savedVariant = [shared stringForKey:AppGroup_SelectedKeyboardLayoutVariant];
NSString *savedProfile = [shared stringForKey:AppGroup_SelectedKeyboardProfileId];
NSString *currentCode = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
NSDictionary *config = [self languageConfigForCode:(savedCode.length ? savedCode : currentCode)];
if (!config) {
config = [self languageConfigForCode:KBLanguageCodeEnglish];
}
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
NSDictionary *layout = nil;
for (NSDictionary *item in layouts) {
NSString *variant = [item[@"variant"] isKindOfClass:NSString.class] ? item[@"variant"] : @"";
if (savedVariant.length > 0 && [variant isEqualToString:savedVariant]) {
layout = item;
break;
}
}
if (!layout) { layout = layouts.firstObject; }
self.selectedLanguageCode = [config[@"code"] isKindOfClass:NSString.class] ? config[@"code"] : KBLanguageCodeEnglish;
self.selectedLayoutVariant = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"qwerty";
self.selectedProfileId = savedProfile.length > 0 ? savedProfile : ([layout[@"profileId"] isKindOfClass:NSString.class] ? layout[@"profileId"] : @"en_US_qwerty");
}
- (NSArray<NSDictionary *> *)languageConfigs {
NSArray<KBInputProfile *> *profiles = [[KBInputProfileManager sharedManager] allProfiles];
NSMutableArray<NSDictionary *> *configs = [NSMutableArray array];
for (KBInputProfile *profile in profiles) {
NSMutableArray<NSDictionary *> *layouts = [NSMutableArray array];
for (KBInputProfileLayout *layout in profile.layouts) {
[layouts addObject:@{
@"variant": layout.variant,
@"title": layout.title,
@"profileId": layout.profileId
}];
}
[configs addObject:@{
@"code": profile.code,
@"name": profile.name,
@"layouts": [layouts copy]
}];
}
return [configs copy];
}
- (nullable NSDictionary *)languageConfigForCode:(NSString *)languageCode {
for (NSDictionary *item in [self languageConfigs]) {
NSString *code = [item[@"code"] isKindOfClass:NSString.class] ? item[@"code"] : @"";
if ([code isEqualToString:languageCode]) {
return item;
}
}
return nil;
}
- (NSString *)layoutTitleForLanguageCode:(NSString *)languageCode variant:(NSString *)variant {
NSDictionary *config = [self languageConfigForCode:languageCode];
NSArray *layouts = [config[@"layouts"] isKindOfClass:NSArray.class] ? config[@"layouts"] : @[];
for (NSDictionary *layout in layouts) {
NSString *v = [layout[@"variant"] isKindOfClass:NSString.class] ? layout[@"variant"] : @"";
if ([v isEqualToString:variant]) {
return [layout[@"title"] isKindOfClass:NSString.class] ? layout[@"title"] : @"";
}
}
return @"";
}
- (NSString *)currentInputProfileDisplayText {
NSDictionary *config = [self languageConfigForCode:self.selectedLanguageCode ?: KBLanguageCodeEnglish];
NSString *languageName = [config[@"name"] isKindOfClass:NSString.class] ? config[@"name"] : @"English";
if ([self.selectedLanguageCode isEqualToString:KBLanguageCodeSpanish]) {
languageName = @"Español";
}
NSString *layoutTitle = [self layoutTitleForLanguageCode:self.selectedLanguageCode variant:self.selectedLayoutVariant];
if (layoutTitle.length == 0) {
return languageName;
}
NSString *variant = self.selectedLayoutVariant ?: @"";
if ([variant.lowercaseString isEqualToString:@"qwerty"] ||
[layoutTitle.lowercaseString isEqualToString:@"qwerty"]) {
return languageName;
}
return [NSString stringWithFormat:@"%@ · %@", languageName, layoutTitle];
}
- (void)openLanguageSelector {
KBKeyboardLanguageSelectVC *vc = [[KBKeyboardLanguageSelectVC alloc] init];
vc.languageConfigs = [self languageConfigs];
vc.selectedLanguageCode = self.selectedLanguageCode ?: KBLanguageCodeEnglish;
vc.selectedLayoutVariant = self.selectedLayoutVariant ?: @"qwerty";
__weak typeof(self) weakSelf = self;
vc.onSelect = ^(NSString *languageCode, NSString *layoutVariant, NSString *profileId) {
[weakSelf applyInputProfileWithLanguageCode:languageCode
layoutVariant:layoutVariant
profileId:profileId
completion:^(BOOL success) {
if (success) {
[weakSelf.navigationController popToViewController:weakSelf animated:YES];
}
}];
};
[self.navigationController pushViewController:vc animated:YES];
}
- (void)applyInputProfileWithLanguageCode:(NSString *)languageCode
layoutVariant:(NSString *)layoutVariant
profileId:(NSString *)profileId
completion:(void(^ _Nullable)(BOOL success))completion {
if (languageCode.length == 0 || layoutVariant.length == 0 || profileId.length == 0) {
if (completion) { completion(NO); }
return;
}
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:languageCode];
NSString *zipName = profile.defaultSkinZip;
if (zipName.length == 0) {
NSLog(@"[KBPersonInfoVC] No defaultSkinZip configured for %@, skipping skin installation", languageCode);
[self commitInputProfileSwitchWithLanguageCode:languageCode
layoutVariant:layoutVariant
profileId:profileId];
if (completion) { completion(YES); }
return;
}
__weak typeof(self) weakSelf = self;
[self installDefaultSkinForLanguageCode:languageCode completion:^(BOOL skinOK) {
if (!skinOK) {
[KBHUD showInfo:KBLocalized(@"Default skin install failed. Please check skin resource configuration.")];
if (completion) { completion(NO); }
return;
}
[weakSelf commitInputProfileSwitchWithLanguageCode:languageCode
layoutVariant:layoutVariant
profileId:profileId];
if (completion) { completion(YES); }
}];
}
- (void)commitInputProfileSwitchWithLanguageCode:(NSString *)languageCode
layoutVariant:(NSString *)layoutVariant
profileId:(NSString *)profileId {
self.selectedLanguageCode = languageCode;
self.selectedLayoutVariant = layoutVariant;
self.selectedProfileId = profileId;
[self rebuildItems];
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
[shared setObject:languageCode forKey:AppGroup_SelectedKeyboardLanguageCode];
[shared setObject:layoutVariant forKey:AppGroup_SelectedKeyboardLayoutVariant];
[shared setObject:profileId forKey:AppGroup_SelectedKeyboardProfileId];
[shared synchronize];
[[KBLocalizationManager shared] setCurrentLanguageCode:languageCode persist:YES];
NSString *message = KBLocalized(@"Changing language will reload the Home screen.");
[KBHUD showInfo:message];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate;
if ([appDelegate respondsToSelector:@selector(setupRootVC)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[appDelegate performSelector:@selector(setupRootVC)];
#pragma clang diagnostic pop
}
});
}
- (void)installDefaultSkinForLanguageCode:(NSString *)languageCode completion:(void(^)(BOOL success))completion {
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:languageCode];
NSString *zipName = profile.defaultSkinZip;
if (zipName.length == 0) {
if (completion) { completion(NO); }
return;
}
NSString *skinId = [NSString stringWithFormat:@"bundle_skin_default_%@", languageCode];
NSDictionary<NSString *, NSString *> *iconShortNames = [KBSkinInstallBridge iconShortNamesForLanguageCode:languageCode];
NSLog(@"[KBPersonInfoVC] Installing skin %@ with %lu icon mappings for language %@", skinId, (unsigned long)iconShortNames.count, languageCode);
[KBSkinInstallBridge publishBundleSkinRequestWithId:skinId
name:skinId
zipName:zipName
iconShortNames:iconShortNames];
[KBSkinInstallBridge consumePendingRequestFromBundle:NSBundle.mainBundle
completion:^(BOOL success, NSError * _Nullable error) {
if (!success && error) {
NSLog(@"[KBPersonInfoVC] default skin install failed: %@", error);
} else if (success) {
NSLog(@"[KBPersonInfoVC] default skin installed: %@", skinId);
}
if (completion) { completion(success); }
}];
}
#pragma mark - UITableView
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return section == 0 ? self.items.count : 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 56.0;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return section == 0 ? 12.0 : 15.0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { return [UIView new]; }
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { return 0.01; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cid = @"KBPersonInfoItemCell";
KBPersonInfoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
if (!cell) { cell = [[KBPersonInfoItemCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cid]; }
if (indexPath.section == 0) {
NSDictionary *it = self.items[indexPath.row];
BOOL isTop = (indexPath.row == 0);
BOOL isBottom = (indexPath.row == self.items.count - 1);
[cell configWithTitle:it[@"title"]
value:it[@"value"]
showArrow:[it[@"arrow"] boolValue]
showCopy:[it[@"copy"] boolValue]
isTop:isTop
isBottom:isBottom];
} else {
[cell configWithTitle:KBLocalized(@"Cancel Account")
value:@""
showArrow:YES
showCopy:NO
isTop:YES
isBottom:YES];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 1) {
KBCancelAccountVC *vc = [[KBCancelAccountVC alloc] init];
[self.navigationController pushViewController:vc animated:YES];
return;
}
if (indexPath.row == kKBPersonInfoRowNickname) {
// 昵称编辑 -> 弹窗
CGFloat width = KB_SCREEN_WIDTH;
KBChangeNicknamePopView *content = [[KBChangeNicknamePopView alloc] initWithFrame:CGRectMake(0, 0, width, 230)];
content.prefillNickname = self.items.firstObject[@"value"] ?: @"";
LSTPopView *pop = [LSTPopView initWithCustomView:content
parentView:self.view
popStyle:LSTPopStyleSmoothFromBottom
dismissStyle:LSTDismissStyleSmoothToBottom];
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
pop.hemStyle = LSTHemStyleBottom; // 居中
pop.isClickBgDismiss = YES; // 点击背景关闭
pop.isAvoidKeyboard = YES; // 规避键盘
pop.avoidKeyboardSpace = 10;
__weak typeof(self) weakSelf = self;
__weak typeof(pop) weakPop = pop;
content.closeHandler = ^{ [weakPop dismiss]; };
content.saveHandler = ^(NSString *nickname) {
if (nickname.length > 0) {
// 更新本地模型,避免返回再进入还是旧数据
weakSelf.userModel.nickName = nickname;
weakSelf.modifyLabel.text = nickname;
[weakSelf rebuildItems];
// 将修改后的用户信息同步到服务端
[weakSelf.myVM updateUserInfo:weakSelf.userModel completion:^(BOOL success, NSError * _Nullable error) {
if (error) {
[KBHUD showError:error.localizedDescription ?: KBLocalized(@"Network error")];
}
}];
}
[weakPop dismiss];
};
[pop pop];
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [content focusInput]; });
} else if (indexPath.row == kKBPersonInfoRowGender) {
// 性别选择 -> 弹窗(性别文案支持多语言)
NSArray *genders = @[
@{@"id":@"1",@"name":KBLocalized(@"Male")},
@{@"id":@"2",@"name":KBLocalized(@"Female")},
@{@"id":@"3",@"name":KBLocalized(@"The Third Gender")},
];
CGFloat width = KB_SCREEN_WIDTH;
KBGenderPickerPopView *content = [[KBGenderPickerPopView alloc] initWithFrame:CGRectMake(0, 0, width, 300)];
content.items = genders;
// 取当前展示值对应的 id如果有的话
NSString *curName = self.items[kKBPersonInfoRowGender][@"value"];
NSString *selId = nil;
for (NSDictionary *d in genders) { if ([d[@"name"] isEqualToString:curName]) { selId = d[@"id"]; break; } }
content.selectedId = selId;
LSTPopView *pop = [LSTPopView initWithCustomView:content
parentView:nil
popStyle:LSTPopStyleSmoothFromBottom
dismissStyle:LSTDismissStyleSmoothToBottom];
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
pop.hemStyle = LSTHemStyleBottom;
pop.isClickBgDismiss = YES;
__weak typeof(self) weakSelf = self; __weak typeof(pop) weakPop = pop;
content.closeHandler = ^{ [weakPop dismiss]; };
content.saveHandler = ^(NSDictionary *selected) {
NSString *name = selected[@"name"] ?: @"";
// 将选择结果同步到本地缓存,供后续登录接口使用
NSString *genderId = selected[@"id"];
NSInteger genderValue = 0;
if ([genderId isKindOfClass:NSString.class]) {
NSInteger v = [genderId integerValue];
// 后端/模型使用 0/1/2对应弹窗里的 1/2/3
if (v >= 1 && v <= 3) {
genderValue = v - 1;
}
}
[[NSUserDefaults standardUserDefaults] setInteger:genderValue forKey:KBSexSelectedGenderKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 同步更新本地 userModel避免再次进入页面还是旧的性别
weakSelf.userModel.gender = (UserSex)genderValue;
[weakSelf rebuildItems];
// 将修改后的用户信息同步到服务端
[weakSelf.myVM updateUserInfo:weakSelf.userModel completion:^(BOOL success, NSError * _Nullable error) {
if (error) {
[KBHUD showError:error.localizedDescription ?: KBLocalized(@"Network error")];
}
}];
[weakPop dismiss];
};
[pop pop];
} else if (indexPath.row == kKBPersonInfoRowLanguage) {
[self openLanguageSelector];
} else if (indexPath.row == kKBPersonInfoRowUserID) {
NSString *userID = self.items[kKBPersonInfoRowUserID][@"value"];
if (userID.length == 0) return;
UIPasteboard.generalPasteboard.string = userID;
[KBHUD showInfo:KBLocalized(@"Copy Success")];
}
}
#pragma mark - Actions
- (void)onTapAvatarEdit {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_avatar_edit"
pageId:@"person_info"
elementId:@"avatar_edit"
extra:nil
completion:nil];
[self presentImagePicker];
}
- (void)onTapLogout {
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_person_logout_btn"
pageId:@"person_info"
elementId:@"logout_btn"
extra:nil
completion:nil];
[self.myVM logout];
}
- (void)onTapUserIdCopy {
NSString *userId = self.userModel.userId ?: @"";
if (userId.length == 0) { return; }
UIPasteboard.generalPasteboard.string = userId;
[KBHUD showInfo:KBLocalized(@"Copied")];
}
#pragma mark - Lazy UI懒加载
- (UITableView *)tableView {
if (!_tableView) {
// 使用 Plain自行实现卡片留白与圆角更贴近设计图
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.backgroundColor = UIColor.clearColor;
_tableView.showsVerticalScrollIndicator = NO;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // 自定义分割
_tableView.dataSource = self; _tableView.delegate = self;
_tableView.contentInset = UIEdgeInsetsMake(8, 0, 16, 0);
}
return _tableView;
}
- (UIView *)headerView {
if (!_headerView) {
CGFloat w = UIScreen.mainScreen.bounds.size.width;
UIView *hv = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 200)];
hv.backgroundColor = UIColor.clearColor;
[hv addSubview:self.avatarView];
[hv addSubview:self.editBadge];
[hv addSubview:self.modifyLabel];
[hv addSubview:self.userIdLabel];
[hv addSubview:self.userIdCopyButton];
[self.avatarView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(hv);
make.top.equalTo(hv).offset(12);
make.width.height.mas_equalTo(96);
}];
[self.editBadge mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(24);
make.centerX.equalTo(self.avatarView.mas_right).offset(-15);
make.centerY.equalTo(self.avatarView.mas_bottom).offset(-15);
}];
[self.modifyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.avatarView.mas_bottom).offset(10);
make.centerX.equalTo(hv);
}];
[self.userIdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.modifyLabel.mas_bottom).offset(10);
make.centerX.equalTo(hv).offset(-KBFit(12.0));
}];
[self.userIdCopyButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.userIdLabel);
make.left.equalTo(self.userIdLabel.mas_right).offset(6);
make.width.height.mas_equalTo(18);
}];
// 头像可点击:弹系统相册
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapAvatarEdit)];
[self.avatarView addGestureRecognizer:tap];
_headerView = hv;
}
return _headerView;
}
- (UIImageView *)avatarView {
if (!_avatarView) {
_avatarView = [[UIImageView alloc] init];
_avatarView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
_avatarView.layer.cornerRadius = 48; _avatarView.layer.masksToBounds = YES;
// 外白描边
_avatarView.layer.borderWidth = 3.0; _avatarView.layer.borderColor = UIColor.whiteColor.CGColor;
// 占位图
UIGraphicsBeginImageContextWithOptions(CGSizeMake(96, 96), NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[[UIColor colorWithRed:0.86 green:0.95 blue:0.90 alpha:1] setFill];
CGContextFillEllipseInRect(ctx, CGRectMake(0, 0, 96, 96));
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
_avatarView.image = img;
_avatarView.userInteractionEnabled = YES;
}
return _avatarView;
}
- (UIButton *)editBadge {
if (!_editBadge) {
_editBadge = [UIButton buttonWithType:UIButtonTypeCustom];
_editBadge.layer.cornerRadius = 12; _editBadge.layer.masksToBounds = YES;
UIImage *img = [UIImage imageNamed:@"myperson_edit_icon"];
[_editBadge setImage:img forState:UIControlStateNormal];
[_editBadge addTarget:self action:@selector(onTapAvatarEdit) forControlEvents:UIControlEventTouchUpInside];
}
return _editBadge;
}
- (UILabel *)modifyLabel {
if (!_modifyLabel) {
_modifyLabel = [UILabel new];
_modifyLabel.text = @"Modify";
_modifyLabel.textColor = [UIColor colorWithHex:KBBlackValue];
_modifyLabel.font = [KBFont bold:18];
}
return _modifyLabel;
}
- (UILabel *)userIdLabel {
if (!_userIdLabel) {
_userIdLabel = [UILabel new];
_userIdLabel.textColor = [UIColor colorWithHex:0x9B9B9B];
_userIdLabel.font = [KBFont regular:12];
}
return _userIdLabel;
}
- (UIButton *)userIdCopyButton {
if (!_userIdCopyButton) {
_userIdCopyButton = [UIButton buttonWithType:UIButtonTypeCustom];
if (@available(iOS 13.0, *)) {
UIImage *image = [UIImage systemImageNamed:@"doc.on.doc"];
if (image) {
[_userIdCopyButton setImage:image forState:UIControlStateNormal];
_userIdCopyButton.tintColor = [UIColor colorWithHex:KBBlackValue];
}
}
if (!_userIdCopyButton.currentImage) {
[_userIdCopyButton setTitle:KBLocalized(@"Copy") forState:UIControlStateNormal];
[_userIdCopyButton setTitleColor:[UIColor colorWithHex:KBBlackValue] forState:UIControlStateNormal];
_userIdCopyButton.titleLabel.font = [KBFont regular:12];
}
[_userIdCopyButton addTarget:self action:@selector(onTapUserIdCopy) forControlEvents:UIControlEventTouchUpInside];
}
return _userIdCopyButton;
}
- (UIButton *)logoutBtn {
if (!_logoutBtn) {
_logoutBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[_logoutBtn setTitle:KBLocalized(@"Log Out") forState:UIControlStateNormal];
[_logoutBtn setTitleColor:[UIColor colorWithHex:0xFF0000] forState:UIControlStateNormal];
_logoutBtn.titleLabel.font = [KBFont medium:16];
_logoutBtn.backgroundColor = UIColor.whiteColor;
_logoutBtn.layer.cornerRadius = 12; _logoutBtn.layer.masksToBounds = YES;
[_logoutBtn addTarget:self action:@selector(onTapLogout) forControlEvents:UIControlEventTouchUpInside];
}
return _logoutBtn;
}
#pragma mark - Image Picker
- (void)presentImagePicker {
if (@available(iOS 14.0, *)) {
PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
config.selectionLimit = 1; // 只选一张
config.filter = [PHPickerFilter imagesFilter];
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:config];
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
} else {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
}
}
#pragma mark - PHPickerViewControllerDelegate
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14.0)) {
[picker dismissViewControllerAnimated:YES completion:nil];
PHPickerResult *first = results.firstObject;
if (!first) return;
NSItemProvider *p = first.itemProvider;
if ([p canLoadObjectOfClass:UIImage.class]) {
__weak typeof(self) weakSelf = self;
[p loadObjectOfClass:UIImage.class completionHandler:^(__kindof id<NSItemProviderReading> _Nullable object, NSError * _Nullable error) {
UIImage *img = ([object isKindOfClass:UIImage.class] ? (UIImage *)object : nil);
if (!img) return;
NSUInteger targetKB = 50;
NSData *compressedData = [weakSelf kb_compressImage:img targetKB:targetKB];
if (!compressedData) return;
UIImage *compressedImage = [UIImage imageWithData:compressedData];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.myVM upLoadAvatarWithData:compressedData completion:^(BOOL success, NSError * _Nullable error) {
if (error) {
[KBHUD showError:error.localizedDescription];
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.avatarView.image = compressedImage;
});
}];
});
}];
}
}
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {
UIImage *img = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage];
if (img) {
UIImage *compressed = [self kb_compressImage:img maxPixel:512 quality:0.85];
self.avatarView.image = compressed;
self.avatarJPEGData = UIImageJPEGRepresentation(compressed, 0.85);
}
[picker dismissViewControllerAnimated:YES completion:nil];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
}
/// 压缩图片:最长边不超过 maxPixel输出近似 quality 的 JPEG
- (UIImage *)kb_compressImage:(UIImage *)image maxPixel:(CGFloat)maxPixel quality:(CGFloat)quality {
if (!image) return nil;
maxPixel = MAX(64, maxPixel);
CGSize size = image.size;
CGFloat maxSide = MAX(size.width, size.height);
CGSize target = size;
if (maxSide > maxPixel) {
CGFloat scale = maxPixel / maxSide;
target = CGSizeMake(floor(size.width * scale), floor(size.height * scale));
}
UIGraphicsBeginImageContextWithOptions(target, YES, 1.0);
[image drawInRect:CGRectMake(0, 0, target.width, target.height)];
UIImage *scaled = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSData *jpeg = UIImageJPEGRepresentation(scaled ?: image, MIN(MAX(quality, 0.2), 0.95));
UIImage *result = [UIImage imageWithData:jpeg] ?: scaled ?: image;
return result;
}
/// 按目标大小KB压缩图片
/// 1. 先逐步降低 JPEG 质量;
/// 2. 如果质量已经降到下限仍然太大,再按比例缩小尺寸,并重复尝试。
- (nullable NSData *)kb_compressImage:(UIImage *)image targetKB:(NSUInteger)targetKB {
if (!image || targetKB == 0) { return nil; }
NSUInteger maxBytes = targetKB * 1024;
// 初始质量参数
CGFloat compression = 0.9f;
CGFloat minCompression = 0.1f; // 不建议再低了,太低会糊
NSData *data = UIImageJPEGRepresentation(image, compression);
if (!data) return nil;
// 如果一开始就小于目标大小,直接返回
if (data.length <= maxBytes) {
return data;
}
// 1) 先通过降低质量来压缩
while (data.length > maxBytes && compression > minCompression + 0.01f) {
compression -= 0.1f;
data = UIImageJPEGRepresentation(image, compression);
if (!data) return nil;
}
if (data.length <= maxBytes) {
return data;
}
// 2) 质量降到下限还是太大 -> 等比缩放尺寸
UIImage *currentImage = image;
// 防止死循环,限定最多缩放几次
NSInteger maxResizeCount = 6;
NSInteger resizeCount = 0;
while (data.length > maxBytes && resizeCount < maxResizeCount) {
resizeCount++;
// 按面积比例来算缩放因子:新的面积约等于 (maxBytes / 当前字节数) * 原面积
CGFloat ratio = (CGFloat)maxBytes / (CGFloat)data.length;
// 为了更激进一点,可以乘个经验系数,比如 0.8
ratio *= 0.8f;
if (ratio <= 0.0f) {
ratio = 0.5f; // 兜底,至少缩小一半
}
CGFloat scale = sqrt(ratio);
if (scale >= 1.0f) {
scale = 0.5f; // 理论上不会走到这里,兜底
}
CGSize newSize = CGSizeMake(currentImage.size.width * scale,
currentImage.size.height * scale);
if (newSize.width < 10 || newSize.height < 10) {
// 太小就没意义了,直接 break返回目前能做到的最小值
break;
}
// 绘制缩小后的图片
UIGraphicsBeginImageContextWithOptions(newSize, NO, currentImage.scale);
[currentImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (!resizedImage) {
break;
}
currentImage = resizedImage;
// 每次尺寸缩小之后,再重新从稍高一点的质量开始压一轮
compression = 0.9f;
data = UIImageJPEGRepresentation(currentImage, compression);
if (!data) return nil;
// 再次在质量范围内往下压
while (data.length > maxBytes && compression > minCompression + 0.01f) {
compression -= 0.1f;
data = UIImageJPEGRepresentation(currentImage, compression);
if (!data) return nil;
}
}
// 最终返回当前能做到的最小数据(可能略大于目标,但已经尽力)
return data;
}
- (KBMyVM *)myVM{
if (!_myVM) {
_myVM = [[KBMyVM alloc] init];
}
return _myVM;
}
@end