TZPhotoPreviewController实现:iOS图片选择器的预览核心组件解析
引言:为什么需要独立的预览控制器?
在iOS开发中,图片选择功能是许多应用的基础模块。无论是社交分享、头像设置还是内容发布,用户都需要一个流畅直观的图片选择体验。你是否还在为系统UIImagePickerController的功能局限性而困扰?希望实现多选、原图预览、视频播放等高级功能?TZImagePickerController作为一个功能全面的第三方图片选择框架,其核心就在于TZPhotoPreviewController——这个为用户提供沉浸式媒体预览体验的组件。
读完本文,你将能够:
- 理解TZPhotoPreviewController的架构设计与核心功能
- 掌握复杂媒体预览界面的实现技巧
- 学会处理图片选择、预览、裁剪的完整流程
- 解决iCloud同步、原图选择等实际开发痛点
一、TZPhotoPreviewController架构概览
1.1 类定义与核心属性
TZPhotoPreviewController继承自UIViewController,主要负责媒体文件的全屏预览功能,支持图片、GIF和视频的预览,并提供选择、裁剪等交互操作。
@interface TZPhotoPreviewController : UIViewController
@property (nonatomic, strong) NSMutableArray *models; ///< 所有图片模型数组
@property (nonatomic, strong) NSMutableArray *photos; ///< 所有图片数组
@property (nonatomic, assign) NSInteger currentIndex; ///< 用户点击的图片索引
@property (nonatomic, assign) BOOL isSelectOriginalPhoto; ///< 是否返回原图
@property (nonatomic, assign) BOOL isCropImage;
/// 回调Block定义
@property (nonatomic, copy) void (^backButtonClickBlock)(BOOL isSelectOriginalPhoto);
@property (nonatomic, copy) void (^doneButtonClickBlock)(BOOL isSelectOriginalPhoto);
@property (nonatomic, copy) void (^doneButtonClickBlockCropMode)(UIImage *cropedImage,id asset);
@property (nonatomic, copy) void (^doneButtonClickBlockWithPreviewType)(NSArray<UIImage *> *photos,NSArray *assets,BOOL isSelectOriginalPhoto);
@end
1.2 核心组件与层次结构
TZPhotoPreviewController的视图结构采用分层设计,主要包含以下组件:
主要视图层次结构如下:
├── 主视图UIView
│ ├── UICollectionView (媒体预览区域)
│ ├── _naviBar (自定义导航栏)
│ │ ├── 返回按钮(_backButton)
│ │ ├── 选择按钮(_selectButton)
│ │ └── 索引标签(_indexLabel)
│ ├── _toolBar (底部工具栏)
│ │ ├── 完成按钮(_doneButton)
│ │ ├── 原图选择按钮(_originalPhotoButton)
│ │ └── 数量指示器(_numberImageView, _numberLabel)
│ └── 裁剪相关视图
│ ├── _cropBgView (裁剪背景)
│ └── _cropView (裁剪框)
二、初始化与配置流程
2.1 生命周期方法解析
TZPhotoPreviewController的初始化流程遵循标准的UIViewController生命周期,并在此基础上添加了自定义配置:
- (void)viewDidLoad {
[super viewDidLoad];
[TZImageManager manager].shouldFixOrientation = YES;
// 获取导航控制器(TZImagePickerController)实例
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
// 初始化原图选择状态
if (!_didSetIsSelectOriginalPhoto) {
_isSelectOriginalPhoto = _tzImagePickerVc.isSelectOriginalPhoto;
}
// 初始化数据模型
if (!self.models.count) {
self.models = [NSMutableArray arrayWithArray:_tzImagePickerVc.selectedModels];
_assetsTemp = [NSMutableArray arrayWithArray:_tzImagePickerVc.selectedAssets];
}
// 配置各个子组件
[self configCollectionView];
[self configCustomNaviBar];
[self configBottomToolBar];
self.view.clipsToBounds = YES;
// 注册状态栏旋转通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeStatusBarOrientationNotification:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
}
2.2 关键组件配置
2.2.1 集合视图配置(UICollectionView)
UICollectionView是预览控制器的核心,用于展示多个媒体项并支持滑动切换:
- (void)configCollectionView {
_layout = [TZCommonTools tz_rtlFlowLayout];
_layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_layout];
_collectionView.backgroundColor = [UIColor blackColor];
_collectionView.dataSource = self;
_collectionView.delegate = self;
_collectionView.pagingEnabled = YES;
_collectionView.scrollsToTop = NO;
_collectionView.showsHorizontalScrollIndicator = NO;
// 配置布局和约束
_collectionView.contentOffset = CGPointMake(0, 0);
_collectionView.contentSize = CGSizeMake(self.models.count * (self.view.tz_width + 20), 0);
if (@available(iOS 11, *)) {
_collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
[self.view addSubview:_collectionView];
// 注册不同类型的单元格
[_collectionView registerClass:[TZPhotoPreviewCell class] forCellWithReuseIdentifier:@"TZPhotoPreviewCell"];
[_collectionView registerClass:[TZPhotoPreviewCell class] forCellWithReuseIdentifier:@"TZPhotoPreviewCellGIF"];
[_collectionView registerClass:[TZVideoPreviewCell class] forCellWithReuseIdentifier:@"TZVideoPreviewCell"];
[_collectionView registerClass:[TZGifPreviewCell class] forCellWithReuseIdentifier:@"TZGifPreviewCell"];
}
2.2.2 自定义导航栏与工具栏
由于系统导航栏无法满足沉浸式预览需求,TZPhotoPreviewController实现了自定义导航栏和工具栏:
- (void)configCustomNaviBar {
_naviBar = [[UIView alloc] initWithFrame:CGRectZero];
_naviBar.backgroundColor = [UIColor colorWithRed:(34/255.0) green:(34/255.0) blue:(34/255.0) alpha:0.7];
// 配置返回按钮
_backButton = [[UIButton alloc] initWithFrame:CGRectZero];
[_backButton setImage:[UIImage tz_imageNamedFromMyBundle:@"navi_back"] forState:UIControlStateNormal];
[_backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_backButton addTarget:self action:@selector(backButtonClick) forControlEvents:UIControlEventTouchUpInside];
// 配置选择按钮
_selectButton = [[UIButton alloc] initWithFrame:CGRectZero];
[_selectButton setImage:tzImagePickerVc.photoDefImage forState:UIControlStateNormal];
[_selectButton setImage:tzImagePickerVc.photoSelImage forState:UIControlStateSelected];
_selectButton.imageView.clipsToBounds = YES;
_selectButton.imageEdgeInsets = UIEdgeInsetsMake(10, 0, 10, 0);
_selectButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
[_selectButton addTarget:self action:@selector(select:) forControlEvents:UIControlEventTouchUpInside];
// 配置索引标签
_indexLabel = [[UILabel alloc] init];
_indexLabel.adjustsFontSizeToFitWidth = YES;
_indexLabel.font = [UIFont systemFontOfSize:14];
_indexLabel.textColor = [UIColor whiteColor];
_indexLabel.textAlignment = NSTextAlignmentCenter;
// 添加子视图
[_naviBar addSubview:_selectButton];
[_naviBar addSubview:_indexLabel];
[_naviBar addSubview:_backButton];
[self.view addSubview:_naviBar];
}
底部工具栏包含完成按钮、原图选择按钮等关键交互元素,其配置过程与导航栏类似。
三、媒体预览核心实现
3.1 集合视图数据源与代理
UICollectionView的数据源方法实现了媒体项的展示逻辑:
#pragma mark - UICollectionViewDataSource && Delegate
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _models.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
TZAssetModel *model = _models[indexPath.item];
TZAssetPreviewCell *cell;
__weak typeof(self) weakSelf = self;
// 根据媒体类型选择不同的单元格类型
if (_tzImagePickerVc.allowPickingMultipleVideo && model.type == TZAssetModelMediaTypeVideo) {
// 视频预览单元格
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZVideoPreviewCell" forIndexPath:indexPath];
TZVideoPreviewCell *currentCell = (TZVideoPreviewCell *)cell;
currentCell.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
model.iCloudFailed = isSyncFailed;
[weakSelf didICloudSyncStatusChanged:model];
};
} else if (_tzImagePickerVc.allowPickingMultipleVideo && model.type == TZAssetModelMediaTypePhotoGif && _tzImagePickerVc.allowPickingGif) {
// GIF预览单元格
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZGifPreviewCell" forIndexPath:indexPath];
TZGifPreviewCell *currentCell = (TZGifPreviewCell *)cell;
currentCell.previewView.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
model.iCloudFailed = isSyncFailed;
[weakSelf didICloudSyncStatusChanged:model];
};
} else {
// 图片/GIF预览单元格
NSString *reuseId = model.type == TZAssetModelMediaTypePhotoGif ? @"TZPhotoPreviewCellGIF" : @"TZPhotoPreviewCell";
cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseId forIndexPath:indexPath];
TZPhotoPreviewCell *photoPreviewCell = (TZPhotoPreviewCell *)cell;
// 配置裁剪参数
photoPreviewCell.cropRect = _tzImagePickerVc.cropRect;
photoPreviewCell.allowCrop = _tzImagePickerVc.allowCrop;
photoPreviewCell.scaleAspectFillCrop = _tzImagePickerVc.scaleAspectFillCrop;
// 设置图片加载进度回调
__weak typeof(_collectionView) weakCollectionView = _collectionView;
__weak typeof(photoPreviewCell) weakCell = photoPreviewCell;
[photoPreviewCell setImageProgressUpdateBlock:^(double progress) {
__strong typeof(weakSelf) strongSelf = weakSelf;
__strong typeof(weakCollectionView) strongCollectionView = weakCollectionView;
__strong typeof(weakCell) strongCell = weakCell;
strongSelf.progress = progress;
if (progress >= 1) {
if (strongSelf.isSelectOriginalPhoto) [strongSelf showPhotoBytes];
if (strongSelf.alertView && [strongCollectionView.visibleCells containsObject:strongCell]) {
[strongSelf.alertView dismissViewControllerAnimated:YES completion:^{
strongSelf.alertView = nil;
[strongSelf doneButtonClick];
}];
}
}
}];
// iCloud同步失败处理
photoPreviewCell.previewView.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
model.iCloudFailed = isSyncFailed;
[weakSelf didICloudSyncStatusChanged:model];
};
}
// 配置单元格数据和回调
cell.model = model;
[cell setSingleTapGestureBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf didTapPreviewCell];
}];
return cell;
}
上述代码根据媒体类型(图片、GIF、视频)返回不同的预览单元格,实现了多类型媒体的统一预览接口。
3.2 滑动切换与索引更新
通过UIScrollViewDelegate实现滑动切换时的索引更新:
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offSetWidth = scrollView.contentOffset.x;
offSetWidth = offSetWidth + ((self.view.tz_width + 20) * 0.5);
NSInteger currentIndex = offSetWidth / (self.view.tz_width + 20);
if (currentIndex < _models.count && _currentIndex != currentIndex) {
_currentIndex = currentIndex;
[self refreshNaviBarAndBottomBarState];
}
[[NSNotificationCenter defaultCenter] postNotificationName:@"photoPreviewCollectionViewDidScroll" object:nil];
}
refreshNaviBarAndBottomBarState方法会根据当前索引更新导航栏和工具栏的状态,如选择按钮状态、索引标签文本等。
四、交互功能实现
4.1 选择功能
选择按钮的点击事件处理是预览控制器的核心交互之一:
- (void)select:(UIButton *)selectButton {
[self select:selectButton refreshCount:YES];
}
- (void)select:(UIButton *)selectButton refreshCount:(BOOL)refreshCount {
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
TZAssetModel *model = _models[self.currentIndex];
if (!selectButton.isSelected) {
// 检查是否超过最大选择数量
if (_tzImagePickerVc.selectedModels.count >= _tzImagePickerVc.maxImagesCount) {
NSString *title = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Select a maximum of %zd photos"], _tzImagePickerVc.maxImagesCount];
[_tzImagePickerVc showAlertWithTitle:title];
return;
} else {
// 检查资产是否可以选择
if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) {
return;
}
// 添加到选中模型数组
[_tzImagePickerVc addSelectedModel:model];
[self setAsset:model.asset isSelect:YES];
// 更新选中图片数组
if (self.photos) {
[_tzImagePickerVc.selectedAssets addObject:_assetsTemp[self.currentIndex]];
[self.photos addObject:_photosTemp[self.currentIndex]];
}
// 视频选择提示
if (model.type == TZAssetModelMediaTypeVideo && !_tzImagePickerVc.allowPickingMultipleVideo) {
[_tzImagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Select the video when in multi state, we will handle the video as a photo"]];
}
}
} else {
// 取消选择逻辑
NSArray *selectedModels = [NSArray arrayWithArray:_tzImagePickerVc.selectedModels];
for (TZAssetModel *model_item in selectedModels) {
if ([model.asset.localIdentifier isEqualToString:model_item.asset.localIdentifier]) {
// 移除选中模型
NSArray *selectedModelsTmp = [NSArray arrayWithArray:_tzImagePickerVc.selectedModels];
for (NSInteger i = 0; i < selectedModelsTmp.count; i++) {
TZAssetModel *model = selectedModelsTmp[i];
if ([model isEqual:model_item]) {
[_tzImagePickerVc removeSelectedModel:model];
break;
}
}
// 更新选中资产数组
if (self.photos) {
NSArray *selectedAssetsTmp = [NSArray arrayWithArray:_tzImagePickerVc.selectedAssets];
for (NSInteger i = 0; i < selectedAssetsTmp.count; i++) {
id asset = selectedAssetsTmp[i];
if ([asset isEqual:_assetsTemp[self.currentIndex]]) {
[_tzImagePickerVc.selectedAssets removeObjectAtIndex:i];
break;
}
}
[self.photos removeObject:_photosTemp[self.currentIndex]];
}
[self setAsset:model.asset isSelect:NO];
break;
}
}
}
// 更新模型选择状态
model.isSelected = !selectButton.isSelected;
if (refreshCount) {
[self refreshNaviBarAndBottomBarState];
}
// 添加选择动画
if (model.isSelected) {
[UIView showOscillatoryAnimationWithLayer:selectButton.imageView.layer type:TZOscillatoryAnimationToBigger];
}
[UIView showOscillatoryAnimationWithLayer:_numberImageView.layer type:TZOscillatoryAnimationToSmaller];
}
4.2 原图选择功能
原图选择按钮的实现考虑了文件大小计算、状态切换等细节:
- (void)originalPhotoButtonClick {
TZAssetModel *model = _models[self.currentIndex];
if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) {
return;
}
// 切换选中状态
_originalPhotoButton.selected = !_originalPhotoButton.isSelected;
_isSelectOriginalPhoto = _originalPhotoButton.isSelected;
_originalPhotoLabel.hidden = !_originalPhotoButton.isSelected;
if (_isSelectOriginalPhoto) {
// 显示文件大小
[self showPhotoBytes];
// 如果未选择当前图片且未达到最大选择数,则自动选择
if (!_selectButton.isSelected) {
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
if (_tzImagePickerVc.selectedModels.count < _tzImagePickerVc.maxImagesCount && _tzImagePickerVc.showSelectBtn) {
[self select:_selectButton];
}
}
}
}
// 计算并显示原图大小
- (void)showPhotoBytes {
TZAssetModel *model = _models[self.currentIndex];
[[TZImageManager manager] getAssetSize:model.asset completion:^(NSString *sizeStr) {
self->_originalPhotoLabel.text = sizeStr;
}];
}
4.3 裁剪功能
裁剪功能的实现涉及裁剪框绘制、图片处理等复杂逻辑:
- (void)configCropView {
TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController;
if (_tzImagePickerVc.maxImagesCount <= 1 && _tzImagePickerVc.allowCrop && _tzImagePickerVc.allowPickingImage) {
// 移除旧的裁剪视图
[_cropView removeFromSuperview];
[_cropBgView removeFromSuperview];
// 创建裁剪背景
_cropBgView = [UIView new];
_cropBgView.userInteractionEnabled = NO;
_cropBgView.frame = self.view.bounds;
_cropBgView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_cropBgView];
// 绘制裁剪区域外的半透明遮罩
[TZImageCropManager overlayClippingWithView:_cropBgView cropRect:_tzImagePickerVc.cropRect containerView:self.view needCircleCrop:_tzImagePickerVc.needCircleCrop];
// 创建裁剪框
_cropView = [UIView new];
_cropView.userInteractionEnabled = NO;
_cropView.frame = _tzImagePickerVc.cropRect;
_cropView.backgroundColor = [UIColor clearColor];
_cropView.layer.borderColor = [UIColor whiteColor].CGColor;
_cropView.layer.borderWidth = 1.0;
// 圆形裁剪
if (_tzImagePickerVc.needCircleCrop) {
_cropView.layer.cornerRadius = _tzImagePickerVc.cropRect.size.width / 2;
_cropView.clipsToBounds = YES;
}
[self.view addSubview:_cropView];
// 裁剪视图自定义配置
if (_tzImagePickerVc.cropViewSettingBlock) {
_tzImagePickerVc.cropViewSettingBlock(_cropView);
}
// 确保裁剪视图在最上层
[self.view bringSubviewToFront:_naviBar];
[self.view bringSubviewToFront:_toolBar];
}
}
完成按钮点击时的裁剪处理:
- (void)doneButtonClick {
// ... 其他逻辑 ...
// 裁剪模式处理
if (_tzImagePickerVc.allowCrop && [cell isKindOfClass:[TZPhotoPreviewCell class]]) {
_doneButton.enabled = NO;
[_tzImagePickerVc showProgressHUD];
// 执行裁剪
UIImage *cropedImage = [TZImageCropManager cropImageView:cell.previewView.imageView toRect:_tzImagePickerVc.cropRect zoomScale:cell.previewView.scrollView.zoomScale containerView:self.view];
// 圆形裁剪处理
if (_tzImagePickerVc.needCircleCrop) {
cropedImage = [TZImageCropManager circularClipImage:cropedImage];
}
_doneButton.enabled = YES;
[_tzImagePickerVc hideProgressHUD];
// 通过Block返回裁剪结果
if (self.doneButtonClickBlockCropMode) {
TZAssetModel *model = _models[self.currentIndex];
self.doneButtonClickBlockCropMode(cropedImage,model.asset);
}
}
// ... 其他逻辑 ...
}
五、高级功能与优化
5.1 iCloud同步处理
针对iCloud照片的同步问题,实现了进度反馈和错误处理:
// 在单元格配置中设置iCloud同步失败处理
currentCell.previewView.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) {
model.iCloudFailed = isSyncFailed;
[weakSelf didICloudSyncStatusChanged:model];
};
// 图片加载进度更新Block
[photoPreviewCell setImageProgressUpdateBlock:^(double progress) {
__strong typeof(weakSelf) strongSelf = weakSelf;
__strong typeof(weakCollectionView) strongCollectionView = weakCollectionView;
__strong typeof(weakCell) strongCell = weakCell;
strongSelf.progress = progress;
if (progress >= 1) {
if (strongSelf.isSelectOriginalPhoto) [strongSelf showPhotoBytes];
if (strongSelf.alertView && [strongCollectionView.visibleCells containsObject:strongCell]) {
[strongSelf.alertView dismissViewControllerAnimated:YES completion:^{
strongSelf.alertView = nil;
[strongSelf doneButtonClick];
}];
}
}
}];
5.2 横竖屏适配
通过监听状态栏旋转通知实现布局自适应:
- (void)didChangeStatusBarOrientationNotification:(NSNotification *)noti {
_offsetItemCount = _collectionView.contentOffset.x / _layout.itemSize.width;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 处理导航栏布局
CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0;
CGFloat statusBarHeightInterval = isFullScreen ? (statusBarHeight - 20) : 0;
CGFloat naviBarHeight = statusBarHeight + _tzImagePickerVc.navigationBar.tz_height;
_naviBar.frame = CGRectMake(0, 0, self.view.tz_width, naviBarHeight);
// 处理集合视图布局
_layout.itemSize = CGSizeMake(self.view.tz_width + 20, self.view.tz_height);
_layout.minimumInteritemSpacing = 0;
_layout.minimumLineSpacing = 0;
_collectionView.frame = CGRectMake(-10, 0, self.view.tz_width + 20, self.view.tz_height);
[_collectionView setCollectionViewLayout:_layout];
// 处理工具栏布局
CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom;
CGFloat toolBarTop = self.view.tz_height - toolBarHeight;
_toolBar.frame = CGRectMake(0, toolBarTop, self.view.tz_width, toolBarHeight);
// 重新配置裁剪视图
[self configCropView];
// 自定义布局回调
if (_tzImagePickerVc.photoPreviewPageDidLayoutSubviewsBlock) {
_tzImagePickerVc.photoPreviewPageDidLayoutSubviewsBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel);
}
}
六、总结与最佳实践
6.1 TZPhotoPreviewController的设计亮点
- 分层架构:将导航栏、预览区、工具栏等拆分为独立模块,职责清晰
- 多类型支持:统一接口处理图片、GIF、视频等多种媒体类型
- 交互体验优化:添加选择动画、平滑过渡等细节处理
- 可扩展性设计:通过Block回调和配置Block支持自定义需求
- 性能优化:实现图片懒加载、iCloud同步状态管理等
6.2 实际开发中的注意事项
- 内存管理:注意大图片预览时的内存占用,及时释放不再需要的资源
- 错误处理:完善iCloud同步失败、权限不足等异常场景的用户提示
- 性能优化:对于大量图片预览,考虑实现预加载和回收机制
- 用户体验:保持交互一致性,添加适当的加载状态和反馈
6.3 未来改进方向
- Swift重构:目前代码为Objective-C实现,可考虑迁移到Swift
- 暗黑模式支持:增加对iOS暗黑模式的完整支持
- 视频编辑功能:扩展视频预览为完整的视频编辑功能
- 性能监控:添加性能指标收集,优化卡顿问题
TZPhotoPreviewController作为TZImagePickerController的核心组件,展示了如何构建一个功能完善、用户体验优秀的媒体预览系统。通过分层设计、接口抽象和细致的交互处理,实现了复杂媒体预览场景的优雅解决方案。开发者可以借鉴其设计思想,构建更强大的媒体处理功能。
要开始使用TZImagePickerController,请克隆仓库:git clone https://gitcode.com/gh_mirrors/tz/TZImagePickerController
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



