// // KBPersonInfoVC.m // keyBoard // // Created by Mac on 2025/11/11. // // // KBPersonInfoVC.m // 个人资料 // #import "KBPersonInfoVC.h" #import #import "KBPersonInfoItemCell.h" #import #import "LSTPopView.h" #import "KBChangeNicknamePopView.h" #import "KBGenderPickerPopView.h" #import "KBMyVM.h" @interface KBPersonInfoVC () // 列表 @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) UIButton *logoutBtn; // 数据 @property (nonatomic, copy) NSArray *items; // {title,value,arrow,copy} // 压缩后的头像 JPEG 数据(可用于上传) @property (nonatomic, strong) NSData *avatarJPEGData; @property (nonatomic, strong) KBMyVM *myVM; @end @implementation KBPersonInfoVC - (void)viewDidLoad { [super viewDidLoad]; self.kb_titleLabel.text = KBLocalized(@"Settings"); // 导航标题 self.kb_navView.backgroundColor = [UIColor clearColor]; self.view.backgroundColor = [UIColor colorWithHex:0xF8F8F8]; // 构造数据 // self.items = @[ // @{ @"title": KBLocalized(@"Nickname"), @"value": @"", @"arrow": @YES, @"copy": @NO }, // @{ @"title": KBLocalized(@"Gender"), @"value": @"Choose", @"arrow": @YES, @"copy": @NO }, // @{ @"title": KBLocalized(@"User ID"), @"value": @"", @"arrow": @NO, @"copy": @YES }, // ]; self.items = @[ @{ @"title": KBLocalized(@"Nickname"), @"value": self.userModel.nickName, @"arrow": @YES, @"copy": @NO }, @{ @"title": KBLocalized(@"Gender"), @"value": @"Choose", @"arrow": @YES, @"copy": @NO }, @{ @"title": KBLocalized(@"User ID"), @"value": self.userModel.userId, @"arrow": @NO, @"copy": @YES }, ]; [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)setUserModel:(KBUser *)userModel{ _userModel = userModel; [self.avatarView kb_setAvatarURL:userModel.avatarUrl placeholder:KBPlaceholderImage]; self.modifyLabel.text = userModel.nickName; } #pragma mark - UITableView - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.items.count; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 56.0; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return 12.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]; } 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]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { // 昵称编辑 -> 弹窗 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) { // 更新第一行展示 NSMutableArray *m = [weakSelf.items mutableCopy]; NSMutableDictionary *d0 = [m.firstObject mutableCopy]; d0[@"value"] = nickname; m[0] = d0; weakSelf.items = m; [weakSelf.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationNone]; } [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 == 1) { // 性别选择 -> 弹窗 NSArray *genders = @[ @{@"id":@"1",@"name":@"Male"}, @{@"id":@"2",@"name":@"Female"}, @{@"id":@"3",@"name":@"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[1][@"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"] ?: @""; NSMutableArray *m = [weakSelf.items mutableCopy]; NSMutableDictionary *d1 = [m[1] mutableCopy]; d1[@"value"] = name; m[1] = d1; weakSelf.items = m; [weakSelf.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone]; [weakPop dismiss]; }; [pop pop]; }else if (indexPath.row == 2){ NSString *userID = self.items[2][@"value"]; if (userID.length == 0) return; UIPasteboard.generalPasteboard.string = userID; [KBHUD showInfo:KBLocalized(@"Copy Success")]; } } #pragma mark - Actions - (void)onTapAvatarEdit { [self presentImagePicker]; } - (void)onTapLogout { [self.myVM logout]; } #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, 180)]; hv.backgroundColor = UIColor.clearColor; [hv addSubview:self.avatarView]; [hv addSubview:self.editBadge]; [hv addSubview:self.modifyLabel]; [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); }]; // 头像可点击:弹系统相册 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; } - (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 *)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 _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 *)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