YYKit-持續補丁

1.索引

[[IQKeyboardManager sharedManager] registerTextFieldViewClass:[YYTextView class]
                                  didBeginEditingNotificationName:YYTextViewTextDidBeginEditingNotification
                                    didEndEditingNotificationName:YYTextViewTextDidEndEditingNotification];
    //默認關閉IQKeyboard 在須要的界面啓用
    [IQKeyboardManager sharedManager].enable = NO;
    [[IQKeyboardManager sharedManager] setEnableAutoToolbar:NO];
複製代碼
  • 佈局問題ide

  • 若是配合AutoLayout 計算Size後務必更新約束工具

  • 目前可以保證沒有問題的方法 SizeThatFits 【注意】若是文本以大段空白(\n)做爲結尾,可能致使計算出來的高度有誤,因此實際中最好進行空白段落處理。oop

//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 內存泄露問題】

檢測工具 : 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.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日

相關文章
相關標籤/搜索