zoukankan      html  css  js  c++  java
  • YYKit持续补丁 brave

    1.索引

        [[IQKeyboardManager sharedManager] registerTextFieldViewClass:[YYTextView class]
                                      didBeginEditingNotificationName:YYTextViewTextDidBeginEditingNotification
                                        didEndEditingNotificationName:YYTextViewTextDidEndEditingNotification];
        //默认关闭IQKeyboard 在需要的界面启用
        [IQKeyboardManager sharedManager].enable = NO;
        [[IQKeyboardManager sharedManager] setEnableAutoToolbar:NO];
    复制代码
    • 布局问题

    • 如果配合AutoLayout 计算Size后务必更新约束

    • 目前能够保证没有问题的方法 SizeThatFits 【注意】如果文本以大段空白(\n)作为结尾,可能导致计算出来的高度有误,所以实际中最好进行空白段落处理。

    
    //preferLayoutWidth布局指定宽度
    
    CGSize fitSize =[self.txtContent sizeThatFits:CGSizeMake(preferLayoutWidth,CGFLOAT_MAX)];
    
    //宽度修正_当文本中包含大量空格时,Size会出问题
    
    float fixWidth = fitSize.width;
    
    if(fitSize.width > preferLayoutWidth)
    
    {
    
    fixWidth = preferLayoutWidth;
    
    }
    
    复制代码
    • 输入标点符号后,键盘不自动切换的问题。(对比UITextView)参见点#392

    • 增加属性NumberOfLine、TruncationToken用于满足行数限制的个别需求(主要应用场景,带有选择菜单、文本选择、放大镜等效果的YYLabel)参见点#392

    • YYTextView的SuperView为UIScrollView时,文本选择及复制菜单的问题。

    • 修改YYKit源码,增加代理方法

    
    YYTextView.h
    
    @protocol YYTextViewSelectMenuDelegate
    
    /**
    
    *@brief在即将进入选择文本选择状态时调用
    
    */
    
    -(void)textViewWillShowSelectMenu:(YYTextView *)textView;
    
    /**
    
    *@brief在即将推出选择文本选择状态时调用
    
    */
    
    -(void)textViewWillHideSelectMenu:(YYTextView *)textView;
    
    @end
    
    YYTextView.m
    
    -(void)_showMenu {
    
    …//代码块最后
    
    if([self.delegateSelectMenu  respondsToSelector:@selector(textViewWillShowSelectMenu:)]){
    
    [self.delegateSelectMenu textViewWillShowSelectMenu:self];
    
    }
    
    }
    
    -(void)_hideMenu {
    
    if(_state.showingMenu){
    
    _state.showingMenu = NO;
    
    UIMenuController *menu =[UIMenuController sharedMenuController];
    
    [menu setMenuVisible:NO animated:YES];
    
    if([self.delegateSelectMenu respondsToSelector:@selector(textViewWillHideSelectMenu:)]){
    
    [self.delegateSelectMenu textViewWillHideSelectMenu:self];
    
    }
    
    }
    
    if(_containerView.isFirstResponder){
    
    _state.ignoreFirstResponder = YES;
    
    [_containerView resignFirstResponder];// it will call[self becomeFirstResponder],ignore it temporary.
    
    _state.ignoreFirstResponder = NO;
    
    }
    
    }
    
    复制代码
    • 需求YYTextView添加控件后,需要自动更新ContentSize

    • YYTextView子类重写

    
    -(void)setContentSize:(CGSize)contentSize{
    
    //不影响其它的位置
    
    //viewExPanle子类属性用于添加控件
    
    if(!self.viewExPanle){
    
    [super setContentSize:contentSize];
    
    return;
    
    }
    
    //实际文本内容Size
    
    CGSize txtSize = self.textLayout.textBoundingSize;
    
    if(txtSize.width > self.preferredMaxLayoutWidth){
    
    txtSize.width = self.preferredMaxLayoutWidth;
    
    }
    
    CGFloat fltExControlHeight = 0;
    
    //这里必须要获取真实的数据
    
    if(!self.viewExPanle.hidden){
    
    fltExControlHeight += self.viewExPanle.height
    
    self.fltExTopMargin;//fltExTopMargin子类属性,控件与文本的间距
    
    }
    
    txtSize.height += fltExControlHeight;
    
    [super setContentSize:txtSize];
    
    }
    
    //子类方法提供外部调用以更新ContentSize
    
    -(void)updateContentSizeByExPanleChange{
    
    if(!self.viewExPanle){
    
    return;
    
    }
    
    //只为触发方法设置
    
    [self setContentSize:CGSizeZero];
    
    }
    
    复制代码
    • 为了调整文本行间距在子类中设置了linePositionModifier,可以给子类暴露一些方法,可自定义行间距参见YYKit Demo WBTextLinePositionModifier

    • 为了适应需求(表情特么不规范!!!),表情在文本中宽度自由

    
    YYTextUtilities.h
    
    新增
    
    /**
    
    Get the `AppleColorEmoji` font's glyph bounding rect with a specified font size.
    
    It may used to create custom emoji.
    
    @param fontSizeThe specified font size.
    
    @param imageScale图片宽高比
    
    @return The font glyph bounding rect.
    
    高度统一,宽度自由
    
    @saylor--为了适应宽度自由的图片作为表情
    
    */
    
    static inline CGRect YYEmojiGetGlyphBoundingLongRectWithFontSize(CGFloat fontSize,CGFloat imageScale){
    
    CGRect rect;
    
    rect.origin.x = 0;
    
    rect.size.height = YYEmojiGetAscentWithFontSize(fontSize);
    
    rect.size.width = rect.size.height * imageScale*0.75;
    
    if(fontSize < 16){
    
    rect.origin.y = -0.2525 * fontSize;
    
    } else if(16 <= fontSize && fontSize <= 24){
    
    rect.origin.y = 0.1225 * fontSize -6;
    
    } else {
    
    rect.origin.y = -0.1275 * fontSize;
    
    }
    
    return rect;
    
    }
    
    NSAttributedString+YYText.m
    
    +(NSMutableAttributedString *)attachmentStringWithEmojiImage:(UIImage *)image
    
    fontSize:(CGFloat)fontSize{
    
    …
    
    //原方法
    
    //CGRect bounding1 = YYEmojiGetGlyphBoundingRectWithFontSize(fontSize);
    
    //计算图片宽高比
    
    CGFloat imageScale = image.size.width / image.size.height;
    
    CGRect bounding = YYEmojiGetGlyphBoundingLongRectWithFontSize(fontSize,imageScale);
    
    ….
    
    }
    
    复制代码
    • 添加IQKeyboard支持 YYTextview
        //添加IQKeyboard支持 YYTextView
        [[IQKeyboardManager sharedManager] registerTextFieldViewClass:[YYTextView class]
                                      didBeginEditingNotificationName:YYTextViewTextDidBeginEditingNotification
                                        didEndEditingNotificationName:YYTextViewTextDidEndEditingNotification];
        //默认关闭IQKeyboard 在需要的界面启用
        [IQKeyboardManager sharedManager].enable = NO;
        [[IQKeyboardManager sharedManager] setEnableAutoToolbar:NO];
    
    复制代码

    #YYLabel

    以下问题的处理均在子类实现

    • 异步绘制,在列表刷新时YYLabel会有闪烁的问题。

    • 开启异步绘制的相关属性

    
    self.displaysAsynchronously = YES;
    //2018.01.04 注:忽略该句代码,否者变更字体、字号等属性不会触发UI更新。
    //self.ignoreCommonProperties = YES;
    
    self.fadeOnAsynchronouslyDisplay = NO;
    
    self.fadeOnHighlight = NO;
    
    //重写方法
    
    -(void)setText:(NSString *)text {
    
      //用于处理异步绘制刷新闪烁的问题
      //strCache自行声明
      if([self.strCache isEqualToString:text]){
      return;
      //防止将内容置为nil
      if(text.length == 0){
        text = @"";
      }
      self.strCache = text;
      //[super setText:text];  //这里可以注释掉
      NSAttributedString *atrContent =[[NSAttributedString alloc]initWithString:text];
      [self fixLayoutSizeWithContent:atrContent];
    }
    
    //对于使用以下方式工作的朋友需要注意!!! 否则会引起刷新闪烁的问题
    - (void)setAttributedText:(NSAttributedString *)attributedText {
        //用于处理 异步绘制 刷新闪烁的问题
        /*
        注意:该段代码存在问题,相同文本内容,但不同的属性设置会导致误判。
        if ([self.atrCache isEqualToAttributedString:attributedText] || [self.atrCache.string isEqualToString:attributedText.string]) {
            return;
        }
        */
        //变更
        if ([self.atrCache isEqualToAttributedString:attributedText]) {
            return;
        }
    
        //防止将内容置为nil
        if (attributedText.length == 0) {
            attributedText = [[NSAttributedString alloc]initWithString:@""];
        }
        self.atrCache = attributedText;
        //使用富文本进行赋值时,所以需要禁用自定义的解析,否则解析代码会覆盖掉富文本的相关属性。如:字体、颜色等
        [self fixLayoutSizeWithContent:attributedText];  
    }
    复制代码
    • iOS10和大屏6P会出现高度问题
    
    -(void)fixLayoutSizeWithContent:(NSAttributedString *)strContent {
      CGFloat perfixLayoutWidth = self.preferredMaxLayoutWidth;//布局指定宽度
      //次要-Frame布局
      if(self.size.width && !perfixLayoutWidth){
          perfixLayoutWidth = self.size.width;
      }
      NSMutableAttributedString *text =[[NSMutableAttributedString alloc]initWithAttributedString:strContent];
      //这里注意要将原有的富文本布局属性持有
      text.alignment = self.textAlignment;
      text.font = self.font;
      text.textColor = self.textColor;
    
      NSRange selectedRange = text.rangeOfAll;
      //我这里有一个自定义的解析器
     //此处增加一个判断,主要是为了防止富文本赋值的情况下,解析会覆盖掉相关的属性。而且需要解析的场景,简单文本赋值应该都能处理了。!!! 控制属性,自行添加
      if(isEnableParser){
          [self.customTextParser parseText:text selectedRange:&selectedRange];
      }
      YYTextContainer *container =[YYTextContainer new];
      container.size = CGSizeMake(perfixLayoutWidth,HUGE);
      container.linePositionModifier = self.customTextModifier;
      container.maximumNumberOfRows = self.numberOfLines;
      container.truncationType = YYTextTruncationTypeEnd;
      YYTextLayout *textLayout =[YYTextLayout layoutWithContainer:container text:text];
    
      CGSize layoutSize = textLayout.textBoundingSize;
      //宽度修正
      if(layoutSize.width > perfixLayoutWidth){
        layoutSize.width = perfixLayoutWidth;
      }
      //这个方法参照YYKit Demo中结果高度始终有问题,所以未采用
      //CGFloat height =[self.customTextModifier heightForLineCount:textLayout.rowCount];
      //iOS 10高度修正,发现iOS10和大屏6P会出现高度问题,如果不修正一个奇怪的现象就是,YYLabel的高亮文本在点击时会有跳动。但YYTextView并没有发现这样的情况
      if((DP_IS_IOS10||DP_IS_IPHONE6PLUS)&& textLayout.rowCount > 1){
      //height += 4;
      layoutSize.height += 4;//这个值也是调来调去
      }
      //最奇葩的是这里赋值一定是这个顺序,如果顺序不对也是Bug多多
      self.size = layoutSize;
      self.textLayout = textLayout;
    }
    
    复制代码
    • 配合AutoLayout:记得在赋值text后,更新Size约束

    • #更新

    • 搜狗输入法,全键盘模式下发现输入英文时,会在键盘的联想状态栏处出现连字情况。

    • 系统输入法,全键盘模式下发现点击联想栏出的单词输入时,单词会被空格打断。

    系统输入法的问题,暂时还原。因为会引起其他问题... 问题修复,问题本质原因是替换文本完成后_selectedTextRange有些情况下没有得到更新。

    问题跟踪

    • 搜狗输入法点击联想栏处输入单词的流程和系统是不同的,搜狗是先调用-(void)deleteBackward()方法,该方法会被调用多次删除之前输入的内容(上次输入的内容),然后再调用- (void)insertText:(NSString *)text方法插入联想词。

    • 系统输入法,不会走上述流程,只有一步调用- (void)replaceRange:(YYTextRange *)range withText:(NSString *)text来完成文本替换。

    ///其实以上问题的根本原因,基本上就是下面这几个代理方法的调用问题
    @protocol UITextInputDelegate <NSObject>
    
    - (void)selectionWillChange:(nullable id <UITextInput>)textInput;
    - (void)selectionDidChange:(nullable id <UITextInput>)textInput;
    - (void)textWillChange:(nullable id <UITextInput>)textInput;
    - (void)textDidChange:(nullable id <UITextInput>)textInput;
    
    @end
    
    复制代码
    • 跟着感觉走,修补的代码主要集中在以下两个方法:
    • (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify
    • (BOOL)_parseText
    ///处理后的代码
    - (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify{
        if (_isExcludeNeed) {
            notify = NO;
        }
    
        if (NSEqualRanges(range.asRange, _selectedTextRange.asRange)) {
            //这里的代理方法需要注释掉 -- 注意:该处注释会引起,iOS13下的双光标问题。不要再参考了!!!
    //        if (notify) [_inputDelegate selectionWillChange:self];
            NSRange newRange = NSMakeRange(0, 0);
            newRange.location = _selectedTextRange.start.offset + text.length;
            _selectedTextRange = [YYTextRange rangeWithRange:newRange];
    //        if (notify) [_inputDelegate selectionDidChange:self];
        } else {
            if (range.asRange.length != text.length) {
                if (notify) [_inputDelegate selectionWillChange:self];
                NSRange unionRange = NSIntersectionRange(_selectedTextRange.asRange, range.asRange);
                if (unionRange.length == 0) {
                    // no intersection
                    if (range.end.offset <= _selectedTextRange.start.offset) {
                        NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
                        NSRange newRange = _selectedTextRange.asRange;
                        newRange.location += ofs;
                        _selectedTextRange = [YYTextRange rangeWithRange:newRange];
                    }
                } else if (unionRange.length == _selectedTextRange.asRange.length) {
                    // target range contains selected range
                    _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(range.start.offset + text.length, 0)];
                } else if (range.start.offset >= _selectedTextRange.start.offset &&
                           range.end.offset <= _selectedTextRange.end.offset) {
                    // target range inside selected range
                    NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
                    NSRange newRange = _selectedTextRange.asRange;
                    newRange.length += ofs;
                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
                } else {
                    // interleaving
                    if (range.start.offset < _selectedTextRange.start.offset) {
                        NSRange newRange = _selectedTextRange.asRange;
                        newRange.location = range.start.offset + text.length;
                        newRange.length -= unionRange.length;
                        _selectedTextRange = [YYTextRange rangeWithRange:newRange];
                    } else {
                        NSRange newRange = _selectedTextRange.asRange;
                        newRange.length -= unionRange.length;
                        _selectedTextRange = [YYTextRange rangeWithRange:newRange];
                    }
                }
            
     _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
                if (notify) [_inputDelegate selectionDidChange:self];
            }
        }
    
        //这里的代理方法 需要判断,如果设置了解析器则不执行,分析解析器方法中重复执行会有问题。
        if (!self.textParser) [_inputDelegate textWillChange:self];
        NSRange newRange = NSMakeRange(range.asRange.location, text.length);
        [_innerText replaceCharactersInRange:range.asRange withString:text];
        [_innerText removeDiscontinuousAttributesInRange:newRange];
        if (!self.textParser) [_inputDelegate textDidChange:self];
    
        /*
         修正光标位置的方法放在这里,因为此处已经替换文本完毕
         问题的本质原因,替换完文本后 range 没有得到更新
         */
    //    NSLog(@"Correct cursor position");
        if (range.asRange.location + range.asRange.length == _selectedTextRange.asRange.location && _selectedTextRange.asRange.length == 0) {
            //修正_selectedTextRange
            [_inputDelegate selectionWillChange:self];
            
            _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.asRange.location + text.length - range.asRange.length, 0)];
            
            [_inputDelegate selectionDidChange:self];
        }
    
    
    
    }
    
    复制代码
    • 解析方法
    - (BOOL)_parseText {
        if (self.textParser) {
            YYTextRange *oldTextRange = _selectedTextRange;
            NSRange newRange = _selectedTextRange.asRange;
            
            //此处方法需要注释掉
    //        if(!_isExcludeNeed)[_inputDelegate textWillChange:self];
            BOOL textChanged = [self.textParser parseText:_innerText selectedRange:&newRange];
    //        if(!_isExcludeNeed)[_inputDelegate textDidChange:self];
            
            YYTextRange *newTextRange = [YYTextRange rangeWithRange:newRange];
            newTextRange = [self _correctedTextRange:newTextRange];
            
            if (![oldTextRange isEqual:newTextRange]) {
                [_inputDelegate selectionWillChange:self];
                _selectedTextRange = newTextRange;
                [_inputDelegate selectionDidChange:self];
            }
            return textChanged;
        }
        return NO;
    }
    复制代码
    • 发现但没有解决掉的问题 往下看
    1. 系统全键盘输入模式下,如果碰巧触发了自动纠正模式,此时如果点击键盘删除键,会执行以下流程: 
    -[UIKeyboardlmpl deleteBackwardAndNotify:]
    -[YYTextView setSelectedTextRange:]
    ...省略部分系统内部调用
    -[UIKeyboardlmpl applyAutocorrection:]
    -[YYTextView replaceRange:withText:]
    ...
    -[YYTextView deleteBackward]
    问题就出在`-[YYTextView replaceRange:withText:]`获得的text参数,传过来的竟然是联想框内高亮的候选词。真心不知道是怎么回事,如果有了解的兄弟希望能讲解下。
    对比UITextView的表现来看,这一步完全是多余的。
    
    2. 搜狗输入法全键盘模式下,输入英文字符后,点击联想框内的候选词发现覆盖现象,正确的表现应该是替换文本、清空联想框内的候选词,然后会追加一个空格。但事实上并不是这样的,也不知道是不是苹果有意的让第三方这样去运行。从相关的代理方法中,并没有找到引起该问题的实质原因。`Tip:系统输入法是会有一个追加空格的动作的,具体可以调试看`
    代码执行流程:
    
    - [UIKeyboadlmp deleteBackwardAndNotify:]
    - [YYTextView deleteBackward]
    - [YYTextView replaceRange:withText:]
    - [YYTextView setSelectedTextRange:]
    //这个会执行多次,每次只能删除一个字符
    //删除后 再进行插入操作
    ...
    - [UIKeyboardlmpl insertText:]
    - [YYTextView insertText:]
    - [YYTextView replaceRange:withText:]
    复制代码

    关于上述问题的定位

    -[YYTextView replaceRange:withText:] YYTextView.m:3541
    -[UIKeyboardImpl applyAutocorrection:] 0x00000001077f78ca
    -[UIKeyboardImpl acceptAutocorrection:executionContextPassingTIKeyboardCandidate:] 0x00000001077ed735
    -[UIKeyboardImpl acceptAutocorrectionForWordTerminator:executionContextPassingTIKeyboardCandidate:] 0x00000001077ece54
    __56-[UIKeyboardImpl acceptAutocorrectionForWordTerminator:]_block_invoke 0x00000001077ecc77
    -[UIKeyboardTaskQueue continueExecutionOnMainThread] 0x000000010805c782
    -[UIKeyboardTaskQueue performTaskOnMainThread:waitUntilDone:] 0x000000010805cbab
    -[UIKeyboardImpl acceptAutocorrectionForWordTerminator:] 0x00000001077ecb87
    -[UIKeyboardImpl acceptAutocorrection] 0x00000001077ef6d9
    -[UIKeyboardImpl prepareForSelectionChange] 0x00000001077e5770
    -[YYTextView setSelectedTextRange:] YYTextView.m:3377
    复制代码

    哈 具体是哪里的问题,周一再分析~~~ 更新解决方法如下: 从上面贴出来的调用栈来看,问题基本出在以下代码中。 -[YYTextView setSelectedTextRange:] YYTextView.m:3377 注释了以后,果然清爽了。

    #pragma mark - @protocol UITextInput
    - (void)setSelectedTextRange:(YYTextRange *)selectedTextRange {
        if (!selectedTextRange) return;
        selectedTextRange = [self _correctedTextRange:selectedTextRange];
        if ([selectedTextRange isEqual:_selectedTextRange]) return;
        [self _updateIfNeeded];
        [self _endTouchTracking];
        [self _hideMenu];
        _state.deleteConfirm = NO;
        _state.typingAttributesOnce = NO;
       //这里有问题 selectionWillChange 不明原因打断了deleteBackwardAndNotify 执行
    //    [_inputDelegate selectionWillChange:self];
        _selectedTextRange = selectedTextRange;
        _lastTypeRange = _selectedTextRange.asRange;
    //    [_inputDelegate selectionDidChange:self];
        
        [self _updateOuterProperties];
        [self _updateSelectionView];
        
        if (self.isFirstResponder) {
            [self _scrollRangeToVisible:_selectedTextRange];
        }
    }
    
    复制代码

    2017年09月04日 update


    • 竖版问题

      参考了Github上另外两人提交的代码  @cszwdy @ smhjsw

      YYLabel.m 源文件做如下修改 参考链接

    + (YYTextLayout *)_shrinkLayoutWithLayout:(YYTextLayout *)layout {
        if (layout.text.length && layout.lines.count == 0) {
            YYTextContainer *container = layout.container.copy;
    //        container.maximumNumberOfRows = 1;
            CGSize containerSize = container.size;
            if (!container.verticalForm) {
                containerSize.height = YYTextContainerMaxSize.height;
            } else {
                containerSize.width = YYTextContainerMaxSize.width;
            }
            container.size = containerSize;
            return [YYTextLayout layoutWithContainer:container text:layout.text];
        } else {
            return nil;
        }
    }
    复制代码

    YYTextLayout.m 源文件做如下修改 参考链接

    //535行起
            if (constraintSizeIsExtended) {
                if (isVerticalForm) {
    //                if (rect.origin.x + rect.size.width >
    //                    constraintRectBeforeExtended.origin.x +
    //                    constraintRectBeforeExtended.size.width) break;
                    if (!CGRectIntersectsRect(rect, constraintRectBeforeExtended)) break;
                    
                } else {
                    if (rect.origin.y + rect.size.height >
                        constraintRectBeforeExtended.origin.y +
                        constraintRectBeforeExtended.size.height) break;
                }
            }
    复制代码

    2018年01月09日 update


    • YYLabel textAlignment 问题

    发现在使用textLayoutYYLabel进行完全控制布局时,如果__赋空值__则预先设置的textAlignment 将自动转换为NSTextAlignmentNatural

    这里就直接上代码了:

    //出现问题的代码 有两部分
    //1. NSAttributedString+YYText.m
    //text为空时,该宏的展开式并不会得到执行。在此情况下设置alignment是无意义的~~~
    //结果就是_innerText.alignment 是默认值 NSTextAlignmentNatural
    #define ParagraphStyleSet(_attr_) 
    
    //2. YYLabel.m 
    - (void)_updateOuterTextProperties {
        _text = [_innerText plainTextForRange:NSMakeRange(0, _innerText.length)];
        _font = _innerText.font;
        if (!_font) _font = [self _defaultFont];
        _textColor = _innerText.color;
        if (!_textColor) _textColor = [UIColor blackColor];
        
       //*******更改部分********
        //因为上面的问题,这里_innerText.alignment 是不能够采信的。
        BOOL isEmptyStr = _innerText.length == 0;
        if(!isEmptyStr)_textAlignment = _innerText.alignment;
        if(!isEmptyStr)_lineBreakMode = _innerText.lineBreakMode;
       //*******更改部分********
    
        NSShadow *shadow = _innerText.shadow;
        _shadowColor = shadow.shadowColor;
    #if !TARGET_INTERFACE_BUILDER
        _shadowOffset = shadow.shadowOffset;
    #else
        _shadowOffset = CGPointMake(shadow.shadowOffset.width, shadow.shadowOffset.height);
    #endif
        
        _shadowBlurRadius = shadow.shadowBlurRadius;
        _attributedText = _innerText;
        [self _updateOuterLineBreakMode];
    }
    
    复制代码

    2018年02月05日 update


    【残疾光标问题】 非YYKit 自身问题,权当自省了。

    问题现象:YYTextView 使用时,苹方14号字体条件下,文本初始编辑状态,输入提示光标为正常的半高(残疾)。 提示: 不要随意更改原有逻辑!!! 遵循原有设计的默认值,如CoreText 默认字号为12,而我这里设置的解析器和行间距调整器的默认字号为14。 出现问题,先找自身原因。

    - (void)setFont:(UIFont *)font {
       /*** 有问题的逻辑
        if ([self.customTextParser.font isEqual:font]) {
            return;
        }
        [super setFont:font];
        */
        
        /****改正后
        [super setFont:font];
        if ([self.customTextParser.font isEqual:font]) {
            return;
        }
        */
    
        self.customTextParser.font = font;
        self.customTextModifier.font = font;
        self.linePositionModifier = self.customTextModifier;
    }
    复制代码

    【YYTextView 内存泄露问题】不建议,iOS 14下会导致联想词功能失效

    检测工具 : MLeaksFinder 特定情形,YYTextView处于编辑状态下,且文本框中有内容。在退出当前控制器,MLeaksFinder 提示内存泄露,并在再次进入控制器,且调用reloadInputViews 时,内存得到释放。

    Simulator Screen Shot - iPhone 7 - 2018-11-07 at 17.06.43.png
    问题梳理过程:
    1. 怀疑 _innerLayout、_innerContainer 未得到正确释放,然无结果。
    2. 发现一个特性,如果文本框处于激活状态且无内容时,退出控制器时并不会造成内存泄露。 
       a. 由于在YYTextDemo中尝试,开始怀疑赋值方式有问题。 做出以下尝试:
    
    - (void)willMoveToSuperview:(UIView *)newSuperview{
        [super willMoveToSuperview:newSuperview];
        //尝试赋值时机
        if (!_state.firstInitFlag) {
            _state.firstInitFlag = YES;
            _innerText = [[NSMutableAttributedString alloc]initWithString:@""];
            return;
        }
    //    for (int i = 0; i< self.subviews.count; i++) {
    //        id view = self.subviews[i];
    //        NSLog(@"___ view %@",view);
    //    }
    
        NSLog(@"retainCount  %@",[self valueForKey:@"retainCount"]);
        NSLog(@"----------");
    //    self.text = nil;
        NSLog(@"**********");
    }
    
    /*这里的尝试起到了一些效果,发现如果YYTextView在首次加载时 
    _innerText 手动置空处理(注意不可以是 nil ),在不主动收起键盘的情况下,且手动输入内容。
     退出控制器时,可得到正确释放。 
    */
    
        b. 尝试调整赋值时机,天真的思考是不是可以把富文本的赋值方式  延后到某个时机。
           主要目的是,初始尝试手动清空,然后insertText 方式加载文本内容。最大的问题是,
           这种情况只要收起键盘,再弹起键盘,最终仍然得不到释放。
    
    3.目标转移至,追踪_innerText 的使用上。 以及
    becomeFirstResponder
    resignFirstResponder
    这两个方法都做了什么。
    
    复制代码
    屏幕快照 2018-11-07 下午5.28.54.png 多次尝试,最终注意到上述调用栈。而且理应注意到,UIKeyboardlmpl 这个对象,看名字就和键盘有点关系。 由于对这个也不是十分理解,所以就不啰嗦了。问题出现在下面的方法中。
    #pragma mark - @protocol UIKeyInput
    - (BOOL)hasText {
        return _innerText.string.length > 0;
    }
    
    - (void)insertText:(NSString *)text {
        略...
    }
    
    - (void)deleteBackward {
         略...
    }
    
    //下面是原文件
    @protocol UIKeyInput <UITextInputTraits>
    
    #if UIKIT_DEFINE_AS_PROPERTIES
    @property(nonatomic, readonly) BOOL hasText;
    #else
    - (BOOL)hasText;
    #endif
    - (void)insertText:(NSString *)text;
    - (void)deleteBackward;
    
    @end
    复制代码

    【注意!!!】 终极解决方案在这里~~~ 不建议修改

    //YYTextView.m 
    #pragma mark - @protocol UIKeyInput
    - (BOOL)hasText {
        //return _innerText.string.length > 0;
        return NO;
    }
    复制代码

    由于是刚出炉的,也没敢在线上环境测试。 看到的朋友们,谨慎对待吧。打算经过一段时间测试后,再适用到线上环境。

    Over ~ 2018年11月07日 update

    【YYTextView 复用时,解析状态丢失】

    项目中有在cell上使用YYTextView展示文本的需要,发现解析的链接等文本高亮状态在复用时丢失。

    YYTextView.m 更改

    - (void)setText:(NSString *)text {
        if (_text == text || [_text isEqualToString:text]){
            //复用的情况下,解析状态会丢失
            if ([self _parseText])_state.needUpdate = YES;
            return;
        }
        [self _setText:text];
        
        _state.selectedWithoutEdit = NO;
        _state.deleteConfirm = NO;
        [self _endTouchTracking];
        [self _hideMenu];
        [self _resetUndoAndRedoStack];
    
        [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:text];
    }
    
    - (void)setAttributedText:(NSAttributedString *)attributedText {
        if (_attributedText == attributedText){
            //复用的情况下,解析状态会丢失
            if ([self _parseText])_state.needUpdate = YES;
            return;
        }
        ....
        ....  
      
    }
    
    复制代码

    2018年11月19日 update


    【YYTextView 选中模式消除】 相关源码修改

    • 【修复】应用在cell(UITableViewCell、UICollectionViewCell)场景下,多个YYTextView可以同时弹出选择文本框并高亮的问题
    • 【修复】文本高亮状态消除逻辑,新增 通过监听系统UIMeunController的相关通知来主动触发。
    • 【修复】[UIScrollView [ YYTextView]] 内部监听顶层的UIScrollView容器的滚动情况,来自动消除选中状态。

    修改的显著效果:再也不用担心选择模式下的两个Dot,在页面切换或者滚动时的消除问题。

    屏幕快照 2019-04-02 下午4.50.15.png

    【错误修复】由于YYActiveObj.h使用类属性(class property)记录处于激活状态的YYTextView 实例,其中static关键字的应用导致实例的生命周期发生变化,致使实例不被释放。

    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    
    @interface YYActiveObj : NSObject
    
    @property(class,nonatomic,weak) UIView *viewActive;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "YYActiveObj.h"
    
    static UIView *_viewActive;
    
    @implementation YYActiveObj
    
    + (void)setViewActive:(UIView *)viewActive{
        _viewActive = viewActive;
    }
    
    + (UIView *)viewActive{
        return _viewActive;
    }
    
    @end
    
    复制代码

    如上,此时声明的weak修饰其实并不起作用,原因就在于static

    2019年4月2日


    【iOS13 DarkMode适配】

    如果你也在做该适配的话,那么很可能遇到以下的问题。 尝试了很多次,最大的问题竟出在UIColor。 不过出现的问题,疑似和YY内部在Runloop 即将休眠时进行绘制任务具有很大的相关性。具体原因还不能确定,等以后再深究一下。

    这里先看下系统UILabel的暗夜适配找寻一下灵感

    UILabel DarkMode.png 可以看到,UILabel的绘制是调用 drawTextInRext,而翻看YY能看到其使用的 CTRunDraw()。由于一开始对UIDynamicProviderColor有误解,也尝试过解析其中打包的颜色,来通过查看CTRun的属性集来判断当前是否正确渲染。

    ....然而,在YYLabel应用上述方案时可能正常,但YYTeView却出现了其它的问题。 ....排查中发现,某些时候UITraitCollection.currentTraitCollection解析出的颜色,和对应的状态不符。 ....最终发现,colorWithDynamicProvider中回调的状态可能出现和当前系统状态不一致的情况,也就是说这个回调有点不那么可信了... 误我青春

    YYLabel.m 添加如下代码

    #pragma mark - DarkMode Adapater
    
    #ifdef __IPHONE_13_0
    - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
        [super traitCollectionDidChange:previousTraitCollection];
        
        if (@available(iOS 13.0, *)) {
            if([UITraitCollection.currentTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]){
                [self.layer setNeedsDisplay];
            }
        } else {
            // Fallback on earlier versions
        }
    }
    #endif
    复制代码

    YYTextView.m 添加如下代码

    #pragma mark - Dark mode Adapter
    
    #ifdef __IPHONE_13_0
    - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
        [super traitCollectionDidChange:previousTraitCollection];
        
        if (@available(iOS 13.0, *)) {
            if([UITraitCollection.currentTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]){
                [self _commitUpdate];
            }
        } else {
            // Fallback on earlier versions
        }
    }
    #endif
    复制代码

    额外要做的事情

    • NSAttributedString+YYText.m 去除部分CGColor 调用
    - (void)setColor:(UIColor *)color {
        [self setColor:color range:NSMakeRange(0, self.length)];
    }
    
    - (void)setStrokeColor:(UIColor *)strokeColor {
        [self setStrokeColor:strokeColor range:NSMakeRange(0, self.length)];
    }
    
    - (void)setStrikethroughColor:(UIColor *)strikethroughColor {
        [self setStrikethroughColor:strikethroughColor range:NSMakeRange(0, self.length)];
    }
    
    - (void)setUnderlineColor:(UIColor *)underlineColor {
        [self setUnderlineColor:underlineColor range:NSMakeRange(0, self.length)];
    }
    
    复制代码
    • UIColor
    /// 坑爹的方法 有时候是不灵的
            UIColor *dynamicColor =  [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * provider) {
            if ([provider userInterfaceStyle] == UIUserInterfaceStyleLight) {
                return lightColor;
            }
            else {
                return darkColor;
            }
            }];
    
    ///建议 从顶层获取当前系统的,暗黑模式状态。
            UIColor *dynamicColor =  [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * provider) {
                /// keyWindow 读取当前状态
                if(keyWindow.isDark){
                    return darkColor;
                }
                return lightColor;
            }];
    复制代码

    2019年6月21日


    【iOS13 下出现scrollsToTop失效】iOS 13.3 已由苹果修复

    在iOS 13.0 版本上,但凡创建YYTextView实例,即会使全局的 scrollsToTop 回顶功能失效。

    排查最终结果为 YYTextEffectWindow 需做如下变更:

    + (instancetype)sharedWindow {
        static YYTextEffectWindow *one = nil;
        if (one == nil) {
            // iOS 9 compatible
            NSString *mode = [NSRunLoop currentRunLoop].currentMode;
            if (mode.length == 27 &&
                [mode hasPrefix:@"UI"] &&
                [mode hasSuffix:@"InitializationRunLoopMode"]) {
                return nil;
            }
        }
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            if (![UIApplication isAppExtension]) {
                one = [self new];
                one.frame = (CGRect){.size = kScreenSize};
                one.userInteractionEnabled = NO;
                one.windowLevel = UIWindowLevelStatusBar + 1;
                //此处处理 iOS 13版本出现的问题
                if (@available(iOS 13.0, *)) {
                    one.hidden = YES;
                }else{
                    one.hidden = NO;
                }
                
                // for iOS 9:
                one.opaque = NO;
                one.backgroundColor = [UIColor clearColor];
                one.layer.backgroundColor = [UIColor clearColor].CGColor;
            }
        });
        return one;
    }
    复制代码

    2019年6月21日


    【iOS13 下双光标问题】

    问题产生原因:

    1. 机制更改
    2. 之前所做的解决搜狗输入法、系统键盘单词中断问题所做的修改存在问题。
    • 机制更改 在真机上尝试了很多次,确认如下: iOS13下,只要遵循了UITextInput相关协议,在进行文本选择操作时系统会自动派生出UITextSelectionView系列组件,显然和YY自有的YYTextSelectionView冲突了。(此处隐藏一颗彩蛋)

    • 之前对YYTextView所做的代码变更,武断的注释掉了如下方法:

    if (notify) [_inputDelegate selectionWillChange:self];
    if (notify) [_inputDelegate selectionDidChange:self];
    复制代码

    会造成内部对文本选择相关的数据错乱,当然这种影响目前只在iOS13下能够看到表现。

    【解决方案】

    • 隐藏掉,系统派生出来的UITextSelectionView
    /// YYTextView.m 
    /// 增加标记位
    struct {
           .....
           unsigned int trackingDeleteBackward : 1;  ///< track deleteBackward operation
           unsigned int trackingTouchBegan : 1;  /// < track touchesBegan event
    } _state;
    
    
    /// 方法重写
    - (void)addSubview:(UIView *)view{
        [super addSubview:view];
    
        Class Cls_selectionView = NSClassFromString(@"UITextSelectionView");
        Class Cls_selectionGrabberDot = NSClassFromString(@"UISelectionGrabberDot");
        if ([view isKindOfClass:[Cls_selectionGrabberDot class]]) {
            view.layer.contents = [UIView new];
        }
        
        if ([view isKindOfClass:[Cls_selectionView class]]) {
            view.hidden = YES;
        }
    }
    
    /// 方法修改
    /// Replace the range with the text, and change the `_selectTextRange`.
    /// The caller should make sure the `range` and `text` are valid before call this method.
    - (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify{
        if (_isExcludeNeed) {
            notify = NO;
        }
    
        if (NSEqualRanges(range.asRange, _selectedTextRange.asRange)) {
            //这里的代理方法需要注释掉 【废止】
            //if (notify) [_inputDelegate selectionWillChange:self];
            /// iOS13 下,双光标问题 便是由此而生。
            if (_state.trackingDeleteBackward)[_inputDelegate selectionWillChange:self];
            NSRange newRange = NSMakeRange(0, 0);
            newRange.location = _selectedTextRange.start.offset + text.length;
            _selectedTextRange = [YYTextRange rangeWithRange:newRange];
            //if (notify) [_inputDelegate selectionDidChange:self];
            /// iOS13 下,双光标问题 便是由此而生。
            if (_state.trackingDeleteBackward) [_inputDelegate selectionDidChange:self];
            ///恢复标记
            _state.trackingDeleteBackward = NO;
        } else {
        .....
        .....
    }
    
    - (void)deleteBackward {
        //标识出删除动作:用于解决双光标相关问题
        _state.trackingDeleteBackward = YES;
        
        [self _updateIfNeeded];
        .....
        .....
    }
    
    - (void)_updateSelectionView {
        _selectionView.frame = _containerView.frame;
        _selectionView.caretBlinks = NO;
        _selectionView.caretVisible = NO;
        _selectionView.selectionRects = nil;
      .....
      .....
        
        if (@available(iOS 13.0, *)) {
            if (_state.trackingTouchBegan) [_inputDelegate selectionWillChange:self];
            [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
            if (_state.trackingTouchBegan) [_inputDelegate selectionDidChange:self];
        }else{
             [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
        }
    
        if (containsDot) {
            [self _startSelectionDotFixTimer];
        } else {
            [self _endSelectionDotFixTimer];
      .....
      .....
    }
    复制代码

    2019年8月26日


    【YYTextView 预输入文本状态下选中功能异常】

    更改前.gif 更改后.gif

    【解决方案】

    - (void)_showMenu {
        //过滤预输入状态
        if (_markedTextRange != nil) {
            return;
        }
        
        CGRect rect;
        if (_selectionView.caretVisible) {
            rect = _selectionView.caretView.frame;
         ...
         ...
    复制代码

    2019年9月5日


    【iOS13新增编辑手势】- 暂时禁用

    编辑手势 描述借鉴 复制:三指捏合 剪切:两次三指捏合 粘贴:三指松开 撤销:三指向左划动(或三指双击) 重做:三指向右划动 快捷菜单:三指单击

    #ifdef __IPHONE_13_0
    - (UIEditingInteractionConfiguration)editingInteractionConfiguration{
        return UIEditingInteractionConfigurationNone;
    }
    #endif
    复制代码

    2019年9月24日


    【iOS13下文本第一次选中时 蓝点问题】

    其实之前说的彩蛋,就是关于系统派生出来的selection组件。之所以对其隐藏的方式使用hidden 、CGSizeZero 而且区别使用的原因就是,这些组件其实会躲避。

    #pragma mark - override
    - (void)addSubview:(UIView *)view{
        
        //解决蓝点问题
        Class Cls_selectionGrabberDot = NSClassFromString(@"UISelectionGrabberDot");
        if ([view isKindOfClass:[Cls_selectionGrabberDot class]]) {
            view.backgroundColor = [UIColor clearColor];
            view.tintColor = [UIColor clearColor];
            view.size = CGSizeZero;
        }
        
        //获取UITextSelectionView
        //解决双光标问题
        Class Cls_selectionView = NSClassFromString(@"UITextSelectionView");
    
        if ([view isKindOfClass:[Cls_selectionView class]]) {
            view.backgroundColor = [UIColor clearColor];
            view.tintColor = [UIColor clearColor];
            view.hidden = YES;
        }
        
        [super addSubview:view];
    }
    复制代码

    2019年9月29日


    【YYTextView 赋空值】-待处理

    YYTextView *txtView = [YYTextView new];
    txtView.text = nil;  
    
    //读取Size
    CGSize size = txtView.self.textLayout.textBoundingSize;
    //存在的问题(高度不为0)  
    size != CGSizeZero
    复制代码

    【iOS 14下输入法联想词功能失效 】

    需要还原 hastext 方法

    YYTextView.m 
    
    #pragma mark - @protocol UIKeyInput
    - (BOOL)hasText {
        //建议 
        return _innerText.length > 0;
        
        /* 
        //不建议
        if(@available(iOS 14.0, *)) {
            return _innerText.length > 0;
        }
        return NO;
       */
    }
    复制代码

    2020年6月30日

     
    作者:Saylor_Lone
    链接:https://juejin.cn/post/6844903958436118541
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    2.4 使用vue-cli创建项目/项目打包/发布
    2.3 vue-cli脚手架工具/nodejs
    2.2 vue的devtools、eslint检测问题
    2. es6扩展运算符
    文件json
    函数
    函数不固定参数
    监控日志,加入黑名单
    非空即真
    随机生成手机号,存入文件
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/15667566.html
Copyright © 2011-2022 走看看