最近做了相册浏览器的扩展开发功能,这里感谢缪烨大大,开发了基础功能,我只是站在巨人的肩膀上。这个扩展的核心点在于增加了适配器处理,简单点来说就是将UI显示做到与数据分离,所有的数据来源通过适配器调用,而UI与数据之间使用了一层协议进行隔离,消除后续数据类型变更造成全面修改,增加灵活性。
这里我们首先来看一下整体设计示意图:

这里解释一下:
- 适配代理器指的是当前要自定义适配而实现适配协议的那个代理类,可以根据需要自己指定;
- 所有的数据模型必须实现定义的协议,协议的字段是可选的,根据需要进行转换;
- 数据模型适配器提供了两种解析方式:一是通过适配代理器进行协议方法实现数据转换,二是通过模型自己实现属性值的映射,然后将模型数据扔给适配器就可以自动实现数据模型转换,这两种的本质都是为了将不同模型转换成协议规定的字段;
- 预览展示分栏控制层引用适配器,可以通过更换适配器对象来实现数据的更换,预览视图的变化是根据适配器来的,也就是预览时图的代理数据就是适配器,因为适配器在最外层,也就是我们只需要关心数据变化本身,而不关心内部视图的实现。
实际显示示意图

这里是一个初版的相册浏览器组件,包含了视频和图片的预览,可以看到目前图片可以进行分类展示,也就是我们所说的二维样板,视频在这里展示显示是一个,但是后续会在此基础上根据需求样板进行扩展,将视频做成多视频展示,并且包含视频中自定义视图的兼容,目前在适配器中已提供自定义视图的扩展介入,待确定UI样式类型。
层次结构图

说明:
- FJKPhotoBaseView是基础构建视图,用于提供公用模型数据对接方法
- FJKPreViewModelAdapterProtocol 包含所有的数据规则协议、代理适配协议、预览数据加载协议等
- FJKPreViewModelAdapter 用于提供数据解析及数据传递的适配器
- FJKNewPhotoPreviewViewController 用于进行相册浏览的控制层,后续所有的预览信息入口由该控制器承载
其余的属于定制的视图及控制器,不在核心,这里不予多阐述
具体使用
1.首先确定需要预览的模型实现各自对应的协议,比如二维数组元素协议:
1 2 3 4 5
| @interface FJKAlbumPhotoInfoModel : NSObject<FJKPreviewBaseArrayAdapterProtocol> @property (nonatomic, copy) NSString *tag; @property (nonatomic, assign) NSInteger count; @property (nonatomic, copy) NSArray <FJKAlbumPhotoDetailInfoModel<FJKPicModelAdapterProtocol> *> *photoInfoList; @end
|
1 2 3 4 5
| @interface FJKAlbumVideoModel : NSObject<FJKPreviewBaseArrayAdapterProtocol> @property (nonatomic, copy) NSString *tag; @property (nonatomic, assign) NSInteger count; @property (nonatomic, copy) NSArray <FJKAlbumVideoDetailInfoModel<FJKVedioModelAdapterProtocol> *> *videoInfoList; @end
|
不管是图片的二维数组元素模型还是视频二维数组元素模型都需要实现FJKPreviewBaseArrayAdapterProtocol
这个二维数组协议。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @protocol FJKPicModelAdapterProtocol <FJKPreviewBaseModelAdapterProtocol> @optional @property (nonatomic, copy) NSString * _Nullable url; @property (nonatomic, copy) NSString *_Nullable agentId; @property (nonatomic, copy) NSString *_Nullable agentName; @property (nonatomic, copy) NSString *_Nullable placeholderImage; @property (nonatomic, copy) NSString *_Nullable picId; @property (nonatomic, copy) NSString *_Nullable location; @property (nonatomic, copy) NSString *_Nullable tag; @property (nonatomic, copy) NSString *_Nullable truePhotoDescription; @property (nonatomic, assign) NSInteger approveStatus; @property (nonatomic, copy) NSString *_Nullable createTime; @property (nonatomic, copy) NSString *_Nullable shootTime; @end
|
这是底层一维具体图片信息的协议,也就是二维数组元素FJKAlbumPhotoInfoModel
里面FJKAlbumPhotoDetailInfoModel
模型需要实现该协议,意思就是不管FJKAlbumPhotoDetailInfoModel
是什么样子,最终需要的数据形式都是FJKPicModelAdapterProtocol
的形式。那么一维具体视频信息也类似。
这里FJKPreviewBaseModelAdapterProtocol
数据基础协议,请看里面:
1 2 3 4 5 6
| @protocol FJKPreviewBaseModelAdapterProtocol <NSObject> @optional
- (NSDictionary *_Nullable)exChangeproperties; @end
|
这里只提供一个进行字段转换的映射方法,因为每个模型都可能需要,这个exChangeproperties
协议方法实际上主要是自动解析需要做的字段配置映射,参考MJExtension的模型字段映射就明白了,关于自动适配解析会在下面讲到。
2.适配器的使用
代理自定义适配:
请看下面的例子:
1 2 3 4 5
| FJKPreViewModelAdapter *adapter = [FJKPreViewModelAdapter new] adapter.selectedIndexPath = indexPath adapter.dataSource = self FJKNewPhotoPreviewViewController *previewVC = [FJKNewPhotoPreviewViewController new] previewVC.adapter = adapter
|
其中FJKPreViewModelAdapter
就是适配器,这里的selectedIndexPath表示默认初始预览哪张图或者视频,如果不赋值则默认第一张,这里适配器的dataSource是当前的控制器,那么请看数据源代理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| - (NSArray<NSString *> *_Nullable)numberDataOfTitles { NSMutableArray *titles = [NSMutableArray array]; for (FJKAlbumPhotoInfoModel *model in self.albumModel.photoList) { NSString *title = [NSString stringWithFormat:@"%@(%ld)",model.tag,model.count]; [titles addObject:title]; } return titles; }
- (NSArray<NSObject<FJKPicModelAdapterProtocol> *> *_Nullable)numberDataOfPicModels { return self.albumModel.photoList; }
- (NSArray<NSObject<FJKVedioModelAdapterProtocol> *> *_Nullable)numberDataOfVedioModels{ return self.albumModel.videoList; }
|
大家大概明白是怎么回事了,就是通过dataSource的这三个协议方法将数据载入适配器。然后我们用于展示相册浏览的控制器FJKNewPhotoPreviewViewController
直接赋值adapter就可以了,剩余的事情交给adapter去做就可以了。
自动适配:
请看下面的例子:
1 2 3 4 5 6
| FJKPreViewModelAdapter *adapter = [FJKPreViewModelAdapter new]; adapter.selectedIndexPath = indexPath;
[adapter adapterModel:strongSelf.albumModel.photoList vedios:strongSelf.albumModel.videoList]; FJKNewPhotoPreviewViewController *previewVC = [FJKNewPhotoPreviewViewController new]; previewVC.adapter = adapter;
|
这里就是不使用datasouce方式,而是直接将图片和视频的二维数组模型扔进适配器,难道这样就可以了吗,当然没这么简单,前面不是提到自动适配会有一道数据映射吗,这是协议规定的。该如何定义呢,这里用一维模型FJKAlbumPhotoDetailInfoModel
来进行说明。
1 2 3 4 5 6 7 8 9 10 11 12
| @interface FJKAlbumPhotoDetailInfoModel : NSObject<FJKPicModelAdapterProtocol> @property (nonatomic, copy) NSString *url; @property (nonatomic, copy) NSString *picId; @property (nonatomic, copy) NSString *agentId; @property (nonatomic, copy) NSString *agentName; @property (nonatomic, copy) NSString *location; @property (nonatomic, copy) NSString *tag; @property (nonatomic, copy) NSString *truePhotoDescription; @property (nonatomic, assign) NSInteger approveStatus; @property (nonatomic, copy) NSString *createTime; @property (nonatomic, copy) NSString *shootTime; @end
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @protocol FJKPicModelAdapterProtocol <FJKPreviewBaseModelAdapterProtocol> @optional @property (nonatomic, copy) NSString * _Nullable url; @property (nonatomic, copy) NSString *_Nullable agentId; @property (nonatomic, copy) NSString *_Nullable agentName; @property (nonatomic, copy) NSString *_Nullable placeholderImage; @property (nonatomic, copy) NSString *_Nullable picId; @property (nonatomic, copy) NSString *_Nullable location; @property (nonatomic, copy) NSString *_Nullable tag; @property (nonatomic, copy) NSString *_Nullable truePhotoDescription; @property (nonatomic, assign) NSInteger approveStatus; @property (nonatomic, copy) NSString *_Nullable createTime; @property (nonatomic, copy) NSString *_Nullable shootTime; @end
|
那么目前我们需要将FJKAlbumPhotoDetailInfoModel
中的字段agentId转换到其实现的协议FJKPicModelAdapterProtocol
中的picId,那么也就是项目里面使用picId时实际上使用的是FJKAlbumPhotoDetailInfoModel
模型的agentId字段,我们只需要在FJKAlbumPhotoDetailInfoModel
的implementation里面实现exChangeproperties
协议方法,如下:
1 2 3 4 5
| @implementation FJKAlbumPhotoDetailInfoModel - (NSDictionary *_Nullable)exChangeproperties { return @{@"agentId":@"picId"}; } @end
|
这样适配器就能识别需要进行字段映射操作,进行值的变换。特别是需要进行使用的实际数据模型存在协议中不存在字段时,但是对应的是协议中一个字段,那么就可以通过这方式将其映射进去就可以了。
预览视图的扩展
在适配器里有一个协议FJKPhotoPreviewViewAdapterProtocol
,这里将预览的数据处理交由适配器进行处理了,请看协议:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @class FJKPhotoPreviewView; @protocol FJKPhotoPreviewViewAdapterProtocol <NSObject> @optional
- (NSInteger)numberOfTotalItems:(FJKPhotoPreviewView *_Nullable)photoPreviewView;
- (NSURL *_Nullable)preview:(FJKPhotoPreviewView *_Nullable)photoPreviewView imagePreviewViewAtIndex:(NSUInteger)index;
- (NSArray<NSString *> *_Nullable)previewCategoryTitleViewTitlts:(JXCategoryTitleView *_Nullable)categoryTitleView;
- (NSObject<FJKPicModelAdapterProtocol> * _Nullable)previewModel:(FJKPhotoPreviewView *_Nullable)photoPreviewView cellItemIndex:(NSIndexPath *_Nonnull)indexPath;
- (NSObject<FJKPicModelAdapterProtocol> * _Nullable)preview:(FJKPhotoPreviewView *_Nullable)photoPreviewView cellItemIndex:(NSInteger)currentIndex;
- (NSInteger)preview:(FJKPhotoPreviewView *_Nullable)photoPreviewView didClickSelectedItemAtIndex:(NSInteger)index;
- (NSInteger)preview:(FJKPhotoPreviewView *_Nullable)photoPreviewView indexPath:(NSIndexPath *_Nonnull)indexPath;
- (NSInteger)preview:(FJKPhotoPreviewView *_Nullable)photoPreviewView currentTagIndex:(NSInteger)currentIndex;
- (UIView *_Nullable)previewExtendView:(FJKPhotoPreviewView *_Nullable)photoPreviewView;
@end
|
具体的含义也都有注释,根据注释去填充即可,这里特别注意一个previewExtendView
方法,该方法是提供另外的视图view通过适配器的代理实现,也就是适配中还有一个视图代理方法
1
| @property (nonatomic, weak) id<FJKPreviewViewDelegateAdapterProtocol> delegate;
|
1 2 3 4 5 6
| - (UIView *_Nullable)previewExtendView:(FJKPhotoPreviewView *_Nullable)photoPreviewView { if (self.delegate && [self.delegate respondsToSelector:@selector(previewExtendView)]) { return [self.delegate previewExtendView]; } return nil; }
|
也就是预览中扩展视图的处理是通过适配器FJKPreviewViewDelegateAdapterProtocol
代理来实现的,当然这个协议不是一成不变的,可以不断进行扩展的,但是有一点需要注意,为了不污染内部数据环境,所有的扩展视图的操作和事件都需要在适配器外部完成,适配器不会去处理对应的操作和事件信息。到这里关于扩展的使用说明完成。
总结
- 该扩展目前还需要补充的地方有两点,一是多视频的容器替换处理,二是扩展视图的具体处理,这个要根据两端具体的UI设计进行兼容,后期根据需求不断进行版本维护。
- 在自动解析数据适配方面还需要扩展提供多类型的数据形式解析api。
- 该组件依赖的三方库包括JXCategoryView、QMUIKit、MJExtension。
- 再次感谢缪烨大大的前期工作,为后期的开发打下了基础。