删除多个文档和文件,包括 micButton 状态表格、QEmotionBoardView 相关问题分析报告、序列图、飘屏组件分析文档等,以清理项目中的冗余内容,提升代码整洁性和可维护性。
This commit is contained in:
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. **低优先级**:添加扩展功能和优化
|
||||
|
||||
通过这种抽象设计,能够在保持现有功能完整性的同时,为未来的功能扩展和维护奠定良好的架构基础。
|
@@ -1,143 +0,0 @@
|
||||
# QEmotionBoardView emoji 不显示问题分析报告
|
||||
|
||||
## 问题描述
|
||||
QEmotionBoardView 表情面板中的 emoji 表情无法正常显示,表情面板显示空白。
|
||||
|
||||
## 代码逻辑梳理
|
||||
|
||||
### 整体架构
|
||||
QEmotionBoardView 是一个表情面板组件,主要包含以下几个核心类:
|
||||
|
||||
1. **QEmotionBoardView**: 主视图控制器,负责整体布局和事件处理
|
||||
2. **UIEmotionPageView**: 表情页面视图,负责单个页面的表情渲染和交互
|
||||
3. **UIEmotionVerticalScrollView**: 垂直滚动视图,包装页面视图
|
||||
4. **QEmotion**: 表情数据模型
|
||||
5. **QEmotionHelper**: 表情管理工具类
|
||||
|
||||
### 核心流程
|
||||
|
||||
#### 1. 表情数据加载流程
|
||||
```
|
||||
AppDelegate.initEmojiData()
|
||||
→ 读取 emoji.plist
|
||||
→ 创建 QEmotion 对象数组
|
||||
→ 设置到 QEmotionHelper.emotionArray
|
||||
```
|
||||
|
||||
#### 2. 表情面板显示流程
|
||||
```
|
||||
QEmotionBoardView 初始化
|
||||
→ 设置 emotions 属性
|
||||
→ layoutSubviews 触发布局
|
||||
→ UIEmotionVerticalScrollView.setEmotions
|
||||
→ UIEmotionPageView.layoutEmotionsIfNeeded
|
||||
→ 创建 CALayer 显示表情图片
|
||||
```
|
||||
|
||||
#### 3. 表情渲染机制
|
||||
- 使用 `CALayer` 而不是 `UIImageView` 来渲染表情,提高性能
|
||||
- 通过 `emotionlayer.contents = (__bridge id)(self.emotions[i].image.CGImage)` 设置图片
|
||||
- 计算每个表情的位置和点击区域
|
||||
|
||||
## 问题根因分析
|
||||
|
||||
### 主要问题:图片加载错误
|
||||
|
||||
在 `AppDelegate+ThirdConfig.m` 的 `initEmojiData` 方法中存在一个严重的 bug:
|
||||
|
||||
**错误代码:**
|
||||
```objective-c
|
||||
UIImage * image = [UIImage imageNamed:dic[@"file"]];
|
||||
```
|
||||
|
||||
**问题分析:**
|
||||
1. `dic[@"file"]` 指向的是外层字典的 `file` 字段,但这个字段在 plist 中并不存在
|
||||
2. 所有表情都尝试加载同一个不存在的图片文件
|
||||
3. 导致所有 `QEmotion.image` 都为 `nil`
|
||||
4. 在 `QEmotionBoardView` 中,`emotionlayer.contents` 被设置为 `nil`,所以表情不显示
|
||||
|
||||
**正确代码:**
|
||||
```objective-c
|
||||
NSDictionary * emotionDic = [emojiArray xpSafeObjectAtIndex:i];
|
||||
UIImage * image = [UIImage imageNamed:emotionDic[@"file"]];
|
||||
```
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修复图片加载逻辑
|
||||
|
||||
已修复 `AppDelegate+ThirdConfig.m` 中的图片加载逻辑:
|
||||
|
||||
```objective-c
|
||||
for (int i = 0; i < emojiArray.count; i++) {
|
||||
NSDictionary * emotionDic = [emojiArray xpSafeObjectAtIndex:i];
|
||||
if (!emotionDic) continue;
|
||||
|
||||
UIImage * image = [UIImage imageNamed:emotionDic[@"file"]];
|
||||
QEmotion * info = [[QEmotion alloc] init];
|
||||
|
||||
info.displayName = emotionDic[@"tag"];
|
||||
info.identifier = emotionDic[@"id"];
|
||||
info.image = image;
|
||||
|
||||
[array addObject:info];
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加调试代码
|
||||
|
||||
在关键位置添加了调试日志:
|
||||
|
||||
**AppDelegate 中:**
|
||||
```objective-c
|
||||
// 添加调试日志
|
||||
NSLog(@"加载表情: %@, 图片: %@, 是否成功: %@", info.displayName, emotionDic[@"file"], image ? @"是" : @"否");
|
||||
```
|
||||
|
||||
**QEmotionBoardView 中:**
|
||||
```objective-c
|
||||
// 添加调试日志
|
||||
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 ? @"存在" : @"不存在");
|
||||
}
|
||||
```
|
||||
|
||||
**表情渲染中:**
|
||||
```objective-c
|
||||
// 添加调试日志
|
||||
if (i < 3) { // 只打印前3个表情的调试信息
|
||||
NSLog(@"渲染表情 %d: %@, 图片: %@, CGImage: %@", i, self.emotions[i].displayName,
|
||||
self.emotions[i].image ? @"存在" : @"不存在",
|
||||
self.emotions[i].image.CGImage ? @"存在" : @"不存在");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 验证图片文件
|
||||
|
||||
确认表情图片文件已正确添加到 Xcode 项目中:
|
||||
- 图片文件位于 `YuMi/CustomUI/InputView/Emoji/` 目录
|
||||
- 文件名格式为 `emoji_XX@2x.png`
|
||||
- plist 中配置的文件名为 `emoji_XX.png`(iOS 会自动处理 @2x 后缀)
|
||||
- 所有图片文件都已添加到项目的 Bundle Resources 中
|
||||
|
||||
## 测试验证
|
||||
|
||||
修复后,可以通过以下方式验证:
|
||||
|
||||
1. **查看控制台日志**:运行应用后查看控制台输出,确认:
|
||||
- 表情数组加载完成,数量正确
|
||||
- 每个表情的图片加载成功
|
||||
- QEmotionBoardView 正确设置表情数组
|
||||
- 表情渲染时图片和 CGImage 都存在
|
||||
|
||||
2. **功能测试**:
|
||||
- 打开表情面板,确认表情正常显示
|
||||
- 点击表情,确认能正常插入到输入框
|
||||
- 测试删除和发送功能
|
||||
|
||||
## 总结
|
||||
|
||||
问题的根本原因是 AppDelegate 中图片加载逻辑的错误,导致所有表情的图片都为 nil。修复后,表情应该能正常显示。如果仍有问题,可以通过添加的调试日志进一步排查。
|
||||
|
@@ -1,175 +0,0 @@
|
||||
# QEmotionBoardView emoji 不显示问题排查报告
|
||||
|
||||
## 问题现状
|
||||
|
||||
- 表情数组已成功加载(125个表情)
|
||||
- 表情图片文件存在且正确
|
||||
- 但表情面板仍然不显示 emoji
|
||||
|
||||
## 排查过程
|
||||
|
||||
### 1. 数据流验证
|
||||
|
||||
已添加调试代码验证数据流:
|
||||
|
||||
- AppDelegate 中表情数组加载完成
|
||||
- QEmotionBoardView 设置表情数组
|
||||
- UIEmotionVerticalScrollView 接收表情数据
|
||||
- UIEmotionPageView 接收表情数据
|
||||
|
||||
### 2. 关键问题发现
|
||||
|
||||
#### 问题1:UIEmotionVerticalScrollView bounds 可能为0
|
||||
|
||||
在 `UIEmotionVerticalScrollView.setEmotions` 方法中:
|
||||
|
||||
```objective-c
|
||||
CGSize contentSize = CGSizeMake(self.bounds.size.width - [self edgeInsetsGetHorizontalValue:paddingInPage],
|
||||
self.bounds.size.height - [self edgeInsetsGetVerticalValue:paddingInPage]);
|
||||
```
|
||||
|
||||
**问题分析:**
|
||||
|
||||
- 当调用 `setEmotions` 时,UIEmotionVerticalScrollView 可能还没有正确的 bounds
|
||||
- 如果 bounds 为 0,contentSize 也会为 0
|
||||
- 导致 `emotionCountPerRow` 计算错误
|
||||
- 最终影响表情的布局和显示
|
||||
|
||||
#### 问题2:布局时机问题
|
||||
|
||||
UIEmotionVerticalScrollView 的 frame 设置时机可能有问题:
|
||||
|
||||
```objective-c
|
||||
CGSize size = [pageView verticalSizeThatFits:self.bounds.size emotionVerticalSpacing:emotionVerticalSpacing];
|
||||
self.pageView.frame = CGRectMake(0, 0, size.width, size.height);
|
||||
```
|
||||
|
||||
**问题分析:**
|
||||
|
||||
- 如果 `self.bounds.size` 为 0,计算出的 size 也会有问题
|
||||
- pageView 的 frame 可能设置不正确
|
||||
- 导致表情无法正确显示
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案1:延迟设置表情数据
|
||||
|
||||
在 QEmotionBoardView 的 layoutSubviews 中设置表情数据,确保 bounds 已经正确:
|
||||
|
||||
```objective-c
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
// 确保 bounds 已经设置
|
||||
if (CGRectIsEmpty(self.bounds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置表情数据
|
||||
if (self.emotions && self.emotions.count > 0) {
|
||||
[self.verticalScrollView setEmotions:self.emotions
|
||||
emotionSize:self.emotionSize
|
||||
minimumEmotionHorizontalSpacing:self.minimumEmotionHorizontalSpacing
|
||||
emotionVerticalSpacing:self.emotionVerticalSpacing
|
||||
emotionSelectedBackgroundExtension:self.emotionSelectedBackgroundExtension
|
||||
paddingInPage:self.paddingInPage];
|
||||
}
|
||||
|
||||
// 其他布局代码...
|
||||
}
|
||||
```
|
||||
|
||||
### 方案2:修复 UIEmotionVerticalScrollView 的 bounds 问题
|
||||
|
||||
在 setEmotions 方法中添加 bounds 检查:
|
||||
|
||||
```objective-c
|
||||
- (void)setEmotions:(NSArray<QEmotion *> *)emotions
|
||||
emotionSize:(CGSize)emotionSize
|
||||
minimumEmotionHorizontalSpacing:(CGFloat)minimumEmotionHorizontalSpacing
|
||||
emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing
|
||||
emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension
|
||||
paddingInPage:(UIEdgeInsets)paddingInPage {
|
||||
|
||||
// 检查 bounds 是否有效
|
||||
if (CGRectIsEmpty(self.bounds)) {
|
||||
NSLog(@"UIEmotionVerticalScrollView bounds 为空,延迟设置表情");
|
||||
// 延迟到下一个运行循环
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self setEmotions:emotions
|
||||
emotionSize:emotionSize
|
||||
minimumEmotionHorizontalSpacing:minimumEmotionHorizontalSpacing
|
||||
emotionVerticalSpacing:emotionVerticalSpacing
|
||||
emotionSelectedBackgroundExtension:emotionSelectedBackgroundExtension
|
||||
paddingInPage:paddingInPage];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 原有的设置逻辑...
|
||||
}
|
||||
```
|
||||
|
||||
### 方案3:使用固定尺寸
|
||||
|
||||
如果 bounds 问题无法解决,可以使用固定的 contentSize:
|
||||
|
||||
```objective-c
|
||||
- (void)setEmotions:(NSArray<QEmotion *> *)emotions
|
||||
emotionSize:(CGSize)emotionSize
|
||||
minimumEmotionHorizontalSpacing:(CGFloat)minimumEmotionHorizontalSpacing
|
||||
emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing
|
||||
emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension
|
||||
paddingInPage:(UIEdgeInsets)paddingInPage {
|
||||
|
||||
UIEmotionPageView *pageView = self.pageView;
|
||||
pageView.emotions = emotions;
|
||||
pageView.padding = paddingInPage;
|
||||
|
||||
// 使用父视图的宽度作为基准
|
||||
CGFloat availableWidth = self.superview ? self.superview.bounds.size.width : 320;
|
||||
CGSize contentSize = CGSizeMake(availableWidth - [self edgeInsetsGetHorizontalValue:paddingInPage],
|
||||
self.bounds.size.height - [self edgeInsetsGetVerticalValue:paddingInPage]);
|
||||
|
||||
// 其他逻辑...
|
||||
}
|
||||
```
|
||||
|
||||
## 调试建议
|
||||
|
||||
### 1. 运行应用并查看日志
|
||||
|
||||
运行应用后,查看控制台输出,特别关注:
|
||||
|
||||
- UIEmotionVerticalScrollView bounds 是否为 0
|
||||
- contentSize 计算是否正确
|
||||
- emotionCountPerRow 和 numberOfRows 的值
|
||||
|
||||
### 2. 检查布局时机
|
||||
|
||||
在 Xcode 中使用 View Debugger 检查:
|
||||
|
||||
- QEmotionBoardView 的 frame 是否正确
|
||||
- UIEmotionVerticalScrollView 的 frame 是否正确
|
||||
- UIEmotionPageView 的 frame 是否正确
|
||||
|
||||
### 3. 验证表情渲染
|
||||
|
||||
检查 CALayer 是否正确创建:
|
||||
|
||||
- emotionLayers 数组是否有内容
|
||||
- 每个 layer 的 frame 是否正确
|
||||
- layer 的 contents 是否设置
|
||||
|
||||
## 预期结果
|
||||
|
||||
修复后,应该能看到:
|
||||
|
||||
1. 控制台日志显示正确的 bounds 和 contentSize
|
||||
2. 表情面板正常显示 emoji
|
||||
3. 表情可以正常点击和选择
|
||||
|
||||
## 总结
|
||||
|
||||
主要问题是 UIEmotionVerticalScrollView 在设置表情数据时,bounds 可能还没有正确设置,导致布局计算错误。通过延迟设置或使用有效的尺寸基准可以解决这个问题。
|
||||
|
@@ -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,130 +0,0 @@
|
||||
# PIGiftBravoGiftBroadcastView 动画速度修复
|
||||
|
||||
## 问题描述
|
||||
`PIGiftBravoGiftBroadcastView` 中内容切换的动画变得非常快,影响用户体验。
|
||||
|
||||
## 问题分析
|
||||
|
||||
### 1. 动画时序问题
|
||||
- **原有时序**:
|
||||
- 入场动画:0.5秒
|
||||
- 停留时间:2.0秒延迟
|
||||
- 出场动画:0.5秒
|
||||
- 下一个动画间隔:0.1秒
|
||||
- **问题**:间隔时间过短,导致动画切换过快
|
||||
|
||||
### 2. 状态管理问题
|
||||
- 缺少严格的状态检查
|
||||
- 可能存在多个动画同时运行的情况
|
||||
- 状态同步不够完善
|
||||
|
||||
### 3. 数据源问题
|
||||
- 没有对数据源进行验证
|
||||
- 数据源不足时可能导致动画异常
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 调整动画时序
|
||||
- **停留时间**:从2秒增加到3秒
|
||||
- **间隔时间**:从0.1秒增加到0.5秒
|
||||
- **总周期**:从3.1秒增加到4.5秒
|
||||
|
||||
### 2. 优化状态管理
|
||||
- 添加更严格的状态检查
|
||||
- 确保只有一个动画在运行
|
||||
- 防止容器中同时存在多个视图
|
||||
|
||||
### 3. 添加数据源检查
|
||||
- 在setupUI中验证数据源数量
|
||||
- 添加详细的日志输出便于调试
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 文件:`PIGiftBravoGiftBroadcastView.m`
|
||||
|
||||
#### 1. setupUI方法
|
||||
```objc
|
||||
// 检查数据源
|
||||
if (self.source.count < 2) {
|
||||
NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 数据源不足 (%lu个),动画可能过快", (unsigned long)self.source.count);
|
||||
} else {
|
||||
NSLog(@"✅ PIGiftBravoGiftBroadcastView: 数据源正常 (%lu个)", (unsigned long)self.source.count);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. playCurrentAnimation方法
|
||||
```objc
|
||||
// 更严格的状态检查
|
||||
if (self.shouldStopAnimation) {
|
||||
self.isAnimating = NO;
|
||||
NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画被停止标志阻止");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保只有一个动画在运行
|
||||
if (self.container.subviews.count > 0) {
|
||||
NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 容器中已有视图,跳过当前动画");
|
||||
return;
|
||||
}
|
||||
|
||||
// 停留时间:从2秒增加到3秒
|
||||
[UIView animateWithDuration:0.5 delay:3.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
item.frame = CGRectMake(-KScreenWidth, 2, KScreenWidth - 25 - 42, 26);
|
||||
} completion:^(BOOL finished) {
|
||||
// 使用定时器延迟下一个动画,从0.1秒增加到0.5秒
|
||||
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO];
|
||||
NSLog(@"⏰ PIGiftBravoGiftBroadcastView: 安排下一个动画,延迟0.5秒");
|
||||
}];
|
||||
```
|
||||
|
||||
#### 3. 添加详细日志
|
||||
- 动画启动/停止状态
|
||||
- 数据源验证结果
|
||||
- 动画播放进度
|
||||
- 状态变化追踪
|
||||
|
||||
## 预期效果
|
||||
|
||||
### 1. 动画速度改善
|
||||
- 动画切换更加平滑自然
|
||||
- 用户有足够时间阅读内容
|
||||
- 减少视觉疲劳
|
||||
|
||||
### 2. 稳定性提升
|
||||
- 防止多个动画同时运行
|
||||
- 更好的状态管理
|
||||
- 减少异常情况
|
||||
|
||||
### 3. 调试便利性
|
||||
- 详细的日志输出
|
||||
- 便于问题定位
|
||||
- 性能监控
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 功能测试
|
||||
- 验证动画时序是否正确
|
||||
- 检查状态管理是否正常
|
||||
- 确认数据源检查是否有效
|
||||
|
||||
### 2. 性能测试
|
||||
- 监控内存使用情况
|
||||
- 检查CPU占用率
|
||||
- 验证动画流畅度
|
||||
|
||||
### 3. 边界测试
|
||||
- 数据源为空的情况
|
||||
- 快速切换场景的情况
|
||||
- 内存压力下的表现
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **向后兼容**:修改保持了原有的API接口不变
|
||||
2. **性能影响**:增加了日志输出,在Release版本中可以考虑移除
|
||||
3. **配置灵活**:动画时间可以通过常量定义,便于后续调整
|
||||
|
||||
## 修改时间
|
||||
2025年1月27日
|
||||
|
||||
## 修改人员
|
||||
AI Assistant
|
@@ -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
|
||||
- **最后更新**: 当前版本
|
@@ -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