This commit is contained in:
2025-11-12 14:36:15 +08:00
parent afc44cb471
commit 1dbe04cdf9
3 changed files with 78 additions and 11 deletions

View File

@@ -33,6 +33,10 @@ typedef void (^KBStreamFetcherFinishHandler)(NSError *_Nullable error);
@property (nonatomic, assign) BOOL treatSlashTAsTab; // 默认 YES将“/t”替换为“\t”
@property (nonatomic, assign) BOOL trimLeadingTabOnce; // 默认 YES首次正文起始的“\t”删一个忽略前导空白
@property (nonatomic, assign) NSTimeInterval requestTimeout; // 默认 30s
/// UI 刷新节奏:当一次回调内解析出多段(如多条 SSE 事件)时,按该间隔逐条回调(默认 0.10s)。
@property (nonatomic, assign) NSTimeInterval flushInterval;
/// 非 SSE 且一次性拿到大段文本时,是否按空格切词逐条回调(模拟“逐词流式”),默认 YES。
@property (nonatomic, assign) BOOL splitLargeDeltasOnWhitespace;
// 回调(统一在主线程触发)
@property (nonatomic, copy, nullable) KBStreamFetcherChunkHandler onChunk;
@@ -45,4 +49,3 @@ typedef void (^KBStreamFetcherFinishHandler)(NSError *_Nullable error);
@end
NS_ASSUME_NONNULL_END

View File

@@ -14,6 +14,9 @@
@property (nonatomic, assign) NSInteger decodedPrefixBytes; // sseTextBuffer SSE
@property (nonatomic, assign) NSInteger deliveredCharCount; // SSE
@property (nonatomic, assign) BOOL hasEmitted; // 1 \t
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingQueue; //
@property (nonatomic, strong) NSTimer *flushTimer; //
@property (nonatomic, strong, nullable) NSError *finishError; //
@end
// UTF-8
@@ -53,6 +56,9 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
_textEncoding = NSUTF8StringEncoding;
_buffer = [NSMutableData data];
_sseTextBuffer = [NSMutableString string];
_pendingQueue = [NSMutableArray array];
_flushInterval = 0.10;
_splitLargeDeltasOnWhitespace = YES;
}
return self;
}
@@ -84,6 +90,9 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
self.decodedPrefixBytes = 0;
self.deliveredCharCount = 0;
self.hasEmitted = NO;
[self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil;
self.task = [self.session dataTaskWithRequest:req];
[self.task resume];
@@ -99,6 +108,9 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
self.decodedPrefixBytes = 0;
self.deliveredCharCount = 0;
self.hasEmitted = NO;
[self.pendingQueue removeAllObjects];
[self.flushTimer invalidate]; self.flushTimer = nil;
self.finishError = nil;
}
#pragma mark - NSURLSessionDataDelegate
@@ -172,9 +184,7 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
if (payload.length > 0 && [payload hasSuffix:@"\n"]) {
[payload deleteCharactersInRange:NSMakeRange(payload.length - 1, 1)];
}
if (payload.length > 0) {
[self emitChunk:payload];
}
if (payload.length > 0) { [self enqueueChunk:payload]; }
}
}
return;
@@ -186,7 +196,21 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
if (self.deliveredCharCount < (NSInteger)prefix.length) {
NSString *delta = [prefix substringFromIndex:self.deliveredCharCount];
self.deliveredCharCount = prefix.length;
[self emitChunk:delta];
if (self.splitLargeDeltasOnWhitespace && delta.length > 16) {
// 使
NSArray<NSString *> *parts = [delta componentsSeparatedByString:@" "];
for (NSUInteger i = 0; i < parts.count; i++) {
NSString *w = parts[i];
if (w.length == 0) { [self enqueueChunk:@" "]; continue; }
if (i + 1 < parts.count) {
[self enqueueChunk:[w stringByAppendingString:@" "]];
} else {
[self enqueueChunk:w];
}
}
} else {
[self enqueueChunk:delta];
}
}
}
@@ -212,8 +236,14 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
}
}
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(error); });
[self cancel];
// finish
if (self.pendingQueue.count > 0) {
self.finishError = error;
[self startFlushTimerIfNeeded];
} else {
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(error); });
[self cancel];
}
}
#pragma mark - Helpers
@@ -243,5 +273,34 @@ static NSUInteger kb_validUTF8PrefixLen(NSData *data) {
self.hasEmitted = YES;
}
@end
#pragma mark - Queue/Flush
- (void)enqueueChunk:(NSString *)s {
if (s.length == 0) return;
[self.pendingQueue addObject:s];
[self startFlushTimerIfNeeded];
}
- (void)startFlushTimerIfNeeded {
if (self.flushTimer) return;
__weak typeof(self) weakSelf = self;
self.flushTimer = [NSTimer scheduledTimerWithTimeInterval:MAX(0.01, self.flushInterval)
repeats:YES
block:^(NSTimer * _Nonnull t) {
__strong typeof(weakSelf) self = weakSelf; if (!self) { [t invalidate]; return; }
if (self.pendingQueue.count == 0) {
[t invalidate]; self.flushTimer = nil;
if (self.finishError || self.finishError == nil) {
NSError *err = self.finishError; self.finishError = nil;
if (self.onFinish) dispatch_async(dispatch_get_main_queue(), ^{ self.onFinish(err); });
[self cancel];
}
return;
}
NSString *first = self.pendingQueue.firstObject;
[self.pendingQueue removeObjectAtIndex:0];
[self emitChunk:first];
}];
}
@end

View File

@@ -56,6 +56,9 @@
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_scrollView.alwaysBounceVertical = YES;
_scrollView.showsVerticalScrollIndicator = YES;
//
_scrollView.contentInset = UIEdgeInsetsMake(0, 0, 6, 0);
_scrollView.scrollIndicatorInsets = _scrollView.contentInset;
[self addSubview:_scrollView];
}
@@ -171,7 +174,6 @@
label.textColor = self.labelTextColor;
label.userInteractionEnabled = YES;
label.text = @"";
label.backgroundColor = [UIColor redColor];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleLabelTap:)];
[label addGestureRecognizer:tap];
@@ -196,8 +198,10 @@
NSString *trimmed = [current.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
current.text = trimmed;
self.buffer = trimmed;
[self layoutLabelsForCurrentWidth];
}
//
[self layoutLabelsForCurrentWidth];
[self scrollToBottomIfNeeded];
}
- (void)layoutSubviews {
@@ -247,7 +251,8 @@
CGFloat contentHeight = self.scrollView.contentSize.height;
if (contentHeight > height && height > 0) {
CGPoint bottomOffset = CGPointMake(0, contentHeight - height);
[self.scrollView setContentOffset:bottomOffset animated:YES];
// 使
[self.scrollView setContentOffset:bottomOffset animated:NO];
}
}