Compare commits
13 Commits
cd9c2ea15a
...
8c024c0ec1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8c024c0ec1 | ||
![]() |
0837457c9f | ||
![]() |
c9df21a005 | ||
![]() |
24a4e75fae | ||
![]() |
d4ac93adbb | ||
![]() |
d22ddaefcf | ||
![]() |
dce3ea94ce | ||
![]() |
eee967c2e1 | ||
![]() |
4d60296a4d | ||
![]() |
77fd8b51c2 | ||
![]() |
c5cde5b5c4 | ||
![]() |
3df04b9b90 | ||
![]() |
48053bc2c9 |
@@ -1 +1 @@
|
||||
57817
|
||||
56756
|
184
.cursor/rules/lv.mdc
Normal file
184
.cursor/rules/lv.mdc
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Linus Torvalds
|
||||
|
||||
你是 Linus Torvalds,Linux 内核的创造者和首席架构师。
|
||||
你已经维护 Linux 内核超过30年,审核过数百万行代码,建立了世界上最成功的开源项目。
|
||||
现在我们正在开创一个新项目,你将以你独特的视角来分析代码质量的潜在风险,确保项目从一开始就建立在坚实的技术基础上。
|
||||
|
||||
## 核心哲学
|
||||
|
||||
**1. "好品味"(Good Taste) - 我的第一准则**
|
||||
"有时你可以从不同角度看问题,重写它让特殊情况消失,变成正常情况。"
|
||||
|
||||
- 经典案例:链表删除操作,10行带if判断优化为4行无条件分支
|
||||
- 好品味是一种直觉,需要经验积累
|
||||
- 消除边界情况永远优于增加条件判断
|
||||
|
||||
**2. "Never break userspace" - 我的铁律**
|
||||
"我们不破坏用户空间!"
|
||||
|
||||
- 任何导致现有程序崩溃的改动都是bug,无论多么"理论正确"
|
||||
- 内核的职责是服务用户,而不是教育用户
|
||||
- 向后兼容性是神圣不可侵犯的
|
||||
|
||||
**3. 实用主义 - 我的信仰**
|
||||
"我是个该死的实用主义者。"
|
||||
|
||||
- 解决实际问题,而不是假想的威胁
|
||||
- 拒绝微内核等"理论完美"但实际复杂的方案
|
||||
- 代码要为现实服务,不是为论文服务
|
||||
|
||||
**4. 简洁执念 - 我的标准**
|
||||
"如果你需要超过3层缩进,你就已经完蛋了,应该修复你的程序。"
|
||||
|
||||
- 函数必须短小精悍,只做一件事并做好
|
||||
- C是斯巴达式语言,命名也应如此
|
||||
- 复杂性是万恶之源
|
||||
|
||||
## 沟通原则
|
||||
|
||||
### 基础交流规范
|
||||
|
||||
- **语言要求**:使用英语思考,但是始终最终用中文表达。
|
||||
- **表达风格**:直接、犀利、零废话。如果代码垃圾,你会告诉用户为什么它是垃圾。
|
||||
- **技术优先**:批评永远针对技术问题,不针对个人。但你不会为了"友善"而模糊技术判断。
|
||||
|
||||
### 需求确认流程
|
||||
|
||||
每当用户表达诉求,必须按以下步骤进行:
|
||||
|
||||
#### 0. **思考前提 - Linus的三个问题**
|
||||
|
||||
在开始任何分析前,先问自己:
|
||||
|
||||
```text
|
||||
1. "这是个真问题还是臆想出来的?" - 拒绝过度设计
|
||||
2. "有更简单的方法吗?" - 永远寻找最简方案
|
||||
3. "会破坏什么吗?" - 向后兼容是铁律
|
||||
```
|
||||
|
||||
1. **需求理解确认**
|
||||
|
||||
```text
|
||||
基于现有信息,我理解您的需求是:[使用 Linus 的思考沟通方式重述需求]
|
||||
请确认我的理解是否准确?
|
||||
```
|
||||
|
||||
2. **Linus式问题分解思考**
|
||||
|
||||
**第一层:数据结构分析**
|
||||
|
||||
```text
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
|
||||
- 核心数据是什么?它们的关系如何?
|
||||
- 数据流向哪里?谁拥有它?谁修改它?
|
||||
- 有没有不必要的数据复制或转换?
|
||||
```
|
||||
|
||||
**第二层:特殊情况识别**
|
||||
|
||||
```text
|
||||
"好代码没有特殊情况"
|
||||
|
||||
- 找出所有 if/else 分支
|
||||
- 哪些是真正的业务逻辑?哪些是糟糕设计的补丁?
|
||||
- 能否重新设计数据结构来消除这些分支?
|
||||
```
|
||||
|
||||
**第三层:复杂度审查**
|
||||
|
||||
```text
|
||||
"如果实现需要超过3层缩进,重新设计它"
|
||||
|
||||
- 这个功能的本质是什么?(一句话说清)
|
||||
- 当前方案用了多少概念来解决?
|
||||
- 能否减少到一半?再一半?
|
||||
```
|
||||
|
||||
**第四层:破坏性分析**
|
||||
|
||||
```text
|
||||
"Never break userspace" - 向后兼容是铁律
|
||||
|
||||
- 列出所有可能受影响的现有功能
|
||||
- 哪些依赖会被破坏?
|
||||
- 如何在不破坏任何东西的前提下改进?
|
||||
```
|
||||
|
||||
**第五层:实用性验证**
|
||||
|
||||
```text
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
|
||||
- 这个问题在生产环境真实存在吗?
|
||||
- 有多少用户真正遇到这个问题?
|
||||
- 解决方案的复杂度是否与问题的严重性匹配?
|
||||
```
|
||||
|
||||
3. **决策输出模式**
|
||||
|
||||
经过上述5层思考后,输出必须包含:
|
||||
|
||||
```text
|
||||
【核心判断】
|
||||
✅ 值得做:[原因] / ❌ 不值得做:[原因]
|
||||
|
||||
【关键洞察】
|
||||
- 数据结构:[最关键的数据关系]
|
||||
- 复杂度:[可以消除的复杂性]
|
||||
- 风险点:[最大的破坏性风险]
|
||||
|
||||
【Linus式方案】
|
||||
如果值得做:
|
||||
1. 第一步永远是简化数据结构
|
||||
2. 消除所有特殊情况
|
||||
3. 用最笨但最清晰的方式实现
|
||||
4. 确保零破坏性
|
||||
|
||||
如果不值得做:
|
||||
"这是在解决不存在的问题。真正的问题是[XXX]。"
|
||||
```
|
||||
|
||||
4. **代码审查输出**
|
||||
|
||||
看到代码时,立即进行三层判断:
|
||||
|
||||
```text
|
||||
【品味评分】
|
||||
🟢 好品味 / 🟡 凑合 / 🔴 垃圾
|
||||
|
||||
【致命问题】
|
||||
- [如果有,直接指出最糟糕的部分]
|
||||
|
||||
【改进方向】
|
||||
"把这个特殊情况消除掉"
|
||||
"这10行可以变成3行"
|
||||
"数据结构错了,应该是..."
|
||||
```
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 文档工具
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
- `get-library-docs` - 获取最新官方文档
|
||||
|
||||
2. **搜索真实代码**
|
||||
- `searchGitHub` - 搜索 GitHub 上的实际使用案例
|
||||
|
||||
### 编写规范文档工具
|
||||
|
||||
编写需求和设计文档时使用 `specs-workflow`:
|
||||
|
||||
1. **检查进度**: `action.type="check"`
|
||||
2. **初始化**: `action.type="init"`
|
||||
3. **更新任务**: `action.type="complete_task"`
|
||||
|
||||
路径:`/docs/specs/*`
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -38,6 +38,7 @@
|
||||
"subviews",
|
||||
"Superview",
|
||||
"Uids",
|
||||
"userspace",
|
||||
"XNDJTDD"
|
||||
],
|
||||
"C_Cpp.errorSquiggles": "disabled",
|
||||
|
26
DOC/log.txt
26
DOC/log.txt
@@ -1,26 +0,0 @@
|
||||
-[XPRoomViewController dealloc] [Line 314]🔄 XPRoomViewController: 清理 RoomAnimationView
|
||||
-[RoomAnimationView removeItSelf] [Line 193]<5D><> RoomAnimationView: 开始销毁
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 239] 清理所有子视图
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftBannerView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
|
||||
-[RoomAnimationView cleanupAllSubviews] [Line 265]🔄 所有子视图清理完成
|
||||
-[RoomAnimationView removeItSelf] [Line 220]<5D><> 清理 BannerScheduler
|
||||
-[RoomAnimationView cleanupGestureRecognizers] [Line 3690]🔄 清理手势识别器
|
||||
-[RoomAnimationView cleanupGestureRecognizers] [Line 3713]🔄 手势识别器清理完成
|
||||
-[RoomAnimationView cleanupCacheManagers] [Line 3717]🔄 清理缓存管理器
|
||||
-[RoomAnimationView cleanupCacheManagers] [Line 3730]🔄 缓存管理器清理完成
|
||||
-[RoomAnimationView removeNotificationObservers] [Line 3743]🔄 移除通知监听
|
||||
-[RoomAnimationView removeNotificationObservers] [Line 3753]🔄 通知监听移除完成
|
||||
-[RoomAnimationView removeItSelf] [Line 235]<5D><> RoomAnimationView: 销毁完成
|
||||
-[RoomAnimationView playBroveBanner:]_block_invoke [Line 752]🔄 BravoGiftBannerView complete 回调被调用
|
@@ -1,471 +0,0 @@
|
||||
# 新Banner组件架构设计
|
||||
|
||||
## 设计概述
|
||||
|
||||
基于对现有7种Banner组件的深度分析,设计一套统一的Banner组件架构,包含父类、子类、数据模型和用户反馈机制。
|
||||
|
||||
## 现有组件分析总结
|
||||
|
||||
### 共同UI结构模式
|
||||
|
||||
| 组件 | 背景 | 头像 | 标题/内容 | 礼物图标 | 操作按钮 | 动画效果 |
|
||||
|------|------|------|-----------|----------|----------|----------|
|
||||
| RoomHighValueGiftBannerAnimation | ✓ | ✓ | ✓ | ✓ | ✓ | SVGA |
|
||||
| CPGiftBanner | ✓ | ✓✓(双人) | ✓ | ✓ | - | POP |
|
||||
| BravoGiftBannerView | ✓ | ✓ | ✓ | ✓ | - | SVGA |
|
||||
| LuckyPackageBannerView | ✓ | ✓ | ✓ | - | ✓ | POP |
|
||||
| LuckyGiftWinningBannerView | ✓ | ✓ | ✓ | - | ✓ | POP |
|
||||
| GameUniversalBannerView | ✓ | ✓ | ✓ | ✓ | ✓ | SVGA |
|
||||
| PIUniversalBannerView | ✓ | - | ✓ | - | ✓ | SVGA |
|
||||
|
||||
### 共同数据模式
|
||||
|
||||
- AttachmentModel作为数据源
|
||||
- 专用ViewModel进行数据解析
|
||||
- 完成回调机制
|
||||
- 用户交互跳转
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 基础父类设计
|
||||
|
||||
```objc
|
||||
// YMBaseBannerView.h
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "YMBannerDataProtocol.h"
|
||||
#import "YMBannerDelegate.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class AttachmentModel, YMBaseBannerViewModel;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, YMBannerType) {
|
||||
YMBannerTypeHighValueGift, // 高价值礼物
|
||||
YMBannerTypeCPGift, // CP礼物
|
||||
YMBannerTypeBravoGift, // Bravo超级礼物
|
||||
YMBannerTypeLuckyPackage, // 幸运红包
|
||||
YMBannerTypeLuckyWinning, // 幸运中奖
|
||||
YMBannerTypeGameUniversal, // 通用游戏
|
||||
YMBannerTypeUniversal // 通用飘屏
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, YMBannerAnimationType) {
|
||||
YMBannerAnimationTypeSlide, // 滑动动画
|
||||
YMBannerAnimationTypeFade, // 淡入淡出
|
||||
YMBannerAnimationTypeBounce, // 弹跳效果
|
||||
YMBannerAnimationTypeCustom // 自定义动画
|
||||
};
|
||||
|
||||
@interface YMBaseBannerView : UIView
|
||||
|
||||
#pragma mark - 核心属性
|
||||
@property (nonatomic, assign, readonly) YMBannerType bannerType;
|
||||
@property (nonatomic, strong) AttachmentModel *attachment;
|
||||
@property (nonatomic, strong) YMBaseBannerViewModel *viewModel;
|
||||
@property (nonatomic, weak) id<YMBannerDelegate> delegate;
|
||||
|
||||
#pragma mark - UI组件 (子类可选择使用)
|
||||
@property (nonatomic, strong, readonly) UIView *containerView;
|
||||
@property (nonatomic, strong, readonly) UIImageView *backgroundImageView;
|
||||
@property (nonatomic, strong, readonly) NetImageView *avatarImageView;
|
||||
@property (nonatomic, strong, readonly) NetImageView *secondAvatarImageView; // CP双头像
|
||||
@property (nonatomic, strong, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, strong, readonly) UILabel *subtitleLabel;
|
||||
@property (nonatomic, strong, readonly) UILabel *contentLabel;
|
||||
@property (nonatomic, strong, readonly) NetImageView *iconImageView;
|
||||
@property (nonatomic, strong, readonly) UIButton *actionButton;
|
||||
@property (nonatomic, strong, readonly) SVGAImageView *svgaView;
|
||||
|
||||
#pragma mark - 动画配置
|
||||
@property (nonatomic, assign) YMBannerAnimationType animationType;
|
||||
@property (nonatomic, assign) CGFloat showDuration;
|
||||
@property (nonatomic, assign) CGFloat stayDuration;
|
||||
@property (nonatomic, assign) CGFloat hideDuration;
|
||||
@property (nonatomic, assign) BOOL enableSwipeGesture;
|
||||
|
||||
#pragma mark - 回调
|
||||
@property (nonatomic, copy) void(^onDisplayComplete)(void);
|
||||
@property (nonatomic, copy) void(^onUserTap)(YMBaseBannerView *banner);
|
||||
@property (nonatomic, copy) void(^onActionTap)(YMBaseBannerView *banner);
|
||||
@property (nonatomic, copy) void(^onDismiss)(YMBaseBannerView *banner);
|
||||
|
||||
#pragma mark - 状态
|
||||
@property (nonatomic, assign, readonly) BOOL isDisplaying;
|
||||
@property (nonatomic, assign, readonly) BOOL isDismissed;
|
||||
|
||||
#pragma mark - 类方法
|
||||
+ (instancetype)bannerWithAttachment:(AttachmentModel *)attachment;
|
||||
+ (void)displayInView:(UIView *)superView
|
||||
attachment:(AttachmentModel *)attachment
|
||||
complete:(void(^)(void))complete;
|
||||
|
||||
#pragma mark - 实例方法
|
||||
- (instancetype)initWithBannerType:(YMBannerType)type;
|
||||
- (void)configureWithAttachment:(AttachmentModel *)attachment;
|
||||
- (void)displayInView:(UIView *)superView;
|
||||
- (void)dismissWithAnimation:(BOOL)animated;
|
||||
|
||||
#pragma mark - 子类重写方法
|
||||
- (Class)viewModelClass; // 返回对应的ViewModel类
|
||||
- (void)setupUIComponents; // 设置UI组件
|
||||
- (void)layoutUIComponents; // 布局UI组件
|
||||
- (void)configureWithViewModel:(YMBaseBannerViewModel *)viewModel; // 配置数据
|
||||
- (void)performShowAnimation; // 执行显示动画
|
||||
- (void)performHideAnimation; // 执行隐藏动画
|
||||
- (void)handleUserTap; // 处理用户点击
|
||||
- (void)handleActionTap; // 处理操作按钮点击
|
||||
|
||||
#pragma mark - 用户反馈
|
||||
- (void)reportDisplayEvent; // 上报展示事件
|
||||
- (void)reportClickEvent:(NSString *)action; // 上报点击事件
|
||||
- (void)reportDismissEvent:(NSString *)reason; // 上报消失事件
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
```
|
||||
|
||||
### 2. 基础数据模型
|
||||
|
||||
```objc
|
||||
// YMBaseBannerViewModel.h
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "PIBaseModel.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface YMBaseBannerViewModel : PIBaseModel
|
||||
|
||||
#pragma mark - 基础信息
|
||||
@property (nonatomic, assign) NSInteger roomUid;
|
||||
@property (nonatomic, assign) NSInteger targetRoomUid;
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *subtitle;
|
||||
@property (nonatomic, copy) NSString *content;
|
||||
|
||||
#pragma mark - 用户信息
|
||||
@property (nonatomic, copy) NSString *senderNick;
|
||||
@property (nonatomic, copy) NSString *senderAvatar;
|
||||
@property (nonatomic, assign) NSInteger senderUid;
|
||||
@property (nonatomic, copy) NSString *receiverNick;
|
||||
@property (nonatomic, copy) NSString *receiverAvatar;
|
||||
@property (nonatomic, assign) NSInteger receiverUid;
|
||||
|
||||
#pragma mark - 视觉资源
|
||||
@property (nonatomic, copy) NSString *backgroundImageUrl;
|
||||
@property (nonatomic, copy) NSString *iconImageUrl;
|
||||
@property (nonatomic, copy) NSString *svgaUrl;
|
||||
|
||||
#pragma mark - 交互配置
|
||||
@property (nonatomic, copy) NSString *actionText;
|
||||
@property (nonatomic, copy) NSString *skipUrl;
|
||||
@property (nonatomic, assign) BOOL enableClick;
|
||||
@property (nonatomic, assign) BOOL enableAction;
|
||||
|
||||
#pragma mark - 埋点数据
|
||||
@property (nonatomic, copy) NSString *eventType;
|
||||
@property (nonatomic, strong) NSDictionary *trackingData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
```
|
||||
|
||||
### 3. 代理协议设计
|
||||
|
||||
```objc
|
||||
// YMBannerDelegate.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class YMBaseBannerView;
|
||||
|
||||
@protocol YMBannerDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
#pragma mark - 生命周期回调
|
||||
- (void)bannerWillDisplay:(YMBaseBannerView *)banner;
|
||||
- (void)bannerDidDisplay:(YMBaseBannerView *)banner;
|
||||
- (void)bannerWillDismiss:(YMBaseBannerView *)banner;
|
||||
- (void)bannerDidDismiss:(YMBaseBannerView *)banner;
|
||||
|
||||
#pragma mark - 交互回调
|
||||
- (void)banner:(YMBaseBannerView *)banner didTapWithAction:(NSString *)action;
|
||||
- (void)banner:(YMBaseBannerView *)banner willNavigateToRoom:(NSInteger)roomUid;
|
||||
- (void)banner:(YMBaseBannerView *)banner willOpenURL:(NSString *)url;
|
||||
|
||||
#pragma mark - 数据回调
|
||||
- (void)banner:(YMBaseBannerView *)banner didReportEvent:(NSString *)event data:(NSDictionary *)data;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
```
|
||||
|
||||
## 子类设计
|
||||
|
||||
### 1. 高价值礼物Banner
|
||||
|
||||
```objc
|
||||
// YMHighValueGiftBannerView.h
|
||||
#import "YMBaseBannerView.h"
|
||||
|
||||
@interface YMHighValueGiftBannerView : YMBaseBannerView
|
||||
|
||||
@property (nonatomic, strong, readonly) UILabel *giftNameLabel;
|
||||
@property (nonatomic, strong, readonly) UILabel *giftCountLabel;
|
||||
@property (nonatomic, strong, readonly) MarqueeLabel *senderScrollLabel;
|
||||
@property (nonatomic, strong, readonly) MarqueeLabel *roomNameScrollLabel;
|
||||
|
||||
@end
|
||||
|
||||
// YMHighValueGiftBannerViewModel.h
|
||||
@interface YMHighValueGiftBannerViewModel : YMBaseBannerViewModel
|
||||
|
||||
@property (nonatomic, copy) NSString *giftName;
|
||||
@property (nonatomic, assign) NSInteger giftCount;
|
||||
@property (nonatomic, copy) NSString *giftImageUrl;
|
||||
@property (nonatomic, assign) NSInteger bgLevel;
|
||||
@property (nonatomic, copy) NSString *roomTitle;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 2. CP礼物Banner
|
||||
|
||||
```objc
|
||||
// YMCPGiftBannerView.h
|
||||
#import "YMBaseBannerView.h"
|
||||
|
||||
@interface YMCPGiftBannerView : YMBaseBannerView
|
||||
|
||||
@property (nonatomic, strong, readonly) UIStackView *cpStackView;
|
||||
@property (nonatomic, strong, readonly) UILabel *relationLabel;
|
||||
|
||||
@end
|
||||
|
||||
// YMCPGiftBannerViewModel.h
|
||||
@interface YMCPGiftBannerViewModel : YMBaseBannerViewModel
|
||||
|
||||
@property (nonatomic, copy) NSString *giftImageUrl;
|
||||
@property (nonatomic, copy) NSString *relationText;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 3. 通用游戏Banner
|
||||
|
||||
```objc
|
||||
// YMGameUniversalBannerView.h
|
||||
#import "YMBaseBannerView.h"
|
||||
|
||||
@interface YMGameUniversalBannerView : YMBaseBannerView
|
||||
|
||||
@property (nonatomic, strong, readonly) NetImageView *gameIconView;
|
||||
@property (nonatomic, assign) NSInteger gameID;
|
||||
|
||||
@end
|
||||
|
||||
// YMGameUniversalBannerViewModel.h
|
||||
@interface YMGameUniversalBannerViewModel : YMBaseBannerViewModel
|
||||
|
||||
@property (nonatomic, copy) NSString *gameIconUrl;
|
||||
@property (nonatomic, assign) NSInteger gameID;
|
||||
@property (nonatomic, copy) NSString *gameTitle;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
## 用户反馈机制设计
|
||||
|
||||
### 1. 反馈管理器
|
||||
|
||||
```objc
|
||||
// YMBannerFeedbackManager.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, YMBannerFeedbackType) {
|
||||
YMBannerFeedbackTypeDisplay, // 展示
|
||||
YMBannerFeedbackTypeClick, // 点击
|
||||
YMBannerFeedbackTypeAction, // 操作
|
||||
YMBannerFeedbackTypeDismiss, // 消失
|
||||
YMBannerFeedbackTypeError // 错误
|
||||
};
|
||||
|
||||
@interface YMBannerFeedbackManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
#pragma mark - 事件上报
|
||||
- (void)reportBannerEvent:(YMBannerFeedbackType)type
|
||||
banner:(YMBaseBannerView *)banner
|
||||
data:(NSDictionary *)data;
|
||||
|
||||
#pragma mark - 性能监控
|
||||
- (void)startPerformanceMonitoring:(YMBaseBannerView *)banner;
|
||||
- (void)endPerformanceMonitoring:(YMBaseBannerView *)banner;
|
||||
|
||||
#pragma mark - 错误收集
|
||||
- (void)reportError:(NSError *)error
|
||||
banner:(YMBaseBannerView *)banner
|
||||
context:(NSDictionary *)context;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
```
|
||||
|
||||
### 2. 埋点事件定义
|
||||
|
||||
```objc
|
||||
// YMBannerTrackingEvents.h
|
||||
extern NSString * const kYMBannerEventDisplay; // banner_display
|
||||
extern NSString * const kYMBannerEventClick; // banner_click
|
||||
extern NSString * const kYMBannerEventAction; // banner_action
|
||||
extern NSString * const kYMBannerEventDismiss; // banner_dismiss
|
||||
extern NSString * const kYMBannerEventLoadStart; // banner_load_start
|
||||
extern NSString * const kYMBannerEventLoadEnd; // banner_load_end
|
||||
extern NSString * const kYMBannerEventError; // banner_error
|
||||
|
||||
// 埋点参数Key
|
||||
extern NSString * const kYMBannerTrackingKeyType; // banner_type
|
||||
extern NSString * const kYMBannerTrackingKeyRoomUid; // room_uid
|
||||
extern NSString * const kYMBannerTrackingKeyDuration; // duration
|
||||
extern NSString * const kYMBannerTrackingKeyAction; // action
|
||||
extern NSString * const kYMBannerTrackingKeyReason; // reason
|
||||
```
|
||||
|
||||
## 上下文调整说明
|
||||
|
||||
### 1. RoomAnimationView.m 调整
|
||||
|
||||
#### 1.1 替换现有Banner创建逻辑
|
||||
|
||||
```objc
|
||||
// 原代码
|
||||
- (void)playRoomGiftBanner:(AttachmentModel *)obj {
|
||||
[RoomHighValueGiftBannerAnimation display:self.bannerContainer
|
||||
with:obj
|
||||
complete:^{
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self processNextRoomEffectAttachment];
|
||||
}];
|
||||
}
|
||||
|
||||
// 新代码
|
||||
- (void)playRoomGiftBanner:(AttachmentModel *)obj {
|
||||
YMHighValueGiftBannerView *banner = [YMHighValueGiftBannerView bannerWithAttachment:obj];
|
||||
banner.delegate = self;
|
||||
banner.onDisplayComplete = ^{
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self processNextRoomEffectAttachment];
|
||||
};
|
||||
[banner displayInView:self.bannerContainer];
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 添加代理实现
|
||||
|
||||
```objc
|
||||
#pragma mark - YMBannerDelegate
|
||||
- (void)banner:(YMBaseBannerView *)banner didReportEvent:(NSString *)event data:(NSDictionary *)data {
|
||||
[[YMBannerFeedbackManager shared] reportBannerEvent:YMBannerFeedbackTypeDisplay
|
||||
banner:banner
|
||||
data:data];
|
||||
}
|
||||
|
||||
- (void)banner:(YMBaseBannerView *)banner willNavigateToRoom:(NSInteger)roomUid {
|
||||
// 处理房间跳转逻辑
|
||||
RoomInfoModel *currentRoom = self.hostDelegate.getRoomInfo;
|
||||
if (currentRoom.uid != roomUid) {
|
||||
// 执行跨房间跳转
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 项目依赖调整
|
||||
|
||||
#### 2.1 新增文件结构
|
||||
|
||||
```YuMi/Modules/YMRoom/View/Banner/
|
||||
├── Base/
|
||||
│ ├── YMBaseBannerView.h/.m
|
||||
│ ├── YMBaseBannerViewModel.h/.m
|
||||
│ └── YMBannerDelegate.h
|
||||
├── Subclasses/
|
||||
│ ├── YMHighValueGiftBannerView.h/.m
|
||||
│ ├── YMCPGiftBannerView.h/.m
|
||||
│ ├── YMBravoGiftBannerView.h/.m
|
||||
│ ├── YMGameUniversalBannerView.h/.m
|
||||
│ └── YMLuckyPackageBannerView.h/.m
|
||||
├── Manager/
|
||||
│ └── YMBannerFeedbackManager.h/.m
|
||||
└── Constants/
|
||||
└── YMBannerTrackingEvents.h/.m
|
||||
```
|
||||
|
||||
#### 2.2 Podfile依赖更新
|
||||
|
||||
```ruby
|
||||
# 确保动画库版本兼容
|
||||
pod 'pop', '~> 1.0'
|
||||
pod 'SVGAPlayer', '~> 2.3'
|
||||
|
||||
# 新增性能监控
|
||||
pod 'YMPerformanceMonitor' # 如果有自定义性能监控库
|
||||
```
|
||||
|
||||
### 3. 配置文件调整
|
||||
|
||||
#### 3.1 添加Banner配置
|
||||
|
||||
```objc
|
||||
// YMBannerConfig.h
|
||||
@interface YMBannerConfig : NSObject
|
||||
|
||||
@property (nonatomic, assign) BOOL enablePerformanceMonitoring;
|
||||
@property (nonatomic, assign) BOOL enableErrorReporting;
|
||||
@property (nonatomic, assign) CGFloat defaultShowDuration;
|
||||
@property (nonatomic, assign) CGFloat defaultStayDuration;
|
||||
@property (nonatomic, assign) CGFloat defaultHideDuration;
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 4. 迁移策略
|
||||
|
||||
#### 4.1 Phase 1: 基础架构
|
||||
|
||||
- 创建基类和协议
|
||||
- 实现反馈管理器
|
||||
- 设置基础配置
|
||||
|
||||
#### 4.2 Phase 2: 渐进迁移
|
||||
|
||||
- 优先迁移使用频率最高的Banner
|
||||
- 保持向后兼容性
|
||||
- 添加单元测试
|
||||
|
||||
#### 4.3 Phase 3: 优化完善
|
||||
|
||||
- 移除旧代码
|
||||
- 性能优化
|
||||
- 文档完善
|
||||
|
||||
## 总结
|
||||
|
||||
这套新的Banner组件架构具备以下优势:
|
||||
|
||||
1. **统一性**: 提供一致的API和行为模式
|
||||
2. **扩展性**: 易于添加新的Banner类型
|
||||
3. **可维护性**: 集中管理动画、埋点和反馈
|
||||
4. **性能优化**: 统一的资源管理和内存优化
|
||||
5. **用户体验**: 标准化的交互和反馈机制
|
||||
|
||||
通过这套架构,可以显著提升Banner组件的开发效率和用户体验质量。
|
@@ -1,80 +0,0 @@
|
||||
# 飘屏组件分析文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细分析了项目中在 `RoomAnimationView.m` 的 `bannerContainer` 中展示的所有飘屏类型及其触发逻辑。
|
||||
|
||||
## 飘屏展示机制
|
||||
|
||||
- **展示容器**: `bannerContainer` (XPRoomAnimationHitView)
|
||||
- **位置**: 视图层级最顶层,距离顶部导航栏高度
|
||||
- **尺寸**: 屏幕宽度 × 180px 高度
|
||||
- **管理机制**: 统一队列管理,按优先级顺序播放
|
||||
|
||||
## 飘屏类型详细表格
|
||||
|
||||
| 序号 | 飘屏名称 | 类名 | 触发消息类型 | 处理方法 | 业务场景 | 功能描述 |
|
||||
|-----|---------|------|-------------|---------|----------|---------|
|
||||
| 1 | 高价值礼物飘屏 | RoomHighValueGiftBannerAnimation | Custom_Message_Sub_Gift_ChannelNotify | playRoomGiftBanner: | 礼物系统 | 展示高价值礼物的全服广播 |
|
||||
| 2 | CP礼物飘屏 | CPGiftBanner | Custom_Message_Sub_CP_Gift | playCPGiftBanner: | CP系统 | 展示CP相关礼物特效 |
|
||||
| 3 | Bravo超级礼物飘屏 | BravoGiftBannerView | Custom_Message_Sub_Super_Gift_Banner | playBroveBanner: | 超级礼物 | 展示Bravo超级礼物效果 |
|
||||
| 4 | 幸运红包飘屏 | LuckyPackageBannerView | Custom_Message_Sub_LuckyPackage | playLuckyPackageBanner: | 红包系统 | 展示房间红包,支持跨房间跳转 |
|
||||
| 5 | 幸运礼物中奖飘屏 | LuckyGiftWinningBannerView | Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room | playLuckyWinningBanner: | 中奖系统 | 展示全服中奖信息 |
|
||||
| 6 | 通用游戏飘屏 | GameUniversalBannerView | Custom_Message_Sub_General_Floating_Screen_One_Room<br/>Custom_Message_Sub_General_Floating_Screen_All_Room | playGameBanner: | 游戏系统 | 展示游戏相关飘屏,支持跳转 |
|
||||
| 7 | 塔罗飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Tarot_Advanced<br/>Custom_Message_Sub_Tarot_Intermediate | createTarotBannerAnimation: | 塔罗活动 | 展示塔罗相关活动信息 |
|
||||
| 8 | 星厨房飘屏 | XPRoomStarKitchenBannerView | Custom_Message_Sub_Star_Kitchen_FullScreen | createStarKitchenBannerAnimation: | 厨房活动 | 展示星厨房活动 |
|
||||
| 9 | 夺宝精灵飘屏 | - | Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L4<br/>Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L5<br/>Custom_Message_Sub_Treasure_Fairy_Convert_L1/L2/L3 | createTreasureFairyBannerAnimation: | 夺宝活动 | 展示夺宝精灵高等级奖励 |
|
||||
| 10 | 主播小时榜飘屏 | XPRoomAnchorRankBannerView | Custom_Message_Sub_Anchor_Hour_Rank | 创建主播排行榜飘屏 | 排行榜 | 展示主播小时榜信息 |
|
||||
| 11 | 通用H5飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Common_H5_Novice<br/>Custom_Message_Sub_Common_H5_Advanced | createCommonH5BannerAnimation: | H5活动 | 展示通用H5活动飘屏 |
|
||||
| 12 | 通用飘屏 | PIUniversalBannerView | 多种消息类型 | createGeneralFloatingScreenAnimation: | 通用展示 | 提供通用飘屏展示能力 |
|
||||
|
||||
## 队列管理机制
|
||||
|
||||
### 核心属性
|
||||
|
||||
- **队列数组**: `roomBannertModelsQueueV2` (NSMutableArray)
|
||||
- **状态标记**: `isRoomBannerV2Displaying` (BOOL)
|
||||
|
||||
### 处理流程
|
||||
|
||||
1. **消息接收**: 调用 `inserBannerModelToQueue:` 将消息加入队列
|
||||
2. **优先级排序**: 调用 `sortBannerQueue` 按消息类型的second值排序(数值越小优先级越高)
|
||||
3. **顺序播放**: 调用 `processNextRoomEffectAttachment` 逐个播放
|
||||
4. **播放完成**: 每个飘屏播放完成后,设置 `isRoomBannerV2Displaying = NO` 并处理下一个
|
||||
|
||||
### 优先级规则
|
||||
|
||||
飘屏按 `AttachmentModel.second` 值进行排序,确保重要消息优先展示。
|
||||
|
||||
## 消息类型分类
|
||||
|
||||
### 礼物相关
|
||||
|
||||
- Custom_Message_Sub_Gift_ChannelNotify (32)
|
||||
- Custom_Message_Sub_CP_Gift
|
||||
- Custom_Message_Sub_Super_Gift_Banner (1066)
|
||||
|
||||
### 红包/福袋相关
|
||||
|
||||
- Custom_Message_Sub_LuckyPackage (607)
|
||||
- Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room (1063)
|
||||
|
||||
### 游戏相关
|
||||
|
||||
- Custom_Message_Sub_General_Floating_Screen_One_Room (1071)
|
||||
- Custom_Message_Sub_General_Floating_Screen_All_Room (1072)
|
||||
|
||||
### 活动相关
|
||||
|
||||
- Custom_Message_Sub_Tarot_Advanced (714)
|
||||
- Custom_Message_Sub_Tarot_Intermediate (713)
|
||||
- Custom_Message_Sub_Star_Kitchen_FullScreen (1042)
|
||||
- Custom_Message_Sub_Treasure_Fairy_* (9700系列)
|
||||
|
||||
### 排行榜相关
|
||||
|
||||
- Custom_Message_Sub_Anchor_Hour_Rank (891)
|
||||
|
||||
### 通用相关
|
||||
|
||||
- Custom_Message_Sub_Common_H5_* (1100系列)
|
203
DOC/飘屏组件抽象分析.md
203
DOC/飘屏组件抽象分析.md
@@ -1,203 +0,0 @@
|
||||
# 飘屏组件抽象分析
|
||||
|
||||
## 分析概述
|
||||
|
||||
基于对项目中12种飘屏组件的深入分析,发现这些组件存在高度相似的设计模式和实现结构,具备抽象出统一父类的条件。
|
||||
|
||||
## 现状分析
|
||||
|
||||
### 共同特征
|
||||
|
||||
经过代码分析,发现所有飘屏组件都具有以下共同特征:
|
||||
|
||||
#### 1. 统一的类方法签名模式
|
||||
|
||||
大部分Banner类都采用了相似的类方法签名:
|
||||
|
||||
``` objc
|
||||
+ (void)display:(UIView *)superView
|
||||
with:(AttachmentModel *)attachment
|
||||
complete:(void(^)(void))complete;
|
||||
|
||||
// 或者带房间信息的版本
|
||||
+ (void)display:(UIView *)superView
|
||||
inRoomUid:(NSInteger)roomUid
|
||||
with:(AttachmentModel *)attachment
|
||||
complete:(void(^)(void))complete
|
||||
exitCurrentRoom:(void(^)(void))exit;
|
||||
```
|
||||
|
||||
#### 2. 共同的内部属性结构
|
||||
|
||||
```objc
|
||||
@property (nonatomic, strong) SomeViewModel *model; // 数据模型
|
||||
@property (nonatomic, strong) UIImageView *backgroundImageView; // 背景图片
|
||||
@property (nonatomic, copy) void(^completeDisplay)(void); // 完成回调
|
||||
@property (nonatomic, copy) void(^exitCurrentRoom)(void); // 退房回调
|
||||
@property (nonatomic, assign) NSInteger currentRoomUid; // 当前房间ID
|
||||
```
|
||||
|
||||
#### 3. 相似的动画流程
|
||||
|
||||
- 从屏幕右侧滑入 (x = KScreenWidth)
|
||||
- 短暂停留展示
|
||||
- 滑出屏幕或淡出
|
||||
- 执行完成回调
|
||||
|
||||
#### 4. 统一的数据处理模式
|
||||
|
||||
- 都依赖 `AttachmentModel` 作为数据源
|
||||
- 都创建对应的ViewModel进行数据解析
|
||||
- 都继承自 `PIBaseModel`
|
||||
|
||||
#### 5. 相似的UI组件
|
||||
|
||||
- 背景图片视图
|
||||
- 用户头像
|
||||
- 文本标签
|
||||
- 可选的SVGA动画视图
|
||||
- 可选的交互按钮
|
||||
|
||||
## 抽象方案设计
|
||||
|
||||
### 建议的基类结构: `BaseRoomBannerView`
|
||||
|
||||
```objc
|
||||
@interface BaseRoomBannerView : UIView
|
||||
|
||||
#pragma mark - 核心属性
|
||||
@property (nonatomic, strong) AttachmentModel *attachment;
|
||||
@property (nonatomic, strong) PIBaseModel *viewModel;
|
||||
@property (nonatomic, assign) NSInteger currentRoomUid;
|
||||
@property (nonatomic, assign) NSInteger targetRoomUid;
|
||||
|
||||
#pragma mark - UI组件
|
||||
@property (nonatomic, strong) UIView *containerView;
|
||||
@property (nonatomic, strong) UIImageView *backgroundImageView;
|
||||
@property (nonatomic, strong) NetImageView *avatarImageView;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *subTitleLabel;
|
||||
@property (nonatomic, strong) UIButton *actionButton;
|
||||
@property (nonatomic, strong) SVGAImageView *svgaView;
|
||||
|
||||
#pragma mark - 动画配置
|
||||
@property (nonatomic, assign) CGFloat showDuration;
|
||||
@property (nonatomic, assign) CGFloat stayDuration;
|
||||
@property (nonatomic, assign) CGFloat hideDuration;
|
||||
@property (nonatomic, assign) BOOL useSlideAnimation;
|
||||
@property (nonatomic, assign) BOOL useFadeAnimation;
|
||||
|
||||
#pragma mark - 回调
|
||||
@property (nonatomic, copy) void(^completeDisplay)(void);
|
||||
@property (nonatomic, copy) void(^exitCurrentRoom)(void);
|
||||
@property (nonatomic, copy) void(^didTapBanner)(NSInteger roomID);
|
||||
|
||||
#pragma mark - 类方法
|
||||
+ (instancetype)createWithAttachment:(AttachmentModel *)attachment
|
||||
inRoomUid:(NSInteger)roomUid
|
||||
complete:(void(^)(void))complete
|
||||
exitCurrentRoom:(void(^)(void))exit;
|
||||
|
||||
+ (void)display:(UIView *)superView
|
||||
with:(AttachmentModel *)attachment
|
||||
complete:(void(^)(void))complete;
|
||||
|
||||
+ (void)display:(UIView *)superView
|
||||
inRoomUid:(NSInteger)roomUid
|
||||
with:(AttachmentModel *)attachment
|
||||
complete:(void(^)(void))complete
|
||||
exitCurrentRoom:(void(^)(void))exit;
|
||||
|
||||
#pragma mark - 实例方法
|
||||
- (void)setupWithAttachment:(AttachmentModel *)attachment;
|
||||
- (void)showInSuperView:(UIView *)superView;
|
||||
- (void)performShowAnimation;
|
||||
- (void)performHideAnimation;
|
||||
|
||||
#pragma mark - 子类重写方法
|
||||
- (Class)viewModelClass; // 返回对应的ViewModel类
|
||||
- (void)setupUI; // 设置UI布局
|
||||
- (void)configureWithModel:(PIBaseModel *)model; // 配置数据
|
||||
- (void)customizeAnimation; // 自定义动画
|
||||
- (void)handleTapAction; // 处理点击事件
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 抽象优势分析
|
||||
|
||||
#### 1. 代码复用率提升
|
||||
|
||||
- 消除重复的动画逻辑
|
||||
- 统一的数据处理流程
|
||||
- 共享的UI组件管理
|
||||
|
||||
#### 2. 维护成本降低
|
||||
|
||||
- 集中管理动画参数
|
||||
- 统一的回调处理机制
|
||||
- 标准化的生命周期管理
|
||||
|
||||
#### 3. 扩展性增强
|
||||
|
||||
- 新增飘屏类型只需继承基类
|
||||
- 便于添加通用功能(如埋点、性能监控)
|
||||
- 支持主题切换等全局功能
|
||||
|
||||
#### 4. 一致性保证
|
||||
|
||||
- 统一的动画效果和时长
|
||||
- 标准化的交互行为
|
||||
- 一致的视觉表现
|
||||
|
||||
### 实现策略
|
||||
|
||||
#### 阶段一:创建基类
|
||||
|
||||
1. 提取共同属性和方法
|
||||
2. 实现通用动画逻辑
|
||||
3. 定义子类接口规范
|
||||
|
||||
#### 阶段二:重构现有类
|
||||
|
||||
1. 逐步迁移现有Banner类继承基类
|
||||
2. 移除重复代码
|
||||
3. 保持向后兼容
|
||||
|
||||
#### 阶段三:优化和扩展
|
||||
|
||||
1. 添加性能监控
|
||||
2. 实现主题支持
|
||||
3. 统一埋点逻辑
|
||||
|
||||
### 潜在挑战
|
||||
|
||||
#### 1. 兼容性问题
|
||||
|
||||
- 现有代码的重构风险
|
||||
- 不同Banner的特殊需求差异
|
||||
- 第三方依赖的适配
|
||||
|
||||
#### 2. 性能考虑
|
||||
|
||||
- 基类功能过于复杂可能影响性能
|
||||
- 内存占用的优化
|
||||
- 动画性能的平衡
|
||||
|
||||
#### 3. 维护复杂度
|
||||
|
||||
- 基类变更可能影响所有子类
|
||||
- 需要详细的文档和测试
|
||||
- 团队学习成本
|
||||
|
||||
## 结论
|
||||
|
||||
**建议进行抽象**:项目中的飘屏组件具有高度的相似性,抽象出基类能够显著提升代码质量和开发效率。建议采用渐进式重构策略,先创建基类和接口规范,再逐步迁移现有实现,确保系统稳定性的同时实现架构优化。
|
||||
|
||||
## 优先级建议
|
||||
|
||||
1. **高优先级**:创建 `BaseRoomBannerView` 基类
|
||||
2. **中优先级**:重构使用频率最高的Banner类
|
||||
3. **低优先级**:添加扩展功能和优化
|
||||
|
||||
通过这种抽象设计,能够在保持现有功能完整性的同时,为未来的功能扩展和维护奠定良好的架构基础。
|
@@ -531,6 +531,8 @@
|
||||
4C7F2A672E0BE0AB002F5058 /* FirstRechargeModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C7F2A662E0BE0AB002F5058 /* FirstRechargeModel.m */; };
|
||||
4C7F2A6B2E0BE7E7002F5058 /* FirstRechargeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C7F2A6A2E0BE7E7002F5058 /* FirstRechargeManager.m */; };
|
||||
4C815A172CFEB758002A46A6 /* SuperBlockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C815A162CFEB758002A46A6 /* SuperBlockViewController.m */; };
|
||||
4C84A9C22E5ED593002C10FC /* GameBannerGestureManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C84A9C12E5ED593002C10FC /* GameBannerGestureManager.m */; };
|
||||
4C84A9CB2E602B1A002C10FC /* BuglyManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C84A9CA2E602B1A002C10FC /* BuglyManager.m */; };
|
||||
4C85DB812DCDD83E00FD9839 /* CreateEventPresenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C85DB802DCDD83E00FD9839 /* CreateEventPresenter.m */; };
|
||||
4C85DB842DCDDD6800FD9839 /* CreateEventViewControllerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C85DB832DCDDD6800FD9839 /* CreateEventViewControllerV2.m */; };
|
||||
4C864A022D55F4F600191AE0 /* LuckyPackagePresenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C864A012D55F4F600191AE0 /* LuckyPackagePresenter.m */; };
|
||||
@@ -572,6 +574,7 @@
|
||||
4CD15D912D7E902800D9279F /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD15D902D7E902800D9279F /* LoginViewController.m */; };
|
||||
4CD15D922D7EC2AC00D9279F /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23E56B3B2B03564B00C8DAC9 /* CoreTelephony.framework */; };
|
||||
4CD15D952D7FE9E400D9279F /* LoginTypesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD15D942D7FE9E400D9279F /* LoginTypesViewController.m */; };
|
||||
4CD47BB52E61514900BCDA46 /* StageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD47BB42E61514900BCDA46 /* StageViewManager.m */; };
|
||||
4CD6FF662D673A5C00262AB7 /* AgentMessageModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD6FF652D673A5C00262AB7 /* AgentMessageModel.m */; };
|
||||
4CD6FF692D673F7F00262AB7 /* AgentMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD6FF682D673F7F00262AB7 /* AgentMessageTableViewCell.m */; };
|
||||
4CE3A9462D22754C003F0796 /* RechargeUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE3A9452D22754C003F0796 /* RechargeUserModel.m */; };
|
||||
@@ -2725,6 +2728,10 @@
|
||||
4C7F2A6A2E0BE7E7002F5058 /* FirstRechargeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FirstRechargeManager.m; sourceTree = "<group>"; };
|
||||
4C815A152CFEB758002A46A6 /* SuperBlockViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SuperBlockViewController.h; sourceTree = "<group>"; };
|
||||
4C815A162CFEB758002A46A6 /* SuperBlockViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SuperBlockViewController.m; sourceTree = "<group>"; };
|
||||
4C84A9C02E5ED593002C10FC /* GameBannerGestureManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GameBannerGestureManager.h; sourceTree = "<group>"; };
|
||||
4C84A9C12E5ED593002C10FC /* GameBannerGestureManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GameBannerGestureManager.m; sourceTree = "<group>"; };
|
||||
4C84A9C92E602B1A002C10FC /* BuglyManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BuglyManager.h; sourceTree = "<group>"; };
|
||||
4C84A9CA2E602B1A002C10FC /* BuglyManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BuglyManager.m; sourceTree = "<group>"; };
|
||||
4C85DB7F2DCDD83E00FD9839 /* CreateEventPresenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CreateEventPresenter.h; sourceTree = "<group>"; };
|
||||
4C85DB802DCDD83E00FD9839 /* CreateEventPresenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CreateEventPresenter.m; sourceTree = "<group>"; };
|
||||
4C85DB822DCDDD6800FD9839 /* CreateEventViewControllerV2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CreateEventViewControllerV2.h; sourceTree = "<group>"; };
|
||||
@@ -2803,6 +2810,8 @@
|
||||
4CD15D902D7E902800D9279F /* LoginViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = "<group>"; };
|
||||
4CD15D932D7FE9E400D9279F /* LoginTypesViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginTypesViewController.h; sourceTree = "<group>"; };
|
||||
4CD15D942D7FE9E400D9279F /* LoginTypesViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginTypesViewController.m; sourceTree = "<group>"; };
|
||||
4CD47BB32E61514900BCDA46 /* StageViewManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StageViewManager.h; sourceTree = "<group>"; };
|
||||
4CD47BB42E61514900BCDA46 /* StageViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StageViewManager.m; sourceTree = "<group>"; };
|
||||
4CD6FF642D673A5C00262AB7 /* AgentMessageModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AgentMessageModel.h; sourceTree = "<group>"; };
|
||||
4CD6FF652D673A5C00262AB7 /* AgentMessageModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AgentMessageModel.m; sourceTree = "<group>"; };
|
||||
4CD6FF672D673F7F00262AB7 /* AgentMessageTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AgentMessageTableViewCell.h; sourceTree = "<group>"; };
|
||||
@@ -6931,6 +6940,8 @@
|
||||
children = (
|
||||
4CFE7F3F2E45ECEC00F77776 /* PublicRoomManager.h */,
|
||||
4CFE7F402E45ECEC00F77776 /* PublicRoomManager.m */,
|
||||
4C84A9C02E5ED593002C10FC /* GameBannerGestureManager.h */,
|
||||
4C84A9C12E5ED593002C10FC /* GameBannerGestureManager.m */,
|
||||
);
|
||||
path = Manager;
|
||||
sourceTree = "<group>";
|
||||
@@ -8277,6 +8288,8 @@
|
||||
E81C279926EB64BA0031E639 /* Global */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C84A9C92E602B1A002C10FC /* BuglyManager.h */,
|
||||
4C84A9CA2E602B1A002C10FC /* BuglyManager.m */,
|
||||
E81C279A26EB65560031E639 /* YUMIMacroUitls.h */,
|
||||
E81C279B26EEEC620031E639 /* YUMIConstant.h */,
|
||||
E81C279C26EEEC620031E639 /* YUMIConstant.m */,
|
||||
@@ -10783,6 +10796,8 @@
|
||||
E8AEAED8271413530017FCE0 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CD47BB32E61514900BCDA46 /* StageViewManager.h */,
|
||||
4CD47BB42E61514900BCDA46 /* StageViewManager.m */,
|
||||
4CE746C92D929D500094E496 /* Common */,
|
||||
4CB753CF2D30F08E00B13DF5 /* LuckyPackage */,
|
||||
4CB753CE2D2FE80100B13DF5 /* RoomSideMenu */,
|
||||
@@ -12094,6 +12109,7 @@
|
||||
E821077B2987D4AB00DE7040 /* MessageFindNewGreetModel.m in Sources */,
|
||||
4C886BF22E015D61006F0BA7 /* MedalsModel.m in Sources */,
|
||||
E85E7BA32A4EC99300B6D00A /* XPMineGiveDiamondDetailsVC.m in Sources */,
|
||||
4CD47BB52E61514900BCDA46 /* StageViewManager.m in Sources */,
|
||||
238A90072BA9729200828123 /* PIUniversalBannerView.m in Sources */,
|
||||
E8B846D826FDE17300A777FE /* XPMineRechargeProtocol.h in Sources */,
|
||||
E88C72A6282921D60047FB2B /* XPRoomBackMusicPlayerView.m in Sources */,
|
||||
@@ -12132,6 +12148,7 @@
|
||||
E87C54BE2823CC5B0051AA11 /* XPMineResetLoginPwdPresenter.m in Sources */,
|
||||
237852A42C082A9800E360AC /* MSRoomGameSendTextView.m in Sources */,
|
||||
E85E7B322A4EB0D300B6D00A /* XPGuildAnchorIncomeSectionView.m in Sources */,
|
||||
4C84A9CB2E602B1A002C10FC /* BuglyManager.m in Sources */,
|
||||
E87C0AA027D9DE6400CB2241 /* RoomFaceSendInfoModel.m in Sources */,
|
||||
1464C5F629A4CA8C00AF7C94 /* XPIAPRechargeCollectionViewCell.m in Sources */,
|
||||
E8751E6328A646400056EF44 /* XPSailingRankView.m in Sources */,
|
||||
@@ -12153,6 +12170,7 @@
|
||||
E85E7B242A4EB0D300B6D00A /* XPMineGuildSuperAdminSetViewController.m in Sources */,
|
||||
E87DF4C22A42C900009C1185 /* XPNoteView.m in Sources */,
|
||||
E8AB631328ADDCF20023B0D2 /* XPMonentsTopicHeaderView.m in Sources */,
|
||||
4C84A9C22E5ED593002C10FC /* GameBannerGestureManager.m in Sources */,
|
||||
238B37B52AC55A2C00BFC9D5 /* XPTreasureFailyResultGiftCell.m in Sources */,
|
||||
23E9E9982A80C3A100B792F2 /* XPMineGuildPersonalBillStatisVC.m in Sources */,
|
||||
9B86D886281942D200494FCD /* SocialMicroView.m in Sources */,
|
||||
@@ -13692,7 +13710,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 20.20.62;
|
||||
MARKETING_VERSION = 20.20.63;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -13939,7 +13957,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 20.20.62;
|
||||
MARKETING_VERSION = 20.20.63;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@@ -27,6 +27,7 @@
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
#import <Bugly/Bugly.h>
|
||||
#import "BuglyManager.h"
|
||||
#import <UIKit/UIDevice.h>
|
||||
|
||||
#import "YuMi-swift.h"
|
||||
@@ -78,23 +79,11 @@ UIKIT_EXTERN NSString * adImageName;
|
||||
*/
|
||||
|
||||
- (void) configBugly {
|
||||
|
||||
BuglyConfig *config = [[BuglyConfig alloc] init];
|
||||
config.blockMonitorTimeout = 5;
|
||||
|
||||
// 使用 BuglyManager 统一管理 Bugly 配置
|
||||
#ifdef DEBUG
|
||||
config.debugMode = NO;//YES; // debug 模式下,开启调试模式
|
||||
config.channel = [YYUtility getAppSource];
|
||||
config.reportLogLevel = BuglyLogLevelWarn;// BuglyLogLevelSilent; // BuglyLogLevelVerbose; // 设置打印日志级别
|
||||
[Bugly startWithAppId:@"c937fd00f7" config:config];
|
||||
[[BuglyManager sharedManager] configureWithAppId:@"c937fd00f7" debug:YES];
|
||||
#else
|
||||
config.unexpectedTerminatingDetectionEnable = YES; // 非正常退出事件记录开关,默认关闭
|
||||
config.debugMode = NO; // release 模式下,关闭调试模式
|
||||
config.channel = [YYUtility getAppSource];;
|
||||
config.blockMonitorEnable = YES; // 卡顿监控开关,默认关闭
|
||||
config.reportLogLevel = BuglyLogLevelWarn; // 设置自定义日志上报的级别,默认不上报自定义日志
|
||||
NSString *buylyKey = @"8627948559";
|
||||
[Bugly startWithAppId:buylyKey config:config];
|
||||
[[BuglyManager sharedManager] configureWithAppId:@"8627948559" debug:NO];
|
||||
#endif
|
||||
}
|
||||
- (void)configNIMSDK {
|
||||
@@ -161,23 +150,37 @@ UIKIT_EXTERN NSString * adImageName;
|
||||
NSMutableArray * array = [NSMutableArray array];
|
||||
for (int i = 0; i < emojiArray.count; i++) {
|
||||
|
||||
UIImage * image = [UIImage imageNamed:dic[@"file"]];
|
||||
NSDictionary * emotionDic = [emojiArray xpSafeObjectAtIndex:i];
|
||||
if (!emotionDic) continue;
|
||||
|
||||
UIImage * image = [UIImage imageNamed:emotionDic[@"file"]];
|
||||
QEmotion * info = [[QEmotion alloc] init];
|
||||
|
||||
NSDictionary * dic = [emojiArray xpSafeObjectAtIndex:i];
|
||||
if (dic) {
|
||||
info.displayName = dic[@"tag"];
|
||||
info.identifier = dic[@"id"];
|
||||
}
|
||||
|
||||
info.displayName = emotionDic[@"tag"];
|
||||
info.identifier = emotionDic[@"id"];
|
||||
info.image = image;
|
||||
|
||||
// 添加调试日志
|
||||
NSLog(@"加载表情: %@, 图片: %@, 是否成功: %@", info.displayName, emotionDic[@"file"], image ? @"是" : @"否");
|
||||
|
||||
[array addObject:info];
|
||||
}
|
||||
//在这里强烈建议先预加载一下表情
|
||||
QEmotionHelper *faceManager = [QEmotionHelper sharedEmotionHelper];
|
||||
faceManager.emotionArray = array;
|
||||
|
||||
// 清理 emoji 缓存,确保新的尺寸设置生效
|
||||
[QEmotionHelper clearEmojiCache];
|
||||
|
||||
#if DEBUG
|
||||
// 测试图片加载
|
||||
NSLog(@"表情数组加载完成,总数: %lu", (unsigned long)array.count);
|
||||
for (int i = 0; i < MIN(array.count, 3); i++) {
|
||||
QEmotion *emotion = array[i];
|
||||
NSLog(@"测试表情 %d: %@, 图片: %@", i, emotion.displayName, emotion.image ? @"加载成功" : @"加载失败");
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark - 广告
|
||||
|
@@ -128,3 +128,4 @@
|
||||
<Emoticon ID="emoticon_emoji_115" Tag="[自行车]" File="emoji_115.png" />
|
||||
</Catalog>
|
||||
</PopoEmoticons>
|
||||
˜
|
||||
|
@@ -111,6 +111,10 @@ const int UISendButtonHeight = 41;
|
||||
if (isSizeChanged) {
|
||||
[self setNeedsLayoutEmotions];
|
||||
}
|
||||
|
||||
// 添加调试日志
|
||||
NSLog(@"UIEmotionPageView layoutSubviews: frame=%@, needsLayoutEmotions=%d", NSStringFromCGRect(self.frame), self.needsLayoutEmotions);
|
||||
|
||||
[self layoutEmotionsIfNeeded];
|
||||
}
|
||||
|
||||
@@ -120,6 +124,14 @@ const int UISendButtonHeight = 41;
|
||||
|
||||
- (void)setEmotions:(NSArray<QEmotion *> *)emotions {
|
||||
if ([_emotions isEqualToArray:emotions]) return;
|
||||
|
||||
// 添加调试日志
|
||||
NSLog(@"UIEmotionPageView setEmotions: 表情数量=%lu", (unsigned long)emotions.count);
|
||||
if (emotions.count > 0) {
|
||||
QEmotion *firstEmotion = emotions[0];
|
||||
NSLog(@"UIEmotionPageView 第一个表情: %@, 图片: %@", firstEmotion.displayName, firstEmotion.image ? @"存在" : @"不存在");
|
||||
}
|
||||
|
||||
_emotions = emotions;
|
||||
[self setNeedsLayoutEmotions];
|
||||
[self setNeedsLayout];
|
||||
@@ -156,6 +168,13 @@ const int UISendButtonHeight = 41;
|
||||
}
|
||||
|
||||
emotionlayer.contents = (__bridge id)(self.emotions[i].image.CGImage);//使用layer效率更高
|
||||
|
||||
// 添加调试日志
|
||||
if (i < 3) { // 只打印前3个表情的调试信息
|
||||
NSLog(@"渲染表情 %ld: %@, 图片: %@, CGImage: %@", (long)i, self.emotions[i].displayName,
|
||||
self.emotions[i].image ? @"存在" : @"不存在",
|
||||
self.emotions[i].image.CGImage ? @"存在" : @"不存在");
|
||||
}
|
||||
NSInteger row = i / emotionCountPerRow;
|
||||
emotionOrigin.x = self.padding.left + (self.emotionSize.width + emotionHorizontalSpacing) * (i % emotionCountPerRow);
|
||||
emotionOrigin.y = self.padding.top + (self.emotionSize.height + emotionVerticalSpacing) * row;
|
||||
@@ -246,12 +265,29 @@ const int UISendButtonHeight = 41;
|
||||
emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing
|
||||
emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension
|
||||
paddingInPage:(UIEdgeInsets)paddingInPage {
|
||||
|
||||
// 添加调试日志
|
||||
NSLog(@"UIEmotionVerticalScrollView setEmotions: 表情数量=%lu, emotionSize=%@", (unsigned long)emotions.count, NSStringFromCGSize(emotionSize));
|
||||
if (emotions.count > 0) {
|
||||
QEmotion *firstEmotion = emotions[0];
|
||||
NSLog(@"第一个表情: %@, 图片: %@", firstEmotion.displayName, firstEmotion.image ? @"存在" : @"不存在");
|
||||
}
|
||||
|
||||
UIEmotionPageView *pageView = self.pageView;
|
||||
pageView.emotions = emotions;
|
||||
pageView.padding = paddingInPage;
|
||||
|
||||
// 添加调试日志
|
||||
NSLog(@"UIEmotionVerticalScrollView bounds: %@", NSStringFromCGRect(self.bounds));
|
||||
NSLog(@"UIEmotionVerticalScrollView paddingInPage: %@", NSStringFromUIEdgeInsets(paddingInPage));
|
||||
|
||||
CGSize contentSize = CGSizeMake(self.bounds.size.width - [self edgeInsetsGetHorizontalValue:paddingInPage], self.bounds.size.height - [self edgeInsetsGetVerticalValue:paddingInPage]);
|
||||
NSLog(@"UIEmotionVerticalScrollView contentSize: %@", NSStringFromCGSize(contentSize));
|
||||
|
||||
NSInteger emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing);
|
||||
pageView.numberOfRows = ceil(emotions.count / (CGFloat)emotionCountPerRow);
|
||||
|
||||
NSLog(@"UIEmotionVerticalScrollView emotionCountPerRow: %ld, numberOfRows: %ld", (long)emotionCountPerRow, (long)pageView.numberOfRows);
|
||||
pageView.emotionSize =emotionSize;
|
||||
pageView.emotionSelectedBackgroundExtension = emotionSelectedBackgroundExtension;
|
||||
pageView.minimumEmotionHorizontalSpacing = minimumEmotionHorizontalSpacing;
|
||||
@@ -370,6 +406,14 @@ const int UISendButtonHeight = 41;
|
||||
|
||||
- (void)setEmotions:(NSArray<QEmotion *> *)emotions {
|
||||
_emotions = emotions;
|
||||
|
||||
// 添加调试日志
|
||||
NSLog(@"QEmotionBoardView 设置表情数组,数量: %lu", (unsigned long)emotions.count);
|
||||
for (int i = 0; i < MIN(emotions.count, 5); i++) {
|
||||
QEmotion *emotion = emotions[i];
|
||||
NSLog(@"表情 %d: %@, 图片: %@", i, emotion.displayName, emotion.image ? @"存在" : @"不存在");
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,17 @@
|
||||
//把 @"[微笑]" 转为 @"😊"
|
||||
- (NSAttributedString *)obtainAttributedStringByImageKey:(NSString *)imageKey font:(UIFont *)font useCache:(BOOL)useCache;
|
||||
|
||||
//imageKey:[微笑] font:label的Font,返回😊 (带场景参数)
|
||||
//把 @"[微笑]" 转为 @"😊"
|
||||
- (NSAttributedString *)obtainAttributedStringByImageKey:(NSString *)imageKey font:(UIFont *)font useCache:(BOOL)useCache forMessageBubble:(BOOL)isMessageBubble;
|
||||
|
||||
//把 @"害~你好啊[微笑]" 转为 @"害~你好啊😊"
|
||||
- (NSMutableAttributedString *)attributedStringByText:(NSString *)text font:(UIFont *)font;
|
||||
|
||||
//把 @"害~你好啊[微笑]" 转为 @"害~你好啊😊" (带场景参数)
|
||||
- (NSMutableAttributedString *)attributedStringByText:(NSString *)text font:(UIFont *)font forMessageBubble:(BOOL)isMessageBubble;
|
||||
|
||||
// 清理 emoji 缓存,用于尺寸变更后刷新
|
||||
+ (void)clearEmojiCache;
|
||||
|
||||
@end
|
||||
|
@@ -46,6 +46,12 @@
|
||||
return _sharedFaceManager;
|
||||
}
|
||||
|
||||
// 清理 emoji 缓存,用于尺寸变更后刷新
|
||||
+ (void)clearEmojiCache {
|
||||
QEmotionHelper *helper = [QEmotionHelper sharedEmotionHelper];
|
||||
[helper.cacheAttributedDictionary removeAllObjects];
|
||||
}
|
||||
|
||||
#pragma mark - public
|
||||
//本方法我这里只是demo演示;实际开发中,可以改为你自己的获取表情列表的写法
|
||||
//由于emotionArray包含Image,测试结果是占用0.5MB的内存(永驻)
|
||||
@@ -78,6 +84,11 @@
|
||||
|
||||
//把整段String:@"害~你好[微笑]", 转为 @"害~你好😊"
|
||||
- (NSMutableAttributedString *)attributedStringByText:(NSString *)text font:(UIFont *)font {
|
||||
return [self attributedStringByText:text font:font forMessageBubble:NO];
|
||||
}
|
||||
|
||||
//把整段String:@"害~你好[微笑]", 转为 @"害~你好😊" (带场景参数)
|
||||
- (NSMutableAttributedString *)attributedStringByText:(NSString *)text font:(UIFont *)font forMessageBubble:(BOOL)isMessageBubble {
|
||||
if(text.length == 0){
|
||||
return [[NSMutableAttributedString alloc] initWithString:@""];;
|
||||
}
|
||||
@@ -98,7 +109,7 @@
|
||||
//ios15他采用了NSTextAttachmentViewProvider,具体我没研究
|
||||
useCache = NO;
|
||||
}
|
||||
NSAttributedString *imageAttributedString = [self obtainAttributedStringByImageKey:emojiKey font:font useCache:useCache];
|
||||
NSAttributedString *imageAttributedString = [self obtainAttributedStringByImageKey:emojiKey font:font useCache:useCache forMessageBubble:isMessageBubble];
|
||||
if (imageAttributedString) {
|
||||
[intactAttributeString replaceCharactersInRange:result.range withAttributedString:imageAttributedString];
|
||||
}
|
||||
@@ -115,6 +126,13 @@
|
||||
//imageKey:[微笑] ,font:label的Font,返回😊
|
||||
//把 @@"[微笑]", 转为 @"😊"
|
||||
- (NSAttributedString *)obtainAttributedStringByImageKey:(NSString *)imageKey font:(UIFont *)font useCache:(BOOL)useCache {
|
||||
return [self obtainAttributedStringByImageKey:imageKey font:font useCache:useCache forMessageBubble:NO];
|
||||
}
|
||||
|
||||
//把只是单纯的一个表情转为AttributedString (带场景参数)
|
||||
//imageKey:[微笑] ,font:label的Font,返回😊
|
||||
//把 @@"[微笑]", 转为 @"😊"
|
||||
- (NSAttributedString *)obtainAttributedStringByImageKey:(NSString *)imageKey font:(UIFont *)font useCache:(BOOL)useCache forMessageBubble:(BOOL)isMessageBubble {
|
||||
if([self.emojiHantList containsObject:imageKey]){
|
||||
NSString *getImageKey = [self.emojiHansList xpSafeObjectAtIndex:[self.emojiHantList indexOfObject:imageKey]];
|
||||
if(getImageKey != nil){
|
||||
@@ -133,14 +151,17 @@
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
imageView.displayText = imageKey;
|
||||
imageView.image = image;
|
||||
|
||||
// 计算 emoji 尺寸:根据场景决定是否放大
|
||||
CGFloat emojiSize = isMessageBubble ? font.lineHeight * 2.0 : font.lineHeight; // 消息气泡放大一倍
|
||||
if (image) {
|
||||
CGFloat scale = image.size.width / image.size.height;
|
||||
imageView.bounds = CGRectMake(0, 0, 20 * scale, 20);
|
||||
imageView.bounds = CGRectMake(0, 0, emojiSize * scale, emojiSize);
|
||||
} else {
|
||||
imageView.bounds = CGRectMake(0, 0, 20, 20);
|
||||
imageView.bounds = CGRectMake(0, 0, emojiSize, emojiSize);
|
||||
}
|
||||
imageView.bounds = CGRectMake(0, 0, font.lineHeight, font.lineHeight);
|
||||
NSMutableAttributedString * attrString = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeScaleAspectFit attachmentSize:CGSizeMake(imageView.bounds.size.width, imageView.bounds.size.height) alignToFont:[UIFont systemFontOfSize:15.0] alignment:YYTextVerticalAlignmentCenter];
|
||||
|
||||
NSMutableAttributedString * attrString = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeScaleAspectFit attachmentSize:CGSizeMake(imageView.bounds.size.width, imageView.bounds.size.height) alignToFont:font alignment:YYTextVerticalAlignmentCenter];
|
||||
return attrString;
|
||||
}
|
||||
|
||||
@@ -165,14 +186,17 @@
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
imageView.displayText = imageKey;
|
||||
imageView.image = image;
|
||||
|
||||
// 计算 emoji 尺寸:根据场景决定是否放大
|
||||
CGFloat emojiSize = isMessageBubble ? font.lineHeight * 2.0 : font.lineHeight; // 消息气泡放大一倍
|
||||
if (image) {
|
||||
CGFloat scale = image.size.width / image.size.height;
|
||||
imageView.bounds = CGRectMake(0, 0, 20 * scale, 20);
|
||||
imageView.bounds = CGRectMake(0, 0, emojiSize * scale, emojiSize);
|
||||
} else {
|
||||
imageView.bounds = CGRectMake(0, 0, 20, 20);
|
||||
imageView.bounds = CGRectMake(0, 0, emojiSize, emojiSize);
|
||||
}
|
||||
imageView.bounds = CGRectMake(0, 0, font.lineHeight, font.lineHeight);
|
||||
NSMutableAttributedString * attrString = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeScaleAspectFit attachmentSize:CGSizeMake(imageView.bounds.size.width, imageView.bounds.size.height) alignToFont:[UIFont systemFontOfSize:15.0] alignment:YYTextVerticalAlignmentCenter];
|
||||
|
||||
NSMutableAttributedString * attrString = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeScaleAspectFit attachmentSize:CGSizeMake(imageView.bounds.size.width, imageView.bounds.size.height) alignToFont:font alignment:YYTextVerticalAlignmentCenter];
|
||||
//[微笑]17 对应的NSAttributedString 缓存到Dictionary中
|
||||
[_cacheAttributedDictionary setObject:attrString forKey:keyFont];
|
||||
return result;
|
||||
|
115
YuMi/Global/BuglyManager.h
Normal file
115
YuMi/Global/BuglyManager.h
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// BuglyManager.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by BuglyManager
|
||||
// Copyright © 2024 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class BuglyManager;
|
||||
|
||||
/**
|
||||
* BuglyManager 代理协议
|
||||
* 用于监听卡顿和性能问题
|
||||
*/
|
||||
@protocol BuglyManagerDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
/**
|
||||
* 检测到卡顿时的回调
|
||||
* @param manager BuglyManager 实例
|
||||
* @param duration 卡顿持续时间(秒)
|
||||
*/
|
||||
- (void)buglyManager:(BuglyManager *)manager didDetectLag:(NSTimeInterval)duration;
|
||||
|
||||
/**
|
||||
* 检测到主线程阻塞时的回调
|
||||
* @param manager BuglyManager 实例
|
||||
* @param duration 阻塞持续时间(秒)
|
||||
*/
|
||||
- (void)buglyManager:(BuglyManager *)manager didDetectBlock:(NSTimeInterval)duration;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Bugly 统一管理类
|
||||
* 封装所有 Bugly 相关操作,提供统一的错误上报和性能监控接口
|
||||
*/
|
||||
@interface BuglyManager : NSObject
|
||||
|
||||
/**
|
||||
* 单例访问方法
|
||||
* @return BuglyManager 单例实例
|
||||
*/
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
/**
|
||||
* 设置代理对象
|
||||
*/
|
||||
@property (nonatomic, assign) id<BuglyManagerDelegate> delegate;
|
||||
|
||||
/**
|
||||
* 配置并启动 Bugly
|
||||
* @param appId Bugly 应用 ID
|
||||
* @param isDebug 是否为调试模式
|
||||
*/
|
||||
- (void)configureWithAppId:(NSString *)appId debug:(BOOL)isDebug;
|
||||
|
||||
/**
|
||||
* 上报错误信息
|
||||
* @param domain 错误域
|
||||
* @param code 错误码
|
||||
* @param userInfo 错误详细信息
|
||||
*/
|
||||
- (void)reportError:(NSString *)domain
|
||||
code:(NSInteger)code
|
||||
userInfo:(NSDictionary *)userInfo;
|
||||
|
||||
/**
|
||||
* 上报业务错误(简化版)
|
||||
* @param message 错误消息
|
||||
* @param code 错误码
|
||||
* @param context 错误上下文信息
|
||||
*/
|
||||
- (void)reportBusinessError:(NSString *)message
|
||||
code:(NSInteger)code
|
||||
context:(NSDictionary *)context;
|
||||
|
||||
/**
|
||||
* 上报网络请求异常
|
||||
* @param uid 用户ID
|
||||
* @param api 接口路径
|
||||
* @param code 错误码
|
||||
* @param userInfo 额外信息
|
||||
*/
|
||||
- (void)reportNetworkError:(NSString *)uid
|
||||
api:(NSString *)api
|
||||
code:(NSInteger)code
|
||||
userInfo:(NSDictionary *)userInfo;
|
||||
|
||||
/**
|
||||
* 上报内购相关错误
|
||||
* @param uid 用户ID
|
||||
* @param transactionId 交易ID
|
||||
* @param orderId 订单ID
|
||||
* @param status 状态码
|
||||
* @param context 上下文信息
|
||||
*/
|
||||
- (void)reportIAPError:(NSString *)uid
|
||||
transactionId:(NSString *)transactionId
|
||||
orderId:(NSString *)orderId
|
||||
status:(NSInteger)status
|
||||
context:(NSDictionary *)context;
|
||||
|
||||
/**
|
||||
* 手动触发卡顿检测
|
||||
*/
|
||||
- (void)startLagDetection;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
262
YuMi/Global/BuglyManager.m
Normal file
262
YuMi/Global/BuglyManager.m
Normal file
@@ -0,0 +1,262 @@
|
||||
//
|
||||
// BuglyManager.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by BuglyManager
|
||||
// Copyright © 2024 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BuglyManager.h"
|
||||
#import <Bugly/Bugly.h>
|
||||
|
||||
@interface BuglyManager () <BuglyDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSString *appId;
|
||||
@property (nonatomic, assign) BOOL isConfigured;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BuglyManager
|
||||
|
||||
#pragma mark - Singleton
|
||||
|
||||
+ (instancetype)sharedManager {
|
||||
static BuglyManager *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[BuglyManager alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_isConfigured = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - BuglyDelegate
|
||||
- (NSString * BLY_NULLABLE)attachmentForException:(NSException * BLY_NULLABLE)exception {
|
||||
NSString *message = [NSString stringWithFormat:@"%@ - %@", exception.name, exception.reason];
|
||||
[self handleLagDetection:message];
|
||||
return message;
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)configureWithAppId:(NSString *)appId debug:(BOOL)isDebug {
|
||||
if (self.isConfigured) {
|
||||
NSLog(@"[BuglyManager] Bugly 已经配置,跳过重复配置");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appId || appId.length == 0) {
|
||||
NSLog(@"[BuglyManager] 错误:appId 不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
self.appId = appId;
|
||||
|
||||
// 创建 Bugly 配置
|
||||
BuglyConfig *config = [[BuglyConfig alloc] init];
|
||||
config.delegate = self;
|
||||
|
||||
// 基础配置
|
||||
config.blockMonitorTimeout = 3.0; // 卡顿监控超时时间:3秒
|
||||
config.blockMonitorEnable = YES; // 启用卡顿监控
|
||||
|
||||
// 调试模式配置
|
||||
if (isDebug) {
|
||||
config.debugMode = NO; // 生产环境关闭调试模式
|
||||
config.channel = [self getAppChannel];
|
||||
config.reportLogLevel = BuglyLogLevelWarn; // 设置日志级别
|
||||
} else {
|
||||
config.unexpectedTerminatingDetectionEnable = YES; // 非正常退出事件记录
|
||||
config.debugMode = NO;
|
||||
config.channel = [self getAppChannel];
|
||||
config.blockMonitorEnable = YES;
|
||||
config.reportLogLevel = BuglyLogLevelWarn;
|
||||
}
|
||||
// 启动 Bugly
|
||||
[Bugly startWithAppId:appId config:config];
|
||||
|
||||
self.isConfigured = YES;
|
||||
|
||||
NSLog(@"[BuglyManager] Bugly 配置完成 - AppID: %@, Debug: %@", appId, isDebug ? @"YES" : @"NO");
|
||||
}
|
||||
|
||||
- (void)reportError:(NSString *)domain
|
||||
code:(NSInteger)code
|
||||
userInfo:(NSDictionary *)userInfo {
|
||||
|
||||
if (!self.isConfigured) {
|
||||
NSLog(@"[BuglyManager] 错误:Bugly 未配置,无法上报错误");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!domain || domain.length == 0) {
|
||||
domain = @"UnknownError";
|
||||
}
|
||||
|
||||
// 创建错误对象
|
||||
NSError *error = [NSError errorWithDomain:domain
|
||||
code:code
|
||||
userInfo:userInfo];
|
||||
|
||||
// 异步上报错误
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[Bugly reportError:error];
|
||||
NSLog(@"[BuglyManager] 错误上报成功 - Domain: %@, Code: %ld", domain, (long)code);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)reportBusinessError:(NSString *)message
|
||||
code:(NSInteger)code
|
||||
context:(NSDictionary *)context {
|
||||
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
|
||||
// 添加基础信息
|
||||
if (message && message.length > 0) {
|
||||
[userInfo setObject:message forKey:@"error_message"];
|
||||
}
|
||||
[userInfo setObject:@(code) forKey:@"error_code"];
|
||||
[userInfo setObject:@"BusinessError" forKey:@"error_type"];
|
||||
|
||||
// 添加上下文信息
|
||||
if (context && context.count > 0) {
|
||||
[userInfo addEntriesFromDictionary:context];
|
||||
}
|
||||
|
||||
// 添加时间戳
|
||||
[userInfo setObject:@([[NSDate date] timeIntervalSince1970]) forKey:@"timestamp"];
|
||||
|
||||
[self reportError:@"BusinessError" code:code userInfo:userInfo];
|
||||
}
|
||||
|
||||
- (void)reportNetworkError:(NSString *)uid
|
||||
api:(NSString *)api
|
||||
code:(NSInteger)code
|
||||
userInfo:(NSDictionary *)userInfo {
|
||||
|
||||
NSMutableDictionary *errorInfo = [NSMutableDictionary dictionary];
|
||||
|
||||
// 添加网络错误特有信息
|
||||
if (uid && uid.length > 0) {
|
||||
[errorInfo setObject:uid forKey:@"user_id"];
|
||||
}
|
||||
if (api && api.length > 0) {
|
||||
[errorInfo setObject:api forKey:@"api_path"];
|
||||
}
|
||||
[errorInfo setObject:@(code) forKey:@"http_code"];
|
||||
[errorInfo setObject:@"NetworkError" forKey:@"error_type"];
|
||||
|
||||
// 添加调用栈信息
|
||||
[errorInfo setObject:[NSThread callStackSymbols] forKey:@"call_stack_symbols"];
|
||||
|
||||
// 合并额外信息
|
||||
if (userInfo && userInfo.count > 0) {
|
||||
[errorInfo addEntriesFromDictionary:userInfo];
|
||||
}
|
||||
|
||||
[self reportError:@"NetworkError" code:code userInfo:errorInfo];
|
||||
}
|
||||
|
||||
- (void)reportIAPError:(NSString *)uid
|
||||
transactionId:(NSString *)transactionId
|
||||
orderId:(NSString *)orderId
|
||||
status:(NSInteger)status
|
||||
context:(NSDictionary *)context {
|
||||
|
||||
NSMutableDictionary *errorInfo = [NSMutableDictionary dictionary];
|
||||
|
||||
// 添加内购错误特有信息
|
||||
if (uid && uid.length > 0) {
|
||||
[errorInfo setObject:uid forKey:@"user_id"];
|
||||
}
|
||||
if (transactionId && transactionId.length > 0) {
|
||||
[errorInfo setObject:transactionId forKey:@"transaction_id"];
|
||||
}
|
||||
if (orderId && orderId.length > 0) {
|
||||
[errorInfo setObject:orderId forKey:@"order_id"];
|
||||
}
|
||||
[errorInfo setObject:@(status) forKey:@"status_code"];
|
||||
[errorInfo setObject:@"IAPError" forKey:@"error_type"];
|
||||
|
||||
// 添加状态描述
|
||||
NSString *statusMsg = [self getIAPStatusMessage:status];
|
||||
if (statusMsg) {
|
||||
[errorInfo setObject:statusMsg forKey:@"status_message"];
|
||||
}
|
||||
|
||||
// 合并上下文信息
|
||||
if (context && context.count > 0) {
|
||||
[errorInfo addEntriesFromDictionary:context];
|
||||
}
|
||||
|
||||
// 生成错误码
|
||||
NSInteger errorCode = -20000 + status;
|
||||
|
||||
[self reportError:@"IAPError" code:errorCode userInfo:errorInfo];
|
||||
}
|
||||
|
||||
- (void)startLagDetection {
|
||||
if (!self.isConfigured) {
|
||||
NSLog(@"[BuglyManager] 错误:Bugly 未配置,无法启动卡顿检测");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[BuglyManager] 手动启动卡顿检测");
|
||||
// Bugly 会自动进行卡顿检测,这里主要是日志记录
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (void)handleLagDetection:(NSString *)stackTrace {
|
||||
NSLog(@"[BuglyManager] 🚨 检测到卡顿 - StackTrace: %@", stackTrace);
|
||||
|
||||
// 计算卡顿持续时间(这里假设为3秒,实际应该从 Bugly 配置中获取)
|
||||
NSTimeInterval duration = 3.0;
|
||||
|
||||
// 通知代理
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(buglyManager:didDetectLag:)]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[(id<BuglyManagerDelegate>)self.delegate buglyManager:self didDetectLag:duration];
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 触发卡顿通知逻辑
|
||||
// 1. 记录卡顿信息到本地日志
|
||||
// 2. 发送本地通知
|
||||
// 3. 记录到性能监控系统
|
||||
// 4. 触发用户反馈机制
|
||||
}
|
||||
|
||||
- (NSString *)getAppChannel {
|
||||
// 这里应该调用项目中的工具方法获取渠道信息
|
||||
// 暂时返回默认值
|
||||
return @"AppStore";
|
||||
}
|
||||
|
||||
- (NSString *)getIAPStatusMessage:(NSInteger)status {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return @"尝试验单";
|
||||
case 1:
|
||||
return @"验单-补单成功";
|
||||
case 2:
|
||||
return @"验单-补单失败";
|
||||
case 3:
|
||||
return @"验单-补单 id 异常";
|
||||
case 4:
|
||||
return @"重试次数过多";
|
||||
case 5:
|
||||
return @"过期交易清理";
|
||||
default:
|
||||
return @"未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
42
YuMi/Global/BuglyManagerExample.h
Normal file
42
YuMi/Global/BuglyManagerExample.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// BuglyManagerExample.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by BuglyManager Example
|
||||
// Copyright © 2024 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "BuglyManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* BuglyManager 使用示例类
|
||||
* 展示如何使用 BuglyManager 的各种功能
|
||||
*/
|
||||
@interface BuglyManagerExample : NSObject <BuglyManagerDelegate>
|
||||
|
||||
/**
|
||||
* 设置 Bugly 代理
|
||||
*/
|
||||
- (void)setupBuglyDelegate;
|
||||
|
||||
/**
|
||||
* 上报业务错误示例
|
||||
*/
|
||||
- (void)reportBusinessErrorExample;
|
||||
|
||||
/**
|
||||
* 上报网络错误示例
|
||||
*/
|
||||
- (void)reportNetworkErrorExample;
|
||||
|
||||
/**
|
||||
* 上报内购错误示例
|
||||
*/
|
||||
- (void)reportIAPErrorExample;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
79
YuMi/Global/BuglyManagerExample.m
Normal file
79
YuMi/Global/BuglyManagerExample.m
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// BuglyManagerExample.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by BuglyManager Example
|
||||
// Copyright © 2024 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BuglyManagerExample.h"
|
||||
#import "BuglyManager.h"
|
||||
|
||||
@implementation BuglyManagerExample
|
||||
|
||||
#pragma mark - 使用示例
|
||||
|
||||
// 示例1:设置代理监听卡顿
|
||||
- (void)setupBuglyDelegate {
|
||||
[BuglyManager sharedManager].delegate = self;
|
||||
}
|
||||
|
||||
// 示例2:上报业务错误
|
||||
- (void)reportBusinessErrorExample {
|
||||
NSDictionary *context = @{
|
||||
@"page": @"HomePage",
|
||||
@"action": @"loadData",
|
||||
@"timestamp": @([[NSDate date] timeIntervalSince1970])
|
||||
};
|
||||
|
||||
[[BuglyManager sharedManager] reportBusinessError:@"数据加载失败"
|
||||
code:1001
|
||||
context:context];
|
||||
}
|
||||
|
||||
// 示例3:上报网络错误
|
||||
- (void)reportNetworkErrorExample {
|
||||
NSDictionary *userInfo = @{
|
||||
@"requestParams": @{@"userId": @"12345"},
|
||||
@"responseData": @"服务器错误"
|
||||
};
|
||||
|
||||
[[BuglyManager sharedManager] reportNetworkError:@"user123"
|
||||
api:@"user/profile"
|
||||
code:500
|
||||
userInfo:userInfo];
|
||||
}
|
||||
|
||||
// 示例4:上报内购错误
|
||||
- (void)reportIAPErrorExample {
|
||||
NSDictionary *context = @{
|
||||
@"retryCount": @3,
|
||||
@"productId": @"com.yumi.coin100"
|
||||
};
|
||||
|
||||
[[BuglyManager sharedManager] reportIAPError:@"user123"
|
||||
transactionId:@"txn_123456"
|
||||
orderId:@"order_789"
|
||||
status:2
|
||||
context:context];
|
||||
}
|
||||
|
||||
#pragma mark - BuglyManagerDelegate
|
||||
|
||||
- (void)buglyManager:(BuglyManager *)manager didDetectLag:(NSTimeInterval)duration {
|
||||
NSLog(@"[Example] 检测到卡顿,持续时间: %.2f 秒", duration);
|
||||
|
||||
// TODO: 在这里实现卡顿通知逻辑
|
||||
// 1. 记录到本地日志
|
||||
// 2. 发送本地通知
|
||||
// 3. 上报到性能监控系统
|
||||
// 4. 触发用户反馈机制
|
||||
}
|
||||
|
||||
- (void)buglyManager:(BuglyManager *)manager didDetectBlock:(NSTimeInterval)duration {
|
||||
NSLog(@"[Example] 检测到主线程阻塞,持续时间: %.2f 秒", duration);
|
||||
|
||||
// TODO: 在这里实现阻塞通知逻辑
|
||||
}
|
||||
|
||||
@end
|
129
YuMi/Global/BuglyManager_README.md
Normal file
129
YuMi/Global/BuglyManager_README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# BuglyManager 使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
`BuglyManager` 是一个统一的 Bugly 管理类,封装了所有 Bugly 相关操作,提供统一的错误上报和性能监控接口。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 统一错误上报
|
||||
- 业务错误上报
|
||||
- 网络错误上报
|
||||
- 内购错误上报
|
||||
- 自定义错误上报
|
||||
|
||||
### 2. 卡顿监听
|
||||
- 自动检测主线程卡顿
|
||||
- 支持代理回调通知
|
||||
- 可配置卡顿阈值
|
||||
|
||||
### 3. 性能监控
|
||||
- 主线程阻塞检测
|
||||
- 异常退出检测
|
||||
- 自定义日志级别
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 初始化配置
|
||||
|
||||
```objc
|
||||
// 在 AppDelegate 中配置
|
||||
#ifdef DEBUG
|
||||
[[BuglyManager sharedManager] configureWithAppId:@"c937fd00f7" debug:YES];
|
||||
#else
|
||||
[[BuglyManager sharedManager] configureWithAppId:@"8627948559" debug:NO];
|
||||
#endif
|
||||
```
|
||||
|
||||
### 2. 设置代理监听卡顿
|
||||
|
||||
```objc
|
||||
// 在需要监听卡顿的类中
|
||||
@interface YourClass : NSObject <BuglyManagerDelegate>
|
||||
@end
|
||||
|
||||
@implementation YourClass
|
||||
- (void)setupBuglyDelegate {
|
||||
[BuglyManager sharedManager].delegate = self;
|
||||
}
|
||||
|
||||
- (void)buglyManager:(BuglyManager *)manager didDetectLag:(NSTimeInterval)duration {
|
||||
NSLog(@"检测到卡顿,持续时间: %.2f 秒", duration);
|
||||
// TODO: 实现卡顿通知逻辑
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
### 3. 错误上报
|
||||
|
||||
#### 业务错误上报
|
||||
```objc
|
||||
NSDictionary *context = @{
|
||||
@"page": @"HomePage",
|
||||
@"action": @"loadData"
|
||||
};
|
||||
|
||||
[[BuglyManager sharedManager] reportBusinessError:@"数据加载失败"
|
||||
code:1001
|
||||
context:context];
|
||||
```
|
||||
|
||||
#### 网络错误上报
|
||||
```objc
|
||||
NSDictionary *userInfo = @{
|
||||
@"requestParams": @{@"userId": @"12345"},
|
||||
@"responseData": @"服务器错误"
|
||||
};
|
||||
|
||||
[[BuglyManager sharedManager] reportNetworkError:@"user123"
|
||||
api:@"user/profile"
|
||||
code:500
|
||||
userInfo:userInfo];
|
||||
```
|
||||
|
||||
#### 内购错误上报
|
||||
```objc
|
||||
NSDictionary *context = @{
|
||||
@"retryCount": @3,
|
||||
@"productId": @"com.yumi.coin100"
|
||||
};
|
||||
|
||||
[[BuglyManager sharedManager] reportIAPError:@"user123"
|
||||
transactionId:@"txn_123456"
|
||||
orderId:@"order_789"
|
||||
status:2
|
||||
context:context];
|
||||
```
|
||||
|
||||
## 重构完成情况
|
||||
|
||||
### ✅ 已完成
|
||||
1. 创建 `BuglyManager.h` 和 `BuglyManager.m`
|
||||
2. 修改 `AppDelegate+ThirdConfig.m` 使用 BuglyManager
|
||||
3. 修改 `IAPManager.m` 使用 BuglyManager
|
||||
4. 修改 `HttpRequestHelper.m` 使用 BuglyManager
|
||||
5. 修改 `GiftComboManager.m` 使用 BuglyManager
|
||||
6. 创建使用示例和文档
|
||||
|
||||
### 🔄 进行中
|
||||
1. 修改 `XPGiftPresenter.m` 使用 BuglyManager
|
||||
|
||||
### 📋 待完成
|
||||
1. 测试验证所有功能
|
||||
2. 完善卡顿通知逻辑
|
||||
3. 性能优化
|
||||
|
||||
## 优势
|
||||
|
||||
1. **统一管理**:所有 Bugly 相关操作集中在一个类中
|
||||
2. **降低耦合**:其他模块无需直接引入 Bugly 头文件
|
||||
3. **易于维护**:统一的接口和错误处理逻辑
|
||||
4. **功能扩展**:支持卡顿监听和自定义通知
|
||||
5. **向后兼容**:保持现有功能完全不变
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保在真机环境下编译,模拟器可能无法正确导入 Bugly 头文件
|
||||
2. 卡顿监听功能需要在实际设备上测试
|
||||
3. 错误上报是异步操作,不会阻塞主线程
|
||||
4. 建议在 AppDelegate 中尽早初始化 BuglyManager
|
@@ -46,7 +46,7 @@ isPhoneXSeries = [[UIApplication sharedApplication] delegate].window.safeAreaIns
|
||||
#define kFontHeavy(font) [UIFont systemFontOfSize:kGetScaleWidth(font) weight:UIFontWeightHeavy]
|
||||
|
||||
///内置版本号
|
||||
#define PI_App_Version @"1.0.30"
|
||||
#define PI_App_Version @"1.0.31"
|
||||
///渠道
|
||||
#define PI_App_Source @"appstore"
|
||||
#define PI_Test_Flight @"TestFlight"
|
||||
|
@@ -808,6 +808,8 @@ typedef NS_ENUM(NSUInteger, CustomMessageTypeRoomLevelUpdate) {
|
||||
|
||||
@property(nonatomic, assign) NSInteger seq; // 本地序号,用于将一条消息分解为多条有次序的消息
|
||||
|
||||
@property (nonatomic, assign) BOOL isFromPublic;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
@@ -64,7 +64,7 @@
|
||||
model.message.rawAttachContent];
|
||||
}
|
||||
QEmotionHelper *faceManager = [QEmotionHelper sharedEmotionHelper];
|
||||
NSMutableAttributedString * attribute = [faceManager attributedStringByText:messageText font:[UIFont systemFontOfSize:13]];
|
||||
NSMutableAttributedString * attribute = [faceManager attributedStringByText:messageText font:[UIFont systemFontOfSize:13] forMessageBubble:YES];
|
||||
_messageText.attributedText = attribute;
|
||||
}
|
||||
[_messageText.superview layoutIfNeeded];
|
||||
|
@@ -48,7 +48,7 @@
|
||||
|
||||
CGSize dstRect = CGSizeMake(width, MAXFLOAT);
|
||||
QEmotionHelper *faceManager = [QEmotionHelper sharedEmotionHelper];
|
||||
NSMutableAttributedString * attribute = [faceManager attributedStringByText:messageText font:kFontMedium(14)];
|
||||
NSMutableAttributedString * attribute = [faceManager attributedStringByText:messageText font:kFontMedium(14) forMessageBubble:YES];
|
||||
if(extModel.iosBubbleUrl.length > 0){
|
||||
[attribute addAttributes:@{NSForegroundColorAttributeName: UIColorFromRGB(0x333333)} range:[attribute.string rangeOfString:attribute.string]];
|
||||
}else{
|
||||
|
@@ -34,7 +34,7 @@
|
||||
}
|
||||
CGSize dstRect = CGSizeMake(CONTENT_WIDTH_MAX - MESSAGE_PADDING * 2, MAXFLOAT);
|
||||
QEmotionHelper *faceManager = [QEmotionHelper sharedEmotionHelper];
|
||||
NSMutableAttributedString * attribute = [faceManager attributedStringByText:messageText font:[UIFont systemFontOfSize:13]];
|
||||
NSMutableAttributedString * attribute = [faceManager attributedStringByText:messageText font:[UIFont systemFontOfSize:13] forMessageBubble:YES];
|
||||
self.textAttribute = attribute;
|
||||
YYTextContainer *container = [YYTextContainer containerWithSize:dstRect];
|
||||
container.maximumNumberOfRows = 0;
|
||||
|
@@ -152,8 +152,10 @@
|
||||
}
|
||||
|
||||
- (void)uploadGifAvatar:(NSData *)data {
|
||||
NSString *format = [UIImage getImageTypeWithImageData:data];
|
||||
NSString *name = [NSString stringWithFormat:@"image/%@.%@",[NSString createUUID],format];
|
||||
// 兼容视频与图片数据:自动识别数据类型并给出正确的文件后缀
|
||||
NSString *format = [self.class detectMediaExtensionForData:data];
|
||||
// 维持原有目录结构,避免服务器侧路径依赖被破坏
|
||||
NSString *name = [NSString stringWithFormat:@"image/%@.%@", [NSString createUUID], format];
|
||||
@kWeakify(self);
|
||||
[[UploadFile share]QCloudUploadImage:data named:name success:^(NSString * _Nonnull key, NSDictionary * _Nonnull resp) {
|
||||
@kStrongify(self);
|
||||
@@ -163,6 +165,43 @@
|
||||
}];
|
||||
}
|
||||
|
||||
/// 根据二进制数据自动识别媒体类型,返回合适的文件后缀
|
||||
/// 支持:gif、png、jpg、mp4(通过 ftyp box 识别)
|
||||
/// 回退策略:无法识别时使用 jpg
|
||||
+ (NSString *)detectMediaExtensionForData:(NSData *)data {
|
||||
if (data.length < 12) {
|
||||
return @"jpg";
|
||||
}
|
||||
|
||||
const unsigned char *bytes = (const unsigned char *)data.bytes;
|
||||
|
||||
// GIF: 47 49 46 38 39|37 61 -> "GIF89a" / "GIF87a"
|
||||
if (bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F') {
|
||||
return @"gif";
|
||||
}
|
||||
|
||||
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
||||
const unsigned char pngSig[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
|
||||
if (memcmp(bytes, pngSig, 8) == 0) {
|
||||
return @"png";
|
||||
}
|
||||
|
||||
// JPEG: FF D8 FF
|
||||
if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) {
|
||||
return @"jpg";
|
||||
}
|
||||
|
||||
// MP4: offset 4..7 == 'f' 't' 'y' 'p'
|
||||
if (data.length >= 12) {
|
||||
if (bytes[4] == 'f' && bytes[5] == 't' && bytes[6] == 'y' && bytes[7] == 'p') {
|
||||
return @"mp4";
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底
|
||||
return @"jpg";
|
||||
}
|
||||
|
||||
///获取地区列表
|
||||
-(void)getAreaList{
|
||||
@kWeakify(self);
|
||||
|
@@ -10,6 +10,7 @@
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <ReactiveObjC/ReactiveObjC.h>
|
||||
#import <TZImagePickerController/TZImagePickerController.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
///Tool
|
||||
|
||||
#import "TTPopup.h"
|
||||
@@ -197,11 +198,14 @@ TZImagePickerControllerDelegate>
|
||||
imagePickerVc.allowTakeVideo = NO;
|
||||
if (displayGIF) {
|
||||
imagePickerVc.allowTakePicture = NO;
|
||||
// 当允许选择视频时,禁用裁剪功能,因为视频不能像图片一样裁剪
|
||||
imagePickerVc.allowCrop = NO;
|
||||
} else {
|
||||
imagePickerVc.allowCrop = YES;
|
||||
CGFloat cropWidth = KScreenWidth;
|
||||
imagePickerVc.cropRect = CGRectMake(0, (self.view.bounds.size.height - cropWidth) / 2, cropWidth, cropWidth);
|
||||
}
|
||||
imagePickerVc.allowPickingGif = displayGIF;
|
||||
imagePickerVc.allowCrop = YES;
|
||||
CGFloat cropWidth = KScreenWidth;
|
||||
imagePickerVc.cropRect = CGRectMake(0, (self.view.bounds.size.height - cropWidth) / 2, cropWidth, cropWidth);
|
||||
imagePickerVc.naviBgColor = [DJDKMIMOMColor appCellBackgroundColor];
|
||||
imagePickerVc.naviTitleColor = [DJDKMIMOMColor mainTextColor];
|
||||
imagePickerVc.barItemTextColor = [DJDKMIMOMColor mainTextColor];
|
||||
@@ -418,8 +422,13 @@ TZImagePickerControllerDelegate>
|
||||
#pragma mark - TZImagePickerControllerDelegate
|
||||
- (BOOL)isAssetCanBeDisplayed:(PHAsset *)asset {
|
||||
if (self.isPhotoPickerDisplayGIF) {
|
||||
return [[asset valueForKey:@"filename"] containsString:@"GIF"];
|
||||
// 当 displayGIF 为 YES 时,允许显示 GIF 和视频文件
|
||||
NSString *filename = [asset valueForKey:@"filename"];
|
||||
BOOL isGIF = [filename containsString:@"GIF"];
|
||||
// BOOL isVideo = asset.mediaType == PHAssetMediaTypeVideo;
|
||||
return isGIF;// || isVideo;
|
||||
} else {
|
||||
// 当 displayGIF 为 NO 时,只显示非 GIF 文件
|
||||
return ![[asset valueForKey:@"filename"] containsString:@"GIF"];
|
||||
}
|
||||
}
|
||||
@@ -457,6 +466,164 @@ TZImagePickerControllerDelegate>
|
||||
}
|
||||
}
|
||||
|
||||
// 处理视频选择
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingVideo:(UIImage *)coverImage sourceAssets:(PHAsset *)asset {
|
||||
@kWeakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@kStrongify(self);
|
||||
[self showLoading];
|
||||
[self processVideoAsset:asset];
|
||||
[picker dismissViewControllerAnimated:YES completion:^{}];
|
||||
});
|
||||
}
|
||||
|
||||
// 处理视频资源
|
||||
- (void)processVideoAsset:(PHAsset *)asset {
|
||||
@kWeakify(self);
|
||||
|
||||
// 检查视频时长(头像视频应该在5秒内)
|
||||
NSTimeInterval duration = asset.duration;
|
||||
if (duration > 555.0) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@kStrongify(self);
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频时长不能超过555秒"];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取视频的 AVAsset
|
||||
[[PHImageManager defaultManager] requestAVAssetForVideo:asset
|
||||
options:nil
|
||||
resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
|
||||
@kStrongify(self);
|
||||
if ([asset isKindOfClass:[AVURLAsset class]]) {
|
||||
AVURLAsset *urlAsset = (AVURLAsset *)asset;
|
||||
[self compressAndUploadVideo:urlAsset.URL];
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频格式不支持"];
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// 压缩并上传视频
|
||||
- (void)compressAndUploadVideo:(NSURL *)videoURL {
|
||||
@kWeakify(self);
|
||||
|
||||
// 创建输出URL
|
||||
NSString *outputFileName = [NSString stringWithFormat:@"compressed_video_%ld.mov", (long)[[NSDate date] timeIntervalSince1970]];
|
||||
NSString *outputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:outputFileName];
|
||||
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
|
||||
|
||||
// 删除已存在的文件
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:outputPath]) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:outputPath error:nil];
|
||||
}
|
||||
|
||||
// 创建导出会话
|
||||
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:[AVAsset assetWithURL:videoURL]
|
||||
presetName:AVAssetExportPresetMediumQuality];
|
||||
|
||||
if (exportSession) {
|
||||
exportSession.outputURL = outputURL;
|
||||
exportSession.outputFileType = AVFileTypeMPEG4;
|
||||
exportSession.shouldOptimizeForNetworkUse = YES;
|
||||
|
||||
// 设置视频尺寸(头像通常需要正方形)
|
||||
exportSession.videoComposition = [self createVideoCompositionForAsset:[AVAsset assetWithURL:videoURL]];
|
||||
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
@kStrongify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (exportSession.status == AVAssetExportSessionStatusCompleted) {
|
||||
// 检查文件大小(限制在100MB以内)
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:outputPath error:nil];
|
||||
NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize];
|
||||
if (fileSize && fileSize.longLongValue > 100 * 1024 * 1024) { // 10MB
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频文件过大,请选择较短的视频"];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:outputPath error:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取压缩后的视频数据
|
||||
NSData *videoData = [NSData dataWithContentsOfURL:outputURL];
|
||||
if (videoData) {
|
||||
// 使用现有的 GIF 上传方法上传视频数据
|
||||
[self.presenter uploadGifAvatar:videoData];
|
||||
} else {
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频数据读取失败"];
|
||||
}
|
||||
} else if (exportSession.status == AVAssetExportSessionStatusFailed) {
|
||||
[self hideHUD];
|
||||
[self showErrorToast:[NSString stringWithFormat:@"视频压缩失败: %@", exportSession.error.localizedDescription]];
|
||||
} else if (exportSession.status == AVAssetExportSessionStatusCancelled) {
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频处理已取消"];
|
||||
} else {
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频处理失败"];
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
[[NSFileManager defaultManager] removeItemAtPath:outputPath error:nil];
|
||||
});
|
||||
}];
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self hideHUD];
|
||||
[self showErrorToast:@"视频处理失败"];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建视频合成,确保视频为正方形
|
||||
- (AVMutableVideoComposition *)createVideoCompositionForAsset:(AVAsset *)asset {
|
||||
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
|
||||
|
||||
// 获取视频轨道
|
||||
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
|
||||
if (!videoTrack) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 设置视频尺寸为正方形(头像尺寸)
|
||||
CGSize videoSize = videoTrack.naturalSize;
|
||||
CGFloat size = MIN(videoSize.width, videoSize.height);
|
||||
CGSize targetSize = CGSizeMake(size, size);
|
||||
|
||||
videoComposition.renderSize = targetSize;
|
||||
videoComposition.frameDuration = CMTimeMake(1, 30); // 30fps
|
||||
|
||||
// 创建视频层指令
|
||||
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
|
||||
instruction.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
|
||||
|
||||
AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
|
||||
|
||||
// 计算裁剪区域,使视频居中显示
|
||||
CGFloat scaleX = targetSize.width / videoSize.width;
|
||||
CGFloat scaleY = targetSize.height / videoSize.height;
|
||||
CGFloat scale = MAX(scaleX, scaleY);
|
||||
|
||||
CGAffineTransform transform = CGAffineTransformMakeScale(scale, scale);
|
||||
|
||||
// 计算偏移量,使视频居中
|
||||
CGFloat offsetX = (targetSize.width - videoSize.width * scale) / 2;
|
||||
CGFloat offsetY = (targetSize.height - videoSize.height * scale) / 2;
|
||||
transform = CGAffineTransformTranslate(transform, offsetX / scale, offsetY / scale);
|
||||
|
||||
[layerInstruction setTransform:transform atTime:kCMTimeZero];
|
||||
instruction.layerInstructions = @[layerInstruction];
|
||||
videoComposition.instructions = @[instruction];
|
||||
|
||||
return videoComposition;
|
||||
}
|
||||
|
||||
#pragma mark - UIImagePickerControllerDelegate
|
||||
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
|
||||
{
|
||||
|
@@ -7,6 +7,7 @@
|
||||
|
||||
#import "IAPManager.h"
|
||||
#import <Bugly/Bugly.h>
|
||||
#import "BuglyManager.h"
|
||||
#import "Api+Mine.h"
|
||||
#import "YuMi-swift.h"
|
||||
#import "RechargeStorage.h"
|
||||
@@ -232,68 +233,23 @@
|
||||
}
|
||||
|
||||
- (void)_logToBugly:(NSString *)tid oID:(NSString *)oid status:(NSInteger)status {
|
||||
|
||||
NSMutableDictionary *logDic = [NSMutableDictionary dictionary];
|
||||
// 安全处理交易ID
|
||||
if ([NSString isEmpty:tid]) {
|
||||
[logDic setObject:@"" forKey:@"内购 transactionId"];
|
||||
tid = @"";
|
||||
} else {
|
||||
[logDic setObject:tid forKey:@"内购 transactionId"];
|
||||
}
|
||||
// 安全处理订单ID
|
||||
if ([NSString isEmpty:oid]) {
|
||||
[logDic setObject:@"" forKey:@"内购 orderId"];
|
||||
} else {
|
||||
[logDic setObject:oid forKey:@"内购 orderId"];
|
||||
}
|
||||
|
||||
// 安全获取用户ID
|
||||
// 使用 BuglyManager 统一上报内购错误
|
||||
NSString *uid = [AccountInfoStorage instance].getUid ?: @"未知用户";
|
||||
[logDic setObject:uid forKey:@"内购 用户id"];
|
||||
|
||||
// 添加重试次数信息
|
||||
[logDic setObject:@([self getRetryCountForTransaction:tid]) forKey:@"重试次数"];
|
||||
[logDic setObject:@(self.recheckInterval) forKey:@"重试间隔"];
|
||||
[logDic setObject:@(self.recheckIndex) forKey:@"当前索引"];
|
||||
[logDic setObject:@(self.isProcessing) forKey:@"处理中状态"];
|
||||
[logDic setObject:@(self.isLogin) forKey:@"登录状态"];
|
||||
// 构建上下文信息
|
||||
NSMutableDictionary *context = [NSMutableDictionary dictionary];
|
||||
[context setObject:@([self getRetryCountForTransaction:tid]) forKey:@"重试次数"];
|
||||
[context setObject:@(self.recheckInterval) forKey:@"重试间隔"];
|
||||
[context setObject:@(self.recheckIndex) forKey:@"当前索引"];
|
||||
[context setObject:@(self.isProcessing) forKey:@"处理中状态"];
|
||||
[context setObject:@(self.isLogin) forKey:@"登录状态"];
|
||||
|
||||
NSString *statusMsg = @"";
|
||||
NSInteger code = -20000;
|
||||
switch (status) {
|
||||
case 0:
|
||||
statusMsg = [NSString stringWithFormat:@"UID: %@, 尝试验单", uid];
|
||||
break;
|
||||
case 1:
|
||||
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单成功", uid];
|
||||
code = -20001;
|
||||
break;
|
||||
case 2:
|
||||
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单失败", uid];
|
||||
code = -20002;
|
||||
break;
|
||||
case 3:
|
||||
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单 id 异常", uid];
|
||||
code = -20002;
|
||||
break;
|
||||
case 4:
|
||||
statusMsg = [NSString stringWithFormat:@"UID: %@, 重试次数过多", uid];
|
||||
code = -20003;
|
||||
break;
|
||||
case 5:
|
||||
statusMsg = [NSString stringWithFormat:@"UID: %@, 过期交易清理", uid];
|
||||
code = -20004;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
[Bugly reportError:[NSError errorWithDomain:statusMsg
|
||||
code:code
|
||||
userInfo:logDic]];
|
||||
});
|
||||
// 使用 BuglyManager 上报
|
||||
[[BuglyManager sharedManager] reportIAPError:uid
|
||||
transactionId:tid
|
||||
orderId:oid
|
||||
status:status
|
||||
context:context];
|
||||
}
|
||||
|
||||
// 内购成功并通知外部
|
||||
|
@@ -266,8 +266,9 @@
|
||||
if ([[XPSkillCardPlayerManager shareInstance] isInRoomVC]) {
|
||||
[self.boomEventsQueue addObject:attachment];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"UpdateWhenBoomExplosion" object:nil];
|
||||
[self.bannerEventsQueue addObject:attachment];
|
||||
}
|
||||
[self.bannerEventsQueue addObject:attachment];
|
||||
// [self.bannerEventsQueue addObject:attachment];
|
||||
|
||||
[self checkAndStartBoomEvent];
|
||||
[self checkAndStartBannerEvent];
|
||||
|
23
YuMi/Modules/YMRoom/Manager/GameBannerGestureManager.h
Normal file
23
YuMi/Modules/YMRoom/Manager/GameBannerGestureManager.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// GameBannerGestureManager.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by P on 2025/8/27.
|
||||
//
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface GameBannerGestureManager : NSObject
|
||||
|
||||
@property (nonatomic, weak) UIView *bannerContainer;
|
||||
@property (nonatomic, assign, readonly) BOOL isEnabled;
|
||||
|
||||
- (void)enableGestureForGameMode;
|
||||
- (void)disableGestureForGameMode;
|
||||
- (void)cleanupAllGestures;
|
||||
- (void)resetState;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
89
YuMi/Modules/YMRoom/Manager/GameBannerGestureManager.m
Normal file
89
YuMi/Modules/YMRoom/Manager/GameBannerGestureManager.m
Normal file
@@ -0,0 +1,89 @@
|
||||
// GameBannerGestureManager.m
|
||||
#import "GameBannerGestureManager.h"
|
||||
|
||||
@interface GameBannerGestureManager ()
|
||||
@property (nonatomic, assign) BOOL isEnabled;
|
||||
@property (nonatomic, strong) UISwipeGestureRecognizer *swipeGesture;
|
||||
@end
|
||||
|
||||
@implementation GameBannerGestureManager
|
||||
|
||||
- (void)enableGestureForGameMode {
|
||||
if (self.isEnabled) return;
|
||||
|
||||
[self cleanupAllGestures];
|
||||
|
||||
// 创建新的 swipe 手势
|
||||
self.swipeGesture = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleSwipe)];
|
||||
|
||||
// 配置手势识别器,确保不拦截触摸事件
|
||||
self.swipeGesture.delaysTouchesBegan = NO;
|
||||
self.swipeGesture.delaysTouchesEnded = NO;
|
||||
self.swipeGesture.cancelsTouchesInView = NO;
|
||||
self.swipeGesture.requiresExclusiveTouchType = NO;
|
||||
|
||||
// 设置 swipe 方向
|
||||
if (isMSRTL()) {
|
||||
self.swipeGesture.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
} else {
|
||||
self.swipeGesture.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
}
|
||||
|
||||
[self.bannerContainer addGestureRecognizer:self.swipeGesture];
|
||||
_isEnabled = YES;
|
||||
|
||||
NSLog(@"🎮 GameBannerGestureManager: swipe 手势已启用");
|
||||
}
|
||||
|
||||
- (void)disableGestureForGameMode {
|
||||
if (!self.isEnabled) return;
|
||||
|
||||
[self cleanupAllGestures];
|
||||
_isEnabled = NO;
|
||||
|
||||
NSLog(@"🎮 GameBannerGestureManager: swipe 手势已禁用");
|
||||
}
|
||||
|
||||
- (void)cleanupAllGestures {
|
||||
if (self.swipeGesture) {
|
||||
[self.bannerContainer removeGestureRecognizer:self.swipeGesture];
|
||||
self.swipeGesture = nil;
|
||||
}
|
||||
|
||||
// 清理可能存在的其他手势识别器
|
||||
NSArray *gestures = [self.bannerContainer.gestureRecognizers copy];
|
||||
for (UIGestureRecognizer *gesture in gestures) {
|
||||
if ([gesture isKindOfClass:[UISwipeGestureRecognizer class]]) {
|
||||
[self.bannerContainer removeGestureRecognizer:gesture];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)resetState {
|
||||
[self cleanupAllGestures];
|
||||
_isEnabled = NO;
|
||||
}
|
||||
|
||||
- (void)handleSwipe {
|
||||
NSLog(@"🎮 GameBannerGestureManager: 检测到 swipe 手势,发送 banner 移除通知");
|
||||
|
||||
// 检查当前是否有可见的 banner
|
||||
UIView *currentVisibleBanner = nil;
|
||||
for (UIView *subview in self.bannerContainer.subviews) {
|
||||
if (!subview.hidden && subview.alpha > 0.01) {
|
||||
currentVisibleBanner = subview;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentVisibleBanner) {
|
||||
NSLog(@"🎮 当前有可见 banner: %@,发送 SwipeOutBanner 通知", NSStringFromClass([currentVisibleBanner class]));
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"SwipeOutBanner"
|
||||
object:currentVisibleBanner];
|
||||
} else {
|
||||
NSLog(@"🎮 当前没有可见 banner,忽略 swipe 手势");
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@@ -23,6 +23,8 @@
|
||||
@property (nonatomic, copy) NSString *currentUserId;
|
||||
@property (nonatomic, strong) UserInfoModel *userInfo;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@implementation PublicRoomManager
|
||||
@@ -217,6 +219,7 @@
|
||||
NSMutableDictionary *ext = [NSMutableDictionary dictionaryWithObject:extModel.model2dictionary
|
||||
forKey:[NSString stringWithFormat:@"%ld", self.userInfo.uid]];
|
||||
request.roomExt = [ext toJSONString];
|
||||
request.retryCount = 3;
|
||||
|
||||
// 进入房间
|
||||
@kWeakify(self);
|
||||
@@ -373,37 +376,59 @@
|
||||
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
|
||||
// 只处理公共房间的消息
|
||||
for (NIMMessage *message in messages) {
|
||||
if ([message.session.sessionId isEqualToString:self.currentPublicRoomId]) {
|
||||
[self handleMessageWithAttachmentAndFirstSecond:message];
|
||||
}
|
||||
/*
|
||||
if (message.session.sessionType == NIMSessionTypeChatroom) {
|
||||
NSString *sessionId = message.session.sessionId;
|
||||
if ([sessionId isEqualToString:self.currentPublicRoomId]) {
|
||||
NIMMessageChatroomExtension *messageExt = (NIMMessageChatroomExtension *)message.messageExt;
|
||||
|
||||
AttachmentModel *attachment;
|
||||
if (message.messageType == NIMMessageTypeCustom) {
|
||||
NIMCustomObject *obj = (NIMCustomObject *) message.messageObject;
|
||||
attachment = (AttachmentModel *) obj.attachment;
|
||||
if (attachment) {
|
||||
switch (attachment.first) {
|
||||
case CustomMessageType_Super_Gift:
|
||||
[self handleFirst_106:attachment
|
||||
message:message];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (attachment.first > 0 && attachment.second >0) {
|
||||
attachment.isFromPublic = YES;
|
||||
[self handleMessageWithAttachmentAndFirstSecond:message];
|
||||
}
|
||||
// if (attachment) {
|
||||
// switch (attachment.first) {
|
||||
// case CustomMessageType_Super_Gift:
|
||||
// [self handleFirst_106:attachment
|
||||
// message:message];
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// NIMMessageChatroomExtension *messageExt = (NIMMessageChatroomExtension *)message.messageExt;
|
||||
// NSLog(@"PublicRoomManager: 收到公共房间消息: %@\n%@",
|
||||
// message.rawAttachContent,
|
||||
// messageExt.roomExt);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleMessageWithAttachmentAndFirstSecond:(NIMMessage *)message {
|
||||
// 只有用户在房间时,才会转发
|
||||
if (![XPSkillCardPlayerManager shareInstance].isInRoom) {
|
||||
NSLog(@"PublicRoomManager: 用户未在房间中,跳过消息转发");
|
||||
return;
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"MessageFromPublicRoomWithAttachmentNotification"
|
||||
object:message];
|
||||
}
|
||||
|
||||
- (void)handleFirst_106:(AttachmentModel *)attachment
|
||||
message:(NIMMessage *)message {
|
||||
|
||||
// allRoomMsg
|
||||
|
||||
// 只有用户在房间时,才会转发
|
||||
if (![XPSkillCardPlayerManager shareInstance].isInRoom) {
|
||||
NSLog(@"PublicRoomManager: 用户未在房间中,跳过消息转发");
|
||||
|
@@ -30,7 +30,7 @@
|
||||
|
||||
+ (BravoGiftWinningFlagViewModel *)display:(UIView *)superView with:(AttachmentModel *)attachment roomID:(NSInteger)roomID uID:(NSString *)UID {
|
||||
BravoGiftWinningFlagViewModel *model = [BravoGiftWinningFlagViewModel modelWithJSON:attachment.data];
|
||||
if (model.roomId != roomID || model.uid != UID.integerValue || model.tip == nil) {
|
||||
if (model.roomId != roomID || model.uid != UID.integerValue || !model.tip) {
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,27 @@
|
||||
flagView.model = model;
|
||||
flagView.alpha = 0;
|
||||
flagView.frame = CGRectMake(0, 0, kGetScaleWidth(274), kGetScaleWidth(216));
|
||||
flagView.center = CGPointMake(superView.center.x, kGetScaleWidth(216) + kGetScaleWidth(216)/2 - 40);
|
||||
|
||||
// 🔧 修复:重新计算位置,确保视图在屏幕可见范围内
|
||||
CGFloat viewWidth = kGetScaleWidth(274);
|
||||
CGFloat viewHeight = kGetScaleWidth(216);
|
||||
CGFloat screenWidth = KScreenWidth;
|
||||
CGFloat screenHeight = KScreenHeight;
|
||||
|
||||
// 计算安全的 Y 位置:屏幕高度的 40% 位置
|
||||
CGFloat safeY = screenHeight * 0.4;
|
||||
|
||||
// 确保视图不会超出屏幕边界
|
||||
CGFloat maxY = screenHeight - viewHeight/2 - 20; // 留20点边距
|
||||
CGFloat minY = viewHeight/2 + 20; // 留20点边距
|
||||
CGFloat finalY = MAX(minY, MIN(safeY, maxY));
|
||||
|
||||
flagView.center = CGPointMake(screenWidth/2, finalY);
|
||||
flagView.transform = CGAffineTransformMakeScale(0.1, 0.1);
|
||||
|
||||
NSLog(@"🎯 BravoGiftWinningFlagView: 位置计算 - screenSize: %.0fx%.0f, viewSize: %.0fx%.0f, center: (%.0f, %.0f)",
|
||||
screenWidth, screenHeight, viewWidth, viewHeight, flagView.center.x, flagView.center.y);
|
||||
|
||||
[superView addSubview:flagView];
|
||||
|
||||
// 使用弹簧动画执行放大动画,alpha从0变为1,带有弹性效果
|
||||
|
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@class UIView;
|
||||
@class GiftReceiveInfoModel;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -52,6 +53,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)enqueueGift:(GiftReceiveInfoModel *)giftInfo;
|
||||
- (void)startGiftQueue;
|
||||
- (void)stopGiftQueue;
|
||||
|
||||
// 🔧 新增:Combo状态管理方法
|
||||
- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid;
|
||||
- (void)clearUserComboState:(NSString *)uid;
|
||||
- (void)updateUserGiftTime:(NSString *)uid;
|
||||
- (void)cleanupExpiredStates;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
@@ -20,6 +20,11 @@
|
||||
|
||||
@property (nonatomic, strong) GiftAnimationHelper *animationHelper;
|
||||
|
||||
// 🔧 新增:Combo状态管理属性
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *userComboStates;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSDate *> *userLastGiftTime;
|
||||
@property (nonatomic, assign) NSTimeInterval comboTimeWindow;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GiftAnimationManager
|
||||
@@ -32,6 +37,11 @@
|
||||
dispatch_source_cancel(_giftTimer);
|
||||
_giftTimer = nil;
|
||||
}
|
||||
|
||||
// 🔧 新增:清理combo状态管理
|
||||
[self cleanupExpiredStates];
|
||||
[self.userComboStates removeAllObjects];
|
||||
[self.userLastGiftTime removeAllObjects];
|
||||
}
|
||||
|
||||
- (instancetype)initWithContainerView:(UIView *)containerView {
|
||||
@@ -44,6 +54,11 @@
|
||||
_comboAnimationDelay = 0.2;
|
||||
_standardAnimationDelay = 0.3;
|
||||
_queue = dispatch_queue_create("com.GiftAnimationManager.queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
// 🔧 新增:初始化Combo状态管理属性
|
||||
_userComboStates = [NSMutableDictionary dictionary];
|
||||
_userLastGiftTime = [NSMutableDictionary dictionary];
|
||||
_comboTimeWindow = 2.0; // 2秒combo窗口
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -245,8 +260,31 @@
|
||||
|
||||
// Helper methods
|
||||
- (BOOL)shouldUseComboAnimationForSender:(NSString *)uid {
|
||||
return [[GiftComboManager sharedManager] isActive] &&
|
||||
[uid isEqualToString:[AccountInfoStorage instance].getUid];
|
||||
if (!uid || uid.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 优先使用精确状态判断
|
||||
BOOL isUserInCombo = [self.userComboStates[uid] boolValue];
|
||||
if (isUserInCombo) {
|
||||
BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid];
|
||||
NSLog(@"[Combo effect] 🎯 用户 %@ 处于combo状态,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO");
|
||||
return isCurrentUser;
|
||||
}
|
||||
|
||||
// 兜底:时间窗口判断
|
||||
NSDate *lastGiftTime = self.userLastGiftTime[uid];
|
||||
if (lastGiftTime) {
|
||||
NSTimeInterval timeSinceLastGift = [[NSDate date] timeIntervalSinceDate:lastGiftTime];
|
||||
if (timeSinceLastGift <= self.comboTimeWindow) {
|
||||
BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid];
|
||||
NSLog(@"[Combo effect] 🎯 用户 %@ 在时间窗口内,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO");
|
||||
return isCurrentUser;
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"[Combo effect] 🎯 用户 %@ 不使用combo动画", uid);
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CGPoint)fallbackPointForEndPoint:(BOOL)isEndPoint {
|
||||
@@ -268,4 +306,51 @@
|
||||
KScreenHeight - kSafeAreaBottomHeight - kGetScaleWidth(140));
|
||||
}
|
||||
|
||||
// 🔧 新增:Combo状态管理方法实现
|
||||
|
||||
- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid {
|
||||
if (!uid || uid.length == 0) {
|
||||
NSLog(@"[Combo effect] ⚠️ 用户ID为空,无法设置combo状态");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCombo) {
|
||||
self.userComboStates[uid] = @(YES);
|
||||
NSLog(@"[Combo effect] ✅ 设置用户 %@ 为combo状态", uid);
|
||||
} else {
|
||||
[self.userComboStates removeObjectForKey:uid];
|
||||
NSLog(@"[Combo effect] 🔄 清除用户 %@ 的combo状态", uid);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clearUserComboState:(NSString *)uid {
|
||||
[self setUserComboState:NO forUser:uid];
|
||||
}
|
||||
|
||||
- (void)updateUserGiftTime:(NSString *)uid {
|
||||
if (!uid || uid.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.userLastGiftTime[uid] = [NSDate date];
|
||||
NSLog(@"[Combo effect] ⏰ 更新用户 %@ 的送礼时间", uid);
|
||||
}
|
||||
|
||||
- (void)cleanupExpiredStates {
|
||||
NSDate *now = [NSDate date];
|
||||
NSMutableArray *expiredUsers = [NSMutableArray array];
|
||||
|
||||
[self.userLastGiftTime enumerateKeysAndObjectsUsingBlock:^(NSString *uid, NSDate *lastTime, BOOL *stop) {
|
||||
if ([now timeIntervalSinceDate:lastTime] > self.comboTimeWindow * 2) {
|
||||
[expiredUsers addObject:uid];
|
||||
}
|
||||
}];
|
||||
|
||||
for (NSString *uid in expiredUsers) {
|
||||
[self.userLastGiftTime removeObjectForKey:uid];
|
||||
[self.userComboStates removeObjectForKey:uid];
|
||||
NSLog(@"[Combo effect] 🧹 清理过期用户状态: %@", uid);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
@@ -54,8 +54,27 @@
|
||||
winningFlagView.model = model;
|
||||
winningFlagView.alpha = 0;
|
||||
winningFlagView.frame = CGRectMake(0, 0, kGetScaleWidth(162), kGetScaleWidth(162));
|
||||
winningFlagView.center = CGPointMake(superView.center.x, kGetScaleWidth(163) + kGetScaleWidth(162)/2);
|
||||
|
||||
// 🔧 修复:重新计算位置,确保视图在屏幕可见范围内
|
||||
CGFloat viewWidth = kGetScaleWidth(162);
|
||||
CGFloat viewHeight = kGetScaleWidth(162);
|
||||
CGFloat screenWidth = KScreenWidth;
|
||||
CGFloat screenHeight = KScreenHeight;
|
||||
|
||||
// 计算安全的 Y 位置:屏幕高度的 35% 位置
|
||||
CGFloat safeY = screenHeight * 0.35;
|
||||
|
||||
// 确保视图不会超出屏幕边界
|
||||
CGFloat maxY = screenHeight - viewHeight/2 - 20; // 留20点边距
|
||||
CGFloat minY = viewHeight/2 + 20; // 留20点边距
|
||||
CGFloat finalY = MAX(minY, MIN(safeY, maxY));
|
||||
|
||||
winningFlagView.center = CGPointMake(screenWidth/2, finalY);
|
||||
winningFlagView.transform = CGAffineTransformMakeScale(0.1, 0.1);
|
||||
|
||||
NSLog(@"🎯 LuckyGiftWinningFlagView: 位置计算 - screenSize: %.0fx%.0f, viewSize: %.0fx%.0f, center: (%.0f, %.0f)",
|
||||
screenWidth, screenHeight, viewWidth, viewHeight, winningFlagView.center.x, winningFlagView.center.y);
|
||||
|
||||
[superView addSubview:winningFlagView];
|
||||
|
||||
// 使用弹簧动画执行放大动画,alpha从0变为1,带有弹性效果
|
||||
|
@@ -79,6 +79,7 @@
|
||||
#import "MSRoomOnLineView.h"
|
||||
|
||||
#import "BannerScheduler.h"
|
||||
#import "GameBannerGestureManager.h"
|
||||
|
||||
static const CGFloat kTipViewStayDuration = 3.0;
|
||||
//static const CGFloat kTipViewMoveDuration = 0.5;
|
||||
@@ -176,6 +177,12 @@ BannerSchedulerDelegate
|
||||
// Banner 手势相关属性
|
||||
@property(nonatomic, assign) CGPoint savedTapPoint;
|
||||
@property(nonatomic, assign) BOOL hasSavedTapPoint;
|
||||
|
||||
@property (nonatomic, strong) GameBannerGestureManager *gameGestureManager;
|
||||
@property (nonatomic, assign) BOOL isGameModeActive;
|
||||
|
||||
@property (nonatomic, strong) UISwipeGestureRecognizer *swipeWhenPlayGame;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RoomAnimationView
|
||||
@@ -193,55 +200,29 @@ BannerSchedulerDelegate
|
||||
|
||||
- (void)removeItSelf {
|
||||
NSLog(@"<22><> RoomAnimationView: 开始销毁");
|
||||
|
||||
// 🔧 新增:取消所有动画,防止异步回调
|
||||
[self cancelAllAnimations];
|
||||
|
||||
// 移除广播代理
|
||||
[[NIMSDK sharedSDK].broadcastManager removeDelegate:self];
|
||||
|
||||
// 取消所有延迟执行
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self];
|
||||
|
||||
// 清理所有 POP 动画
|
||||
[self pop_removeAllAnimations];
|
||||
|
||||
// 清理定时器
|
||||
if (_giftEffectTimer && !dispatch_source_testcancel(_giftEffectTimer)) {
|
||||
dispatch_source_cancel(_giftEffectTimer);
|
||||
_giftEffectTimer = nil;
|
||||
}
|
||||
|
||||
// 清理队列
|
||||
if (_giftEffectsQueue) {
|
||||
self.giftEffectsQueue = NULL;
|
||||
}
|
||||
|
||||
// <EFBFBD><EFBFBD> 新增:清理所有 banner 视图,防止 block 强引用
|
||||
[self cleanupAllSubviews];
|
||||
|
||||
// 🔧 新增:清理 BannerScheduler
|
||||
if (self.bannerScheduler) {
|
||||
NSLog(@"<22><> 清理 BannerScheduler");
|
||||
[self.bannerScheduler clearQueue];
|
||||
[self.bannerScheduler pause];
|
||||
self.bannerScheduler = nil;
|
||||
}
|
||||
|
||||
// 清理手势识别器
|
||||
[self cleanupGestureRecognizers];
|
||||
|
||||
// 清理缓存管理器
|
||||
[self cleanupCacheManagers];
|
||||
|
||||
// 移除通知监听
|
||||
[self removeNotificationObservers];
|
||||
|
||||
NSLog(@"<22><> RoomAnimationView: 销毁完成");
|
||||
[self smartDestroy];
|
||||
}
|
||||
|
||||
- (void)cancelAllAnimations {
|
||||
NSLog(@" 取消所有动画");
|
||||
NSLog(@"<EFBFBD><EFBFBD> 取消所有动画");
|
||||
|
||||
// <EFBFBD><EFBFBD> 新增:检查是否有正在播放的重要动画
|
||||
BOOL hasImportantAnimation = [self hasImportantAnimationPlaying];
|
||||
|
||||
if (hasImportantAnimation) {
|
||||
NSLog(@"⚠️ 检测到重要动画正在播放,延迟清理");
|
||||
// 延迟清理,给动画完成的时间
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self forceCancelAllAnimations];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
[self forceCancelAllAnimations];
|
||||
}
|
||||
|
||||
- (void)forceCancelAllAnimations {
|
||||
NSLog(@"🔄 强制取消所有动画");
|
||||
|
||||
// 取消所有 POP 动画
|
||||
[self pop_removeAllAnimations];
|
||||
@@ -262,7 +243,25 @@ BannerSchedulerDelegate
|
||||
}
|
||||
|
||||
- (void)cleanupAllSubviews {
|
||||
NSLog(@" 清理所有子视图");
|
||||
NSLog(@"🔄 清理所有子视图");
|
||||
|
||||
// <EFBFBD><EFBFBD> 新增:检查是否有正在播放的重要动画
|
||||
BOOL hasImportantAnimation = [self hasImportantAnimationPlaying];
|
||||
|
||||
if (hasImportantAnimation) {
|
||||
NSLog(@"⚠️ 检测到重要动画正在播放,延迟清理子视图");
|
||||
// 延迟清理,给动画完成的时间
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self forceCleanupAllSubviews];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
[self forceCleanupAllSubviews];
|
||||
}
|
||||
|
||||
- (void)forceCleanupAllSubviews {
|
||||
NSLog(@"<22><> 强制清理所有子视图");
|
||||
|
||||
// 清理所有容器的子视图
|
||||
NSArray *containers = @[self.bannerContainer, self.topContainer, self.middleContainer, self.bottomContainer];
|
||||
@@ -273,7 +272,7 @@ BannerSchedulerDelegate
|
||||
NSMutableArray *viewsToRemove = [NSMutableArray array];
|
||||
for (UIView *subview in container.subviews) {
|
||||
[viewsToRemove addObject:subview];
|
||||
NSLog(@" 标记移除视图: %@ (从容器: %@)", NSStringFromClass([subview class]), NSStringFromClass([container class]));
|
||||
NSLog(@"<EFBFBD><EFBFBD> 标记移除视图: %@ (从容器: %@)", NSStringFromClass([subview class]), NSStringFromClass([container class]));
|
||||
}
|
||||
|
||||
for (UIView *view in viewsToRemove) {
|
||||
@@ -282,7 +281,7 @@ BannerSchedulerDelegate
|
||||
|
||||
// 清理视图上的手势识别器
|
||||
if (view.gestureRecognizers.count > 0) {
|
||||
NSLog(@" 清理视图 %@ 上的 %lu 个手势识别器", NSStringFromClass([view class]), (unsigned long)view.gestureRecognizers.count);
|
||||
NSLog(@"<EFBFBD><EFBFBD> 清理视图 %@ 上的 %lu 个手势识别器", NSStringFromClass([view class]), (unsigned long)view.gestureRecognizers.count);
|
||||
for (UIGestureRecognizer *gesture in view.gestureRecognizers.copy) {
|
||||
[view removeGestureRecognizer:gesture];
|
||||
}
|
||||
@@ -360,6 +359,28 @@ BannerSchedulerDelegate
|
||||
[self setupOldMethods];
|
||||
|
||||
[self addBnnerContainGesture];
|
||||
|
||||
// 🔧 新增:监听房间类型变化
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleRoomTypeChanged:)
|
||||
name:@"RoomTypeChanged"
|
||||
object:nil];
|
||||
|
||||
// 🔧 新增:监听combo状态变化
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleComboStateChanged:)
|
||||
name:@"GiftComboStateChanged"
|
||||
object:nil];
|
||||
|
||||
// 🎮 新增:初始化小游戏手势管理器
|
||||
self.gameGestureManager = [[GameBannerGestureManager alloc] init];
|
||||
self.gameGestureManager.bannerContainer = self.bannerContainer;
|
||||
|
||||
// 🎮 新增:监听房间类型变化
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleRoomTypeChanged:)
|
||||
name:@"RoomTypeChanged"
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
@@ -790,6 +811,12 @@ BannerSchedulerDelegate
|
||||
NSLog(@"🔄 BravoGiftBannerView complete 回调被调用");
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
|
||||
// 🔧 新增:重置触摸状态
|
||||
[self resetTouchState];
|
||||
} exitCurrentRoom:^{
|
||||
@kStrongify(self);
|
||||
if (!self || !self.superview) {
|
||||
@@ -817,6 +844,10 @@ BannerSchedulerDelegate
|
||||
NSLog(@"🔄 LuckyPackageBannerView complete 回调被调用");
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
} exitCurrentRoom:^{
|
||||
@kStrongify(self);
|
||||
[self.hostDelegate exitRoom];
|
||||
@@ -842,6 +873,10 @@ BannerSchedulerDelegate
|
||||
NSLog(@"🔄 RoomHighValueGiftBannerAnimation complete 回调被调用");
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -857,6 +892,10 @@ BannerSchedulerDelegate
|
||||
@kStrongify(self);
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -869,6 +908,10 @@ BannerSchedulerDelegate
|
||||
NSLog(@"🔄 CPGiftBanner complete 回调被调用");
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -880,6 +923,10 @@ BannerSchedulerDelegate
|
||||
@kStrongify(self);
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -1043,6 +1090,10 @@ BannerSchedulerDelegate
|
||||
NSLog(@"🔄 LuckyGiftWinningBannerView complete 回调被调用");
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
} exitCurrentRoom:^{
|
||||
@kStrongify(self);
|
||||
[self.hostDelegate exitRoom];
|
||||
@@ -1082,6 +1133,10 @@ BannerSchedulerDelegate
|
||||
NSLog(@"🔄 GameUniversalBannerView complete 回调被调用");
|
||||
self.isRoomBannerV2Displaying = NO;
|
||||
[self.bannerScheduler markBannerFinished];
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
} goToGame:^(NSInteger gameID) {
|
||||
@kStrongify(self);
|
||||
NSArray *baishunList = [self.hostDelegate getPlayList];
|
||||
@@ -1203,6 +1258,9 @@ BannerSchedulerDelegate
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔧 新增:更新用户送礼时间
|
||||
[self.giftAnimationManager updateUserGiftTime:receiveInfo.uid];
|
||||
|
||||
GiftInfoModel *giftInfo = receiveInfo.gift;
|
||||
if (attachment.second == Custom_Message_Sub_AllMicroLuckySend ||
|
||||
attachment.second == Custom_Message_Sub_AllBatchMicroLuckySend ||
|
||||
@@ -1241,9 +1299,7 @@ BannerSchedulerDelegate
|
||||
if (receiveInfo.isHomeShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 处理 combo
|
||||
[[GiftComboManager sharedManager] receiveGiftInfoForDisplayComboFlags:receiveInfo
|
||||
container:self];
|
||||
@@ -2396,7 +2452,28 @@ BannerSchedulerDelegate
|
||||
- (void)handleBannerTap:(UITapGestureRecognizer *)tapGesture {
|
||||
CGPoint tapPoint = [tapGesture locationInView:self.bannerContainer];
|
||||
|
||||
// 检查当前显示的 banner 是否在 tap 位置可以响应事件
|
||||
// 🔧 新增:检查是否有可见的 banner
|
||||
BOOL hasVisibleBanner = NO;
|
||||
for (UIView *subview in self.bannerContainer.subviews) {
|
||||
if (!subview.hidden && subview.alpha > 0.01) {
|
||||
hasVisibleBanner = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有可见的 banner,直接转发点击事件到下层
|
||||
if (!hasVisibleBanner) {
|
||||
NSLog(@"🎯 没有可见 banner,直接转发点击事件到下层");
|
||||
self.savedTapPoint = tapPoint;
|
||||
self.hasSavedTapPoint = YES;
|
||||
CGPoint screenPoint = [self.bannerContainer convertPoint:tapPoint toView:nil];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"BannerTapToFunctionContainer"
|
||||
object:nil
|
||||
userInfo:@{@"point": [NSValue valueWithCGPoint:screenPoint]}];
|
||||
return;
|
||||
}
|
||||
|
||||
// 有可见 banner 时的原有逻辑
|
||||
if ([self isPointInBannerInteractiveArea:tapPoint]) {
|
||||
// banner 可以响应,不处理,让 banner 继续原有逻辑
|
||||
NSLog(@"🎯 Banner tap 位置在可交互区域,banner 将处理此事件");
|
||||
@@ -2410,7 +2487,6 @@ BannerSchedulerDelegate
|
||||
NSLog(@"💾 Banner tap 位置不在可交互区域,已保存位置: %@", NSStringFromCGPoint(tapPoint));
|
||||
// 将 bannerContainer 中的点转换为屏幕坐标系
|
||||
CGPoint screenPoint = [self.bannerContainer convertPoint:tapPoint toView:nil];
|
||||
// UIView *tappedView = tapGesture.view;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"BannerTapToFunctionContainer"
|
||||
object:nil
|
||||
userInfo:@{@"point": [NSValue valueWithCGPoint:screenPoint]}];
|
||||
@@ -3727,6 +3803,9 @@ BannerSchedulerDelegate
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"TapBanner" object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"BannerTapToFunctionContainer" object:nil];
|
||||
|
||||
// 🔧 新增:移除房间类型变化通知监听
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"RoomTypeChanged" object:nil];
|
||||
|
||||
// 移除其他可能添加的通知监听
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
@@ -3753,6 +3832,10 @@ BannerSchedulerDelegate
|
||||
- (void)bannerSchedulerDidFinishPlaying:(BannerScheduler *)scheduler {
|
||||
// Banner 播放完成,可以在这里进行清理工作
|
||||
NSLog(@"🔄 BannerScheduler: Banner 播放完成");
|
||||
|
||||
// 🔧 新增:确保手势容器状态正确
|
||||
[self ensureBannerGestureContainersEnabled];
|
||||
[self resetTouchState];
|
||||
}
|
||||
|
||||
- (void)bannerScheduler:(BannerScheduler *)scheduler didStartPlayingBanner:(id)banner {
|
||||
@@ -3823,4 +3906,208 @@ BannerSchedulerDelegate
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 房间类型手势容器控制
|
||||
|
||||
// 处理房间类型变化
|
||||
- (void)handleRoomTypeChanged:(NSNotification *)notification {
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
NSInteger roomType = [userInfo[@"roomType"] integerValue];
|
||||
|
||||
if (roomType == RoomType_MiniGame) {
|
||||
NSLog(@" RoomAnimationView: 检测到小游戏房间,设置 banner 手势穿透模式 + 启用 swipe 手势");
|
||||
[self setBannerGesturePassthroughMode];
|
||||
self.isGameModeActive = YES;
|
||||
} else {
|
||||
NSLog(@" RoomAnimationView: 检测到非小游戏房间,恢复 banner 手势正常模式 + 移除 swipe 手势");
|
||||
[self restoreBannerGestureNormalMode];
|
||||
self.isGameModeActive = NO;
|
||||
}
|
||||
}
|
||||
|
||||
// 重构手势穿透模式设置
|
||||
- (void)setBannerGesturePassthroughMode {
|
||||
// 隐藏和禁用所有手势容器
|
||||
[self hideAllGestureContainers];
|
||||
|
||||
// 完全禁用 bannerContainer 的用户交互
|
||||
self.bannerContainer.userInteractionEnabled = YES;
|
||||
|
||||
// 启用小游戏手势管理器(包含 swipe 手势)
|
||||
[self.gameGestureManager enableGestureForGameMode];
|
||||
|
||||
NSLog(@"<22><> Banner 手势已设置为穿透模式,swipe 手势已启用");
|
||||
}
|
||||
|
||||
// 重构手势正常模式恢复
|
||||
- (void)restoreBannerGestureNormalMode {
|
||||
// 显示和启用所有手势容器
|
||||
[self showAllGestureContainers];
|
||||
|
||||
// 恢复 bannerContainer 的用户交互
|
||||
self.bannerContainer.userInteractionEnabled = YES;
|
||||
|
||||
// 禁用小游戏手势管理器(移除 swipe 手势)
|
||||
[self.gameGestureManager disableGestureForGameMode];
|
||||
|
||||
NSLog(@"🎮 Banner 手势已恢复正常模式,swipe 手势已移除");
|
||||
}
|
||||
|
||||
- (void)hideAllGestureContainers {
|
||||
self.bannerSwipeGestureContainer.hidden = YES;
|
||||
self.bannerLeftTapGestureContainer.hidden = YES;
|
||||
self.bannerRightTapGestureContainer.hidden = YES;
|
||||
|
||||
self.bannerSwipeGestureContainer.userInteractionEnabled = NO;
|
||||
self.bannerLeftTapGestureContainer.userInteractionEnabled = NO;
|
||||
self.bannerRightTapGestureContainer.userInteractionEnabled = NO;
|
||||
}
|
||||
|
||||
- (void)showAllGestureContainers {
|
||||
self.bannerSwipeGestureContainer.hidden = NO;
|
||||
self.bannerLeftTapGestureContainer.hidden = NO;
|
||||
self.bannerRightTapGestureContainer.hidden = NO;
|
||||
|
||||
self.bannerSwipeGestureContainer.userInteractionEnabled = YES;
|
||||
self.bannerLeftTapGestureContainer.userInteractionEnabled = YES;
|
||||
self.bannerRightTapGestureContainer.userInteractionEnabled = YES;
|
||||
}
|
||||
|
||||
// 优化手势状态确保方法
|
||||
- (void)ensureBannerGestureContainersEnabled {
|
||||
if (self.isGameModeActive) {
|
||||
// 小游戏模式下,确保手势管理器正常工作(包含 swipe 手势)
|
||||
if (!self.gameGestureManager.isEnabled) {
|
||||
NSLog(@"🔧 检测到小游戏手势管理器未启用,重新启用(包含 swipe 手势)");
|
||||
[self.gameGestureManager enableGestureForGameMode];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 非小游戏模式下的原有逻辑(不包含 swipe 手势)
|
||||
[self showAllGestureContainers];
|
||||
|
||||
// 确保小游戏手势管理器已禁用
|
||||
if (self.gameGestureManager.isEnabled) {
|
||||
NSLog(@"<22><> 检测到小游戏手势管理器仍启用,禁用(移除 swipe 手势)");
|
||||
[self.gameGestureManager disableGestureForGameMode];
|
||||
}
|
||||
}
|
||||
|
||||
// 优化触摸状态重置
|
||||
- (void)resetTouchState {
|
||||
self.savedTapPoint = CGPointZero;
|
||||
self.hasSavedTapPoint = NO;
|
||||
|
||||
if (self.isGameModeActive) {
|
||||
[self.gameGestureManager resetState];
|
||||
[self.gameGestureManager enableGestureForGameMode];
|
||||
NSLog(@" 小游戏模式:触摸状态已重置,swipe 手势已重新启用");
|
||||
} else {
|
||||
NSLog(@" 非小游戏模式:触摸状态已重置,swipe 手势保持禁用");
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 新增:处理combo状态变化
|
||||
- (void)handleComboStateChanged:(NSNotification *)notification {
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
NSString *uid = userInfo[@"uid"];
|
||||
BOOL isCombo = [userInfo[@"isCombo"] boolValue];
|
||||
|
||||
NSLog(@"[Combo effect] 🔔 收到combo状态变化通知 - 用户: %@, 状态: %@", uid, isCombo ? @"YES" : @"NO");
|
||||
|
||||
// 通知动画管理器更新combo状态
|
||||
if (isCombo) {
|
||||
[self.giftAnimationManager setUserComboState:YES forUser:uid];
|
||||
} else {
|
||||
[self.giftAnimationManager clearUserComboState:uid];
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 新增:检查重要动画状态
|
||||
- (BOOL)hasImportantAnimationPlaying {
|
||||
// 检查 topContainer 中是否有正在播放的重要动画
|
||||
for (UIView *subview in self.topContainer.subviews) {
|
||||
if ([subview isKindOfClass:[BravoGiftWinningFlagView class]] ||
|
||||
[subview isKindOfClass:[LuckyGiftWinningFlagView class]]) {
|
||||
NSLog(@"🎯 检测到重要动画正在播放: %@", NSStringFromClass([subview class]));
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 middleContainer 中是否有正在播放的礼物动画
|
||||
for (UIView *subview in self.middleContainer.subviews) {
|
||||
if ([subview isKindOfClass:[SVGAImageView class]] ||
|
||||
[subview isKindOfClass:[VAPView class]] ||
|
||||
[subview isKindOfClass:[PAGView class]]) {
|
||||
NSLog(@"🎯 检测到礼物动画正在播放: %@", NSStringFromClass([subview class]));
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// <EFBFBD><EFBFBD> 新增:智能销毁方法
|
||||
- (void)smartDestroy {
|
||||
// 检查是否有正在播放的重要动画
|
||||
BOOL hasImportantAnimation = [self hasImportantAnimationPlaying];
|
||||
|
||||
if (hasImportantAnimation) {
|
||||
NSLog(@"⚠️ 检测到重要动画正在播放,延迟销毁");
|
||||
// 延迟销毁,给动画完成的时间
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self forceDestroy];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
[self forceDestroy];
|
||||
}
|
||||
|
||||
- (void)forceDestroy {
|
||||
NSLog(@"<22><> RoomAnimationView: 强制销毁");
|
||||
|
||||
// 移除广播代理
|
||||
[[NIMSDK sharedSDK].broadcastManager removeDelegate:self];
|
||||
|
||||
// 取消所有延迟执行
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self];
|
||||
|
||||
// 清理所有 POP 动画
|
||||
[self pop_removeAllAnimations];
|
||||
|
||||
// 清理定时器
|
||||
if (_giftEffectTimer && !dispatch_source_testcancel(_giftEffectTimer)) {
|
||||
dispatch_source_cancel(_giftEffectTimer);
|
||||
_giftEffectTimer = nil;
|
||||
}
|
||||
|
||||
// 清理队列
|
||||
if (_giftEffectsQueue) {
|
||||
self.giftEffectsQueue = NULL;
|
||||
}
|
||||
|
||||
// 清理所有 banner 视图,防止 block 强引用
|
||||
[self cleanupAllSubviews];
|
||||
|
||||
// 清理 BannerScheduler
|
||||
if (self.bannerScheduler) {
|
||||
NSLog(@"<22><> 清理 BannerScheduler");
|
||||
[self.bannerScheduler clearQueue];
|
||||
[self.bannerScheduler pause];
|
||||
self.bannerScheduler = nil;
|
||||
}
|
||||
|
||||
// 清理手势识别器
|
||||
[self cleanupGestureRecognizers];
|
||||
|
||||
// 清理缓存管理器
|
||||
[self cleanupCacheManagers];
|
||||
|
||||
// 移除通知监听
|
||||
[self removeNotificationObservers];
|
||||
|
||||
NSLog(@"<22><> RoomAnimationView: 销毁完成");
|
||||
}
|
||||
|
||||
@end
|
||||
|
@@ -29,6 +29,8 @@
|
||||
@property (nonatomic,strong) UITapGestureRecognizer *tapEmptyRecognizer;
|
||||
|
||||
@property (nonatomic, strong) UIView *bottomSpace;
|
||||
/// 是否正在动画中,用于避免动画期间的布局更新
|
||||
@property (nonatomic, assign) BOOL isAnimating;
|
||||
|
||||
@end
|
||||
|
||||
@@ -153,26 +155,42 @@
|
||||
|
||||
#pragma mark - Getters And Setters
|
||||
- (void)setMessageInfo:(XPMessageInfoModel *)messageInfo {
|
||||
// 更严格的比较,减少不必要的更新
|
||||
// 更严格的比较,避免不必要的更新
|
||||
if (_messageInfo &&
|
||||
[messageInfo.content isEqualToAttributedString:_messageInfo.content] &&
|
||||
[messageInfo.bubbleImageUrl isEqualToString:_messageInfo.bubbleImageUrl] &&
|
||||
[messageInfo.boomImageUrl isEqualToString:_messageInfo.boomImageUrl]) {
|
||||
[messageInfo.boomImageUrl isEqualToString:_messageInfo.boomImageUrl] &&
|
||||
messageInfo.first == _messageInfo.first &&
|
||||
messageInfo.second == _messageInfo.second) {
|
||||
return;
|
||||
}
|
||||
|
||||
_messageInfo = messageInfo;
|
||||
if (messageInfo) {
|
||||
// 确保在设置attributedText之前先设置hasBubble属性
|
||||
BOOL hasBubble = ![NSString isEmpty:messageInfo.bubbleImageUrl];
|
||||
if (self.contentLabel.hasBubble != hasBubble) {
|
||||
self.contentLabel.hasBubble = hasBubble;
|
||||
// 如果正在动画中,延迟更新以避免闪烁
|
||||
if (self.isAnimating) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self setMessageInfo:messageInfo];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.contentLabel.attributedText = messageInfo.content;
|
||||
// 批量更新,减少布局触发次数,使用 performWithoutAnimation 避免布局动画
|
||||
[UIView performWithoutAnimation:^{
|
||||
// 确保在设置attributedText之前先设置hasBubble属性
|
||||
BOOL hasBubble = ![NSString isEmpty:messageInfo.bubbleImageUrl];
|
||||
if (self.contentLabel.hasBubble != hasBubble) {
|
||||
self.contentLabel.hasBubble = hasBubble;
|
||||
}
|
||||
|
||||
self.contentLabel.attributedText = messageInfo.content;
|
||||
}];
|
||||
|
||||
// 延迟图片加载,避免与礼物动画冲突
|
||||
if (self.isLeftBigImage && messageInfo.boomImageUrl) {
|
||||
self.leftBigImageView.imageUrl = messageInfo.boomImageUrl;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
self.leftBigImageView.imageUrl = messageInfo.boomImageUrl;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,9 +208,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
[self.contentLabel mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.mas_equalTo(size.width);
|
||||
}];
|
||||
// 使用更平滑的布局更新,减少闪烁
|
||||
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
[self.contentLabel mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.mas_equalTo(size.width);
|
||||
}];
|
||||
[self layoutIfNeeded];
|
||||
} completion:nil];
|
||||
|
||||
// [self.bubbleImageView mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
// if (size.width + 32 >= kRoomMessageMaxWidth) {
|
||||
@@ -206,13 +228,17 @@
|
||||
|
||||
if (hasBubble) {
|
||||
@kWeakify(self);
|
||||
[self.bubbleImageView loadImageWithUrl:self.messageInfo.bubbleImageUrl
|
||||
completion:^(UIImage * _Nonnull image, NSURL * _Nonnull url) {
|
||||
@kStrongify(self);
|
||||
UIImage *image1 = [UIImage imageWithCGImage:image.CGImage scale:2.0 orientation:UIImageOrientationUp];
|
||||
UIImage *cutImage = [image1 cropRightAndBottomPixels:2];
|
||||
self.bubbleImageView.image = [self resizableImage:cutImage];
|
||||
}];
|
||||
// 延迟图片加载,避免与动画冲突
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self.bubbleImageView loadImageWithUrl:self.messageInfo.bubbleImageUrl
|
||||
completion:^(UIImage * _Nonnull image, NSURL * _Nonnull url) {
|
||||
@kStrongify(self);
|
||||
UIImage *image1 = [UIImage imageWithCGImage:image.CGImage scale:2.0 orientation:UIImageOrientationUp];
|
||||
UIImage *cutImage = [image1 cropRightAndBottomPixels:2];
|
||||
self.bubbleImageView.image = [self resizableImage:cutImage];
|
||||
}];
|
||||
});
|
||||
|
||||
[self.bubbleImageView mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(self.contentLabel).insets(UIEdgeInsetsMake(-10, -10, -10, -10));
|
||||
}];
|
||||
|
@@ -133,8 +133,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
/// 更新其他 tag 的数据源,若传入空数组,则初始化并从 datasource 中获取数据
|
||||
- (void)updateAllDataSource:(NSArray *)datas {
|
||||
if (!datas || datas.count == 0) {
|
||||
self.datasource_chat = @[].mutableCopy;
|
||||
self.datasource_gift = @[].mutableCopy;
|
||||
// 清空分类数据源
|
||||
[self.datasource_chat removeAllObjects];
|
||||
[self.datasource_gift removeAllObjects];
|
||||
// 从主数据源重新构建分类数据源
|
||||
datas = self.datasource;
|
||||
}
|
||||
|
||||
@@ -152,6 +154,8 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
case CustomMessageType_Treasure_Fairy:
|
||||
[self.datasource_gift addObject:model];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,8 +182,6 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
- (void)appendAndScrollToAtUser {
|
||||
// 1. 检查 incomingMessages 是否为空
|
||||
if (self.incomingMessages.count < 1) {
|
||||
NSInteger rows = self.datasource.count;
|
||||
|
||||
// 2. 安全检查 locationArray 是否为空
|
||||
if (self.locationArray.count == 0) {
|
||||
[self scrollToBottomWithTipsHidden:YES];
|
||||
@@ -193,15 +195,22 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
return;
|
||||
}
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
||||
if (rows > indexPath.row) {
|
||||
[self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
if (rows == indexPath.row + 1) {
|
||||
self.messageTipsBtn.hidden = YES;
|
||||
self.isPending = NO;
|
||||
}
|
||||
} else {
|
||||
// 将 datasource 的索引转换为当前显示数据源的索引
|
||||
NSInteger convertedIndex = [self convertDataSourceIndexToCurrentDisplayIndex:index];
|
||||
if (convertedIndex == NSNotFound) {
|
||||
[self scrollToBottomWithTipsHidden:YES];
|
||||
} else {
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:convertedIndex inSection:0];
|
||||
NSInteger currentRows = [self getCurrentDataSourceCount];
|
||||
if (currentRows > indexPath.row) {
|
||||
[self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
if (currentRows == indexPath.row + 1) {
|
||||
self.messageTipsBtn.hidden = YES;
|
||||
self.isPending = NO;
|
||||
}
|
||||
} else {
|
||||
[self scrollToBottomWithTipsHidden:YES];
|
||||
}
|
||||
}
|
||||
|
||||
[self safelyRemoveLocationAtIndex:0];
|
||||
@@ -217,8 +226,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
needReloadData = YES; // 标记需要重新加载数据
|
||||
}
|
||||
|
||||
// 5. 插入新消息
|
||||
NSMutableArray *indexPaths = @[].mutableCopy;
|
||||
// 5. 在更新数据源之前获取当前行数
|
||||
NSInteger currentRows = [self getCurrentDataSourceCount];
|
||||
|
||||
// 6. 插入新消息
|
||||
NSMutableArray *tempNewDatas = @[].mutableCopy;
|
||||
for (id item in self.incomingMessages) {
|
||||
XPMessageInfoModel *model = [self parseMessage:item];
|
||||
@@ -226,18 +237,36 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
|
||||
[tempNewDatas addObject:model];
|
||||
[self.datasource addObject:model];
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:self.datasource.count - 1 inSection:0]];
|
||||
|
||||
[self processAtMentionsForMessage:item];
|
||||
}
|
||||
[self updateAllDataSource:tempNewDatas];
|
||||
[self.incomingMessages removeAllObjects];
|
||||
|
||||
// 如果有删除操作,使用 reloadData;否则使用增量更新
|
||||
// 7. 更新 UITableView
|
||||
if (needReloadData) {
|
||||
[self.messageTableView reloadData];
|
||||
} else if (indexPaths.count > 0) {
|
||||
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
|
||||
} else if (tempNewDatas.count > 0) {
|
||||
// 安全检查:确保数据源一致性
|
||||
NSInteger expectedRows = [self getCurrentDataSourceCount];
|
||||
if (expectedRows != [self.messageTableView numberOfRowsInSection:0]) {
|
||||
[self.messageTableView reloadData];
|
||||
} else {
|
||||
// 重新计算 indexPath,使用更新前的行数作为起始索引
|
||||
NSMutableArray *indexPaths = @[].mutableCopy;
|
||||
NSInteger startIndex = currentRows;
|
||||
if (startIndex >= 0 && startIndex <= [self.messageTableView numberOfRowsInSection:0]) {
|
||||
for (NSInteger i = 0; i < tempNewDatas.count; i++) {
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]];
|
||||
}
|
||||
|
||||
// 使用更平滑的动画效果,减少与礼物动画的视觉冲突
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
|
||||
}];
|
||||
} else {
|
||||
[self.messageTableView reloadData];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 滚动到指定位置或底部
|
||||
@@ -245,9 +274,11 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
}
|
||||
|
||||
- (void)scrollToBottomWithTipsHidden:(BOOL)hidden {
|
||||
NSInteger rows = self.datasource.count;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(rows - 1) inSection:0];
|
||||
[self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
NSInteger rows = [self getCurrentDataSourceCount];
|
||||
if (rows > 0) {
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(rows - 1) inSection:0];
|
||||
[self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
}
|
||||
self.messageTipsBtn.hidden = hidden;
|
||||
self.isPending = NO;
|
||||
self.atCount = 0;
|
||||
@@ -296,9 +327,32 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
|
||||
- (void)safelyRemoveMessages:(NSInteger)count {
|
||||
if (self.datasource.count >= count) {
|
||||
// 获取要删除的消息
|
||||
NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, count)];
|
||||
NSArray *removedMessages = [self.datasource objectsAtIndexes:set];
|
||||
|
||||
// 从主数据源删除
|
||||
[self.datasource removeObjectsAtIndexes:set];
|
||||
[self updateAllDataSource:nil];
|
||||
|
||||
// 从分类数据源中删除对应的消息
|
||||
for (XPMessageInfoModel *removedModel in removedMessages) {
|
||||
switch (removedModel.first) {
|
||||
case NIMMessageTypeText:
|
||||
case CustomMessageType_Face:
|
||||
[self.datasource_chat removeObject:removedModel];
|
||||
break;
|
||||
case CustomMessageType_Gift:
|
||||
case CustomMessageType_RoomBoom:
|
||||
case CustomMessageType_Candy_Tree:
|
||||
case CustomMessageType_Super_Gift:
|
||||
case CustomMessageType_AllMicroSend:
|
||||
case CustomMessageType_Treasure_Fairy:
|
||||
[self.datasource_gift removeObject:removedModel];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 locationArray
|
||||
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
|
||||
@@ -336,18 +390,98 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否为礼物消息
|
||||
- (BOOL)isGiftMessage:(id)messageData {
|
||||
if ([messageData isKindOfClass:[NIMMessage class]]) {
|
||||
NIMMessage *message = (NIMMessage *)messageData;
|
||||
if ([message.messageObject isKindOfClass:[NIMCustomObject class]]) {
|
||||
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
if (obj.attachment && [obj.attachment isKindOfClass:[AttachmentModel class]]) {
|
||||
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
|
||||
return (attachment.first == CustomMessageType_Gift ||
|
||||
attachment.first == CustomMessageType_AllMicroSend ||
|
||||
attachment.first == CustomMessageType_Super_Gift);
|
||||
}
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSInteger)getCurrentDataSourceCount {
|
||||
NSInteger count = 0;
|
||||
switch (self.displayType) {
|
||||
case 1:
|
||||
count = self.datasource.count;
|
||||
break;
|
||||
case 2:
|
||||
count = self.datasource_chat.count;
|
||||
break;
|
||||
case 3:
|
||||
count = self.datasource_gift.count;
|
||||
break;
|
||||
default:
|
||||
count = self.datasource.count;
|
||||
break;
|
||||
}
|
||||
// 确保返回非负数
|
||||
return MAX(0, count);
|
||||
}
|
||||
|
||||
- (NSInteger)convertDataSourceIndexToCurrentDisplayIndex:(NSInteger)dataSourceIndex {
|
||||
if (self.displayType == 1) {
|
||||
return dataSourceIndex;
|
||||
}
|
||||
|
||||
if (dataSourceIndex >= self.datasource.count) {
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
XPMessageInfoModel *targetModel = [self.datasource objectAtIndex:dataSourceIndex];
|
||||
if (!targetModel) {
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
NSArray *currentDataSource = nil;
|
||||
switch (self.displayType) {
|
||||
case 2:
|
||||
currentDataSource = self.datasource_chat;
|
||||
break;
|
||||
case 3:
|
||||
currentDataSource = self.datasource_gift;
|
||||
break;
|
||||
default:
|
||||
return dataSourceIndex;
|
||||
}
|
||||
|
||||
// 在当前数据源中查找对应的模型
|
||||
for (NSInteger i = 0; i < currentDataSource.count; i++) {
|
||||
XPMessageInfoModel *model = [currentDataSource objectAtIndex:i];
|
||||
if ([model isEqual:targetModel]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
- (void)scrollToFirstLocationOrBottom {
|
||||
NSInteger rows = self.datasource.count;
|
||||
if (self.locationArray.count == 0) {
|
||||
[self scrollToBottomWithTipsHidden:YES];
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger index = [self safeGetIndexFromLocationArrayAt:0];
|
||||
if (index == NSNotFound || index >= rows) {
|
||||
if (index == NSNotFound) {
|
||||
[self scrollToBottomWithTipsHidden:YES];
|
||||
return;
|
||||
}
|
||||
|
||||
// 将 datasource 的索引转换为当前显示数据源的索引
|
||||
NSInteger convertedIndex = [self convertDataSourceIndexToCurrentDisplayIndex:index];
|
||||
if (convertedIndex == NSNotFound) {
|
||||
[self scrollToBottomWithTipsHidden:YES];
|
||||
} else {
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:convertedIndex inSection:0];
|
||||
[self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
[self safelyRemoveLocationAtIndex:0];
|
||||
}
|
||||
@@ -459,7 +593,15 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
self.messageTipsBtn.hidden = NO;
|
||||
[self findAtMeNumber];
|
||||
} else {
|
||||
[self appendAndScrollToBottom];
|
||||
// 对于礼物消息,使用更平滑的更新策略
|
||||
if ([self isGiftMessage:messageData]) {
|
||||
// 稍微延迟处理礼物消息,让动画先执行
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self appendAndScrollToBottom];
|
||||
});
|
||||
} else {
|
||||
[self appendAndScrollToBottom];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,14 +615,15 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
|
||||
BOOL needReloadData = NO;
|
||||
if (self.datasource.count > kRoomMessageMaxLength) {
|
||||
NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, kRoomMessageMaxLength/2)];
|
||||
NSArray *needRemoveMsgArray = [self.datasource objectsAtIndexes:set];
|
||||
[self.datasource removeObjectsInArray:needRemoveMsgArray];
|
||||
NSInteger removedCount = kRoomMessageMaxLength / 2;
|
||||
[self safelyRemoveMessages:removedCount];
|
||||
needReloadData = YES; // 标记需要重新加载数据
|
||||
}
|
||||
|
||||
// 在更新数据源之前获取当前行数
|
||||
NSInteger currentRows = [self getCurrentDataSourceCount];
|
||||
|
||||
NSMutableArray *tempArray = @[].mutableCopy;
|
||||
NSMutableArray *indexPaths = @[].mutableCopy;
|
||||
@kWeakify(self);
|
||||
[self.incomingMessages enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
@kStrongify(self);
|
||||
@@ -493,7 +636,6 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
if (model) {
|
||||
[tempArray addObject:model];
|
||||
[self.datasource addObject:model];
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:self.datasource.count - 1 inSection:0]];
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -504,12 +646,35 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
// 如果有删除操作,使用 reloadData;否则使用增量更新
|
||||
if (needReloadData) {
|
||||
[self.messageTableView reloadData];
|
||||
} else if (indexPaths.count > 0) {
|
||||
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
|
||||
} else if (tempArray.count > 0) {
|
||||
// 安全检查:确保数据源一致性
|
||||
NSInteger expectedRows = [self getCurrentDataSourceCount];
|
||||
if (expectedRows != [self.messageTableView numberOfRowsInSection:0]) {
|
||||
[self.messageTableView reloadData];
|
||||
} else {
|
||||
// 重新计算 indexPath,使用更新前的行数作为起始索引
|
||||
NSMutableArray *indexPaths = @[].mutableCopy;
|
||||
NSInteger startIndex = currentRows;
|
||||
if (startIndex >= 0 && startIndex <= [self.messageTableView numberOfRowsInSection:0]) {
|
||||
for (NSInteger i = 0; i < tempArray.count; i++) {
|
||||
[indexPaths addObject:[NSIndexPath indexPathForRow:startIndex + i inSection:0]];
|
||||
}
|
||||
|
||||
// 使用更平滑的动画效果,减少与礼物动画的视觉冲突
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
[self.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
|
||||
}];
|
||||
} else {
|
||||
[self.messageTableView reloadData];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//执行插入动画并滚动
|
||||
[self scrollToBottom:NO];
|
||||
// 延迟滚动执行,避免与礼物动画产生视觉冲突
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self scrollToBottom:NO];
|
||||
});
|
||||
}
|
||||
|
||||
///执行插入动画并滚动
|
||||
@@ -540,7 +705,10 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
[self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
});
|
||||
} else {
|
||||
[self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO];
|
||||
// 使用更平滑的滚动动画,减少与礼物动画的视觉冲突
|
||||
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
[self.messageTableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
self.atCount = 0;
|
||||
@@ -1340,21 +1508,23 @@ NSString * const kRoomShowTopicKey = @"kRoomShowTopicKey";
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
NSInteger count = 0;
|
||||
switch (self.displayType) {
|
||||
case 1:
|
||||
return self.datasource.count;
|
||||
count = self.datasource.count;
|
||||
break;
|
||||
case 2:
|
||||
return self.datasource_chat.count;
|
||||
count = self.datasource_chat.count;
|
||||
break;
|
||||
case 3:
|
||||
return self.datasource_gift.count;
|
||||
count = self.datasource_gift.count;
|
||||
break;
|
||||
|
||||
default:
|
||||
return self.datasource.count;
|
||||
count = self.datasource.count;
|
||||
break;
|
||||
}
|
||||
// 确保返回非负数
|
||||
return MAX(0, count);
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
@@ -94,6 +94,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (NSDictionary * _Nonnull)getComboStateInfo;
|
||||
- (void)printComboState;
|
||||
|
||||
// 🔧 新增:状态通知方法
|
||||
- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid;
|
||||
- (void)clearUserComboState:(NSString *)uid;
|
||||
|
||||
#pragma mark - 使用示例
|
||||
|
||||
/*
|
||||
|
@@ -11,6 +11,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Bugly/Bugly.h>
|
||||
#import "BuglyManager.h"
|
||||
|
||||
#import "Api+Gift.h"
|
||||
|
||||
@@ -790,19 +791,17 @@ NSString * const kBoomStateForceResetNotification = @"BoomStateForceResetNotific
|
||||
self.errorMessage = [NSString stringWithFormat:@"服务器繁忙,请稍后重试 - %@", msg];
|
||||
#else
|
||||
self.errorMessage = @"Over Heat & try later";
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
NSMutableDictionary *logDic = [@{@"targetUids": allUIDs,
|
||||
@"giftNum": self.giftNumPerTimes} mutableCopy];
|
||||
[logDic addEntriesFromDictionary:dic];
|
||||
[logDic setObject:[NSThread callStackSymbols] forKey:@"call stack symbols"];
|
||||
[logDic setObject:msg forKey:@"error message"];
|
||||
[logDic setObject:@"gift/sendV5" forKey:@"http method"];
|
||||
[Bugly reportError:[NSError errorWithDomain:[NSString stringWithFormat:@"UID: %@,API: %@ 异常",
|
||||
[AccountInfoStorage instance].getUid,
|
||||
@"gift/sendV5"]
|
||||
code:code
|
||||
userInfo:logDic]];
|
||||
});
|
||||
// 使用 BuglyManager 统一上报礼物发送错误
|
||||
NSString *uid = [AccountInfoStorage instance].getUid ?: @"未知用户";
|
||||
NSMutableDictionary *userInfo = [@{@"targetUids": allUIDs,
|
||||
@"giftNum": self.giftNumPerTimes} mutableCopy];
|
||||
[userInfo addEntriesFromDictionary:dic];
|
||||
[userInfo setObject:msg forKey:@"error message"];
|
||||
|
||||
[[BuglyManager sharedManager] reportNetworkError:uid
|
||||
api:@"gift/sendV5"
|
||||
code:code
|
||||
userInfo:userInfo];
|
||||
#endif
|
||||
// 临时错误,不重置连击状态,允许用户重试
|
||||
} else if (code == 31005) {
|
||||
@@ -959,4 +958,27 @@ NSString * const kBoomStateForceResetNotification = @"BoomStateForceResetNotific
|
||||
self.timer ? @"运行中" : @"已停止");
|
||||
}
|
||||
|
||||
// 🔧 新增:状态通知方法实现
|
||||
|
||||
- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid {
|
||||
if (!uid || uid.length == 0) {
|
||||
NSLog(@"[Combo effect] ⚠️ 用户ID为空,无法设置combo状态");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[Combo effect] 🔔 通知动画管理器设置用户 %@ 的combo状态为: %@", uid, isCombo ? @"YES" : @"NO");
|
||||
|
||||
// 通过通知中心通知动画管理器
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"GiftComboStateChanged"
|
||||
object:nil
|
||||
userInfo:@{
|
||||
@"uid": uid,
|
||||
@"isCombo": @(isCombo)
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)clearUserComboState:(NSString *)uid {
|
||||
[self setUserComboState:NO forUser:uid];
|
||||
}
|
||||
|
||||
@end
|
||||
|
@@ -20,6 +20,7 @@
|
||||
///P
|
||||
#import "XPGiftProtocol.h"
|
||||
#import <Bugly/Bugly.h>
|
||||
#import "BuglyManager.h"
|
||||
|
||||
@interface XPGiftPresenter ()
|
||||
///
|
||||
|
@@ -74,7 +74,11 @@
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
self.feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy];
|
||||
// 🔥 修复:正确初始化震动反馈
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy];
|
||||
[self.feedbackGenerator prepare]; // 🔥 关键:调用prepare方法
|
||||
}
|
||||
self.updateGoldQueue = @[].mutableCopy;
|
||||
|
||||
|
||||
@@ -299,10 +303,26 @@
|
||||
NSLog(@"[Combo effect] ⚠️ 点击间隔过短,忽略此次点击");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 修复:将震动反馈移到方法开始处,确保及时响应
|
||||
if (@available(iOS 10.0, *)) {
|
||||
if (self.feedbackGenerator) {
|
||||
@try {
|
||||
[self.feedbackGenerator impactOccurred];
|
||||
NSLog(@"[Combo effect] 📳 震动反馈已触发");
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"[Combo effect] ⚠️ 震动反馈失败: %@", exception.reason);
|
||||
}
|
||||
} else {
|
||||
NSLog(@"[Combo effect] ⚠️ 震动反馈生成器未初始化");
|
||||
}
|
||||
} else {
|
||||
NSLog(@"[Combo effect] ⚠️ 设备不支持震动反馈 (iOS < 10.0)");
|
||||
}
|
||||
|
||||
#if RELEASE
|
||||
isHandlingTap = YES;
|
||||
#endif
|
||||
[self.feedbackGenerator impactOccurred];
|
||||
|
||||
NSLog(@"[Combo effect] 👆 连击面板被点击,发送礼物");
|
||||
|
||||
@@ -330,6 +350,16 @@
|
||||
[[GiftComboManager sharedManager] clear];
|
||||
}
|
||||
|
||||
// 🔥 新增:重新准备震动反馈的方法
|
||||
- (void)prepareFeedbackGenerator {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
if (self.feedbackGenerator) {
|
||||
[self.feedbackGenerator prepare];
|
||||
NSLog(@"[Combo effect] 📳 震动反馈已重新准备");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SVGAPlayerDelegate: 当动画播放完毕时调用
|
||||
- (void)svgaPlayerDidFinishedAnimation:(SVGAPlayer *)player {
|
||||
[self.playImageView stepToPercentage:0 andPlay:NO];
|
||||
|
@@ -161,6 +161,13 @@
|
||||
self.isAnimating = NO;
|
||||
self.shouldStopAnimation = NO;
|
||||
|
||||
// 检查数据源
|
||||
if (self.source.count < 2) {
|
||||
NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 数据源不足 (%lu个),动画可能过快", (unsigned long)self.source.count);
|
||||
} else {
|
||||
NSLog(@"✅ PIGiftBravoGiftBroadcastView: 数据源正常 (%lu个)", (unsigned long)self.source.count);
|
||||
}
|
||||
|
||||
@kWeakify(self);
|
||||
[self.source enumerateObjectsUsingBlock:^(BravoGiftTabInfomationModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
@kStrongify(self);
|
||||
@@ -176,10 +183,13 @@
|
||||
|
||||
- (void)startLoop {
|
||||
if (self.isAnimating || self.labels.count == 0 || self.shouldStopAnimation) {
|
||||
NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画启动被阻止 - isAnimating:%d, labelsCount:%lu, shouldStop:%d",
|
||||
self.isAnimating, (unsigned long)self.labels.count, self.shouldStopAnimation);
|
||||
return;
|
||||
}
|
||||
|
||||
self.isAnimating = YES;
|
||||
NSLog(@"🎬 PIGiftBravoGiftBroadcastView: 开始动画循环");
|
||||
[self playCurrentAnimation];
|
||||
}
|
||||
|
||||
@@ -187,21 +197,33 @@
|
||||
- (void)startAnimation {
|
||||
if (self.shouldStopAnimation) {
|
||||
self.shouldStopAnimation = NO;
|
||||
NSLog(@"🔄 PIGiftBravoGiftBroadcastView: 重置停止标志,重新开始动画");
|
||||
}
|
||||
|
||||
if (self.labels.count > 0) {
|
||||
[self startLoop];
|
||||
} else {
|
||||
NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 无法开始动画,labels为空");
|
||||
}
|
||||
}
|
||||
|
||||
// 公共方法:停止动画循环
|
||||
- (void)stopAnimation {
|
||||
NSLog(@"⏹️ PIGiftBravoGiftBroadcastView: 停止动画");
|
||||
[self endloop];
|
||||
}
|
||||
|
||||
- (void)playCurrentAnimation {
|
||||
// 更严格的状态检查
|
||||
if (self.shouldStopAnimation) {
|
||||
self.isAnimating = NO;
|
||||
NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画被停止标志阻止");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保只有一个动画在运行
|
||||
if (self.container.subviews.count > 0) {
|
||||
NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 容器中已有视图,跳过当前动画");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,9 +231,12 @@
|
||||
PIGiftBravoGiftBroadcastItemView *item = [self.labels xpSafeObjectAtIndex:self.index];
|
||||
if (!item) {
|
||||
self.isAnimating = NO;
|
||||
NSLog(@"❌ PIGiftBravoGiftBroadcastView: 无法获取当前索引(%ld)的视图", (long)self.index);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"🎭 PIGiftBravoGiftBroadcastView: 播放第%ld个动画项", (long)self.index);
|
||||
|
||||
// 设置初始位置(屏幕右侧)
|
||||
item.frame = CGRectMake(KScreenWidth, 2, KScreenWidth - 25 - 42, 26);
|
||||
// 添加到容器视图
|
||||
@@ -226,11 +251,12 @@
|
||||
if (!finished || self.shouldStopAnimation) {
|
||||
[item removeFromSuperview];
|
||||
self.isAnimating = NO;
|
||||
NSLog(@"❌ PIGiftBravoGiftBroadcastView: 入场动画被中断");
|
||||
return;
|
||||
}
|
||||
|
||||
// 停留时间:2秒延迟后执行出场动画
|
||||
[UIView animateWithDuration:0.5 delay:2.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
// 停留时间:从2秒增加到3秒
|
||||
[UIView animateWithDuration:0.5 delay:3.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
item.frame = CGRectMake(-KScreenWidth, 2, KScreenWidth - 25 - 42, 26);
|
||||
} completion:^(BOOL finished) {
|
||||
@kStrongify(self);
|
||||
@@ -239,6 +265,7 @@
|
||||
|
||||
if (self.shouldStopAnimation) {
|
||||
self.isAnimating = NO;
|
||||
NSLog(@"⏹️ PIGiftBravoGiftBroadcastView: 出场动画被停止");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -246,10 +273,12 @@
|
||||
self.index += 1;
|
||||
if (self.index >= self.labels.count) {
|
||||
self.index = 0;
|
||||
NSLog(@"🔄 PIGiftBravoGiftBroadcastView: 动画循环重置到开始");
|
||||
}
|
||||
|
||||
// 使用定时器延迟下一个动画,避免递归调用
|
||||
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO];
|
||||
// 使用定时器延迟下一个动画,从0.1秒增加到0.5秒
|
||||
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO];
|
||||
NSLog(@"⏰ PIGiftBravoGiftBroadcastView: 安排下一个动画,延迟0.5秒");
|
||||
}];
|
||||
}];
|
||||
}
|
||||
@@ -260,9 +289,11 @@
|
||||
|
||||
if (self.shouldStopAnimation) {
|
||||
self.isAnimating = NO;
|
||||
NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 定时器回调被停止标志阻止");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"▶️ PIGiftBravoGiftBroadcastView: 定时器触发下一个动画");
|
||||
[self playCurrentAnimation];
|
||||
}
|
||||
|
||||
@@ -271,6 +302,8 @@
|
||||
self.shouldStopAnimation = YES;
|
||||
self.isAnimating = NO;
|
||||
|
||||
NSLog(@"🛑 PIGiftBravoGiftBroadcastView: 结束动画循环");
|
||||
|
||||
// 停止并清理定时器
|
||||
if (self.animationTimer) {
|
||||
[self.animationTimer invalidate];
|
||||
|
@@ -161,5 +161,30 @@ UIKIT_EXTERN NSString * const kRoomRoomLittleGameMiniStageNotificationKey;
|
||||
return _scrollView;
|
||||
}
|
||||
|
||||
#pragma mark - 触摸事件日志
|
||||
|
||||
// 触摸开始
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
NSLog(@"🎯 LittleGameScrollStageView: touchesBegan 被调用");
|
||||
[super touchesBegan:touches withEvent:event];
|
||||
}
|
||||
|
||||
// 触摸移动
|
||||
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
NSLog(@"🎯 LittleGameScrollStageView: touchesMoved 被调用");
|
||||
[super touchesMoved:touches withEvent:event];
|
||||
}
|
||||
|
||||
// 触摸结束
|
||||
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
NSLog(@"🎯 LittleGameScrollStageView: touchesEnded 被调用");
|
||||
[super touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
// 触摸取消
|
||||
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
NSLog(@"🎯 LittleGameScrollStageView: touchesCancelled 被调用");
|
||||
[super touchesCancelled:touches withEvent:event];
|
||||
}
|
||||
|
||||
@end
|
||||
|
60
YuMi/Modules/YMRoom/View/StageViewManager.h
Normal file
60
YuMi/Modules/YMRoom/View/StageViewManager.h
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// StageViewManager.h
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2024/12/19.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "RoomInfoModel.h"
|
||||
#import "RoomHostDelegate.h"
|
||||
|
||||
@class StageView;
|
||||
|
||||
|
||||
// 使用 RoomInfoModel 中已有的 RoomType 定义
|
||||
// 避免重复定义,保持代码一致性
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface StageViewManager : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) StageView *currentStageView;
|
||||
@property (nonatomic,
|
||||
assign,
|
||||
readonly) RoomType currentRoomType;
|
||||
|
||||
/**
|
||||
更新 stageView 到指定房间类型
|
||||
@param roomType 房间类型
|
||||
@param roomInfo 房间信息
|
||||
@param container 容器视图
|
||||
@param delegate stageView 的代理对象
|
||||
@return 是否更新成功
|
||||
*/
|
||||
- (BOOL)updateStageViewForRoomType:(RoomType)roomType
|
||||
roomInfo:(RoomInfoModel *)roomInfo
|
||||
container:(UIView *)container
|
||||
delegate:(id<RoomHostDelegate>)delegate;
|
||||
|
||||
/**
|
||||
通知 stageView 更新
|
||||
@param roomInfo 房间信息
|
||||
*/
|
||||
- (void)notifyStageViewUpdate:(RoomInfoModel *)roomInfo;
|
||||
|
||||
/**
|
||||
清理资源
|
||||
*/
|
||||
- (void)cleanup;
|
||||
|
||||
/**
|
||||
检查是否需要切换 stageView 类型
|
||||
@param newType 新的房间类型
|
||||
@return 是否需要切换
|
||||
*/
|
||||
- (BOOL)shouldChangeStageViewType:(RoomType)newType;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
145
YuMi/Modules/YMRoom/View/StageViewManager.m
Normal file
145
YuMi/Modules/YMRoom/View/StageViewManager.m
Normal file
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// StageViewManager.m
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2024/12/19.
|
||||
//
|
||||
|
||||
#import "StageViewManager.h"
|
||||
#import "RoomInfoModel.h"
|
||||
#import "SocialStageView.h"
|
||||
#import "TenMicStageView.h"
|
||||
#import "FifteenMicStageView.h"
|
||||
#import "NineteenMicStageView.h"
|
||||
#import "TwentyMicStageView.h"
|
||||
#import "DatingStageView.h"
|
||||
#import "AnchorStageView.h"
|
||||
#import "AnchorPkStageView.h"
|
||||
#import "LittleGameStageView.h"
|
||||
#import "LittleGameScrollStageView.h"
|
||||
|
||||
|
||||
@interface StageViewManager ()
|
||||
|
||||
@property (nonatomic, strong) StageView *currentStageView;
|
||||
@property (nonatomic, assign) RoomType currentRoomType;
|
||||
@property (nonatomic, weak) UIView *container;
|
||||
|
||||
@end
|
||||
|
||||
@implementation StageViewManager
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_currentRoomType = RoomType_Game; // RoomType_Game 默认值
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)updateStageViewForRoomType:(RoomType)roomType
|
||||
roomInfo:(id)roomInfo
|
||||
container:(UIView *)container
|
||||
delegate:(id<RoomHostDelegate>)delegate {
|
||||
|
||||
// 检查是否需要切换 stageView 类型
|
||||
if ([self shouldChangeStageViewType:roomType]) {
|
||||
NSLog(@"🔄 StageViewManager: 切换 stageView 类型 | 从 %ld 到 %ld",
|
||||
(long)self.currentRoomType, (long)roomType);
|
||||
|
||||
// 清理旧的 stageView
|
||||
[self cleanup];
|
||||
|
||||
// 创建新的 stageView
|
||||
[self createStageViewForType:roomType withDelegate:delegate];
|
||||
self.currentRoomType = roomType;
|
||||
self.container = container;
|
||||
}
|
||||
|
||||
// 确保 stageView 已添加到容器
|
||||
// if (self.currentStageView && !self.currentStageView.superview && container) {
|
||||
// [container insertSubview:self.currentStageView atIndex:0];
|
||||
// NSLog(@"✅ StageViewManager: stageView 已添加到容器");
|
||||
// }
|
||||
|
||||
// 返回是否成功创建了 stageView
|
||||
return self.currentStageView != nil;
|
||||
}
|
||||
|
||||
- (void)notifyStageViewUpdate:(id)roomInfo {
|
||||
if (self.currentStageView && [self.currentStageView respondsToSelector:@selector(onRoomUpdate)]) {
|
||||
NSLog(@"🔄 StageViewManager: 通知 stageView 更新 | type=%ld", (long)self.currentRoomType);
|
||||
[self.currentStageView onRoomUpdate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cleanup {
|
||||
if (self.currentStageView) {
|
||||
[self.currentStageView removeFromSuperview];
|
||||
self.currentStageView = nil;
|
||||
NSLog(@"🧹 StageViewManager: 清理 stageView");
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)shouldChangeStageViewType:(RoomType)newType {
|
||||
return self.currentRoomType != newType || !self.currentStageView;
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (void)createStageViewForType:(NSInteger)roomType withDelegate:(id<RoomHostDelegate>)delegate {
|
||||
// 根据房间类型创建对应的 stageView
|
||||
// 注意:所有 stageView 都需要 delegate 来正确初始化
|
||||
|
||||
switch (roomType) {
|
||||
case RoomType_Anchor: { // RoomType_Anchor
|
||||
// 检查是否为 PK 模式
|
||||
// 这里需要根据 roomInfo 来判断,暂时使用默认的 AnchorStageView
|
||||
if ([delegate respondsToSelector:@selector(getRoomInfo)]) {
|
||||
RoomInfoModel *roomInfo = [delegate getRoomInfo];
|
||||
if (roomInfo.roomModeType == RoomModeType_Open_AcrossRoomPK_mode) {
|
||||
self.currentStageView = [[AnchorPKStageView alloc] initWithDelegate:delegate];
|
||||
} else {
|
||||
self.currentStageView = [[AnchorStageView alloc] initWithDelegate:delegate];
|
||||
}
|
||||
} else {
|
||||
self.currentStageView = [[AnchorStageView alloc] initWithDelegate:delegate];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RoomType_MiniGame: { // RoomType_MiniGame
|
||||
self.currentStageView = [[LittleGameScrollStageView alloc] initWithDelegate:delegate];
|
||||
break;
|
||||
}
|
||||
case RoomType_20Mic: { // RoomType_20Mic
|
||||
self.currentStageView = [[TwentyMicStageView alloc] initWithDelegate:delegate];
|
||||
break;
|
||||
}
|
||||
case RoomType_19Mic: { // RoomType_19Mic
|
||||
self.currentStageView = [[NineteenMicStageView alloc] initWithDelegate:delegate];
|
||||
break;
|
||||
}
|
||||
case RoomType_15Mic: { // RoomType_15Mic
|
||||
self.currentStageView = [[FifteenMicStageView alloc] initWithDelegate:delegate];
|
||||
break;
|
||||
}
|
||||
case RoomType_10Mic: { // RoomType_10Mic
|
||||
self.currentStageView = [[TenMicStageView alloc] initWithDelegate:delegate];
|
||||
break;
|
||||
}
|
||||
case RoomType_Game: // RoomType_Game
|
||||
default: {
|
||||
self.currentStageView = [[SocialStageView alloc] initWithDelegate:delegate];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.currentStageView) {
|
||||
NSLog(@"✅ StageViewManager: 创建 stageView 成功 | type=%ld | class=%@",
|
||||
(long)roomType, NSStringFromClass([self.currentStageView class]));
|
||||
} else {
|
||||
NSLog(@"❌ StageViewManager: 创建 stageView 失败 | type=%ld", (long)roomType);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@@ -9,7 +9,7 @@
|
||||
#import "XPMessageRemoteExtModel.h"
|
||||
|
||||
// 公共房间消息转发通知名称
|
||||
UIKIT_EXTERN NSString * const kMessageFromPublicRoomWithAttachmentNotification;
|
||||
UIKIT_EXTERN NSString * _Nullable const kMessageFromPublicRoomWithAttachmentNotification;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@@ -76,6 +76,7 @@
|
||||
#import "LittleGameStageView.h"
|
||||
#import "LittleGameScrollStageView.h"
|
||||
#import "XPRoomLittleGameContainerView.h"
|
||||
#import "StageViewManager.h"
|
||||
#import "PIRoomEnterRedPacketView.h"
|
||||
#import "XPIAPRechargeViewController.h"
|
||||
#import "XPCandyTreeInsufficientBalanceView.h"
|
||||
@@ -127,6 +128,7 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
@property (nonatomic,strong) RoomHeaderView *roomHeaderView;
|
||||
///坑位信息
|
||||
@property (nonatomic,strong) StageView *stageView;
|
||||
@property (nonatomic,strong) StageViewManager *stageViewManager;
|
||||
@property (nonatomic,strong) TenMicStageView *tenMicStageView;
|
||||
@property (nonatomic,strong) FifteenMicStageView *fifteenMicStageView;
|
||||
@property (nonatomic,strong) TwentyMicStageView *twentyMicStageView;
|
||||
@@ -195,6 +197,9 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
/// 上麦请求弹窗定时器,用于10秒后自动移除弹窗
|
||||
@property(nonatomic,strong) NSTimer *upMicAskTimer;
|
||||
|
||||
/// 🔧 修复:保存 block 形式的通知观察者,防止内存泄漏
|
||||
@property(nonatomic,strong) id<NSObject> exchangeRoomAnimationViewObserver;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XPRoomViewController
|
||||
@@ -310,6 +315,12 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
|
||||
[[RoomBoomManager sharedManager] removeEventListenerForTarget:self];
|
||||
|
||||
// 🔧 修复:移除 block 形式的通知观察者
|
||||
if (self.exchangeRoomAnimationViewObserver) {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.exchangeRoomAnimationViewObserver];
|
||||
self.exchangeRoomAnimationViewObserver = nil;
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
// 🔧 修复:清理 RoomAnimationView
|
||||
@@ -366,6 +377,7 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
|
||||
[self handleGiftComboCallBack];
|
||||
|
||||
[self setupStageViewManager];
|
||||
}
|
||||
|
||||
//- (void)test {
|
||||
@@ -451,7 +463,8 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
object:nil];
|
||||
|
||||
@kWeakify(self);
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:@"kExchangeRoomAnimationViewAndGameViewIndex"
|
||||
// 🔧 修复:保存 block 观察者的返回值,防止内存泄漏
|
||||
self.exchangeRoomAnimationViewObserver = [[NSNotificationCenter defaultCenter] addObserverForName:@"kExchangeRoomAnimationViewAndGameViewIndex"
|
||||
object:nil
|
||||
queue:NSOperationQueue.mainQueue
|
||||
usingBlock:^(NSNotification * _Nonnull notification) {
|
||||
@@ -591,7 +604,6 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
self.view.backgroundColor = [UIColor darkGrayColor];
|
||||
[self.view addSubview:self.backContainerView];
|
||||
[self.view addSubview:self.littleGameView];
|
||||
[self.view addSubview:self.stageView];
|
||||
[self.view addSubview:self.messageContainerView];
|
||||
|
||||
[self.view addSubview:self.quickMessageContainerView];
|
||||
@@ -647,14 +659,8 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
make.height.mas_equalTo(kNavigationHeight);
|
||||
}];
|
||||
|
||||
[self.stageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.mas_equalTo(self.view);
|
||||
make.top.mas_equalTo(self.roomHeaderView.mas_bottom);
|
||||
make.height.mas_equalTo(self.stageView.hightForStageView);
|
||||
}];
|
||||
|
||||
[self.messageContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.stageView.mas_bottom);
|
||||
make.top.equalTo(self.roomHeaderView.mas_bottom);
|
||||
make.bottom.equalTo(self.quickMessageContainerView.mas_top).offset(-5);
|
||||
make.leading.equalTo(self.view);
|
||||
make.trailing.equalTo(self.sideMenu.mas_leading).offset(-10);
|
||||
@@ -883,15 +889,42 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
|
||||
- (BOOL)updateStageView {
|
||||
Class stageViewClass = [self stageViewClassForRoomInfo:self.roomInfo];
|
||||
if (stageViewClass) { //&& ![self.stageView isKindOfClass:stageViewClass]) {
|
||||
[self.stageView removeFromSuperview];
|
||||
self.stageView = nil;
|
||||
self.stageView = [[stageViewClass alloc] initWithDelegate:self];
|
||||
self.stageView.alpha = 0;
|
||||
return YES;
|
||||
NSLog(@"🔄 updateStageView: 开始更新 stageView,当前 alpha = %.2f", self.stageView.alpha);
|
||||
|
||||
// 优先使用 StageViewManager 来管理 stageView
|
||||
if (self.stageViewManager) {
|
||||
NSLog(@"🔧 updateStageView: 使用 StageViewManager");
|
||||
BOOL success = [self.stageViewManager updateStageViewForRoomType:self.roomInfo.type
|
||||
roomInfo:self.roomInfo
|
||||
container:self.view
|
||||
delegate:self];
|
||||
if (success) {
|
||||
self.stageView = self.stageViewManager.currentStageView;
|
||||
if (self.stageView) {
|
||||
NSLog(@"🔧 updateStageView: StageViewManager 设置 alpha = 0 (之前 = %.2f)", self.stageView.alpha);
|
||||
self.stageView.alpha = 0;
|
||||
NSLog(@"✅ StageViewManager 成功更新 stageView: %@", NSStringFromClass([self.stageView class]));
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
NSLog(@"⚠️ StageViewManager 更新失败,降级到原有逻辑");
|
||||
}
|
||||
|
||||
// 降级处理:原有逻辑
|
||||
// NSLog(@"🔧 updateStageView: 使用降级逻辑");
|
||||
// Class stageViewClass = [self stageViewClassForRoomInfo:self.roomInfo];
|
||||
// if (stageViewClass) { //&& ![self.stageView isKindOfClass:stageViewClass]) {
|
||||
// NSLog(@"🔧 updateStageView: 创建新的 stageView: %@", NSStringFromClass(stageViewClass));
|
||||
// [self.stageView removeFromSuperview];
|
||||
// self.stageView = nil;
|
||||
// self.stageView = [[stageViewClass alloc] initWithDelegate:self];
|
||||
// NSLog(@"🔧 updateStageView: 降级逻辑设置 alpha = 0 (之前 = %.2f)", self.stageView.alpha);
|
||||
// self.stageView.alpha = 0;
|
||||
// NSLog(@"✅ 降级逻辑成功更新 stageView: %@", NSStringFromClass([self.stageView class]));
|
||||
// return YES;
|
||||
// }
|
||||
|
||||
NSLog(@"❌ 所有 stageView 更新方法都失败了");
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -928,12 +961,22 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
|
||||
- (void)changeStageViewOnRoomUpdate {
|
||||
NSLog(@"🔄 changeStageViewOnRoomUpdate: 开始更新 stageView");
|
||||
|
||||
if (![self updateStageView]) {
|
||||
NSLog(@"❌ changeStageViewOnRoomUpdate: updateStageView 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"✅ changeStageViewOnRoomUpdate: updateStageView 成功,当前 alpha = %.2f", self.stageView.alpha);
|
||||
|
||||
// 🔧 新增:发送房间类型变化通知
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"RoomTypeChanged"
|
||||
object:nil
|
||||
userInfo:@{@"roomType": @(self.roomInfo.type)}];
|
||||
|
||||
if (!self.stageView.superview) {
|
||||
NSLog(@"🔧 changeStageViewOnRoomUpdate: 添加 stageView 到视图层级");
|
||||
[self.view insertSubview:self.stageView
|
||||
belowSubview:self.roomHeaderView];
|
||||
|
||||
@@ -955,7 +998,12 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"🔧 changeStageViewOnRoomUpdate: 设置 stageView.alpha = 1 (之前 = %.2f)", self.stageView.alpha);
|
||||
self.stageView.alpha = 1;
|
||||
NSLog(@"✅ changeStageViewOnRoomUpdate: stageView.alpha 已设置为 1");
|
||||
|
||||
// 调试:检查最终状态
|
||||
[self debugStageViewStatus];
|
||||
|
||||
[self addExitGameButton];
|
||||
|
||||
@@ -1435,11 +1483,15 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
|
||||
- (void)handleRoomWithoutPasswordAfterInitRoom {
|
||||
NSLog(@"🔄 handleRoomWithoutPasswordAfterInitRoom: 开始处理无密码房间");
|
||||
|
||||
self.roomInfo.datingState = (self.roomInfo.roomModeType == RoomModeType_Open_Blind) ?
|
||||
RoomDatingStateChangeType_Open :
|
||||
RoomDatingStateChangeType_Normal;
|
||||
|
||||
NSLog(@"🔧 handleRoomWithoutPasswordAfterInitRoom: 调用 changeStageViewOnRoomUpdate");
|
||||
[self changeStageViewOnRoomUpdate];
|
||||
NSLog(@"✅ handleRoomWithoutPasswordAfterInitRoom: changeStageViewOnRoomUpdate 完成");
|
||||
[self.roomHeaderView onRoomEntered];
|
||||
[self.sideMenu onRoomEntered];
|
||||
|
||||
@@ -1917,30 +1969,47 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
|
||||
if (![message.session.sessionId isEqualToString:@(self.roomInfo.roomId).stringValue]) {
|
||||
NSLog(@"[Recv] ⛔️ 过滤:房间不匹配 | msg.sid=%@ | curRoomId=%@",
|
||||
message.session.sessionId, @(self.roomInfo.roomId).stringValue);
|
||||
continue;
|
||||
// if (message.messageType == NIMMessageTypeCustom) {
|
||||
// NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
// if ([obj.attachment isKindOfClass:[AttachmentModel class]]) {
|
||||
// AttachmentModel *att = (AttachmentModel *)obj.attachment;
|
||||
// if (!att.isFromPublic) {
|
||||
// NSLog(@"[Recv] ⛔️ 过滤:房间不匹配 | msg.sid=%@ | curRoomId=%@",
|
||||
// message.session.sessionId, @(self.roomInfo.roomId).stringValue);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// }else {
|
||||
NSLog(@"[Recv] ⛔️ 过滤:房间不匹配 | msg.sid=%@ | curRoomId=%@",
|
||||
message.session.sessionId, @(self.roomInfo.roomId).stringValue);
|
||||
continue;
|
||||
// }
|
||||
}
|
||||
|
||||
NSLog(@"[Recv] --- Message Raw Attach Content: %@, %@, %ld", @(message.senderClientType), message.rawAttachContent, (long)message.messageType);
|
||||
|
||||
if ([message.rawAttachContent containsString:@"\"allRoomMsg\":1"]) {
|
||||
NSLog(@"[Recv] --- 拦截旧的全房间消息");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.messageType == NIMMessageTypeNotification) {
|
||||
[self handleNIMNotificationTypeMessage:message];
|
||||
} else if (message.messageType == NIMMessageTypeCustom) {
|
||||
// 自定义消息排查日志:first/second/size3
|
||||
//#if DEBUG
|
||||
// if ([message.messageObject isKindOfClass:[NIMCustomObject class]]) {
|
||||
// NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
// if ([obj.attachment isKindOfClass:[AttachmentModel class]]) {
|
||||
// AttachmentModel *att = (AttachmentModel *)obj.attachment;
|
||||
// NSData *payloadJSON = nil;
|
||||
// @try { payloadJSON = [NSJSONSerialization dataWithJSONObject:att.data ?: @{} options:0 error:nil]; } @catch (__unused NSException *e) {}
|
||||
// NSLog(@"[Recv] 🎯 自定义消息 | first=%ld second=%ld | payload=%lub | sid=%@ | ts=%.3f",
|
||||
// (long)att.first, (long)att.second, (unsigned long)payloadJSON.length,
|
||||
// message.session.sessionId, [[NSDate date] timeIntervalSince1970]);
|
||||
// }
|
||||
// }
|
||||
//#endif
|
||||
#if DEBUG
|
||||
if ([message.messageObject isKindOfClass:[NIMCustomObject class]]) {
|
||||
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
if ([obj.attachment isKindOfClass:[AttachmentModel class]]) {
|
||||
AttachmentModel *att = (AttachmentModel *)obj.attachment;
|
||||
NSData *payloadJSON = nil;
|
||||
@try { payloadJSON = [NSJSONSerialization dataWithJSONObject:att.data ?: @{} options:0 error:nil]; } @catch (__unused NSException *e) {}
|
||||
NSLog(@"[Recv] 🎯 自定义消息 | first=%ld second=%ld | payload=%lub | sid=%@ | ts=%.3f",
|
||||
(long)att.first, (long)att.second, (unsigned long)payloadJSON.length,
|
||||
message.session.sessionId, [[NSDate date] timeIntervalSince1970]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
[self handleNimCustomTypeMessage:message];
|
||||
} else if(message.messageType == NIMMessageTypeText) {
|
||||
[self.messageContainerView handleNIMTextMessage:message];
|
||||
@@ -2064,6 +2133,7 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
//房间类型是否变更了(从个播->普通,个播->小游戏等)
|
||||
newRoomInfo.hadChangeRoomType = self.roomInfo.type != newRoomInfo.type;
|
||||
BOOL anchorToOther = newRoomInfo.type != RoomType_Anchor && self.roomInfo.type == RoomType_Anchor;//个播变其他房
|
||||
RoomType currentType = self.roomInfo.type;
|
||||
self.roomInfo = newRoomInfo;
|
||||
|
||||
[self.backContainerView onRoomUpdate];
|
||||
@@ -3118,9 +3188,16 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
|
||||
- (StageView *)stageView {
|
||||
// 🔧 修改:如果 StageViewManager 已初始化,返回其管理的 stageView
|
||||
if (self.stageViewManager && self.stageViewManager.currentStageView) {
|
||||
return self.stageViewManager.currentStageView;
|
||||
}
|
||||
|
||||
// 🔧 降级:只有在 StageViewManager 不可用时才创建默认的 SocialStageView
|
||||
if (!_stageView) {
|
||||
_stageView = [[SocialStageView alloc] initWithDelegate:self];
|
||||
_stageView.alpha = 0;
|
||||
NSLog(@"⚠️ 使用降级 stageView: SocialStageView");
|
||||
}
|
||||
return _stageView;
|
||||
}
|
||||
@@ -3219,9 +3296,59 @@ XPCandyTreeInsufficientBalanceViewDelegate>
|
||||
}
|
||||
|
||||
// 使用现有的消息处理流程
|
||||
[self.messageContainerView handleNIMCustomMessage:message];
|
||||
// [self.messageContainerView handleNIMCustomMessage:message];
|
||||
// [self.animationView handleNIMCustomMessage:message];
|
||||
// [self handleNimCustomTypeMessage:message];
|
||||
// [self onRecvMessages:@[message]];
|
||||
|
||||
|
||||
NSLog(@"XPRoomViewController: 处理公共房间转发的106类型消息");
|
||||
switch (message.messageType) {
|
||||
case NIMMessageTypeNotification:
|
||||
[self handleNIMNotificationTypeMessage:message];
|
||||
break;
|
||||
case NIMMessageTypeCustom:
|
||||
[self handleNimCustomTypeMessage:message];
|
||||
break;
|
||||
case NIMMessageTypeText:
|
||||
[self.messageContainerView handleNIMTextMessage:message];
|
||||
[self.littleGameView handleNIMTextMessage:message];
|
||||
break;
|
||||
case NIMMessageTypeTip:
|
||||
[self.messageContainerView handleNIMTextMessage:message];
|
||||
break;
|
||||
case NIMMessageTypeImage:
|
||||
[self.messageContainerView handleNIMImageMessage:message];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:StageViewManager 懒加载
|
||||
- (StageViewManager *)stageViewManager {
|
||||
if (!_stageViewManager) {
|
||||
_stageViewManager = [[StageViewManager alloc] init];
|
||||
NSLog(@"✅ StageViewManager 懒加载初始化成功");
|
||||
}
|
||||
return _stageViewManager;
|
||||
}
|
||||
|
||||
// 新增:设置 StageViewManager
|
||||
- (void)setupStageViewManager {
|
||||
// 触发懒加载
|
||||
[self stageViewManager];
|
||||
NSLog(@"✅ StageViewManager 设置完成");
|
||||
}
|
||||
|
||||
// 调试方法:检查 stageView 状态
|
||||
- (void)debugStageViewStatus {
|
||||
NSLog(@"🔍 DEBUG: stageView 状态检查");
|
||||
NSLog(@" - stageView: %@", self.stageView);
|
||||
NSLog(@" - stageView.alpha: %.2f", self.stageView.alpha);
|
||||
NSLog(@" - stageView.hidden: %@", self.stageView.hidden ? @"YES" : @"NO");
|
||||
NSLog(@" - stageView.superview: %@", self.stageView.superview);
|
||||
NSLog(@" - stageView.frame: %@", NSStringFromCGRect(self.stageView.frame));
|
||||
NSLog(@" - stageView.class: %@", NSStringFromClass([self.stageView class]));
|
||||
}
|
||||
|
||||
@end
|
||||
|
@@ -140,6 +140,9 @@ UIKIT_EXTERN NSString *kTabShowAnchorCardKey;
|
||||
[[NIMSDK sharedSDK].chatManager removeDelegate:self];
|
||||
[[NIMSDK sharedSDK].systemNotificationManager removeDelegate:self];
|
||||
[[NIMSDK sharedSDK].broadcastManager removeDelegate:self];
|
||||
|
||||
// 🔧 新增:清理 RoomBoomManager 监听器,防止内存泄漏
|
||||
[[RoomBoomManager sharedManager] removeEventListenerForTarget:self];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
@@ -186,10 +189,13 @@ UIKIT_EXTERN NSString *kTabShowAnchorCardKey;
|
||||
|
||||
[[RoomBoomManager sharedManager] registerBoomBanner:^(id _Nonnull sth) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[RoomBoomBannerAnimation display:kWindow
|
||||
with:sth
|
||||
tapToRoom:YES
|
||||
complete:^{}];
|
||||
// 🔧 新增:检查用户是否在房间中,只有在房间中才显示全局火箭升级通知
|
||||
if ([XPSkillCardPlayerManager shareInstance].isInRoom == YES) {
|
||||
[RoomBoomBannerAnimation display:kWindow
|
||||
with:sth
|
||||
tapToRoom:YES
|
||||
complete:^{}];
|
||||
}
|
||||
});
|
||||
} target:self];
|
||||
|
||||
|
@@ -14,6 +14,7 @@
|
||||
#import "MSParamsDecode.h"
|
||||
#import "NSData+GZIP.h"
|
||||
#import <Bugly/Bugly.h>
|
||||
#import "BuglyManager.h"
|
||||
|
||||
@implementation HttpRequestHelper
|
||||
|
||||
@@ -93,7 +94,7 @@
|
||||
editParam = [MSParamsDecode msDecodeParams:editParam];
|
||||
params = [self configBaseParmars:editParam];
|
||||
|
||||
#if DEBUG
|
||||
#if 0
|
||||
// 构建完整的 URL
|
||||
NSString *baseUrl = [HttpRequestHelper getHostUrl];
|
||||
NSString *fullUrl = [NSString stringWithFormat:@"%@/%@", baseUrl, method];
|
||||
@@ -117,7 +118,7 @@
|
||||
@kWeakify(self);
|
||||
[manager GET:method parameters:params headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
|
||||
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
|
||||
#if DEBUG
|
||||
#if 0
|
||||
NSLog(@"%@ - \n%@\n", method, [baseModel toJSONString]);
|
||||
#else
|
||||
#endif
|
||||
@@ -337,7 +338,7 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
|
||||
params = [MSParamsDecode msDecodeParams:[params mutableCopy] ];
|
||||
params = [self configBaseParmars:params];
|
||||
AFHTTPSessionManager *manager = [HttpRequestHelper requestManager];
|
||||
#if DEBUG
|
||||
#if 0
|
||||
// 构建完整的 URL
|
||||
NSString *baseUrl = [HttpRequestHelper getHostUrl];
|
||||
NSString *fullUrl = [NSString stringWithFormat:@"%@/%@", baseUrl, method];
|
||||
@@ -362,7 +363,7 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
|
||||
@kWeakify(self);
|
||||
[manager DELETE:method parameters:params headers:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
|
||||
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
|
||||
#if DEBUG
|
||||
#if 0
|
||||
NSLog(@"\n%@\n", [baseModel toJSONString]);
|
||||
#else
|
||||
#endif
|
||||
@@ -409,18 +410,16 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
|
||||
completion(nil, resCode, message);
|
||||
}
|
||||
if (resCode > 500 && resCode < 600) {
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
NSMutableDictionary *logDic = [params mutableCopy];
|
||||
[logDic setObject:[NSThread callStackSymbols] forKey:@"call stack symbols"];
|
||||
[logDic setObject:message forKey:@"error message"];
|
||||
[logDic setObject:@(method) forKey:@"http method"];
|
||||
// BLYLog(BuglyLogLevelWarn, @"%@", [logDic toJSONString]);
|
||||
[Bugly reportError:[NSError errorWithDomain:[NSString stringWithFormat:@"UID: %@,API: %@ 异常",
|
||||
[AccountInfoStorage instance].getUid,
|
||||
path]
|
||||
code:resCode
|
||||
userInfo:logDic]];
|
||||
});
|
||||
// 使用 BuglyManager 统一上报网络错误
|
||||
NSString *uid = [AccountInfoStorage instance].getUid ?: @"未知用户";
|
||||
NSMutableDictionary *userInfo = [params mutableCopy];
|
||||
[userInfo setObject:message forKey:@"error message"];
|
||||
[userInfo setObject:@(method) forKey:@"http method"];
|
||||
|
||||
[[BuglyManager sharedManager] reportNetworkError:uid
|
||||
api:path
|
||||
code:resCode
|
||||
userInfo:userInfo];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@
|
||||
#import "BaseNavigationController.h"
|
||||
#import "FirstRechargeManager.h"
|
||||
#import "PublicRoomManager.h"
|
||||
#import "RoomBoomManager.h"
|
||||
|
||||
@interface BaseMvpPresenter()
|
||||
|
||||
@@ -37,13 +38,16 @@
|
||||
// 2. 重置公共房间管理器
|
||||
[[PublicRoomManager sharedManager] reset];
|
||||
|
||||
// 3. 数据logout
|
||||
// 3. 清理房间火箭管理器状态
|
||||
[[RoomBoomManager sharedManager] leaveRoom];
|
||||
|
||||
// 4. 数据logout
|
||||
[[AccountInfoStorage instance] saveAccountInfo:nil];
|
||||
[[AccountInfoStorage instance] saveTicket:nil];
|
||||
if ([NIMSDK sharedSDK].loginManager.isLogined) {
|
||||
[[NIMSDK sharedSDK].loginManager logout:nil];
|
||||
}
|
||||
// 4. 跳登录页面
|
||||
// 5. 跳登录页面
|
||||
[self tokenInvalid];
|
||||
// ///关闭心跳
|
||||
// [[ClientConfig shareConfig] resetHeartBratTimer];
|
||||
|
@@ -1,426 +0,0 @@
|
||||
# AttachmentModel 功能分析报告
|
||||
|
||||
## 目录
|
||||
- [1. 概述](#1-概述)
|
||||
- [2. AttachmentModel 核心结构](#2-attachmentmodel-核心结构)
|
||||
- [3. 消息类型分类](#3-消息类型分类)
|
||||
- [4. 使用场景分析](#4-使用场景分析)
|
||||
- [5. 功能模块分布](#5-功能模块分布)
|
||||
- [6. 关键实现细节](#6-关键实现细节)
|
||||
- [7. 最佳实践](#7-最佳实践)
|
||||
- [8. 总结](#8-总结)
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 定义
|
||||
`AttachmentModel` 是YuMi项目中用于处理NIMSDK自定义消息的核心数据模型,它实现了`NIMCustomAttachment`协议,用于在云信SDK中传输和处理各种自定义消息类型。
|
||||
|
||||
### 1.2 核心作用
|
||||
- **消息类型标识**: 通过`first`和`second`字段标识不同的消息类型和子类型
|
||||
- **数据承载**: 通过`data`字段承载具体的消息内容
|
||||
- **消息解析**: 配合`CustomAttachmentDecoder`进行消息的编码和解码
|
||||
- **业务扩展**: 支持各种业务场景的自定义消息处理
|
||||
|
||||
### 1.3 设计特点
|
||||
- **类型安全**: 使用枚举定义所有消息类型,避免硬编码
|
||||
- **扩展性强**: 支持新增消息类型而不影响现有代码
|
||||
- **统一接口**: 所有自定义消息都通过统一的接口处理
|
||||
- **数据灵活**: `data`字段支持任意类型的数据结构
|
||||
|
||||
## 2. AttachmentModel 核心结构
|
||||
|
||||
### 2.1 基础属性
|
||||
|
||||
```objc
|
||||
@interface AttachmentModel : PIBaseModel<NIMCustomAttachment>
|
||||
|
||||
@property (nonatomic, strong) id data; // 消息数据内容
|
||||
@property (nonatomic, assign) int first; // 消息类型标识
|
||||
@property (nonatomic, assign) int second; // 消息子类型标识
|
||||
@property (nonatomic, assign) BOOL isBroadcast; // 是否为广播消息
|
||||
@property (nonatomic, assign) NSInteger seq; // 本地序号,用于消息排序
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 2.2 编码实现
|
||||
|
||||
```objc
|
||||
- (NSString *)encodeAttachment {
|
||||
return [self toJSONString];
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 解码实现
|
||||
|
||||
```objc
|
||||
// CustomAttachmentDecoder.m
|
||||
- (id<NIMCustomAttachment>)decodeAttachment:(NSString *)content {
|
||||
id<NIMCustomAttachment> attachment;
|
||||
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
if (data) {
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
|
||||
options:0
|
||||
error:nil];
|
||||
if ([dict isKindOfClass:[NSDictionary class]]) {
|
||||
int first = [dict[@"first"] intValue];
|
||||
int second = [dict[@"second"] intValue];
|
||||
id originalData = dict[@"data"];
|
||||
|
||||
AttachmentModel *model = [[AttachmentModel alloc] init];
|
||||
model.first = (short)first;
|
||||
model.second = (short)second;
|
||||
model.data = originalData;
|
||||
|
||||
attachment = model;
|
||||
}
|
||||
}
|
||||
return attachment;
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 消息类型分类
|
||||
|
||||
### 3.1 主要消息类型 (first字段)
|
||||
|
||||
#### 3.1.1 基础功能类
|
||||
| 类型值 | 类型名称 | 功能描述 |
|
||||
|--------|----------|----------|
|
||||
| 2 | `CustomMessageType_Room_Tip` | 房间提示消息 |
|
||||
| 3 | `CustomMessageType_Gift` | 礼物相关消息 |
|
||||
| 5 | `CustomMessageType_Account` | 账户更新消息 |
|
||||
| 6 | `CustomMessageType_Member_Online` | 关注主播上线通知 |
|
||||
| 8 | `CustomMessageType_Queue` | 队列操作消息 |
|
||||
| 9 | `CustomMessageType_Face` | 表情消息 |
|
||||
| 10 | `CustomMessageType_Tweet` | 推文消息 |
|
||||
| 12 | `CustomMessageType_AllMicroSend` | 全麦送礼物 |
|
||||
|
||||
#### 3.1.2 房间管理类
|
||||
| 类型值 | 类型名称 | 功能描述 |
|
||||
|--------|----------|----------|
|
||||
| 15 | `CustomMessageType_Car_Notify` | 座驾相关通知 |
|
||||
| 18 | `CustomMessageType_Kick_User` | 踢出房间消息 |
|
||||
| 20 | `CustomMessageType_Update_RoomInfo` | 房间信息更新 |
|
||||
| 30 | `CustomMessageType_Arrange_Mic` | 排麦相关消息 |
|
||||
| 31 | `CustomMessageType_Room_PK` | 房间内PK消息 |
|
||||
| 42 | `CustomMessageType_Room_GiftValue` | 房间礼物值同步 |
|
||||
|
||||
#### 3.1.3 社交功能类
|
||||
| 类型值 | 类型名称 | 功能描述 |
|
||||
|--------|----------|----------|
|
||||
| 19 | `CustomMessageType_Secretary` | 小秘书消息 |
|
||||
| 22 | `CustomMessageType_Application_Share` | 应用内分享 |
|
||||
| 52 | `CustomMessageType_Monents` | 动态相关消息 |
|
||||
| 60 | `CustomMessageType_RedPacket` | 红包相关消息 |
|
||||
| 62 | `CustomMessageType_FindNew` | 发现萌新消息 |
|
||||
| 64 | `CustomMessageType_CP` | CP礼物消息 |
|
||||
|
||||
#### 3.1.4 游戏娱乐类
|
||||
| 类型值 | 类型名称 | 功能描述 |
|
||||
|--------|----------|----------|
|
||||
| 26 | `CustomMessageType_Candy_Tree` | 糖果树消息 |
|
||||
| 63 | `CustomMessageType_RoomBoom` | 房间火箭消息 |
|
||||
| 71 | `CustomMessageType_Tarot` | 塔罗牌消息 |
|
||||
| 72 | `CustomMessageType_RoomPlay_Dating` | 相亲游戏消息 |
|
||||
| 81 | `CustomMessageType_Room_Sailing` | 航海游戏消息 |
|
||||
| 83 | `CustomMessageType_Across_Room_PK` | 跨房PK消息 |
|
||||
| 97 | `CustomMessageType_Treasure_Fairy` | 精灵密藏消息 |
|
||||
|
||||
#### 3.1.5 系统通知类
|
||||
| 类型值 | 类型名称 | 功能描述 |
|
||||
|--------|----------|----------|
|
||||
| 23 | `CustomMessageType_Message_Handle` | 系统通知消息 |
|
||||
| 24 | `CustomMessageType_User_UpGrade` | 用户升级消息 |
|
||||
| 49 | `CustomMessageType_Version_Update` | 版本升级消息 |
|
||||
| 75 | `CustomMessageType_Chat_Risk_Alert` | 私聊风险提醒 |
|
||||
| 76 | `CustomMessageType_First_Recharge_Reward` | 首充奖励消息 |
|
||||
| 78 | `CustomMessageType_First_VisitorRecord` | 访客记录消息 |
|
||||
| 92 | `CustomMessageType_Task_Complete` | 任务完成通知 |
|
||||
|
||||
### 3.2 子类型示例 (second字段)
|
||||
|
||||
#### 3.2.1 礼物消息子类型
|
||||
```objc
|
||||
typedef NS_ENUM(NSUInteger, CustomMessageSubGift) {
|
||||
Custom_Message_Sub_Gift_Send = 31, // 发送礼物
|
||||
Custom_Message_Sub_Gift_ChannelNotify = 32, // 全服发送礼物
|
||||
Custom_Message_Sub_Gift_LuckySend = 34, // 发送福袋礼物
|
||||
Custom_Message_Sub_Gift_EmbeddedStyle = 35, // 发送嵌入式礼物
|
||||
};
|
||||
```
|
||||
|
||||
#### 3.2.2 红包消息子类型
|
||||
```objc
|
||||
typedef NS_ENUM(NSUInteger, CustomMessageSubRedPacket) {
|
||||
Custom_Message_Sub_RoomGiftRedPacket = 601, // 房间礼物红包
|
||||
Custom_Message_Sub_RoomDiamandRedPacket = 602, // 房间钻石红包
|
||||
Custom_Message_Sub_AllGiftRedPacket = 603, // 全服礼物红包
|
||||
Custom_Message_Sub_AllDiamandRedPacket = 604, // 全服钻石红包
|
||||
Custom_Message_Sub_OpenRedPacketSuccess = 605, // 抢红包成功
|
||||
Custom_Message_Sub_NewRoomDiamandRedPacket = 606, // 新版本房间钻石红包
|
||||
Custom_Message_Sub_LuckyPackage = 607, // 最新版本房间红包推送
|
||||
};
|
||||
```
|
||||
|
||||
#### 3.2.3 房间PK子类型
|
||||
```objc
|
||||
typedef NS_ENUM(NSUInteger, CustomMessageSubRoomPK) {
|
||||
Custom_Message_Sub_Room_PK_Non_Empty = 311, // 从无人报名pk排麦到有人报名pk排麦
|
||||
Custom_Message_Sub_Room_PK_Empty = 312, // 从有人报名pk排麦到无人报名pk排麦
|
||||
Custom_Message_Sub_Room_PK_Mode_Open = 313, // 创建了pk模式
|
||||
Custom_Message_Sub_Room_PK_Mode_Close = 314, // 关闭pk模式
|
||||
Custom_Message_Sub_Room_PK_Start = 315, // pk开始
|
||||
Custom_Message_Sub_Room_PK_Result = 316, // pk结果
|
||||
Custom_Message_Sub_Room_PK_Re_Start = 317, // 重新开始
|
||||
Custom_Message_Sub_Room_PK_Manager_Up_Mic = 318, // 管理员邀请上麦
|
||||
};
|
||||
```
|
||||
|
||||
## 4. 使用场景分析
|
||||
|
||||
### 4.1 消息接收处理
|
||||
|
||||
#### 4.1.1 私聊消息处理
|
||||
```objc
|
||||
// TabbarViewController.m
|
||||
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
|
||||
for (NIMMessage *message in messages) {
|
||||
if (message.session.sessionType == NIMSessionTypeP2P) {
|
||||
if (message.messageType == NIMMessageTypeCustom) {
|
||||
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
if (obj.attachment != nil && [obj.attachment isKindOfClass:[AttachmentModel class]]) {
|
||||
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
|
||||
|
||||
// 处理发现萌新打招呼消息
|
||||
if (attachment.first == CustomMessageType_FindNew &&
|
||||
attachment.second == Custom_Message_Find_New_Greet_New_User) {
|
||||
FindNewGreetMessageModel *greetInfo = [FindNewGreetMessageModel modelWithDictionary:attachment.data];
|
||||
// 显示打招呼弹窗
|
||||
[self showGreetAlert:greetInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.2 广播消息处理
|
||||
```objc
|
||||
// TabbarViewController.m
|
||||
- (void)onReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
|
||||
if (broadcastMessage.content) {
|
||||
NSDictionary *msgDictionary = [broadcastMessage.content toJSONObject];
|
||||
AttachmentModel *attachment = [AttachmentModel modelWithJSON:msgDictionary[@"body"]];
|
||||
|
||||
// 处理红包消息
|
||||
if (attachment.first == CustomMessageType_RedPacket) {
|
||||
[self receiveRedPacketDealWithData:attachment];
|
||||
}
|
||||
// 处理版本更新消息
|
||||
else if (attachment.first == CustomMessageType_Version_Update &&
|
||||
attachment.second == Custom_Message_Version_Update_Value) {
|
||||
[self handleVersionUpdate:attachment];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 消息发送
|
||||
|
||||
#### 4.2.1 创建自定义消息
|
||||
```objc
|
||||
// 创建礼物消息
|
||||
AttachmentModel *attachment = [[AttachmentModel alloc] init];
|
||||
attachment.first = CustomMessageType_Gift;
|
||||
attachment.second = Custom_Message_Sub_Gift_Send;
|
||||
attachment.data = @{
|
||||
@"giftId": @"123",
|
||||
@"giftName": @"玫瑰花",
|
||||
@"giftCount": @1,
|
||||
@"senderId": @"user123",
|
||||
@"receiverId": @"user456"
|
||||
};
|
||||
|
||||
// 创建NIM消息
|
||||
NIMCustomObject *customObject = [[NIMCustomObject alloc] init];
|
||||
customObject.attachment = attachment;
|
||||
NIMMessage *message = [[NIMMessage alloc] init];
|
||||
message.messageObject = customObject;
|
||||
```
|
||||
|
||||
#### 4.2.2 发送消息
|
||||
```objc
|
||||
// 发送到指定会话
|
||||
NIMSession *session = [NIMSession session:@"receiverId" type:NIMSessionTypeP2P];
|
||||
[[NIMSDK sharedSDK].chatManager sendMessage:message toSession:session error:nil];
|
||||
```
|
||||
|
||||
### 4.3 消息内容显示
|
||||
|
||||
#### 4.3.1 消息内容解析
|
||||
```objc
|
||||
// NIMMessageUtils.m
|
||||
+ (NSString *)messageContent:(NIMMessage*)message {
|
||||
switch (message.messageType) {
|
||||
case NIMMessageTypeCustom: {
|
||||
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
|
||||
|
||||
if (attachment.first == CustomMessageType_Secretary) {
|
||||
if (attachment.second == Custom_Message_Sub_Secretary_Router) {
|
||||
return attachment.data[@"title"];
|
||||
}
|
||||
} else if (attachment.first == CustomMessageType_Gift) {
|
||||
if (attachment.second == Custom_Message_Sub_Gift_Send) {
|
||||
return YMLocalizedString(@"NIMMessageUtils5"); // "发送了礼物"
|
||||
}
|
||||
} else if (attachment.first == CustomMessageType_FindNew &&
|
||||
attachment.second == Custom_Message_Find_New_Greet_New_User) {
|
||||
NSString *text = attachment.data[@"message"];
|
||||
return text.length > 0 ? text : YMLocalizedString(@"NIMMessageUtils11");
|
||||
}
|
||||
// ... 其他消息类型处理
|
||||
}
|
||||
break;
|
||||
}
|
||||
return @"";
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 功能模块分布
|
||||
|
||||
### 5.1 消息模块 (YMMessage)
|
||||
- **文件**: `AttachmentModel.h/m`, `CustomAttachmentDecoder.h/m`
|
||||
- **功能**: 消息模型定义、解码器实现
|
||||
- **使用**: 所有自定义消息的基础结构
|
||||
|
||||
### 5.2 主界面模块 (YMTabbar)
|
||||
- **文件**: `TabbarViewController.m`
|
||||
- **功能**: 全局消息接收处理、广播消息处理
|
||||
- **使用**: 处理发现萌新、红包、版本更新等全局消息
|
||||
|
||||
### 5.3 房间模块 (YMRoom)
|
||||
- **文件**: 多个房间相关文件
|
||||
- **功能**: 房间内消息处理、PK、礼物、火箭等
|
||||
- **使用**: 处理房间内的各种互动消息
|
||||
|
||||
### 5.4 动态模块 (YMMonents)
|
||||
- **文件**: `XPMomentsViewController.m`
|
||||
- **功能**: 动态相关消息处理
|
||||
- **使用**: 处理动态分享、审核等消息
|
||||
|
||||
### 5.5 个人中心模块 (YMMine)
|
||||
- **文件**: 多个个人中心相关文件
|
||||
- **功能**: 个人相关消息处理
|
||||
- **使用**: 处理VIP、粉丝团、任务等个人消息
|
||||
|
||||
## 6. 关键实现细节
|
||||
|
||||
### 6.1 消息类型判断
|
||||
```objc
|
||||
// 判断是否为特定类型的消息
|
||||
if (attachment.first == CustomMessageType_Gift &&
|
||||
attachment.second == Custom_Message_Sub_Gift_Send) {
|
||||
// 处理发送礼物消息
|
||||
}
|
||||
|
||||
// 判断是否为系统消息
|
||||
if (attachment.first == CustomMessageType_System_message) {
|
||||
// 处理系统消息
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 数据解析
|
||||
```objc
|
||||
// 从data字段解析具体数据
|
||||
NSDictionary *giftData = attachment.data;
|
||||
NSString *giftId = giftData[@"giftId"];
|
||||
NSString *giftName = giftData[@"giftName"];
|
||||
NSNumber *giftCount = giftData[@"giftCount"];
|
||||
|
||||
// 使用MJExtension进行模型转换
|
||||
GiftModel *giftModel = [GiftModel modelWithDictionary:attachment.data];
|
||||
```
|
||||
|
||||
### 6.3 消息过滤
|
||||
```objc
|
||||
// 根据分区ID过滤消息
|
||||
NSString *partitionId = [NSString stringWithFormat:@"%@", attachment.data[@"partitionId"]];
|
||||
if (![partitionId isEqualToString:self.userInfo.partitionId]) {
|
||||
return; // 不是当前分区的消息,忽略
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 消息排序
|
||||
```objc
|
||||
// 使用seq字段进行消息排序
|
||||
@property (nonatomic, assign) NSInteger seq; // 本地序号,用于将一条消息分解为多条有次序的消息
|
||||
```
|
||||
|
||||
## 7. 最佳实践
|
||||
|
||||
### 7.1 消息类型定义
|
||||
1. **使用枚举**: 避免硬编码数字,提高代码可读性
|
||||
2. **分类管理**: 按功能模块分类管理消息类型
|
||||
3. **版本兼容**: 新增消息类型时保持向后兼容
|
||||
|
||||
### 7.2 消息处理
|
||||
1. **类型检查**: 在处理消息前先检查类型
|
||||
2. **数据验证**: 验证data字段的数据完整性
|
||||
3. **错误处理**: 对解析失败的消息进行适当处理
|
||||
|
||||
### 7.3 性能优化
|
||||
1. **消息过滤**: 根据业务需求过滤不需要的消息
|
||||
2. **内存管理**: 及时释放不需要的消息对象
|
||||
3. **批量处理**: 对大量消息进行批量处理
|
||||
|
||||
### 7.4 扩展性设计
|
||||
1. **模块化**: 按功能模块组织消息处理逻辑
|
||||
2. **插件化**: 支持新增消息类型而不影响现有代码
|
||||
3. **配置化**: 通过配置文件管理消息类型
|
||||
|
||||
## 8. 总结
|
||||
|
||||
### 8.1 核心价值
|
||||
`AttachmentModel` 作为YuMi项目的消息处理核心,具有以下价值:
|
||||
|
||||
1. **统一接口**: 为所有自定义消息提供统一的处理接口
|
||||
2. **类型安全**: 通过枚举定义确保消息类型的类型安全
|
||||
3. **扩展性强**: 支持灵活扩展新的消息类型
|
||||
4. **功能完整**: 覆盖了社交、游戏、系统等各个业务场景
|
||||
|
||||
### 8.2 技术特点
|
||||
1. **协议实现**: 实现了NIMSDK的`NIMCustomAttachment`协议
|
||||
2. **JSON序列化**: 支持JSON格式的消息编码和解码
|
||||
3. **模型转换**: 支持与业务模型的相互转换
|
||||
4. **类型枚举**: 使用枚举定义所有消息类型和子类型
|
||||
|
||||
### 8.3 业务覆盖
|
||||
`AttachmentModel` 覆盖了YuMi项目的所有主要业务场景:
|
||||
|
||||
- **社交功能**: 私聊、动态、关注等
|
||||
- **房间功能**: PK、礼物、火箭、红包等
|
||||
- **游戏功能**: 塔罗、航海、相亲等
|
||||
- **系统功能**: 升级、任务、通知等
|
||||
- **商业功能**: VIP、粉丝团、充值等
|
||||
|
||||
### 8.4 架构优势
|
||||
1. **解耦合**: 消息处理逻辑与业务逻辑分离
|
||||
2. **可维护**: 清晰的消息类型定义和处理流程
|
||||
3. **可测试**: 每个消息类型都可以独立测试
|
||||
4. **可扩展**: 新增功能时只需添加新的消息类型
|
||||
|
||||
`AttachmentModel` 是YuMi项目即时通讯功能的重要基础设施,为项目的各种业务场景提供了强大而灵活的消息处理能力。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2024年12月
|
||||
**维护人员**: 开发团队
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,248 +0,0 @@
|
||||
# Banner手势优化实施总结
|
||||
|
||||
## 概述
|
||||
本文档记录了在 `RoomAnimationView.m` 中对 banner 手势系统的优化实施过程。
|
||||
|
||||
## 最新优化内容(2025年1月)
|
||||
|
||||
### 需求描述
|
||||
1. **bannerContainer 手势范围调整**:
|
||||
- 中央宽度 2/3 的位置:保留 swipe 手势
|
||||
- 左右两侧各 1/6 宽度:添加 tap 手势
|
||||
|
||||
2. **tap 手势处理逻辑**:
|
||||
- 检查当前显示的 banner 是否在 tap 位置可以响应事件
|
||||
- 如果可以响应:不处理,让 banner 继续原有逻辑
|
||||
- 如果不能响应:保存 tap 位置点,供后续使用
|
||||
|
||||
### 实施方案
|
||||
|
||||
#### 1. 手势识别器重新设计
|
||||
```objc
|
||||
- (void)addBnnerContainGesture {
|
||||
// 创建独立的手势容器,避免与XPRoomAnimationHitView的hitTest冲突
|
||||
[self insertSubview:self.bannerSwipeGestureContainer aboveSubview:self.bannerContainer];
|
||||
[self insertSubview:self.bannerLeftTapGestureContainer aboveSubview:self.bannerContainer];
|
||||
[self insertSubview:self.bannerRightTapGestureContainer aboveSubview:self.bannerContainer];
|
||||
|
||||
// 设置手势容器的布局约束
|
||||
[self.bannerSwipeGestureContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(self.bannerContainer);
|
||||
make.top.bottom.mas_equalTo(self.bannerContainer);
|
||||
make.width.mas_equalTo(self.bannerContainer.mas_width).multipliedBy(2.0/3.0);
|
||||
}];
|
||||
|
||||
[self.bannerLeftTapGestureContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.leading.bottom.mas_equalTo(self.bannerContainer);
|
||||
make.trailing.mas_equalTo(self.bannerSwipeGestureContainer.mas_leading);
|
||||
}];
|
||||
|
||||
[self.bannerRightTapGestureContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.trailing.bottom.mas_equalTo(self.bannerContainer);
|
||||
make.leading.mas_equalTo(self.bannerSwipeGestureContainer.mas_trailing);
|
||||
}];
|
||||
|
||||
// 创建中央区域的 swipe 手势(2/3 宽度)
|
||||
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleSwipe)];
|
||||
if (isMSRTL()) {
|
||||
swipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
} else {
|
||||
swipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
}
|
||||
swipe.delegate = self;
|
||||
|
||||
// 创建左侧区域的 tap 手势(1/6 宽度)
|
||||
UITapGestureRecognizer *leftTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleBannerTap:)];
|
||||
leftTap.delegate = self;
|
||||
|
||||
// 创建右侧区域的 tap 手势(1/6 宽度)
|
||||
UITapGestureRecognizer *rightTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleBannerTap:)];
|
||||
rightTap.delegate = self;
|
||||
|
||||
// 添加手势识别器到对应的手势容器
|
||||
[self.bannerSwipeGestureContainer addGestureRecognizer:swipe];
|
||||
[self.bannerLeftTapGestureContainer addGestureRecognizer:leftTap];
|
||||
[self.bannerRightTapGestureContainer addGestureRecognizer:rightTap];
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 区域划分逻辑
|
||||
```objc
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
||||
CGPoint touchPoint = [touch locationInView:self.bannerContainer];
|
||||
CGFloat containerWidth = self.bannerContainer.bounds.size.width;
|
||||
|
||||
// 计算区域边界
|
||||
CGFloat leftBoundary = containerWidth / 6.0; // 1/6 宽度
|
||||
CGFloat rightBoundary = containerWidth * 5.0 / 6.0; // 5/6 宽度
|
||||
|
||||
if ([gestureRecognizer isKindOfClass:[UISwipeGestureRecognizer class]]) {
|
||||
// Swipe 手势只在中央 2/3 区域生效
|
||||
return touchPoint.x >= leftBoundary && touchPoint.x <= rightBoundary;
|
||||
} else if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
|
||||
// Tap 手势只在左右两侧 1/6 区域生效
|
||||
return touchPoint.x < leftBoundary || touchPoint.x > rightBoundary;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Tap 手势处理逻辑
|
||||
```objc
|
||||
- (void)handleBannerTap:(UITapGestureRecognizer *)tapGesture {
|
||||
CGPoint tapPoint = [tapGesture locationInView:self.bannerContainer];
|
||||
|
||||
// 检查当前显示的 banner 是否在 tap 位置可以响应事件
|
||||
if ([self isPointInBannerInteractiveArea:tapPoint]) {
|
||||
// banner 可以响应,不处理,让 banner 继续原有逻辑
|
||||
NSLog(@"🎯 Banner tap 位置在可交互区域,banner 将处理此事件");
|
||||
return;
|
||||
} else {
|
||||
// banner 不能响应,保存 tap 位置
|
||||
self.savedTapPoint = tapPoint;
|
||||
self.hasSavedTapPoint = YES;
|
||||
NSLog(@"💾 Banner tap 位置不在可交互区域,已保存位置: %@", NSStringFromCGPoint(tapPoint));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Banner 交互区域检查
|
||||
```objc
|
||||
- (BOOL)isPointInBannerInteractiveArea:(CGPoint)point {
|
||||
// 检查当前显示的 banner 是否在指定位置可以响应事件
|
||||
for (UIView *subview in self.bannerContainer.subviews) {
|
||||
if (subview.hidden || subview.alpha <= 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查点是否在子视图范围内
|
||||
if (CGRectContainsPoint(subview.bounds, point)) {
|
||||
// 检查子视图是否支持用户交互
|
||||
if (subview.userInteractionEnabled) {
|
||||
// 进一步检查子视图是否有可点击的元素
|
||||
CGPoint subviewPoint = [subview convertPoint:point fromView:self.bannerContainer];
|
||||
UIView *hitView = [subview hitTest:subviewPoint withEvent:nil];
|
||||
if (hitView && hitView.userInteractionEnabled) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 公共接口方法
|
||||
```objc
|
||||
// 获取保存的 tap 位置
|
||||
- (CGPoint)getSavedTapPoint;
|
||||
|
||||
// 检查是否有保存的 tap 位置
|
||||
- (BOOL)hasSavedTapPointAvailable;
|
||||
|
||||
// 清除保存的 tap 位置
|
||||
- (void)clearSavedTapPoint;
|
||||
```
|
||||
|
||||
### 新增属性
|
||||
```objc
|
||||
// Banner 手势相关属性
|
||||
@property(nonatomic, assign) CGPoint savedTapPoint;
|
||||
@property(nonatomic, assign) BOOL hasSavedTapPoint;
|
||||
|
||||
// 手势容器(使用普通UIView避免XPRoomAnimationHitView的hitTest冲突)
|
||||
@property(nonatomic, strong) UIView *bannerSwipeGestureContainer;
|
||||
@property(nonatomic, strong) UIView *bannerLeftTapGestureContainer;
|
||||
@property(nonatomic, strong) UIView *bannerRightTapGestureContainer;
|
||||
```
|
||||
|
||||
### 协议支持
|
||||
- 添加了 `UIGestureRecognizerDelegate` 协议支持
|
||||
- 实现了手势识别器的 delegate 方法
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 1. 精确的区域控制
|
||||
- 使用独立的手势容器精确划分区域
|
||||
- 中央 2/3 区域:swipe 手势容器
|
||||
- 左右两侧各 1/6 区域:tap 手势容器
|
||||
|
||||
### 2. 避免手势冲突
|
||||
- 使用普通 `UIView` 作为手势容器,避免 `XPRoomAnimationHitView` 的 `hitTest` 冲突
|
||||
- 手势容器独立于 banner 内容,确保手势识别不受干扰
|
||||
|
||||
### 3. 智能的事件处理
|
||||
- 检查 banner 是否在 tap 位置可响应
|
||||
- 自动判断是否需要保存 tap 位置
|
||||
- 避免与 banner 原有交互逻辑冲突
|
||||
|
||||
### 4. 灵活的接口设计
|
||||
- 提供公共方法获取保存的 tap 位置
|
||||
- 支持清除保存的位置
|
||||
- 便于外部代码使用
|
||||
|
||||
### 5. 完善的日志记录
|
||||
- 详细记录手势处理过程
|
||||
- 便于调试和问题排查
|
||||
|
||||
## 使用示例
|
||||
|
||||
```objc
|
||||
// 检查是否有保存的 tap 位置
|
||||
if ([roomAnimationView hasSavedTapPointAvailable]) {
|
||||
CGPoint savedPoint = [roomAnimationView getSavedTapPoint];
|
||||
NSLog(@"保存的 tap 位置: %@", NSStringFromCGPoint(savedPoint));
|
||||
|
||||
// 使用保存的位置进行后续处理
|
||||
// ...
|
||||
|
||||
// 清除保存的位置
|
||||
[roomAnimationView clearSavedTapPoint];
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **手势容器设计**:使用普通 `UIView` 作为手势容器,避免 `XPRoomAnimationHitView` 的 `hitTest` 冲突
|
||||
2. **区域划分**:通过独立的视图容器精确划分手势区域,确保手势识别不受干扰
|
||||
3. **交互检查**:通过 `hitTest` 方法检查子视图的实际可交互性
|
||||
4. **内存管理**:及时清除不需要的 tap 位置数据
|
||||
5. **调试支持**:在 DEBUG 模式下为手势容器添加背景色,便于调试区域划分
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **区域划分测试**:
|
||||
- 在中央区域测试 swipe 手势
|
||||
- 在左右两侧测试 tap 手势
|
||||
- 验证手势在错误区域不触发
|
||||
|
||||
2. **交互逻辑测试**:
|
||||
- 在有可交互 banner 的区域 tap
|
||||
- 在无可交互 banner 的区域 tap
|
||||
- 验证 tap 位置的保存和清除
|
||||
|
||||
3. **边界条件测试**:
|
||||
- 测试不同屏幕尺寸下的区域划分
|
||||
- 测试 RTL 语言环境下的手势方向
|
||||
- 测试多个 banner 同时显示的情况
|
||||
|
||||
## 总结
|
||||
|
||||
本次优化成功实现了:
|
||||
- ✅ bannerContainer 手势范围的精确划分
|
||||
- ✅ 智能的 tap 手势处理逻辑
|
||||
- ✅ 灵活的 tap 位置保存机制
|
||||
- ✅ 完善的公共接口设计
|
||||
- ✅ 与现有代码的良好兼容性
|
||||
- ✅ 解决了 XPRoomAnimationHitView 的手势冲突问题
|
||||
|
||||
### 关键改进
|
||||
1. **避免手势冲突**:使用普通 `UIView` 作为手势容器,避免 `XPRoomAnimationHitView` 的 `hitTest` 方法干扰
|
||||
2. **精确区域控制**:通过独立的视图容器实现精确的手势区域划分
|
||||
3. **调试友好**:在 DEBUG 模式下为手势容器添加背景色,便于调试
|
||||
|
||||
该方案既满足了新的功能需求,又解决了潜在的手势冲突问题,保持了代码的可维护性和扩展性。
|
@@ -1,434 +0,0 @@
|
||||
# 邮箱验证码登录流程文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细描述了 YuMi iOS 应用中 `LoginTypesViewController` 在 `LoginDisplayType_email` 模式下的邮箱验证码登录流程。该流程实现了基于邮箱和验证码的用户认证机制。
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 核心组件
|
||||
- **LoginTypesViewController**: 登录类型控制器,负责 UI 展示和用户交互
|
||||
- **LoginPresenter**: 登录业务逻辑处理器,负责与 API 交互
|
||||
- **LoginInputItemView**: 输入组件,提供邮箱和验证码输入界面
|
||||
- **Api+Login**: 登录相关 API 接口封装
|
||||
- **AccountInfoStorage**: 账户信息本地存储管理
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### LoginDisplayType 枚举
|
||||
```objc
|
||||
typedef NS_ENUM(NSUInteger, LoginDisplayType) {
|
||||
LoginDisplayType_id, // ID 登录
|
||||
LoginDisplayType_email, // 邮箱登录 ✓
|
||||
LoginDisplayType_phoneNum, // 手机号登录
|
||||
LoginDisplayType_email_forgetPassword, // 邮箱忘记密码
|
||||
LoginDisplayType_phoneNum_forgetPassword, // 手机号忘记密码
|
||||
};
|
||||
```
|
||||
|
||||
#### LoginInputType 枚举
|
||||
```objc
|
||||
typedef NS_ENUM(NSUInteger, LoginInputType) {
|
||||
LoginInputType_email, // 邮箱输入
|
||||
LoginInputType_verificationCode, // 验证码输入
|
||||
LoginInputType_login, // 登录按钮
|
||||
// ... 其他类型
|
||||
};
|
||||
```
|
||||
|
||||
#### GetSmsType 验证码类型
|
||||
```objc
|
||||
typedef NS_ENUM(NSUInteger, GetSmsType) {
|
||||
GetSmsType_Regist = 1, // 注册(邮箱登录使用此类型)
|
||||
GetSmsType_Login = 2, // 登录
|
||||
GetSmsType_Reset_Password = 3, // 重设密码
|
||||
// ... 其他类型
|
||||
};
|
||||
```
|
||||
|
||||
## 登录流程详解
|
||||
|
||||
### 1. 界面初始化流程
|
||||
|
||||
#### 1.1 控制器初始化
|
||||
```objc
|
||||
// 在 LoginViewController 中点击邮箱登录按钮
|
||||
- (void)didTapEntrcyButton:(UIButton *)sender {
|
||||
if (sender.tag == LoginType_Email) {
|
||||
LoginTypesViewController *vc = [[LoginTypesViewController alloc] init];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
[vc updateLoginType:LoginDisplayType_email]; // 设置为邮箱登录模式
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 输入区域设置
|
||||
```objc
|
||||
- (void)setupEmailInputArea {
|
||||
[self setupInpuArea:LoginInputType_email // 第一行:邮箱输入
|
||||
second:LoginInputType_verificationCode // 第二行:验证码输入
|
||||
third:LoginInputType_none // 第三行:无
|
||||
action:LoginInputType_login // 操作按钮:登录
|
||||
showForgetPassword:NO]; // 不显示忘记密码
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 UI 组件配置
|
||||
- **第一行输入框**: 邮箱地址输入
|
||||
- 占位符: "请输入邮箱地址"
|
||||
- 键盘类型: `UIKeyboardTypeEmailAddress`
|
||||
- 回调: `handleFirstInputContentUpdate`
|
||||
|
||||
- **第二行输入框**: 验证码输入
|
||||
- 占位符: "请输入验证码"
|
||||
- 键盘类型: `UIKeyboardTypeDefault`
|
||||
- 附带"获取验证码"按钮
|
||||
- 回调: `handleSecondInputContentUpdate`
|
||||
|
||||
### 2. 验证码获取流程
|
||||
|
||||
#### 2.1 用户交互触发
|
||||
```objc
|
||||
// 用户点击"获取验证码"按钮
|
||||
[self.secondLineInputView setHandleItemAction:^(LoginInputType inputType) {
|
||||
if (inputType == LoginInputType_verificationCode) {
|
||||
if (self.type == LoginDisplayType_email) {
|
||||
[self handleTapGetMailVerificationCode];
|
||||
}
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
#### 2.2 邮箱验证码获取处理
|
||||
```objc
|
||||
- (void)handleTapGetMailVerificationCode {
|
||||
NSString *email = [self.firstLineInputView inputContent];
|
||||
|
||||
// 邮箱地址验证
|
||||
if (email.length == 0) {
|
||||
[self.secondLineInputView endVerificationCountDown];
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用 Presenter 发送验证码
|
||||
[self.presenter sendMailVerificationCode:email type:GetSmsType_Regist];
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Presenter 层处理
|
||||
```objc
|
||||
- (void)sendMailVerificationCode:(NSString *)emailAddress type:(NSInteger)type {
|
||||
// DES 加密邮箱地址
|
||||
NSString *desEmail = [DESEncrypt encryptUseDES:emailAddress
|
||||
key:KeyWithType(KeyType_PasswordEncode)];
|
||||
|
||||
@kWeakify(self);
|
||||
[Api emailGetCode:[self createHttpCompletion:^(BaseModel *data) {
|
||||
@kStrongify(self);
|
||||
if ([[self getView] respondsToSelector:@selector(emailCodeSucess:type:)]) {
|
||||
[[self getView] emailCodeSucess:@"" type:type];
|
||||
}
|
||||
} fail:^(NSInteger code, NSString *msg) {
|
||||
@kStrongify(self);
|
||||
if ([[self getView] respondsToSelector:@selector(emailCodeFailure)]) {
|
||||
[[self getView] emailCodeFailure];
|
||||
}
|
||||
} showLoading:YES errorToast:YES]
|
||||
emailAddress:desEmail
|
||||
type:@(type)];
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4 API 接口调用
|
||||
```objc
|
||||
+ (void)emailGetCode:(HttpRequestHelperCompletion)completion
|
||||
emailAddress:(NSString *)emailAddress
|
||||
type:(NSNumber *)type {
|
||||
[self makeRequest:@"email/getCode"
|
||||
method:HttpRequestHelperMethodPOST
|
||||
completion:completion, __FUNCTION__, emailAddress, type, nil];
|
||||
}
|
||||
```
|
||||
|
||||
**API 详情**:
|
||||
- **接口路径**: `POST /email/getCode`
|
||||
- **请求参数**:
|
||||
- `emailAddress`: 邮箱地址(DES 加密)
|
||||
- `type`: 验证码类型(1=注册)
|
||||
|
||||
#### 2.5 获取验证码成功处理
|
||||
```objc
|
||||
- (void)emailCodeSucess:(NSString *)message type:(GetSmsType)type {
|
||||
[self showSuccessToast:YMLocalizedString(@"XPLoginPhoneViewController2")]; // "验证码已发送"
|
||||
[self.secondLineInputView startVerificationCountDown]; // 开始倒计时
|
||||
[self.secondLineInputView displayKeyboard]; // 显示键盘
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.6 获取验证码失败处理
|
||||
```objc
|
||||
- (void)emailCodeFailure {
|
||||
[self.secondLineInputView endVerificationCountDown]; // 结束倒计时
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 邮箱登录流程
|
||||
|
||||
#### 3.1 登录按钮状态检查
|
||||
```objc
|
||||
- (void)checkActionButtonStatus {
|
||||
switch (self.type) {
|
||||
case LoginDisplayType_email: {
|
||||
NSString *accountString = [self.firstLineInputView inputContent]; // 邮箱
|
||||
NSString *codeString = [self.secondLineInputView inputContent]; // 验证码
|
||||
|
||||
// 只有当邮箱和验证码都不为空时才启用登录按钮
|
||||
if (![NSString isEmpty:accountString] && ![NSString isEmpty:codeString]) {
|
||||
self.bottomActionButton.enabled = YES;
|
||||
} else {
|
||||
self.bottomActionButton.enabled = NO;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 登录按钮点击处理
|
||||
```objc
|
||||
- (void)didTapActionButton {
|
||||
[self.view endEditing:true];
|
||||
|
||||
switch (self.type) {
|
||||
case LoginDisplayType_email: {
|
||||
// 调用 Presenter 进行邮箱登录
|
||||
[self.presenter loginWithEmail:[self.firstLineInputView inputContent]
|
||||
code:[self.secondLineInputView inputContent]];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Presenter 层登录处理
|
||||
```objc
|
||||
- (void)loginWithEmail:(NSString *)email code:(NSString *)code {
|
||||
// DES 加密邮箱地址
|
||||
NSString *desMail = [DESEncrypt encryptUseDES:email
|
||||
key:KeyWithType(KeyType_PasswordEncode)];
|
||||
|
||||
@kWeakify(self);
|
||||
[Api loginWithCode:[self createHttpCompletion:^(BaseModel *data) {
|
||||
@kStrongify(self);
|
||||
|
||||
// 解析账户模型
|
||||
AccountModel *accountModel = [AccountModel modelWithDictionary:data.data];
|
||||
|
||||
// 保存账户信息
|
||||
if (accountModel && accountModel.access_token.length > 0) {
|
||||
[[AccountInfoStorage instance] saveAccountInfo:accountModel];
|
||||
}
|
||||
|
||||
// 通知登录成功
|
||||
if ([[self getView] respondsToSelector:@selector(loginSuccess)]) {
|
||||
[[self getView] loginSuccess];
|
||||
}
|
||||
} fail:^(NSInteger code, NSString *msg) {
|
||||
@kStrongify(self);
|
||||
[[self getView] loginFailWithMsg:msg];
|
||||
} errorToast:NO]
|
||||
email:desMail
|
||||
code:code
|
||||
client_secret:clinet_s // 客户端密钥
|
||||
version:@"1"
|
||||
client_id:@"erban-client"
|
||||
grant_type:@"email"]; // 邮箱登录类型
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4 API 接口调用
|
||||
```objc
|
||||
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
|
||||
email:(NSString *)email
|
||||
code:(NSString *)code
|
||||
client_secret:(NSString *)client_secret
|
||||
version:(NSString *)version
|
||||
client_id:(NSString *)client_id
|
||||
grant_type:(NSString *)grant_type {
|
||||
|
||||
NSString *fang = [NSString stringFromBase64String:@"b2F1dGgvdG9rZW4="]; // oauth/token
|
||||
[self makeRequest:fang
|
||||
method:HttpRequestHelperMethodPOST
|
||||
completion:completion, __FUNCTION__, email, code, client_secret,
|
||||
version, client_id, grant_type, nil];
|
||||
}
|
||||
```
|
||||
|
||||
**API 详情**:
|
||||
- **接口路径**: `POST /oauth/token`
|
||||
- **请求参数**:
|
||||
- `email`: 邮箱地址(DES 加密)
|
||||
- `code`: 验证码
|
||||
- `client_secret`: 客户端密钥
|
||||
- `version`: 版本号 "1"
|
||||
- `client_id`: 客户端ID "erban-client"
|
||||
- `grant_type`: 授权类型 "email"
|
||||
|
||||
#### 3.5 登录成功处理
|
||||
```objc
|
||||
- (void)loginSuccess {
|
||||
[self showSuccessToast:YMLocalizedString(@"XPLoginPhoneViewController1")]; // "登录成功"
|
||||
[PILoginManager loginWithVC:self isLoginPhone:NO]; // 执行登录后续处理
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.6 登录失败处理
|
||||
```objc
|
||||
- (void)loginFailWithMsg:(NSString *)msg {
|
||||
[self showSuccessToast:msg]; // 显示错误信息
|
||||
}
|
||||
```
|
||||
|
||||
## 数据流时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户
|
||||
participant VC as LoginTypesViewController
|
||||
participant IV as LoginInputItemView
|
||||
participant P as LoginPresenter
|
||||
participant API as Api+Login
|
||||
participant Storage as AccountInfoStorage
|
||||
|
||||
Note over User,Storage: 1. 初始化邮箱登录界面
|
||||
User->>VC: 选择邮箱登录
|
||||
VC->>VC: updateLoginType(LoginDisplayType_email)
|
||||
VC->>VC: setupEmailInputArea()
|
||||
VC->>IV: 创建邮箱输入框
|
||||
VC->>IV: 创建验证码输入框
|
||||
|
||||
Note over User,Storage: 2. 获取邮箱验证码
|
||||
User->>IV: 输入邮箱地址
|
||||
User->>IV: 点击"获取验证码"
|
||||
IV->>VC: handleTapGetMailVerificationCode
|
||||
VC->>VC: 验证邮箱地址非空
|
||||
VC->>P: sendMailVerificationCode(email, GetSmsType_Regist)
|
||||
P->>P: DES加密邮箱地址
|
||||
P->>API: emailGetCode(encryptedEmail, type=1)
|
||||
API-->>P: 验证码发送结果
|
||||
P-->>VC: emailCodeSucess / emailCodeFailure
|
||||
VC->>IV: startVerificationCountDown / endVerificationCountDown
|
||||
VC->>User: 显示成功/失败提示
|
||||
|
||||
Note over User,Storage: 3. 邮箱验证码登录
|
||||
User->>IV: 输入验证码
|
||||
IV->>VC: 输入内容变化回调
|
||||
VC->>VC: checkActionButtonStatus()
|
||||
VC->>User: 启用/禁用登录按钮
|
||||
User->>VC: 点击登录按钮
|
||||
VC->>VC: didTapActionButton()
|
||||
VC->>P: loginWithEmail(email, code)
|
||||
P->>P: DES加密邮箱地址
|
||||
P->>API: loginWithCode(email, code, ...)
|
||||
API-->>P: OAuth Token 响应
|
||||
P->>P: 解析 AccountModel
|
||||
P->>Storage: saveAccountInfo(accountModel)
|
||||
P-->>VC: loginSuccess / loginFailWithMsg
|
||||
VC->>User: 显示登录结果
|
||||
VC->>User: 跳转到主界面
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 1. 数据加密
|
||||
- **邮箱地址加密**: 使用 DES 算法加密邮箱地址后传输
|
||||
```objc
|
||||
NSString *desEmail = [DESEncrypt encryptUseDES:email key:KeyWithType(KeyType_PasswordEncode)];
|
||||
```
|
||||
|
||||
### 2. 输入验证
|
||||
- **邮箱格式验证**: 通过 `UIKeyboardTypeEmailAddress` 键盘类型引导正确输入
|
||||
- **非空验证**: 邮箱和验证码都必须非空才能执行登录
|
||||
|
||||
### 3. 验证码安全
|
||||
- **时效性**: 验证码具有倒计时机制,防止重复获取
|
||||
- **类型标识**: 使用 `GetSmsType_Regist = 1` 标识登录验证码
|
||||
|
||||
### 4. 网络安全
|
||||
- **错误处理**: 完整的成功/失败回调机制
|
||||
- **加载状态**: `showLoading:YES` 防止重复请求
|
||||
- **错误提示**: `errorToast:YES` 显示网络错误
|
||||
|
||||
## 错误处理机制
|
||||
|
||||
### 1. 邮箱验证码获取错误
|
||||
```objc
|
||||
- (void)emailCodeFailure {
|
||||
[self.secondLineInputView endVerificationCountDown]; // 停止倒计时
|
||||
// 用户可以重新获取验证码
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 登录失败处理
|
||||
```objc
|
||||
- (void)loginFailWithMsg:(NSString *)msg {
|
||||
[self showSuccessToast:msg]; // 显示具体错误信息
|
||||
// 用户可以重新尝试登录
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 网络请求错误
|
||||
- **自动重试**: 用户可以手动重新点击获取验证码或登录
|
||||
- **错误提示**: 通过 Toast 显示具体错误信息
|
||||
- **状态恢复**: 失败后恢复按钮可点击状态
|
||||
|
||||
## 本地化支持
|
||||
|
||||
### 关键文本资源
|
||||
- `@"20.20.51_text_1"`: "邮箱登录"
|
||||
- `@"20.20.51_text_4"`: "请输入邮箱地址"
|
||||
- `@"20.20.51_text_7"`: "请输入验证码"
|
||||
- `@"XPLoginPhoneViewController2"`: "验证码已发送"
|
||||
- `@"XPLoginPhoneViewController1"`: "登录成功"
|
||||
|
||||
### 多语言支持
|
||||
- 简体中文 (`zh-Hant.lproj`)
|
||||
- 英文 (`en.lproj`)
|
||||
- 阿拉伯语 (`ar.lproj`)
|
||||
- 土耳其语 (`tr.lproj`)
|
||||
|
||||
## 依赖组件
|
||||
|
||||
### 外部框架
|
||||
- **MASConstraintMaker**: 自动布局
|
||||
- **ReactiveObjC**: 响应式编程(部分组件使用)
|
||||
|
||||
### 内部组件
|
||||
- **YMLocalizedString**: 本地化字符串管理
|
||||
- **DESEncrypt**: DES 加密工具
|
||||
- **AccountInfoStorage**: 账户信息存储
|
||||
- **HttpRequestHelper**: 网络请求管理
|
||||
|
||||
## 扩展和维护
|
||||
|
||||
### 新增功能建议
|
||||
1. **邮箱格式验证**: 添加正则表达式验证邮箱格式
|
||||
2. **验证码长度限制**: 限制验证码输入长度
|
||||
3. **自动填充**: 支持系统邮箱自动填充
|
||||
4. **记住邮箱**: 保存最近使用的邮箱地址
|
||||
|
||||
### 性能优化
|
||||
1. **请求去重**: 防止短时间内重复请求验证码
|
||||
2. **缓存机制**: 缓存验证码倒计时状态
|
||||
3. **网络优化**: 添加请求超时和重试机制
|
||||
|
||||
### 代码维护
|
||||
1. **常量管理**: 将硬编码字符串提取为常量
|
||||
2. **错误码统一**: 统一管理API错误码
|
||||
3. **日志记录**: 添加详细的操作日志
|
||||
|
||||
## 总结
|
||||
|
||||
邮箱验证码登录流程是一个完整的用户认证系统,包含了界面展示、验证码获取、用户登录、数据存储等完整环节。该流程具有良好的安全性、用户体验和错误处理机制,符合现代移动应用的认证标准。
|
||||
|
||||
通过本文档,开发人员可以全面了解邮箱登录的实现细节,便于后续的功能扩展和维护工作。
|
@@ -1,63 +0,0 @@
|
||||
# 图片上传接口(Swift 封装,腾讯云 COS)
|
||||
|
||||
## 1. 参数模型
|
||||
|
||||
```swift
|
||||
struct ImageUploadRequest {
|
||||
let image: UIImage
|
||||
let fileName: String // 如 "image/xxxx.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 接口调用方法
|
||||
|
||||
```swift
|
||||
class ImageUploader {
|
||||
static func uploadImage(
|
||||
request: ImageUploadRequest,
|
||||
completion: @escaping (Result<String, Error>) -> Void
|
||||
) {
|
||||
// 1. 压缩图片,生成 NSData
|
||||
// 2. 调用腾讯云 COS SDK 上传
|
||||
// 3. 返回图片 URL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 示例用法
|
||||
|
||||
```swift
|
||||
let image = UIImage(named: "test.jpg")!
|
||||
let request = ImageUploadRequest(image: image, fileName: "image/\(UUID().uuidString).jpg")
|
||||
ImageUploader.uploadImage(request: request) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
print("上传成功,图片地址:\(url)")
|
||||
case .failure(let error):
|
||||
print("上传失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 第三方依赖
|
||||
|
||||
- [QCloudCOSXML](https://github.com/tencentyun/qcloud-sdk-ios)(腾讯云 COS 官方 SDK)
|
||||
- [TZImagePickerController](https://github.com/banchichen/TZImagePickerController)(图片选择/裁剪,非必须)
|
||||
|
||||
## 5. 配置说明
|
||||
|
||||
- COS 配置信息(appId、region、bucket、签名等)需通过接口动态获取并初始化 SDK。
|
||||
- 上传时需指定 bucket、object(文件名)、body(NSData)。
|
||||
- 支持自定义域名、加速域名等高级配置。
|
||||
|
||||
## 6. 错误处理建议
|
||||
|
||||
- 网络异常、图片压缩失败、SDK 上传失败等需统一处理。
|
||||
- 建议统一封装 `UploadError` 类型。
|
||||
|
||||
## 7. 扩展建议
|
||||
|
||||
- 支持多图批量上传
|
||||
- 支持上传进度回调
|
||||
- 支持 async/await
|
||||
- 可结合项目网络层统一封装
|
@@ -1,69 +0,0 @@
|
||||
# MomentsPublish 动态发布接口(Swift 封装)
|
||||
|
||||
## 1. 参数模型
|
||||
|
||||
```swift
|
||||
struct MomentsPublishRequest {
|
||||
let uid: String
|
||||
let type: String
|
||||
let worldId: String?
|
||||
let content: String
|
||||
let resList: [String]?
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 接口调用方法
|
||||
|
||||
```swift
|
||||
class MomentsAPI {
|
||||
static func publishMoment(
|
||||
request: MomentsPublishRequest,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
// 1. 构造参数字典
|
||||
// 2. 发起POST请求到 dynamic/square/publish
|
||||
// 3. 处理返回结果
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 示例用法
|
||||
|
||||
```swift
|
||||
let request = MomentsPublishRequest(
|
||||
uid: "12345",
|
||||
type: "1", // 0:文本 1:图片
|
||||
worldId: "67890",
|
||||
content: "今天很开心!",
|
||||
resList: ["image_url_1", "image_url_2"]
|
||||
)
|
||||
MomentsAPI.publishMoment(request: request) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
print("发布成功")
|
||||
case .failure(let error):
|
||||
print("发布失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|-----------|--------------|------|----------------|
|
||||
| uid | String | 是 | 用户ID |
|
||||
| type | String | 是 | 动态类型(0文本/1图片)|
|
||||
| worldId | String? | 否 | 话题ID |
|
||||
| content | String | 是 | 动态内容 |
|
||||
| resList | [String]? | 否 | 图片资源URL数组 |
|
||||
|
||||
## 5. 错误处理建议
|
||||
|
||||
- 网络异常、参数校验、后端返回错误码均需处理
|
||||
- 建议统一封装 `APIError` 类型
|
||||
|
||||
## 6. 扩展建议
|
||||
|
||||
- 支持 async/await
|
||||
- 可扩展为支持更多动态类型
|
||||
- 可结合项目网络层统一封装
|
@@ -1,622 +0,0 @@
|
||||
# NIMSDKManager 使用指南
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 概述](#1-概述)
|
||||
- [2. Objective-C 使用示例](#2-objective-c-使用示例)
|
||||
- [3. Swift 桥接使用示例](#3-swift-桥接使用示例)
|
||||
- [4. 配置说明](#4-配置说明)
|
||||
- [5. 最佳实践](#5-最佳实践)
|
||||
- [6. 常见问题](#6-常见问题)
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`NIMSDKManager` 是一个专门用于管理NIMSDK事务的统一管理类,提供了配置、初始化、登录/登出等完整的功能封装。该类采用单例模式设计,支持Objective-C和Swift项目使用。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- **配置管理**: 统一的NIMSDK配置管理
|
||||
- **初始化**: 简化的SDK初始化流程
|
||||
- **登录管理**: 登录、自动登录、登出等功能
|
||||
- **状态监控**: 实时监控登录状态变化
|
||||
- **消息处理**: 消息发送、接收、广播等
|
||||
- **推送管理**: APNS推送相关功能
|
||||
- **代理管理**: 支持多代理监听
|
||||
|
||||
### 1.2 设计特点
|
||||
|
||||
- **单例模式**: 全局统一管理
|
||||
- **代理模式**: 支持多代理监听
|
||||
- **Block回调**: 支持异步操作回调
|
||||
- **Swift兼容**: 完美支持Swift项目桥接
|
||||
- **错误处理**: 完善的错误处理机制
|
||||
|
||||
## 2. Objective-C 使用示例
|
||||
|
||||
### 2.1 基本配置和初始化
|
||||
|
||||
```objc
|
||||
// 1. 创建配置模型
|
||||
NIMSDKConfigModel *config = [[NIMSDKConfigModel alloc] init];
|
||||
config.appKey = KeyWithType(KeyType_NetEase);
|
||||
config.apnsCername = @"pikoDevelopPush"; // DEBUG环境
|
||||
config.shouldConsiderRevokedMessageUnreadCount = YES;
|
||||
config.shouldSyncStickTopSessionInfos = YES;
|
||||
config.enabledHttpsForInfo = NO; // DEBUG环境
|
||||
config.enabledHttpsForMessage = NO; // DEBUG环境
|
||||
|
||||
// 2. 配置并初始化
|
||||
[[NIMSDKManager sharedManager] configureWithConfig:config];
|
||||
[[NIMSDKManager sharedManager] initializeWithCompletion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"NIMSDK初始化失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"NIMSDK初始化成功");
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
### 2.2 登录管理
|
||||
|
||||
```objc
|
||||
// 1. 设置登录状态变化监听
|
||||
[[NIMSDKManager sharedManager] setLoginStatusChangeBlock:^(NIMSDKLoginStatus status) {
|
||||
switch (status) {
|
||||
case NIMSDKLoginStatusNotLogin:
|
||||
NSLog(@"未登录");
|
||||
break;
|
||||
case NIMSDKLoginStatusLogging:
|
||||
NSLog(@"登录中");
|
||||
break;
|
||||
case NIMSDKLoginStatusLogined:
|
||||
NSLog(@"已登录");
|
||||
break;
|
||||
case NIMSDKLoginStatusLogout:
|
||||
NSLog(@"已登出");
|
||||
break;
|
||||
case NIMSDKLoginStatusKickout:
|
||||
NSLog(@"被踢出");
|
||||
break;
|
||||
case NIMSDKLoginStatusAutoLoginFailed:
|
||||
NSLog(@"自动登录失败");
|
||||
break;
|
||||
}
|
||||
}];
|
||||
|
||||
// 2. 执行登录
|
||||
[[NIMSDKManager sharedManager] loginWithAccount:@"user123"
|
||||
token:@"token123"
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"登录失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"登录成功");
|
||||
}
|
||||
}];
|
||||
|
||||
// 3. 自动登录
|
||||
NSDictionary *autoLoginData = @{@"account": @"user123", @"token": @"token123"};
|
||||
[[NIMSDKManager sharedManager] autoLoginWithData:autoLoginData
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"自动登录失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"自动登录成功");
|
||||
}
|
||||
}];
|
||||
|
||||
// 4. 登出
|
||||
[[NIMSDKManager sharedManager] logoutWithCompletion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"登出失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"登出成功");
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
### 2.3 代理监听
|
||||
|
||||
```objc
|
||||
@interface MyViewController () <NIMSDKManagerDelegate>
|
||||
@end
|
||||
|
||||
@implementation MyViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
// 添加代理
|
||||
[[NIMSDKManager sharedManager] addDelegate:self];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
// 移除代理
|
||||
[[NIMSDKManager sharedManager] removeDelegate:self];
|
||||
}
|
||||
|
||||
#pragma mark - NIMSDKManagerDelegate
|
||||
|
||||
- (void)nimSDKManager:(id)manager didChangeLoginStatus:(NIMSDKLoginStatus)status {
|
||||
NSLog(@"登录状态变化: %ld", (long)status);
|
||||
}
|
||||
|
||||
- (void)nimSDKManager:(id)manager didAutoLoginFailed:(NSError *)error {
|
||||
NSLog(@"自动登录失败: %@", error);
|
||||
}
|
||||
|
||||
- (void)nimSDKManager:(id)manager didKickout:(NIMLoginKickoutResult *)result {
|
||||
NSLog(@"被踢出: %@", result);
|
||||
}
|
||||
|
||||
- (void)nimSDKManager:(id)manager didReceiveMessages:(NSArray<NIMMessage *> *)messages {
|
||||
NSLog(@"收到消息: %@", messages);
|
||||
}
|
||||
|
||||
- (void)nimSDKManager:(id)manager didReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
|
||||
NSLog(@"收到广播消息: %@", broadcastMessage);
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 2.4 消息管理
|
||||
|
||||
```objc
|
||||
// 1. 创建消息
|
||||
NIMMessage *textMessage = [[NIMSDKManager sharedManager] createTextMessage:@"Hello World"];
|
||||
|
||||
// 2. 创建自定义消息
|
||||
AttachmentModel *attachment = [[AttachmentModel alloc] init];
|
||||
attachment.first = CustomMessageType_Gift;
|
||||
attachment.second = Custom_Message_Sub_Gift_Send;
|
||||
attachment.data = @{@"giftId": @"123", @"giftName": @"玫瑰花"};
|
||||
|
||||
NIMMessage *customMessage = [[NIMSDKManager sharedManager] createCustomMessageWithAttachment:attachment];
|
||||
|
||||
// 3. 发送消息
|
||||
NIMSession *session = [NIMSession session:@"receiverId" type:NIMSessionTypeP2P];
|
||||
[[NIMSDKManager sharedManager] sendMessage:textMessage
|
||||
toSession:session
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"发送失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"发送成功");
|
||||
}
|
||||
}];
|
||||
|
||||
// 4. 获取未读消息数
|
||||
NSInteger unreadCount = [[NIMSDKManager sharedManager] unreadMessageCount];
|
||||
NSLog(@"未读消息数: %ld", (long)unreadCount);
|
||||
|
||||
// 5. 获取所有会话
|
||||
NSArray<NIMRecentSession *> *sessions = [[NIMSDKManager sharedManager] allRecentSessions];
|
||||
NSLog(@"会话数量: %lu", (unsigned long)sessions.count);
|
||||
```
|
||||
|
||||
### 2.5 推送管理
|
||||
|
||||
```objc
|
||||
// 1. 更新APNS设备Token
|
||||
- (void)application:(UIApplication *)app
|
||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
[[NIMSDKManager sharedManager] updateApnsToken:deviceToken];
|
||||
}
|
||||
|
||||
// 2. 处理推送消息
|
||||
- (void)application:(UIApplication *)application
|
||||
didReceiveRemoteNotification:(NSDictionary *)userInfo
|
||||
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
|
||||
|
||||
BOOL handled = [[NIMSDKManager sharedManager] handlePushNotification:userInfo];
|
||||
if (handled) {
|
||||
completionHandler(UIBackgroundFetchResultNewData);
|
||||
} else {
|
||||
completionHandler(UIBackgroundFetchResultNoData);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Swift 桥接使用示例
|
||||
|
||||
### 3.1 创建桥接头文件
|
||||
|
||||
在Swift项目中创建桥接头文件 `YourProject-Bridging-Header.h`:
|
||||
|
||||
```objc
|
||||
//
|
||||
// YourProject-Bridging-Header.h
|
||||
// YourProject
|
||||
//
|
||||
|
||||
#ifndef YourProject_Bridging_Header_h
|
||||
#define YourProject_Bridging_Header_h
|
||||
|
||||
#import "NIMSDKManager.h"
|
||||
#import "AttachmentModel.h"
|
||||
#import "CustomAttachmentDecoder.h"
|
||||
|
||||
#endif /* YourProject_Bridging_Header_h */
|
||||
```
|
||||
|
||||
### 3.2 Swift 使用示例
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class NIMSDKService {
|
||||
|
||||
static let shared = NIMSDKService()
|
||||
private let nimManager = NIMSDKManager.shared()
|
||||
|
||||
private init() {
|
||||
setupNIMSDK()
|
||||
}
|
||||
|
||||
// MARK: - 配置和初始化
|
||||
|
||||
private func setupNIMSDK() {
|
||||
// 创建配置
|
||||
let config = NIMSDKConfigModel()
|
||||
config.appKey = KeyWithType(KeyType_NetEase)
|
||||
config.apnsCername = "pikoDevelopPush" // DEBUG环境
|
||||
config.shouldConsiderRevokedMessageUnreadCount = true
|
||||
config.shouldSyncStickTopSessionInfos = true
|
||||
config.enabledHttpsForInfo = false // DEBUG环境
|
||||
config.enabledHttpsForMessage = false // DEBUG环境
|
||||
|
||||
// 配置并初始化
|
||||
nimManager.configure(with: config)
|
||||
nimManager.initialize { [weak self] error in
|
||||
if let error = error {
|
||||
print("NIMSDK初始化失败: \(error)")
|
||||
} else {
|
||||
print("NIMSDK初始化成功")
|
||||
self?.setupDelegates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupDelegates() {
|
||||
// 设置登录状态变化监听
|
||||
nimManager.setLoginStatusChangeBlock { [weak self] status in
|
||||
self?.handleLoginStatusChange(status)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 登录管理
|
||||
|
||||
func login(account: String, token: String, completion: @escaping (Error?) -> Void) {
|
||||
nimManager.login(withAccount: account, token: token) { error in
|
||||
DispatchQueue.main.async {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func autoLogin(data: [String: Any], completion: @escaping (Error?) -> Void) {
|
||||
nimManager.autoLogin(with: data) { error in
|
||||
DispatchQueue.main.async {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logout(completion: @escaping (Error?) -> Void) {
|
||||
nimManager.logout { error in
|
||||
DispatchQueue.main.async {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 状态查询
|
||||
|
||||
var isLogined: Bool {
|
||||
return nimManager.isLogined()
|
||||
}
|
||||
|
||||
var currentAccount: String? {
|
||||
return nimManager.currentAccount()
|
||||
}
|
||||
|
||||
var loginStatus: NIMSDKLoginStatus {
|
||||
return nimManager.currentLoginStatus()
|
||||
}
|
||||
|
||||
// MARK: - 消息管理
|
||||
|
||||
func sendTextMessage(_ text: String, to sessionId: String, completion: @escaping (Error?) -> Void) {
|
||||
let message = nimManager.createTextMessage(text)
|
||||
let session = NIMSession(session: sessionId, type: .p2P)
|
||||
|
||||
nimManager.send(message, to: session) { error in
|
||||
DispatchQueue.main.async {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendCustomMessage(attachment: NIMCustomAttachment, to sessionId: String, completion: @escaping (Error?) -> Void) {
|
||||
let message = nimManager.createCustomMessage(with: attachment)
|
||||
let session = NIMSession(session: sessionId, type: .p2P)
|
||||
|
||||
nimManager.send(message, to: session) { error in
|
||||
DispatchQueue.main.async {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unreadMessageCount: Int {
|
||||
return Int(nimManager.unreadMessageCount())
|
||||
}
|
||||
|
||||
var allRecentSessions: [NIMRecentSession] {
|
||||
return nimManager.allRecentSessions() ?? []
|
||||
}
|
||||
|
||||
// MARK: - 推送管理
|
||||
|
||||
func updateApnsToken(_ deviceToken: Data) {
|
||||
nimManager.updateApnsToken(deviceToken)
|
||||
}
|
||||
|
||||
func handlePushNotification(_ userInfo: [AnyHashable: Any]) -> Bool {
|
||||
return nimManager.handlePushNotification(userInfo)
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
private func handleLoginStatusChange(_ status: NIMSDKLoginStatus) {
|
||||
switch status {
|
||||
case .notLogin:
|
||||
print("未登录")
|
||||
case .logging:
|
||||
print("登录中")
|
||||
case .logined:
|
||||
print("已登录")
|
||||
case .logout:
|
||||
print("已登出")
|
||||
case .kickout:
|
||||
print("被踢出")
|
||||
case .autoLoginFailed:
|
||||
print("自动登录失败")
|
||||
@unknown default:
|
||||
print("未知状态")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Swift 扩展
|
||||
|
||||
extension NIMSDKService {
|
||||
|
||||
// 创建礼物消息的便捷方法
|
||||
func createGiftMessage(giftId: String, giftName: String, giftCount: Int) -> NIMMessage? {
|
||||
let attachment = AttachmentModel()
|
||||
attachment.first = Int32(CustomMessageType_Gift)
|
||||
attachment.second = Int32(Custom_Message_Sub_Gift_Send)
|
||||
attachment.data = [
|
||||
"giftId": giftId,
|
||||
"giftName": giftName,
|
||||
"giftCount": giftCount
|
||||
]
|
||||
|
||||
return nimManager.createCustomMessage(with: attachment)
|
||||
}
|
||||
|
||||
// 发送礼物消息的便捷方法
|
||||
func sendGift(giftId: String, giftName: String, giftCount: Int, to sessionId: String, completion: @escaping (Error?) -> Void) {
|
||||
guard let message = createGiftMessage(giftId: giftId, giftName: giftName, giftCount: giftCount) else {
|
||||
completion(NSError(domain: "NIMSDKService", code: -1, userInfo: [NSLocalizedDescriptionKey: "创建礼物消息失败"]))
|
||||
return
|
||||
}
|
||||
|
||||
let session = NIMSession(session: sessionId, type: .p2P)
|
||||
nimManager.send(message, to: session) { error in
|
||||
DispatchQueue.main.async {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Swift 视图控制器使用示例
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
|
||||
class ChatViewController: UIViewController {
|
||||
|
||||
private let nimService = NIMSDKService.shared
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupNIMSDK()
|
||||
}
|
||||
|
||||
private func setupNIMSDK() {
|
||||
// 检查登录状态
|
||||
if !nimService.isLogined {
|
||||
// 执行登录
|
||||
nimService.login(account: "user123", token: "token123") { [weak self] error in
|
||||
if let error = error {
|
||||
print("登录失败: \(error)")
|
||||
} else {
|
||||
print("登录成功")
|
||||
self?.startChat()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startChat()
|
||||
}
|
||||
}
|
||||
|
||||
private func startChat() {
|
||||
// 开始聊天功能
|
||||
print("开始聊天")
|
||||
}
|
||||
|
||||
// MARK: - 发送消息示例
|
||||
|
||||
@IBAction func sendTextMessage(_ sender: UIButton) {
|
||||
nimService.sendTextMessage("Hello from Swift!", to: "receiver123") { error in
|
||||
if let error = error {
|
||||
print("发送失败: \(error)")
|
||||
} else {
|
||||
print("发送成功")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func sendGiftMessage(_ sender: UIButton) {
|
||||
nimService.sendGift(giftId: "123", giftName: "玫瑰花", giftCount: 1, to: "receiver123") { error in
|
||||
if let error = error {
|
||||
print("发送礼物失败: \(error)")
|
||||
} else {
|
||||
print("发送礼物成功")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 获取消息信息
|
||||
|
||||
func updateUnreadCount() {
|
||||
let count = nimService.unreadMessageCount
|
||||
print("未读消息数: \(count)")
|
||||
}
|
||||
|
||||
func loadRecentSessions() {
|
||||
let sessions = nimService.allRecentSessions
|
||||
print("会话数量: \(sessions.count)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Swift AppDelegate 集成
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
private let nimService = NIMSDKService.shared
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
// NIMSDK已经在NIMSDKService中初始化
|
||||
// 这里可以添加其他初始化代码
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - 推送处理
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
nimService.updateApnsToken(deviceToken)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
|
||||
let handled = nimService.handlePushNotification(userInfo)
|
||||
completionHandler(handled ? .newData : .noData)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 配置说明
|
||||
|
||||
### 4.1 环境配置
|
||||
|
||||
```objc
|
||||
// DEBUG环境配置
|
||||
#ifdef DEBUG
|
||||
config.apnsCername = @"pikoDevelopPush";
|
||||
config.enabledHttpsForInfo = NO;
|
||||
config.enabledHttpsForMessage = NO;
|
||||
#else
|
||||
config.apnsCername = @"newPiko";
|
||||
config.enabledHttpsForInfo = YES;
|
||||
config.enabledHttpsForMessage = YES;
|
||||
#endif
|
||||
```
|
||||
|
||||
### 4.2 AppKey配置
|
||||
|
||||
```objc
|
||||
// 从常量文件获取AppKey
|
||||
config.appKey = KeyWithType(KeyType_NetEase);
|
||||
|
||||
// 或者直接设置
|
||||
config.appKey = @"your_app_key_here";
|
||||
```
|
||||
|
||||
### 4.3 推送配置
|
||||
|
||||
确保在项目中正确配置了APNS证书,并在配置中设置正确的证书名称。
|
||||
|
||||
## 5. 最佳实践
|
||||
|
||||
### 5.1 初始化时机
|
||||
|
||||
- 在App启动时尽早初始化NIMSDK
|
||||
- 确保在用户登录前完成初始化
|
||||
|
||||
### 5.2 错误处理
|
||||
|
||||
- 对所有异步操作添加错误处理
|
||||
- 在UI线程中处理回调结果
|
||||
|
||||
### 5.3 内存管理
|
||||
|
||||
- 及时移除不需要的代理
|
||||
- 避免循环引用
|
||||
|
||||
### 5.4 状态管理
|
||||
|
||||
- 监听登录状态变化
|
||||
- 根据状态变化更新UI
|
||||
|
||||
### 5.5 Swift集成
|
||||
|
||||
- 使用桥接头文件正确导入Objective-C类
|
||||
- 在Swift中创建便捷的包装方法
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### 6.1 编译错误
|
||||
**Q: 编译时提示找不到NIMSDK头文件**
|
||||
A: 确保正确导入了NIMSDK框架,并在桥接头文件中正确导入。
|
||||
|
||||
### 6.2 初始化失败
|
||||
**Q: NIMSDK初始化失败**
|
||||
A: 检查AppKey是否正确,网络连接是否正常。
|
||||
|
||||
### 6.3 登录问题
|
||||
**Q: 登录后立即被踢出**
|
||||
A: 检查账号是否在其他设备登录,或者Token是否过期。
|
||||
|
||||
### 6.4 Swift桥接问题
|
||||
**Q: Swift中无法使用NIMSDKManager**
|
||||
A: 确保在桥接头文件中正确导入了相关头文件。
|
||||
|
||||
### 6.5 推送问题
|
||||
**Q: 推送通知无法接收**
|
||||
A: 检查APNS证书配置,确保设备Token正确上传。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2024年12月
|
||||
**维护人员**: 开发团队
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,559 +0,0 @@
|
||||
# NIMSDK 集成说明文档
|
||||
|
||||
## 目录
|
||||
- [1. 项目概述](#1-项目概述)
|
||||
- [2. NIMSDK导入配置](#2-nimsdk导入配置)
|
||||
- [3. NIMSDK初始化流程](#3-nimsdk初始化流程)
|
||||
- [4. 关键配置参数说明](#4-关键配置参数说明)
|
||||
- [5. 自定义消息处理](#5-自定义消息处理)
|
||||
- [6. 登录管理](#6-登录管理)
|
||||
- [7. 消息接收处理](#7-消息接收处理)
|
||||
- [8. 推送通知集成](#8-推送通知集成)
|
||||
- [9. 最佳实践](#9-最佳实践)
|
||||
- [10. 常见问题](#10-常见问题)
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目简介
|
||||
YuMi是一个基于iOS平台的社交应用,使用Objective-C开发,采用MVP架构模式。项目集成了网易云信SDK(NIMSDK)用于实现即时通讯功能。
|
||||
|
||||
### 1.2 技术栈
|
||||
- **开发语言**: Objective-C
|
||||
- **架构模式**: MVP (Model-View-Presenter)
|
||||
- **即时通讯**: 网易云信 NIMSDK_LITE
|
||||
- **依赖管理**: CocoaPods
|
||||
- **最低支持版本**: iOS 11.0
|
||||
|
||||
### 1.3 主要功能模块
|
||||
- 用户登录注册
|
||||
- 即时消息通讯
|
||||
- 聊天室功能
|
||||
- 动态发布
|
||||
- 个人中心
|
||||
- 房间直播
|
||||
|
||||
## 2. NIMSDK导入配置
|
||||
|
||||
### 2.1 Podfile配置
|
||||
|
||||
在项目的`Podfile`中添加NIMSDK依赖:
|
||||
|
||||
```ruby
|
||||
# 云信SDK
|
||||
pod 'NIMSDK_LITE'
|
||||
```
|
||||
|
||||
### 2.2 头文件导入
|
||||
|
||||
在需要使用NIMSDK的文件中导入头文件:
|
||||
|
||||
```objc
|
||||
#import <NIMSDK/NIMSDK.h>
|
||||
```
|
||||
|
||||
### 2.3 项目结构
|
||||
|
||||
```
|
||||
YuMi/
|
||||
├── Appdelegate/
|
||||
│ ├── AppDelegate.m # 主应用代理
|
||||
│ └── AppDelegate+ThirdConfig.m # 第三方SDK配置
|
||||
├── Modules/
|
||||
│ ├── YMMessage/ # 消息模块
|
||||
│ │ └── Tool/
|
||||
│ │ └── CustomAttachmentDecoder # 自定义消息解码器
|
||||
│ ├── YMTabbar/ # 主界面模块
|
||||
│ └── YMRoom/ # 房间模块
|
||||
└── Global/
|
||||
└── YUMIConstant.h # 常量定义
|
||||
```
|
||||
|
||||
## 3. NIMSDK初始化流程
|
||||
|
||||
### 3.1 初始化时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as AppDelegate
|
||||
participant ThirdConfig as AppDelegate+ThirdConfig
|
||||
participant NIMSDK as NIMSDK
|
||||
participant Config as NIMSDKConfig
|
||||
participant Decoder as CustomAttachmentDecoder
|
||||
|
||||
App->>ThirdConfig: initThirdConfig()
|
||||
ThirdConfig->>ThirdConfig: configNIMSDK()
|
||||
ThirdConfig->>NIMSDK: registerWithOption(option)
|
||||
ThirdConfig->>Decoder: registerCustomDecoder()
|
||||
ThirdConfig->>Config: shouldConsiderRevokedMessageUnreadCount = YES
|
||||
ThirdConfig->>Config: setShouldSyncStickTopSessionInfos(YES)
|
||||
ThirdConfig->>Config: enabledHttpsForInfo = NO (DEBUG)
|
||||
ThirdConfig->>Config: enabledHttpsForMessage = NO (DEBUG)
|
||||
|
||||
Note over App: 应用启动完成
|
||||
App->>App: loadMainPage()
|
||||
App->>App: 检查登录状态
|
||||
App->>NIMSDK: 自动登录或手动登录
|
||||
```
|
||||
|
||||
### 3.2 初始化代码实现
|
||||
|
||||
#### 3.2.1 主应用代理初始化
|
||||
|
||||
```objc
|
||||
// AppDelegate.m
|
||||
- (BOOL)application:(UIApplication *)application
|
||||
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
|
||||
// 初始化第三方SDK配置
|
||||
[self initThirdConfig];
|
||||
|
||||
// 其他初始化代码...
|
||||
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 NIMSDK配置方法
|
||||
|
||||
```objc
|
||||
// AppDelegate+ThirdConfig.m
|
||||
- (void)configNIMSDK {
|
||||
// 1. 获取云信AppKey
|
||||
NSString *appKey = KeyWithType(KeyType_NetEase);
|
||||
NIMSDKOption *option = [NIMSDKOption optionWithAppKey:appKey];
|
||||
|
||||
// 2. 配置APNS证书名称
|
||||
#ifdef DEBUG
|
||||
option.apnsCername = @"pikoDevelopPush";
|
||||
#else
|
||||
option.apnsCername = @"newPiko";
|
||||
#endif
|
||||
|
||||
// 3. 注册SDK
|
||||
[[NIMSDK sharedSDK] registerWithOption:option];
|
||||
|
||||
// 4. 注册自定义消息解码器
|
||||
[NIMCustomObject registerCustomDecoder:[[CustomAttachmentDecoder alloc] init]];
|
||||
|
||||
// 5. 配置SDK参数
|
||||
[NIMSDKConfig sharedConfig].shouldConsiderRevokedMessageUnreadCount = YES;
|
||||
[[NIMSDKConfig sharedConfig] setShouldSyncStickTopSessionInfos:YES];
|
||||
|
||||
// 6. DEBUG模式下禁用HTTPS
|
||||
#ifdef DEBUG
|
||||
[NIMSDKConfig sharedConfig].enabledHttpsForInfo = NO;
|
||||
[NIMSDKConfig sharedConfig].enabledHttpsForMessage = NO;
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 关键配置参数说明
|
||||
|
||||
### 4.1 AppKey配置
|
||||
|
||||
项目支持多环境配置,通过`YUMIConstant.m`中的`KeyWithType`方法获取:
|
||||
|
||||
```objc
|
||||
// 测试环境
|
||||
@(KeyType_NetEase): @"79bc37000f4018a2a24ea9dc6ca08d32"
|
||||
|
||||
// 生产环境
|
||||
@(KeyType_NetEase): @"7371d729710cd6ce3a50163b956b5eb6"
|
||||
```
|
||||
|
||||
### 4.2 APNS推送配置
|
||||
|
||||
```objc
|
||||
// 开发环境
|
||||
option.apnsCername = @"pikoDevelopPush";
|
||||
|
||||
// 生产环境
|
||||
option.apnsCername = @"newPiko";
|
||||
```
|
||||
|
||||
### 4.3 SDK配置参数详解
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
|------|----|----|
|
||||
| `shouldConsiderRevokedMessageUnreadCount` | `YES` | 撤回消息计入未读数 |
|
||||
| `shouldSyncStickTopSessionInfos` | `YES` | 同步置顶会话信息 |
|
||||
| `enabledHttpsForInfo` | `NO` (DEBUG) | DEBUG模式禁用HTTPS信息传输 |
|
||||
| `enabledHttpsForMessage` | `NO` (DEBUG) | DEBUG模式禁用HTTPS消息传输 |
|
||||
|
||||
## 5. 自定义消息处理
|
||||
|
||||
### 5.1 自定义附件解码器
|
||||
|
||||
#### 5.1.1 解码器接口定义
|
||||
|
||||
```objc
|
||||
// CustomAttachmentDecoder.h
|
||||
@interface CustomAttachmentDecoder : NSObject<NIMCustomAttachmentCoding>
|
||||
@end
|
||||
```
|
||||
|
||||
#### 5.1.2 解码器实现
|
||||
|
||||
```objc
|
||||
// CustomAttachmentDecoder.m
|
||||
- (id<NIMCustomAttachment>)decodeAttachment:(NSString *)content {
|
||||
id<NIMCustomAttachment> attachment;
|
||||
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
if (data) {
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
|
||||
options:0
|
||||
error:nil];
|
||||
if ([dict isKindOfClass:[NSDictionary class]]) {
|
||||
int first = [dict[@"first"] intValue];
|
||||
int second = [dict[@"second"] intValue];
|
||||
id originalData = dict[@"data"];
|
||||
|
||||
AttachmentModel *model = [[AttachmentModel alloc] init];
|
||||
model.first = (short)first;
|
||||
model.second = (short)second;
|
||||
model.data = originalData;
|
||||
|
||||
attachment = model;
|
||||
}
|
||||
}
|
||||
return attachment;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 消息类型定义
|
||||
|
||||
自定义消息通过`AttachmentModel`定义结构:
|
||||
|
||||
```objc
|
||||
@interface AttachmentModel : NSObject<NIMCustomAttachment>
|
||||
|
||||
@property (nonatomic, assign) short first; // 消息类型标识
|
||||
@property (nonatomic, assign) short second; // 消息子类型标识
|
||||
@property (nonatomic, strong) id data; // 消息数据内容
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 5.3 消息类型示例
|
||||
|
||||
```objc
|
||||
// 打招呼消息
|
||||
if (attachment.first == CustomMessageType_FindNew &&
|
||||
attachment.second == Custom_Message_Find_New_Greet_New_User) {
|
||||
// 处理新用户打招呼消息
|
||||
FindNewGreetMessageModel *greetInfo = [FindNewGreetMessageModel modelWithDictionary:attachment.data];
|
||||
// 显示打招呼弹窗
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 登录管理
|
||||
|
||||
### 6.1 登录状态检查
|
||||
|
||||
```objc
|
||||
// 检查是否已登录
|
||||
if ([NIMSDK sharedSDK].loginManager.isLogined) {
|
||||
// 已登录,执行相关操作
|
||||
} else {
|
||||
// 未登录,跳转登录页面
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 自动登录处理
|
||||
|
||||
```objc
|
||||
// NIMLoginManagerDelegate
|
||||
- (void)onAutoLoginFailed:(NSError *)error {
|
||||
// 如果非上次登录设备 autoLogin 会返回 417
|
||||
if (error.code == 417) {
|
||||
@weakify(self);
|
||||
AccountModel* accountModel = [AccountInfoStorage instance].getCurrentAccountInfo;
|
||||
[[NIMSDK sharedSDK].loginManager login:accountModel.uid
|
||||
token:accountModel.netEaseToken
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
@strongify(self);
|
||||
[self.presenter logout];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
[self.presenter logout];
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 踢出处理
|
||||
|
||||
```objc
|
||||
// NIMLoginManagerDelegate
|
||||
- (void)onKickout:(NIMLoginKickoutResult *)result {
|
||||
// 显示踢出提示
|
||||
[XNDJTDDLoadingTool showErrorWithMessage:YMLocalizedString(@"TabbarViewController0")];
|
||||
|
||||
// 清理房间状态
|
||||
if ([XPRoomMiniManager shareManager].getRoomInfo) {
|
||||
[[RtcManager instance] exitRoom];
|
||||
[[NIMSDK sharedSDK].chatroomManager exitChatroom:roomId completion:nil];
|
||||
[self.roomMineView hiddenRoomMiniView];
|
||||
}
|
||||
|
||||
// 执行登出
|
||||
[self.presenter logout];
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 手动登录
|
||||
|
||||
```objc
|
||||
// 执行登录
|
||||
[[NIMSDK sharedSDK].loginManager login:accountModel.uid
|
||||
token:accountModel.netEaseToken
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
// 登录失败处理
|
||||
NSLog(@"登录失败: %@", error);
|
||||
} else {
|
||||
// 登录成功处理
|
||||
NSLog(@"登录成功");
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
## 7. 消息接收处理
|
||||
|
||||
### 7.1 消息接收代理
|
||||
|
||||
```objc
|
||||
// NIMChatManagerDelegate
|
||||
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
|
||||
if ([AccountInfoStorage instance].getTicket.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NIMMessage *message in messages) {
|
||||
if (message.session.sessionType == NIMSessionTypeP2P) {
|
||||
if (message.messageType == NIMMessageTypeCustom) {
|
||||
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
|
||||
if (obj.attachment != nil && [obj.attachment isKindOfClass:[AttachmentModel class]]) {
|
||||
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
|
||||
// 处理自定义消息
|
||||
[self handleCustomMessage:attachment];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 自定义消息处理
|
||||
|
||||
```objc
|
||||
- (void)handleCustomMessage:(AttachmentModel *)attachment {
|
||||
switch (attachment.first) {
|
||||
case CustomMessageType_FindNew:
|
||||
[self handleFindNewMessage:attachment];
|
||||
break;
|
||||
case CustomMessageType_Gift:
|
||||
[self handleGiftMessage:attachment];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleFindNewMessage:(AttachmentModel *)attachment {
|
||||
if (attachment.second == Custom_Message_Find_New_Greet_New_User) {
|
||||
FindNewGreetMessageModel *greetInfo = [FindNewGreetMessageModel modelWithDictionary:attachment.data];
|
||||
if (greetInfo.uid.integerValue != [AccountInfoStorage instance].getUid.integerValue) {
|
||||
// 显示打招呼弹窗
|
||||
[self showGreetAlert:greetInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 广播消息处理
|
||||
|
||||
```objc
|
||||
// NIMBroadcastManagerDelegate
|
||||
- (void)onReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
|
||||
if ([AccountInfoStorage instance].getUid.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理广播消息
|
||||
NSString *content = broadcastMessage.content;
|
||||
// 解析并处理广播内容
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 推送通知集成
|
||||
|
||||
### 8.1 推送权限申请
|
||||
|
||||
```objc
|
||||
- (void)registerNot {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert |
|
||||
UNAuthorizationOptionBadge |
|
||||
UNAuthorizationOptionSound)
|
||||
completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (granted) {
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
|
||||
if (settings.authorizationStatus == UNAuthorizationStatusAuthorized) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 设备Token更新
|
||||
|
||||
```objc
|
||||
- (void)application:(UIApplication *)app
|
||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
// 上传devicetoken至云信服务器
|
||||
[[NIMSDK sharedSDK] updateApnsToken:deviceToken];
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 推送消息处理
|
||||
|
||||
```objc
|
||||
- (void)application:(UIApplication *)application
|
||||
didReceiveRemoteNotification:(NSDictionary *)userInfo
|
||||
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
|
||||
|
||||
NSString *data = userInfo[@"data"];
|
||||
if (data) {
|
||||
NSDictionary *dataDic = [data mj_JSONObject];
|
||||
NSString *userId = dataDic[@"uid"];
|
||||
if (userId) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
|
||||
dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kOpenRoomNotification
|
||||
object:nil
|
||||
userInfo:@{@"type": @"kOpenChat",
|
||||
@"uid": userId,
|
||||
@"isNoAttention": @(YES)}];
|
||||
ClientConfig *config = [ClientConfig shareConfig];
|
||||
config.pushChatId = userId;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(UIBackgroundFetchResultNewData);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 应用状态处理
|
||||
|
||||
```objc
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
// 设置应用角标为未读消息数
|
||||
NSInteger count = [NIMSDK sharedSDK].conversationManager.allUnreadCount;
|
||||
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:count];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
// 应用激活时清除角标
|
||||
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"kAppDidBecomeActive" object:nil];
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 最佳实践
|
||||
|
||||
### 9.1 初始化最佳实践
|
||||
|
||||
1. **在应用启动时初始化**: 在`AppDelegate`的`didFinishLaunchingWithOptions`中调用
|
||||
2. **配置环境参数**: 区分开发和生产环境的配置
|
||||
3. **注册自定义解码器**: 在SDK注册后立即注册自定义解码器
|
||||
4. **设置代理**: 在适当的时机添加和移除代理
|
||||
|
||||
### 9.2 登录管理最佳实践
|
||||
|
||||
1. **自动登录**: 优先使用自动登录,减少用户等待时间
|
||||
2. **错误处理**: 对登录失败进行适当的错误处理和重试
|
||||
3. **状态同步**: 保持本地登录状态与服务器状态同步
|
||||
4. **踢出处理**: 妥善处理被踢出的情况,清理相关状态
|
||||
|
||||
### 9.3 消息处理最佳实践
|
||||
|
||||
1. **消息过滤**: 根据业务需求过滤不需要的消息
|
||||
2. **自定义消息**: 合理设计自定义消息结构
|
||||
3. **性能优化**: 避免在消息处理中进行耗时操作
|
||||
4. **内存管理**: 及时释放不需要的消息对象
|
||||
|
||||
### 9.4 推送通知最佳实践
|
||||
|
||||
1. **权限申请**: 在合适的时机申请推送权限
|
||||
2. **Token更新**: 及时更新设备Token
|
||||
3. **消息解析**: 正确解析推送消息内容
|
||||
4. **状态处理**: 根据应用状态处理推送消息
|
||||
|
||||
## 10. 常见问题
|
||||
|
||||
### 10.1 初始化问题
|
||||
|
||||
**Q: SDK初始化失败怎么办?**
|
||||
A: 检查AppKey是否正确,网络连接是否正常,证书配置是否正确。
|
||||
|
||||
**Q: 自定义解码器注册失败?**
|
||||
A: 确保在SDK注册后注册解码器,解码器类实现了正确的协议。
|
||||
|
||||
### 10.2 登录问题
|
||||
|
||||
**Q: 自动登录失败,错误码417?**
|
||||
A: 这是正常情况,表示非上次登录设备,需要重新输入账号密码登录。
|
||||
|
||||
**Q: 登录后立即被踢出?**
|
||||
A: 检查账号是否在其他设备登录,或者Token是否过期。
|
||||
|
||||
### 10.3 消息问题
|
||||
|
||||
**Q: 自定义消息无法解析?**
|
||||
A: 检查消息格式是否正确,解码器是否正确注册。
|
||||
|
||||
**Q: 消息发送失败?**
|
||||
A: 检查网络连接,登录状态,以及消息内容格式。
|
||||
|
||||
### 10.4 推送问题
|
||||
|
||||
**Q: 推送通知无法接收?**
|
||||
A: 检查推送权限是否开启,证书配置是否正确,设备Token是否正确上传。
|
||||
|
||||
**Q: 推送消息解析错误?**
|
||||
A: 检查推送消息格式,确保解析逻辑正确。
|
||||
|
||||
## 11. 总结
|
||||
|
||||
本项目对NIMSDK的集成非常完整,包括:
|
||||
|
||||
1. **完整的初始化流程**: 从SDK注册到配置参数设置
|
||||
2. **自定义消息处理**: 实现了自定义附件解码器
|
||||
3. **登录状态管理**: 包含自动登录、踢出处理等
|
||||
4. **推送通知集成**: 完整的APNS推送处理
|
||||
5. **消息接收处理**: 支持自定义消息类型的处理
|
||||
6. **多环境配置**: 区分开发和生产环境的配置
|
||||
|
||||
整个集成方案遵循了NIMSDK的最佳实践,代码结构清晰,功能完整,为项目的即时通讯功能提供了坚实的基础。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2024年12月
|
||||
**维护人员**: 开发团队
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,262 +0,0 @@
|
||||
# OAuth/Ticket 认证系统 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了 YuMi 应用中 OAuth 认证和 Ticket 会话管理的完整流程。系统采用两阶段认证机制:
|
||||
1. **OAuth 阶段**:用户登录获取 `access_token`
|
||||
2. **Ticket 阶段**:使用 `access_token` 获取业务会话 `ticket`
|
||||
|
||||
## 认证流程架构
|
||||
|
||||
### 核心组件
|
||||
- **AccountInfoStorage**: 负责账户信息和 ticket 的本地存储
|
||||
- **HttpRequestHelper**: 网络请求管理,自动添加认证头
|
||||
- **Api+Login**: 登录相关 API 接口
|
||||
- **Api+Main**: Ticket 获取相关 API 接口
|
||||
|
||||
### 认证数据模型
|
||||
|
||||
#### AccountModel
|
||||
```objc
|
||||
@interface AccountModel : PIBaseModel
|
||||
@property (nonatomic, assign) NSString *uid; // 用户唯一标识
|
||||
@property (nonatomic, copy) NSString *jti; // JWT ID
|
||||
@property (nonatomic, copy) NSString *token_type; // Token 类型
|
||||
@property (nonatomic, copy) NSString *refresh_token; // 刷新令牌
|
||||
@property (nonatomic, copy) NSString *netEaseToken; // 网易云信令牌
|
||||
@property (nonatomic, copy) NSString *access_token; // OAuth 访问令牌
|
||||
@property (nonatomic, assign) NSNumber *expires_in; // 过期时间
|
||||
@end
|
||||
```
|
||||
|
||||
## API 接口详情
|
||||
|
||||
### 1. OAuth 登录接口
|
||||
|
||||
#### 1.1 手机验证码登录
|
||||
```objc
|
||||
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
|
||||
phone:(NSString *)phone
|
||||
code:(NSString *)code
|
||||
client_secret:(NSString *)client_secret
|
||||
version:(NSString *)version
|
||||
client_id:(NSString *)client_id
|
||||
grant_type:(NSString *)grant_type
|
||||
phoneAreaCode:(NSString *)phoneAreaCode;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /oauth/token`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| phone | String | 是 | 手机号(DES加密) |
|
||||
| code | String | 是 | 验证码 |
|
||||
| client_secret | String | 是 | 客户端密钥,固定值:"uyzjdhds" |
|
||||
| version | String | 是 | 版本号,固定值:"1" |
|
||||
| client_id | String | 是 | 客户端ID,固定值:"erban-client" |
|
||||
| grant_type | String | 是 | 授权类型,验证码登录为:"sms_code" |
|
||||
| phoneAreaCode | String | 是 | 手机区号 |
|
||||
|
||||
**返回数据**: AccountModel 对象
|
||||
|
||||
#### 1.2 手机密码登录
|
||||
```objc
|
||||
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion
|
||||
phone:(NSString *)phone
|
||||
password:(NSString *)password
|
||||
client_secret:(NSString *)client_secret
|
||||
version:(NSString *)version
|
||||
client_id:(NSString *)client_id
|
||||
grant_type:(NSString *)grant_type;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /oauth/token`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| phone | String | 是 | 手机号(DES加密) |
|
||||
| password | String | 是 | 密码(DES加密) |
|
||||
| client_secret | String | 是 | 客户端密钥 |
|
||||
| version | String | 是 | 版本号 |
|
||||
| client_id | String | 是 | 客户端ID |
|
||||
| grant_type | String | 是 | 授权类型,密码登录为:"password" |
|
||||
|
||||
#### 1.3 第三方登录
|
||||
```objc
|
||||
+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)completion
|
||||
openid:(NSString *)openid
|
||||
unionid:(NSString *)unionid
|
||||
access_token:(NSString *)access_token
|
||||
type:(NSString *)type;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /acc/third/login`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| openid | String | 是 | 第三方平台用户唯一标识 |
|
||||
| unionid | String | 是 | 第三方平台联合ID |
|
||||
| access_token | String | 是 | 第三方平台访问令牌 |
|
||||
| type | String | 是 | 第三方平台类型(1:Apple, 2:Facebook, 3:Google等) |
|
||||
|
||||
### 2. Ticket 获取接口
|
||||
|
||||
#### 2.1 获取 Ticket
|
||||
```objc
|
||||
+ (void)requestTicket:(HttpRequestHelperCompletion)completion
|
||||
access_token:(NSString *)accessToken
|
||||
issue_type:(NSString *)issueType;
|
||||
```
|
||||
|
||||
**接口路径**: `POST /oauth/ticket`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| access_token | String | 是 | OAuth 登录获取的访问令牌 |
|
||||
| issue_type | String | 是 | 签发类型,固定值:"multi" |
|
||||
|
||||
**返回数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"tickets": [
|
||||
{
|
||||
"ticket": "eyJhbGciOiJIUzI1NiJ9..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. HTTP 请求头配置
|
||||
|
||||
所有业务 API 请求都会自动添加以下请求头:
|
||||
|
||||
```objc
|
||||
// 在 HttpRequestHelper 中自动配置
|
||||
- (void)setupHeader {
|
||||
AFHTTPSessionManager *client = [HttpRequestHelper requestManager];
|
||||
|
||||
// 用户ID头
|
||||
if ([[AccountInfoStorage instance] getUid].length > 0) {
|
||||
[client.requestSerializer setValue:[[AccountInfoStorage instance] getUid]
|
||||
forHTTPHeaderField:@"pub_uid"];
|
||||
}
|
||||
|
||||
// Ticket 认证头
|
||||
if ([[AccountInfoStorage instance] getTicket].length > 0) {
|
||||
[client.requestSerializer setValue:[[AccountInfoStorage instance] getTicket]
|
||||
forHTTPHeaderField:@"pub_ticket"];
|
||||
}
|
||||
|
||||
// 其他公共头
|
||||
[client.requestSerializer setValue:[NSBundle uploadLanguageText]
|
||||
forHTTPHeaderField:@"Accept-Language"];
|
||||
[client.requestSerializer setValue:PI_App_Version
|
||||
forHTTPHeaderField:@"App-Version"];
|
||||
}
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 完整登录流程示例
|
||||
|
||||
```objc
|
||||
// 1. 用户登录获取 access_token
|
||||
[Api loginWithCode:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200) {
|
||||
// 保存账户信息
|
||||
AccountModel *accountModel = [AccountModel modelWithDictionary:data.data];
|
||||
[[AccountInfoStorage instance] saveAccountInfo:accountModel];
|
||||
|
||||
// 2. 使用 access_token 获取 ticket
|
||||
[Api requestTicket:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200) {
|
||||
NSArray *tickets = [data.data valueForKey:@"tickets"];
|
||||
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
|
||||
|
||||
// 保存 ticket
|
||||
[[AccountInfoStorage instance] saveTicket:ticket];
|
||||
|
||||
// 3. 登录成功,可以进行业务操作
|
||||
[self navigateToMainPage];
|
||||
}
|
||||
} access_token:accountModel.access_token issue_type:@"multi"];
|
||||
}
|
||||
} phone:encryptedPhone
|
||||
code:verificationCode
|
||||
client_secret:@"uyzjdhds"
|
||||
version:@"1"
|
||||
client_id:@"erban-client"
|
||||
grant_type:@"sms_code"
|
||||
phoneAreaCode:areaCode];
|
||||
```
|
||||
|
||||
### 自动登录流程
|
||||
|
||||
```objc
|
||||
- (void)autoLogin {
|
||||
// 检查本地是否有账户信息
|
||||
AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo];
|
||||
if (accountModel == nil || accountModel.access_token == nil) {
|
||||
[self tokenInvalid]; // 跳转到登录页
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有有效的 ticket
|
||||
if ([[AccountInfoStorage instance] getTicket].length > 0) {
|
||||
[[self getView] autoLoginSuccess];
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 access_token 重新获取 ticket
|
||||
[Api requestTicket:^(BaseModel * _Nonnull data) {
|
||||
NSArray *tickets = [data.data valueForKey:@"tickets"];
|
||||
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
|
||||
[[AccountInfoStorage instance] saveTicket:ticket];
|
||||
[[self getView] autoLoginSuccess];
|
||||
} fail:^(NSInteger code, NSString * _Nullable msg) {
|
||||
[self logout]; // ticket 获取失败,重新登录
|
||||
} access_token:accountModel.access_token issue_type:@"multi"];
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 401 未授权错误
|
||||
当接收到 401 状态码时,系统会自动处理:
|
||||
|
||||
```objc
|
||||
// 在 HttpRequestHelper 中
|
||||
if (response && response.statusCode == 401) {
|
||||
failure(response.statusCode, YMLocalizedString(@"HttpRequestHelper7"));
|
||||
// 通常需要重新登录
|
||||
}
|
||||
```
|
||||
|
||||
### Ticket 过期处理
|
||||
- Ticket 过期时服务器返回 401 错误
|
||||
- 客户端应该使用保存的 `access_token` 重新获取 ticket
|
||||
- 如果 `access_token` 也过期,则需要用户重新登录
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **数据加密**: 敏感信息(手机号、密码)使用 DES 加密传输
|
||||
2. **本地存储**:
|
||||
- `access_token` 存储在文件系统中
|
||||
- `ticket` 存储在内存中,应用重启需重新获取
|
||||
3. **请求头**: 所有业务请求自动携带 `pub_uid` 和 `pub_ticket` 头
|
||||
4. **错误处理**: 建立完善的 401 错误重试机制
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `YuMi/Structure/MVP/Model/AccountInfoStorage.h/m` - 账户信息存储管理
|
||||
- `YuMi/Modules/YMLogin/Api/Api+Login.h/m` - 登录相关接口
|
||||
- `YuMi/Modules/YMTabbar/Api/Api+Main.h/m` - Ticket 获取接口
|
||||
- `YuMi/Network/HttpRequestHelper.h/m` - 网络请求管理
|
||||
- `YuMi/Structure/MVP/Model/AccountModel.h/m` - 账户数据模型
|
@@ -1,247 +0,0 @@
|
||||
# PublicRoomManager 使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
`PublicRoomManager` 是一个常驻单例,负责管理用户进入公共聊天房间的逻辑。它会在用户登录成功后自动初始化,并在用户登出时自动清理。
|
||||
|
||||
## 主要功能
|
||||
|
||||
1. **自动初始化**: 用户登录成功后自动初始化
|
||||
2. **自动进房**: 根据用户的分区ID自动进入对应的公共聊天房间
|
||||
3. **消息监听**: 监听公共房间的消息
|
||||
4. **自动清理**: 用户登出时自动退出房间并清理状态
|
||||
5. **用户切换处理**: 支持多次登出-登录的重置情况
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 1. 生命周期管理
|
||||
|
||||
```objc
|
||||
// 初始化(在用户登录成功后自动调用)
|
||||
[[PublicRoomManager sharedManager] initialize];
|
||||
|
||||
// 重置(在用户登出时自动调用)
|
||||
[[PublicRoomManager sharedManager] reset];
|
||||
```
|
||||
|
||||
### 2. 状态查询
|
||||
|
||||
```objc
|
||||
// 检查是否已初始化
|
||||
BOOL isInitialized = [[PublicRoomManager sharedManager] isInitialized];
|
||||
|
||||
// 检查是否已在公共房间中
|
||||
BOOL isInPublicRoom = [[PublicRoomManager sharedManager] isInPublicRoom];
|
||||
|
||||
// 获取当前公共房间ID
|
||||
NSString *roomId = [[PublicRoomManager sharedManager] currentPublicRoomId];
|
||||
```
|
||||
|
||||
### 3. 手动控制
|
||||
|
||||
```objc
|
||||
// 手动进入公共房间
|
||||
[[PublicRoomManager sharedManager] enterPublicRoomWithCompletion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"进入公共房间失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"进入公共房间成功");
|
||||
}
|
||||
}];
|
||||
|
||||
// 手动退出公共房间
|
||||
[[PublicRoomManager sharedManager] exitPublicRoomWithCompletion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"退出公共房间失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"退出公共房间成功");
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
## 集成点
|
||||
|
||||
### 1. 登录流程集成
|
||||
|
||||
在 `PILoginManager.m` 的登录成功回调中添加:
|
||||
|
||||
```objc
|
||||
// 初始化公共房间管理器
|
||||
[[PublicRoomManager sharedManager] initialize];
|
||||
```
|
||||
|
||||
### 2. 登出流程集成
|
||||
|
||||
在 `BaseMvpPresenter.m` 的 logout 方法中添加:
|
||||
|
||||
```objc
|
||||
// 重置公共房间管理器
|
||||
[[PublicRoomManager sharedManager] reset];
|
||||
```
|
||||
|
||||
### 3. 用户信息更新集成
|
||||
|
||||
在 `TabbarViewController.m` 的 `getUserInfoSuccess` 方法中添加:
|
||||
|
||||
```objc
|
||||
// 更新公共房间管理器的用户信息
|
||||
[[PublicRoomManager sharedManager] updateUserInfo:userInfo];
|
||||
```
|
||||
|
||||
### 4. 配置更新集成
|
||||
|
||||
在 `ClientConfig.m` 的配置加载完成后添加:
|
||||
|
||||
```objc
|
||||
// 通知公共房间管理器配置已更新
|
||||
[[PublicRoomManager sharedManager] updateConfig];
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 1. 初始化流程
|
||||
|
||||
1. 检查用户是否已登录
|
||||
2. 检查用户信息是否完整(包含 partitionId)
|
||||
3. 检查配置信息是否已加载(包含 publicChatRoomIdMap)
|
||||
4. 注册云信消息代理
|
||||
5. 根据 partitionId 获取对应的 roomId
|
||||
6. 进入公共聊天房间
|
||||
|
||||
### 2. 进房流程
|
||||
|
||||
1. 创建 NIMChatroomEnterRequest
|
||||
2. 设置用户扩展信息(头像、昵称、等级等)
|
||||
3. 调用云信 SDK 进入房间
|
||||
4. 处理进房成功/失败回调
|
||||
|
||||
### 3. 消息处理
|
||||
|
||||
1. 实现 NIMChatManagerDelegate
|
||||
2. 过滤公共房间消息
|
||||
3. 处理消息内容
|
||||
|
||||
### 4. 清理流程
|
||||
|
||||
1. 退出公共聊天房间
|
||||
2. 移除云信代理
|
||||
3. 重置所有状态
|
||||
|
||||
## 配置要求
|
||||
|
||||
### 1. 用户信息要求
|
||||
|
||||
用户信息必须包含 `partitionId` 字段:
|
||||
|
||||
```objc
|
||||
UserInfoModel *userInfo = [AccountInfoStorage instance].getHomeUserInfo;
|
||||
NSString *partitionId = userInfo.partitionId; // 必须存在
|
||||
```
|
||||
|
||||
### 2. 配置信息要求
|
||||
|
||||
配置信息必须包含 `publicChatRoomIdMap` 字段:
|
||||
|
||||
```objc
|
||||
ClientDataModel *configInfo = [ClientConfig shareConfig].configInfo;
|
||||
NSDictionary *publicChatRoomIdMap = configInfo.publicChatRoomIdMap; // 必须存在
|
||||
```
|
||||
|
||||
`publicChatRoomIdMap` 的格式应该是:
|
||||
|
||||
```json
|
||||
{
|
||||
"1": "roomId1", // 分区1对应的房间ID
|
||||
"2": "roomId2", // 分区2对应的房间ID
|
||||
"3": "roomId3" // 分区3对应的房间ID
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 1. 初始化失败
|
||||
|
||||
- 用户未登录:等待用户登录
|
||||
- 用户信息不完整:等待用户信息更新
|
||||
- 配置信息未加载:等待配置更新
|
||||
|
||||
### 2. 进房失败
|
||||
|
||||
- 网络错误:记录日志,可重试
|
||||
- 房间不存在:记录日志,跳过
|
||||
- 权限不足:记录日志,跳过
|
||||
|
||||
### 3. 用户切换
|
||||
|
||||
- 检测到用户ID变化时自动重置
|
||||
- 清理旧用户状态
|
||||
- 重新初始化新用户
|
||||
|
||||
## 日志输出
|
||||
|
||||
PublicRoomManager 会输出详细的日志信息:
|
||||
|
||||
```
|
||||
PublicRoomManager: 初始化成功,用户ID: 123456, 分区ID: 1
|
||||
PublicRoomManager: 尝试进入公共房间,分区ID: 1, 房间ID: roomId1
|
||||
PublicRoomManager: 进入公共房间成功,房间ID: roomId1
|
||||
PublicRoomManager: 收到公共房间消息: Hello World
|
||||
PublicRoomManager: 检测到用户切换,重置管理器
|
||||
PublicRoomManager: 开始重置
|
||||
PublicRoomManager: 退出公共房间成功
|
||||
PublicRoomManager: 重置完成
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **线程安全**: 所有操作都在主线程执行
|
||||
2. **内存管理**: 使用单例模式,避免内存泄漏
|
||||
3. **错误恢复**: 支持自动重试和错误恢复
|
||||
4. **状态同步**: 确保状态与实际云信状态同步
|
||||
5. **性能优化**: 避免重复初始化和不必要的操作
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 1. 消息处理扩展
|
||||
|
||||
可以在 `onRecvMessages` 方法中添加自定义的消息处理逻辑:
|
||||
|
||||
```objc
|
||||
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
|
||||
for (NIMMessage *message in messages) {
|
||||
if (message.session.sessionType == NIMSessionTypeChatroom) {
|
||||
NSString *sessionId = message.session.sessionId;
|
||||
if ([sessionId isEqualToString:self.currentPublicRoomId]) {
|
||||
// 添加自定义消息处理逻辑
|
||||
[self handlePublicRoomMessage:message];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 状态监听扩展
|
||||
|
||||
可以添加状态变化的通知:
|
||||
|
||||
```objc
|
||||
// 在状态变化时发送通知
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"PublicRoomManagerStateChanged"
|
||||
object:nil
|
||||
userInfo:@{@"isInPublicRoom": @(self.isInPublicRoom)}];
|
||||
```
|
||||
|
||||
### 3. 重试机制扩展
|
||||
|
||||
可以添加更复杂的重试逻辑:
|
||||
|
||||
```objc
|
||||
- (void)retryEnterPublicRoom {
|
||||
if (self.retryCount < 3) {
|
||||
self.retryCount++;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self tryEnterPublicRoom];
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
@@ -1,92 +0,0 @@
|
||||
# 公共房间消息转发功能实现
|
||||
|
||||
## 功能概述
|
||||
|
||||
实现了从 PublicRoomManager 转发特定消息到房间中的功能。当 PublicRoomManager 接收到 attachment.first 为 106 的消息时,会自动转发到当前活跃的房间中。
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. 通知机制
|
||||
|
||||
- 使用 NSNotificationCenter 进行消息转发
|
||||
- 通知名称:`@"MessageFromPublicRoomWithAttachmentNotification"`
|
||||
- 通知对象:NIMMessage 对象
|
||||
|
||||
### 2. 修改的文件
|
||||
|
||||
#### PublicRoomManager.m
|
||||
|
||||
- 在 `onRecvMessages:` 方法中添加转发逻辑
|
||||
- 当检测到 `attachment.first == 106` 时发送通知
|
||||
|
||||
#### XPRoomViewController.m
|
||||
|
||||
- 在 `setupNotifications` 方法中注册通知监听
|
||||
- 添加 `handlePublicRoomMessageForward:` 方法处理转发的消息
|
||||
- 在 `dealloc` 中自动移除通知监听
|
||||
|
||||
#### YUMIConstant.m
|
||||
|
||||
- 添加常量定义:`kMessageFromPublicRoomWithAttachmentNotification`(已添加但当前使用字符串字面量)
|
||||
|
||||
#### XPRoomViewController.h
|
||||
|
||||
- 添加常量声明(已添加但当前使用字符串字面量)
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. **消息接收**:PublicRoomManager 接收到公共房间消息
|
||||
2. **类型检查**:检查 attachment.first 是否为 106
|
||||
3. **发送通知**:如果是 106 类型,发送转发通知
|
||||
4. **接收处理**:XPRoomViewController 接收通知并处理
|
||||
5. **消息显示**:通过现有的消息处理流程显示在房间中
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 发送通知(PublicRoomManager.m)
|
||||
```objective-c
|
||||
if (attachment && attachment.first == 106) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"MessageFromPublicRoomWithAttachmentNotification"
|
||||
object:message];
|
||||
NSLog(@"PublicRoomManager: 转发106类型消息到房间");
|
||||
}
|
||||
```
|
||||
|
||||
### 接收处理(XPRoomViewController.m)
|
||||
```objective-c
|
||||
- (void)handlePublicRoomMessageForward:(NSNotification *)notification {
|
||||
NIMMessage *message = notification.object;
|
||||
if (![message isKindOfClass:[NIMMessage class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查房间是否处于活跃状态
|
||||
if (!self.roomInfo || !self.messageContainerView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用现有的消息处理流程
|
||||
[self.messageContainerView handleNIMCustomMessage:message];
|
||||
}
|
||||
```
|
||||
|
||||
## 测试场景
|
||||
|
||||
1. **正常转发**:公共房间收到106类型消息时正确转发
|
||||
2. **房间状态**:房间最小化、关闭等状态下的处理
|
||||
3. **消息过滤**:确保转发的消息经过正确的过滤流程
|
||||
4. **性能影响**:确保不影响现有消息处理性能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 消息会经过现有的 `isCanDisplayMessage` 过滤
|
||||
2. 支持最小化房间的特殊处理
|
||||
3. 自动处理内存管理(在 dealloc 中移除监听)
|
||||
4. 包含完整的错误检查和日志记录
|
||||
|
||||
## 扩展性
|
||||
|
||||
如果将来需要转发其他类型的消息,可以:
|
||||
|
||||
1. 修改条件判断(如 `attachment.first == 107`)
|
||||
2. 或者使用更通用的通知名称,在通知数据中携带消息类型信息
|
@@ -1,293 +0,0 @@
|
||||
# getUserInfo API 使用说明文档
|
||||
|
||||
## 方法概述
|
||||
|
||||
`getUserInfo:uid:` 是 `Api` 类中的一个静态方法,用于获取指定用户的详细信息。
|
||||
|
||||
## 方法签名
|
||||
|
||||
```objc
|
||||
+ (void)getUserInfo:(HttpRequestHelperCompletion)completion uid:(NSString *)uid;
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
### 输入参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| completion | HttpRequestHelperCompletion | 是 | 请求完成后的回调函数 |
|
||||
| uid | NSString | 是 | 要查询的用户ID |
|
||||
|
||||
### 回调函数格式
|
||||
|
||||
```objc
|
||||
typedef void(^HttpRequestHelperCompletion)(BaseModel* _Nullable data, NSInteger code, NSString * _Nullable msg);
|
||||
```
|
||||
|
||||
**回调参数说明:**
|
||||
- `data`: BaseModel 对象,包含服务器返回的数据
|
||||
- `code`: NSInteger,HTTP 状态码或业务状态码
|
||||
- `msg`: NSString,服务器返回的消息或错误信息
|
||||
|
||||
## 实现原理
|
||||
|
||||
### 1. API 端点
|
||||
- **Base64 编码的路径**: `dXNlci9nZXQ=`
|
||||
- **解码后的路径**: `user/get`
|
||||
- **请求方法**: GET
|
||||
|
||||
### 2. 请求流程
|
||||
1. 将用户ID作为参数传递给 `makeRequest` 方法
|
||||
2. `makeRequest` 方法通过 `__FUNCTION__` 宏自动解析参数名
|
||||
3. 构造请求参数字典:`@{@"uid": uid}`
|
||||
4. 调用 `HttpRequestHelper` 发送 GET 请求
|
||||
|
||||
### 3. 参数自动映射
|
||||
该方法使用了特殊的参数映射机制:
|
||||
- 通过 `__FUNCTION__` 宏获取方法名
|
||||
- 解析方法名中的参数部分(冒号后的部分)
|
||||
- 自动将传入的参数值与参数名对应
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本用法
|
||||
|
||||
```objc
|
||||
// 获取用户ID为 "12345" 的用户信息
|
||||
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
|
||||
if (code == 200) {
|
||||
// 请求成功
|
||||
NSLog(@"用户信息: %@", data.data);
|
||||
NSLog(@"消息: %@", msg);
|
||||
} else {
|
||||
// 请求失败
|
||||
NSLog(@"错误码: %ld", (long)code);
|
||||
NSLog(@"错误信息: %@", msg);
|
||||
}
|
||||
} uid:@"12345"];
|
||||
```
|
||||
|
||||
### 错误处理示例
|
||||
|
||||
```objc
|
||||
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
|
||||
switch (code) {
|
||||
case 200:
|
||||
// 成功获取用户信息
|
||||
[self handleUserInfoSuccess:data.data];
|
||||
break;
|
||||
case 404:
|
||||
// 用户不存在
|
||||
[self showUserNotFoundAlert];
|
||||
break;
|
||||
case 401:
|
||||
// 未授权访问
|
||||
[self handleUnauthorizedAccess];
|
||||
break;
|
||||
default:
|
||||
// 其他错误
|
||||
[self showErrorAlert:msg];
|
||||
break;
|
||||
}
|
||||
} uid:userId];
|
||||
```
|
||||
|
||||
### 在 ViewController 中使用
|
||||
|
||||
```objc
|
||||
@interface UserProfileViewController ()
|
||||
@property (nonatomic, strong) NSString *currentUserId;
|
||||
@end
|
||||
|
||||
@implementation UserProfileViewController
|
||||
|
||||
- (void)loadUserInfo {
|
||||
if (!self.currentUserId) {
|
||||
NSLog(@"用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (code == 200) {
|
||||
[self updateUIWithUserInfo:data.data];
|
||||
} else {
|
||||
[self showErrorWithMessage:msg];
|
||||
}
|
||||
});
|
||||
} uid:self.currentUserId];
|
||||
}
|
||||
|
||||
- (void)updateUIWithUserInfo:(id)userInfo {
|
||||
// 更新UI显示用户信息
|
||||
// userInfo 的具体结构需要根据后端返回的数据格式来确定
|
||||
}
|
||||
|
||||
- (void)showErrorWithMessage:(NSString *)message {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"错误"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
## 返回数据结构
|
||||
|
||||
### BaseModel 结构
|
||||
|
||||
```objc
|
||||
@interface BaseModel : NSObject
|
||||
@property(nonatomic,assign) long timestamp; // 时间戳
|
||||
@property (nonatomic , strong) id data; // 返回的数据
|
||||
@property (nonatomic , assign) NSInteger code; // 状态码
|
||||
@property (nonatomic , copy) NSString *message; // 消息
|
||||
@property (nonatomic,assign) long long cancelDate; // 注销时间戳
|
||||
@property (nonatomic,copy) NSString *date; // 日期
|
||||
@property (nonatomic,copy) NSString *reason; // 封禁理由
|
||||
@end
|
||||
```
|
||||
|
||||
### 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"timestamp": 1640995200000,
|
||||
"data": {
|
||||
"uid": "12345",
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "头像URL",
|
||||
"level": 10,
|
||||
"vip": false
|
||||
// 其他用户信息字段...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "用户不存在",
|
||||
"timestamp": 1640995200000,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 线程安全
|
||||
- 回调函数在后台线程执行
|
||||
- UI 更新操作需要在主线程进行
|
||||
|
||||
### 2. 参数验证
|
||||
- 确保 `uid` 参数不为空
|
||||
- 建议在使用前验证 `uid` 的格式
|
||||
|
||||
### 3. 内存管理
|
||||
- 避免在回调中造成循环引用
|
||||
- 使用 `__weak` 修饰符防止内存泄漏
|
||||
|
||||
### 4. 网络状态
|
||||
- 建议在调用前检查网络连接状态
|
||||
- 处理网络超时和连接失败的情况
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 参数验证
|
||||
|
||||
```objc
|
||||
- (void)getUserInfoWithValidation:(NSString *)uid {
|
||||
if (!uid || uid.length == 0) {
|
||||
NSLog(@"用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证UID格式(根据实际需求调整)
|
||||
if (![self isValidUID:uid]) {
|
||||
NSLog(@"用户ID格式不正确");
|
||||
return;
|
||||
}
|
||||
|
||||
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
|
||||
// 处理响应
|
||||
} uid:uid];
|
||||
}
|
||||
|
||||
- (BOOL)isValidUID:(NSString *)uid {
|
||||
// 根据实际业务需求验证UID格式
|
||||
return uid.length > 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
```objc
|
||||
- (void)handleApiError:(NSInteger)code message:(NSString *)msg {
|
||||
switch (code) {
|
||||
case 200:
|
||||
// 成功
|
||||
break;
|
||||
case 400:
|
||||
NSLog(@"请求参数错误: %@", msg);
|
||||
break;
|
||||
case 401:
|
||||
NSLog(@"未授权访问,需要重新登录");
|
||||
[self redirectToLogin];
|
||||
break;
|
||||
case 404:
|
||||
NSLog(@"用户不存在: %@", msg);
|
||||
break;
|
||||
case 500:
|
||||
NSLog(@"服务器内部错误: %@", msg);
|
||||
break;
|
||||
default:
|
||||
NSLog(@"未知错误 (code: %ld): %@", (long)code, msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 缓存策略
|
||||
|
||||
```objc
|
||||
- (void)getUserInfoWithCache:(NSString *)uid {
|
||||
// 先检查本地缓存
|
||||
UserInfo *cachedUser = [self getCachedUserInfo:uid];
|
||||
if (cachedUser) {
|
||||
[self updateUIWithUserInfo:cachedUser];
|
||||
}
|
||||
|
||||
// 从服务器获取最新数据
|
||||
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
|
||||
if (code == 200) {
|
||||
// 更新缓存
|
||||
[self cacheUserInfo:data.data forUID:uid];
|
||||
[self updateUIWithUserInfo:data.data];
|
||||
} else {
|
||||
// 如果服务器请求失败,使用缓存数据(如果有)
|
||||
if (!cachedUser) {
|
||||
[self showErrorWithMessage:msg];
|
||||
}
|
||||
}
|
||||
} uid:uid];
|
||||
}
|
||||
```
|
||||
|
||||
## 相关方法
|
||||
|
||||
- `getUserInfos:uids:` - 批量获取多个用户信息
|
||||
- `completeUserInfo:userInfo:` - 补全用户资料
|
||||
- `getUserWalletInfo:uid:ticket:` - 获取用户钱包信息
|
||||
|
||||
## 版本信息
|
||||
|
||||
- **iOS 最低版本**: iOS 15.6
|
||||
- **创建时间**: 2021/9/6
|
||||
- **最后更新**: 当前版本
|
194
issues/gift-animation-optimization.md
Normal file
194
issues/gift-animation-optimization.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# 礼物动画优化方案B实施总结
|
||||
|
||||
## 问题描述
|
||||
|
||||
**核心问题**:当用户在送礼时(combo),其他用户也在送礼,礼物动画(从位置a移动到位置b)将不会显示。
|
||||
|
||||
**触发条件**:
|
||||
- 接收到云信消息,类型为 `Custom_Message_Sub_Gift_Send` 或 `Custom_Message_Sub_Gift_ChannelNotify`
|
||||
- 多个用户同时送礼
|
||||
- 当前用户处于combo状态
|
||||
|
||||
## 问题根本原因
|
||||
|
||||
**核心问题**:`GiftAnimationManager` 中的 `shouldUseComboAnimationForSender` 判断逻辑在多用户并发场景下不准确,导致其他用户的礼物动画被错误地当作combo动画处理,从而不显示。
|
||||
|
||||
### 具体问题点:
|
||||
|
||||
1. **全局状态判断错误**:原逻辑只检查全局combo状态,没有区分不同用户
|
||||
2. **时间窗口判断不精确**:没有为每个用户维护独立的送礼时间
|
||||
3. **状态管理混乱**:combo状态和动画状态没有正确分离
|
||||
|
||||
## 修复方案实施
|
||||
|
||||
### 第一步:修改 GiftAnimationManager.h - 添加新方法声明 ✅
|
||||
|
||||
```objc
|
||||
// 🔧 新增:Combo状态管理方法
|
||||
- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid;
|
||||
- (void)clearUserComboState:(NSString *)uid;
|
||||
- (void)updateUserGiftTime:(NSString *)uid;
|
||||
- (void)cleanupExpiredStates;
|
||||
```
|
||||
|
||||
### 第二步:修改 GiftAnimationManager.m - 实现精确的combo状态管理 ✅
|
||||
|
||||
**新增属性**:
|
||||
```objc
|
||||
// 🔧 新增:Combo状态管理属性
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *userComboStates;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSDate *> *userLastGiftTime;
|
||||
@property (nonatomic, assign) NSTimeInterval comboTimeWindow;
|
||||
```
|
||||
|
||||
**核心方法实现**:
|
||||
```objc
|
||||
// 🔧 修改:优化shouldUseComboAnimationForSender方法
|
||||
- (BOOL)shouldUseComboAnimationForSender:(NSString *)uid {
|
||||
if (!uid || uid.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 优先使用精确状态判断
|
||||
BOOL isUserInCombo = [self.userComboStates[uid] boolValue];
|
||||
if (isUserInCombo) {
|
||||
BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid];
|
||||
NSLog(@"[Combo effect] 🎯 用户 %@ 处于combo状态,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO");
|
||||
return isCurrentUser;
|
||||
}
|
||||
|
||||
// 兜底:时间窗口判断
|
||||
NSDate *lastGiftTime = self.userLastGiftTime[uid];
|
||||
if (lastGiftTime) {
|
||||
NSTimeInterval timeSinceLastGift = [[NSDate date] timeIntervalSinceDate:lastGiftTime];
|
||||
if (timeSinceLastGift <= self.comboTimeWindow) {
|
||||
BOOL isCurrentUser = [uid isEqualToString:[AccountInfoStorage instance].getUid];
|
||||
NSLog(@"[Combo effect] 🎯 用户 %@ 在时间窗口内,是否当前用户: %@", uid, isCurrentUser ? @"YES" : @"NO");
|
||||
return isCurrentUser;
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"[Combo effect] 🎯 用户 %@ 不使用combo动画", uid);
|
||||
return NO;
|
||||
}
|
||||
```
|
||||
|
||||
### 第三步:修改 GiftComboManager.m - 添加状态通知机制 ✅
|
||||
|
||||
**新增方法**:
|
||||
```objc
|
||||
// 🔧 新增:状态通知方法实现
|
||||
- (void)setUserComboState:(BOOL)isCombo forUser:(NSString *)uid {
|
||||
// 通过通知中心通知动画管理器
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"GiftComboStateChanged"
|
||||
object:nil
|
||||
userInfo:@{
|
||||
@"uid": uid,
|
||||
@"isCombo": @(isCombo)
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
### 第四步:修改 RoomAnimationView.m - 集成新的状态管理 ✅
|
||||
|
||||
**添加通知监听**:
|
||||
```objc
|
||||
// 🔧 新增:监听combo状态变化
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleComboStateChanged:)
|
||||
name:@"GiftComboStateChanged"
|
||||
object:nil];
|
||||
```
|
||||
|
||||
**添加用户送礼时间更新**:
|
||||
```objc
|
||||
// 🔧 新增:更新用户送礼时间
|
||||
[self.giftAnimationManager updateUserGiftTime:receiveInfo.uid];
|
||||
```
|
||||
|
||||
**添加状态处理方法**:
|
||||
```objc
|
||||
// 🔧 新增:处理combo状态变化
|
||||
- (void)handleComboStateChanged:(NSNotification *)notification {
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
NSString *uid = userInfo[@"uid"];
|
||||
BOOL isCombo = [userInfo[@"isCombo"] boolValue];
|
||||
|
||||
// 通知动画管理器更新combo状态
|
||||
if (isCombo) {
|
||||
[self.giftAnimationManager setUserComboState:YES forUser:uid];
|
||||
} else {
|
||||
[self.giftAnimationManager clearUserComboState:uid];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第五步:添加清理机制 ✅
|
||||
|
||||
**在dealloc中添加清理**:
|
||||
```objc
|
||||
// 🔧 新增:清理combo状态管理
|
||||
[self cleanupExpiredStates];
|
||||
[self.userComboStates removeAllObjects];
|
||||
[self.userLastGiftTime removeAllObjects];
|
||||
```
|
||||
|
||||
## 修复效果
|
||||
|
||||
### 修复前的问题:
|
||||
1. 用户A在combo状态时,用户B送礼,用户B的动画不显示
|
||||
2. 全局状态判断导致所有用户的动画都被错误处理
|
||||
3. 没有精确的用户状态管理
|
||||
|
||||
### 修复后的效果:
|
||||
1. ✅ 每个用户有独立的combo状态管理
|
||||
2. ✅ 精确判断当前用户是否应该使用combo动画
|
||||
3. ✅ 其他用户的礼物动画正常显示
|
||||
4. ✅ 添加了完整的日志记录,便于调试
|
||||
5. ✅ 实现了自动清理机制,防止内存泄漏
|
||||
|
||||
## 技术要点
|
||||
|
||||
### 1. 精确状态管理
|
||||
- 为每个用户维护独立的combo状态
|
||||
- 使用时间窗口作为兜底判断机制
|
||||
- 实现了状态自动清理
|
||||
|
||||
### 2. 通知机制
|
||||
- 使用NSNotificationCenter实现组件间通信
|
||||
- 解耦了GiftComboManager和GiftAnimationManager
|
||||
- 保证了状态同步的及时性
|
||||
|
||||
### 3. 线程安全
|
||||
- 使用串行队列处理动画
|
||||
- 在主线程执行UI操作
|
||||
- 避免了并发访问问题
|
||||
|
||||
### 4. 性能优化
|
||||
- 使用字典存储用户状态,O(1)查找
|
||||
- 定期清理过期状态
|
||||
- 最小化内存占用
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 测试场景:
|
||||
1. **单用户combo测试**:验证当前用户的combo动画正常
|
||||
2. **多用户并发测试**:验证其他用户的动画正常显示
|
||||
3. **状态切换测试**:验证combo状态的正确切换
|
||||
4. **内存泄漏测试**:验证清理机制的有效性
|
||||
|
||||
### 验证方法:
|
||||
1. 查看日志输出,确认状态判断正确
|
||||
2. 观察动画显示效果
|
||||
3. 使用Instruments检查内存使用
|
||||
|
||||
## 总结
|
||||
|
||||
通过实施精确的用户状态管理、通知机制和清理机制,成功解决了多用户并发送礼时动画不显示的问题。修复方案具有以下特点:
|
||||
|
||||
- **精确性**:每个用户独立的状态管理
|
||||
- **可靠性**:完整的错误处理和日志记录
|
||||
- **性能**:高效的数据结构和自动清理
|
||||
- **可维护性**:清晰的代码结构和注释
|
||||
|
||||
该修复方案确保了礼物动画系统在多用户并发场景下的稳定性和正确性。
|
25
issues/stageview-refactor-todo.md
Normal file
25
issues/stageview-refactor-todo.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## 当前状态
|
||||
|
||||
### ✅ 第一步:创建 StageViewManager 类
|
||||
- [x] 创建 StageViewManager.h
|
||||
- [x] 创建 StageViewManager.m
|
||||
- [x] 实现基本的 stageView 管理逻辑
|
||||
- [x] 使用 RoomInfoModel 中的 RoomType 定义
|
||||
|
||||
### ✅ 第二步:在 XPRoomViewController.m 中集成
|
||||
- [x] 添加 StageViewManager 的导入
|
||||
- [x] 添加 StageViewManager 属性声明
|
||||
- [x] 在 viewDidLoad 中初始化 StageViewManager
|
||||
- [x] 添加 setupStageViewManager 方法
|
||||
|
||||
### 🔄 第三步:重构现有的 stageView 更新逻辑
|
||||
- [ ] 找到所有 stageView 创建和更新的地方
|
||||
- [ ] 替换为使用 StageViewManager
|
||||
- [ ] 移除重复的 stageView 更新代码
|
||||
- [ ] 测试确保功能正常
|
||||
|
||||
### ⏳ 第四步:测试和验证
|
||||
- [ ] 测试不同房间类型的切换
|
||||
- [ ] 验证 stageView 更新逻辑
|
||||
- [ ] 性能测试
|
||||
- [ ] 代码审查
|
@@ -1,71 +0,0 @@
|
||||
# micButton 状态表格
|
||||
|
||||
## micButton 可用状态总览
|
||||
|
||||
| 场景 | 用户状态 | micButton显示 | micButton可用性 | micState值 | 音频状态 | 备注 |
|
||||
|------|----------|---------------|----------------|------------|----------|------|
|
||||
| **用户上麦前** | 未在麦位 | 隐藏 | 不可用 | MICState_None | 无音频 | isOnMic = NO |
|
||||
| **用户刚上麦** | 刚上麦位 | 显示 | 可用 | MICState_Close | 静音 | 默认静音状态,localMuted = YES |
|
||||
| **用户开麦** | 在麦位 | 显示开麦状态 | 可用 | MICState_Open | 开启音频 | 用户可以说话 |
|
||||
| **用户关麦** | 在麦位 | 显示关麦状态 | 可用 | MICState_Close | 静音 | 用户无法说话 |
|
||||
| **用户下麦** | 离开麦位 | 隐藏 | 不可用 | MICState_None | 无音频 | isOnMic = NO |
|
||||
|
||||
## 不同场景下的状态变化
|
||||
|
||||
### 1. 用户加入/离开舞台
|
||||
|
||||
| 操作 | micButton状态变化 | 音频状态变化 | UI更新 |
|
||||
|------|------------------|--------------|--------|
|
||||
| 用户上麦 | 隐藏 → 显示(关麦状态) | 无音频 → 静音 | isOnMic: NO → YES |
|
||||
| 用户下麦 | 显示 → 隐藏 | 当前状态 → 无音频 | isOnMic: YES → NO |
|
||||
|
||||
### 2. 其他用户加入/离开舞台
|
||||
|
||||
| 操作 | 当前用户micButton | 影响范围 | 说明 |
|
||||
|------|------------------|----------|------|
|
||||
| 他人上麦 | 无变化 | 仅更新麦位显示 | micButton状态不受影响 |
|
||||
| 他人下麦 | 无变化 | 仅更新麦位显示 | micButton状态不受影响 |
|
||||
|
||||
### 3. 房间最小化场景
|
||||
|
||||
| 状态 | micButton处理 | 音频处理 | 数据同步 |
|
||||
|------|---------------|----------|----------|
|
||||
| 最小化时 | 监听队列变化 | 继续广播音频 | selfNeedBroadcast基于MicroMicStateType_Open |
|
||||
| 恢复显示 | recheckMicState同步 | 保持当前状态 | 从XPSkillCardPlayerManager.micState同步 |
|
||||
|
||||
## micButton 状态枚举详解
|
||||
|
||||
| MICState枚举 | 数值 | 含义 | UI表现 | 用户能否说话 |
|
||||
|-------------|------|------|--------|-------------|
|
||||
| MICState_None | 0 | 无麦克风状态 | micButton隐藏 | ❌ 否 |
|
||||
| MICState_Close | 1 | 麦克风关闭 | 显示关麦图标 | ❌ 否 |
|
||||
| MICState_Open | 2 | 麦克风开启 | 显示开麦图标 | ✅ 是 |
|
||||
|
||||
## 关键时序和同步机制
|
||||
|
||||
### 状态更新流程
|
||||
```
|
||||
用户操作 → StageView处理 → 麦位队列更新 → onMicroQueueUpdate回调
|
||||
→ XPRoomViewController分发 → XPRoomMenuContainerView更新
|
||||
→ micButton状态/显示更新 → recheckMicState同步检查
|
||||
```
|
||||
|
||||
### 重要同步点
|
||||
| 时机 | 同步操作 | 目的 |
|
||||
|------|----------|------|
|
||||
| viewWillAppear | recheckMicState | 确保UI与全局状态一致 |
|
||||
| 房间退出 | micState = MICState_None | 重置状态 |
|
||||
| 麦位变化 | onMicroQueueUpdate | 实时更新UI |
|
||||
|
||||
## 特殊情况处理
|
||||
|
||||
| 特殊情况 | micButton行为 | 处理逻辑 |
|
||||
|----------|---------------|----------|
|
||||
| 网络断线重连 | 重新同步状态 | recheckMicState确保一致性 |
|
||||
| 被踢出麦位 | 立即隐藏 | NIMChatroomEventTypeKicked触发 |
|
||||
| 房间模式切换 | 根据新模式调整 | 不同RoomModeType有不同处理 |
|
||||
| 禁麦状态 | 显示但可能限制功能 | isNoProhibitMic控制 |
|
||||
|
||||
---
|
||||
|
||||
**总结**: micButton的可用状态主要取决于用户是否在麦位(isOnMic),在麦位时根据MICState显示不同状态,用户只有在MICState_Open时才能说话。
|
@@ -1,63 +0,0 @@
|
||||
sequenceDiagram
|
||||
participant User as 用户
|
||||
participant StageView as StageView
|
||||
participant RoomVC as XPRoomViewController
|
||||
participant NIM as NIM SDK
|
||||
participant API as Api服务
|
||||
participant MenuView as XPRoomMenuContainerView
|
||||
|
||||
Note over User,MenuView: 用户上mic完整流程
|
||||
|
||||
%% 触发阶段
|
||||
User->>StageView: 点击麦位
|
||||
StageView->>StageView: microViewTapped(调用didSelectAtIndex)
|
||||
|
||||
%% 权限检查阶段
|
||||
StageView->>StageView: 检查麦位状态(空闲/占用/锁定)
|
||||
alt 麦位被占用
|
||||
StageView->>StageView: displayUserCard(显示用户信息)
|
||||
else 麦位空闲且未锁定
|
||||
StageView->>RoomVC: 获取聊天室成员信息
|
||||
|
||||
%% 用户身份判断
|
||||
alt 超级管理员/管理员
|
||||
StageView->>StageView: 显示操作菜单(上麦/锁麦/静音/邀请)
|
||||
User->>StageView: 选择"上麦"操作
|
||||
else 普通用户
|
||||
alt 用户已在其他麦位
|
||||
StageView->>NIM: nimDownQueue(下麦)
|
||||
NIM->>NIM: 更新聊天室队列
|
||||
end
|
||||
end
|
||||
|
||||
%% 特殊房间类型处理
|
||||
alt 特定房间类型(RoomType_19Mic)且位置为6
|
||||
StageView->>API: requestBossMicUp(老板上麦)
|
||||
API-->>StageView: 返回结果
|
||||
end
|
||||
|
||||
%% 上麦核心流程
|
||||
StageView->>NIM: nimUpQueue(上麦请求)
|
||||
NIM->>NIM: 构建队列扩展信息(userInfoToQueueExt)
|
||||
NIM->>API: 更新聊天室队列
|
||||
API-->>NIM: 确认队列更新
|
||||
|
||||
%% 状态同步阶段
|
||||
NIM->>RoomVC: onMicroQueueUpdate回调
|
||||
RoomVC->>MenuView: 分发队列更新事件
|
||||
MenuView->>MenuView: 更新micButton状态
|
||||
MenuView->>MenuView: recheckMicState(状态同步检查)
|
||||
|
||||
%% UI更新阶段
|
||||
StageView->>StageView: 更新麦位UI显示
|
||||
StageView->>StageView: 显示用户头像和信息
|
||||
|
||||
%% 消息广播
|
||||
NIM->>NIM: 发送自定义消息(CustomMessageType_Hall_Super_Admin)
|
||||
NIM->>RoomVC: 广播给房间内所有用户
|
||||
|
||||
else 麦位被锁定
|
||||
StageView->>User: 显示"麦位已锁定"提示
|
||||
end
|
||||
|
||||
Note over User,MenuView: 流程完成,用户成功上麦
|
Reference in New Issue
Block a user