Files
keyboard/CustomKeyboard/View/KBStreamTextView.m

422 lines
16 KiB
Mathematica
Raw Normal View History

2025-11-11 19:39:33 +08:00
//
// KBStreamTextView.m
// KeyBoard
//
//
// "\t" UILabel
// UIScrollView
//
#import "KBStreamTextView.h"
2025-11-11 21:48:26 +08:00
#import "KBResponderUtils.h" // UIInputViewController宿
2025-11-11 19:39:33 +08:00
@interface KBStreamTextView ()
//
@property (nonatomic, strong) UIScrollView *scrollView;
//
@property (nonatomic, strong) NSMutableArray<UILabel *> *labels;
// labels 线
@property (nonatomic, strong) NSMutableArray<UIView *> *separatorLines;
2025-12-05 21:54:10 +08:00
// labels
@property (nonatomic, strong) NSMutableArray<NSString *> *segmentTexts;
2025-11-11 19:39:33 +08:00
//
@property (nonatomic, copy) NSString *buffer;
@end
static const CGFloat kKBStreamSeparatorHeight = 0.5f;
static const CGFloat kKBStreamSeparatorSpacingAbove = 5.0f;
static const CGFloat kKBStreamSeparatorSpacingBelow = 10.0f;
2025-11-11 19:39:33 +08:00
@implementation KBStreamTextView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self commonInit];
}
return self;
}
- (void)commonInit {
_delimiter = @"\t";
_labelFont = [UIFont systemFontOfSize:16.0];
2025-12-05 21:54:10 +08:00
// 使 #02BEAC
_labelTextColor = [UIColor colorWithRed:2.0/255.0 green:190.0/255.0 blue:172.0/255.0 alpha:1.0];
2025-11-11 19:39:33 +08:00
_contentHorizontalPadding = 12.0;
_interItemSpacing = 5.0; // 5pt
_labels = [NSMutableArray array];
_separatorLines = [NSMutableArray array];
2025-12-05 21:54:10 +08:00
_segmentTexts = [NSMutableArray array];
2025-11-11 19:39:33 +08:00
_buffer = @"";
_shouldTrimSegments = YES;
//
_scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_scrollView.alwaysBounceVertical = YES;
_scrollView.showsVerticalScrollIndicator = YES;
2025-11-12 14:36:15 +08:00
//
_scrollView.contentInset = UIEdgeInsetsMake(0, 0, 6, 0);
_scrollView.scrollIndicatorInsets = _scrollView.contentInset;
2025-11-11 19:39:33 +08:00
[self addSubview:_scrollView];
}
2025-11-12 15:31:22 +08:00
#pragma mark - Helpers
// /
static inline NSString *KBTrimRight(NSString *s) {
if (s.length == 0) return s;
NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSInteger end = (NSInteger)s.length - 1;
while (end >= 0 && [set characterIsMember:[s characterAtIndex:(NSUInteger)end]]) {
end--;
}
if (end < 0) return @"";
return [s substringToIndex:(NSUInteger)end + 1];
}
2025-11-11 19:39:33 +08:00
#pragma mark - Public API
// label label
- (void)appendStreamText:(NSString *)text {
if (text.length == 0) { return; }
// 线线 UI
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf appendStreamText:text];
});
return;
}
// 退
if (self.delimiter.length == 0) {
[self ensureCurrentLabelExists];
self.buffer = [self.buffer stringByAppendingString:text];
2025-12-05 21:54:10 +08:00
//
NSUInteger idx = (self.labels.count > 0) ? (self.labels.count - 1) : 0;
[self setContent:self.buffer forSegmentAtIndex:idx];
2025-11-11 19:39:33 +08:00
[self layoutLabelsForCurrentWidth];
[self scrollToBottomIfNeeded];
return;
}
// label
[self ensureCurrentLabelExists];
// parts.count = = parts.count - 1
NSArray<NSString *> *parts = [text componentsSeparatedByString:self.delimiter];
// 1)
self.buffer = [self.buffer stringByAppendingString:parts.firstObject ?: @""];
2025-12-05 21:54:10 +08:00
//
NSUInteger currentIndex = (self.labels.count > 0) ? (self.labels.count - 1) : 0;
[self setContent:self.buffer forSegmentAtIndex:currentIndex];
2025-11-11 19:39:33 +08:00
[self layoutLabelsForCurrentWidth];
2025-11-12 15:31:22 +08:00
// 2)
2025-11-11 19:39:33 +08:00
for (NSUInteger i = 1; i < parts.count; i++) {
// a)
2025-12-05 21:54:10 +08:00
NSString *currentText = (self.segmentTexts.count > currentIndex)
? (self.segmentTexts[currentIndex] ?: @"")
: (self.buffer ?: @"");
if (self.shouldTrimSegments) {
currentText = KBTrimRight(currentText ?: @"");
}
[self setContent:currentText forSegmentAtIndex:currentIndex];
2025-11-11 19:39:33 +08:00
[self layoutLabelsForCurrentWidth];
// b)
[self createEmptyLabelAsNewSegment];
2025-12-05 21:54:10 +08:00
currentIndex = (self.labels.count > 0) ? (self.labels.count - 1) : 0;
2025-11-11 19:39:33 +08:00
// c)
2025-12-05 21:54:10 +08:00
NSString *piece = parts[i] ?: @"";
self.buffer = piece;
[self setContent:self.buffer forSegmentAtIndex:currentIndex];
2025-11-11 19:39:33 +08:00
[self layoutLabelsForCurrentWidth];
}
[self scrollToBottomIfNeeded];
}
- (void)reset {
for (UILabel *lbl in self.labels) {
[lbl removeFromSuperview];
}
[self.labels removeAllObjects];
for (UIView *line in self.separatorLines) {
[line removeFromSuperview];
}
[self.separatorLines removeAllObjects];
2025-12-05 21:54:10 +08:00
[self.segmentTexts removeAllObjects];
2025-11-11 19:39:33 +08:00
self.buffer = @"";
self.scrollView.contentSize = CGSizeMake(self.bounds.size.width, 0);
}
#pragma mark - Layout Helpers
- (void)addLabelForText:(NSString *)text {
2025-11-12 15:31:22 +08:00
if (self.shouldTrimSegments) { text = KBTrimRight(text ?: @""); }
2025-11-11 19:39:33 +08:00
// UILabel
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.numberOfLines = 0;
label.font = self.labelFont;
label.textColor = self.labelTextColor;
label.userInteractionEnabled = YES; //
2025-12-05 21:54:10 +08:00
NSUInteger idx = self.labels.count;
NSString *content = text ?: @"";
label.text = [self displayTextForSegmentIndex:idx content:content];
2025-11-11 19:39:33 +08:00
//
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleLabelTap:)];
[label addGestureRecognizer:tap];
UIView *separator = [self buildSeparatorLine];
[self.scrollView addSubview:separator];
[self.separatorLines addObject:separator];
2025-11-11 19:39:33 +08:00
[self.scrollView addSubview:label];
[self.labels addObject:label];
2025-12-05 21:54:10 +08:00
// labels
if (self.segmentTexts.count <= idx) {
while (self.segmentTexts.count < idx) { [self.segmentTexts addObject:@""]; }
[self.segmentTexts addObject:content];
} else {
self.segmentTexts[idx] = content;
}
2025-11-11 19:39:33 +08:00
//
[self layoutLabelsForCurrentWidth];
//
[self scrollToBottomIfNeeded];
}
#pragma mark - Streaming Helpers
// label
- (void)ensureCurrentLabelExists {
if (self.labels.lastObject) { return; }
[self createEmptyLabelAsNewSegment];
}
//
- (void)createEmptyLabelAsNewSegment {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.numberOfLines = 0;
label.font = self.labelFont;
label.textColor = self.labelTextColor;
label.userInteractionEnabled = YES;
2025-12-05 21:54:10 +08:00
NSUInteger idx = self.labels.count;
// 1:
label.text = [self displayTextForSegmentIndex:idx content:@""];
2025-11-11 19:39:33 +08:00
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleLabelTap:)];
[label addGestureRecognizer:tap];
UIView *separator = [self buildSeparatorLine];
[self.scrollView addSubview:separator];
[self.separatorLines addObject:separator];
2025-11-11 19:39:33 +08:00
[self.scrollView addSubview:label];
[self.labels addObject:label];
2025-12-05 21:54:10 +08:00
// segmentTexts labels
if (self.segmentTexts.count <= idx) {
while (self.segmentTexts.count < idx) { [self.segmentTexts addObject:@""]; }
[self.segmentTexts addObject:@""];
} else {
self.segmentTexts[idx] = @"";
}
2025-11-11 19:39:33 +08:00
self.buffer = @"";
[self layoutLabelsForCurrentWidth];
}
- (void)finishStreaming {
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf finishStreaming];
});
return;
}
2025-12-05 21:54:10 +08:00
if (self.labels.count == 0) { return; }
NSUInteger idx = self.labels.count - 1;
NSString *content = (self.segmentTexts.count > idx)
? (self.segmentTexts[idx] ?: @"")
: (self.buffer ?: @"");
2025-11-11 19:39:33 +08:00
if (self.shouldTrimSegments) {
2025-12-05 21:54:10 +08:00
content = KBTrimRight(content ?: @"");
2025-11-11 19:39:33 +08:00
}
2025-12-05 21:54:10 +08:00
[self setContent:content forSegmentAtIndex:idx];
self.buffer = content;
2025-11-12 14:36:15 +08:00
//
[self layoutLabelsForCurrentWidth];
[self scrollToBottomIfNeeded];
2025-11-11 19:39:33 +08:00
}
- (void)layoutSubviews {
[super layoutSubviews];
// /
[self layoutLabelsForCurrentWidth];
}
- (void)layoutLabelsForCurrentWidth {
CGFloat width = self.bounds.size.width;
if (width <= 0) { return; }
CGFloat x = self.contentHorizontalPadding;
CGFloat maxLabelWidth = MAX(0.0, width - 2.0 * self.contentHorizontalPadding);
CGFloat y = self.interItemSpacing; //
for (NSUInteger idx = 0; idx < self.labels.count; idx++) {
UILabel *label = self.labels[idx];
CGSize size = [self sizeForText:label.text font:label.font maxWidth:maxLabelWidth];
label.frame = CGRectMake(x, y, maxLabelWidth, size.height);
y += size.height;
UIView *separator = (idx < self.separatorLines.count) ? self.separatorLines[idx] : nil;
CGFloat separatorTop = y + kKBStreamSeparatorSpacingAbove;
if (separator) {
separator.hidden = NO;
separator.frame = CGRectMake(x, separatorTop, maxLabelWidth, kKBStreamSeparatorHeight);
}
y = separatorTop + kKBStreamSeparatorHeight;
2025-11-11 19:39:33 +08:00
if (idx + 1 < self.labels.count) {
y += kKBStreamSeparatorSpacingBelow;
2025-11-11 19:39:33 +08:00
}
}
y += self.interItemSpacing;
CGFloat contentHeight = MAX(y, self.bounds.size.height + 1.0);
2025-11-11 19:39:33 +08:00
self.scrollView.contentSize = CGSizeMake(width, contentHeight);
}
- (CGSize)sizeForText:(NSString *)text font:(UIFont *)font maxWidth:(CGFloat)maxWidth {
if (text.length == 0) {
//
return CGSizeMake(maxWidth, font.lineHeight);
}
CGRect rect = [text boundingRectWithSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName: font}
context:nil];
//
return CGSizeMake(maxWidth, ceil(rect.size.height));
}
- (void)scrollToBottomIfNeeded {
CGFloat height = self.scrollView.bounds.size.height;
CGFloat contentHeight = self.scrollView.contentSize.height;
if (contentHeight > height && height > 0) {
CGPoint bottomOffset = CGPointMake(0, contentHeight - height);
2025-11-12 14:36:15 +08:00
// 使
[self.scrollView setContentOffset:bottomOffset animated:NO];
2025-11-11 19:39:33 +08:00
}
}
#pragma mark - Tap Handling
- (void)handleLabelTap:(UITapGestureRecognizer *)tap {
UILabel *label = (UILabel *)tap.view;
if (![label isKindOfClass:[UILabel class]]) { return; }
NSInteger index = [self.labels indexOfObject:label];
2025-12-05 21:54:10 +08:00
// 宿使
NSString *rawText = (index != NSNotFound && index < (NSInteger)self.segmentTexts.count)
? (self.segmentTexts[(NSUInteger)index] ?: @"")
: (label.text ?: @"");
2025-11-11 19:39:33 +08:00
if (index != NSNotFound && self.onLabelTap) {
2025-12-05 21:54:10 +08:00
self.onLabelTap(index, rawText);
2025-11-11 21:48:26 +08:00
}
// 宿/TextView
// 访 insertText:
UIInputViewController *ivc = KBFindInputViewController(self);
if (ivc) {
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
if ([proxy conformsToProtocol:@protocol(UITextDocumentProxy)]) {
2025-12-16 21:43:00 +08:00
//
BOOL canAdjustCaret = [proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)];
if (canAdjustCaret) {
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
while (contextAfter.length > 0) {
NSInteger offset = (NSInteger)contextAfter.length;
[proxy adjustTextPositionByCharacterOffset:offset];
for (NSUInteger i = 0; i < contextAfter.length; i++) {
[proxy deleteBackward];
}
contextAfter = proxy.documentContextAfterInput ?: @"";
}
}
2025-12-16 21:43:00 +08:00
NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
while (contextBefore.length > 0) {
for (NSUInteger i = 0; i < contextBefore.length; i++) {
[proxy deleteBackward];
}
contextBefore = proxy.documentContextBeforeInput ?: @"";
}
if (rawText.length > 0) {
[proxy insertText:rawText];
}
2025-11-11 21:48:26 +08:00
}
2025-11-11 19:39:33 +08:00
}
}
2025-12-05 21:54:10 +08:00
#pragma mark - Segment Helpers
// 1
- (NSString *)displayTextForSegmentIndex:(NSUInteger)index content:(NSString *)content {
NSInteger displayIndex = (NSInteger)index + 1;
//
NSString *body = content ?: @"";
body = [body stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"];
//
while ([body containsString:@"\n\n"]) {
body = [body stringByReplacingOccurrencesOfString:@"\n\n" withString:@"\n"];
}
//
body = [body stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
if (body.length > 0) {
return [NSString stringWithFormat:@"%ld: %@", (long)displayIndex, body];
} else {
return [NSString stringWithFormat:@"%ld: ", (long)displayIndex];
}
}
// label
- (void)setContent:(NSString *)content forSegmentAtIndex:(NSUInteger)idx {
if (idx >= self.labels.count) { return; }
NSString *body = content ?: @"";
if (self.segmentTexts.count <= idx) {
while (self.segmentTexts.count < idx) { [self.segmentTexts addObject:@""]; }
[self.segmentTexts addObject:body];
} else {
self.segmentTexts[idx] = body;
}
UILabel *label = self.labels[idx];
label.text = [self displayTextForSegmentIndex:idx content:body];
}
- (UIView *)buildSeparatorLine {
UIView *line = [[UIView alloc] initWithFrame:CGRectZero];
line.backgroundColor = [UIColor colorWithHex:0x02BEAC];
line.hidden = YES;
return line;
}
2025-11-11 19:39:33 +08:00
@end