前言
应用场景:上传和展示多张图片的场景,比如风险商户处理、发布商品图片
技术特点:使用UICollectionViewCell、UITableViewCell 控件进行搭建,使用Masonry 框架布局,采用MVVM结构。
点击文末原文下载demo:https://download.csdn.net/download/u011018979/15868813
I、 使用方法
先配置相册访问权限key
NSPhotoLibraryUsageDescription
The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data.
1.1 初始化 cell
- cell
case ERPRelease_commoditiesViewSection4UploadPic:{ return [ERPcomposePhotosTableViewCell tableViewCellWithTableView:tableView block:^(id _Nonnull sender) { } models:self.viewModel.Model4UploadPictures]; }break;
- 上传图片界面的初始模型数据
#pragma mark - ******** 上传图片界面的初始模型数据 + (NSMutableArray*)getModel4ADDUploadPicturesWithBlock:(void (^)(id sender))block { NSMutableArray *tmpD = @[ @{@"block":block,@"imgName":@"img_zhuece_tianjia",@"imageType":[NSNumber numberWithInt:ERPimageType4name],@"type":[NSNumber numberWithInt:QCTCollectionModelType4UploadPicturesAddIcon],@"isHiddenDelBtn":@1}, ]; NSMutableArray *tmp = [[self class] mj_objectArrayWithKeyValuesArray:tmpD]; return tmp; }
- 处理上传图片逻辑
- (void)Model4UploadPictures{ __weak __typeof__(self) weakSelf = self; self.viewModel.Model4UploadPictures = [QCTCollectionModel getModel4ADDUploadPicturesWithBlock:^(UISwitch* sender) { [weakSelf setupChooseimage];//上传图片 } ]; }
1.2 初始化 cellView
ERPcomposePhotosV4UploadPictures
- (ERPcomposePhotosV4UploadPictures *)cellView{ if (nil == _cellView) { ERPcomposePhotosV4UploadPictures *tmpView = [[ERPcomposePhotosV4UploadPictures alloc]init]; _cellView = tmpView; [tmpView setBackgroundColor:kcellColor]; [self.contentView addSubview:tmpView]; __weak __typeof__(self) weakSelf = self; [tmpView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(weakSelf.contentView).offset(kAdjustRatio(20)); make.right.equalTo(weakSelf.contentView).offset(- kAdjustRatio(20)); make.top.equalTo(weakSelf.contentView).offset(kAdjustRatio(0)); make.bottom.equalTo(weakSelf.contentView).offset(- kAdjustRatio(0)); }]; } return _cellView; }
1.3 约束的设置
根据模型数据,更新视图高度
mas_updateConstraints 计算宽度的时候采用UIScreen进行屏幕宽度的获取
- (void)setModels:( NSMutableArray*)models{ _models =models; self.cellView.models = models; NSInteger maxclos= 3;// 列数 CGFloat margin = 10; // // [self.cellView layoutIfNeeded]; // CGFloat w = (self.cellView.frame.size.width - margin*(maxclos-1))/maxclos; CGFloat cellViewW =kWidth-kAdjustRatio(20*2) ;//计算宽度的时候采用UIScreen进行屏幕宽度的获取 CGFloat w = (cellViewW- margin*(maxclos-1))/maxclos; CGFloat cell_H = w*(1);//宽高比 // NSLog(@"cell_H:%f",cell_H);// NSInteger row = [QCT_Common getRowWithCount:models.count clos:maxclos]; [self.cellView mas_updateConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(kAdjustRatio(cell_H*row + (row -1)*10 )); }]; }
设置sizeForItemAtIndexPath
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGSize size; NSInteger maxclos= 3;// 列数 CGFloat margin = 10; CGFloat w = (self.frame.size.width - margin*(maxclos-1))/maxclos; CGFloat cell_H = w*(1);//宽高比 123/234 size = CGSizeMake(cell_H, cell_H); return size; }
初始化collectionView,并设置每一行之间的间距
- (UICollectionView *)collectionView { if (_collectionView == nil) { UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; // 2.设置整个collectionView的内边距 //分别为上、左、下、右 flowLayout.sectionInset = UIEdgeInsetsMake(kAdjustRatio(0),kAdjustRatio(0),kAdjustRatio(0),kAdjustRatio(0)); //.设置每一行之间的间距 flowLayout.minimumLineSpacing = kAdjustRatio(10); flowLayout.minimumInteritemSpacing = 0; // flowLayout.itemSize = CGSizeMake((SCREEN_WIDTH-3*kAdjustRatio(10))/3.0, self.optionsView.height); _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.showsVerticalScrollIndicator = NO; _collectionView.bounces = NO; _collectionView.dataSource = self; _collectionView.delegate = self; [_collectionView registerClass:[ERPUploadPicturesUICollectionViewCell class] forCellWithReuseIdentifier:@"ERPUploadPicturesUICollectionViewCell"]; if (@available(iOS 11.0, *)) { _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { // Fallback on earlier versions } _collectionView.scrollEnabled = NO; // UICollectionViewScrollDirectionHorizontal __weak __typeof__(self) weakSelf = self; [self addSubview:_collectionView]; [_collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.top.bottom.right.equalTo(weakSelf); }]; } return _collectionView; }
1.4 处理图片添加成功的用法举例
- (void)UpdateImageToViewWithurl:(NSString*)picurl{ __weak __typeof__(self) weakSelf = self; // 新增模型 QCTCollectionModel *tm = [QCTCollectionModel new]; tm.type = QCTCollectionModelType4ShowUploadPictures; tm.imageType = ERPimageType4url; tm.picurl = picurl; [tm setDelblock:^(QCTCollectionModel* sender) { [weakSelf.viewModel.Model4UploadPictures removeObject:sender]; //判断是否已经包含添加图片视图,如果没有,且数量小于上传的最大张数,则显示添加按钮 if(self.viewModel.Model4UploadPictures.count<maxcount4UploadPicturesInRelease_commodities && ( ![QCTCollectionModel idContainsUploadPicturesAddIconWithArr:self.viewModel.Model4UploadPictures])){// 处理最大数量 [weakSelf.viewModel.Model4UploadPictures addObject:[QCTCollectionModel getModel4ADDUploadPicturesWithBlock:^(UISwitch* sender) { [weakSelf setupChooseimage]; } ].firstObject]; } [weakSelf.viewModel.reloadProductFileUploadSubject sendNext:nil]; }]; [tm setBlock:^(id _Nonnull sender) { }]; [self.viewModel.Model4UploadPictures insertObject:tm atIndex:self.viewModel.Model4UploadPictures.count-1]; if(self.viewModel.Model4UploadPictures.count>maxcount4UploadPicturesInRelease_commodities){// 处理最大数量 [self.viewModel.Model4UploadPictures removeLastObject]; } // 刷新视图 [self.viewModel.reloadProductFileUploadSubject sendNext:nil]; }
II、核心代码
2.0 处理是否已经包含添加图片视图
2.0.1 判断是否已经包含添加图片视图
+ (BOOL)idContainsUploadPicturesAddIconWithArr:(NSArray*)arr{ NSPredicate* predicate = [NSPredicate predicateWithFormat:@"type == %d",QCTCollectionModelType4UploadPicturesAddIcon]; NSArray *tmparr = [arr filteredArrayUsingPredicate:predicate]; if(tmparr.count>0){ return YES; } return NO; }
2.0.2 添加按钮数据模型
+ (NSMutableArray*)getModel4ADDUploadPicturesWithBlock:(void (^)(id sender))block { NSMutableArray *tmpD = @[ @{@"block":block,@"imgName":@"img_zhuece_tianjia",@"imageType":[NSNumber numberWithInt:ERPimageType4name],@"type":[NSNumber numberWithInt:QCTCollectionModelType4UploadPicturesAddIcon],@"isHiddenDelBtn":@1}, ]; NSMutableArray *tmp = [[self class] mj_objectArrayWithKeyValuesArray:tmpD]; return tmp; }
2.0.3 判断是否已经包含添加图片视图,如果没有,且数量小于上传的最大张数,则显示添加按钮
[tm setDelblock:^(QCTCollectionModel* sender) { [weakSelf.viewModel.Model4UploadPictures removeObject:sender]; //判断是否已经包含添加图片视图,如果没有,且数量小于上传的最大张数,则显示添加按钮 if(self.viewModel.Model4UploadPictures.count<maxcount4UploadPicturesInRelease_commodities && ( ![QCTCollectionModel idContainsUploadPicturesAddIconWithArr:self.viewModel.Model4UploadPictures])){// 处理最大数量 [weakSelf.viewModel.Model4UploadPictures addObject:[QCTCollectionModel getModel4ADDUploadPicturesWithBlock:^(UISwitch* sender) { [weakSelf setupChooseimage]; } ].firstObject]; } [weakSelf.viewModel.reloadProductFileUploadSubject sendNext:nil]; }];
2.1 ShowImageView 显示图片的视图
2.1.1 .h
#import <UIKit/UIKit.h> #import "QCTCollectionModel.h" #define KNDeleteH 7 //删除按钮的高度的一半 NS_ASSUME_NONNULL_BEGIN /** 显示图片的视图 */ @interface ERPShowImageView : UIView @property (nonatomic,strong) QCTCollectionModel *model; @property (nonatomic,weak) UIImageView *imageView; @property (nonatomic,weak) UIButton *deleteBtn; @end
2.1.2.m
#import "ERPShowImageView.h" @implementation ERPShowImageView - (UIImageView *)imageView{ if (nil == _imageView) { UIImageView *tmpView = [[UIImageView alloc]init]; _imageView = tmpView; tmpView.contentMode = UIViewContentModeScaleAspectFill; tmpView.clipsToBounds = YES; [self addSubview:_imageView]; __weak __typeof__(self) weakSelf = self; [tmpView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.offset(kAdjustRatio(KNDeleteH)); make.left.offset(kAdjustRatio(KNDeleteH)); make.right.offset(kAdjustRatio(-KNDeleteH)); make.bottom.offset(kAdjustRatio(-KNDeleteH)); }]; tmpView.userInteractionEnabled = YES; UITapGestureRecognizer *cutTap = [[UITapGestureRecognizer alloc] init]; // __weak __typeof__(self) weakSelf = self; [[cutTap rac_gestureSignal] subscribeNext:^(id x) { if(weakSelf.model.type != QCTCollectionModelType4UploadPicturesAddIcon ){ return ; } NSLog(@" 上传图片 "); if (weakSelf.model.block) { weakSelf.model.block(weakSelf.model); } }]; [tmpView addGestureRecognizer:cutTap]; } return _imageView; } - (UIButton *)deleteBtn{ if (nil == _deleteBtn) { UIButton *tmpView = [[UIButton alloc]init]; _deleteBtn = tmpView; [tmpView addTarget:self action:@selector(clickDeleteBtn) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:_deleteBtn]; // __weak __typeof__(self) weakSelf = self; [tmpView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(weakSelf.imageView.mas_top); make.centerX.equalTo(weakSelf.imageView.mas_right); make.width.height.mas_equalTo(kAdjustRatio(2*KNDeleteH)); }]; } return _deleteBtn; } - (void)clickDeleteBtn{ // [self removeFromSuperview]; if(self.model.delblock){ self.model.delblock(self.model); } } - (instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; NSLog(@"kninitWithFrame"); if (self) { //构建子控件 [self setupSubviews]; } return self; } - (void)setupSubviews{ NSLog(@"setupSubviews"); self.imageView.hidden = NO; [self.deleteBtn setImage:[self imageWithImageName:@"icon_xinzengmendian_shanchu.png"] forState:UIControlStateNormal]; } - (UIImage*)imageWithImageName:(NSString*)name{ return [UIImage imageNamed:name]; // } - (void)layoutSubviews{ [super layoutSubviews]; // self.imageView.frame = self.bounds; [self layoutIfNeeded]; } - (void)setModel:(QCTCollectionModel *)model{ _model = model; // tm.imageType = ; switch (model.imageType) { case ERPimageType4name: { self.imageView.image = [UIImage imageNamed:model.imgName]; } break; case ERPimageType4url: { [self.imageView sd_setImageWithURL:[NSURL URLWithString:model.picurl] placeholderImage:[UIImage imageNamed:@"占位"]]; } break; default: break; } self.deleteBtn.hidden = model.isHiddenDelBtn; }
2.2 数据模型 QCTCollectionModel
typedef enum : NSUInteger { ERPimageType4name, ERPimageType4url, ERPimageType4Uiimage, } ERPimageType; @property (nonatomic,assign) ERPimageType imageType; // @property (nonatomic , copy) NSString *picurl; /** 默认NO 显示删除按钮 */ @property (nonatomic,assign) BOOL isHiddenDelBtn; @property (nonatomic,copy) NSString *imgName;
2.3 完整Demo下载
demo源码下载:https://download.csdn.net/download/u011018979/15868813
- 应用场景:上传和展示多张图片的场景,比如风险商户处理、发布商品图
- 技术特点:使用UICollectionViewCell、UITableViewCell 控件进行搭建,使用Masonry 框架布局,采用MVVM结构。
III、注意事项
3.1 QMUIKit在iOS14 下首次唤起键盘卡住主线程的解决方案
iOS14 下首次唤起键盘卡住主线程
Main Thread Checker: UI API called on a background thread: -[UIWindow windowScene]
================================================================= Main Thread Checker: UI API called on a background thread: -[UIWindow windowScene] PID: 580, TID: 21138, Thread name: (none), Queue name: com.apple.root.user-initiated-qos, QoS: 25 Backtrace: 4 retail 0x000000010576b628 __62+[UIWindow(QMUIUserInterfaceStyleWillChangeNotification) load]_block_invoke_3 + 296 Main Thread Checker: UI API called on a background thread: -[UIWindow traitCollection] PID: 509, TID: 22376, Thread name: (none), Queue name: com.apple.root.user-initiated-qos, QoS: 25 Backtrace: 4 Housekeeper 0x0000000100f3c000 __62+[UIWindow(QMUIUserInterfaceStyleWillChangeNotification) load]_block_invoke_3 + 92
- 解决方案:如果你没使用QMUITheme,就直接注释掉代码即可。
如果你使用QMUITheme,则及时你更新4.2.1版本也无法根本性解决
这是因为系统自己在子线程访问了这些方法,只是 Main Thread Checker 对其做了兼容,发现 App 自己修改了这些方法的实现,才报错,没修改则不报错。检测方式可以打条件符号断点,然后把 QMUI 那段代码注释掉,运行起来后会发现依然能命中这个断点,说明系统自身确实是在子线程访问了(UIKit 这种行为特别多,不只是这里)。所以从原理上看,QMUI 命中这个主线程检测是不可避免的,目前只是做了一些优化,只有真正使用了 QMUITheme 组件时才会出现这个情况,没使用的时候就不会命中,以减少一部分的出错场景。这个优化将会跟随 4.2.1 版本发布。