IOS图文混排编辑器
前言
IOS中图文混排使用的还是比较多的,网上也有相应的第三方库,例如YYText就是一个很好用的第三方库,但是涉及的业务需求不同和考虑到包的资源大小,往往很难复用网上的一些例子和第三方库,正好前段时间公司项目中需要一个图文混排的编辑器,于是乎就根据需求自己写了个图文混排的编辑器,效果和微博 头条文章 正文编辑器比较类似,现在把自己的一些心得和经验分享如下。
牛刀小试篇
- 刚开始脑子中的第一个想法就是 使用系统自带的UITextView来做,使用UITextView的一个好处是,它本身就是系统提供给我们来处理用户输入文字编辑文字的一个控件,API 提供的一些方法也比较好用。只要我们解决了图片在里面正常显示就可以达到我们想要的效果。于是乎查找下UITextView的API 看看有没有提供相关的方法,最后惊喜地发现UITextView有一个NSTextStorage类型的属性textStorage,而NSTextStorage又是什么呢?
这里简单介绍下:
NSTextStorage保存并管理UITextView要展示的文字内容,该类是NSMutableAttributedString的子类,由于可以灵活地往文字添加或修改属性,所以非常适用于保存并修改文字属性。
这样我们就可以通过富文本向UITextView中插入图片。可能有人就要问了怎样通过富文本向UITextView中插入图片呢?不要忘了系统有给我们提供一个这样的方法:
+ (NSAttributedString *)attributedStringWithAttachment:(NSTextAttachment *)attachment NS_AVAILABLE(10_0, 7_0);
通过这个方法中的NSTextAttachment 类我们可以将UIImage类型转换成NSAttributedString类型,这样我们就可以随心所欲的将文字和图片在UITextView中进行操作。
这里的NSTextAttachment的工作原理是将想要插入的图片作为一个字符处理,转换成NSAttributedString。
思路通了就赶紧去验证下是否可行,下面直接上代码:
#pragma mark -相册代理
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
//适配屏幕宽度
UIImage *image1 = [image scaleToSize:CGSizeMake(screen_Width, image.size.height*screen_Width/image.size.width)];
//设置可编辑区域
UIView * _whiteView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, screen_Width, screen_Height)];
_whiteView.backgroundColor = [UIColor whiteColor];
[[UIApplication sharedApplication].keyWindow addSubview:_whiteView];
backView = [[SLEditPhoto alloc]initWithFrame:CGRectMake(0, 0, screen_Width, image1.size.height) andWithImage:image1];
backView.center = _whiteView.center;
backView.userInteractionEnabled = YES;
backView.center = self.view.center;
[_whiteView addSubview:backView];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(screen_Width, backView.frame.size.height), YES, 1.0);
[backView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *uiImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[_whiteView removeFromSuperview];
[backView removeFromSuperview];
//裁剪
NSTextAttachment *imgTextAtta = [[NSTextAttachment alloc]init];
CGImageRef img = CGImageCreateWithImageInRect(uiImage.CGImage, CGRectMake(0, 0, image1.size.width-10, image1.size.height));
UIImage *newImage = [UIImage imageWithCGImage:img];
imgTextAtta.image = newImage;
//插入图片
[_textView.textStorage insertAttributedString:[NSAttributedString attributedStringWithAttachment:imgTextAtta]atIndex:_textView.selectedRange.location];
//设置光标位置
_textView.selectedRange = NSMakeRange(_textView.selectedRange.location + 1, _textView.selectedRange.length);
//设置样式
[self resetTextStyle];
[self dismissViewControllerAnimated:YES completion:nil];
//每次添加完图片后 返回编辑状态
[_textView becomeFirstResponder];
}
这里需要注意的一点是我们在获得图片后,首先要给图片设置一个可编辑的区域然后再插入,否则插入的图片会是黑色的一个区域。还有就是需要设置textview的光标的位置,以提示用户接下来的编辑位置。
效果图如下:
有设置可编辑区:
没设置可编辑区 :
到了这里看似已经完美实现了图文混排的编辑和删除功能,但是结合项目的实际需求,当用户编辑完内容发布后,再次进入这个页面编辑内容的时候,我们首先要把从服务端得到的内容按照之前的格式展示出来,在展示的过程中有一个问题:我们需要先把文本中包含的图片下载下来然后在去展示,这样就会出现一个问题:我们的初衷是将文字和图片一起当作字符来显示,现在布局的时候我们不能立刻拿到相应的图片,就会导致布局错乱。当然我们还可以先布局所有文字,然后等图片下载好后再转换成字符插入到相应的文字中,但是这样的话我们会多出很多管理图片插入,记录图片在文本中位置的工作量,而且频繁的这样操作,会使我们的代码逻辑一片混乱。不过如果大家的业务不涉及图片的上传和下载,也就是编辑的图片是在本地加载的,那这是一种十分可行的实现方式,本人亲测可以。
峰回路转篇
显然这样做并不符合我们要求,无奈只有转换思路寻找另一种实现方式。总结上面的实现方式,发现主要的问题是在服务端返回的图片异步加载的时候我们不能立马拿到图片而导致布局的错乱,于是顺着这个问题去思考,要想解决这个问题我们平时最常用的就是使用UIImageView通过SDWebImage来处理,当没获得图片的时候可以使用默认的图片来占位加载,得到图片后在更新UI 。那么我们是不是可以通过UIImageView来加载图片,通过UITextviiew来展示文字,为了可以滑动我们可以将他们加载在UIScrollerView上来展示,这样我们就可以更好的控制文字和图片。
好的,根据上面的分析我们可以将方案抽象为:这个图文混排的编辑器由展示文字的UITextviiew,展示图片的UIImageView和承载这两个控件的UIScrollerView组成。一切搞定,我们来看看具体的实现:
首先我们定义两个方法来创建UITextviiew和UIImageView,采用Masnory自动布局。实现比较简单直接上代码:
#pragma mark--CreatCompoment
- (GADetialTextView * )creatTxetViewWithObject:(id)object text:(NSString *)text type:(ObjectType)type{
GADetialTextView *textView = [[GADetialTextView alloc]init];
[textView addSubview:self.placeholder];
textView.textAlignment = NSTextAlignmentLeft;
self.placeholder.hidden = YES;
if (_isDeaultTextView) {
self.placeholder.hidden = NO;
}
if (!_isDeaultTextView) {
[self.insertObjecArray addObject:textView];
}
_isDeaultTextView = NO;
textView.text = text;
textView.font = [UIFont systemFontOfSize:17.0];
CGFloat height = [textView sizeThatFits:CGSizeMake(Width, CGFLOAT_MAX)].height;
if (!height) {
height = TexViewDefaultHeight;
}
textView.backgroundColor = [UIColor whiteColor];
textView.textContainerInset = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f);
textView.delegate = self;
textView.dataDetectorTypes = UIDataDetectorTypeLink;
textView.showsVerticalScrollIndicator = NO;
textView.showsHorizontalScrollIndicator = NO;
[textView setAutocorrectionType:UITextAutocorrectionTypeNo];
[textView setAutocapitalizationType:UITextAutocapitalizationTypeNone];
[self.textScroller addSubview:textView];
UIView *viewObject = nil;
UITextView *textViewObject = nil;
UIScrollView *scrollerObject = nil;
switch (type) {
case ObjectofUIview:{
viewObject = (UIView *)object;
[textView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(viewObject.mas_bottom).offset(TextToImageSpace);
make.height.mas_equalTo(height);
make.centerX.equalTo(self.textScroller);
make.width.mas_equalTo(Width);
}];
}
break;
case ObjectofScrollerView:{
scrollerObject = (UIScrollView *)object;
[textView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(scrollerObject.mas_top).offset(TextToImageSpace);
make.height.mas_equalTo(height);
make.centerX.equalTo(scrollerObject);
make.width.mas_equalTo(Width);
}];
}
break;
case ObjectofTextView:{
textViewObject = (UITextView *)object;
[textView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(textViewObject.mas_bottom).offset(TextToImageSpace);
make.height.mas_equalTo(height);
make.centerX.equalTo(self.textScroller);
make.width.mas_equalTo(Width);
}];
}
break;
default:
break;
}
[self performSelector:@selector(gettingFrame) withObject:nil afterDelay:0.5];
return textView;
}
- (void)gettingFrame{
CGFloat height ;
_isEditeTextView = NO;
if (!_isFirst) {
//默认只有一个texview
_isFirst = YES;
GADetialTextView *defaultTextView = [self.defaultTextViewArray firstObject];
height = defaultTextView.frame.origin.y + defaultTextView.frame.size.height;
[self changeTextStrollerContentSizeWithHeight:height];
}else{
for ( int i = 0 ; i < self.insertObjecArray.count; i++) {
if ([[self.insertObjecArray objectAtIndex:i] isKindOfClass:[GAEditPhoto class]]) {
GAEditPhoto *photo =(GAEditPhoto*)[self.insertObjecArray objectAtIndex:i];
height = photo.frame.origin.y + photo.frame.size.height;
[self changeTextStrollerContentSizeWithHeight:height];
}else{
GADetialTextView *textView =(GADetialTextView*)[self.insertObjecArray objectAtIndex:i];
height = textView.frame.origin.y + textView.frame.size.height;
[self changeTextStrollerContentSizeWithHeight:height];
}
}
}
}
- (GAEditPhoto *)creatPhotoViewWithObject:(id)object iamgeSize:(CGSize)size model:(GAXMLModel *)model image:(UIImage*)image type:(ObjectType)type{
NSString *url = model.src;
GAEditPhoto *showPhoto = [[GAEditPhoto alloc]init];
showPhoto.tag = _iamgeViewTag;
_iamgeViewTag ++;
[self.insertObjecArray addObject:showPhoto];
self.objectType = ObjectofUIview;
showPhoto.backgroundColor = [UIColor whiteColor];
[self.textScroller addSubview:showPhoto];
CGFloat imageMinHeight = 50;
CGFloat height = EditePothoDefHeight;
if (image) {
if (image.size.height <= imageMinHeight) {
image = [image scaleToSize:CGSizeMake(image.size.width * imageMinHeight /image.size.height, imageMinHeight)];
height = image.size.height;
}else{
height = image.size.height;
}
}else if (model.height) {
height = [model.height floatValue];
}
UIView *viewObject = nil;
UITextView *textViewObject = nil;
UIScrollView *scrollerObject = nil;
@weakify(self);
switch (type) {
case ObjectofUIview:{
viewObject = (UIView *)object;
[showPhoto mas_makeConstraints:^(MASConstraintMaker *make) {
@strongify(self);
make.top.equalTo(viewObject.mas_bottom).offset(TextToImageSpace);
make.height.mas_equalTo(height);
make.centerX.equalTo(self.textScroller);
make.width.mas_equalTo(Width);
}];
}
break;
case ObjectofScrollerView:{
scrollerObject = (UIScrollView *)object;
[showPhoto mas_makeConstraints:^(MASConstraintMaker *make) {
@strongify(self);
make.top.equalTo(scrollerObject.mas_top).offset(TextToImageSpace);
make.height.mas_equalTo(height);
make.centerX.equalTo(self.textScroller);
make.width.mas_equalTo(Width);
}];
}
break;
case ObjectofTextView:{
textViewObject = (UITextView *)object;
[showPhoto mas_makeConstraints:^(MASConstraintMaker *make) {
@strongify(self);
make.top.equalTo(textViewObject.mas_bottom).offset(TextToImageSpace);
make.height.mas_equalTo(height);
make.centerX.equalTo(self.textScroller);
make.width.mas_equalTo(Width);
}];
}
break;
default:
break;
}
[showPhoto.deleButton addTarget:self action:@selector(deletePhoto:) forControlEvents:UIControlEventTouchUpInside];
if (image) {
[showPhoto creatImageViewWith:image andWidth:image.size.width];
[self.insertImageArray addObject:image];
[showPhoto addingDeleButton];
showPhoto.deleButton.tag = _deleTag;
_deleTag++;
}
return showPhoto;
}
庖丁解牛篇
一个图文混排的编辑器仅仅可以创建文本和 图片是远远不够,完整的图文混排编辑器肯要包含文本和图片的编辑功能,甚至是将文章内容转换成可以让服务端识别的文本格式,并且可以解析和展示内容。由于本次的文本转换解析方式只是对准公司特定的业务需求,不具有通用性所以这里就不拿出来介绍了,大家可以根据自己的业务需求自行发挥这部分。故下面主要介绍下文字和图片的编辑逻辑的实现:
-
插入图片
这里先说明一下本图文混排编辑器的整体设计,首先会声明两个全局的可变数组一个存放默认的UITextview文本控件,另外一个存储用户在编辑过程中创建的文本或图片控件,并且如果用户创建图片控件的时候后面会默认创建一个文本控件,这样可以让用户永远知道下一步操作的地方在哪里。
好了,知道了整体设计后我们赶紧回归正题,为了给大家讲清楚这个插入图片的逻辑,本人特意画了一个简易的流程图,供大家参考。
相信聪明的你看完这个流程图后肯定知道怎么实现了吧,下面我们来看下具体的代码实现:
#pragma mark--InsertCompoment
- (void)insertNewImageWithImage:(UIImage *)image model:(GAXMLModel *)model{
// 插入图片
GAEditPhoto *photo = nil;
GADetialTextView *textView = nil;
if (self.insertObjecArray.count > 0) {
//规避容器中没有视图的情况 以上个视图为参照物
if ([[self.insertObjecArray lastObject] isKindOfClass:[GAEditPhoto class]]) {
//得到布局参照物
photo =(GAEditPhoto*)[self.insertObjecArray lastObject];
// 插入图片 并且要在后面放一个默认的textview
GAEditPhoto *newPhoto = [self creatPhotoViewWithObject:photo iamgeSize:image.size model:model image:image type:ObjectofUIview];
[self creatTxetViewWithObject:newPhoto text:@"" type:ObjectofUIview];
self.placeholder.hidden = YES;
}else{
//得到布局参照物
textView =(GADetialTextView*)[self.insertObjecArray lastObject];
if (textView.text.length == 0) {
[self.insertObjecArray removeObject:textView];
[textView removeFromSuperview];
// 插入图片 并且要在后面放一个默认的textview
id object = nil;
photo = [self.insertObjecArray lastObject];
if (photo) {
object = photo;
}else{
object = self.textScroller;
}
GAEditPhoto *newPhoto = [self creatPhotoViewWithObject:object iamgeSize:image.size model:model image:image type:ObjectofUIview];
[self creatTxetViewWithObject:newPhoto text:@"" type:ObjectofUIview];
self.placeholder.hidden =YES;
}else{
// 插入图片 并且要在后面放一个默认的textview
GAEditPhoto *newPhoto = [self creatPhotoViewWithObject:textView iamgeSize:image.size model:model image:image type:ObjectofTextView];
[self creatTxetViewWithObject:newPhoto text:@"" type:ObjectofUIview];
self.placeholder.hidden = YES;
}
}
}else{
//容器中没有视图 以容器为参照物 因默认是一个textview 当插入图片时移除默认的textview 创建图片
GADetialTextView *defaultTextView = [self.defaultTextViewArray firstObject];
if (defaultTextView.text.length == 0) {
//默认texview 为空 则删
[defaultTextView removeFromSuperview];
[self.defaultTextViewArray removeAllObjects];
photo = [self creatPhotoViewWithObject:self.textScroller iamgeSize:image.size model:model image:image type:ObjectofScrollerView];
[self creatTxetViewWithObject:photo text:@"" type:ObjectofUIview];
}else{
[self.insertObjecArray addObject:defaultTextView];
//[self.defaultTextViewArray removeAllObjects];
photo = [self creatPhotoViewWithObject:defaultTextView iamgeSize:image.size model:model image:image type:ObjectofTextView];
[self creatTxetViewWithObject:photo text:@"" type:ObjectofUIview];
}
}
//更新容器的cosntentsize
_isEditeTextView = NO;
[self getInserObjectLastObjectToChangeContensize];
}
-
删除图片流程
能插入图片肯定还要删除图片,我们接着看下一张流程图:
无需多言一切都在图中,请看删除图片的代码实现:
#pragma mark--DeleteCompoment
- (void)deletePhoto:(id)sender{
UIButton *deleButton = (UIButton *)sender;
//需要判断这个textview 是在哪个位置 若在最后一个不做操作 其他情况删除 更新布局
NSInteger currentImageTag = deleButton.tag - MaxTag;
GAEditPhoto *currentPhoto = [self.textScroller viewWithTag:currentImageTag];
NSInteger currentImageIndext = [self.insertObjecArray indexOfObject:currentPhoto];
NSInteger count = self.insertObjecArray.count;
if (currentImageIndext >count || currentImageIndext < 0) {
return;
}
NSInteger lastIndext = currentImageIndext -1;
NSInteger nextIndext = currentImageIndext +1;
NSInteger finalIdext = currentImageIndext +2;
if (!((currentImageIndext == self.insertObjecArray.count -1) ||currentImageIndext == 0)) {
// 不是最后位置 并且也不在第一个
id lastObject = [self.insertObjecArray objectAtIndex:lastIndext];
id nextObject = [self.insertObjecArray objectAtIndex:nextIndext];
id finalObject = nil;
if (self.insertObjecArray.count -1 >= finalIdext) {
finalObject = [self.insertObjecArray objectAtIndex:finalIdext];
}
//布局nextObject
if ([lastObject isKindOfClass:[GADetialTextView class]]) {
[self updateDetialTextViewBottomViewWithNextIndext:nextIndext photo:lastObject];
GADetialTextView *lastTextview = lastObject;
//若是两个textview就合并
if ([nextObject isKindOfClass:[GADetialTextView class]]) {
GADetialTextView *nextTextView = nextObject;
lastTextview.text = [lastTextview.text stringByAppendingString:nextTextView.text];
//改变textview的height
CGFloat height = [lastTextview sizeThatFits:CGSizeMake(Width, CGFLOAT_MAX)].height;
//更新当前textview的高
[lastTextview mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(height);
}];
if (finalObject) {
[finalObject mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(lastTextview.mas_bottom).offset(10);
}];
[self upadteSubViewLayoutWithObject:finalObject];
}
[nextObject removeFromSuperview];
[self.insertObjecArray removeObject:nextObject];
}else if ([nextObject isKindOfClass:[GAEditPhoto class]]){
[self updateDetialTextViewBottomViewWithNextIndext:nextIndext photo:lastObject];
}
}else if ([lastObject isKindOfClass:[GAEditPhoto class]]){
[self updateEditPhotoBottomViewWithNextIndext:nextIndext photo:lastObject];
}
}else if (currentImageIndext == 0){
//在第一个位置
//布局nextObject
[self updateTextScrollerBottomViewWithNextIndext:nextIndext];
}
//移除数组中的数据
[self.insertObjecArray removeObject:currentPhoto];
[self.insertImageArray removeObject:currentPhoto.addImageView.image];
[currentPhoto removeFromSuperview];
_deleTag--;
_iamgeViewTag--;
if (self.insertObjecArray.count == 1) {
if ([[self.insertObjecArray firstObject] isKindOfClass:[GADetialTextView class]]){
GADetialTextView *textView = [self.insertObjecArray firstObject];
if (textView.textStorage.length > 0) {
self.placeholder.hidden = YES;
}else{
self.placeholder.hidden = NO;
}
}
}
[self performSelector:@selector(updateScrollerContentsize) withObject:nil afterDelay:0.5];
}
- (void)updateScrollerContentsize{
_isEditeTextView = NO;
[self getInserObjectLastObjectToChangeContensize];
}
-
文本内容发生变化处理逻辑
知道了图片的插入和删除的编辑实现,老司机的你肯定知道接下来要说的什么吧,请看文字变化的时候需要实现的逻辑流程图和代码实现,这里说明下文字变化时的处理逻辑主要是在UITextView的textViewDidChange方法中实现。
- (void)textViewDidChange:(UITextView *)textView{
GADetialTextView *lastTextView = nil;
if (self.insertObjecArray.count > 0) {
lastTextView = (GADetialTextView *)[self.insertObjecArray lastObject];
}else{
lastTextView = (GADetialTextView *)[self.defaultTextViewArray firstObject];
}
if (textView.textStorage.length > 0 && (lastTextView != textView)) {
_isEditeTextView = YES;
}else{
_isEditeTextView = NO;
}
//控制placeholder的显示
if (![textView.subviews containsObject:self.placeholder]) {
[textView addSubview:self.placeholder];
}
GADetialTextView *firsttextView = nil;
GADetialTextView *defaultTextView = [self.defaultTextViewArray firstObject];
if (self.insertObjecArray.count == 1) {
if ([[self.insertObjecArray firstObject] isKindOfClass:[GADetialTextView class]]){
firsttextView = [self.insertObjecArray firstObject];
self.placeholder.hidden = NO;
}
}
//当前texvtview是默认的
if (textView == defaultTextView || textView == firsttextView) {
if (textView.textStorage.length > 0 ){
self.placeholder.hidden = YES;
}else{
self.placeholder.hidden = NO;
}
}
//改变textview的height
CGFloat height = [textView sizeThatFits:CGSizeMake(Width, CGFLOAT_MAX)].height;
//更新当前textview的高
[textView mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(height);
}];
//容器第一个是texview
if (self.insertObjecArray.count > 0) {
//容器第一个是texview 并且 此时teview就是第一个默认textview
GADetialTextView *firstTextView = nil;
//容器第一个是texview
if ([[self.defaultTextViewArray firstObject] isKindOfClass:[GADetialTextView class]]) {
firstTextView = [self.defaultTextViewArray firstObject];
//当前texvtview是默认的
if (textView == firstTextView) {
if (textView.textStorage.length > 0 ){
self.placeholder.hidden = YES;
}else{
self.placeholder.hidden = NO;
}
//当前视图是否只有默认的这个texview
if (self.insertObjecArray.count > 1) {
//还有别的视图 先更新布局
if (textView.text.length >0) {
for (NSInteger i = 0; i < self.insertObjecArray.count; i++){
if ([[self.insertObjecArray objectAtIndex:i] isKindOfClass:[GAEditPhoto class]]) {
GAEditPhoto *photo =(GAEditPhoto*)[self.insertObjecArray objectAtIndex:i];
[photo mas_updateConstraints:^(MASConstraintMaker *make) {
}];
}else{
GADetialTextView *textView =(GADetialTextView*)[self.insertObjecArray objectAtIndex:i];
[textView mas_updateConstraints:^(MASConstraintMaker *make) {
}];
}
}
}else{
//文字为空 删除
NSInteger indext = [self.insertObjecArray indexOfObject:textView];
//取出下个视图 更新布局
NSInteger nextIndext = indext +1;
if (self.insertObjecArray.count >= nextIndext + 1) {
if ([[self.insertObjecArray objectAtIndex:nextIndext] isKindOfClass:[GAEditPhoto class]]) {
GAEditPhoto *photo =(GAEditPhoto*)[self.insertObjecArray objectAtIndex:nextIndext];
[photo mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.textScroller.mas_top).offset(10);
}];
[self upadteSubViewLayoutWithObject:photo];
}else{
GADetialTextView *textView =(GADetialTextView*)[self.insertObjecArray objectAtIndex:nextIndext];
[textView mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.textScroller.mas_top).offset(10);
}];
[self upadteSubViewLayoutWithObject:textView];
}
}
//移除数组中的数据
[self.insertObjecArray removeObject:textView];
[textView removeFromSuperview];
}
//设置scrollercontensize
[self getInserObjectLastObjectToChangeContensize];
}else{
//只有默认的texview
[self changeTextStrollerContentSizeWithHeight:height];
}
}else{
//不是默认的
//容器第一个是texview 并且 此时teview不是第一个默认textview 并且也不在第一个位置
if (textView.text.length > 0) {
[self upadteSubViewLayoutWithObject:textView];
}else{
//需要判断这个textview 是在哪个位置 若在最后一个不做操作 其他情况删除 更新布局
NSInteger indext = [self.insertObjecArray indexOfObject:textView];
NSInteger lastIndext = indext -1;
NSInteger nextIndext = indext +1;
id lastObject = nil;
if (!(indext == self.insertObjecArray.count -1)) {
// 不是最后位置 并且这个textview也不在第一个
if (lastIndext < 0) {
//防止上个视图不存在问题
lastObject = nil;
}else{
lastObject = [self.insertObjecArray objectAtIndex:lastIndext];
}
//布局nextObject
if (lastObject) {
if ([lastObject isKindOfClass:[GADetialTextView class]]) {
[self updateDetialTextViewBottomViewWithNextIndext:nextIndext photo:lastObject];
}else if ([lastObject isKindOfClass:[GAEditPhoto class]]){
[self updateEditPhotoBottomViewWithNextIndext:nextIndext photo:lastObject];
}
}else{
[self updateTextScrollerBottomViewWithNextIndext:nextIndext];
}
//移除数组中的数据
[self.insertObjecArray removeObject:textView];
[textView removeFromSuperview];
}
}
}
}else{
//容器第一个不是textview
//容器第一个不是texview 并且 此时teview不是第一个默认textview
if (textView.text.length > 0) {
[self upadteSubViewLayoutWithObject:textView];
}else{
//需要判断这个textview 是在哪个位置 若在最后一个不做操作 其他情况删除 更新布局
NSInteger indext = [self.insertObjecArray indexOfObject:textView];
NSInteger lastIndext = indext -1;
NSInteger nextIndext = indext +1;
if (!(indext == self.insertObjecArray.count -1)) {
// 不是最后位置 并且这个textview也不在第一个
id lastObject = [self.insertObjecArray objectAtIndex:lastIndext];
//布局nextObject
if ([lastObject isKindOfClass:[GADetialTextView class]]) {
[self updateDetialTextViewBottomViewWithNextIndext:nextIndext photo:lastObject];
}else if ([lastObject isKindOfClass:[GAEditPhoto class]]){
[self updateEditPhotoBottomViewWithNextIndext:nextIndext photo:lastObject];
}
//移除数组中的数据
[self.insertObjecArray removeObject:textView];
[textView removeFromSuperview];
}
}
}
}else{
//只有默认textview时 更新scrollrer
GADetialTextView *textView =(GADetialTextView*)[self.defaultTextViewArray firstObject];
CGFloat height = textView.frame.origin.y + textView.frame.size.height;
[self changeTextStrollerContentSizeWithHeight:height];
}
//保证最后一个textview的 placeholder 不显示
if (self.insertObjecArray.count > 1) {
if ([[self.insertObjecArray lastObject] isKindOfClass:[GADetialTextView class]]){
self.placeholder.hidden = YES;
}
}
[textView scrollRangeToVisible:NSMakeRange(0, 0)];
}
到这里这个混排编辑器的主要实现已经介绍完了,但是还有一些键盘的适配的逻辑这里我没有在介绍,这些都是常有的知识,相信大家肯定都知道。具体的效果请戳这里https://github.com/FlyWithCode/ImageAndTextMixed
总结
本文按照自己真实项目中解决问题的思路,介绍了两种IOS文混排编辑器的实现。对比这两种各有优缺点,第一种实现起来会相对简单,我们不需要去管理整个编辑器的编辑逻辑,但是具有局限性。第二种是实现方式可能逻辑比较复杂点但是可扩展性比较好,适用范围比较大。也许大家还有更好的实现方式,可能本人的代码还有很多的可优化点,望留言指出,大家一起探讨交流,共同进步。