فهرست منبع

Autolayout support, Interface Builder support, bugfix.

ibireme 9 سال پیش
والد
کامیت
e8c86c1b09
8فایلهای تغییر یافته به همراه543 افزوده شده و 68 حذف شده
  1. 0 1
      README.md
  2. 2 2
      YYText/Component/YYTextLayout.h
  3. 2 2
      YYText/Component/YYTextLayout.m
  4. 0 2
      YYText/Component/YYTextSelectionView.m
  5. 80 17
      YYText/YYLabel.h
  6. 237 23
      YYText/YYLabel.m
  7. 71 15
      YYText/YYTextView.h
  8. 151 6
      YYText/YYTextView.m

+ 0 - 1
README.md

@@ -1061,7 +1061,6 @@ YYText 和 TextKit 架构对比
 已知问题
 ==============
 * YYText 并不能支持所有 CoreText/TextKit 的属性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 YYText 中基本都有对应属性作为替代。详情见上方表格。
-* 当 YYLabel 的高度小于第一行文本的内容高度时,文本不会显示。
 * YYTextView 目前还未实现局部刷新,所以在输入和编辑大量的文本(比如超过5K个汉字、10K个英文字符)时会出现较明显的卡顿现象。
 * 竖排版时,添加 exclusionPaths 在少数情况下可能会导致文本显示空白。
 * 当添加了非矩形的 textContainerPath 时,并且有嵌入大于文本排版方向宽度的 RunDelegate 时,RunDelegate 后面的文字会无法显示。这是 CoreText 的 Bug(也可能是 Feature)。

+ 2 - 2
YYText/Component/YYTextLayout.h

@@ -223,8 +223,8 @@ extern const CGSize YYTextContainerMaxSize;
 @property (nonatomic, readonly) NSSet *attachmentContentsSet;  ///< Set of Attachment (UIImage/UIView/CALayer)
 @property (nonatomic, readonly) NSUInteger rowCount;           ///< Number of rows
 @property (nonatomic, readonly) NSRange visibleRange;          ///< Visible text range
-@property (nonatomic, readonly) CGRect textBoundingRect;       ///< Text bounding rect (only contains text glyph)
-@property (nonatomic, readonly) CGSize textBoundingSize;       ///< Text bounding size (encompasses all text and insets)
+@property (nonatomic, readonly) CGRect textBoundingRect;       ///< Bounding rect (glyphs)
+@property (nonatomic, readonly) CGSize textBoundingSize;       ///< Bounding size (glyphs and insets, ceil to pixel)
 @property (nonatomic, readonly) BOOL containsHighlight;        ///< Has highlight attribute
 @property (nonatomic, readonly) BOOL needDrawBlockBorder;      ///< Has block border attribute
 @property (nonatomic, readonly) BOOL needDrawBackgroundBorder; ///< Has background border attribute

+ 2 - 2
YYText/Component/YYTextLayout.m

@@ -619,8 +619,8 @@ dispatch_semaphore_signal(_lock);
         size.height += rect.origin.y;
         if (size.width < 0) size.width = 0;
         if (size.height < 0) size.height = 0;
-        size.width = ceil(size.width);
-        size.height = ceil(size.height);
+        size.width = YYTextCGFloatPixelCeil(size.width);
+        size.height = YYTextCGFloatPixelCeil(size.height);
         textBoundingSize = size;
     }
     

+ 0 - 2
YYText/Component/YYTextSelectionView.m

@@ -90,8 +90,6 @@
 - (void)layoutSubviews {
     [super layoutSubviews];
     [self setDotDirection:_dotDirection];
-    CGFloat minWidth = MIN(self.bounds.size.width, self.bounds.size.height);
-    self.layer.cornerRadius = minWidth / 2;
 }
 
 - (CGRect)touchRect {

+ 80 - 17
YYText/YYLabel.h

@@ -21,6 +21,8 @@
 #import "YYTextAttribute.h"
 #endif
 
+#if !TARGET_INTERFACE_BUILDER
+
 /**
  The YYLabel class implements a read-only text view.
  
@@ -55,47 +57,47 @@
  Set a new value to this property also causes the new font to be applied to the entire `attributedText`.
  Get the value returns the font at the head of `attributedText`.
  */
-@property (null_resettable, nonatomic, retain) UIFont *font;
+@property (null_resettable, nonatomic, strong) UIFont *font;
 
 /**
  The color of the text. Default is black.
  Set a new value to this property also causes the new color to be applied to the entire `attributedText`.
  Get the value returns the color at the head of `attributedText`.
  */
-@property (null_resettable, nonatomic, retain) UIColor *textColor;
+@property (null_resettable, nonatomic, strong) UIColor *textColor;
 
 /**
  The shadow color of the text. Default is nil.
  Set a new value to this property also causes the shadow color to be applied to the entire `attributedText`.
  Get the value returns the shadow color at the head of `attributedText`.
  */
-@property (nullable, nonatomic, retain) UIColor *shadowColor;
+@property (nullable, nonatomic, strong) UIColor *shadowColor;
 
 /**
  The shadow offset of the text. Default is CGSizeZero.
  Set a new value to this property also causes the shadow offset to be applied to the entire `attributedText`.
  Get the value returns the shadow offset at the head of `attributedText`.
  */
-@property (nonatomic, assign) CGSize shadowOffset;
+@property (nonatomic) CGSize shadowOffset;
 
 /**
  The shadow blur of the text. Default is 0.
  Set a new value to this property also causes the shadow blur to be applied to the entire `attributedText`.
  Get the value returns the shadow blur at the head of `attributedText`.
  */
-@property (nonatomic, assign) CGFloat shadowBlurRadius;
+@property (nonatomic) CGFloat shadowBlurRadius;
 
 /**
  The technique to use for aligning the text. Default is NSLeftTextAlignment.
  Set a new value to this property also causes the new alignment to be applied to the entire `attributedText`.
  Get the value returns the alignment at the head of `attributedText`.
  */
-@property (nonatomic, assign) NSTextAlignment textAlignment;
+@property (nonatomic) NSTextAlignment textAlignment;
 
 /**
  The text vertical aligmnent in container. Default is YYTextVerticalAlignmentCenter.
  */
-@property (nonatomic, assign) YYTextVerticalAlignment textVerticalAlignment;
+@property (nonatomic) YYTextVerticalAlignment textVerticalAlignment;
 
 /**
  The styled text displayed by the label.
@@ -111,7 +113,7 @@
  The technique to use for wrapping and truncating the label's text.
  Default is NSLineBreakByTruncatingTail.
  */
-@property (nonatomic, assign) NSLineBreakMode lineBreakMode;
+@property (nonatomic) NSLineBreakMode lineBreakMode;
 
 /**
  The truncation token string used when text is truncated. Default is nil.
@@ -123,7 +125,7 @@
  The maximum number of lines to use for rendering text. Default value is 1.
  0 means no limit.
  */
-@property (nonatomic, assign) NSUInteger numberOfLines;
+@property (nonatomic) NSUInteger numberOfLines;
 
 /**
  When `text` or `attributedText` is changed, the parser will be called to modify the text.
@@ -162,13 +164,13 @@
  The inset of the text container's layout area within the text view's content area.
  Default value is UIEdgeInsetsZero.
  */
-@property (nonatomic, assign) UIEdgeInsets textContainerInset;
+@property (nonatomic) UIEdgeInsets textContainerInset;
 
 /**
  Whether the receiver's layout orientation is vertical form. Default is NO.
  It may used to display CJK text.
  */
-@property (nonatomic, assign, getter=isVerticalForm) BOOL verticalForm;
+@property (nonatomic, getter=isVerticalForm) BOOL verticalForm;
 
 /**
  The text line position modifier used to modify the lines' position in layout.
@@ -181,7 +183,24 @@
  The debug option to display CoreText layout result.
  The default value is [YYTextDebugOption sharedDebugOption].
  */
-@property (nonnull, nonatomic, copy) YYTextDebugOption *debugOption;
+@property (nullable, nonatomic, copy) YYTextDebugOption *debugOption;
+
+
+#pragma mark - Getting the Layout Constraints
+///=============================================================================
+/// @name Getting the Layout Constraints
+///=============================================================================
+
+/**
+ The preferred maximum width (in points) for a multiline label.
+ 
+ @discussion This property affects the size of the label when layout constraints 
+     are applied to it. During layout, if the text extends beyond the width 
+     specified by this property, the additional text is flowed to one or more new 
+     lines, thereby increasing the height of the label. If the text is vertical 
+     form, this value will match to text height.
+ */
+@property (nonatomic) CGFloat preferredMaxLayoutWidth;
 
 
 #pragma mark - Interacting with Text Data
@@ -213,7 +232,7 @@
  
  The default value is `NO`.
  */
-@property (nonatomic, assign) BOOL displaysAsynchronously;
+@property (nonatomic) BOOL displaysAsynchronously;
 
 /**
  If the value is YES, and the layer is rendered asynchronously, then it will
@@ -227,7 +246,7 @@
  for display. You may manually clear the content by set the layer.contents to nil 
  after you update the label's properties, or you can just set this property to YES.
  */
-@property (nonatomic, assign) BOOL clearContentsBeforeAsynchronouslyDisplay;
+@property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay;
 
 /**
  If the value is YES, and the layer is rendered asynchronously, then it will add 
@@ -235,7 +254,7 @@
  
  The default value is `YES`.
  */
-@property (nonatomic, assign) BOOL fadeOnAsynchronouslyDisplay;
+@property (nonatomic) BOOL fadeOnAsynchronouslyDisplay;
 
 /**
  If the value is YES, then it will add a fade animation on layer when some range
@@ -243,7 +262,7 @@
  
  The default value is `YES`.
  */
-@property (nonatomic, assign) BOOL fadeOnHighlight;
+@property (nonatomic) BOOL fadeOnHighlight;
 
 /**
  Ignore common properties (such as text, font, textColor, attributedText...) and
@@ -254,7 +273,7 @@
  @discussion If you control the label content only through "textLayout", then
  you may set this value to YES for higher performance.
  */
-@property (nonatomic, assign) BOOL ignoreCommonProperties;
+@property (nonatomic) BOOL ignoreCommonProperties;
 
 /*
  Tips:
@@ -297,3 +316,47 @@
  */
 
 @end
+
+
+#else // TARGET_INTERFACE_BUILDER
+IB_DESIGNABLE
+@interface YYLabel : UIView <NSCoding>
+@property (nullable, nonatomic, copy) IBInspectable NSString *text;
+@property (null_resettable, nonatomic, strong) IBInspectable UIColor *textColor;
+@property (nullable, nonatomic, strong) IBInspectable NSString *fontName_;
+@property (nonatomic) IBInspectable CGFloat fontSize_;
+@property (nonatomic) IBInspectable BOOL fontIsBold_;
+@property (nonatomic) IBInspectable NSUInteger numberOfLines;
+@property (nonatomic) IBInspectable NSInteger lineBreakMode;
+@property (nonatomic) IBInspectable CGFloat preferredMaxLayoutWidth;
+@property (nonatomic, getter=isVerticalForm) IBInspectable BOOL verticalForm;
+@property (nonatomic) IBInspectable NSInteger textAlignment;
+@property (nonatomic) IBInspectable NSInteger textVerticalAlignment;
+@property (nullable, nonatomic, strong) IBInspectable UIColor *shadowColor;
+@property (nonatomic) IBInspectable CGPoint shadowOffset;
+@property (nonatomic) IBInspectable CGFloat shadowBlurRadius;
+@property (nullable, nonatomic, copy) IBInspectable NSAttributedString *attributedText;
+@property (nonatomic) IBInspectable CGFloat insetTop_;
+@property (nonatomic) IBInspectable CGFloat insetBottom_;
+@property (nonatomic) IBInspectable CGFloat insetLeft_;
+@property (nonatomic) IBInspectable CGFloat insetRight_;
+@property (nonatomic) IBInspectable BOOL debugEnabled_;
+
+@property (null_resettable, nonatomic, strong) UIFont *font;
+@property (nullable, nonatomic, copy) NSAttributedString *truncationToken;
+@property (nullable, nonatomic, strong) id<YYTextParser> textParser;
+@property (nullable, nonatomic, strong) YYTextLayout *textLayout;
+@property (nullable, nonatomic, copy) UIBezierPath *textContainerPath;
+@property (nullable, nonatomic, copy) NSArray *exclusionPaths;
+@property (nonatomic) UIEdgeInsets textContainerInset;
+@property (nullable, nonatomic, copy) id<YYTextLinePositionModifier> linePositionModifier;
+@property (nonnull, nonatomic, copy) YYTextDebugOption *debugOption;
+@property (nullable, nonatomic, copy) YYTextAction highlightTapAction;
+@property (nullable, nonatomic, copy) YYTextAction highlightLongPressAction;
+@property (nonatomic) BOOL displaysAsynchronously;
+@property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay;
+@property (nonatomic) BOOL fadeOnAsynchronouslyDisplay;
+@property (nonatomic) BOOL fadeOnHighlight;
+@property (nonatomic) BOOL ignoreCommonProperties;
+@end
+#endif // !TARGET_INTERFACE_BUILDER

+ 237 - 23
YYText/YYLabel.m

@@ -50,6 +50,9 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange`
     YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed
     
+    YYTextLayout *_shrinkInnerLayout;
+    YYTextLayout *_shrinkHighlightLayout;
+    
     NSTimer *_longPressTimer;
     CGPoint _touchBeganPoint;
     
@@ -81,6 +84,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
 
 - (void)_updateLayout {
     _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:_innerText];
+    _shrinkInnerLayout = [YYLabel _shrinkLayoutWithLayout:_innerLayout];
 }
 
 - (void)_setLayoutNeedUpdate {
@@ -97,6 +101,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     if (!_innerLayout) return;
     YYTextLayout *layout = _innerLayout;
     _innerLayout = nil;
+    _shrinkInnerLayout = nil;
     dispatch_async(YYLabelGetReleaseQueue(), ^{
         NSAttributedString *text = [layout text]; // capture to block and release in background
         if (layout.attachments.count) {
@@ -107,6 +112,31 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     });
 }
 
+- (YYTextLayout *)_innerLayout {
+    return _shrinkInnerLayout ? _shrinkInnerLayout : _innerLayout;
+}
+
+- (YYTextLayout *)_highlightLayout {
+    return _shrinkHighlightLayout ? _shrinkHighlightLayout : _highlightLayout;
+}
+
++ (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;
+    }
+}
+
 - (void)_startLongPressTimer {
     [_longPressTimer invalidate];
     _longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration
@@ -130,7 +160,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
             YYTextPosition *start = [YYTextPosition positionWithOffset:_highlightRange.location];
             YYTextPosition *end = [YYTextPosition positionWithOffset:_highlightRange.location + _highlightRange.length affinity:YYTextAffinityBackward];
             YYTextRange *range = [YYTextRange rangeWithStart:start end:end];
-            CGRect rect = [_innerLayout rectForRange:range];
+            CGRect rect = [self._innerLayout rectForRange:range];
             rect = [self _convertRectFromLayout:rect];
             longPressAction(self, _innerText, _highlightRange, rect);
             [self _removeHighlightAnimated:YES];
@@ -140,9 +170,9 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
 }
 
 - (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range {
-    if (!_innerLayout.containsHighlight) return nil;
+    if (!self._innerLayout.containsHighlight) return nil;
     point = [self _convertPointToLayout:point];
-    YYTextRange *textRange = [_innerLayout textRangeAtPoint:point];
+    YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
     if (!textRange) return nil;
     
     NSUInteger startIndex = textRange.start.offset;
@@ -171,6 +201,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
             [hiText yy_setAttribute:key value:value range:_highlightRange];
         }];
         _highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText];
+        _shrinkHighlightLayout = [YYLabel _shrinkLayoutWithLayout:_highlightLayout];
         if (!_highlightLayout) _highlight = nil;
     }
     
@@ -193,6 +224,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     [self _hideHighlightAnimated:animated];
     _highlight = nil;
     _highlightLayout = nil;
+    _shrinkHighlightLayout = nil;
 }
 
 - (void)_endTouch {
@@ -202,11 +234,11 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
 }
 
 - (CGPoint)_convertPointToLayout:(CGPoint)point {
-    CGSize boundingSize = _innerLayout.textBoundingSize;
-    if (_innerLayout.container.isVerticalForm) {
-        CGFloat w = _innerLayout.textBoundingSize.width;
+    CGSize boundingSize = self._innerLayout.textBoundingSize;
+    if (self._innerLayout.container.isVerticalForm) {
+        CGFloat w = self._innerLayout.textBoundingSize.width;
         if (w < self.bounds.size.width) w = self.bounds.size.width;
-        point.x += _innerLayout.container.size.width - w;
+        point.x += self._innerLayout.container.size.width - w;
         if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
             point.x += (self.bounds.size.width - boundingSize.width) * 0.5;
         } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
@@ -224,11 +256,11 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
 }
 
 - (CGPoint)_convertPointFromLayout:(CGPoint)point {
-    CGSize boundingSize = _innerLayout.textBoundingSize;
-    if (_innerLayout.container.isVerticalForm) {
-        CGFloat w = _innerLayout.textBoundingSize.width;
+    CGSize boundingSize = self._innerLayout.textBoundingSize;
+    if (self._innerLayout.container.isVerticalForm) {
+        CGFloat w = self._innerLayout.textBoundingSize.width;
         if (w < self.bounds.size.width) w = self.bounds.size.width;
-        point.x -= _innerLayout.container.size.width - w;
+        point.x -= self._innerLayout.container.size.width - w;
         if (boundingSize.width < self.bounds.size.width) {
             if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
                 point.x -= (self.bounds.size.width - boundingSize.width) * 0.5;
@@ -267,7 +299,11 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     if (!_shadowColor || _shadowBlurRadius < 0) return nil;
     NSShadow *shadow = [NSShadow new];
     shadow.shadowColor = _shadowColor;
+#if !TARGET_INTERFACE_BUILDER
     shadow.shadowOffset = _shadowOffset;
+#else
+    shadow.shadowOffset = CGSizeMake(_shadowOffset.x, _shadowOffset.y);
+#endif
     shadow.shadowBlurRadius = _shadowBlurRadius;
     return shadow;
 }
@@ -301,7 +337,12 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     _lineBreakMode = _innerText.yy_lineBreakMode;
     NSShadow *shadow = _innerText.yy_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];
@@ -450,6 +491,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     
     _highlight = [self _getHighlightAtPoint:point range:&_highlightRange];
     _highlightLayout = nil;
+    _shrinkHighlightLayout = nil;
     
     if (_highlight) {
         _touchBeganPoint = point;
@@ -514,7 +556,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
                 YYTextPosition *start = [YYTextPosition positionWithOffset:_highlightRange.location];
                 YYTextPosition *end = [YYTextPosition positionWithOffset:_highlightRange.location + _highlightRange.length affinity:YYTextAffinityBackward];
                 YYTextRange *range = [YYTextRange rangeWithStart:start end:end];
-                CGRect rect = [_innerLayout rectForRange:range];
+                CGRect rect = [self._innerLayout rectForRange:range];
                 rect = [self _convertRectFromLayout:rect];
                 tapAction(self, _innerText, _highlightRange, rect);
             }
@@ -568,6 +610,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -584,6 +627,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -614,6 +658,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     }
 }
 
+#if !TARGET_INTERFACE_BUILDER
 - (void)setShadowOffset:(CGSize)shadowOffset {
     if (CGSizeEqualToSize(_shadowOffset, shadowOffset)) return;
     _shadowOffset = shadowOffset;
@@ -625,6 +670,19 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         [self _setLayoutNeedUpdate];
     }
 }
+#else
+- (void)setShadowOffset:(CGPoint)shadowOffset {
+    if (CGPointEqualToPoint(_shadowOffset, shadowOffset)) return;
+    _shadowOffset = shadowOffset;
+    _innerText.yy_shadow = [self _shadowFromProperties];
+    if (_innerText.length && !_ignoreCommonProperties) {
+        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
+            [self _clearContents];
+        }
+        [self _setLayoutNeedUpdate];
+    }
+}
+#endif
 
 - (void)setShadowBlurRadius:(CGFloat)shadowBlurRadius {
     if (_shadowBlurRadius == shadowBlurRadius) return;
@@ -648,6 +706,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -683,6 +742,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -695,6 +755,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -708,6 +769,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -721,6 +783,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -751,6 +814,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         [self _updateOuterTextProperties];
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -768,6 +832,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -781,6 +846,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -794,6 +860,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -807,6 +874,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -820,6 +888,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         }
         [self _setLayoutNeedUpdate];
         [self _endTouch];
+        [self invalidateIntrinsicContentSize];
     }
 }
 
@@ -834,12 +903,14 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
             }
             [self _setLayoutNeedUpdate];
             [self _endTouch];
+            [self invalidateIntrinsicContentSize];
         }
     }
 }
 
 - (void)setTextLayout:(YYTextLayout *)textLayout {
     _innerLayout = textLayout;
+    _shrinkInnerLayout = nil;
     
     if (_ignoreCommonProperties) {
         _innerText = (NSMutableAttributedString *)textLayout.text;
@@ -866,6 +937,7 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     _state.layoutNeedUpdate = NO;
     [self _setLayoutNeedRedraw];
     [self _endTouch];
+    [self invalidateIntrinsicContentSize];
 }
 
 - (YYTextLayout *)textLayout {
@@ -878,6 +950,41 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     ((YYTextAsyncLayer *)self.layer).displaysAsynchronously = displaysAsynchronously;
 }
 
+#pragma mark - AutoLayout
+
+- (void)setPreferredMaxLayoutWidth:(CGFloat)preferredMaxLayoutWidth {
+    if (_preferredMaxLayoutWidth == preferredMaxLayoutWidth) return;
+    _preferredMaxLayoutWidth = preferredMaxLayoutWidth;
+    [self invalidateIntrinsicContentSize];
+}
+
+- (CGSize)intrinsicContentSize {
+    if (_preferredMaxLayoutWidth == 0) {
+        YYTextContainer *container = [_innerContainer copy];
+        container.size = YYTextContainerMaxSize;
+        
+        YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+        return layout.textBoundingSize;
+    }
+    
+    CGSize containerSize = _innerContainer.size;
+    if (!_verticalForm) {
+        containerSize.height = YYTextContainerMaxSize.height;
+        containerSize.width = _preferredMaxLayoutWidth;
+        if (containerSize.width == 0) containerSize.width = self.bounds.size.width;
+    } else {
+        containerSize.width = YYTextContainerMaxSize.width;
+        containerSize.height = _preferredMaxLayoutWidth;
+        if (containerSize.height == 0) containerSize.height = self.bounds.size.height;
+    }
+    
+    YYTextContainer *container = [_innerContainer copy];
+    container.size = containerSize;
+    
+    YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+    return layout.textBoundingSize;
+}
+
 #pragma mark - YYTextDebugTarget
 
 - (void)setDebugOption:(YYTextDebugOption *)debugOption {
@@ -902,7 +1009,8 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
     NSMutableArray *attachmentLayers = _attachmentLayers;
     BOOL layoutNeedUpdate = _state.layoutNeedUpdate;
     BOOL fadeForAsync = _displaysAsynchronously && _fadeOnAsynchronouslyDisplay;
-    __block YYTextLayout *layout = (_state.showingHighlight && _highlightLayout) ? _highlightLayout : _innerLayout;
+    __block YYTextLayout *layout = (_state.showingHighlight && _highlightLayout) ? self._highlightLayout : self._innerLayout;
+    __block YYTextLayout *shrinkLayout = nil;
     __block BOOL layoutUpdated = NO;
     if (layoutNeedUpdate) {
         text = text.copy;
@@ -939,35 +1047,42 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         if (isCancelled()) return;
         if (text.length == 0) return;
         
+        YYTextLayout *drawLayout = layout;
         if (layoutNeedUpdate) {
             layout = [YYTextLayout layoutWithContainer:container text:text];
+            shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout];
             if (isCancelled()) return;
             layoutUpdated = YES;
+            drawLayout = shrinkLayout ? shrinkLayout : layout;
         }
         
-        CGSize boundingSize = layout.textBoundingSize;
+        CGSize boundingSize = drawLayout.textBoundingSize;
         CGPoint point = CGPointZero;
         if (verticalAlignment == YYTextVerticalAlignmentCenter) {
-            if (layout.container.isVerticalForm) {
+            if (drawLayout.container.isVerticalForm) {
                 point.x = -(size.width - boundingSize.width) * 0.5;
             } else {
                 point.y = (size.height - boundingSize.height) * 0.5;
             }
         } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
-            if (layout.container.isVerticalForm) {
+            if (drawLayout.container.isVerticalForm) {
                 point.x = -(size.width - boundingSize.width);
             } else {
                 point.y = (size.height - boundingSize.height);
             }
         }
         point = YYTextCGPointPixelRound(point);
-        [layout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
+        [drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
     };
 
     task.didDisplay = ^(CALayer *layer, BOOL finished) {
+        YYTextLayout *drawLayout = layout;
+        if (layoutUpdated && shrinkLayout) {
+            drawLayout = shrinkLayout;
+        }
         if (!finished) {
             // If the display task is cancelled, we should clear the attachments.
-            for (YYTextAttachment *a in layout.attachments) {
+            for (YYTextAttachment *a in drawLayout.attachments) {
                 if ([a.content isKindOfClass:[UIView class]]) {
                     if (((UIView *)a.content).superview == layer.delegate) {
                         [((UIView *)a.content) removeFromSuperview];
@@ -986,28 +1101,29 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
         if (!view) return;
         if (view->_state.layoutNeedUpdate && layoutUpdated) {
             view->_innerLayout = layout;
+            view->_shrinkInnerLayout = shrinkLayout;
             view->_state.layoutNeedUpdate = NO;
         }
         
         CGSize size = layer.bounds.size;
-        CGSize boundingSize = layout.textBoundingSize;
+        CGSize boundingSize = drawLayout.textBoundingSize;
         CGPoint point = CGPointZero;
         if (verticalAlignment == YYTextVerticalAlignmentCenter) {
-            if (layout.container.isVerticalForm) {
+            if (drawLayout.container.isVerticalForm) {
                 point.x = -(size.width - boundingSize.width) * 0.5;
             } else {
                 point.y = (size.height - boundingSize.height) * 0.5;
             }
         } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
-            if (layout.container.isVerticalForm) {
+            if (drawLayout.container.isVerticalForm) {
                 point.x = -(size.width - boundingSize.width);
             } else {
                 point.y = (size.height - boundingSize.height);
             }
         }
         point = YYTextCGPointPixelRound(point);
-        [layout drawInContext:nil size:CGSizeZero point:point view:view layer:layer debug:nil cancel:NULL];
-        for (YYTextAttachment *a in layout.attachments) {
+        [drawLayout drawInContext:nil size:size point:point view:view layer:layer debug:nil cancel:NULL];
+        for (YYTextAttachment *a in drawLayout.attachments) {
             if ([a.content isKindOfClass:[UIView class]]) [attachmentViews addObject:a.content];
             else if ([a.content isKindOfClass:[CALayer class]]) [attachmentLayers addObject:a.content];
         }
@@ -1031,3 +1147,101 @@ static dispatch_queue_t YYLabelGetReleaseQueue() {
 }
 
 @end
+
+
+
+@interface YYLabel(IBInspectableProperties)
+@end
+
+@implementation YYLabel (IBInspectableProperties)
+
+- (BOOL)fontIsBold_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
+    return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
+}
+
+- (UIFont *)boldFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
+}
+
+- (UIFont *)normalFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
+}
+
+- (void)setFontName_:(NSString *)fontName {
+    if (!fontName) return;
+    UIFont *font = self.font;
+    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+        font = [UIFont systemFontOfSize:font.pointSize];
+    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+        font = [UIFont boldSystemFontOfSize:font.pointSize];
+    } else {
+        if ([self fontIsBold_:font] && ![fontName.lowercaseString containsString:@"bold"]) {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+            font = [self boldFont_:font];
+        } else {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+        }
+        if (font) self.font = font;
+    }
+}
+
+- (void)setFontSize_:(CGFloat)fontSize {
+    if (fontSize <= 0) return;
+    UIFont *font = self.font;
+    font = [font fontWithSize:fontSize];
+    if (font) self.font = font;
+}
+
+- (void)setFontIsBold_:(BOOL)fontBold {
+    UIFont *font = self.font;
+    if ([self fontIsBold_:font] == fontBold) return;
+    if (fontBold) {
+        font = [self boldFont_:font];
+    } else {
+        font = [self normalFont_:font];
+    }
+    if (font) self.font = font;
+}
+
+- (void)setInsetTop_:(CGFloat)textInsetTop {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.top = textInsetTop;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetBottom_:(CGFloat)textInsetBottom {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.bottom = textInsetBottom;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetLeft_:(CGFloat)textInsetLeft {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.left = textInsetLeft;
+    self.textContainerInset = insets;
+    
+}
+
+- (void)setInsetRight_:(CGFloat)textInsetRight {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.right = textInsetRight;
+    self.textContainerInset = insets;
+}
+
+- (void)setDebugEnabled_:(BOOL)enabled {
+    if (!enabled) {
+        self.debugOption = nil;
+    } else {
+        YYTextDebugOption *debugOption = [YYTextDebugOption new];
+        debugOption.baselineColor = [UIColor redColor];
+        debugOption.CTFrameBorderColor = [UIColor redColor];
+        debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
+        debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
+        self.debugOption = debugOption;
+    }
+}
+
+@end

+ 71 - 15
YYText/YYTextView.h

@@ -49,6 +49,7 @@ NS_ASSUME_NONNULL_BEGIN
 @end
 
 
+#if !TARGET_INTERFACE_BUILDER
 
 /**
  The YYTextView class implements the behavior for a scrollable, multiline text region.
@@ -108,18 +109,18 @@ NS_ASSUME_NONNULL_BEGIN
  Set a new value to this property also causes the new alignment to be applied to the entire `attributedText`.
  Get the value returns the alignment at the head of `attributedText`.
  */
-@property (nonatomic, assign) NSTextAlignment textAlignment;
+@property (nonatomic) NSTextAlignment textAlignment;
 
 /**
  The text vertical aligmnent in container. Default is YYTextVerticalAlignmentTop.
  */
-@property (nonatomic, assign) YYTextVerticalAlignment textVerticalAlignment;
+@property (nonatomic) YYTextVerticalAlignment textVerticalAlignment;
 
 /**
  The types of data converted to clickable URLs in the text view. Default is UIDataDetectorTypeNone.
  The tap or long press action should be handled by delegate.
  */
-@property (nonatomic, assign) UIDataDetectorTypes dataDetectorTypes;
+@property (nonatomic) UIDataDetectorTypes dataDetectorTypes;
 
 /**
  The attributes to apply to links at normal state. Default is light blue color.
@@ -212,7 +213,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  The inset of the text container's layout area within the text view's content area.
  */
-@property (nonatomic, assign) UIEdgeInsets textContainerInset;
+@property (nonatomic) UIEdgeInsets textContainerInset;
 
 /**
  An array of UIBezierPath objects representing the exclusion paths inside the 
@@ -224,7 +225,7 @@ NS_ASSUME_NONNULL_BEGIN
  Whether the receiver's layout orientation is vertical form. Default is NO.
  It may used to edit/display CJK text.
  */
-@property (nonatomic, assign, getter=isVerticalForm) BOOL verticalForm;
+@property (nonatomic, getter=isVerticalForm) BOOL verticalForm;
 
 /**
  The text line position modifier used to modify the lines' position in layout.
@@ -252,13 +253,13 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  The current selection range of the receiver.
  */
-@property (nonatomic, assign) NSRange selectedRange;
+@property (nonatomic) NSRange selectedRange;
 
 /**
  A Boolean value indicating whether inserting text replaces the previous contents.
  The default value is NO.
  */
-@property (nonatomic, assign) BOOL clearsOnInsertion;
+@property (nonatomic) BOOL clearsOnInsertion;
 
 /**
  A Boolean value indicating whether the receiver is selectable. Default is YES.
@@ -282,20 +283,20 @@ NS_ASSUME_NONNULL_BEGIN
  A Boolean value indicating whether the receiver can paste image from pasteboard. Default is NO.
  When the value of this property is YES, user can paste image from pasteboard via "paste" menu.
  */
-@property (nonatomic, assign) BOOL allowsPasteImage;
+@property (nonatomic) BOOL allowsPasteImage;
 
 /**
  A Boolean value indicating whether the receiver can paste attributed text from pasteboard. Default is NO.
  When the value of this property is YES, user can paste attributed text from pasteboard via "paste" menu.
  */
-@property (nonatomic, assign) BOOL allowsPasteAttributedString;
+@property (nonatomic) BOOL allowsPasteAttributedString;
 
 /**
  A Boolean value indicating whether the receiver can copy attributed text to pasteboard. Default is YES.
  When the value of this property is YES, user can copy attributed text (with attachment image)
  from text view to pasteboard via "copy" menu.
  */
-@property (nonatomic, assign) BOOL allowsCopyAttributedString;
+@property (nonatomic) BOOL allowsCopyAttributedString;
 
 
 #pragma mark - Manage the undo and redo
@@ -307,12 +308,12 @@ NS_ASSUME_NONNULL_BEGIN
  A Boolean value indicating whether the receiver can undo and redo typing with
  shake gesture. The default value is YES.
  */
-@property (nonatomic, assign) BOOL allowsUndoAndRedo;
+@property (nonatomic) BOOL allowsUndoAndRedo;
 
 /**
  The maximum undo/redo level. The default value is 20.
  */
-@property (nonatomic, assign) NSUInteger maximumUndoLevel;
+@property (nonatomic) NSUInteger maximumUndoLevel;
 
 
 #pragma mark - Replacing the System Input Views
@@ -327,7 +328,7 @@ NS_ASSUME_NONNULL_BEGIN
  @discussion If set the value while first responder, it will not take effect until 
  'reloadInputViews' is called.
  */
-@property (nullable, readwrite, retain) UIView *inputView;
+@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputView;
 
 /**
  The custom accessory view to display when the text view becomes the first responder.
@@ -336,16 +337,71 @@ NS_ASSUME_NONNULL_BEGIN
  @discussion If set the value while first responder, it will not take effect until
  'reloadInputViews' is called.
  */
-@property (nullable, readwrite, retain) UIView *inputAccessoryView;
+@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputAccessoryView;
 
 /**
  If you use an custom accessory view without "inputAccessoryView" property,
  you may set the accessory view's height. It may used by auto scroll calculation.
  */
-@property (nonatomic, assign) CGFloat extraAccessoryViewHeight;
+@property (nonatomic) CGFloat extraAccessoryViewHeight;
 
 @end
 
+
+#else // TARGET_INTERFACE_BUILDER
+IB_DESIGNABLE
+@interface YYTextView : UIScrollView <UITextInput>
+@property (null_resettable, nonatomic, copy) IBInspectable NSString *text;
+@property (nullable, nonatomic, strong) IBInspectable UIColor *textColor;
+@property (nullable, nonatomic, strong) IBInspectable NSString *fontName_;
+@property (nonatomic) IBInspectable CGFloat fontSize_;
+@property (nonatomic) IBInspectable BOOL fontIsBold_;
+@property (nonatomic) IBInspectable NSTextAlignment textAlignment;
+@property (nonatomic) IBInspectable YYTextVerticalAlignment textVerticalAlignment;
+@property (nullable, nonatomic, copy) IBInspectable NSString *placeholderText;
+@property (nullable, nonatomic, strong) IBInspectable UIColor *placeholderTextColor;
+@property (nullable, nonatomic, strong) IBInspectable NSString *placeholderFontName_;
+@property (nonatomic) IBInspectable CGFloat placeholderFontSize_;
+@property (nonatomic) IBInspectable BOOL placeholderFontIsBold_;
+@property (nonatomic, getter=isVerticalForm) IBInspectable BOOL verticalForm;
+@property (nonatomic) IBInspectable BOOL clearsOnInsertion;
+@property (nonatomic, getter=isSelectable) IBInspectable BOOL selectable;
+@property (nonatomic, getter=isHighlightable) IBInspectable BOOL highlightable;
+@property (nonatomic, getter=isEditable) IBInspectable BOOL editable;
+@property (nonatomic) IBInspectable BOOL allowsPasteImage;
+@property (nonatomic) IBInspectable BOOL allowsPasteAttributedString;
+@property (nonatomic) IBInspectable BOOL allowsCopyAttributedString;
+@property (nonatomic) IBInspectable BOOL allowsUndoAndRedo;
+@property (nonatomic) IBInspectable NSUInteger maximumUndoLevel;
+@property (nonatomic) IBInspectable CGFloat insetTop_;
+@property (nonatomic) IBInspectable CGFloat insetBottom_;
+@property (nonatomic) IBInspectable CGFloat insetLeft_;
+@property (nonatomic) IBInspectable CGFloat insetRight_;
+@property (nonatomic) IBInspectable BOOL debugEnabled_;
+@property (nullable, nonatomic, weak) id<YYTextViewDelegate> delegate;
+@property (nullable, nonatomic, strong) UIFont *font;
+@property (nonatomic) UIDataDetectorTypes dataDetectorTypes;
+@property (nullable, nonatomic, copy) NSDictionary *linkTextAttributes;
+@property (nullable, nonatomic, copy) NSDictionary *highlightTextAttributes;
+@property (nullable, nonatomic, copy) NSDictionary *typingAttributes;
+@property (nullable, nonatomic, copy) NSAttributedString *attributedText;
+@property (nullable, nonatomic, strong) id<YYTextParser> textParser;
+@property (nullable, nonatomic, strong, readonly) YYTextLayout *textLayout;
+@property (nullable, nonatomic, strong) UIFont *placeholderFont;
+@property (nullable, nonatomic, copy) NSAttributedString *placeholderAttributedText;
+@property (nonatomic) UIEdgeInsets textContainerInset;
+@property (nullable, nonatomic, copy) NSArray *exclusionPaths;
+@property (nullable, nonatomic, copy) id<YYTextLinePositionModifier> linePositionModifier;
+@property (nullable, nonatomic, copy) YYTextDebugOption *debugOption;
+- (void)scrollRangeToVisible:(NSRange)range;
+@property (nonatomic) NSRange selectedRange;
+@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputView;
+@property (nullable, nonatomic, readwrite, strong) __kindof UIView *inputAccessoryView;
+@property (nonatomic) CGFloat extraAccessoryViewHeight;
+@end
+#endif // !TARGET_INTERFACE_BUILDER
+
+
 // Notifications, see UITextView's documentation for more information.
 UIKIT_EXTERN NSString *const YYTextViewTextDidBeginEditingNotification;
 UIKIT_EXTERN NSString *const YYTextViewTextDidChangeNotification;

+ 151 - 6
YYText/YYTextView.m

@@ -71,13 +71,11 @@ static float _YYDeviceSystemVersion() {
 #define kDefaultVerticalInset UIEdgeInsetsMake(4, 6, 4, 6)
 
 
-
 NSString *const YYTextViewTextDidBeginEditingNotification = @"YYTextViewTextDidBeginEditing";
 NSString *const YYTextViewTextDidChangeNotification = @"YYTextViewTextDidChange";
 NSString *const YYTextViewTextDidEndEditingNotification = @"YYTextViewTextDidEndEditing";
 
 
-
 typedef NS_ENUM (NSUInteger, YYTextGrabberDirection) {
     kStart = 1,
     kEnd   = 2,
@@ -91,7 +89,6 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
 };
 
 
-
 /// An object that captures the state of the text view. Used for undo and redo.
 @interface _YYTextViewUndoObject : NSObject
 @property (nonatomic, strong) NSAttributedString *text;
@@ -107,7 +104,6 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
 @end
 
 
-
 @interface YYTextView () <UIScrollViewDelegate, UIAlertViewDelegate, YYTextDebugTarget, YYTextKeyboardObserver> {
     
     YYTextRange *_selectedTextRange; /// nonnull
@@ -215,8 +211,12 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
 
 /// Update layout and selection before runloop sleep/end.
 - (void)_commitUpdate {
+#if !TARGET_INTERFACE_BUILDER
     _state.needUpdate = YES;
     [[YYTextTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];
+#else
+    [self _update];
+#endif
 }
 
 /// Update layout and selection view if needed.
@@ -358,8 +358,12 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
 
 /// Update placeholder before runloop sleep/end.
 - (void)_commitPlaceholderUpdate {
+#if !TARGET_INTERFACE_BUILDER
     _state.placeholderNeedUpdate = YES;
     [[YYTextTransaction transactionWithTarget:self selector:@selector(_updatePlaceholderIfNeeded)] commit];
+#else
+    [self _updatePlaceholder];
+#endif
 }
 
 /// Update placeholder if needed.
@@ -380,7 +384,7 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
         container.size = self.bounds.size;
         container.truncationType = YYTextTruncationTypeEnd;
         container.truncationToken = nil;
-        YYTextLayout *layout = [YYTextLayout layoutWithContainer:_innerContainer text:_placeholderAttributedText];
+        YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_placeholderAttributedText];
         CGSize size = [layout textBoundingSize];
         BOOL needDraw = size.width > 1 && size.height > 1;
         if (needDraw) {
@@ -393,7 +397,6 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
             frame.size = image.size;
             if (container.isVerticalForm) {
                 frame.origin.x = self.bounds.size.width - image.size.width;
-                frame.origin.y = self.bounds.size.height - image.size.height;
             } else {
                 frame.origin = CGPointZero;
             }
@@ -1941,6 +1944,7 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
     self.delaysContentTouches = NO;
     self.canCancelContentTouches = YES;
     self.multipleTouchEnabled = NO;
+    self.clipsToBounds = YES;
     [super setDelegate:self];
     
     _text = @"";
@@ -3670,3 +3674,144 @@ typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
 }
 
 @end
+
+
+
+@interface YYTextView(IBInspectableProperties)
+@end
+
+@implementation YYTextView(IBInspectableProperties)
+
+- (BOOL)fontIsBold_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
+    return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
+}
+
+- (UIFont *)boldFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
+}
+
+- (UIFont *)normalFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
+}
+
+- (void)setFontName_:(NSString *)fontName {
+    if (!fontName) return;
+    UIFont *font = self.font;
+    if (!font) font = [self _defaultFont];
+    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+        font = [UIFont systemFontOfSize:font.pointSize];
+    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+        font = [UIFont boldSystemFontOfSize:font.pointSize];
+    } else {
+        if ([self fontIsBold_:font] && ![fontName.lowercaseString containsString:@"bold"]) {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+            font = [self boldFont_:font];
+        } else {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+        }
+        if (font) self.font = font;
+    }
+}
+
+- (void)setFontSize_:(CGFloat)fontSize {
+    if (fontSize <= 0) return;
+    UIFont *font = self.font;
+    if (!font) font = [self _defaultFont];
+    if (!font) font = [self _defaultFont];
+    font = [font fontWithSize:fontSize];
+    if (font) self.font = font;
+}
+
+- (void)setFontIsBold_:(BOOL)fontBold {
+    UIFont *font = self.font;
+    if (!font) font = [self _defaultFont];
+    if ([self fontIsBold_:font] == fontBold) return;
+    if (fontBold) {
+        font = [self boldFont_:font];
+    } else {
+        font = [self normalFont_:font];
+    }
+    if (font) self.font = font;
+}
+
+- (void)setPlaceholderFontName_:(NSString *)fontName {
+    if (!fontName) return;
+    UIFont *font = self.placeholderFont;
+    if (!font) font = [self _defaultFont];
+    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+        font = [UIFont systemFontOfSize:font.pointSize];
+    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+        font = [UIFont boldSystemFontOfSize:font.pointSize];
+    } else {
+        if ([self fontIsBold_:font] && ![fontName.lowercaseString containsString:@"bold"]) {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+            font = [self boldFont_:font];
+        } else {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+        }
+        if (font) self.placeholderFont = font;
+    }
+}
+
+- (void)setPlaceholderFontSize_:(CGFloat)fontSize {
+    if (fontSize <= 0) return;
+    UIFont *font = self.placeholderFont;
+    if (!font) font = [self _defaultFont];
+    font = [font fontWithSize:fontSize];
+    if (font) self.placeholderFont = font;
+}
+
+- (void)setPlaceholderFontIsBold_:(BOOL)fontBold {
+    UIFont *font = self.placeholderFont;
+    if (!font) font = [self _defaultFont];
+    if ([self fontIsBold_:font] == fontBold) return;
+    if (fontBold) {
+        font = [self boldFont_:font];
+    } else {
+        font = [self normalFont_:font];
+    }
+    if (font) self.placeholderFont = font;
+}
+
+- (void)setInsetTop_:(CGFloat)textInsetTop {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.top = textInsetTop;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetBottom_:(CGFloat)textInsetBottom {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.bottom = textInsetBottom;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetLeft_:(CGFloat)textInsetLeft {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.left = textInsetLeft;
+    self.textContainerInset = insets;
+    
+}
+
+- (void)setInsetRight_:(CGFloat)textInsetRight {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.right = textInsetRight;
+    self.textContainerInset = insets;
+}
+
+- (void)setDebugEnabled_:(BOOL)enabled {
+    if (!enabled) {
+        self.debugOption = nil;
+    } else {
+        YYTextDebugOption *debugOption = [YYTextDebugOption new];
+        debugOption.baselineColor = [UIColor redColor];
+        debugOption.CTFrameBorderColor = [UIColor redColor];
+        debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
+        debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
+        self.debugOption = debugOption;
+    }
+}
+
+@end