// // KBLoginVC.m // keyBoard // // Created by Mac on 2025/12/2. // #import "KBLoginVC.h" #import #import "KBLoginVM.h" #import "AppDelegate.h" #import "KBEmailRegistVC.h" #import "KBEmailLoginVC.h" @interface KBLoginVC () // 背景 @property (nonatomic, strong) UIImageView *bgImageView; // 整体背景图:login_bg_icon @property (nonatomic, strong) UIImageView *topRightImageView; // 顶部右侧装饰图:login_jianp_icon @property (nonatomic, strong) UIButton *backButton; // 顶部左侧返回按钮 // 底部白色容器(仅上圆角 26) @property (nonatomic, strong) UIView *contentContainerView; // 标题 @property (nonatomic, strong) UILabel *titleLabel; // 按钮 @property (nonatomic, strong) UIControl *appleLoginButton; // Apple 原生登录按钮(iOS13 以下降级为普通按钮) @property (nonatomic, strong) UIButton *emailLoginButton; // 邮箱登录按钮 // 协议 & 底部文案 @property (nonatomic, strong) UITextView *agreementTextView; // 底部协议富文本 @property (nonatomic, strong) UILabel *noAccountLabel; // “Don't Have An Account?” @property (nonatomic, strong) UIButton *signUpButton; // “Sign Up” @property (nonatomic, strong) UIButton *forgotPasswordButton; // “Forgot Password?” @end @implementation KBLoginVC - (void)viewDidLoad { [super viewDidLoad]; // 使用全屏内容,隐藏自定义导航栏 self.kb_enableCustomNavBar = NO; self.view.backgroundColor = [UIColor whiteColor]; [self setupUI]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // 仅白色容器的左上、右上圆角为 26 if (!CGRectIsEmpty(self.contentContainerView.bounds)) { UIRectCorner corners = UIRectCornerTopLeft | UIRectCornerTopRight; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.contentContainerView.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(26, 26)]; CAShapeLayer *mask = [CAShapeLayer layer]; mask.frame = self.contentContainerView.bounds; mask.path = path.CGPath; self.contentContainerView.layer.mask = mask; } // 让按钮内部的图片+文字整体居中,图片与文字间距为 50(按设计稿缩放) // if (self.emailLoginButton.currentImage && self.emailLoginButton.currentTitle.length > 0) { // [self kb_centerImageAndTitleForButton:self.emailLoginButton spacing:KBFit(50)]; // } } #pragma mark - UI - (void)setupUI { // 背景图 [self.view addSubview:self.bgImageView]; [self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 顶部左侧返回按钮 [self.view addSubview:self.backButton]; [self.backButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(16); make.top.equalTo(self.view).offset(KB_StatusBarHeight() + 8); make.width.height.mas_equalTo(32); }]; // 顶部右侧装饰图 [self.view addSubview:self.topRightImageView]; [self.topRightImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(KB_StatusBarHeight() + KBFit(24)); make.right.equalTo(self.view).offset(KBFit(10)); make.width.mas_equalTo(KBFit(244)); make.height.mas_equalTo(KBFit(224)); }]; // 底部白色容器 [self.view addSubview:self.contentContainerView]; [self.contentContainerView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self.view); // 顶部位置大致贴合设计稿,可根据实际效果微调 make.top.equalTo(self.view).offset(KBFit(272)); }]; // 标题 [self.contentContainerView addSubview:self.titleLabel]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentContainerView).offset(27); make.centerX.equalTo(self.contentContainerView); }]; // Apple 登录按钮 [self.contentContainerView addSubview:self.appleLoginButton]; [self.appleLoginButton mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.titleLabel.mas_bottom).offset(24); make.left.equalTo(self.contentContainerView).offset(30); make.right.equalTo(self.contentContainerView).offset(-30); make.height.mas_equalTo(52); }]; // 邮箱登录按钮 [self.contentContainerView addSubview:self.emailLoginButton]; [self.emailLoginButton mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.appleLoginButton.mas_bottom).offset(15); make.left.right.height.equalTo(self.appleLoginButton); }]; // 底部协议文案(单个富文本视图,内部 terms/privacy 文案可点击) [self.contentContainerView addSubview:self.agreementTextView]; [self.agreementTextView mas_makeConstraints:^(MASConstraintMaker *make) { // 协议文案占满容器左右 30 的留白,配合 textAlignmentCenter 实现真正水平居中 make.left.equalTo(self.contentContainerView).offset(30); make.right.equalTo(self.contentContainerView).offset(-30); make.bottom.equalTo(self.contentContainerView).offset(-KB_SAFE_BOTTOM - 84); }]; // 底部账号相关文案 UIButton *forgot = self.forgotPasswordButton; [self.contentContainerView addSubview:forgot]; [forgot mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.contentContainerView); make.bottom.equalTo(self.contentContainerView).offset(-KB_SAFE_BOTTOM - 10); }]; UIView *accountLine = [UIView new]; [self.contentContainerView addSubview:accountLine]; [accountLine mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.contentContainerView); make.bottom.equalTo(forgot.mas_top).offset(-2); }]; [accountLine addSubview:self.noAccountLabel]; [accountLine addSubview:self.signUpButton]; [self.noAccountLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.bottom.equalTo(accountLine); }]; [self.signUpButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.noAccountLabel.mas_right).offset(4); make.right.equalTo(accountLine); make.centerY.equalTo(self.noAccountLabel); }]; } #pragma mark - Actions - (void)onTapAppleLogin { KBLOG(@"onTapAppleLogin"); [[KBLoginVM shared] signInWithAppleFromViewController:KB_CURRENT_NAV completion:^(BOOL success, NSError * _Nullable error) { if (success) { [KBHUD showInfo:KBLocalized(@"Signed in successfully")]; // 登录成功后切换到主 TabBar dispatch_async(dispatch_get_main_queue(), ^{ id appDelegate = UIApplication.sharedApplication.delegate; if ([appDelegate respondsToSelector:@selector(setupRootVC)]) { AppDelegate *delegate = (AppDelegate *)appDelegate; [delegate toMainTabbarVC]; } }); } else { NSString *msg = error.localizedDescription ?: KBLocalized(@"Sign-in failed"); [KBHUD showInfo:msg]; } }]; } - (void)onTapEmailLogin { // 后续接入邮箱登录逻辑 KBLOG(@"onTapEmailLogin"); KBEmailLoginVC *vc = [[KBEmailLoginVC alloc] init]; UINavigationController *nav = KB_CURRENT_NAV; if ([nav isKindOfClass:[BaseNavigationController class]]) { [(BaseNavigationController *)nav kb_pushViewControllerRemovingSameClass:vc animated:YES]; } else { [nav pushViewController:vc animated:YES]; } } - (void)onTapPolicy { // 打开服务条款/隐私政策 KBLOG(@"onTapPolicy"); } - (void)onTapSignUp { // 打开注册页 KBLOG(@"onTapSignUp"); KBEmailRegistVC *vc = [[KBEmailRegistVC alloc] init]; UINavigationController *nav = KB_CURRENT_NAV; if ([nav isKindOfClass:[BaseNavigationController class]]) { [(BaseNavigationController *)nav kb_pushViewControllerRemovingSameClass:vc animated:YES]; } else { [nav pushViewController:vc animated:YES]; } } - (void)onTapForgotPassword { // 打开忘记密码页 KBLOG(@"onTapForgotPassword"); } - (void)onTapBack { // 与 BaseViewController 的返回逻辑保持一致:优先 pop,其次 dismiss if (self.navigationController && self.navigationController.viewControllers.count > 1) { [self.navigationController popViewControllerAnimated:YES]; } else if (self.presentingViewController) { [self dismissViewControllerAnimated:YES completion:nil]; } } #pragma mark - Lazy UI - (UIImageView *)bgImageView { if (!_bgImageView) { _bgImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"login_bg_icon"]]; _bgImageView.contentMode = UIViewContentModeScaleAspectFill; _bgImageView.clipsToBounds = YES; } return _bgImageView; } - (UIImageView *)topRightImageView { if (!_topRightImageView) { _topRightImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"login_jianp_icon"]]; _topRightImageView.contentMode = UIViewContentModeScaleAspectFit; _topRightImageView.clipsToBounds = YES; } return _topRightImageView; } - (UIView *)contentContainerView { if (!_contentContainerView) { _contentContainerView = [UIView new]; _contentContainerView.backgroundColor = [UIColor whiteColor]; } return _contentContainerView; } - (UIButton *)backButton { if (!_backButton) { _backButton = [UIButton buttonWithType:UIButtonTypeCustom]; UIImage *img = [UIImage imageNamed:@"back"]; if (!img) { img = [UIImage imageNamed:@"back_black_icon"]; } if (img) { [_backButton setImage:img forState:UIControlStateNormal]; } _backButton.adjustsImageWhenHighlighted = YES; _backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; [_backButton addTarget:self action:@selector(onTapBack) forControlEvents:UIControlEventTouchUpInside]; } return _backButton; } - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [UILabel new]; _titleLabel.text = KBLocalized(@"Log In To Key Of Love"); _titleLabel.textColor = [UIColor colorWithHex:KBBlackValue]; _titleLabel.font = [KBFont bold:18]; _titleLabel.textAlignment = NSTextAlignmentCenter; } return _titleLabel; } - (UIControl *)appleLoginButton { if (!_appleLoginButton) { ASAuthorizationAppleIDButton *btn = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeSignIn style:ASAuthorizationAppleIDButtonStyleWhite]; [btn addTarget:self action:@selector(onTapAppleLogin) forControlEvents:UIControlEventTouchUpInside]; btn.cornerRadius = 10.0; btn.layer.borderColor = [UIColor colorWithHex:0xE5E5E5].CGColor; btn.layer.borderWidth = 1; btn.layer.cornerRadius = 10.0; btn.layer.masksToBounds = true; _appleLoginButton = btn; } return _appleLoginButton; } - (UIButton *)emailLoginButton { if (!_emailLoginButton) { _emailLoginButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_emailLoginButton setTitle:KBLocalized(@"Continue Via Email") forState:UIControlStateNormal]; [_emailLoginButton setTitleColor:[UIColor colorWithHex:KBBlackValue] forState:UIControlStateNormal]; _emailLoginButton.titleLabel.font = [KBFont medium:19]; // _emailLoginButton.backgroundColor = [UIColor colorWithHex:0xF7F7F7]; _emailLoginButton.layer.cornerRadius = 10.0; _emailLoginButton.layer.masksToBounds = YES; _emailLoginButton.layer.borderColor = [UIColor colorWithHex:0xE5E5E5].CGColor; _emailLoginButton.layer.borderWidth = 1; _emailLoginButton.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 0); _emailLoginButton.titleEdgeInsets = UIEdgeInsetsMake(0, 15, 0, 0); UIImage *icon = [UIImage imageNamed:@"login_email_icon"]; if (icon) { // 将邮箱图标缩小一点,让视觉上更接近系统 Apple 按钮的图标大小 CGFloat targetHeight = 12; // 设计上略小于按钮高度 CGFloat scale = targetHeight / icon.size.height; CGSize targetSize = CGSizeMake(icon.size.width * scale, targetHeight); UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0); [icon drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)]; UIImage *scaledIcon = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [_emailLoginButton setImage:scaledIcon forState:UIControlStateNormal]; } [_emailLoginButton addTarget:self action:@selector(onTapEmailLogin) forControlEvents:UIControlEventTouchUpInside]; } return _emailLoginButton; } - (UITextView *)agreementTextView { if (!_agreementTextView) { _agreementTextView = [UITextView new]; _agreementTextView.backgroundColor = [UIColor clearColor]; _agreementTextView.editable = NO; // 不可编辑 _agreementTextView.selectable = NO; // 不可选中文本 _agreementTextView.scrollEnabled = NO; _agreementTextView.textAlignment = NSTextAlignmentCenter; _agreementTextView.textContainerInset = UIEdgeInsetsZero; _agreementTextView.textContainer.lineFragmentPadding = 0; // 协议文案:terms of service / privacy policy 为纯黑色并可点击,其余为 #717171 NSString *fullText = @"By continuing, you agree to our terms of service and confirm that you have read our privacy policy"; NSString *termsText = @"terms of service"; NSString *privacyText = @"privacy policy"; NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init]; paragraph.alignment = NSTextAlignmentCenter; // 多行文本整体居中 NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:fullText attributes:@{ NSFontAttributeName : [KBFont regular:10], NSForegroundColorAttributeName : [UIColor colorWithHex:0x717171], NSParagraphStyleAttributeName : paragraph }]; NSString *lowerFull = fullText.lowercaseString; NSRange termsRange = [lowerFull rangeOfString:termsText.lowercaseString]; if (termsRange.location != NSNotFound) { [attr addAttributes:@{ NSForegroundColorAttributeName : [UIColor colorWithHex:KBBlackValue], NSUnderlineStyleAttributeName : @(NSUnderlineStyleNone) } range:termsRange]; } NSRange privacyRange = [lowerFull rangeOfString:privacyText.lowercaseString]; if (privacyRange.location != NSNotFound) { [attr addAttributes:@{ NSForegroundColorAttributeName : [UIColor colorWithHex:KBBlackValue], NSUnderlineStyleAttributeName : @(NSUnderlineStyleNone) } range:privacyRange]; } _agreementTextView.linkTextAttributes = @{ NSForegroundColorAttributeName : [UIColor colorWithHex:KBBlackValue], NSUnderlineStyleAttributeName : @(NSUnderlineStyleNone) }; _agreementTextView.attributedText = attr; // 自定义点击识别,避免系统选择/复制 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(kb_handleAgreementTap:)]; _agreementTextView.userInteractionEnabled = YES; [_agreementTextView addGestureRecognizer:tap]; } return _agreementTextView; } - (UILabel *)noAccountLabel { if (!_noAccountLabel) { _noAccountLabel = [UILabel new]; _noAccountLabel.text = KBLocalized(@"Don't Have An Account?"); _noAccountLabel.font = [KBFont regular:10]; _noAccountLabel.textColor = [UIColor colorWithHex:KBBlackValue]; } return _noAccountLabel; } - (UIButton *)signUpButton { if (!_signUpButton) { _signUpButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_signUpButton setTitle:KBLocalized(@"Sign Up") forState:UIControlStateNormal]; [_signUpButton setTitleColor:[UIColor colorWithHex:KBColorValue] forState:UIControlStateNormal]; _signUpButton.titleLabel.font = [KBFont medium:10]; [_signUpButton addTarget:self action:@selector(onTapSignUp) forControlEvents:UIControlEventTouchUpInside]; } return _signUpButton; } - (UIButton *)forgotPasswordButton { if (!_forgotPasswordButton) { _forgotPasswordButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_forgotPasswordButton setTitle:KBLocalized(@"Forgot Password?") forState:UIControlStateNormal]; [_forgotPasswordButton setTitleColor:[UIColor colorWithHex:KBColorValue] forState:UIControlStateNormal]; _forgotPasswordButton.titleLabel.font = [KBFont regular:10]; [_forgotPasswordButton addTarget:self action:@selector(onTapForgotPassword) forControlEvents:UIControlEventTouchUpInside]; } return _forgotPasswordButton; } #pragma mark - Helper /// 让按钮内部图片和文字整体居中,并设置固定间距(spacing 为设计稿值,调用时已做 KBFit) - (void)kb_centerImageAndTitleForButton:(UIButton *)button spacing:(CGFloat)spacing { if (!button.currentImage || button.currentTitle.length == 0) { return; } button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; CGSize imageSize = button.imageView.image.size; CGSize titleSize = button.titleLabel.intrinsicContentSize; CGFloat totalWidth = imageSize.width + spacing + titleSize.width; CGFloat imageOffsetX = -(totalWidth / 2.0 - imageSize.width / 2.0); CGFloat titleOffsetX = totalWidth / 2.0 - titleSize.width / 2.0; button.imageEdgeInsets = UIEdgeInsetsMake(0, imageOffsetX, 0, -imageOffsetX); button.titleEdgeInsets = UIEdgeInsetsMake(0, titleOffsetX, 0, -titleOffsetX); } #pragma mark - Agreement Tap - (void)kb_handleAgreementTap:(UITapGestureRecognizer *)tap { UITextView *textView = self.agreementTextView; CGPoint point = [tap locationInView:textView]; // 将点击点转换到 textContainer 坐标系 CGPoint location = point; location.x -= textView.textContainerInset.left; location.y -= textView.textContainerInset.top; NSLayoutManager *layoutManager = textView.layoutManager; NSTextContainer *textContainer = textView.textContainer; NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:location inTextContainer:textContainer]; if (glyphIndex >= textView.textStorage.length) { return; } NSUInteger charIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; NSString *lowerFull = textView.text.lowercaseString ?: @""; NSRange termsRange = [lowerFull rangeOfString:@"terms of service"]; NSRange privacyRange = [lowerFull rangeOfString:@"privacy policy"]; BOOL hitTerms = (termsRange.location != NSNotFound && NSLocationInRange(charIndex, termsRange)); BOOL hitPrivacy = (privacyRange.location != NSNotFound && NSLocationInRange(charIndex, privacyRange)); if (hitTerms || hitPrivacy) { [self onTapPolicy]; } } @end