// // KBStreamOverlayView.m // #import "KBStreamOverlayView.h" #import "KBStreamTextView.h" #import "Masonry.h" @interface KBStreamOverlayView () @property (nonatomic, strong) KBStreamTextView *textViewInternal; @property (nonatomic, strong) UIButton *closeButton; // 新增:流式打字机用的缓冲 & 定时器 @property (nonatomic, strong) NSMutableString *pendingText; @property (nonatomic, strong) NSTimer *streamTimer; @property (nonatomic, assign) NSInteger charsPerTick; // 每次“跳”几个字符 // 新增:标记 SSE 已经收到 done @property (nonatomic, assign) BOOL streamDidReceiveDone; @end @implementation KBStreamOverlayView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.92]; self.layer.cornerRadius = 12.0; self.layer.masksToBounds = YES; [self addSubview:self.textViewInternal]; [self addSubview:self.closeButton]; [self.textViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).offset(0); make.right.equalTo(self.mas_right).offset(0); make.bottom.equalTo(self.mas_bottom).offset(0); make.top.equalTo(self.mas_top).offset(0); }]; [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_top).offset(8); make.right.equalTo(self.mas_right).offset(-8); make.height.mas_equalTo(28); make.width.mas_greaterThanOrEqualTo(56); }]; _pendingText = [NSMutableString string]; _charsPerTick = 2; // 每次输出 1~2 个字符,可以自己调 _streamDidReceiveDone = NO; } return self; } - (KBStreamTextView *)textViewInternal { if (!_textViewInternal) { _textViewInternal = [[KBStreamTextView alloc] init]; } return _textViewInternal; } - (UIButton *)closeButton { if (!_closeButton) { UIButton *del = [UIButton buttonWithType:UIButtonTypeSystem]; del.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.35]; del.layer.cornerRadius = 14; del.layer.masksToBounds = YES; del.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]; [del setTitle:KBLocalized(@"common_back") forState:UIControlStateNormal]; [del setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [del addTarget:self action:@selector(onTapClose) forControlEvents:UIControlEventTouchUpInside]; _closeButton = del; } return _closeButton; } - (void)onTapClose { if ([self.delegate respondsToSelector:@selector(streamOverlayDidTapClose:)]) { [self.delegate streamOverlayDidTapClose:self]; } } - (void)appendChunk:(NSString *)text { if (text.length == 0) return; if (![NSThread isMainThread]) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf appendChunk:text]; }); return; } [self.pendingText appendString:text]; [self startStreamTimerIfNeeded]; } - (void)startStreamTimerIfNeeded { if (self.streamTimer) return; self.streamTimer = [NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:@selector(handleStreamTick) userInfo:nil repeats:YES]; } - (void)stopStreamTimer { [self.streamTimer invalidate]; self.streamTimer = nil; } - (void)handleStreamTick { if (self.pendingText.length == 0) { // 如果已经收到 done 并且没有待播内容了,这里再真正 finish if (self.streamDidReceiveDone) { [self.textViewInternal finishStreaming]; } [self stopStreamTimer]; return; } NSInteger len = MIN(self.charsPerTick, self.pendingText.length); NSString *slice = [self.pendingText substringToIndex:len]; [self.pendingText deleteCharactersInRange:NSMakeRange(0, len)]; [self.textViewInternal appendStreamText:slice]; } - (void)finish { if (![NSThread isMainThread]) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf finish]; }); return; } // 只标记“流已结束” self.streamDidReceiveDone = YES; // 如果此时已经没有待播内容了,可以立即结束 if (self.pendingText.length == 0) { [self stopStreamTimer]; [self.textViewInternal finishStreaming]; } // 否则等 handleStreamTick 把 pendingText 慢慢播完, // 它看到 pendingText == 0 且 streamDidReceiveDone == YES 时会自动调用 finishStreaming } - (KBStreamTextView *)textView { return self.textViewInternal; } @end