自从第一篇文章的排版那么丑后,后来发现原来有MarkDown可以用,操作也很简单,今天就用MarkDown来编辑这篇文章吧!
进入主题!!!
一、为什么写这篇文章
我们都知道,苹果每个版本的更新都会有一些改动,虽然有时候表面看起来和之前的没有什么不一样,但其实背地里恐怕已经是其他的了。
最近比较关注我负责的某金服的app的崩溃统计,我们用的是听云,感觉是个很不错的统计工具,比起友盟感觉要强很多,因为其中的很多信息都一目了然,尤其是崩溃时的堆栈信息。
其中就发现了一个崩溃,信息如下:
[<UITextView 0x1379bf600> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key _placeholderLabel.
从其中可以看出是因为key为_placeholderLabel不被允许(compliant)操作了。
这个时候如果有意识的话,会发现崩溃的系统都是iOS8.1和iOS8.2系列的。
但是如果你按照常理去查找的话,会发现iOS11是正常的,iOS10呢?试一试还是正常的,iOS9呢?还是正常的。这个时候有没有感觉很惊喜?有没有很意外?最后终于来到了iOS8,你会发现iOS8.4、iOS8.3都是正常的,只有iOS8.1和iOS8.2有问题。我相信一般很难有小伙伴走到这一步吧!
这个问题的症结在于这么一个需求:
多行编辑文本框(UITextView)需要一个类似于单行编辑文本框(UITextField)的提示文本,但是UITextView并没有暴露相关的接口,所以有小伙伴就想利用高大上的KVC。
个人对于KVC和利用runtime去访问一些私有接口比较忌讳,因为这样访问很容易出现各种问题。比如最近发布的手机iPhone X,你会发现下面的方法在同样系统但是不同类型手机(iPhone 7 和 iPhone X比较)上时,iPhone X会崩溃!!!(这也是发现在我们工程里的问题)
// 状态栏是由当前app控制的,首先获取当前app
UIApplication *app = [UIApplication sharedApplication];
NSArray *children = [[[app valueForKeyPath:@"statusBar"] valueForKeyPath:@"foregroundView"] subviews];
int type = 0;
for (id child in children) {
if ([child isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
type = [[child valueForKeyPath:@"dataNetworkType"] intValue];
}
}
......
基于以上原因,我决定做一些力所能及的研究,并最终实现产品的需求!!!
二、UITextView的结构
下图是模拟器上当前编辑框状态:
下图是对应的层次结构:
(这里是非选中状态下的结构,有兴趣可以自己先研究有选中时的结构)
1、解析
这里可以发现,文本的展示主要是靠一个叫_UITextContainerView的容器在管理,它的高度会随着文本的多少自己变化;
然后还有一个UITextSelectionView,这个主要是用来管理选中文本的视图,其大小是完全随着_UITextContainerView来变化的,从某种角度来说,光标(图中的蓝色箭头)是一个选中了文字长度为0的视图,所以它位于该视图中。如果你看了有选中内容的时候的结构就更能理解该视图图如其名了。
下面主要看一下_UITextContainerView这个类。
2、_UITextContainerView
<<_UITextContainerView: 0x7fa6ef52cb50; frame = (0 0; 345 58); layer = <__UITextTiledLayer: 0x6040000dff70>> minSize = {0, 0}, maxSize = {1.7976931348623157e+308, 1.7976931348623157e+308}, textContainer = <NSTextContainer: 0x6040001038d0 size = (345.000000,inf); widthTracksTextView = YES; heightTracksTextView = NO>; exclusionPaths = 0x60400000fe20; lineBreakMode = 0>
上面是对此时_UITextContainerView的说明。这里我也暂时没找到更多的解释,还在努力查找中,如果您有资料,麻烦发我一份!
注意,这里有一个视图:NSTextContainer,是不是很熟悉啊?可以通过UITextView的属性textContainer获得对于这个类,官方是这样说的:
A region where text is laid out.
An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on. An NSTextContainer object typically defines rectangular regions, but you can define exclusion paths inside the text container to create regions where text does not flow. You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics.
Instances of the NSTextContainer, NSLayoutManager, and NSTextStorage classes can be accessed from threads other than the main thread as long as the app guarantees access from only one thread at a time.
简单说,它和NSLayoutManager组合,可以用来决定文字的展示方式,包括展示位置、和四边的距离、换行位置,以及展示区域的形状等等,如果有需要,我们可以实现一个子类,比如实现一个圆形区域。
这里说到了三个类:NSTextContainer, NSLayoutManager, and NSTextStorage 。NSTextContainer用来控制文字的展示区域;NSLayoutManager控制文字的展示方式,即排版布局;NSTextStorage则用来储存文本信息(它是NSMutableAttributeString的子类)。
它们三个并不是独立运行的,相互之间有约束,相互作用,相互包含
下面就一起来研究一下吧!!!
3、NSTextContainer, NSLayoutManager, and NSTextStorage
在开始下面的内容前,先来一个默认时的内容展示:
(1)、先来几个坑,一起填平吧!
A、NSLayoutManager初始化
NSLayoutManager * layoutManager = [[NSLayoutManager alloc] init]; //这里必须要用这个方法初始化
B、给UITextView赋值
NSTextStorage * storeage = [[NSTextStorage alloc] initWithString:[self contentStr]];
[storeage addLayoutManager:layoutManager];
NSTextContainer * textContainer = [[NSTextContainer alloc] init];
textContainer.widthTracksTextView = YES;
textContainer.heightTracksTextView = YES;
[layoutManager addTextContainer:textContainer];
//上面这部分不能写到其他方法里,必须和textView的初始化放到一起,不知道是为什么,我刚开始写在外面是不行的
_textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 10, [UIScreen mainScreen].bounds.size.width - 2 * 10, [self getContentHeight]) textContainer:textContainer];
_textView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:_textView];
(2)、NSTextContainer简单使用
A、只有一个container
- (void)configBasicTextView {
CGFloat height = [self getContentHeight];
NSLayoutManager * layoutManager = [[NSLayoutManager alloc] init]; //这里必须要用这个方法初始化
NSTextStorage * storeage = [[NSTextStorage alloc] initWithString:[self contentStr]];
[storeage addLayoutManager:layoutManager];
NSTextContainer * textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake([UIScreen mainScreen].bounds.size.width, height / 2)];
textContainer.widthTracksTextView = YES;
[layoutManager addTextContainer:textContainer];
_textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 10, [UIScreen mainScreen].bounds.size.width - 2 * 10, height) textContainer:textContainer];
_textView.backgroundColor = [UIColor orangeColor];
_textView.font = [UIFont systemFontOfSize:18];
[self.view addSubview:_textView];
}
- (NSString *)contentStr {
return @"A region where text is laid out.An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on. An NSTextContainer object typically defines rectangular regions, but you can define exclusion paths inside the text container to create regions where text does not flow. You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics.Instances of the NSTextContainer, NSLayoutManager, and NSTextStorage classes can be accessed from threads other than the main thread as long as the app guarantees access from only one thread at a time.";
}
注意,这里的NSTextContainer的size我设置成了和UITextView宽度一样,但是高度只有其一半,你会发现,这里只出现了一半。
B、有两个container
- (void)configTextViewWithTwoContainer {
CGFloat height = [self getContentHeight];
NSLayoutManager * layoutManager = [[NSLayoutManager alloc] init]; //这里必须要用这个方法初始化
NSTextStorage * storeage = [[NSTextStorage alloc] initWithString:[self contentStr]];
[storeage addLayoutManager:layoutManager];
NSTextContainer * textContainer1 = [[NSTextContainer alloc] initWithSize:CGSizeMake([UIScreen mainScreen].bounds.size.width, height / 2)];
textContainer1.widthTracksTextView = YES;
NSTextContainer * textContainer2 = [[NSTextContainer alloc] initWithSize:CGSizeMake([UIScreen mainScreen].bounds.size.width, height / 2)];
textContainer2.widthTracksTextView = YES;
[layoutManager addTextContainer:textContainer1];
[layoutManager addTextContainer:textContainer2];
_textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 10, [UIScreen mainScreen].bounds.size.width - 2 * 10, height) textContainer:textContainer2];
_textView.backgroundColor = [UIColor orangeColor];
_textView.font = [UIFont systemFontOfSize:18];
[self.view addSubview:_textView];
}
这里虽然有两个NSTextContainer,但是,因为我初始化TextView的时候用的是第二个,你会发现展示的和预想的不太一样,它只展示了一部分,而这部分恰好是在第一个NSTextContainer中无法展示的内容。
通过上面可以发现,NSTextContainer会将内容自动按照自己的尺寸做特殊的分页展示。
C、NSTextContainer属性lineBreakMode
先看看该属性的可选值:
typedef NS_ENUM(NSInteger, NSLineBreakMode) {
NSLineBreakByWordWrapping = 0, // Wrap at word boundaries, default
NSLineBreakByCharWrapping, // Wrap at character boundaries
NSLineBreakByClipping, // Simply clip
NSLineBreakByTruncatingHead, // Truncate at head of line: "...wxyz"
NSLineBreakByTruncatingTail, // Truncate at tail of line: "abcd..."
NSLineBreakByTruncatingMiddle // Truncate middle of line: "ab...yz"
}
使用很简单,只需要在原有的基础上添加一句类似下面的代码:
textContainer.lineBreakMode = NSLineBreakByTruncatingMiddle;
我就不粘贴代码了!
该值默认为NSLineBreakByWordWrapping;
注意:这里如果设置成后三种带有截断性质的话,展示会有所不同,比如设置成NSLineBreakByTruncatingMiddle,展示如下图:
(自己找不同哦)
结论:
1、通过实验,你会发现,后三种截断的出现都只在最后一行的相应位置
2、虽然截断了,也展示出了整段文字的最后一部分的信息,但是并不影响第二个NSTextContainer的展示。
D、NSTextContainer属性lineFragmentPadding
该属性设置padding,即内容距离左右两边的距离,默认值为5.
textContainer.lineFragmentPadding = 20;
效果如下:
注意:
UITextView有个属性textContainerInset,该属性表示content的开始位置距离四边的距离,它确定了开始位置;
NSTextContainer属性lineFragmentPadding,该属性表示每行相对于内容开始和结束的地方再偏离的值;
它俩的联系,个人觉得是:lineFragmentPadding是基于textContainerInset做的判断。
比如此时代码如下:
textContainer.widthTracksTextView = YES;
textContainer.lineFragmentPadding = 20;
[layoutManager addTextContainer:textContainer];
_textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 10, [UIScreen mainScreen].bounds.size.width - 2 * 10, height) textContainer:textContainer];
_textView.backgroundColor = [UIColor orangeColor];
_textView.textContainerInset = UIEdgeInsetsMake(0, 20, 0, 20);
_textView.font = [UIFont systemFontOfSize:18];
[self.view addSubview:_textView];
NSLog(@"%@",NSStringFromUIEdgeInsets(_textView.textContainerInset)); //{20, 0, 20, 0}
效果图如下:
E、NSTextContainer属性exclusionPaths
这个应该是该类的精华,可以用该属性指定内容不展示的位置!该值的定义和说明如下:
// Default value : empty array An array of UIBezierPath representing the exclusion paths inside the receiver's bounding rect.
@property (copy, NS_NONATOMIC_IOSONLY) NSArray<UIBezierPath *> *exclusionPaths NS_AVAILABLE(10_11, 7_0);
不说了,直接上代码!!!
- (void)configTextViewWithContainerAndExclusionPaths {
CGFloat height = [self getContentHeight];
CGFloat width = [UIScreen mainScreen].bounds.size.width - 2 * 10;
NSLayoutManager * layoutManager = [[NSLayoutManager alloc] init]; //这里必须要用这个方法初始化
NSTextStorage * storeage = [[NSTextStorage alloc] initWithString:[self contentStr]];
[storeage addLayoutManager:layoutManager];
NSTextContainer * textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake([UIScreen mainScreen].bounds.size.width, height / 2)];
textContainer.widthTracksTextView = YES;
UIBezierPath * path1 = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, width / 2, textContainer.size.height / 2)];
UIBezierPath * path2 = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(width / 2, textContainer.size.height * 3 / 4, width / 2, textContainer.size.height / 4) byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:CGSizeMake(20, 20)];
textContainer.exclusionPaths = @[path1, path2];
[layoutManager addTextContainer:textContainer];
_textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 10, width, height) textContainer:textContainer];
_textView.backgroundColor = [UIColor orangeColor];
_textView.font = [UIFont systemFontOfSize:18];
[self.view addSubview:_textView];
}
这里画出来了两个区域,左上角上一个矩形不带圆角,右下角是一个左上角带圆角的矩形。可以看到,在这两个区域寸草不生了。