Repository: banchichen/TZImagePickerController Branch: master Commit: 87b704b1eadf Files: 98 Total size: 638.0 KB Directory structure: gitextract_nmz_btnz/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── -------.md │ ├── ----.md │ └── --bug.md ├── .gitignore ├── .swiftpm/ │ └── xcode/ │ └── package.xcworkspace/ │ └── contents.xcworkspacedata ├── LICENSE ├── Package.swift ├── README.md ├── TZImagePickerController/ │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets/ │ │ ├── AlbumAddBtn.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── back.imageset/ │ │ │ └── Contents.json │ │ ├── leftVideoEdit.imageset/ │ │ │ └── Contents.json │ │ ├── photo_delete.imageset/ │ │ │ └── Contents.json │ │ └── rightVideoEdit.imageset/ │ │ └── Contents.json │ ├── Base.lproj/ │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── FLAnimatedImage/ │ │ ├── FLAnimatedImage.h │ │ ├── FLAnimatedImage.m │ │ ├── FLAnimatedImageView.h │ │ └── FLAnimatedImageView.m │ ├── Info.plist │ ├── Location/ │ │ ├── TZLocationManager.h │ │ └── TZLocationManager.m │ ├── LxGridViewFlowLayout.h │ ├── LxGridViewFlowLayout.m │ ├── Resources/ │ │ └── TZImagePickerController.bundle/ │ │ ├── ar.lproj/ │ │ │ └── Localizable.strings │ │ ├── de.lproj/ │ │ │ └── Localizable.strings │ │ ├── en.lproj/ │ │ │ └── Localizable.strings │ │ ├── es.lproj/ │ │ │ └── Localizable.strings │ │ ├── fr.lproj/ │ │ │ └── Localizable.strings │ │ ├── ja.lproj/ │ │ │ └── Localizable.strings │ │ ├── ko-KP.lproj/ │ │ │ └── Localizable.strings │ │ ├── pt.lproj/ │ │ │ └── Localizable.strings │ │ ├── ru.lproj/ │ │ │ └── Localizable.strings │ │ ├── vi.lproj/ │ │ │ └── Localizable.strings │ │ ├── zh-Hans.lproj/ │ │ │ └── Localizable.strings │ │ └── zh-Hant.lproj/ │ │ └── Localizable.strings │ ├── TZImagePickerController/ │ │ ├── NSBundle+TZImagePicker.h │ │ ├── NSBundle+TZImagePicker.m │ │ ├── TZAssetCell.h │ │ ├── TZAssetCell.m │ │ ├── TZAssetModel.h │ │ ├── TZAssetModel.m │ │ ├── TZAuthLimitedFooterTipView.h │ │ ├── TZAuthLimitedFooterTipView.m │ │ ├── TZGifPhotoPreviewController.h │ │ ├── TZGifPhotoPreviewController.m │ │ ├── TZImageCropManager.h │ │ ├── TZImageCropManager.m │ │ ├── TZImageManager.h │ │ ├── TZImageManager.m │ │ ├── TZImagePickerController.h │ │ ├── TZImagePickerController.m │ │ ├── TZImageRequestOperation.h │ │ ├── TZImageRequestOperation.m │ │ ├── TZPhotoPickerController.h │ │ ├── TZPhotoPickerController.m │ │ ├── TZPhotoPreviewCell.h │ │ ├── TZPhotoPreviewCell.m │ │ ├── TZPhotoPreviewController.h │ │ ├── TZPhotoPreviewController.m │ │ ├── TZProgressView.h │ │ ├── TZProgressView.m │ │ ├── TZVideoCropController.h │ │ ├── TZVideoCropController.m │ │ ├── TZVideoEditedPreviewController.h │ │ ├── TZVideoEditedPreviewController.m │ │ ├── TZVideoPlayerController.h │ │ ├── TZVideoPlayerController.m │ │ ├── UIView+TZLayout.h │ │ └── UIView+TZLayout.m │ ├── TZImageUploadOperation.h │ ├── TZImageUploadOperation.m │ ├── TZTestCell.h │ ├── TZTestCell.m │ ├── ViewController.h │ ├── ViewController.m │ ├── ar-001.lproj/ │ │ ├── LaunchScreen.strings │ │ ├── Localizable.strings │ │ └── Main.strings │ ├── main.m │ ├── tz-ru.lproj/ │ │ └── Localizable.strings │ └── zh-Hans.lproj/ │ ├── LaunchScreen.strings │ └── Main.strings ├── TZImagePickerController.podspec ├── TZImagePickerController.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── TZImagePickerControllerFramework.xcscheme ├── TZImagePickerControllerFramework/ │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── TZImagePickerControllerTests/ │ ├── Info.plist │ └── TZImagePickerControllerTests.m └── TZImagePickerControllerUITests/ ├── Info.plist └── TZImagePickerControllerUITests.m ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/-------.md ================================================ --- name: 希望添加新功能 about: 建议添加XX功能 --- **问:你希望加什么新功能?** 答: **问:微信、QQ等大APP的选择器,是否有这个功能?** 答: **问:这个功能是否适合所有人?如不是,是否要加个属性开关?** 答: **其它你希望补充的内容** ================================================ FILE: .github/ISSUE_TEMPLATE/----.md ================================================ --- name: 其它问题 about: 使用问题咨询等等 --- **咨询前必看** 请先回答下列三个问题,否则不允处理,谢谢配合。 1、我最新的Demo是否有这个bug?【**如果Demo没问题,请升级新版**】 答: 2、你用的是什么版本?升级到最新版后是否正常? 答: 3、是否有改动过我库内部的代码?【**如有,请说明改动点**】 答: ================================================ FILE: .github/ISSUE_TEMPLATE/--bug.md ================================================ --- name: 提交Bug about: 发现一个bug或疑似bug --- **提bug前必看** 请先回答下列三个问题,否则不允处理,谢谢配合。 1、我最新的Demo是否有这个bug?【**如果Demo没问题,请升级新版**】 答: 2、你用的是什么版本?升级到最新版后是否正常? 答: 3、是否有改动过我库内部的代码?【**如有,请说明改动点**】 答: **bug内容描述** **我如何复现这个bug?** **截图** **其它说明** 有没有其它要补充的?比如你的初始化TZImagePickerController的代码 ================================================ FILE: .gitignore ================================================ # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate *.build # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # #Pods/ .DS_Store ================================================ FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Zhen Tan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "TZImagePickerController", defaultLocalization: "en", platforms: [.iOS(.v10)], products: [ .library( name: "TZImagePickerController", targets: ["TZImagePickerController"]), ], dependencies: [], targets: [ .target( name: "TZImagePickerController", path: ".", sources: [ "TZImagePickerController/TZImagePickerController", "TZImagePickerController/Location" ], resources: [ .process("TZImagePickerController/Resources/TZImagePickerController.bundle"), .process("TZImagePickerControllerFramework/PrivacyInfo.xcprivacy") ], publicHeadersPath: "TZImagePickerController/TZImagePickerController", linkerSettings: [ .linkedFramework("Photos"), .linkedFramework("PhotosUI") ] ) ] ) ================================================ FILE: README.md ================================================ # TZImagePickerController [![CocoaPods](https://img.shields.io/cocoapods/v/TZImagePickerController.svg?style=flat)](https://github.com/banchichen/TZImagePickerController) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) A clone of UIImagePickerController, support picking multiple photos、original photo、video, also allow preview photo and video, support iOS10+. 一个支持多选、选原图和视频的图片选择器,同时有预览功能,支持iOS10+。 ## 重要提示1:提issue前,请先对照Demo、常见问题自查!Demo正常说明你可以升级下新版试试。 ## 重要提示2:3.8.8版本修复了iOS18下无照片和openURL失效的问题 关于iOS14模拟器的问题 PHAuthorizationStatusLimited授权模式下,iOS14模拟器有bug,未授权照片无法显示,真机正常,暂可忽略:https://github.com/banchichen/TZImagePickerController/issues/1347 关于升级iOS10和Xcdoe8的提示: 在Xcode8环境下将项目运行在iOS10的设备/模拟器中,访问相册和相机需要额外配置info.plist文件。分别是Privacy - Photo Library Usage Description和Privacy - Camera Usage Description字段,详见Demo中info.plist中的设置。 项目截图 1.Demo首页 2.照片列表页 3.照片预览页 4.视频预览页 ## 一. Installation 安装 #### CocoaPods > pod 'TZImagePickerController' # Full version with all features > pod 'TZImagePickerController/Basic' # No location code #### Carthage > github "banchichen/TZImagePickerController" #### 手动安装 > 将TZImagePickerController文件夹拽入项目中,导入头文件:#import "TZImagePickerController.h" ## 二. Example 例子 TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:9 delegate:self]; // You can get the photos by block, the same as by delegate. // 你可以通过block或者代理,来得到用户选择的照片. [imagePickerVc setDidFinishPickingPhotosHandle:^(NSArray *photos, NSArray *assets, BOOL isSelectOriginalPhoto) { }]; [self presentViewController:imagePickerVc animated:YES completion:nil]; ## 三. Requirements 要求 iOS 10 or later. 支持iOS10及以上系统。 TZImagePickerController uses Camera、Location、Microphone、Photo Library,you need add these properties to info.plist like Demo: TZImagePickerController使用了相机、定位、麦克风、相册,请参考Demo添加下列属性到info.plist文件: `Privacy - Camera Usage Description` `Privacy - Location Usage Description` `Privacy - Location When In Use Usage Description` `Privacy - Microphone Usage Description` `Privacy - Photo Library Usage Description` `Prevent limited photos access alert` ## 四. More 更多 If you find a bug, please create a issue. More information please view code. 如果你发现了bug,请提一个issue。 更多信息详见代码,也可查看我的博客: [我的博客](http://www.jianshu.com/p/1975411a31bb "半尺尘 - 简书") 关于issue: 请尽可能详细地描述**系统版本**、**手机型号**、**库的版本**、**崩溃日志**和**复现步骤**,**请先更新到最新版再测试一下**,如果新版还存在再提~如果已有开启的类似issue,请直接在该issue下评论说出你的问题 ## 五. FAQ 常见问题 **Q:pod search TZImagePickerController 搜索出来的不是最新版本** A:需要在终端执行cd转换文件路径命令退回到Desktop,然后执行pod setup命令更新本地spec缓存(可能需要几分钟),然后再搜索就可以了 **Q:拍照后照片保存失败** A:请参考issue481:https://github.com/banchichen/TZImagePickerController/issues/481 的信息排查,若还有问题请直接在issue内评论 **Q:photos数组图片不是原图,如何获取原图?** A:请参考issue457的解释:https://github.com/banchichen/TZImagePickerController/issues/457 **Q:系统语言是中文/英文,界面上却有部分相册名字、返回按钮显示成了英文/中文?** A:请参考 https://github.com/banchichen/TZImagePickerController/issues/443 和 https://github.com/banchichen/TZImagePickerController/issues/929 **Q:预览界面能否支持传入NSURL、UIImage对象?** A:3.0.1版本已支持,需新接一个库:[TZImagePreviewController](https://github.com/banchichen/TZImagePreviewController),请参考里面的Demo使用。 **Q:设置可选视频的最大/最小时长?照片的最小/最大尺寸?不符合要求的不显示** A:可以的,参照Demo的isAssetCanBeDisplayed方法实现。我会返回asset出来,显示与否你来决定,注意这个是一个同步方法,对于需要根据asset去异步获取的信息如视频的大小、视频是否存在iCloud里来过滤的,无法做到。如果真要这样做,相册打开速度会变慢,你需要改我源码。 如果需要显示,选择时才提醒用户不可选,则实现isAssetCanBeSelected,用户选择时会调用它 **Q:预览页面出现了导航栏?** A:https://github.com/banchichen/TZImagePickerController/issues/652 **Q:可否增加微信编辑图片的功能?** A:考虑下,优先级低 **Q:是否有QQ/微信群/钉钉群?** A:有「钉钉群:33192786」和「QQ群:859033147」,推荐加钉钉群,答疑响应更快 **Q:想提交一个Pull Request?** A:请先加钉钉群(33192786)说下方案,和我确认下,避免同时改动同一处内容。**一个PR请只修复1个问题,变动内容越少越好**。 **Q:demo在真机上跑不起来?** A:1、team选你自己的;2、bundleId也改成你自己的或改成一个不会和别人重复的。可参考[简书的这篇博客](https://www.jianshu.com/p/cbe59138fca6) **Q:3.6.4以上版本设置导航栏颜色无效?** A:参考Demo里的代码,加上imagePickerVc.navigationBar.standardAppearance的相关设置 **Q:设置导航栏颜色无效?导航栏颜色总是白色?** A:是否有集成WRNavigationBar?如有,参考其readme调一下它的wr_setBlackList,把TZImagePickerController相关的控制器放到黑名单里,使得不受WRNavigationBar的影响。如果没有集成,可在issues列表里搜一下看看类似的issue参考下,如实在没头绪,可加群提供个能复现该问题的demo,0~2天给你解决。最近发现WRNavigationBar的黑名单会有不生效的情况,临时解决方案大家可参考:[https://github.com/wangrui460/WRNavigationBar/issues/145](https://github.com/wangrui460/WRNavigationBar/issues/145) **Q:导航栏没了?** A:是否有集成GKNavigationBarViewController?需要升级到2.0.4及以上版本,详见issue:[https://github.com/QuintGao/GKNavigationBarViewController/issues/7](https://github.com/QuintGao/GKNavigationBarViewController/issues/7)。 **Q:有的视频导出失败?** A:升级到2.2.6及以上版本试试,发现是修正视频转向导致的,2.2.6开始默认不再主动修正。如需打开,可设置needFixComposition为YES,但有几率导致安卓拍的视频导出失败。此外也可参考这个issue:https://github.com/banchichen/TZImagePickerController/issues/1073 **Q:视频导出慢?** A:视频导出分两步,第一步是通过PHAsset获取AVURLAsset,如是iCloud视频则涉及到网络请求,耗时容易不可控,第二步是通过AVURLAsset把视频保存到沙盒,耗时不算多。但第一步耗时不可控,你可以拷贝我源码出来拿到第一步的进度给用户一个进度提示... **Q:有的图片info里没有PHImageFileURLKey?** A:不要去拿PHImageFileURLKey,没用的,只有通过Photos框架才能访问相册照片,光拿一个路径没用。 如果需要通过路径上传照片,请先把UIImage保存到沙盒,**用沙盒路径**。 如果你上传照片需要一个名字参数,请参考Demo**直接用照片名字**。 ## 六. Release Notes 最近更新 **3.8.8 支持iOS18,修复openURL的失效问题** [#1686](https://github.com/banchichen/TZImagePickerController/issues/1686) **3.8.5 新增隐私清单文件** [#1675](https://github.com/banchichen/TZImagePickerController/pull/1675) **3.8.4 支持使用不带定位代码的版本** [#1606](https://github.com/banchichen/TZImagePickerController/pull/1606) 3.8.1 iOS14下可添加访问更多照片,详见PR内的评论 [#1526](https://github.com/banchichen/TZImagePickerController/pull/1526) 3.7.6 修复iOS15.2下初次授权相册权限时的长时间卡顿&白屏问题 [#1547](https://github.com/banchichen/TZImagePickerController/issues/1547) **3.6.7 修复Xcode13&iOS15下导航栏颜色异常问题** 3.6.2 新增allowEditVideo,单选视频时支持裁剪 3.6.0 修复iOS14下iCloud视频导出失败问题 **3.5.2 适配iPhone12系列设备** 3.4.4 支持Dark Mode 3.4.2 适配iOS14,若干问题修复 3.3.2 适配iOS13,若干问题修复 3.2.1 新增裁剪用scaleAspectFillCrop属性,设置为YES后,照片尺寸小于裁剪框时会自动放大撑满 3.2.0 加入用NSOperationQueue控制获取原图并发数降低内存的示例 3.1.8 批量获取图片时加入队列控制,尝试优化大批量选择图片时CPU和内存占用过高的问题(仍然危险,maxImagesCount谨慎设置过大...) 3.1.5 相册内无照片时给出提示,修复快速滑动时内存一直增加的问题 3.1.3 适配阿拉伯等语言下从右往左布局的特性 3.0.8 新增gifImagePlayBlock允许使用FLAnimatedImage等替换内部的GIF播放方案 3.0.7 适配iPhoneXR、XS、XS Max 3.0.6 优化保存照片、视频的方法 3.0.1 新增对[TZImagePreviewController](https://github.com/banchichen/TZImagePreviewController)库的支持,允许预览UIImage、NSURL、PHAsset对象 **3.0.0 去除iOS6和7的适配代码,更轻量,最低支持iOS8** 2.2.6 新增needFixComposition属性,默认为NO,不再主动修正视频转向,防止部分安卓拍的视频导出失败(**最后一个支持iOS6和7的版本**) 2.1.5 修复开启showSelectedIndex后照片列表页iCloud图片进度条紊乱的bug 2.1.4 新增多个页面和组件的样式自定义block,允许自定义绝大多数UI样式 2.1.2 新增showPhotoCannotSelectLayer属性,当已选照片张数达到最大可选张数时,可像微信一样让其它照片显示一个提示不可选的浮层 2.1.1 新增是否显示图片选中序号的属性,优化一些细节 2.1.0.3 新增拍摄视频功能,优化一些细节 2.0.0.6 优化自定义languageBundle的支持,加入使用示例 2.0.0.5 优化性能,提高选择器打开速度,新增越南语支持 2.0.0.2 新增繁体语言,可设置首选语言,国际化支持更强大;优化一些细节 1.9.8 支持Carthage,优化一些细节 1.9.6 优化视频预览和gif预览页toolbar在iPhoneX上的样式 ... 1.8.4 加入横竖屏适配;支持视频/gif多选;支持视频和照片一起选 1.8.1 新增2个代理方法,支持由上层来决定相册/照片的显示与否 ... 1.7.7 支持GIF图片的播放和选择 1.7.6 支持对共享相册和同步相册的显示 1.7.5 允许不进入预览页面直接选择照片 1.7.4 支持单选模式下裁剪照片,支持任意矩形和圆形裁剪框 1.7.3 优化iCloud照片的显示与选择 ... 1.5.0 可把拍照按钮放在外面;可自定义照片排序方式;Demo页的UI大改版,新增若干开关; ... 1.4.5 性能大幅提升(性能测试截图请去博客查看);可在照片列表页拍照;Demo大幅优化; ... ## 七. Common links 常用链接 1. Json diff online: https://www.jsondiffonline.com/ ================================================ FILE: TZImagePickerController/AppDelegate.h ================================================ // // AppDelegate.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end ================================================ FILE: TZImagePickerController/AppDelegate.m ================================================ // // AppDelegate.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "AppDelegate.h" #import "TZImagePickerController.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 打开下面这句代码,使用导航栏控制器作为rootViewController // [self useNavControllerAsRoot]; return YES; } - (void)useNavControllerAsRoot { UIViewController *vc = [[UIViewController alloc] init]; vc.view.backgroundColor = [UIColor whiteColor]; UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(20, 200, 300, 44)]; [btn setTitle:@"pushTZImagePickerController" forState:UIControlStateNormal]; [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; [btn addTarget:self action:@selector(pushTZImagePickerController) forControlEvents:UIControlEventTouchUpInside]; [vc.view addSubview:btn]; UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc]; nav.navigationBar.barStyle = UIBarStyleBlack; nav.navigationBar.translucent = YES; nav.navigationBar.barTintColor = [UIColor colorWithRed:(34/255.0) green:(34/255.0) blue:(34/255.0) alpha:1.0]; nav.navigationBar.tintColor = [UIColor blackColor]; if (@available(iOS 13.0, *)) { UINavigationBarAppearance *barAppearance = [[UINavigationBarAppearance alloc] init]; if (nav.navigationBar.isTranslucent) { UIColor *barTintColor = nav.navigationBar.barTintColor; barAppearance.backgroundColor = [barTintColor colorWithAlphaComponent:0.85]; } else { barAppearance.backgroundColor = nav.navigationBar.barTintColor; } barAppearance.titleTextAttributes = nav.navigationBar.titleTextAttributes; nav.navigationBar.standardAppearance = barAppearance; nav.navigationBar.scrollEdgeAppearance = barAppearance; } [nav setNavigationBarHidden:YES]; self.window.rootViewController = nav; [self.window makeKeyAndVisible]; } - (void)pushTZImagePickerController { TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:9 columnNumber:4 delegate:nil pushPhotoPickerVc:YES]; imagePickerVc.modalPresentationStyle = UIModalPresentationFullScreen; UINavigationController *nav = (UINavigationController *)[UIApplication sharedApplication].keyWindow.rootViewController; [nav.topViewController presentViewController:imagePickerVc animated:YES completion:nil]; } - (void)applicationWillResignActive:(UIApplication *)application { } - (void)applicationDidEnterBackground:(UIApplication *)application { } - (void)applicationWillEnterForeground:(UIApplication *)application { } - (void)applicationDidBecomeActive:(UIApplication *)application { } - (void)applicationWillTerminate:(UIApplication *)application { } @end ================================================ FILE: TZImagePickerController/Assets.xcassets/AlbumAddBtn.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "AlbumAddBtn@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: TZImagePickerController/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: TZImagePickerController/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: TZImagePickerController/Assets.xcassets/back.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "back@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: TZImagePickerController/Assets.xcassets/leftVideoEdit.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "filename" : "hx_videoedit_left@2x.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: TZImagePickerController/Assets.xcassets/photo_delete.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "photo_delete@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: TZImagePickerController/Assets.xcassets/rightVideoEdit.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "filename" : "hx_videoedit_right@2x.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: TZImagePickerController/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: TZImagePickerController/Base.lproj/Main.storyboard ================================================ ================================================ FILE: TZImagePickerController/FLAnimatedImage/FLAnimatedImage.h ================================================ // // FLAnimatedImage.h // Flipboard // // Created by Raphael Schaad on 7/8/13. // Copyright (c) 2013-2015 Flipboard. All rights reserved. // #import // Allow user classes conveniently just importing one header. #import "FLAnimatedImageView.h" #ifndef NS_DESIGNATED_INITIALIZER #if __has_attribute(objc_designated_initializer) #define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer)) #else #define NS_DESIGNATED_INITIALIZER #endif #endif extern const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum; // // An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`. // It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion. // It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back. // Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization. // @interface FLAnimatedImage : NSObject @property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0` @property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size` @property (nonatomic, assign, readonly) NSUInteger loopCount; // 0 means repeating the animation indefinitely @property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s @property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]` @property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount] @property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default) // Intended to be called from main thread synchronously; will return immediately. // If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling. // After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache. - (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index; // Pass either a `UIImage` or an `FLAnimatedImage` and get back its size + (CGSize)sizeForImage:(id)image; // On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged. - (instancetype)initWithAnimatedGIFData:(NSData *)data; // Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default. - (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER; + (instancetype)animatedImageWithGIFData:(NSData *)data; @property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only @end typedef NS_ENUM(NSUInteger, FLLogLevel) { FLLogLevelNone = 0, FLLogLevelError, FLLogLevelWarn, FLLogLevelInfo, FLLogLevelDebug, FLLogLevelVerbose }; @interface FLAnimatedImage (Logging) + (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel; + (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level; @end #define FLLog(logLevel, format, ...) [FLAnimatedImage logStringFromBlock:^NSString *{ return [NSString stringWithFormat:(format), ## __VA_ARGS__]; } withLevel:(logLevel)] @interface FLWeakProxy : NSProxy + (instancetype)weakProxyForObject:(id)targetObject; @end ================================================ FILE: TZImagePickerController/FLAnimatedImage/FLAnimatedImage.m ================================================ // // FLAnimatedImage.m // Flipboard // // Created by Raphael Schaad on 7/8/13. // Copyright (c) 2013-2015 Flipboard. All rights reserved. // #import "FLAnimatedImage.h" #import #import // From vm_param.h, define for iOS 8.0 or higher to build on device. #ifndef BYTE_SIZE #define BYTE_SIZE 8 // byte size in bits #endif #define MEGABYTE (1024 * 1024) // This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02; // An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB). // E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category. typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) { FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU) FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile) FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance) FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no. }; typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) { FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand. FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback. FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc. }; #if defined(DEBUG) && DEBUG @protocol FLAnimatedImageDebugDelegate @optional - (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache; - (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index; - (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage; @end #endif @interface FLAnimatedImage () @property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes @property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // Enables predrawing of images to improve performance. @property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default) @property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index @property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes @property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes; @property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames @property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background @property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes @property (nonatomic, assign) NSUInteger memoryWarningCount; @property (nonatomic, strong, readonly) dispatch_queue_t serialQueue; @property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource; // The weak proxy is used to break retain cycles with delayed actions from memory warnings. // We are lying about the actual type here to gain static type checking and eliminate casts. // The actual type of the object is `FLWeakProxy`. @property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy; #if defined(DEBUG) && DEBUG @property (nonatomic, weak) id debug_delegate; #endif @end // For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying. static NSHashTable *allAnimatedImagesWeak; @implementation FLAnimatedImage #pragma mark - Accessors #pragma mark Public // This is the definite value the frame cache needs to size itself to. - (NSUInteger)frameCacheSizeCurrent { NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal; // If set, respect the caps. if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) { frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax); } if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) { frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal); } return frameCacheSizeCurrent; } - (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax { if (_frameCacheSizeMax != frameCacheSizeMax) { // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed. BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent); // Update the value _frameCacheSizeMax = frameCacheSizeMax; if (willFrameCacheSizeShrink) { [self purgeFrameCacheIfNeeded]; } } } #pragma mark Private - (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal { if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) { // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed. BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent); // Update the value _frameCacheSizeMaxInternal = frameCacheSizeMaxInternal; if (willFrameCacheSizeShrink) { [self purgeFrameCacheIfNeeded]; } } } #pragma mark - Life Cycle + (void)initialize { if (self == [FLAnimatedImage class]) { // UIKit memory warning notification handler shared by all of the instances allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable]; [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) { // UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread"); // Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc. // Note that FLAnimatedImages can be created on any thread, so the hash table must be locked. NSArray *images = nil; @synchronized(allAnimatedImagesWeak) { images = [[allAnimatedImagesWeak allObjects] copy]; } // Now issue notifications to all of the images while holding a strong reference to them [images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note]; }]; } } - (instancetype)init { FLAnimatedImage *animatedImage = [self initWithAnimatedGIFData:nil]; if (!animatedImage) { FLLog(FLLogLevelError, @"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`."); } return animatedImage; } - (instancetype)initWithAnimatedGIFData:(NSData *)data { return [self initWithAnimatedGIFData:data optimalFrameCacheSize:0 predrawingEnabled:YES]; } - (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled { // Early return if no data supplied! BOOL hasData = ([data length] > 0); if (!hasData) { FLLog(FLLogLevelError, @"No animated GIF data supplied."); return nil; } self = [super init]; if (self) { // Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides. // Keep a strong reference to `data` and expose it read-only publicly. // However, we will use the `_imageSource` as handler to the image data throughout our life cycle. _data = data; _predrawingEnabled = isPredrawingEnabled; // Initialize internal data structures _cachedFramesForIndexes = [[NSMutableDictionary alloc] init]; _cachedFrameIndexes = [[NSMutableIndexSet alloc] init]; _requestedFrameIndexes = [[NSMutableIndexSet alloc] init]; // Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`. _imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO}); // Early return on failure! if (!_imageSource) { FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data); return nil; } // Early return if not GIF! CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource); BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF); if (!isGIFData) { FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data); return nil; } // Get `LoopCount` // Note: 0 means repeating the animation indefinitely. // Image properties example: // { // FileSize = 314446; // "{GIF}" = { // HasGlobalColorMap = 1; // LoopCount = 0; // }; // } NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL); _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; // Iterate through frame images size_t imageCount = CGImageSourceGetCount(_imageSource); NSUInteger skippedFrameCount = 0; NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount]; for (size_t i = 0; i < imageCount; i++) { @autoreleasepool { CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL); if (frameImageRef) { UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef]; // Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid). if (frameImage) { // Set poster image if (!self.posterImage) { _posterImage = frameImage; // Set its size to proxy our size. _size = _posterImage.size; // Remember index of poster image so we never purge it; also add it to the cache. _posterImageFrameIndex = i; [self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)]; [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex]; } // Get `DelayTime` // Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`. // Frame properties example: // { // ColorModel = RGB; // Depth = 8; // PixelHeight = 960; // PixelWidth = 640; // "{GIF}" = { // DelayTime = "0.4"; // UnclampedDelayTime = "0.4"; // }; // } NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL); NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary]; // Try to use the unclamped delay time; fall back to the normal delay time. NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime]; if (!delayTime) { delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime]; } // If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value. const NSTimeInterval kDelayTimeIntervalDefault = 0.1; if (!delayTime) { if (i == 0) { FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties); delayTime = @(kDelayTimeIntervalDefault); } else { FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties); delayTime = delayTimesForIndexesMutable[@(i - 1)]; } } // Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility. // To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO. if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) { FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum); delayTime = @(kDelayTimeIntervalDefault); } delayTimesForIndexesMutable[@(i)] = delayTime; } else { skippedFrameCount++; FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef); } CFRelease(frameImageRef); } else { skippedFrameCount++; FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, self->_imageSource); } } } _delayTimesForIndexes = [delayTimesForIndexesMutable copy]; _frameCount = imageCount; if (self.frameCount == 0) { FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties); return nil; } else if (self.frameCount == 1) { // Warn when we only have a single frame but return a valid GIF. FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties); } else { // We have multiple frames, rock on! } // If no value is provided, select a default based on the GIF. if (optimalFrameCacheSize == 0) { // Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size. // It's only dependent on the image size & number of frames and never changes. CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (self.frameCount - skippedFrameCount) / MEGABYTE; if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) { _frameCacheSizeOptimal = self.frameCount; } else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) { // This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames. _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault; } else { // The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning. _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory; } } else { // Use the provided value. _frameCacheSizeOptimal = optimalFrameCacheSize; } // In any case, cap the optimal cache size at the frame count. _frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount); // Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`. _allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)]; // See the property declarations for descriptions. _weakProxy = (id)[FLWeakProxy weakProxyForObject:self]; // Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone. // Note that FLAnimatedImages can be created on any thread, so the hash table must be locked. @synchronized(allAnimatedImagesWeak) { [allAnimatedImagesWeak addObject:self]; } } return self; } + (instancetype)animatedImageWithGIFData:(NSData *)data { FLAnimatedImage *animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data]; return animatedImage; } - (void)dealloc { if (_weakProxy) { [NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy]; } if (_imageSource) { CFRelease(_imageSource); } } #pragma mark - Public Methods // See header for more details. // Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size). - (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index { // Early return if the requested index is beyond bounds. // Note: We're comparing an index with a count and need to bail on greater than or equal to. if (index >= self.frameCount) { FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self); return nil; } // Remember requested frame index, this influences what we should cache next. self.requestedFrameIndex = index; #if defined(DEBUG) && DEBUG if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) { [self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index]; } #endif // Quick check to avoid doing any work if we already have all possible frames cached, a common case. if ([self.cachedFrameIndexes count] < self.frameCount) { // If we have frames that should be cached but aren't and aren't requested yet, request them. // Exclude existing cached frames, frames already requested, and specially cached poster image. NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache]; [frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes]; [frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes]; [frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex]; NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy]; // Asynchronously add frames to our cache. if ([frameIndexesToAddToCache count] > 0) { [self addFrameIndexesToCache:frameIndexesToAddToCache]; } } // Get the specified image. UIImage *image = self.cachedFramesForIndexes[@(index)]; // Purge if needed based on the current playhead position. [self purgeFrameCacheIfNeeded]; return image; } // Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping. - (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache { // Order matters. First, iterate over the indexes starting from the requested frame index. // Then, if there are any indexes before the requested frame index, do those. NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex); NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex); if (firstRange.length + secondRange.length != self.frameCount) { FLLog(FLLogLevelWarn, @"Two-part frame cache range doesn't equal full range."); } // Add to the requested list before we actually kick them off, so they don't get into the queue twice. [self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache]; // Lazily create dedicated isolation queue. if (!self.serialQueue) { _serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL); } // Start streaming requested frames in the background into the cache. // Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away. FLAnimatedImage * __weak weakSelf = self; dispatch_async(self.serialQueue, ^{ // Produce and cache next needed frame. void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) { // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`. for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { #if defined(DEBUG) && DEBUG CFTimeInterval predrawBeginTime = CACurrentMediaTime(); #endif UIImage *image = [weakSelf imageAtIndex:i]; #if defined(DEBUG) && DEBUG CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime; CFTimeInterval slowdownDuration = 0.0; if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) { CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self]; slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration; [NSThread sleepForTimeInterval:slowdownDuration]; } FLLog(FLLogLevelVerbose, @"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self); #endif // The results get returned one by one as soon as they're ready (and not in batch). // The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy. if (image && weakSelf) { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.cachedFramesForIndexes[@(i)] = image; [weakSelf.cachedFrameIndexes addIndex:i]; [weakSelf.requestedFrameIndexes removeIndex:i]; #if defined(DEBUG) && DEBUG if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) { [weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes]; } #endif }); } } }; [frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock]; [frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock]; }); } + (CGSize)sizeForImage:(id)image { CGSize imageSize = CGSizeZero; // Early return for nil if (!image) { return imageSize; } if ([image isKindOfClass:[UIImage class]]) { UIImage *uiImage = (UIImage *)image; imageSize = uiImage.size; } else if ([image isKindOfClass:[FLAnimatedImage class]]) { FLAnimatedImage *animatedImage = (FLAnimatedImage *)image; imageSize = animatedImage.size; } else { // Bear trap to capture bad images; we have seen crashers cropping up on iOS 7. FLLog(FLLogLevelError, @"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image); } return imageSize; } #pragma mark - Private Methods #pragma mark Frame Loading - (UIImage *)imageAtIndex:(NSUInteger)index { // It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time. CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); // Early return for nil if (!imageRef) { return nil; } UIImage *image = [UIImage imageWithCGImage:imageRef]; CFRelease(imageRef); // Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread. if (self.isPredrawingEnabled) { image = [[self class] predrawnImageFromImage:image]; } return image; } #pragma mark Frame Caching - (NSMutableIndexSet *)frameIndexesToCache { NSMutableIndexSet *indexesToCache = nil; // Quick check to avoid building the index set if the number of frames to cache equals the total frame count. if (self.frameCacheSizeCurrent == self.frameCount) { indexesToCache = [self.allFramesIndexSet mutableCopy]; } else { indexesToCache = [[NSMutableIndexSet alloc] init]; // Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end. // The second, if needed, the remaining number of frames beginning at index zero. NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex); NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength); [indexesToCache addIndexesInRange:firstRange]; NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength; if (secondLength > 0) { NSRange secondRange = NSMakeRange(0, secondLength); [indexesToCache addIndexesInRange:secondRange]; } // Double check our math, before we add the poster image index which may increase it by one. if ([indexesToCache count] != self.frameCacheSizeCurrent) { FLLog(FLLogLevelWarn, @"Number of frames to cache doesn't equal expected cache size."); } [indexesToCache addIndex:self.posterImageFrameIndex]; } return indexesToCache; } - (void)purgeFrameCacheIfNeeded { // Purge frames that are currently cached but don't need to be. // But not if we're still under the number of frames to cache. // This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below. if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) { NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy]; [indexesToPurge removeIndexes:[self frameIndexesToCache]]; [indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) { // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`. for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { [self.cachedFrameIndexes removeIndex:i]; [self.cachedFramesForIndexes removeObjectForKey:@(i)]; // Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access. #if defined(DEBUG) && DEBUG if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes]; }); } #endif } }]; } } - (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize { self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue]; FLLog(FLLogLevelDebug, @"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self); // Schedule resetting the frame cache size max completely after a while. const NSTimeInterval kResetDelay = 3.0; [self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay]; } - (void)resetFrameCacheSizeMaxInternal { self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit; FLLog(FLLogLevelDebug, @"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self); } #pragma mark System Memory Warnings Notification Handler - (void)didReceiveMemoryWarning:(NSNotification *)notification { self.memoryWarningCount++; // If we were about to grow larger, but got rapped on our knuckles by the system again, cancel. [NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)]; [NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil]; // Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand. FLLog(FLLogLevelDebug, @"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self); self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory; // Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage. // // [mem]^ (2) (5) (6) 1) Loading frames for the first time // (*)| , , , 2) Mem warning #1; purge cache // | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs // | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs // |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache // |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache // +----------------------> 7) After 3 mem warnings, stay at minimum cache size // [t] // *) The mem high water mark before we get warned might change for every cycle. // const NSUInteger kGrowAttemptsMax = 2; const NSTimeInterval kGrowDelay = 2.0; if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) { [self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay]; } // Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477 } #pragma mark Image Decoding // Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread. // On success, the returned object is a new `UIImage` instance with the same content as the one passed in. // On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged. // First inspired by & good Karma to: https://gist.github.com/steipete/1144242 + (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw { // Always use a device RGB color space for simplicity and predictability what will be going on. CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB(); // Early return on failure! if (!colorSpaceDeviceRGBRef) { FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw); return imageToPredraw; } // Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB: // kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast // (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts") size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A // "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs) void *data = NULL; size_t width = imageToPredraw.size.width; size_t height = imageToPredraw.size.height; size_t bitsPerComponent = CHAR_BIT; size_t bitsPerPixel = (bitsPerComponent * numberOfComponents); size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE); size_t bytesPerRow = (bytesPerPixel * width); CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage); // If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one. // "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs) if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) { alphaInfo = kCGImageAlphaNoneSkipFirst; } else if (alphaInfo == kCGImageAlphaFirst) { alphaInfo = kCGImageAlphaPremultipliedFirst; } else if (alphaInfo == kCGImageAlphaLast) { alphaInfo = kCGImageAlphaPremultipliedLast; } // "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs) bitmapInfo |= alphaInfo; // Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time). // Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why. CGContextRef bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo); CGColorSpaceRelease(colorSpaceDeviceRGBRef); // Early return on failure! if (!bitmapContextRef) { FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw); return imageToPredraw; } // Draw image in bitmap context and create image by preserving receiver's properties. CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage); CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef); UIImage *predrawnImage = [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation]; CGImageRelease(predrawnImageRef); CGContextRelease(bitmapContextRef); // Early return on failure! if (!predrawnImage) { FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw); return imageToPredraw; } return predrawnImage; } #pragma mark - Description - (NSString *)description { NSString *description = [super description]; description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)]; description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount]; return description; } @end #pragma mark - Logging @implementation FLAnimatedImage (Logging) static void (^_logBlock)(NSString *logString, FLLogLevel logLevel) = nil; static FLLogLevel _logLevel; + (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel { _logBlock = logBlock; _logLevel = logLevel; } + (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level { if (level <= _logLevel && _logBlock && stringBlock) { _logBlock(stringBlock(), level); } } @end #pragma mark - FLWeakProxy @interface FLWeakProxy () @property (nonatomic, weak) id target; @end @implementation FLWeakProxy #pragma mark Life Cycle // This is the designated creation method of an `FLWeakProxy` and // as a subclass of `NSProxy` it doesn't respond to or need `-init`. + (instancetype)weakProxyForObject:(id)targetObject { FLWeakProxy *weakProxy = [FLWeakProxy alloc]; weakProxy.target = targetObject; return weakProxy; } #pragma mark Forwarding Messages - (id)forwardingTargetForSelector:(SEL)selector { // Keep it lightweight: access the ivar directly return _target; } #pragma mark - NSWeakProxy Method Overrides #pragma mark Handling Unimplemented Methods - (void)forwardInvocation:(NSInvocation *)invocation { // Fallback for when target is nil. Don't do anything, just return 0/NULL/nil. // The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing. // We can't really handle struct return types here because we don't know the length. void *nullPointer = NULL; [invocation setReturnValue:&nullPointer]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { // We only get here if `forwardingTargetForSelector:` returns nil. // In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing. // We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`. // Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well. // See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache. return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } @end ================================================ FILE: TZImagePickerController/FLAnimatedImage/FLAnimatedImageView.h ================================================ // // FLAnimatedImageView.h // Flipboard // // Created by Raphael Schaad on 7/8/13. // Copyright (c) 2013-2015 Flipboard. All rights reserved. // #import @class FLAnimatedImage; @protocol FLAnimatedImageViewDebugDelegate; // // An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed. // The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`. // It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`. // Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`. // @interface FLAnimatedImageView : UIImageView // Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`. // And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`. @property (nonatomic, strong) FLAnimatedImage *animatedImage; @property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining); @property (nonatomic, strong, readonly) UIImage *currentFrame; @property (nonatomic, assign, readonly) NSUInteger currentFrameIndex; // The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes. // To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode. @property (nonatomic, copy) NSString *runLoopMode; @end ================================================ FILE: TZImagePickerController/FLAnimatedImage/FLAnimatedImageView.m ================================================ // // FLAnimatedImageView.h // Flipboard // // Created by Raphael Schaad on 7/8/13. // Copyright (c) 2013-2015 Flipboard. All rights reserved. // #import "FLAnimatedImageView.h" #import "FLAnimatedImage.h" #import #if defined(DEBUG) && DEBUG @protocol FLAnimatedImageViewDebugDelegate @optional - (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration; @end #endif @interface FLAnimatedImageView () // Override of public `readonly` properties as private `readwrite` @property (nonatomic, strong, readwrite) UIImage *currentFrame; @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; @property (nonatomic, assign) NSUInteger loopCountdown; @property (nonatomic, assign) NSTimeInterval accumulator; @property (nonatomic, strong) CADisplayLink *displayLink; @property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed. @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; #if defined(DEBUG) && DEBUG @property (nonatomic, weak) id debug_delegate; #endif @end @implementation FLAnimatedImageView @synthesize runLoopMode = _runLoopMode; #pragma mark - Initializers // -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be. // Using -initWithImage: doesn't call any of the other designated initializers. - (instancetype)initWithImage:(UIImage *)image { self = [super initWithImage:image]; if (self) { [self commonInit]; } return self; } // -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers. - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage { self = [super initWithImage:image highlightedImage:highlightedImage]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } - (void)commonInit { self.runLoopMode = [[self class] defaultRunLoopMode]; if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; } } #pragma mark - Accessors #pragma mark Public - (void)setAnimatedImage:(FLAnimatedImage *)animatedImage { if (![_animatedImage isEqual:animatedImage]) { if (animatedImage) { // Clear out the image. super.image = nil; // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`). super.highlighted = NO; // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image. [self invalidateIntrinsicContentSize]; } else { // Stop animating before the animated image gets cleared out. [self stopAnimating]; } _animatedImage = animatedImage; self.currentFrame = animatedImage.posterImage; self.currentFrameIndex = 0; if (animatedImage.loopCount > 0) { self.loopCountdown = animatedImage.loopCount; } else { self.loopCountdown = NSUIntegerMax; } self.accumulator = 0.0; // Start animating after the new animated image has been set. [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } [self.layer setNeedsDisplay]; } } #pragma mark - Life Cycle - (void)dealloc { // Removes the display link from all run loop modes. [_displayLink invalidate]; } #pragma mark - UIView Method Overrides #pragma mark Observing View-Related Changes - (void)didMoveToSuperview { [super didMoveToSuperview]; [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } else { [self stopAnimating]; } } - (void)didMoveToWindow { [super didMoveToWindow]; [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } else { [self stopAnimating]; } } - (void)setAlpha:(CGFloat)alpha { [super setAlpha:alpha]; [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } else { [self stopAnimating]; } } - (void)setHidden:(BOOL)hidden { [super setHidden:hidden]; [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } else { [self stopAnimating]; } } #pragma mark Auto Layout - (CGSize)intrinsicContentSize { // Default to let UIImageView handle the sizing of its image, and anything else it might consider. CGSize intrinsicContentSize = [super intrinsicContentSize]; // If we have have an animated image, use its image size. // UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize. // (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.) if (self.animatedImage) { intrinsicContentSize = self.image.size; } return intrinsicContentSize; } #pragma mark Smart Invert Colors #pragma mark - UIImageView Method Overrides #pragma mark Image Data - (UIImage *)image { UIImage *image = nil; if (self.animatedImage) { // Initially set to the poster image. image = self.currentFrame; } else { image = super.image; } return image; } - (void)setImage:(UIImage *)image { if (image) { // Clear out the animated image and implicitly pause animation playback. self.animatedImage = nil; } super.image = image; } #pragma mark Animating Images - (NSTimeInterval)frameDelayGreatestCommonDivisor { // Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping. const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum; NSArray *delays = self.animatedImage.delayTimesForIndexes.allValues; // Scales the frame delays by `kGreatestCommonDivisorPrecision` // then converts it to an UInteger for in order to calculate the GCD. NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision); for (NSNumber *value in delays) { scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD); } // Reverse to scale to get the value back into seconds. return scaledGCD / kGreatestCommonDivisorPrecision; } static NSUInteger gcd(NSUInteger a, NSUInteger b) { // http://en.wikipedia.org/wiki/Greatest_common_divisor if (a < b) { return gcd(b, a); } else if (a == b) { return b; } while (true) { NSUInteger remainder = a % b; if (remainder == 0) { return b; } a = b; b = remainder; } } - (void)startAnimating { if (self.animatedImage) { // Lazily create the display link. if (!self.displayLink) { // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:` // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display // link which will lead to the deallocation of both the display link and the weak proxy. FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self]; self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)]; [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode]; } // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz). // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh. const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1); self.displayLink.paused = NO; } else { [super startAnimating]; } } - (void)setRunLoopMode:(NSString *)runLoopMode { if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) { NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode); _runLoopMode = [[self class] defaultRunLoopMode]; } else { _runLoopMode = runLoopMode; } } - (void)stopAnimating { if (self.animatedImage) { self.displayLink.paused = YES; } else { [super stopAnimating]; } } - (BOOL)isAnimating { BOOL isAnimating = NO; if (self.animatedImage) { isAnimating = self.displayLink && !self.displayLink.isPaused; } else { isAnimating = [super isAnimating]; } return isAnimating; } #pragma mark Highlighted Image Unsupport - (void)setHighlighted:(BOOL)highlighted { // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell. if (!self.animatedImage) { [super setHighlighted:highlighted]; } } #pragma mark - Private Methods #pragma mark Animation // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons. // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed. - (void)updateShouldAnimate { BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0; self.shouldAnimate = self.animatedImage && isVisible; } - (void)displayDidRefresh:(CADisplayLink *)displayLink { // If for some reason a wild call makes it through when we shouldn't be animating, bail. // Early return! if (!self.shouldAnimate) { FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self); return; } NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)]; // If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block). if (delayTimeNumber) { NSTimeInterval delayTime = [delayTimeNumber floatValue]; // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead. UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex]; if (image) { FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage); self.currentFrame = image; if (self.needsDisplayWhenImageBecomesAvailable) { [self.layer setNeedsDisplay]; self.needsDisplayWhenImageBecomesAvailable = NO; } self.accumulator += displayLink.duration * displayLink.frameInterval; // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m while (self.accumulator >= delayTime) { self.accumulator -= delayTime; self.currentFrameIndex++; if (self.currentFrameIndex >= self.animatedImage.frameCount) { // If we've looped the number of times that this animated image describes, stop looping. self.loopCountdown--; if (self.loopCompletionBlock) { self.loopCompletionBlock(self.loopCountdown); } if (self.loopCountdown == 0) { [self stopAnimating]; return; } self.currentFrameIndex = 0; } // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to. // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded. self.needsDisplayWhenImageBecomesAvailable = YES; } } else { FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage); #if defined(DEBUG) && DEBUG if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) { [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)displayLink.duration * displayLink.frameInterval]; } #endif } } else { self.currentFrameIndex++; } } + (NSString *)defaultRunLoopMode { // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; } #pragma mark - CALayerDelegate (Informal) #pragma mark Providing the Layer's Content - (void)displayLayer:(CALayer *)layer { layer.contents = (__bridge id)self.image.CGImage; } @end ================================================ FILE: TZImagePickerController/Info.plist ================================================ CFBundleDevelopmentRegion en_US CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription 访问相机以拍照 NSLocationUsageDescription 允许定位以把位置保存到照片中 NSLocationWhenInUseUsageDescription 允许定位以把位置保存到照片中 NSMicrophoneUsageDescription 访问麦克风以录像 NSPhotoLibraryUsageDescription 访问相册以选择照片 PHPhotoLibraryPreventAutomaticLimitedAccessAlert UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UIStatusBarHidden UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance ================================================ FILE: TZImagePickerController/Location/TZLocationManager.h ================================================ // // TZLocationManager.h // TZImagePickerController // // Created by 谭真 on 2017/06/03. // Copyright © 2017年 谭真. All rights reserved. // 定位管理类 #import #import @interface TZLocationManager : NSObject + (instancetype)manager NS_SWIFT_NAME(default()); /// 开始定位 - (void)startLocation; - (void)startLocationWithSuccessBlock:(void (^)(NSArray *))successBlock failureBlock:(void (^)(NSError *error))failureBlock; - (void)startLocationWithGeocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock; - (void)startLocationWithSuccessBlock:(void (^)(NSArray *))successBlock failureBlock:(void (^)(NSError *error))failureBlock geocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock; /// 结束定位 - (void)stopUpdatingLocation; @end ================================================ FILE: TZImagePickerController/Location/TZLocationManager.m ================================================ // // TZLocationManager.m // TZImagePickerController // // Created by 谭真 on 2017/06/03. // Copyright © 2017年 谭真. All rights reserved. // 定位管理类 #import "TZLocationManager.h" @interface TZLocationManager () @property (nonatomic, strong) CLLocationManager *locationManager; /// 定位成功的回调block @property (nonatomic, copy) void (^successBlock)(NSArray *); /// 编码成功的回调block @property (nonatomic, copy) void (^geocodeBlock)(NSArray *geocodeArray); /// 定位失败的回调block @property (nonatomic, copy) void (^failureBlock)(NSError *error); @end @implementation TZLocationManager + (instancetype)manager { static TZLocationManager *manager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ manager = [[self alloc] init]; manager.locationManager = [[CLLocationManager alloc] init]; manager.locationManager.delegate = manager; [manager.locationManager requestWhenInUseAuthorization]; }); return manager; } - (void)startLocation { [self startLocationWithSuccessBlock:nil failureBlock:nil geocoderBlock:nil]; } - (void)startLocationWithSuccessBlock:(void (^)(NSArray *))successBlock failureBlock:(void (^)(NSError *error))failureBlock { [self startLocationWithSuccessBlock:successBlock failureBlock:failureBlock geocoderBlock:nil]; } - (void)startLocationWithGeocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock { [self startLocationWithSuccessBlock:nil failureBlock:nil geocoderBlock:geocoderBlock]; } - (void)startLocationWithSuccessBlock:(void (^)(NSArray *))successBlock failureBlock:(void (^)(NSError *error))failureBlock geocoderBlock:(void (^)(NSArray *geocoderArray))geocoderBlock { [self.locationManager startUpdatingLocation]; _successBlock = successBlock; _geocodeBlock = geocoderBlock; _failureBlock = failureBlock; } - (void)stopUpdatingLocation { [self.locationManager stopUpdatingLocation]; } #pragma mark - CLLocationManagerDelegate /// 地理位置发生改变时触发 - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { [manager stopUpdatingLocation]; if (_successBlock) { _successBlock(locations); } if (_geocodeBlock && locations.count) { CLGeocoder *geocoder = [[CLGeocoder alloc] init]; [geocoder reverseGeocodeLocation:[locations firstObject] completionHandler:^(NSArray *array, NSError *error) { self->_geocodeBlock(array); }]; } } /// 定位失败回调方法 - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { NSLog(@"定位失败, 错误: %@",error); switch([error code]) { case kCLErrorDenied: { // 用户禁止了定位权限 } break; default: break; } if (_failureBlock) { _failureBlock(error); } } @end ================================================ FILE: TZImagePickerController/LxGridViewFlowLayout.h ================================================ // // LxGridViewFlowLayout.h // LxGridView // #import /* 此类来源于DeveloperLx的优秀开源项目:LxGridView github链接:https://github.com/DeveloperLx/LxGridView 我对这个类的代码做了一些修改; 感谢DeveloperLx的优秀代码~ */ @interface LxGridViewFlowLayout : UICollectionViewFlowLayout @property (nonatomic,assign) BOOL panGestureRecognizerEnable; @end @protocol LxGridViewDataSource @optional - (void)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath willMoveToIndexPath:(NSIndexPath *)destinationIndexPath; - (void)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath didMoveToIndexPath:(NSIndexPath *)destinationIndexPath; - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath; - (BOOL)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath canMoveToIndexPath:(NSIndexPath *)destinationIndexPath; @end @protocol LxGridViewDelegateFlowLayout @optional - (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout willBeginDraggingItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout didBeginDraggingItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout willEndDraggingItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout didEndDraggingItemAtIndexPath:(NSIndexPath *)indexPath; @end ================================================ FILE: TZImagePickerController/LxGridViewFlowLayout.m ================================================ // // LxGridViewFlowLayout.m // LxGridView // #import "LxGridViewFlowLayout.h" #import "TZTestCell.h" #import "UIView+TZLayout.h" #define stringify __STRING static CGFloat const PRESS_TO_MOVE_MIN_DURATION = 0.1; static CGFloat const MIN_PRESS_TO_BEGIN_EDITING_DURATION = 0.6; CG_INLINE CGPoint CGPointOffset(CGPoint point, CGFloat dx, CGFloat dy) { return CGPointMake(point.x + dx, point.y + dy); } /* 此类来源于DeveloperLx的优秀开源项目:LxGridView github链接:https://github.com/DeveloperLx/LxGridView 我对这个类的代码做了一些修改; 感谢DeveloperLx的优秀代码~ */ @interface LxGridViewFlowLayout () @property (nonatomic,readonly) id dataSource; @property (nonatomic,readonly) id delegate; @end @implementation LxGridViewFlowLayout { UILongPressGestureRecognizer * _longPressGestureRecognizer; UIPanGestureRecognizer * _panGestureRecognizer; NSIndexPath * _movingItemIndexPath; UIView * _beingMovedPromptView; CGPoint _sourceItemCollectionViewCellCenter; CADisplayLink * _displayLink; CFTimeInterval _remainSecondsToBeginEditing; } #pragma mark - setup - (void)dealloc { [_displayLink invalidate]; [self removeGestureRecognizers]; [self removeObserver:self forKeyPath:@stringify(collectionView)]; } - (instancetype)init { if (self = [super init]) { [self setup]; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { [self setup]; } return self; } - (void)setup { [self addObserver:self forKeyPath:@stringify(collectionView) options:NSKeyValueObservingOptionNew context:nil]; } - (void)addGestureRecognizers { self.collectionView.userInteractionEnabled = YES; _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressGestureRecognizerTriggerd:)]; _longPressGestureRecognizer.cancelsTouchesInView = NO; _longPressGestureRecognizer.minimumPressDuration = PRESS_TO_MOVE_MIN_DURATION; _longPressGestureRecognizer.delegate = self; for (UIGestureRecognizer * gestureRecognizer in self.collectionView.gestureRecognizers) { if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) { [gestureRecognizer requireGestureRecognizerToFail:_longPressGestureRecognizer]; } } [self.collectionView addGestureRecognizer:_longPressGestureRecognizer]; _panGestureRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRecognizerTriggerd:)]; _panGestureRecognizer.delegate = self; [self.collectionView addGestureRecognizer:_panGestureRecognizer]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; } - (void)removeGestureRecognizers { if (_longPressGestureRecognizer) { if (_longPressGestureRecognizer.view) { [_longPressGestureRecognizer.view removeGestureRecognizer:_longPressGestureRecognizer]; } _longPressGestureRecognizer = nil; } if (_panGestureRecognizer) { if (_panGestureRecognizer.view) { [_panGestureRecognizer.view removeGestureRecognizer:_panGestureRecognizer]; } _panGestureRecognizer = nil; } [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; } #pragma mark - getter and setter implementation - (id)dataSource { return (id)self.collectionView.dataSource; } - (id)delegate { return (id)self.collectionView.delegate; } #pragma mark - override UICollectionViewLayout methods - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray * layoutAttributesForElementsInRect = [super layoutAttributesForElementsInRect:rect]; for (UICollectionViewLayoutAttributes * layoutAttributes in layoutAttributesForElementsInRect) { if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) { layoutAttributes.hidden = [layoutAttributes.indexPath isEqual:_movingItemIndexPath]; } } return layoutAttributesForElementsInRect; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes * layoutAttributes = [super layoutAttributesForItemAtIndexPath:indexPath]; if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) { layoutAttributes.hidden = [layoutAttributes.indexPath isEqual:_movingItemIndexPath]; } return layoutAttributes; } #pragma mark - gesture - (void)setPanGestureRecognizerEnable:(BOOL)panGestureRecognizerEnable { _panGestureRecognizer.enabled = panGestureRecognizerEnable; } - (BOOL)panGestureRecognizerEnable { return _panGestureRecognizer.enabled; } - (void)longPressGestureRecognizerTriggerd:(UILongPressGestureRecognizer *)longPress { switch (longPress.state) { case UIGestureRecognizerStatePossible: break; case UIGestureRecognizerStateBegan: { if (_displayLink == nil) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTriggered:)]; _displayLink.frameInterval = 6; [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; _remainSecondsToBeginEditing = MIN_PRESS_TO_BEGIN_EDITING_DURATION; } _movingItemIndexPath = [self.collectionView indexPathForItemAtPoint:[longPress locationInView:self.collectionView]]; if ([self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)] && [self.dataSource collectionView:self.collectionView canMoveItemAtIndexPath:_movingItemIndexPath] == NO) { _movingItemIndexPath = nil; return; } if ([self.delegate respondsToSelector:@selector(collectionView:layout:willBeginDraggingItemAtIndexPath:)]) { [self.delegate collectionView:self.collectionView layout:self willBeginDraggingItemAtIndexPath:_movingItemIndexPath]; } UICollectionViewCell *sourceCollectionViewCell = [self.collectionView cellForItemAtIndexPath:_movingItemIndexPath]; TZTestCell *sourceCell = (TZTestCell *)sourceCollectionViewCell; _beingMovedPromptView = [[UIView alloc]initWithFrame:CGRectOffset(sourceCollectionViewCell.frame, -10, -10)]; _beingMovedPromptView.tz_width += 20; _beingMovedPromptView.tz_height += 20; sourceCollectionViewCell.highlighted = YES; UIView * highlightedSnapshotView = [sourceCell snapshotView]; highlightedSnapshotView.frame = _beingMovedPromptView.bounds; highlightedSnapshotView.alpha = 1; sourceCollectionViewCell.highlighted = NO; UIView * snapshotView = [sourceCell snapshotView]; snapshotView.frame = _beingMovedPromptView.bounds; snapshotView.alpha = 0; [_beingMovedPromptView addSubview:snapshotView]; [_beingMovedPromptView addSubview:highlightedSnapshotView]; [self.collectionView addSubview:_beingMovedPromptView]; _sourceItemCollectionViewCellCenter = sourceCollectionViewCell.center; typeof(self) __weak weakSelf = self; [UIView animateWithDuration:0 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ typeof(self) __strong strongSelf = weakSelf; if (strongSelf) { highlightedSnapshotView.alpha = 0; snapshotView.alpha = 1; } } completion:^(BOOL finished) { typeof(self) __strong strongSelf = weakSelf; if (strongSelf) { [highlightedSnapshotView removeFromSuperview]; if ([strongSelf.delegate respondsToSelector:@selector(collectionView:layout:didBeginDraggingItemAtIndexPath:)]) { [strongSelf.delegate collectionView:strongSelf.collectionView layout:strongSelf didBeginDraggingItemAtIndexPath:self->_movingItemIndexPath]; } } }]; [self invalidateLayout]; } break; case UIGestureRecognizerStateChanged: break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: { [_displayLink invalidate]; _displayLink = nil; NSIndexPath * movingItemIndexPath = _movingItemIndexPath; if (movingItemIndexPath) { if ([self.delegate respondsToSelector:@selector(collectionView:layout:willEndDraggingItemAtIndexPath:)]) { [self.delegate collectionView:self.collectionView layout:self willEndDraggingItemAtIndexPath:movingItemIndexPath]; } _movingItemIndexPath = nil; _sourceItemCollectionViewCellCenter = CGPointZero; UICollectionViewLayoutAttributes * movingItemCollectionViewLayoutAttributes = [self layoutAttributesForItemAtIndexPath:movingItemIndexPath]; _longPressGestureRecognizer.enabled = NO; typeof(self) __weak weakSelf = self; [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ typeof(self) __strong strongSelf = weakSelf; if (strongSelf) { self->_beingMovedPromptView.center = movingItemCollectionViewLayoutAttributes.center; } } completion:^(BOOL finished) { self->_longPressGestureRecognizer.enabled = YES; typeof(self) __strong strongSelf = weakSelf; if (strongSelf) { [self->_beingMovedPromptView removeFromSuperview]; self->_beingMovedPromptView = nil; [strongSelf invalidateLayout]; if ([strongSelf.delegate respondsToSelector:@selector(collectionView:layout:didEndDraggingItemAtIndexPath:)]) { [strongSelf.delegate collectionView:strongSelf.collectionView layout:strongSelf didEndDraggingItemAtIndexPath:movingItemIndexPath]; } } }]; } } break; case UIGestureRecognizerStateFailed: break; default: break; } } - (void)panGestureRecognizerTriggerd:(UIPanGestureRecognizer *)pan { switch (pan.state) { case UIGestureRecognizerStatePossible: break; case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateChanged: { CGPoint panTranslation = [pan translationInView:self.collectionView]; _beingMovedPromptView.center = CGPointOffset(_sourceItemCollectionViewCellCenter, panTranslation.x, panTranslation.y); NSIndexPath * sourceIndexPath = _movingItemIndexPath; NSIndexPath * destinationIndexPath = [self.collectionView indexPathForItemAtPoint:_beingMovedPromptView.center]; if ((destinationIndexPath == nil) || [destinationIndexPath isEqual:sourceIndexPath]) { return; } if ([self.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:canMoveToIndexPath:)] && [self.dataSource collectionView:self.collectionView itemAtIndexPath:sourceIndexPath canMoveToIndexPath:destinationIndexPath] == NO) { return; } if ([self.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:willMoveToIndexPath:)]) { [self.dataSource collectionView:self.collectionView itemAtIndexPath:sourceIndexPath willMoveToIndexPath:destinationIndexPath]; } _movingItemIndexPath = destinationIndexPath; typeof(self) __weak weakSelf = self; [self.collectionView performBatchUpdates:^{ typeof(self) __strong strongSelf = weakSelf; if (strongSelf) { if (sourceIndexPath && destinationIndexPath) { [strongSelf.collectionView deleteItemsAtIndexPaths:@[sourceIndexPath]]; [strongSelf.collectionView insertItemsAtIndexPaths:@[destinationIndexPath]]; } } } completion:^(BOOL finished) { typeof(self) __strong strongSelf = weakSelf; if ([strongSelf.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)]) { [strongSelf.dataSource collectionView:strongSelf.collectionView itemAtIndexPath:sourceIndexPath didMoveToIndexPath:destinationIndexPath]; } }]; } break; case UIGestureRecognizerStateEnded: break; case UIGestureRecognizerStateCancelled: break; case UIGestureRecognizerStateFailed: break; default: break; } } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ([_panGestureRecognizer isEqual:gestureRecognizer]) { return _movingItemIndexPath != nil; } return YES; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { // only _longPressGestureRecognizer and _panGestureRecognizer can recognize simultaneously if ([_longPressGestureRecognizer isEqual:gestureRecognizer]) { return [_panGestureRecognizer isEqual:otherGestureRecognizer]; } if ([_panGestureRecognizer isEqual:gestureRecognizer]) { return [_longPressGestureRecognizer isEqual:otherGestureRecognizer]; } return NO; } #pragma mark - displayLink - (void)displayLinkTriggered:(CADisplayLink *)displayLink { if (_remainSecondsToBeginEditing <= 0) { [_displayLink invalidate]; _displayLink = nil; } _remainSecondsToBeginEditing = _remainSecondsToBeginEditing - 0.1; } #pragma mark - KVO and notification - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@stringify(collectionView)]) { if (self.collectionView) { [self addGestureRecognizers]; } else { [self removeGestureRecognizers]; } } } - (void)applicationWillResignActive:(NSNotification *)notificaiton { _panGestureRecognizer.enabled = NO; _panGestureRecognizer.enabled = YES; } @end ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/ar.lproj/Localizable.strings ================================================ "KEY" = "阿拉伯语"; "OK" = "حسنا"; "Back" = "الى الخلف"; "Done" = "فعله"; "Edit" = "تعديل"; "Sorry" = "آسف"; "Cancel" = "إلغاء"; "Setting" = "ضبط"; "Photos" = "الصور"; "Videos" = "أشرطة فيديو"; "Preview" = "معاينة"; "Full image" = "الصورة كاملة"; "Processing..." = "معالجة..."; "No Photos or Videos" = "لا توجد صور أو مقاطع فيديو"; "Synchronizing photos from iCloud" = "مزامنة الصور من iCloud"; "iCloud sync failed" = "iCloud فشلت المزامنة"; "Can not use camera" = "لا يمكن استخدام الكاميرا"; "Can not choose both video and photo" = "لا يمكن اختيار كل من الفيديو والصور"; "Can not choose both photo and GIF" = "لا يمكن اختيار كل من الصور و GIF"; "Select the video when in multi state, we will handle the video as a photo" = "حدد مقطع الفيديو عندما يكون في حالة متعددة، وسنعمل على معالجة مقطع الفيديو كصورة"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "إذا تعذّر الانتقال إلى صفحة \"إعدادات الخصوصية\"، فيرجى الانتقال إلى صفحة \"الإعدادات\" بنفسك، شكرًا لك"; "Select a maximum of %zd photos" = "حدد فقط ما يصل إلى %zd صورة"; "Select a minimum of %zd photos" = "الرجاء تحديد %zd صورة على الأقل"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "السماح لـ %@ بالوصول إلى الألبوم في \"الإعدادات > الخصوصية > الصور\""; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "الرجاء السماح لـ %@ بالوصول إلى الكاميرا في \"الإعدادات > الخصوصية > الكاميرا\""; "Selected for %ld seconds" = "محدد لمدة %ld ثانية"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/de.lproj/Localizable.strings ================================================ "KEY" = "德语"; "OK" = "OK"; "Back" = "Zurück"; "Done" = "Erledigt"; "Edit" = "Bearbeiten"; "Sorry" = "Es tut uns leid"; "Cancel" = "Stornieren"; "Setting" = "Rahmen"; "Photos" = "Fotos"; "Videos" = "Videos"; "Preview" = "Vorschau"; "Full image" = "Vollbild"; "Processing..." = "Wird bearbeitet..."; "No Photos or Videos" = "Keine Fotos oder Videos"; "Synchronizing photos from iCloud" = "Fotos aus iCloud synchronisieren"; "iCloud sync failed" = "iCloud Synchronisierung fehlgeschlagen"; "Can not use camera" = "Kann die Kamera nicht benutzen"; "Can not choose both video and photo" = "Video und Foto können nicht ausgewählt werden"; "Can not choose both photo and GIF" = "Foto und GIF können nicht ausgewählt werden"; "Select the video when in multi state, we will handle the video as a photo" = "Wenn Sie das Video im Multi-Status auswählen, wird es als Foto behandelt"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Sie können nicht zur Seite mit den Datenschutz-Einstellungen springen; bitte navigieren Sie selbst zur Einstellungsseite. Vielen Dank."; "Select a maximum of %zd photos" = "Wählen Sie maximal %zd Bilder aus"; "Select a minimum of %zd photos" = "Bitte wählen Sie mindestens %zd Fotos aus"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Erlauben Sie %@ den Zugriff auf Ihr Album unter: „Einstellungen > Datenschutz > Fotos“"; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Erlauben Sie %@ den Zugriff auf Ihre Kamera unter: „Einstellungen > Datenschutz > Kamera“"; "Selected for %ld seconds" = "Ausgewählt für %ld Sekunden"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/es.lproj/Localizable.strings ================================================ "KEY" = "西班牙语"; "OK" = "DE ACUERDO"; "Back" = "Espalda"; "Done" = "Hecho"; "Edit" = "επεξεργασία"; "Sorry" = "Lo siento"; "Cancel" = "Cancelar"; "Setting" = "Ajuste"; "Photos" = "Las fotos"; "Videos" = "Videos"; "Preview" = "Avance"; "Full image" = "Imagen completa"; "Processing..." = "Tratamiento..."; "No Photos or Videos" = "No hay fotos o videos"; "Synchronizing photos from iCloud" = "Sincronizando fotos desde iCloud"; "iCloud sync failed" = "la sincronización falló"; "Can not use camera" = "No puedo usar la camara"; "Can not choose both video and photo" = "No se puede elegir tanto el video como la foto."; "Can not choose both photo and GIF" = "No se puede elegir tanto foto como GIF"; "Select the video when in multi state, we will handle the video as a photo" = "Seleccione el vídeo en estado múltiple, trataremos el vídeo como una fotografía"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "No se puede saltar a la página de ajustes de privacidad, vaya a la página de ajustes manualmente, muchas gracias"; "Select a maximum of %zd photos" = "Seleccione solamente hasta %zd imágenes"; "Select a minimum of %zd photos" = "Seleccione al menos %zd fotografías"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Permita que %@ acceda a su galería en \"Ajustes > Privacidad > Fotografías\""; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Permita que %@ acceda a su cámara en \"Ajustes > Privacidad > Cámara\""; "Selected for %ld seconds" = "Seleccionado para %ld segundos"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/fr.lproj/Localizable.strings ================================================ "KEY" = "法语"; "OK" = "D'accord"; "Back" = "Retour"; "Done" = "Terminé"; "Edit" = "Éditer"; "Sorry" = "Pardon"; "Cancel" = "Annuler"; "Setting" = "Réglage"; "Photos" = "Photos"; "Videos" = "Vidéos"; "Preview" = "Aperçu"; "Full image" = "Image complète"; "Processing..." = "En traitement..."; "No Photos or Videos" = "Aucune photo ou vidéo"; "Synchronizing photos from iCloud" = "Synchroniser des photos depuis iCloud"; "iCloud sync failed" = "iCloud échec de la synchronisation"; "Can not use camera" = "Impossible d'utiliser la caméra"; "Can not choose both video and photo" = "Impossible de choisir à la fois la vidéo et la photo"; "Can not choose both photo and GIF" = "Impossible de choisir à la fois photo et GIF"; "Select the video when in multi state, we will handle the video as a photo" = "Sélectionnez la vidéo lorsqu’elle est en état multiple, nous la traiterons comme une photo"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Impossible d'ouvrir la page des paramètres de confidentialité, veuillez accéder vous-même à la page des paramètres, merci"; "Select a maximum of %zd photos" = "Vous pouvez uniquement sélectionner un maximum de %zd images"; "Select a minimum of %zd photos" = "Veuillez sélectionner un minimum de %zd photos"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Autorisez %@ à accéder à votre album dans « Paramètres > Confidentialité > Photos »"; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Autorisez %@ à accéder à votre appareil photo dans « Paramètres > Confidentialité > Appareil photo »"; "Selected for %ld seconds" = "Sélectionné pendant %ld secondes"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/ja.lproj/Localizable.strings ================================================ "KEY" = "日语"; "OK" = "OK"; "Back" = "バック"; "Done" = "完了"; "Edit" = "編集する"; "Sorry" = "ごめんなさい"; "Cancel" = "キャンセル"; "Setting" = "設定"; "Photos" = "写真"; "Videos" = "動画"; "Preview" = "プレビュー"; "Full image" = "フルイメージ"; "Processing..." = "処理..."; "No Photos or Videos" = "写真やビデオはありません"; "Synchronizing photos from iCloud" = "iCloudから写真を同期する"; "iCloud sync failed" = "iCloud同期に失敗しました"; "Can not use camera" = "カメラが使えない"; "Can not choose both video and photo" = "ビデオと写真の両方を選択することはできません"; "Can not choose both photo and GIF" = "写真とGIFの両方を選択することはできません"; "Select the video when in multi state, we will handle the video as a photo" = "多肢選択の状態で、ビデオを選択すると、ビデオをデフォルトに画像として送信します。"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "プライバシー設定画面にジャンプできません。手動で設定画面を表示してください。"; "Select a maximum of %zd photos" = "写真は多くとも%zd 枚選択できます。"; "Select a minimum of %zd photos" = "少なくとも %zd 枚の写真を選択してください。"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "iPhoneの「設定-プライバシー-写真」のオプションで、r%@の携帯電話のアルバムへのアクセス権限を許可してください。"; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "iPhoneの「設定-プライバシー-カメラ」で、%@のカメラへのアクセス権限を許可してください。"; "Selected for %ld seconds" = "%ld 秒間選択されました"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/ko-KP.lproj/Localizable.strings ================================================ "KEY" = "朝鲜语"; "OK" = "그래"; "Back" = "뒤로"; "Done" = "완료"; "Edit" = "편집하다"; "Sorry" = "미안해요"; "Cancel" = "취소"; "Setting" = "설정"; "Photos" = "사진"; "Videos" = "동영상"; "Preview" = "미리 보기"; "Full image" = "전체 이미지"; "Processing..." = "처리..."; "No Photos or Videos" = "아무 사진이 나 동영상"; "Synchronizing photos from iCloud" = "ICloud에서 사진을 동기화"; "iCloud sync failed" = "iCloud동기화 실패"; "Can not use camera" = "카메라를 사용할 수 없습니다."; "Can not choose both video and photo" = "비디오와 사진 둘 다를 선택할 수 없습니다."; "Can not choose both photo and GIF" = "사진 및 GIF를 선택할 수 없습니다."; "Select the video when in multi state, we will handle the video as a photo" = "다중 선택 모드에서 비디오를 선택하면 비디오를 사진으로 처리합니다."; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "개인 정보 보호 설정 페이지로 바로 이동할 수 없습니다. 설정 페이지로 직접 이동해 주세요. 감사합니다."; "Select a maximum of %zd photos" = "최대 %zd장의 이미지만 선택할 수 있습니다."; "Select a minimum of %zd photos" = "최소 %zd장의 사진을 선택해 주세요."; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "\"설정 > 개인 정보 보호 > 사진\"에서 %@이(가) 앨범에 접근할 수 있도록 허용하세요."; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "\"설정 > 개인 정보 보호 > 카메라\"에서 %@이(가) 카메라에 접근할 수 있도록 허용하세요."; "Selected for %ld seconds" = "%ld 초 동안 선택됨"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/pt.lproj/Localizable.strings ================================================ "KEY" = "葡萄牙语"; "OK" = "Está bem"; "Back" = "De volta"; "Done" = "Feito"; "Edit" = "editar"; "Sorry" = "Desculpa"; "Cancel" = "Cancelar"; "Setting" = "Configuração"; "Photos" = "Fotos"; "Videos" = "Vídeos"; "Preview" = "Visualizar"; "Full image" = "Imagem Completa"; "Processing..." = "Em processamento..."; "No Photos or Videos" = "Sem fotos ou vídeos"; "Synchronizing photos from iCloud" = "Sincronizando fotos do iCloud"; "iCloud sync failed" = "iCloud falha na sincronização"; "Can not use camera" = "Não pode usar a câmera"; "Can not choose both video and photo" = "Não é possível escolher vídeo e foto"; "Can not choose both photo and GIF" = "Não é possível escolher foto e GIF"; "Select the video when in multi state, we will handle the video as a photo" = "Se estiver em estado múltiplo, selecione a opção vídeo; iremos utilizar o vídeo como uma foto"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Não é possível avançar para a página de definições de privacidade, aceda à página de definições você mesmo, obrigado"; "Select a maximum of %zd photos" = "Selecione apenas %zd imagens,no máximo"; "Select a minimum of %zd photos" = "Selecione %zd fotos,no mínimo"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Permita a %@ aceder ao seu álbum em “Definições > Privacidade > Fotos”"; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Permita a %@ aceder à sua câmara em “Definições > Privacidade > Câmara”"; "Selected for %ld seconds" = "Selecionado por %ld segundos"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/ru.lproj/Localizable.strings ================================================ "KEY" = "俄语"; "OK" = "Хорошо"; "Back" = "назад"; "Done" = "Готово"; "Edit" = "редактировать"; "Sorry" = "сожалею"; "Cancel" = "отменить"; "Setting" = "настройка"; "Photos" = "Фото"; "Videos" = "Видео"; "Preview" = "предварительный просмотр"; "Full image" = "Полное изображение"; "Processing..." = "Обработка ..."; "No Photos or Videos" = "Нет фото или видео"; "Synchronizing photos from iCloud" = "Синхронизация фотографий из iCloud"; "iCloud sync failed" = "iCloud сбой синхронизации"; "Can not use camera" = "Не могу использовать камеру"; "Can not choose both video and photo" = "Не могу выбрать как видео,так и фото"; "Can not choose both photo and GIF" = "Не могу выбрать фото и GIF"; "Select the video when in multi state, we will handle the video as a photo" = "В случае выбора видео при нахождении в мультирежиме видео будет обработано как фотография"; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Не удается перейти на страницу настроек конфиденциальности. Перейдите на эту страницу самостоятельно"; "Select a maximum of %zd photos" = "Вы можете выбрать до %zd изображений"; "Select a minimum of %zd photos" = "Вы можете выбрать не менее %zd изображений"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Разрешите доступ %@ к вашему альбому,перейдя в Настройки > Конфиденциальность > Фото"; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Разрешите доступ %@ к камере вашего устройства,перейдя в Настройки > Конфиденциальность > Камера"; "Selected for %ld seconds" = "Выбрано для %ld секунд"; ================================================ FILE: TZImagePickerController/Resources/TZImagePickerController.bundle/vi.lproj/Localizable.strings ================================================ "KEY" = "越南语"; "OK" = "Xác nhận"; "Back" = "Quay lại"; "Done" = "Hoàn thành"; "Edit" = "biên tập"; "Sorry" = "Xin lỗi"; "Cancel" = "Hủy"; "Setting" = "Cài đặt"; "Photos" = "Hình"; "Videos" = "Clip"; "Preview" = "Xem trước"; "Full image" = "Hình gốc"; "Processing..." = "Đang xử lý..."; "No Photos or Videos" = "Không có ảnh hoặc video"; "Can not use camera" = "Máy chụp hình không khả dụng"; "Synchronizing photos from iCloud" = "Đang đồng bộ hình ảnh từ ICloud"; "iCloud sync failed" = "iCloud đồng bộ hóa không thành công"; "Can not choose both video and photo" = "Trong lúc chọn hình ảnh không cùng lúc chọn video"; "Can not choose both photo and GIF" = "Trong lúc chọn hình ảnh không cùng lúc chọn hình GIF"; "Select the video when in multi state, we will handle the video as a photo" = "Chọn hình ảnh cùng video, video sẽ bị mặc nhận thành hình ảnh và gửi đi."; "Can not jump to the privacy settings page, please go to the settings page by self, thank you" = "Không thể chuyển tự động qua trang cài đặt riêng tư, bạn hãy thoát ra cà điều chỉnh lại, cám ơn bạn."; "Select a maximum of %zd photos" = "Bạn chỉ được chọn nhiều nhất %zd tấm hình"; "Select a minimum of %zd photos" = "Chọn ít nhất %zd tấm hình"; "Allow %@ to access your album in \"Settings -> Privacy -> Photos\"" = "Vui lòng tại mục iPhone \" Cài đặt – quyền riêng tư - Ảnh\" mở quyền cho phép %@ truy cập ảnh."; "Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\"" = "Vui lòng tại mục iPhone \" Cài đặt – quyền riêng tư - Ảnh\" mở quyền cho phép %@ truy cập máy ảnh"; "Selected for %ld seconds" = "Đã chọn cho %ld giây"; ================================================ FILE: TZImagePickerController/TZImagePickerController/NSBundle+TZImagePicker.h ================================================ // // NSBundle+TZImagePicker.h // TZImagePickerController // // Created by 谭真 on 16/08/18. // Copyright © 2016年 谭真. All rights reserved. // #import @interface NSBundle (TZImagePicker) + (NSBundle *)tz_imagePickerBundle; + (NSString *)tz_localizedStringForKey:(NSString *)key value:(NSString *)value; + (NSString *)tz_localizedStringForKey:(NSString *)key; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/NSBundle+TZImagePicker.m ================================================ // // NSBundle+TZImagePicker.m // TZImagePickerController // // Created by 谭真 on 16/08/18. // Copyright © 2016年 谭真. All rights reserved. // #import "NSBundle+TZImagePicker.h" #import "TZImagePickerController.h" @implementation NSBundle (TZImagePicker) + (NSBundle *)tz_imagePickerBundle { #ifdef SWIFT_PACKAGE NSBundle *bundle = SWIFTPM_MODULE_BUNDLE; #else NSBundle *bundle = [NSBundle bundleForClass:[TZImagePickerController class]]; #endif NSURL *url = [bundle URLForResource:@"TZImagePickerController" withExtension:@"bundle"]; if (url) { bundle = [NSBundle bundleWithURL:url]; } return bundle; } + (NSString *)tz_localizedStringForKey:(NSString *)key { return [self tz_localizedStringForKey:key value:@""]; } + (NSString *)tz_localizedStringForKey:(NSString *)key value:(NSString *)value { NSBundle *bundle = [TZImagePickerConfig sharedInstance].languageBundle; NSString *value1 = [bundle localizedStringForKey:key value:value table:nil]; return value1; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZAssetCell.h ================================================ // // TZAssetCell.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import #import typedef enum : NSUInteger { TZAssetCellTypePhoto = 0, TZAssetCellTypeLivePhoto, TZAssetCellTypePhotoGif, TZAssetCellTypeVideo, TZAssetCellTypeAudio, } TZAssetCellType; @class TZAssetModel; @interface TZAssetCell : UICollectionViewCell @property (weak, nonatomic) UIButton *selectPhotoButton; @property (weak, nonatomic) UIButton *cannotSelectLayerButton; @property (nonatomic, strong) TZAssetModel *model; @property (assign, nonatomic) NSInteger index; @property (nonatomic, copy) void (^didSelectPhotoBlock)(BOOL); @property (nonatomic, assign) TZAssetCellType type; @property (nonatomic, assign) BOOL allowPickingGif; @property (nonatomic, assign) BOOL allowPickingMultipleVideo; @property (nonatomic, copy) NSString *representedAssetIdentifier; @property (nonatomic, assign) int32_t imageRequestID; @property (nonatomic, strong) UIImage *photoSelImage; @property (nonatomic, strong) UIImage *photoDefImage; @property (nonatomic, assign) BOOL showSelectBtn; @property (assign, nonatomic) BOOL allowPreview; @property (nonatomic, copy) void (^assetCellDidSetModelBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView); @property (nonatomic, copy) void (^assetCellDidLayoutSubviewsBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView); @end @class TZAlbumModel; @interface TZAlbumCell : UITableViewCell @property (nonatomic, strong) TZAlbumModel *model; @property (weak, nonatomic) UIButton *selectedCountButton; @property (nonatomic, copy) void (^albumCellDidSetModelBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel); @property (nonatomic, copy) void (^albumCellDidLayoutSubviewsBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel); @end @interface TZAssetCameraCell : UICollectionViewCell @property (nonatomic, strong) UIImageView *imageView; @end @interface TZAssetAddMoreCell : TZAssetCameraCell @property (nonatomic, strong) UILabel *tipLabel; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZAssetCell.m ================================================ // // TZAssetCell.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "TZAssetCell.h" #import "TZAssetModel.h" #import "UIView+TZLayout.h" #import "TZImageManager.h" #import "TZImagePickerController.h" #import "TZProgressView.h" @interface TZAssetCell () @property (weak, nonatomic) UIImageView *imageView; // The photo / 照片 @property (weak, nonatomic) UIImageView *selectImageView; @property (weak, nonatomic) UILabel *indexLabel; @property (weak, nonatomic) UIView *bottomView; @property (weak, nonatomic) UILabel *timeLength; @property (strong, nonatomic) UITapGestureRecognizer *tapGesture; @property (nonatomic, weak) UIImageView *videoImgView; @property (nonatomic, strong) TZProgressView *progressView; @property (nonatomic, assign) int32_t bigImageRequestID; @end @implementation TZAssetCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload:) name:@"TZ_PHOTO_PICKER_RELOAD_NOTIFICATION" object:nil]; return self; } - (void)setModel:(TZAssetModel *)model { _model = model; self.representedAssetIdentifier = model.asset.localIdentifier; int32_t imageRequestID = [[TZImageManager manager] getPhotoWithAsset:model.asset photoWidth:self.tz_width completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { // Set the cell's thumbnail image if it's still showing the same asset. if ([self.representedAssetIdentifier isEqualToString:model.asset.localIdentifier]) { self.imageView.image = photo; [self setNeedsLayout]; } else { // NSLog(@"this cell is showing other asset"); [[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID]; } if (!isDegraded) { [self hideProgressView]; self.imageRequestID = 0; } } progressHandler:nil networkAccessAllowed:NO]; if (imageRequestID && self.imageRequestID && imageRequestID != self.imageRequestID) { [[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID]; // NSLog(@"cancelImageRequest %d",self.imageRequestID); } self.imageRequestID = imageRequestID; self.selectPhotoButton.selected = model.isSelected; self.selectImageView.image = self.selectPhotoButton.isSelected ? self.photoSelImage : self.photoDefImage; self.indexLabel.hidden = !self.selectPhotoButton.isSelected; self.type = (NSInteger)model.type; // 让宽度/高度小于 最小可选照片尺寸 的图片不能选中 if (![[TZImageManager manager] isPhotoSelectableWithAsset:model.asset]) { if (_selectImageView.hidden == NO) { self.selectPhotoButton.hidden = YES; _selectImageView.hidden = YES; } } // 如果用户选中了该图片,提前获取一下大图 if (model.isSelected) { [self requestBigImage]; } else { [self cancelBigImageRequest]; } [self setNeedsLayout]; if (self.assetCellDidSetModelBlock) { self.assetCellDidSetModelBlock(self, _imageView, _selectImageView, _indexLabel, _bottomView, _timeLength, _videoImgView); } } - (void)setIndex:(NSInteger)index { _index = index; self.indexLabel.text = [NSString stringWithFormat:@"%zd", index]; [self.contentView bringSubviewToFront:self.indexLabel]; } - (void)setShowSelectBtn:(BOOL)showSelectBtn { _showSelectBtn = showSelectBtn; BOOL selectable = [[TZImageManager manager] isPhotoSelectableWithAsset:self.model.asset]; if (!self.selectPhotoButton.hidden) { self.selectPhotoButton.hidden = !showSelectBtn || !selectable; } if (!self.selectImageView.hidden) { self.selectImageView.hidden = !showSelectBtn || !selectable; } } - (void)setType:(TZAssetCellType)type { _type = type; if (type == TZAssetCellTypePhoto || type == TZAssetCellTypeLivePhoto || (type == TZAssetCellTypePhotoGif && !self.allowPickingGif) || self.allowPickingMultipleVideo) { _selectImageView.hidden = NO; _selectPhotoButton.hidden = NO; _bottomView.hidden = YES; } else { // Video of Gif _selectImageView.hidden = YES; _selectPhotoButton.hidden = YES; } if (type == TZAssetCellTypeVideo) { self.bottomView.hidden = NO; self.timeLength.text = _model.timeLength; self.videoImgView.hidden = NO; _timeLength.tz_left = self.videoImgView.tz_right; _timeLength.textAlignment = NSTextAlignmentRight; } else if (type == TZAssetCellTypePhotoGif && self.allowPickingGif) { self.bottomView.hidden = NO; self.timeLength.text = @"GIF"; self.videoImgView.hidden = YES; _timeLength.tz_left = 5; _timeLength.textAlignment = NSTextAlignmentLeft; } } - (void)setAllowPreview:(BOOL)allowPreview { _allowPreview = allowPreview; if (allowPreview) { _imageView.userInteractionEnabled = NO; _tapGesture.enabled = NO; } else { _imageView.userInteractionEnabled = YES; _tapGesture.enabled = YES; } } - (void)selectPhotoButtonClick:(UIButton *)sender { if (self.didSelectPhotoBlock) { self.didSelectPhotoBlock(sender.isSelected); } self.selectImageView.image = sender.isSelected ? self.photoSelImage : self.photoDefImage; if (sender.isSelected) { [UIView showOscillatoryAnimationWithLayer:_selectImageView.layer type:TZOscillatoryAnimationToBigger]; // 用户选中了该图片,提前获取一下大图 [self requestBigImage]; } else { // 取消选中,取消大图的获取 [self cancelBigImageRequest]; } } /// 只在单选状态且allowPreview为NO时会有该事件 - (void)didTapImageView { if (self.didSelectPhotoBlock) { self.didSelectPhotoBlock(NO); } } - (void)hideProgressView { if (_progressView) { self.progressView.hidden = YES; self.imageView.alpha = 1.0; } } - (void)requestBigImage { if (_bigImageRequestID) { [[PHImageManager defaultManager] cancelImageRequest:_bigImageRequestID]; } _bigImageRequestID = [[TZImageManager manager] requestImageDataForAsset:_model.asset completion:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { BOOL iCloudSyncFailed = !imageData && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.model.iCloudFailed = iCloudSyncFailed; if (iCloudSyncFailed && self.didSelectPhotoBlock) { self.didSelectPhotoBlock(YES); self.selectImageView.image = self.photoDefImage; } [self hideProgressView]; } progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { if (self.model.isSelected) { progress = progress > 0.02 ? progress : 0.02;; self.progressView.progress = progress; self.progressView.hidden = NO; self.imageView.alpha = 0.4; if (progress >= 1) { [self hideProgressView]; } } else { // 快速连续点几次,会EXC_BAD_ACCESS... // *stop = YES; [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; [self cancelBigImageRequest]; } }]; if (_model.type == TZAssetCellTypeVideo) { [[TZImageManager manager] getVideoWithAsset:_model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) { BOOL iCloudSyncFailed = !playerItem && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.model.iCloudFailed = iCloudSyncFailed; if (iCloudSyncFailed && self.didSelectPhotoBlock) { dispatch_async(dispatch_get_main_queue(), ^{ self.didSelectPhotoBlock(YES); self.selectImageView.image = self.photoDefImage; }); } }]; } } - (void)cancelBigImageRequest { if (_bigImageRequestID) { [[PHImageManager defaultManager] cancelImageRequest:_bigImageRequestID]; } [self hideProgressView]; } #pragma mark - Notification - (void)reload:(NSNotification *)noti { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)noti.object; UIViewController *parentViewController = nil; UIResponder *responder = self.nextResponder; do { if ([responder isKindOfClass:[UIViewController class]]) { parentViewController = (UIViewController *)responder; break; } responder = responder.nextResponder; } while (responder); if (parentViewController.navigationController != tzImagePickerVc) { return; } if (self.model.isSelected && tzImagePickerVc.showSelectedIndex) { self.index = [tzImagePickerVc.selectedAssetIds indexOfObject:self.model.asset.localIdentifier] + 1; } self.indexLabel.hidden = !self.selectPhotoButton.isSelected; BOOL notSelectable = [TZCommonTools isAssetNotSelectable:self.model tzImagePickerVc:tzImagePickerVc]; if (notSelectable && tzImagePickerVc.showPhotoCannotSelectLayer && !self.model.isSelected) { self.cannotSelectLayerButton.backgroundColor = tzImagePickerVc.cannotSelectLayerColor; self.cannotSelectLayerButton.hidden = NO; } else { self.cannotSelectLayerButton.hidden = YES; } } #pragma mark - Lazy load - (UIButton *)selectPhotoButton { if (_selectPhotoButton == nil) { UIButton *selectPhotoButton = [[UIButton alloc] init]; [selectPhotoButton addTarget:self action:@selector(selectPhotoButtonClick:) forControlEvents:UIControlEventTouchUpInside]; [self.contentView addSubview:selectPhotoButton]; _selectPhotoButton = selectPhotoButton; } return _selectPhotoButton; } - (UIImageView *)imageView { if (_imageView == nil) { UIImageView *imageView = [[UIImageView alloc] init]; imageView.contentMode = UIViewContentModeScaleAspectFill; imageView.clipsToBounds = YES; [self.contentView addSubview:imageView]; _imageView = imageView; _tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapImageView)]; [_imageView addGestureRecognizer:_tapGesture]; self.allowPreview = self.allowPreview; } return _imageView; } - (UIImageView *)selectImageView { if (_selectImageView == nil) { UIImageView *selectImageView = [[UIImageView alloc] init]; selectImageView.contentMode = UIViewContentModeCenter; selectImageView.clipsToBounds = YES; [self.contentView addSubview:selectImageView]; _selectImageView = selectImageView; } return _selectImageView; } - (UIView *)bottomView { if (_bottomView == nil) { UIView *bottomView = [[UIView alloc] init]; static NSInteger rgb = 0; bottomView.userInteractionEnabled = NO; bottomView.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.8]; [self.contentView addSubview:bottomView]; _bottomView = bottomView; } return _bottomView; } - (UIButton *)cannotSelectLayerButton { if (_cannotSelectLayerButton == nil) { UIButton *cannotSelectLayerButton = [[UIButton alloc] init]; [self.contentView addSubview:cannotSelectLayerButton]; _cannotSelectLayerButton = cannotSelectLayerButton; } return _cannotSelectLayerButton; } - (UIImageView *)videoImgView { if (_videoImgView == nil) { UIImageView *videoImgView = [[UIImageView alloc] init]; [videoImgView setImage:[UIImage tz_imageNamedFromMyBundle:@"VideoSendIcon"]]; [self.bottomView addSubview:videoImgView]; _videoImgView = videoImgView; } return _videoImgView; } - (UILabel *)timeLength { if (_timeLength == nil) { UILabel *timeLength = [[UILabel alloc] init]; timeLength.font = [UIFont boldSystemFontOfSize:11]; timeLength.textColor = [UIColor whiteColor]; timeLength.textAlignment = NSTextAlignmentRight; [self.bottomView addSubview:timeLength]; _timeLength = timeLength; } return _timeLength; } - (UILabel *)indexLabel { if (_indexLabel == nil) { UILabel *indexLabel = [[UILabel alloc] init]; indexLabel.font = [UIFont systemFontOfSize:14]; indexLabel.adjustsFontSizeToFitWidth = YES; indexLabel.textColor = [UIColor whiteColor]; indexLabel.textAlignment = NSTextAlignmentCenter; [self.contentView addSubview:indexLabel]; _indexLabel = indexLabel; } return _indexLabel; } - (TZProgressView *)progressView { if (_progressView == nil) { _progressView = [[TZProgressView alloc] init]; _progressView.hidden = YES; [self addSubview:_progressView]; } return _progressView; } - (void)layoutSubviews { [super layoutSubviews]; _cannotSelectLayerButton.frame = self.bounds; if (self.allowPreview) { _selectPhotoButton.frame = CGRectMake(self.tz_width - 44, 0, 44, 44); } else { _selectPhotoButton.frame = self.bounds; } _selectImageView.frame = CGRectMake(self.tz_width - 27, 3, 24, 24); if (_selectImageView.image.size.width <= 27) { _selectImageView.contentMode = UIViewContentModeCenter; } else { _selectImageView.contentMode = UIViewContentModeScaleAspectFit; } _indexLabel.frame = _selectImageView.frame; _imageView.frame = self.bounds; static CGFloat progressWH = 20; CGFloat progressXY = (self.tz_width - progressWH) / 2; _progressView.frame = CGRectMake(progressXY, progressXY, progressWH, progressWH); _bottomView.frame = CGRectMake(0, self.tz_height - 17, self.tz_width, 17); _videoImgView.frame = CGRectMake(8, 0, 17, 17); _timeLength.frame = CGRectMake(self.videoImgView.tz_right, 0, self.tz_width - self.videoImgView.tz_right - 5, 17); self.type = (NSInteger)self.model.type; self.showSelectBtn = self.showSelectBtn; [self.contentView bringSubviewToFront:_bottomView]; [self.contentView bringSubviewToFront:_cannotSelectLayerButton]; [self.contentView bringSubviewToFront:_selectPhotoButton]; [self.contentView bringSubviewToFront:_selectImageView]; [self.contentView bringSubviewToFront:_indexLabel]; if (self.assetCellDidLayoutSubviewsBlock) { self.assetCellDidLayoutSubviewsBlock(self, _imageView, _selectImageView, _indexLabel, _bottomView, _timeLength, _videoImgView); } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end @interface TZAlbumCell () @property (weak, nonatomic) UIImageView *posterImageView; @property (weak, nonatomic) UILabel *titleLabel; @end @implementation TZAlbumCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; self.backgroundColor = [UIColor whiteColor]; self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; return self; } - (void)setModel:(TZAlbumModel *)model { _model = model; UIColor *nameColor = UIColor.blackColor; if (@available(iOS 13.0, *)) { nameColor = UIColor.labelColor; } NSMutableAttributedString *nameString = [[NSMutableAttributedString alloc] initWithString:model.name attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:nameColor}]; NSAttributedString *countString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" (%zd)",model.count] attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:[UIColor lightGrayColor]}]; [nameString appendAttributedString:countString]; self.titleLabel.attributedText = nameString; [[TZImageManager manager] getPostImageWithAlbumModel:model completion:^(UIImage *postImage) { self.posterImageView.image = postImage; [self setNeedsLayout]; }]; if (model.selectedCount) { self.selectedCountButton.hidden = NO; [self.selectedCountButton setTitle:[NSString stringWithFormat:@"%zd",model.selectedCount] forState:UIControlStateNormal]; } else { self.selectedCountButton.hidden = YES; } if (self.albumCellDidSetModelBlock) { self.albumCellDidSetModelBlock(self, _posterImageView, _titleLabel); } } - (void)layoutSubviews { [super layoutSubviews]; CGFloat selectedCountButtonX; CGFloat titleLabelX; CGFloat posterImageViewX; if ([TZCommonTools tz_isRightToLeftLayout]) { selectedCountButtonX = 24; titleLabelX = 24; posterImageViewX = self.contentView.tz_width - 70; } else { selectedCountButtonX = self.contentView.tz_width - 24; titleLabelX = 80; posterImageViewX = 0; } _selectedCountButton.frame = CGRectMake(selectedCountButtonX, 23, 24, 24); NSInteger titleHeight = ceil(self.titleLabel.font.lineHeight); self.titleLabel.frame = CGRectMake(titleLabelX, (self.tz_height - titleHeight) / 2, self.tz_width - 80 - 50, titleHeight); self.posterImageView.frame = CGRectMake(posterImageViewX, 0, 70, 70); if (self.albumCellDidLayoutSubviewsBlock) { self.albumCellDidLayoutSubviewsBlock(self, _posterImageView, _titleLabel); } } - (void)layoutSublayersOfLayer:(CALayer *)layer { [super layoutSublayersOfLayer:layer]; } #pragma mark - Lazy load - (UIImageView *)posterImageView { if (_posterImageView == nil) { UIImageView *posterImageView = [[UIImageView alloc] init]; posterImageView.contentMode = UIViewContentModeScaleAspectFill; posterImageView.clipsToBounds = YES; [self.contentView addSubview:posterImageView]; _posterImageView = posterImageView; } return _posterImageView; } - (UILabel *)titleLabel { if (_titleLabel == nil) { UILabel *titleLabel = [[UILabel alloc] init]; titleLabel.font = [UIFont boldSystemFontOfSize:17]; if (@available(iOS 13.0, *)) { titleLabel.textColor = UIColor.labelColor; } else { titleLabel.textColor = [UIColor blackColor]; } titleLabel.textAlignment = NSTextAlignmentNatural; [self.contentView addSubview:titleLabel]; _titleLabel = titleLabel; } return _titleLabel; } - (UIButton *)selectedCountButton { if (_selectedCountButton == nil) { UIButton *selectedCountButton = [[UIButton alloc] init]; selectedCountButton.titleLabel.adjustsFontSizeToFitWidth = YES; selectedCountButton.layer.cornerRadius = 12; selectedCountButton.clipsToBounds = YES; selectedCountButton.backgroundColor = [UIColor redColor]; [selectedCountButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; selectedCountButton.titleLabel.font = [UIFont systemFontOfSize:15]; [self.contentView addSubview:selectedCountButton]; _selectedCountButton = selectedCountButton; } return _selectedCountButton; } @end @implementation TZAssetCameraCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor whiteColor]; _imageView = [[UIImageView alloc] init]; _imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; _imageView.contentMode = UIViewContentModeScaleAspectFill; [self.contentView addSubview:_imageView]; self.clipsToBounds = YES; } return self; } - (void)layoutSubviews { [super layoutSubviews]; _imageView.frame = self.bounds; } @end @implementation TZAssetAddMoreCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { _tipLabel = [[UILabel alloc] init]; _tipLabel.numberOfLines = 2; _tipLabel.textAlignment = NSTextAlignmentCenter; _tipLabel.font = [UIFont systemFontOfSize:12]; _tipLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; CGFloat rgb = 156 / 255.0; _tipLabel.textColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:1.0]; [self.contentView addSubview:_tipLabel]; self.clipsToBounds = YES; } return self; } - (void)layoutSubviews { [super layoutSubviews]; _tipLabel.frame = CGRectMake(5, self.tz_height / 2, self.tz_width - 10, self.tz_height / 2 - 5); } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZAssetModel.h ================================================ // // TZAssetModel.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import #import #import typedef enum : NSUInteger { TZAssetModelMediaTypePhoto = 0, TZAssetModelMediaTypeLivePhoto, TZAssetModelMediaTypePhotoGif, TZAssetModelMediaTypeVideo, TZAssetModelMediaTypeAudio } TZAssetModelMediaType; @class PHAsset; @interface TZAssetModel : NSObject @property (nonatomic, strong) PHAsset *asset; @property (nonatomic, assign) BOOL isSelected; ///< The select status of a photo, default is No @property (nonatomic, assign) TZAssetModelMediaType type; @property (nonatomic, copy) NSString *timeLength; @property (nonatomic, assign) BOOL iCloudFailed; /// Init a photo dataModel With a PHAsset /// 用一个PHAsset实例,初始化一个照片模型 + (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type; + (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type timeLength:(NSString *)timeLength; @end @class PHFetchResult; @interface TZAlbumModel : NSObject @property (nonatomic, strong) NSString *name; ///< The album name @property (nonatomic, assign) NSInteger count; ///< Count of photos the album contain @property (nonatomic, strong) PHFetchResult *result; @property (nonatomic, strong) PHAssetCollection *collection; @property (nonatomic, strong) PHFetchOptions *options; @property (nonatomic, strong) NSArray *models; @property (nonatomic, strong) NSArray *selectedModels; @property (nonatomic, assign) NSUInteger selectedCount; @property (nonatomic, assign) BOOL isCameraRoll; - (void)setResult:(PHFetchResult *)result needFetchAssets:(BOOL)needFetchAssets; - (void)refreshFetchResult; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZAssetModel.m ================================================ // // TZAssetModel.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "TZAssetModel.h" #import "TZImageManager.h" @implementation TZAssetModel + (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type{ TZAssetModel *model = [[TZAssetModel alloc] init]; model.asset = asset; model.isSelected = NO; model.type = type; return model; } + (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type timeLength:(NSString *)timeLength { TZAssetModel *model = [self modelWithAsset:asset type:type]; model.timeLength = timeLength; return model; } @end @implementation TZAlbumModel - (void)setResult:(PHFetchResult *)result needFetchAssets:(BOOL)needFetchAssets { _result = result; if (needFetchAssets) { [[TZImageManager manager] getAssetsFromFetchResult:result completion:^(NSArray *models) { self->_models = models; if (self->_selectedModels) { [self checkSelectedModels]; } }]; } } - (void)refreshFetchResult { PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:self.collection options:self.options]; self.count = fetchResult.count; [self setResult:fetchResult]; } - (void)setSelectedModels:(NSArray *)selectedModels { _selectedModels = selectedModels; if (_models) { [self checkSelectedModels]; } } - (void)checkSelectedModels { self.selectedCount = 0; NSMutableSet *selectedAssets = [NSMutableSet setWithCapacity:_selectedModels.count]; for (TZAssetModel *model in _selectedModels) { [selectedAssets addObject:model.asset]; } for (TZAssetModel *model in _models) { if ([selectedAssets containsObject:model.asset]) { self.selectedCount ++; } } } - (NSString *)name { if (_name) { return _name; } return @""; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZAuthLimitedFooterTipView.h ================================================ // // TZAuthLimitedFooterTipView.h // TZImagePickerController // // Created by qiaoxy on 2021/8/24. // #import NS_ASSUME_NONNULL_BEGIN @interface TZAuthLimitedFooterTipView : UIView @end NS_ASSUME_NONNULL_END ================================================ FILE: TZImagePickerController/TZImagePickerController/TZAuthLimitedFooterTipView.m ================================================ // // TZAuthLimitedFooterTipView.m // TZImagePickerController // // Created by qiaoxy on 2021/8/24. // #import "TZAuthLimitedFooterTipView.h" #import "TZImagePickerController.h" @interface TZAuthLimitedFooterTipView() @property (nonatomic,strong) UIImageView *tipImgView; @property (nonatomic,strong) UILabel *tipLable; @property (nonatomic,strong) UIImageView *detailImgView; @end @implementation TZAuthLimitedFooterTipView - (instancetype)init { self = [super init]; if (self) { [self initSubViews]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initSubViews]; } return self; } - (void)initSubViews { [self addSubview:self.tipImgView]; [self addSubview:self.tipLable]; [self addSubview:self.detailImgView]; CGFloat margin = 15; CGFloat tipImgViewWH = 20; CGFloat detailImgViewWH = 12; CGFloat screenW = [UIScreen mainScreen].bounds.size.width; self.tipImgView.frame = CGRectMake(margin, 0, tipImgViewWH, tipImgViewWH); self.detailImgView.frame = CGRectMake(screenW - margin - detailImgViewWH, 0, detailImgViewWH, detailImgViewWH); CGFloat tipLabelX = CGRectGetMaxX(self.tipImgView.frame) + 10; CGFloat tipLabelW = screenW - tipLabelX - detailImgViewWH - margin - 4; self.tipLable.frame = CGRectMake(tipLabelX, 0, tipLabelW, self.bounds.size.height); self.tipImgView.center = CGPointMake(self.tipImgView.center.x, self.tipLable.center.y); self.detailImgView.center = CGPointMake(self.detailImgView.center.x, self.tipLable.center.y); } #pragma mark - Getter - (UIImageView *)tipImgView { if (!_tipImgView) { _tipImgView = [[UIImageView alloc] init]; _tipImgView.contentMode = UIViewContentModeScaleAspectFit; _tipImgView.image = [UIImage tz_imageNamedFromMyBundle:@"tip"]; } return _tipImgView; } - (UILabel *)tipLable { if (!_tipLable) { _tipLable = [[UILabel alloc] init]; NSString *appName = [TZCommonTools tz_getAppName]; _tipLable.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Allow %@ to access your all photos"], appName]; _tipLable.numberOfLines = 0; _tipLable.font = [UIFont systemFontOfSize:14]; _tipLable.textColor = [UIColor colorWithRed:0.40 green:0.40 blue:0.40 alpha:1.0]; } return _tipLable; } - (UIImageView *)detailImgView { if (!_detailImgView) { _detailImgView = [[UIImageView alloc] init]; _detailImgView.contentMode = UIViewContentModeScaleAspectFit; _detailImgView.image = [UIImage tz_imageNamedFromMyBundle:@"right_arrow"]; } return _detailImgView; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZGifPhotoPreviewController.h ================================================ // // TZGifPhotoPreviewController.h // TZImagePickerController // // Created by ttouch on 2016/12/13. // Copyright © 2016年 谭真. All rights reserved. // #import @class TZAssetModel; @interface TZGifPhotoPreviewController : UIViewController @property (nonatomic, strong) TZAssetModel *model; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZGifPhotoPreviewController.m ================================================ // // TZGifPhotoPreviewController.m // TZImagePickerController // // Created by ttouch on 2016/12/13. // Copyright © 2016年 谭真. All rights reserved. // #import "TZGifPhotoPreviewController.h" #import "TZImagePickerController.h" #import "TZAssetModel.h" #import "UIView+TZLayout.h" #import "TZPhotoPreviewCell.h" #import "TZImageManager.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @interface TZGifPhotoPreviewController () { UIView *_toolBar; UIButton *_doneButton; UILabel *_byteLabel; UIProgressView *_progress; TZPhotoPreviewView *_previewView; UIStatusBarStyle _originStatusBarStyle; } @property (assign, nonatomic) BOOL needShowStatusBar; @end @implementation TZGifPhotoPreviewController - (void)viewDidLoad { [super viewDidLoad]; self.needShowStatusBar = ![UIApplication sharedApplication].statusBarHidden; self.view.backgroundColor = [UIColor blackColor]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc) { self.navigationItem.title = [NSString stringWithFormat:@"GIF %@",tzImagePickerVc.previewBtnTitleStr]; } [self configPreviewView]; [self configBottomToolBar]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; _originStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if (self.needShowStatusBar) { [UIApplication sharedApplication].statusBarHidden = NO; } [UIApplication sharedApplication].statusBarStyle = _originStatusBarStyle; } - (void)configPreviewView { _previewView = [[TZPhotoPreviewView alloc] initWithFrame:CGRectZero]; _previewView.model = self.model; __weak typeof(self) weakSelf = self; [_previewView setSingleTapGestureBlock:^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf signleTapAction]; }]; [self.view addSubview:_previewView]; } - (void)configBottomToolBar { _toolBar = [[UIView alloc] initWithFrame:CGRectZero]; CGFloat rgb = 34 / 255.0; _toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7]; _doneButton = [UIButton buttonWithType:UIButtonTypeCustom]; _doneButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc) { [_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal]; [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal]; } else { [_doneButton setTitle:[NSBundle tz_localizedStringForKey:@"Done"] forState:UIControlStateNormal]; [_doneButton setTitleColor:[UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:1.0] forState:UIControlStateNormal]; } [_toolBar addSubview:_doneButton]; _byteLabel = [[UILabel alloc] init]; _byteLabel.textColor = [UIColor whiteColor]; _byteLabel.font = [UIFont systemFontOfSize:13]; __weak typeof(_byteLabel) byteLabel = _byteLabel; [[TZImageManager manager] getPhotosBytesWithArray:@[_model] completion:^(NSString *totalBytes) { byteLabel.text = totalBytes; }]; [_toolBar addSubview:_byteLabel]; [self.view addSubview:_toolBar]; if (tzImagePickerVc.gifPreviewPageUIConfigBlock) { tzImagePickerVc.gifPreviewPageUIConfigBlock(_toolBar, _doneButton); } } - (UIStatusBarStyle)preferredStatusBarStyle { TZImagePickerController *tzImagePicker = (TZImagePickerController *)self.navigationController; if (tzImagePicker && [tzImagePicker isKindOfClass:[TZImagePickerController class]]) { return tzImagePicker.statusBarStyle; } return [super preferredStatusBarStyle]; } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; _previewView.frame = self.view.bounds; _previewView.scrollView.frame = self.view.bounds; CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom; _toolBar.frame = CGRectMake(0, self.view.tz_height - toolBarHeight, self.view.tz_width, toolBarHeight); [_doneButton sizeToFit]; if ([TZCommonTools tz_isRightToLeftLayout]) { _doneButton.frame = CGRectMake(12, 0, MAX(44, _doneButton.tz_width), 44); _byteLabel.frame = CGRectMake(self.view.tz_width - 100 - 10, 0, 100, 44); } else { _doneButton.frame = CGRectMake(self.view.tz_width - _doneButton.tz_width - 12, 0, MAX(44, _doneButton.tz_width), 44); _byteLabel.frame = CGRectMake(10, 0, 100, 44); } TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc.gifPreviewPageDidLayoutSubviewsBlock) { tzImagePickerVc.gifPreviewPageDidLayoutSubviewsBlock(_toolBar, _doneButton); } } #pragma mark - Click Event - (void)signleTapAction { _toolBar.hidden = !_toolBar.isHidden; [self.navigationController setNavigationBarHidden:_toolBar.isHidden]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (_toolBar.isHidden) { [UIApplication sharedApplication].statusBarHidden = YES; } else if (tzImagePickerVc.needShowStatusBar) { [UIApplication sharedApplication].statusBarHidden = NO; } } - (void)doneButtonClick { if (self.navigationController) { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; if (imagePickerVc.autoDismiss) { [self.navigationController dismissViewControllerAnimated:YES completion:^{ [self callDelegateMethod]; }]; } else { [self callDelegateMethod]; } } else { [self dismissViewControllerAnimated:YES completion:^{ [self callDelegateMethod]; }]; } } - (void)callDelegateMethod { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; UIImage *animatedImage = _previewView.imageView.image; if ([imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingGifImage:sourceAssets:)]) { [imagePickerVc.pickerDelegate imagePickerController:imagePickerVc didFinishPickingGifImage:animatedImage sourceAssets:_model.asset]; } if (imagePickerVc.didFinishPickingGifImageHandle) { imagePickerVc.didFinishPickingGifImageHandle(animatedImage,_model.asset); } } #pragma clang diagnostic pop @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImageCropManager.h ================================================ // // TZImageCropManager.h // TZImagePickerController // // Created by 谭真 on 2016/12/5. // Copyright © 2016年 谭真. All rights reserved. // 图片裁剪管理类 #import #import @interface TZImageCropManager : NSObject /// 裁剪框背景的处理 + (void)overlayClippingWithView:(UIView *)view cropRect:(CGRect)cropRect containerView:(UIView *)containerView needCircleCrop:(BOOL)needCircleCrop; /* 1.7.2 为了解决多位同学对于图片裁剪的需求,我这两天有空便在研究图片裁剪 幸好有tuyou的PhotoTweaks库做参考,裁剪的功能实现起来简单许多 该方法和其内部引用的方法基本来自于tuyou的PhotoTweaks库,我做了稍许删减和修改 感谢tuyou同学在github开源了优秀的裁剪库PhotoTweaks,表示感谢 PhotoTweaks库的github链接:https://github.com/itouch2/PhotoTweaks */ /// 获得裁剪后的图片 + (UIImage *)cropImageView:(UIImageView *)imageView toRect:(CGRect)rect zoomScale:(double)zoomScale containerView:(UIView *)containerView; /// 获取圆形图片 + (UIImage *)circularClipImage:(UIImage *)image; @end /// 该分类的代码来自SDWebImage:https://github.com/rs/SDWebImage /// 为了防止冲突,我将分类名字和方法名字做了修改 @interface UIImage (TZGif) + (UIImage *)sd_tz_animatedGIFWithData:(NSData *)data; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImageCropManager.m ================================================ // // TZImageCropManager.m // TZImagePickerController // // Created by 谭真 on 2016/12/5. // Copyright © 2016年 谭真. All rights reserved. // #import "TZImageCropManager.h" #import "UIView+TZLayout.h" #import #import "TZImageManager.h" #import "TZImagePickerController.h" @implementation TZImageCropManager /// 裁剪框背景的处理 + (void)overlayClippingWithView:(UIView *)view cropRect:(CGRect)cropRect containerView:(UIView *)containerView needCircleCrop:(BOOL)needCircleCrop { UIBezierPath *path= [UIBezierPath bezierPathWithRect:[UIScreen mainScreen].bounds]; CAShapeLayer *layer = [CAShapeLayer layer]; if (needCircleCrop) { // 圆形裁剪框 [path appendPath:[UIBezierPath bezierPathWithRoundedRect:cropRect cornerRadius:cropRect.size.width / 2]]; } else { // 矩形裁剪框 [path appendPath:[UIBezierPath bezierPathWithRect:cropRect]]; } layer.path = path.CGPath; layer.fillRule = kCAFillRuleEvenOdd; layer.fillColor = [[UIColor blackColor] CGColor]; layer.opacity = 0.5; [view.layer addSublayer:layer]; } /// 获得裁剪后的图片 + (UIImage *)cropImageView:(UIImageView *)imageView toRect:(CGRect)rect zoomScale:(double)zoomScale containerView:(UIView *)containerView { CGAffineTransform transform = CGAffineTransformIdentity; // 平移的处理 CGRect imageViewRect = [imageView convertRect:imageView.bounds toView:containerView]; CGPoint point = CGPointMake(imageViewRect.origin.x + imageViewRect.size.width / 2, imageViewRect.origin.y + imageViewRect.size.height / 2); CGFloat xMargin = containerView.tz_width - CGRectGetMaxX(rect) - rect.origin.x; CGPoint zeroPoint = CGPointMake((CGRectGetWidth(containerView.frame) - xMargin) / 2, containerView.center.y); CGPoint translation = CGPointMake(point.x - zeroPoint.x, point.y - zeroPoint.y); transform = CGAffineTransformTranslate(transform, translation.x, translation.y); // 缩放的处理 transform = CGAffineTransformScale(transform, zoomScale, zoomScale); CGImageRef imageRef = [self newTransformedImage:transform sourceImage:imageView.image.CGImage sourceSize:imageView.image.size outputWidth:rect.size.width * [UIScreen mainScreen].scale cropSize:rect.size imageViewSize:imageView.frame.size]; UIImage *cropedImage = [UIImage imageWithCGImage:imageRef]; cropedImage = [[TZImageManager manager] fixOrientation:cropedImage]; CGImageRelease(imageRef); return cropedImage; } + (CGImageRef)newTransformedImage:(CGAffineTransform)transform sourceImage:(CGImageRef)sourceImage sourceSize:(CGSize)sourceSize outputWidth:(CGFloat)outputWidth cropSize:(CGSize)cropSize imageViewSize:(CGSize)imageViewSize { CGImageRef source = [self newScaledImage:sourceImage toSize:sourceSize]; CGFloat aspect = cropSize.height/cropSize.width; CGSize outputSize = CGSizeMake(outputWidth, outputWidth*aspect); CGContextRef context = CGBitmapContextCreate(NULL, outputSize.width, outputSize.height, CGImageGetBitsPerComponent(source), 0, CGImageGetColorSpace(source), CGImageGetBitmapInfo(source)); CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]); CGContextFillRect(context, CGRectMake(0, 0, outputSize.width, outputSize.height)); CGAffineTransform uiCoords = CGAffineTransformMakeScale(outputSize.width / cropSize.width, outputSize.height / cropSize.height); uiCoords = CGAffineTransformTranslate(uiCoords, cropSize.width/2.0, cropSize.height / 2.0); uiCoords = CGAffineTransformScale(uiCoords, 1.0, -1.0); CGContextConcatCTM(context, uiCoords); CGContextConcatCTM(context, transform); CGContextScaleCTM(context, 1.0, -1.0); CGContextDrawImage(context, CGRectMake(-imageViewSize.width/2, -imageViewSize.height/2.0, imageViewSize.width, imageViewSize.height), source); CGImageRef resultRef = CGBitmapContextCreateImage(context); CGContextRelease(context); CGImageRelease(source); return resultRef; } + (CGImageRef)newScaledImage:(CGImageRef)source toSize:(CGSize)size { CGSize srcSize = size; CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, rgbColorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGColorSpaceRelease(rgbColorSpace); CGContextSetInterpolationQuality(context, kCGInterpolationNone); CGContextTranslateCTM(context, size.width/2, size.height/2); CGContextDrawImage(context, CGRectMake(-srcSize.width/2, -srcSize.height/2, srcSize.width, srcSize.height), source); CGImageRef resultRef = CGBitmapContextCreateImage(context); CGContextRelease(context); return resultRef; } /// 获取圆形图片 + (UIImage *)circularClipImage:(UIImage *)image { UIGraphicsBeginImageContextWithOptions(image.size, NO, [UIScreen mainScreen].scale); CGContextRef ctx = UIGraphicsGetCurrentContext(); CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height); CGContextAddEllipseInRect(ctx, rect); CGContextClip(ctx); [image drawInRect:rect]; UIImage *circleImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return circleImage; } @end @implementation UIImage (TZGif) + (UIImage *)sd_tz_animatedGIFWithData:(NSData *)data { if (!data) { return nil; } CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); size_t count = CGImageSourceGetCount(source); UIImage *animatedImage; if (count <= 1) { animatedImage = [[UIImage alloc] initWithData:data]; } else { // images数组过大时内存会飙升,在这里限制下最大count NSInteger maxCount = [TZImagePickerConfig sharedInstance].gifPreviewMaxImagesCount ?: 50; NSInteger interval = MAX((count + maxCount / 2) / maxCount, 1); NSMutableArray *images = [NSMutableArray array]; NSTimeInterval duration = 0.0f; for (size_t i = 0; i < count; i+=interval) { CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL); if (!image) { continue; } duration += [self sd_frameDurationAtIndex:i source:source] * MIN(interval, 3); [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]]; CGImageRelease(image); } if (!duration) { duration = (1.0f / 10.0f) * count; } animatedImage = [UIImage animatedImageWithImages:images duration:duration]; } CFRelease(source); return animatedImage; } + (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source { float frameDuration = 0.1f; CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; if (delayTimeUnclampedProp) { frameDuration = [delayTimeUnclampedProp floatValue]; } else { NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; if (delayTimeProp) { frameDuration = [delayTimeProp floatValue]; } } // Many annoying ads specify a 0 duration to make an image flash as quickly as possible. // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify // a duration of <= 10 ms. See and // for more information. if (frameDuration < 0.011f) { frameDuration = 0.100f; } CFRelease(cfFrameProperties); return frameDuration; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImageManager.h ================================================ // // TZImageManager.h // TZImagePickerController // // Created by 谭真 on 16/1/4. // Copyright © 2016年 谭真. All rights reserved. // 图片资源获取管理类 #import #import #import #import #import "TZAssetModel.h" @class TZAlbumModel,TZAssetModel; @protocol TZImagePickerControllerDelegate; @interface TZImageManager : NSObject @property (nonatomic, strong) PHCachingImageManager *cachingImageManager; + (instancetype)manager NS_SWIFT_NAME(default()); + (void)deallocManager; @property (weak, nonatomic) id pickerDelegate; @property (nonatomic, assign) BOOL shouldFixOrientation; @property (nonatomic, assign) BOOL isPreviewNetworkImage; /// Default is 600px / 默认600像素宽 @property (nonatomic, assign) CGFloat photoPreviewMaxWidth; /// The pixel width of output image, Default is 828px / 导出图片的宽度,默认828像素宽 @property (nonatomic, assign) CGFloat photoWidth; /// Default is 4, Use in photos collectionView in TZPhotoPickerController /// 默认4列, TZPhotoPickerController中的照片collectionView @property (nonatomic, assign) NSInteger columnNumber; /// Sort photos ascending by modificationDate,Default is YES /// 对照片排序,按修改时间升序,默认是YES。如果设置为NO,最新的照片会显示在最前面,内部的拍照按钮会排在第一个 @property (nonatomic, assign) BOOL sortAscendingByModificationDate; /// Minimum selectable photo width, Default is 0 /// 最小可选中的图片宽度,默认是0,小于这个宽度的图片不可选中 @property (nonatomic, assign) NSInteger minPhotoWidthSelectable; @property (nonatomic, assign) NSInteger minPhotoHeightSelectable; @property (nonatomic, assign) BOOL hideWhenCanNotSelect; /// Return YES if Authorized 返回YES如果得到了授权 - (BOOL)authorizationStatusAuthorized; - (void)requestAuthorizationWithCompletion:(void (^)(void))completion; - (BOOL)isPHAuthorizationStatusLimited; /// Get Album 获得相册/相册数组 - (void)getCameraRollAlbumWithFetchAssets:(BOOL)needFetchAssets completion:(void (^)(TZAlbumModel *model))completion; - (void)getCameraRollAlbum:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage needFetchAssets:(BOOL)needFetchAssets completion:(void (^)(TZAlbumModel *model))completion __attribute__((deprecated("Use -getCameraRollAlbumWithFetchAssets:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig"))); - (void)getAllAlbums:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage needFetchAssets:(BOOL)needFetchAssets completion:(void (^)(NSArray *models))completion __attribute__((deprecated("Use -getAllAlbumsWithFetchAssets:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig"))); - (void)getAllAlbumsWithFetchAssets:(BOOL)needFetchAssets completion:(void (^)(NSArray *models))completion; /// Get Assets 获得Asset数组 - (void)getAssetsFromFetchResult:(PHFetchResult *)result completion:(void (^)(NSArray *models))completion; - (void)getAssetsFromFetchResult:(PHFetchResult *)result allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage completion:(void (^)(NSArray *models))completion __attribute__((deprecated("Use -getAssetsFromFetchResult:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig"))); - (void)getAssetFromFetchResult:(PHFetchResult *)result atIndex:(NSInteger)index allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage completion:(void (^)(TZAssetModel *model))completion __attribute__((deprecated("Use -getAssetFromFetchResult:atIndex:completion:. You can config allowPickingImage、allowPickingVideo by TZImagePickerConfig"))); - (void)getAssetFromFetchResult:(PHFetchResult *)result atIndex:(NSInteger)index completion:(void (^)(TZAssetModel *model))completion; /// Get photo 获得照片 - (PHImageRequestID)getPostImageWithAlbumModel:(TZAlbumModel *)model completion:(void (^)(UIImage *postImage))completion; - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion; - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset photoWidth:(CGFloat)photoWidth completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion; - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler networkAccessAllowed:(BOOL)networkAccessAllowed; - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset photoWidth:(CGFloat)photoWidth completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler networkAccessAllowed:(BOOL)networkAccessAllowed; - (PHImageRequestID)requestImageDataForAsset:(PHAsset *)asset completion:(void (^)(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler; /// Get full Image 获取原图 /// 如下两个方法completion一般会调多次,一般会先返回缩略图,再返回原图(详见方法内部使用的系统API的说明),如果info[PHImageResultIsDegradedKey] 为 YES,则表明当前返回的是缩略图,否则是原图。 - (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info))completion; - (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset newCompletion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion; - (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler newCompletion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion; // 该方法中,completion只会走一次 - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion; - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion; /// Get Image For VideoURL - (UIImage *)getImageWithVideoURL:(NSURL *)videoURL; /// Save photo 保存照片 - (void)savePhotoWithImage:(UIImage *)image completion:(void (^)(PHAsset *asset, NSError *error))completion; - (void)savePhotoWithImage:(UIImage *)image location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion; - (void)savePhotoWithImage:(UIImage *)image meta:(NSDictionary *)meta location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion; /// Save video 保存视频 - (void)saveVideoWithUrl:(NSURL *)url completion:(void (^)(PHAsset *asset, NSError *error))completion; - (void)saveVideoWithUrl:(NSURL *)url location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion; /// Get video 获得视频 - (void)getVideoWithAsset:(PHAsset *)asset completion:(void (^)(AVPlayerItem * playerItem, NSDictionary * info))completion; - (void)getVideoWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(AVPlayerItem *, NSDictionary *))completion; /// Export video 导出视频 presetName: 预设名字,默认值是AVAssetExportPreset640x480 - (void)getVideoOutputPathWithAsset:(PHAsset *)asset success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure; - (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure; - (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName timeRange:(CMTimeRange)timeRange success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure; /// 新的导出视频API,解决iOS14 iCloud视频导出失败的问题,未大量测试,请大家多多测试,有问题群里反馈 - (void)requestVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure; /// 得到视频原始文件地址 - (void)requestVideoURLWithAsset:(PHAsset *)asset success:(void (^)(NSURL *videoURL))success failure:(void (^)(NSDictionary* info))failure; /// Get photo bytes 获得一组照片的大小 - (void)getPhotosBytesWithArray:(NSArray *)photos completion:(void (^)(NSString *totalBytes))completion; - (BOOL)isCameraRollAlbum:(PHAssetCollection *)metadata; /// 检查照片大小是否满足最小要求 - (BOOL)isPhotoSelectableWithAsset:(PHAsset *)asset; /// 检查照片能否被选中 - (BOOL)isAssetCannotBeSelected:(PHAsset *)asset; /// 修正图片转向 - (UIImage *)fixOrientation:(UIImage *)aImage; /// 获取asset的资源类型 - (TZAssetModelMediaType)getAssetType:(PHAsset *)asset; /// 缩放图片至新尺寸 - (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size; /// 判断asset是否是视频 - (BOOL)isVideo:(PHAsset *)asset; /// for TZImagePreviewController - (NSString *)getNewTimeFromDurationSecond:(NSInteger)duration; - (TZAssetModel *)createModelWithAsset:(PHAsset *)asset; @end //@interface TZSortDescriptor : NSSortDescriptor // //@end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImageManager.m ================================================ // // TZImageManager.m // TZImagePickerController // // Created by 谭真 on 16/1/4. // Copyright © 2016年 谭真. All rights reserved. // #import "TZImageManager.h" #import "TZAssetModel.h" #import "TZImagePickerController.h" #import @interface TZImageManager () #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @end @implementation TZImageManager CGSize AssetGridThumbnailSize; CGFloat TZScreenWidth; CGFloat TZScreenScale; static TZImageManager *manager; static dispatch_once_t onceToken; + (instancetype)manager { dispatch_once(&onceToken, ^{ manager = [[self alloc] init]; // manager.cachingImageManager = [[PHCachingImageManager alloc] init]; // manager.cachingImageManager.allowsCachingHighQualityImages = YES; [manager configTZScreenWidth]; }); return manager; } + (void)deallocManager { onceToken = 0; manager = nil; } - (void)setPhotoWidth:(CGFloat)photoWidth { _photoWidth = photoWidth; TZScreenWidth = photoWidth / 2; } - (void)setColumnNumber:(NSInteger)columnNumber { [self configTZScreenWidth]; _columnNumber = columnNumber; CGFloat margin = 4; CGFloat itemWH = (TZScreenWidth - 2 * margin - 4) / columnNumber - margin; AssetGridThumbnailSize = CGSizeMake(itemWH * TZScreenScale, itemWH * TZScreenScale); } - (void)configTZScreenWidth { TZScreenWidth = [UIScreen mainScreen].bounds.size.width; // 测试发现,如果scale在plus真机上取到3.0,内存会增大特别多。故这里写死成2.0 TZScreenScale = 2.0; if (TZScreenWidth > 700) { TZScreenScale = 1.5; } } - (BOOL)isPHAuthorizationStatusLimited { if (@available(iOS 14,*)) { NSInteger status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite]; if (status == PHAuthorizationStatusLimited) { return YES; } } return NO; } /// Return YES if Authorized 返回YES如果得到了授权 - (BOOL)authorizationStatusAuthorized { if (self.isPreviewNetworkImage) { return YES; } NSInteger status = [PHPhotoLibrary authorizationStatus]; if (status == 0) { /** * 当某些情况下AuthorizationStatus == AuthorizationStatusNotDetermined时,无法弹出系统首次使用的授权alertView,系统应用设置里亦没有相册的设置,此时将无法使用,故作以下操作,弹出系统首次使用的授权alertView */ [self requestAuthorizationWithCompletion:nil]; } return status == 3; } - (void)requestAuthorizationWithCompletion:(void (^)(void))completion { void (^callCompletionBlock)(void) = ^(){ dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(); } }); }; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { callCompletionBlock(); }]; }); } #pragma mark - Get Album - (void)getCameraRollAlbum:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage needFetchAssets:(BOOL)needFetchAssets completion:(void (^)(TZAlbumModel *model))completion { TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; config.allowPickingVideo = allowPickingVideo; config.allowPickingImage = allowPickingImage; [self getCameraRollAlbumWithFetchAssets:needFetchAssets completion:completion]; } /// Get Album 获得相册/相册数组 - (void)getCameraRollAlbumWithFetchAssets:(BOOL)needFetchAssets completion:(void (^)(TZAlbumModel *model))completion { __block TZAlbumModel *model; TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; PHFetchOptions *option = [[PHFetchOptions alloc] init]; if (!config.allowPickingVideo) option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeImage]; if (!config.allowPickingImage) option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeVideo]; // option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"modificationDate" ascending:self.sortAscendingByModificationDate]]; if (!self.sortAscendingByModificationDate) { option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:self.sortAscendingByModificationDate]]; } PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny options:nil]; for (PHAssetCollection *collection in smartAlbums) { // 有可能是PHCollectionList类的的对象,过滤掉 if (![collection isKindOfClass:[PHAssetCollection class]]) continue; // 过滤空相册 if (collection.estimatedAssetCount <= 0) continue; if ([self isCameraRollAlbum:collection]) { PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:option]; model = [self modelWithResult:fetchResult collection:collection isCameraRoll:YES needFetchAssets:needFetchAssets options:option]; if (completion) completion(model); break; } } } - (void)getAllAlbums:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage needFetchAssets:(BOOL)needFetchAssets completion:(void (^)(NSArray *))completion { TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; config.allowPickingVideo = allowPickingVideo; config.allowPickingImage = allowPickingImage; [self getAllAlbumsWithFetchAssets:needFetchAssets completion:completion]; } - (void)getAllAlbumsWithFetchAssets:(BOOL)needFetchAssets completion:(void (^)(NSArray *))completion { TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; NSMutableArray *albumArr = [NSMutableArray array]; PHFetchOptions *option = [[PHFetchOptions alloc] init]; if (!config.allowPickingVideo) option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeImage]; if (!config.allowPickingImage) option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeVideo]; // option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"modificationDate" ascending:self.sortAscendingByModificationDate]]; if (!self.sortAscendingByModificationDate) { option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:self.sortAscendingByModificationDate]]; } // 我的照片流 1.6.10重新加入.. PHFetchResult *myPhotoStreamAlbum = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumMyPhotoStream options:nil]; PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny options:nil]; PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil]; PHFetchResult *syncedAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumSyncedAlbum options:nil]; PHFetchResult *sharedAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumCloudShared options:nil]; NSArray *allAlbums = @[myPhotoStreamAlbum,smartAlbums,topLevelUserCollections,syncedAlbums,sharedAlbums]; for (PHFetchResult *fetchResult in allAlbums) { for (PHAssetCollection *collection in fetchResult) { // 有可能是PHCollectionList类的的对象,过滤掉 if (![collection isKindOfClass:[PHAssetCollection class]]) continue; // 过滤空相册 if (collection.estimatedAssetCount <= 0 && ![self isCameraRollAlbum:collection]) continue; PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:option]; if (fetchResult.count < 1 && ![self isCameraRollAlbum:collection]) continue; if ([self.pickerDelegate respondsToSelector:@selector(isAlbumCanSelect:result:)]) { if (![self.pickerDelegate isAlbumCanSelect:collection.localizedTitle result:fetchResult]) { continue; } } if (collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumAllHidden) continue; if (collection.assetCollectionSubtype == 1000000201) continue; //『最近删除』相册 if ([self isCameraRollAlbum:collection]) { [albumArr insertObject:[self modelWithResult:fetchResult collection:collection isCameraRoll:YES needFetchAssets:needFetchAssets options:option] atIndex:0]; } else { [albumArr addObject:[self modelWithResult:fetchResult collection:collection isCameraRoll:NO needFetchAssets:needFetchAssets options:option]]; } } } if (completion) { completion(albumArr); } } #pragma mark - Get Assets /// Get Assets 获得照片数组 - (void)getAssetsFromFetchResult:(PHFetchResult *)result allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage completion:(void (^)(NSArray *))completion { TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; config.allowPickingVideo = allowPickingVideo; config.allowPickingImage = allowPickingImage; return [self getAssetsFromFetchResult:result completion:completion]; } - (void)getAssetsFromFetchResult:(PHFetchResult *)result completion:(void (^)(NSArray *))completion { TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; NSMutableArray *photoArr = [NSMutableArray array]; [result enumerateObjectsUsingBlock:^(PHAsset *asset, NSUInteger idx, BOOL * _Nonnull stop) { TZAssetModel *model = [self assetModelWithAsset:asset allowPickingVideo:config.allowPickingVideo allowPickingImage:config.allowPickingImage]; if (model) { [photoArr addObject:model]; } }]; if (completion) completion(photoArr); } /// Get asset at index 获得下标为index的单个照片 /// if index beyond bounds, return nil in callback 如果索引越界, 在回调中返回 nil - (void)getAssetFromFetchResult:(PHFetchResult *)result atIndex:(NSInteger)index allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage completion:(void (^)(TZAssetModel *))completion { TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; config.allowPickingVideo = allowPickingVideo; config.allowPickingImage = allowPickingImage; [self getAssetFromFetchResult:result atIndex:index allowPickingVideo:config.allowPickingVideo allowPickingImage:config.allowPickingImage completion:completion]; } - (void)getAssetFromFetchResult:(PHFetchResult *)result atIndex:(NSInteger)index completion:(void (^)(TZAssetModel *))completion { PHAsset *asset; @try { asset = result[index]; } @catch (NSException* e) { if (completion) completion(nil); return; } TZImagePickerConfig *config = [TZImagePickerConfig sharedInstance]; TZAssetModel *model = [self assetModelWithAsset:asset allowPickingVideo:config.allowPickingVideo allowPickingImage:config.allowPickingImage]; if (completion) completion(model); } - (TZAssetModel *)assetModelWithAsset:(PHAsset *)asset allowPickingVideo:(BOOL)allowPickingVideo allowPickingImage:(BOOL)allowPickingImage { BOOL canSelect = YES; if ([self.pickerDelegate respondsToSelector:@selector(isAssetCanSelect:)]) { canSelect = [self.pickerDelegate isAssetCanSelect:asset]; } if ([self.pickerDelegate respondsToSelector:@selector(isAssetCanBeDisplayed:)]){ canSelect = [self.pickerDelegate isAssetCanBeDisplayed:asset]; } if (!canSelect) return nil; TZAssetModel *model; TZAssetModelMediaType type = [self getAssetType:asset]; if (!allowPickingVideo && type == TZAssetModelMediaTypeVideo) return nil; if (!allowPickingImage && type == TZAssetModelMediaTypePhoto) return nil; if (!allowPickingImage && type == TZAssetModelMediaTypePhotoGif) return nil; PHAsset *phAsset = (PHAsset *)asset; if (self.hideWhenCanNotSelect) { // 过滤掉尺寸不满足要求的图片 if (![self isPhotoSelectableWithAsset:phAsset]) { return nil; } } NSString *timeLength = type == TZAssetModelMediaTypeVideo ? [NSString stringWithFormat:@"%0.0f",phAsset.duration] : @""; timeLength = [self getNewTimeFromDurationSecond:timeLength.integerValue]; model = [TZAssetModel modelWithAsset:asset type:type timeLength:timeLength]; return model; } - (TZAssetModelMediaType)getAssetType:(PHAsset *)asset { TZAssetModelMediaType type = TZAssetModelMediaTypePhoto; PHAsset *phAsset = (PHAsset *)asset; if (phAsset.mediaType == PHAssetMediaTypeVideo) type = TZAssetModelMediaTypeVideo; else if (phAsset.mediaType == PHAssetMediaTypeAudio) type = TZAssetModelMediaTypeAudio; else if (phAsset.mediaType == PHAssetMediaTypeImage) { if (@available(iOS 9.1, *)) { // if (asset.mediaSubtypes == PHAssetMediaSubtypePhotoLive) type = TZAssetModelMediaTypeLivePhoto; } // Gif if ([[phAsset valueForKey:@"filename"] hasSuffix:@"GIF"]) { type = TZAssetModelMediaTypePhotoGif; } } return type; } - (NSString *)getNewTimeFromDurationSecond:(NSInteger)duration { NSString *newTime; if (duration < 10) { newTime = [NSString stringWithFormat:@"0:0%zd",duration]; } else if (duration < 60) { newTime = [NSString stringWithFormat:@"0:%zd",duration]; } else { NSInteger min = duration / 60; NSInteger sec = duration - (min * 60); if (sec < 10) { newTime = [NSString stringWithFormat:@"%zd:0%zd",min,sec]; } else { newTime = [NSString stringWithFormat:@"%zd:%zd",min,sec]; } } return newTime; } /// Get photo bytes 获得一组照片的大小 - (void)getPhotosBytesWithArray:(NSArray *)photos completion:(void (^)(NSString *totalBytes))completion { if (!photos || !photos.count) { if (completion) completion(@"0B"); return; } __block NSInteger dataLength = 0; __block NSInteger assetCount = 0; for (NSInteger i = 0; i < photos.count; i++) { TZAssetModel *model = photos[i]; PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; options.resizeMode = PHImageRequestOptionsResizeModeFast; options.networkAccessAllowed = YES; if (model.type == TZAssetModelMediaTypePhotoGif) { options.version = PHImageRequestOptionsVersionOriginal; } [[PHImageManager defaultManager] requestImageDataForAsset:model.asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { if (model.type != TZAssetModelMediaTypeVideo) dataLength += imageData.length; assetCount ++; if (assetCount >= photos.count) { NSString *bytes = [self getBytesFromDataLength:dataLength]; if (completion) completion(bytes); } }]; } } - (NSString *)getBytesFromDataLength:(NSInteger)dataLength { NSString *bytes; if (dataLength >= 0.1 * (1024 * 1024)) { bytes = [NSString stringWithFormat:@"%0.1fM",dataLength/1024/1024.0]; } else if (dataLength >= 1024) { bytes = [NSString stringWithFormat:@"%0.0fK",dataLength/1024.0]; } else { bytes = [NSString stringWithFormat:@"%zdB",dataLength]; } return bytes; } #pragma mark - Get Photo /// Get photo 获得照片本身 - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *, NSDictionary *, BOOL isDegraded))completion { return [self getPhotoWithAsset:asset completion:completion progressHandler:nil networkAccessAllowed:YES]; } - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset photoWidth:(CGFloat)photoWidth completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion { return [self getPhotoWithAsset:asset photoWidth:photoWidth completion:completion progressHandler:nil networkAccessAllowed:YES]; } - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler networkAccessAllowed:(BOOL)networkAccessAllowed { CGFloat fullScreenWidth = TZScreenWidth; if (_photoPreviewMaxWidth > 0 && fullScreenWidth > _photoPreviewMaxWidth) { fullScreenWidth = _photoPreviewMaxWidth; } return [self getPhotoWithAsset:asset photoWidth:fullScreenWidth completion:completion progressHandler:progressHandler networkAccessAllowed:networkAccessAllowed]; } - (PHImageRequestID)requestImageDataForAsset:(PHAsset *)asset completion:(void (^)(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler { PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; options.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ if (progressHandler) { progressHandler(progress, error, stop, info); } }); }; options.networkAccessAllowed = YES; options.resizeMode = PHImageRequestOptionsResizeModeFast; int32_t imageRequestID = [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { if (completion) completion(imageData,dataUTI,orientation,info); }]; return imageRequestID; } - (PHImageRequestID)getPhotoWithAsset:(PHAsset *)asset photoWidth:(CGFloat)photoWidth completion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler networkAccessAllowed:(BOOL)networkAccessAllowed { CGSize imageSize; if (photoWidth < TZScreenWidth && photoWidth < _photoPreviewMaxWidth) { imageSize = AssetGridThumbnailSize; } else { PHAsset *phAsset = (PHAsset *)asset; CGFloat aspectRatio = phAsset.pixelWidth / (CGFloat)phAsset.pixelHeight; CGFloat pixelWidth = photoWidth * TZScreenScale; // 超宽图片 if (aspectRatio > 1.8) { pixelWidth = pixelWidth * aspectRatio; } // 超高图片 if (aspectRatio < 0.2) { pixelWidth = pixelWidth * 0.5; } CGFloat pixelHeight = pixelWidth / aspectRatio; imageSize = CGSizeMake(pixelWidth, pixelHeight); } // 修复获取图片时出现的瞬间内存过高问题 // 下面两行代码,来自hsjcom,他的github是:https://github.com/hsjcom 表示感谢 PHImageRequestOptions *option = [[PHImageRequestOptions alloc] init]; option.resizeMode = PHImageRequestOptionsResizeModeFast; int32_t imageRequestID = [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:imageSize contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage *result, NSDictionary *info) { BOOL cancelled = [[info objectForKey:PHImageCancelledKey] boolValue]; if (!cancelled && result) { result = [self fixOrientation:result]; if (completion) completion(result,info,[[info objectForKey:PHImageResultIsDegradedKey] boolValue]); } // Download image from iCloud / 从iCloud下载图片 if ([info objectForKey:PHImageResultIsInCloudKey] && !result && networkAccessAllowed) { PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; options.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ if (progressHandler) { progressHandler(progress, error, stop, info); } }); }; options.networkAccessAllowed = YES; options.resizeMode = PHImageRequestOptionsResizeModeFast; [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { UIImage *resultImage = [UIImage imageWithData:imageData]; if (![TZImagePickerConfig sharedInstance].notScaleImage) { resultImage = [self scaleImage:resultImage toSize:imageSize]; } if (!resultImage && result) { resultImage = result; } resultImage = [self fixOrientation:resultImage]; if (completion) completion(resultImage,info,NO); }]; } }]; return imageRequestID; } /// Get postImage / 获取封面图 - (PHImageRequestID)getPostImageWithAlbumModel:(TZAlbumModel *)model completion:(void (^)(UIImage *))completion { id asset = [model.result lastObject]; if (!self.sortAscendingByModificationDate) { asset = [model.result firstObject]; } if (!asset) { return -1; } return [[TZImageManager manager] getPhotoWithAsset:asset photoWidth:80 completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (completion) completion(photo); }]; } /// Get Original Photo / 获取原图 - (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset completion:(void (^)(UIImage *photo,NSDictionary *info))completion { return [self getOriginalPhotoWithAsset:asset newCompletion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (completion) { completion(photo,info); } }]; } - (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset newCompletion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion { return [self getOriginalPhotoWithAsset:asset progressHandler:nil newCompletion:completion]; } - (PHImageRequestID)getOriginalPhotoWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler newCompletion:(void (^)(UIImage *photo,NSDictionary *info,BOOL isDegraded))completion { PHImageRequestOptions *option = [[PHImageRequestOptions alloc]init]; option.networkAccessAllowed = YES; if (progressHandler) { [option setProgressHandler:progressHandler]; } option.resizeMode = PHImageRequestOptionsResizeModeFast; return [[PHImageManager defaultManager] requestImageDataForAsset:asset options:option resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) { BOOL cancelled = [[info objectForKey:PHImageCancelledKey] boolValue]; if (!cancelled && imageData) { UIImage *result = [self fixOrientation:[UIImage imageWithData:imageData]]; BOOL isDegraded = [[info objectForKey:PHImageResultIsDegradedKey] boolValue]; if (completion) completion(result,info,isDegraded); } }]; } - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion { return [self getOriginalPhotoDataWithAsset:asset progressHandler:nil completion:completion]; } - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion { PHImageRequestOptions *option = [[PHImageRequestOptions alloc] init]; option.networkAccessAllowed = YES; if ([[asset valueForKey:@"filename"] hasSuffix:@"GIF"]) { // if version isn't PHImageRequestOptionsVersionOriginal, the gif may cann't play option.version = PHImageRequestOptionsVersionOriginal; } [option setProgressHandler:progressHandler]; option.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; return [[PHImageManager defaultManager] requestImageDataForAsset:asset options:option resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { BOOL cancelled = [[info objectForKey:PHImageCancelledKey] boolValue]; if (!cancelled && imageData) { if (completion) completion(imageData,info,NO); } }]; } - (UIImage *)getImageWithVideoURL:(NSURL *)videoURL { AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:videoURL options:nil]; if (!asset) { return nil; } AVAssetImageGenerator *generator =[[AVAssetImageGenerator alloc] initWithAsset:asset]; generator.appliesPreferredTrackTransform = YES; generator.apertureMode = AVAssetImageGeneratorApertureModeEncodedPixels; CFTimeInterval time = 0.1; CGImageRef imageRef = [generator copyCGImageAtTime:CMTimeMake(time, 60) actualTime:NULL error:nil]; UIImage *image = [[UIImage alloc] initWithCGImage:imageRef]; CGImageRelease(imageRef); return image; } #pragma mark - Save photo - (void)savePhotoWithImage:(UIImage *)image completion:(void (^)(PHAsset *asset, NSError *error))completion { [self savePhotoWithImage:image location:nil completion:completion]; } - (void)savePhotoWithImage:(UIImage *)image location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion { __block NSString *localIdentifier = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ PHAssetChangeRequest *request = [PHAssetChangeRequest creationRequestForAssetFromImage:image]; localIdentifier = request.placeholderForCreatedAsset.localIdentifier; if (location) { request.location = location; } request.creationDate = [NSDate date]; } completionHandler:^(BOOL success, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (success && completion && localIdentifier) { [self fetchAssetByIocalIdentifier:localIdentifier retryCount:10 completion:completion]; } else { if (error) { NSLog(@"保存照片出错:%@",error.localizedDescription); } if (completion) { completion(nil, error); } } }); }]; } - (void)savePhotoWithImage:(UIImage *)image meta:(NSDictionary *)meta location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion { NSData *imageData = UIImageJPEGRepresentation(image, 1.0f); CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); NSDateFormatter *formater = [[NSDateFormatter alloc] init]; [formater setDateFormat:@"yyyy-MM-dd-HH:mm:ss-SSS"]; NSString *path = [NSTemporaryDirectory() stringByAppendingFormat:@"image-%@.jpg", [formater stringFromDate:[NSDate date]]]; NSURL *tmpURL = [NSURL fileURLWithPath:path]; CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)tmpURL, kUTTypeJPEG, 1, NULL); CGImageDestinationAddImageFromSource(destination, source, 0, (__bridge CFDictionaryRef)meta); CGImageDestinationFinalize(destination); CFRelease(source); CFRelease(destination); __block NSString *localIdentifier = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ PHAssetChangeRequest *request = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:tmpURL]; localIdentifier = request.placeholderForCreatedAsset.localIdentifier; if (location) { request.location = location; } request.creationDate = [NSDate date]; } completionHandler:^(BOOL success, NSError *error) { [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; dispatch_async(dispatch_get_main_queue(), ^{ if (success && completion && localIdentifier) { [self fetchAssetByIocalIdentifier:localIdentifier retryCount:10 completion:completion]; } else { if (error) { NSLog(@"保存照片出错:%@",error.localizedDescription); } if (completion) { completion(nil, error); } } }); }]; } - (void)fetchAssetByIocalIdentifier:(NSString *)localIdentifier retryCount:(NSInteger)retryCount completion:(void (^)(PHAsset *asset, NSError *error))completion { PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil] firstObject]; if (asset || retryCount <= 0) { if (completion) { completion(asset, nil); } return; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self fetchAssetByIocalIdentifier:localIdentifier retryCount:retryCount - 1 completion:completion]; }); } #pragma mark - Save video - (void)saveVideoWithUrl:(NSURL *)url completion:(void (^)(PHAsset *asset, NSError *error))completion { [self saveVideoWithUrl:url location:nil completion:completion]; } - (void)saveVideoWithUrl:(NSURL *)url location:(CLLocation *)location completion:(void (^)(PHAsset *asset, NSError *error))completion { __block NSString *localIdentifier = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ PHAssetChangeRequest *request = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url]; localIdentifier = request.placeholderForCreatedAsset.localIdentifier; if (location) { request.location = location; } request.creationDate = [NSDate date]; } completionHandler:^(BOOL success, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (success && completion && localIdentifier) { [self fetchAssetByIocalIdentifier:localIdentifier retryCount:10 completion:completion]; } else { if (error) { NSLog(@"保存视频出错:%@",error.localizedDescription); } if (completion) { completion(nil, error); } } }); }]; } #pragma mark - Get Video /// Get Video / 获取视频 - (void)getVideoWithAsset:(PHAsset *)asset completion:(void (^)(AVPlayerItem *, NSDictionary *))completion { [self getVideoWithAsset:asset progressHandler:nil completion:completion]; } - (void)getVideoWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(AVPlayerItem *, NSDictionary *))completion { PHVideoRequestOptions *option = [[PHVideoRequestOptions alloc] init]; option.networkAccessAllowed = YES; option.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ if (progressHandler) { progressHandler(progress, error, stop, info); } }); }; [[PHImageManager defaultManager] requestPlayerItemForVideo:asset options:option resultHandler:^(AVPlayerItem *playerItem, NSDictionary *info) { if (completion) completion(playerItem,info); }]; } #pragma mark - Export video /// Export Video / 导出视频 - (void)getVideoOutputPathWithAsset:(PHAsset *)asset success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { [self getVideoOutputPathWithAsset:asset presetName:AVAssetExportPresetMediumQuality success:success failure:failure]; } - (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { [self getVideoOutputPathWithAsset:asset presetName:presetName timeRange:kCMTimeRangeZero success:success failure:failure]; } - (void)startExportVideoWithVideoAsset:(AVURLAsset *)videoAsset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { [self startExportVideoWithVideoAsset:videoAsset timeRange:kCMTimeRangeZero presetName:presetName success:success failure:failure]; } - (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName timeRange:(CMTimeRange)timeRange success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { if (@available(iOS 14.0, *)) { [self requestVideoOutputPathWithAsset:asset presetName:presetName timeRange:timeRange success:success failure:failure]; return; } [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:[self getVideoRequestOptions] resultHandler:^(AVAsset* avasset, AVAudioMix* audioMix, NSDictionary* info){ // NSLog(@"Info:\n%@",info); AVURLAsset *videoAsset = (AVURLAsset*)avasset; // NSLog(@"AVAsset URL: %@",myAsset.URL); [self startExportVideoWithVideoAsset:videoAsset timeRange:timeRange presetName:presetName success:success failure:failure]; }]; } - (void)startExportVideoWithVideoAsset:(AVURLAsset *)videoAsset timeRange:(CMTimeRange)timeRange presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { if (!presetName) { presetName = AVAssetExportPresetMediumQuality; } // Find compatible presets by video asset. NSArray *presets = [AVAssetExportSession exportPresetsCompatibleWithAsset:videoAsset]; // Begin to compress video // Now we just compress to low resolution if it supports // If you need to upload to the server, but server does't support to upload by streaming, // You can compress the resolution to lower. Or you can support more higher resolution. if ([presets containsObject:presetName]) { AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:videoAsset presetName:presetName]; NSString *outputPath = [self getVideoOutputPath]; // Optimize for network use. session.shouldOptimizeForNetworkUse = true; if (!CMTimeRangeEqual(timeRange, kCMTimeRangeZero)) { session.timeRange = timeRange; } NSArray *supportedTypeArray = session.supportedFileTypes; if ([supportedTypeArray containsObject:AVFileTypeMPEG4]) { session.outputFileType = AVFileTypeMPEG4; } else if (supportedTypeArray.count == 0) { if (failure) { failure(@"该视频类型暂不支持导出", nil); } NSLog(@"No supported file types 视频类型暂不支持导出"); return; } else { session.outputFileType = [supportedTypeArray objectAtIndex:0]; if (videoAsset.URL && videoAsset.URL.lastPathComponent) { outputPath = [outputPath stringByReplacingOccurrencesOfString:@".mp4" withString:[NSString stringWithFormat:@"-%@", videoAsset.URL.lastPathComponent]]; } } // NSLog(@"video outputPath = %@",outputPath); session.outputURL = [NSURL fileURLWithPath:outputPath]; if (![[NSFileManager defaultManager] fileExistsAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"]]) { [[NSFileManager defaultManager] createDirectoryAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"] withIntermediateDirectories:YES attributes:nil error:nil]; } if ([TZImagePickerConfig sharedInstance].needFixComposition) { AVMutableVideoComposition *videoComposition = [self fixedCompositionWithAsset:videoAsset]; if (videoComposition.renderSize.width) { // 修正视频转向 session.videoComposition = videoComposition; } } // Begin to export video to the output path asynchronously. [session exportAsynchronouslyWithCompletionHandler:^(void) { [self handleVideoExportResult:session outputPath:outputPath success:success failure:failure]; }]; } else { if (failure) { NSString *errorMessage = [NSString stringWithFormat:@"当前设备不支持该预设:%@", presetName]; failure(errorMessage, nil); } } } - (void)requestVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { [self requestVideoOutputPathWithAsset:asset presetName:presetName timeRange:kCMTimeRangeZero success:success failure:failure]; } - (void)requestVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName timeRange:(CMTimeRange)timeRange success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { if (!presetName) { presetName = AVAssetExportPresetMediumQuality; } [[PHImageManager defaultManager] requestExportSessionForVideo:asset options:[self getVideoRequestOptions] exportPreset:presetName resultHandler:^(AVAssetExportSession *_Nullable exportSession, NSDictionary *_Nullable info) { NSString *outputPath = [self getVideoOutputPath]; exportSession.outputURL = [NSURL fileURLWithPath:outputPath]; exportSession.shouldOptimizeForNetworkUse = NO; exportSession.outputFileType = AVFileTypeMPEG4; if (!CMTimeRangeEqual(timeRange, kCMTimeRangeZero)) { exportSession.timeRange = timeRange; } [exportSession exportAsynchronouslyWithCompletionHandler:^{ [self handleVideoExportResult:exportSession outputPath:outputPath success:success failure:failure]; }]; }]; } - (void)requestVideoURLWithAsset:(PHAsset *)asset success:(void (^)(NSURL *videoURL))success failure:(void (^)(NSDictionary* info))failure { [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:[self getVideoRequestOptions] resultHandler:^(AVAsset* avasset, AVAudioMix* audioMix, NSDictionary* info){ // NSLog(@"AVAsset URL: %@",myAsset.URL); if ([avasset isKindOfClass:[AVURLAsset class]]) { NSURL *url = [(AVURLAsset *)avasset URL]; if (success) { success(url); } } else if (failure) { failure(info); } }]; } - (void)handleVideoExportResult:(AVAssetExportSession *)session outputPath:(NSString *)outputPath success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure { dispatch_async(dispatch_get_main_queue(), ^{ switch (session.status) { case AVAssetExportSessionStatusUnknown: { NSLog(@"AVAssetExportSessionStatusUnknown"); } break; case AVAssetExportSessionStatusWaiting: { NSLog(@"AVAssetExportSessionStatusWaiting"); } break; case AVAssetExportSessionStatusExporting: { NSLog(@"AVAssetExportSessionStatusExporting"); } break; case AVAssetExportSessionStatusCompleted: { NSLog(@"AVAssetExportSessionStatusCompleted"); if (success) { success(outputPath); } } break; case AVAssetExportSessionStatusFailed: { NSLog(@"AVAssetExportSessionStatusFailed"); if (failure) { failure(@"视频导出失败", session.error); } } break; case AVAssetExportSessionStatusCancelled: { NSLog(@"AVAssetExportSessionStatusCancelled"); if (failure) { failure(@"导出任务已被取消", nil); } } break; default: break; } }); } - (PHVideoRequestOptions *)getVideoRequestOptions { PHVideoRequestOptions* options = [[PHVideoRequestOptions alloc] init]; options.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic; options.networkAccessAllowed = YES; return options; } - (NSString *)getVideoOutputPath { NSDateFormatter *formater = [[NSDateFormatter alloc] init]; [formater setDateFormat:@"yyyy-MM-dd-HH-mm-ss-SSS"]; NSString *outputPath = [NSHomeDirectory() stringByAppendingFormat:@"/tmp/video-%@-%d.mp4", [formater stringFromDate:[NSDate date]], arc4random_uniform(10000000)]; return outputPath; } - (BOOL)isCameraRollAlbum:(PHAssetCollection *)metadata { NSString *versionStr = [[UIDevice currentDevice].systemVersion stringByReplacingOccurrencesOfString:@"." withString:@""]; if (versionStr.length <= 1) { versionStr = [versionStr stringByAppendingString:@"00"]; } else if (versionStr.length <= 2) { versionStr = [versionStr stringByAppendingString:@"0"]; } CGFloat version = versionStr.floatValue; // 目前已知8.0.0 ~ 8.0.2系统,拍照后的图片会保存在最近添加中 if (version >= 800 && version <= 802) { return ((PHAssetCollection *)metadata).assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumRecentlyAdded; } else { return ((PHAssetCollection *)metadata).assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary; } } /// 检查照片大小是否满足最小要求 - (BOOL)isPhotoSelectableWithAsset:(PHAsset *)asset { CGSize photoSize = CGSizeMake(asset.pixelWidth, asset.pixelHeight); if (self.minPhotoWidthSelectable > photoSize.width || self.minPhotoHeightSelectable > photoSize.height) { return NO; } return YES; } /// 检查照片能否被选中 - (BOOL)isAssetCannotBeSelected:(PHAsset *)asset { if ([self.pickerDelegate respondsToSelector:@selector(isAssetCanBeSelected:)]) { BOOL canSelectAsset = [self.pickerDelegate isAssetCanBeSelected:asset]; return !canSelectAsset; } return NO; } #pragma mark - Private Method - (TZAlbumModel *)modelWithResult:(PHFetchResult *)result collection:(PHAssetCollection *)collection isCameraRoll:(BOOL)isCameraRoll needFetchAssets:(BOOL)needFetchAssets options:(PHFetchOptions *)options { TZAlbumModel *model = [[TZAlbumModel alloc] init]; [model setResult:result needFetchAssets:needFetchAssets]; model.name = collection.localizedTitle; model.collection = collection; model.options = options; model.isCameraRoll = isCameraRoll; model.count = result.count; return model; } /// 缩放图片至新尺寸 - (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size { if (image.size.width > size.width && size.width > 0 && size.height > 0) { UIGraphicsBeginImageContext(size); [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return newImage; /* 好像不怎么管用:https://mp.weixin.qq.com/s/CiqMlEIp1Ir2EJSDGgMooQ CGFloat maxPixelSize = MAX(size.width, size.height); CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)UIImageJPEGRepresentation(image, 0.9), nil); NSDictionary *options = @{(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways:(__bridge id)kCFBooleanTrue, (__bridge id)kCGImageSourceThumbnailMaxPixelSize:[NSNumber numberWithFloat:maxPixelSize] }; CGImageRef imageRef = CGImageSourceCreateImageAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); UIImage *newImage = [UIImage imageWithCGImage:imageRef scale:2 orientation:image.imageOrientation]; CGImageRelease(imageRef); CFRelease(sourceRef); return newImage; */ } else { return image; } } /// 判断asset是否是视频 - (BOOL)isVideo:(PHAsset *)asset { return asset.mediaType == PHAssetMediaTypeVideo; } - (TZAssetModel *)createModelWithAsset:(PHAsset *)asset { TZAssetModelMediaType type = [[TZImageManager manager] getAssetType:asset]; NSString *timeLength = type == TZAssetModelMediaTypeVideo ? [NSString stringWithFormat:@"%0.0f",asset.duration] : @""; timeLength = [[TZImageManager manager] getNewTimeFromDurationSecond:timeLength.integerValue]; TZAssetModel *model = [TZAssetModel modelWithAsset:asset type:type timeLength:timeLength]; return model; } /// 获取优化后的视频转向信息 - (AVMutableVideoComposition *)fixedCompositionWithAsset:(AVAsset *)videoAsset { AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; // 视频转向 int degrees = [self degressFromVideoFileWithAsset:videoAsset]; if (degrees != 0) { CGAffineTransform translateToCenter; CGAffineTransform mixedTransform; videoComposition.frameDuration = CMTimeMake(1, 30); NSArray *tracks = [videoAsset tracksWithMediaType:AVMediaTypeVideo]; AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; AVMutableVideoCompositionInstruction *roateInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; roateInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [videoAsset duration]); AVMutableVideoCompositionLayerInstruction *roateLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack]; if (degrees == 90) { // 顺时针旋转90° translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.height, 0.0); mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2); videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width); [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero]; } else if(degrees == 180){ // 顺时针旋转180° translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.width, videoTrack.naturalSize.height); mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI); videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.width,videoTrack.naturalSize.height); [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero]; } else if(degrees == 270){ // 顺时针旋转270° translateToCenter = CGAffineTransformMakeTranslation(0.0, videoTrack.naturalSize.width); mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2*3.0); videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width); [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero]; } roateInstruction.layerInstructions = @[roateLayerInstruction]; // 加入视频方向信息 videoComposition.instructions = @[roateInstruction]; } return videoComposition; } /// 获取视频角度 - (int)degressFromVideoFileWithAsset:(AVAsset *)asset { int degress = 0; NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; if([tracks count] > 0) { AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; CGAffineTransform t = videoTrack.preferredTransform; if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0){ // Portrait degress = 90; } else if(t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0){ // PortraitUpsideDown degress = 270; } else if(t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0){ // LandscapeRight degress = 0; } else if(t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0){ // LandscapeLeft degress = 180; } } return degress; } /// 修正图片转向 - (UIImage *)fixOrientation:(UIImage *)aImage { if (!self.shouldFixOrientation) return aImage; // No-op if the orientation is already correct if (aImage.imageOrientation == UIImageOrientationUp) return aImage; // We need to calculate the proper transformation to make the image upright. // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. CGAffineTransform transform = CGAffineTransformIdentity; switch (aImage.imageOrientation) { case UIImageOrientationDown: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height); transform = CGAffineTransformRotate(transform, M_PI); break; case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: transform = CGAffineTransformTranslate(transform, aImage.size.width, 0); transform = CGAffineTransformRotate(transform, M_PI_2); break; case UIImageOrientationRight: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, 0, aImage.size.height); transform = CGAffineTransformRotate(transform, -M_PI_2); break; default: break; } switch (aImage.imageOrientation) { case UIImageOrientationUpMirrored: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, aImage.size.width, 0); transform = CGAffineTransformScale(transform, -1, 1); break; case UIImageOrientationLeftMirrored: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, aImage.size.height, 0); transform = CGAffineTransformScale(transform, -1, 1); break; default: break; } // Now we draw the underlying CGImage into a new context, applying the transform // calculated above. CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height, CGImageGetBitsPerComponent(aImage.CGImage), 0, CGImageGetColorSpace(aImage.CGImage), CGImageGetBitmapInfo(aImage.CGImage)); CGContextConcatCTM(ctx, transform); switch (aImage.imageOrientation) { case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: case UIImageOrientationRight: case UIImageOrientationRightMirrored: // Grr... CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.height,aImage.size.width), aImage.CGImage); break; default: CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.width,aImage.size.height), aImage.CGImage); break; } // And now we just create a new UIImage from the drawing context CGImageRef cgimg = CGBitmapContextCreateImage(ctx); UIImage *img = [UIImage imageWithCGImage:cgimg]; CGContextRelease(ctx); CGImageRelease(cgimg); return img; } #pragma clang diagnostic pop @end //@implementation TZSortDescriptor // //- (id)reversedSortDescriptor { // return [NSNumber numberWithBool:![TZImageManager manager].sortAscendingByModificationDate]; //} // //@end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImagePickerController.h ================================================ // // TZImagePickerController.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // version 3.8.12 - 2026.2.6 // 更多信息,请前往项目的github地址:https://github.com/banchichen/TZImagePickerController /* 经过测试,比起xib的方式,把TZAssetCell改用纯代码的方式来写,滑动帧数明显提高了(约提高10帧左右) 最初发现这个问题并修复的是@小鱼周凌宇同学,她的博客地址: http://zhoulingyu.com/ 表示感谢~ 原来xib确实会导致性能问题啊...大家也要注意了... */ #import #import "TZAssetModel.h" #import "NSBundle+TZImagePicker.h" #import "TZImageManager.h" #import "TZVideoPlayerController.h" #import "TZGifPhotoPreviewController.h" #import "TZPhotoPreviewController.h" #import "TZPhotoPreviewCell.h" #if __has_include("TZLocationManager.h") #define TZ_HAVE_LOCATION_CODE 1 #import "TZLocationManager.h" #else #undef TZ_HAVE_LOCATION_CODE #endif #define CURRENT_SYSTEM_VERSION [[UIDevice currentDevice] systemVersion] #define SYSTEM_VERSION_GREATER_THAN_15 ([CURRENT_SYSTEM_VERSION floatValue] >= 15.0) @class TZAlbumCell, TZAssetCell; @protocol TZImagePickerControllerDelegate; @interface TZImagePickerController : UINavigationController #pragma mark - /// Use this init method / 用这个初始化方法 - (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount delegate:(id)delegate; - (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount columnNumber:(NSInteger)columnNumber delegate:(id)delegate; - (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount columnNumber:(NSInteger)columnNumber delegate:(id)delegate pushPhotoPickerVc:(BOOL)pushPhotoPickerVc; /// This init method just for previewing photos / 用这个初始化方法以预览图片 - (instancetype)initWithSelectedAssets:(NSMutableArray *)selectedAssets selectedPhotos:(NSMutableArray *)selectedPhotos index:(NSInteger)index; /// This init method for crop photo / 用这个初始化方法以裁剪图片 - (instancetype)initCropTypeWithAsset:(PHAsset *)asset photo:(UIImage *)photo completion:(void (^)(UIImage *cropImage,PHAsset *asset))completion; #pragma mark - /// Default is 9 / 默认最大可选9张图片 @property (nonatomic, assign) NSInteger maxImagesCount; /// The minimum count photos user must pick, Default is 0 /// 最小照片必选张数,默认是0 @property (nonatomic, assign) NSInteger minImagesCount; /// If the user does not select any pictures, the current picture is automatically selected when the Finish button is clicked, Default is YES /// 如果用户未选择任何图片,在点击完成按钮时自动选中当前图片,默认YES @property (nonatomic, assign) BOOL autoSelectCurrentWhenDone; /// Always enale the done button, not require minimum 1 photo be picked /// 让完成按钮一直可以点击,无须最少选择一张图片 @property (nonatomic, assign) BOOL alwaysEnableDoneBtn; /// Sort photos ascending by modificationDate,Default is YES /// 对照片排序,按修改时间升序,默认是YES。如果设置为NO,最新的照片会显示在最前面,内部的拍照按钮会排在第一个 @property (nonatomic, assign) BOOL sortAscendingByModificationDate; /// The pixel width of output image, Default is 828px,you need to set photoPreviewMaxWidth at the same time /// 导出图片的宽度,默认828像素宽,你需要同时设置photoPreviewMaxWidth的值 @property (nonatomic, assign) CGFloat photoWidth; /// Default is 600px / 默认600像素宽 @property (nonatomic, assign) CGFloat photoPreviewMaxWidth; /// Default is 30, While fetching photo, HUD will dismiss automatic if timeout; /// 超时时间,默认为30秒,当取图片时间超过30秒还没有取成功时,会自动dismiss HUD; @property (nonatomic, assign) NSInteger timeout; /// Default is YES, if set NO, the original photo button will hide. user can't picking original photo. /// 默认为YES,如果设置为NO,原图按钮将隐藏,用户不能选择发送原图 @property (nonatomic, assign) BOOL allowPickingOriginalPhoto; /// Default is YES, if set NO, user can't picking video. /// 默认为YES,如果设置为NO,用户将不能选择视频 @property (nonatomic, assign) BOOL allowPickingVideo; /// Default is NO, if set YES, user can edit video. /// 默认为NO,如果设置为YES, 用户能编辑视频 @property (nonatomic, assign) BOOL allowEditVideo; /// Export quality of cropped video, Default is AVAssetExportPresetMediumQuality /// 裁剪视频的导出质量,默认是 AVAssetExportPresetMediumQuality @property (nonatomic, copy) NSString *presetName; /// Default is 30s. If it exceeds the video duration, it is the video duration.The minimum duration of video crop is 1s. /// 默认是30s,如果超过视频时长,则为视频时长,小于1s不裁剪 @property (nonatomic, assign) NSInteger maxCropVideoDuration; /// Default is NO, if set YES, The edited video will be automatically saved to the album. /// 默认为NO,如果设置为YES,编辑后的视频会自动保存到相册 @property (nonatomic, assign) BOOL saveEditedVideoToAlbum; /// Default is NO / 默认为NO,为YES时可以多选视频/gif/图片,和照片共享最大可选张数maxImagesCount的限制 @property (nonatomic, assign) BOOL allowPickingMultipleVideo; /// Default is NO, if set YES, user can picking gif image. When NO, gif will be treated as a regular image. If want not displayed, please refer to isAssetCanBeDisplayed /// 默认为NO,如果设置为YES,用户可以选择gif图片。为NO时gif会被当成普通图片,若要不显示,请参考isAssetCanBeDisplayed @property (nonatomic, assign) BOOL allowPickingGif; /// Default is YES, if set NO, user can't picking image. /// 默认为YES,如果设置为NO,用户将不能选择发送图片 @property (nonatomic, assign) BOOL allowPickingImage; /// Default is YES, if set NO, user can't take picture. /// 默认为YES,如果设置为NO, 用户将不能拍摄照片 @property (nonatomic, assign) BOOL allowTakePicture; #ifdef TZ_HAVE_LOCATION_CODE @property (nonatomic, assign) BOOL allowCameraLocation; #endif /// Default is YES, if set NO, user can't take video. /// 默认为YES,如果设置为NO, 用户将不能拍摄视频 @property(nonatomic, assign) BOOL allowTakeVideo; /// Default value is 10 minutes / 视频最大拍摄时间,默认是10分钟,单位是秒 @property (assign, nonatomic) NSTimeInterval videoMaximumDuration; /// Customizing UIImagePickerController's other properties, such as videoQuality / 定制UIImagePickerController的其它属性,比如视频拍摄质量videoQuality @property (nonatomic, copy) void(^uiImagePickerControllerSettingBlock)(UIImagePickerController *imagePickerController); /// 首选语言,如果设置了就用该语言,不设则取当前系统语言。 /// 支持zh-Hans、zh-Hant、en、vi等值,详见TZImagePickerController.bundle内的语言资源 @property (copy, nonatomic) NSString *preferredLanguage; /// 语言bundle,preferredLanguage变化时languageBundle会变化 /// 可通过手动设置bundle,让选择器支持新的的语言(需要在设置preferredLanguage后设置languageBundle)。欢迎提交PR把语言文件提交上来~ @property (strong, nonatomic) NSBundle *languageBundle; /// Default is YES, if set NO, user can't preview photo. /// 默认为YES,如果设置为NO,预览按钮将隐藏,用户将不能去预览照片 @property (nonatomic, assign) BOOL allowPreview; /// Default is YES, if set NO, the picker don't dismiss itself. /// 默认为YES,如果设置为NO, 选择器将不会自己dismiss @property(nonatomic, assign) BOOL autoDismiss; /// Default is NO, if set YES, in the delegate method the photos and infos will be nil, only assets hava value. /// 默认为NO,如果设置为YES,代理方法里photos和infos会是nil,只返回assets @property (assign, nonatomic) BOOL onlyReturnAsset; /// Default is NO, if set YES, will show the image's selected index. /// 默认为NO,如果设置为YES,会显示照片的选中序号 @property (assign, nonatomic) BOOL showSelectedIndex; /// Default is NO, if set YES, when selected photos's count up to maxImagesCount, other photo will show float layer what's color is cannotSelectLayerColor. /// 默认是NO,如果设置为YES,当照片选择张数达到maxImagesCount时,其它照片会显示颜色为cannotSelectLayerColor的浮层 @property (assign, nonatomic) BOOL showPhotoCannotSelectLayer; /// Default is white color with 0.8 alpha; @property (strong, nonatomic) UIColor *cannotSelectLayerColor; /// Default is YES, if set NO, the result photo will be scaled to photoWidth pixel width. The photoWidth default is 828px /// 默认是YES,如果设置为NO,内部会缩放图片到photoWidth像素宽 @property (assign, nonatomic) BOOL notScaleImage; /// 默认是NO,如果设置为YES,导出视频时会修正转向(慎重设为YES,可能导致部分安卓下拍的视频导出失败) @property (assign, nonatomic) BOOL needFixComposition; /// The photos user have selected /// 用户选中过的图片数组 @property (nonatomic, strong) NSMutableArray *selectedAssets; @property (nonatomic, strong) NSMutableArray *selectedModels; @property (nonatomic, strong) NSMutableArray *selectedAssetIds; - (void)addSelectedModel:(TZAssetModel *)model; - (void)removeSelectedModel:(TZAssetModel *)model; /// Minimum selectable photo width, Default is 0 /// 最小可选中的图片宽度,默认是0,小于这个宽度的图片不可选中 @property (nonatomic, assign) NSInteger minPhotoWidthSelectable; @property (nonatomic, assign) NSInteger minPhotoHeightSelectable; /// Hide the photo what can not be selected, Default is NO /// 隐藏不可以选中的图片,默认是NO,不推荐将其设置为YES @property (nonatomic, assign) BOOL hideWhenCanNotSelect; /// Deprecated, Use statusBarStyle (顶部statusBar 是否为系统默认的黑色,默认为NO) @property (nonatomic, assign) BOOL isStatusBarDefault __attribute__((deprecated("Use -statusBarStyle."))); /// statusBar的样式,默认为UIStatusBarStyleLightContent @property (assign, nonatomic) UIStatusBarStyle statusBarStyle; #pragma mark - /// Single selection mode, valid when maxImagesCount = 1 /// 单选模式,maxImagesCount为1时才生效 @property (nonatomic, assign) BOOL showSelectBtn; ///< 在单选模式下,照片列表页中,显示选择按钮,默认为NO @property (nonatomic, assign) BOOL allowCrop; ///< 允许裁剪,默认为YES,showSelectBtn为NO才生效 @property (nonatomic, assign) BOOL scaleAspectFillCrop; ///< 是否图片等比缩放填充cropRect区域,开启后预览页面无法左右滑动切换图片 @property (nonatomic, assign) CGRect cropRect; ///< 裁剪框的尺寸 @property (nonatomic, assign) CGRect cropRectPortrait; ///< 裁剪框的尺寸(竖屏) @property (nonatomic, assign) CGRect cropRectLandscape; ///< 裁剪框的尺寸(横屏) @property (nonatomic, assign) BOOL needCircleCrop; ///< 需要圆形裁剪框 @property (nonatomic, assign) NSInteger circleCropRadius; ///< 圆形裁剪框半径大小 @property (nonatomic, copy) void (^cropViewSettingBlock)(UIView *cropView); ///< 自定义裁剪框的其他属性 @property (nonatomic, copy) void (^navLeftBarButtonSettingBlock)(UIButton *leftButton); ///< 自定义返回按钮样式及其属性 /// 【自定义各页面/组件的样式】在界面初始化/组件setModel完成后调用,允许外界修改样式等 @property (nonatomic, copy) void (^photoPickerPageUIConfigBlock)(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine); @property (nonatomic, copy) void (^photoPreviewPageUIConfigBlock)(UICollectionView *collectionView, UIView *naviBar, UIButton *backButton, UIButton *selectButton, UILabel *indexLabel, UIView *toolBar, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel); @property (nonatomic, copy) void (^videoPreviewPageUIConfigBlock)(UIButton *playButton, UIView *toolBar, UIButton *editBtn, UIButton *doneButton); @property (nonatomic, copy) void (^videoEditViewPageUIConfigBlock)(UIButton *playButton,UILabel *cropVideoDurationLabel, UIButton *editButton, UIButton *doneButton); @property (nonatomic, copy) void (^gifPreviewPageUIConfigBlock)(UIView *toolBar, UIButton *doneButton); @property (nonatomic, copy) void (^albumPickerPageUIConfigBlock)(UITableView *tableView); @property (nonatomic, copy) void (^assetCellDidSetModelBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView); @property (nonatomic, copy) void (^albumCellDidSetModelBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel); /// 【自定义各页面/组件的frame】在界面viewDidLayoutSubviews/组件layoutSubviews后调用,允许外界修改frame等 @property (nonatomic, copy) void (^photoPickerPageDidLayoutSubviewsBlock)(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine); @property (nonatomic, copy) void (^photoPreviewPageDidLayoutSubviewsBlock)(UICollectionView *collectionView, UIView *naviBar, UIButton *backButton, UIButton *selectButton, UILabel *indexLabel, UIView *toolBar, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel); @property (nonatomic, copy) void (^videoPreviewPageDidLayoutSubviewsBlock)(UIButton *playButton, UIView *toolBar, UIButton *editButton, UIButton *doneButton); @property (nonatomic, copy) void (^videoEditViewPageDidLayoutSubviewsBlock)(UIButton *playButton, UILabel *cropVideoDurationLabel, UIButton *cancelButton, UIButton *doneButton); @property (nonatomic, copy) void (^gifPreviewPageDidLayoutSubviewsBlock)(UIView *toolBar, UIButton *doneButton); @property (nonatomic, copy) void (^albumPickerPageDidLayoutSubviewsBlock)(UITableView *tableView); @property (nonatomic, copy) void (^assetCellDidLayoutSubviewsBlock)(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView); @property (nonatomic, copy) void (^albumCellDidLayoutSubviewsBlock)(TZAlbumCell *cell, UIImageView *posterImageView, UILabel *titleLabel); /// 自定义各页面/组件的frame】刷新底部状态(refreshNaviBarAndBottomBarState)使用的 @property (nonatomic, copy) void (^photoPickerPageDidRefreshStateBlock)(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine); @property (nonatomic, copy) void (^photoPreviewPageDidRefreshStateBlock)(UICollectionView *collectionView, UIView *naviBar, UIButton *backButton, UIButton *selectButton, UILabel *indexLabel, UIView *toolBar, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel); #pragma mark - - (UIAlertController *)showAlertWithTitle:(NSString *)title; - (void)showProgressHUD; - (void)hideProgressHUD; @property (nonatomic, assign) BOOL isSelectOriginalPhoto; @property (assign, nonatomic) BOOL needShowStatusBar; #pragma mark - @property (nonatomic, copy) NSString *takePictureImageName __attribute__((deprecated("Use -takePictureImage."))); @property (nonatomic, copy) NSString *photoSelImageName __attribute__((deprecated("Use -photoSelImage."))); @property (nonatomic, copy) NSString *photoDefImageName __attribute__((deprecated("Use -photoDefImage."))); @property (nonatomic, copy) NSString *photoOriginSelImageName __attribute__((deprecated("Use -photoOriginSelImage."))); @property (nonatomic, copy) NSString *photoOriginDefImageName __attribute__((deprecated("Use -photoOriginDefImage."))); @property (nonatomic, copy) NSString *photoPreviewOriginDefImageName __attribute__((deprecated("Use -photoPreviewOriginDefImage."))); @property (nonatomic, copy) NSString *photoNumberIconImageName __attribute__((deprecated("Use -photoNumberIconImage."))); @property (nonatomic, strong) UIImage *takePictureImage; @property (nonatomic, strong) UIImage *addMorePhotoImage; @property (nonatomic, strong) UIImage *photoSelImage; @property (nonatomic, strong) UIImage *photoDefImage; @property (nonatomic, strong) UIImage *photoOriginSelImage; @property (nonatomic, strong) UIImage *photoOriginDefImage; @property (nonatomic, strong) UIImage *photoPreviewOriginDefImage; @property (nonatomic, strong) UIImage *photoNumberIconImage; #pragma mark - /// Appearance / 外观颜色 + 按钮文字 @property (nonatomic, strong) UIColor *oKButtonTitleColorNormal; @property (nonatomic, strong) UIColor *oKButtonTitleColorDisabled; @property (nonatomic, strong) UIColor *naviBgColor; @property (nonatomic, strong) UIColor *naviTitleColor; @property (nonatomic, strong) UIFont *naviTitleFont; @property (nonatomic, strong) UIColor *barItemTextColor; @property (nonatomic, strong) UIFont *barItemTextFont; @property (nonatomic, copy) NSString *doneBtnTitleStr; @property (nonatomic, copy) NSString *cancelBtnTitleStr; @property (nonatomic, copy) NSString *previewBtnTitleStr; @property (nonatomic, copy) NSString *fullImageBtnTitleStr; @property (nonatomic, copy) NSString *settingBtnTitleStr; @property (nonatomic, copy) NSString *processHintStr; @property (nonatomic, copy) NSString *editBtnTitleStr; @property (nonatomic, copy) NSString *editViewCancelBtnTitleStr; /// Icon theme color, default is green color like wechat, the value is r:31 g:185 b:34. Currently only support image selection icon when showSelectedIndex is YES. If you need it, please set it as soon as possible /// icon主题色,默认是微信的绿色,值是r:31 g:185 b:34。目前仅支持showSelectedIndex为YES时的图片选中icon。如需要,请尽早设置它。 @property (strong, nonatomic) UIColor *iconThemeColor; #pragma mark - - (void)cancelButtonClick; // For method annotations, see the corresponding method in TZImagePickerControllerDelegate / 方法注释见TZImagePickerControllerDelegate中对应方法 @property (nonatomic, copy) void (^didFinishPickingPhotosHandle)(NSArray *photos,NSArray *assets,BOOL isSelectOriginalPhoto); @property (nonatomic, copy) void (^didFinishPickingPhotosWithInfosHandle)(NSArray *photos,NSArray *assets,BOOL isSelectOriginalPhoto,NSArray *infos); @property (nonatomic, copy) void (^imagePickerControllerDidCancelHandle)(void); @property (nonatomic, copy) void (^didFinishPickingVideoHandle)(UIImage *coverImage,PHAsset *asset); @property (nonatomic, copy) void (^didFinishPickingAndEditingVideoHandle)(UIImage *coverImage,NSString *outputPath,NSString *errorMsg); @property (nonatomic, copy) void (^didFinishPickingGifImageHandle)(UIImage *animatedImage,id sourceAssets); @property (nonatomic, weak) id pickerDelegate; @end @protocol TZImagePickerControllerDelegate @optional // The picker should dismiss itself; when it dismissed these callback will be called. // You can also set autoDismiss to NO, then the picker don't dismiss itself. // If isOriginalPhoto is YES, user picked the original photo. // You can get original photo with asset, by the method [[TZImageManager manager] getOriginalPhotoWithAsset:completion:]. // The UIImage Object in photos default width is 828px, you can set it by photoWidth property. // 这个照片选择器会自己dismiss,当选择器dismiss的时候,会执行下面的代理方法 // 你也可以设置autoDismiss属性为NO,选择器就不会自己dismis了 // 如果isSelectOriginalPhoto为YES,表明用户选择了原图 // 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:] // photos数组里的UIImage对象,默认是828像素宽,你可以通过设置photoWidth属性的值来改变它 - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto; - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray *)infos; - (void)tz_imagePickerControllerDidCancel:(TZImagePickerController *)picker; /// 如果用户选择了某张照片下面的代理方法会被执行 /// 如果isSelectOriginalPhoto为YES,表明用户选择了原图 /// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:] - (void)imagePickerController:(TZImagePickerController *)picker didSelectAsset:(PHAsset *)asset photo:(UIImage *)photo isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto; /// 如果用户取消选择了某张照片下面的代理方法会被执行 /// 如果isSelectOriginalPhoto为YES,表明用户选择了原图 /// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:] - (void)imagePickerController:(TZImagePickerController *)picker didDeselectAsset:(PHAsset *)asset photo:(UIImage *)photo isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto; // If user picking a video and allowPickingMultipleVideo is NO, this callback will be called. // If allowPickingMultipleVideo is YES, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: // 如果用户选择了一个视频且allowPickingMultipleVideo是NO,下面的代理方法会被执行 // 如果allowPickingMultipleVideo是YES,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingVideo:(UIImage *)coverImage sourceAssets:(PHAsset *)asset; // If allowEditVideo is YES and allowPickingMultipleVideo is NO, When user picking a video, this callback will be called. // If allowPickingMultipleVideo is YES, video editing is not supported, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: // 当allowEditVideo是YES且allowPickingMultipleVideo是NO是,如果用户选择了一个视频,下面的代理方法会被执行 // 如果allowPickingMultipleVideo是YES,则不支持编辑视频,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingAndEditingVideo:(UIImage *)coverImage outputPath:(NSString *)outputPath error:(NSString *)errorMsg; // When saving the edited video to the album fails, this callback will be called. // 编辑后的视频自动保存到相册失败时,下面的代理方法会被执行 - (void)imagePickerController:(TZImagePickerController *)picker didFailToSaveEditedVideoWithError:(NSError *)error; // If user picking a gif image and allowPickingMultipleVideo is NO, this callback will be called. // If allowPickingMultipleVideo is YES, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: // 如果用户选择了一个gif图片且allowPickingMultipleVideo是NO,下面的代理方法会被执行 // 如果allowPickingMultipleVideo是YES,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingGifImage:(UIImage *)animatedImage sourceAssets:(PHAsset *)asset; // Decide album show or not't // 决定相册显示与否 albumName:相册名字 result:相册原始数据 - (BOOL)isAlbumCanSelect:(NSString *)albumName result:(PHFetchResult *)result; // Decide asset show or not't // 决定照片显示与否 - (BOOL)isAssetCanSelect:(PHAsset *)asset __attribute__((deprecated("Use -isAssetCanBeDisplayed:."))); - (BOOL)isAssetCanBeDisplayed:(PHAsset *)asset; // Decide asset can be selected // 决定照片能否被选中 - (BOOL)isAssetCanBeSelected:(PHAsset *)asset; @end @interface TZAlbumPickerController : UIViewController @property (nonatomic, assign) NSInteger columnNumber; @property (assign, nonatomic) BOOL isFirstAppear; - (void)configTableView; @end @interface UIImage (MyBundle) + (UIImage *)tz_imageNamedFromMyBundle:(NSString *)name; @end @interface TZCommonTools : NSObject + (UIEdgeInsets)tz_safeAreaInsets; + (BOOL)tz_isIPhoneX; + (BOOL)tz_isLandscape; + (CGFloat)tz_statusBarHeight; // 获得Info.plist数据字典 + (NSDictionary *)tz_getInfoDictionary; + (NSString *)tz_getAppName; + (BOOL)tz_isRightToLeftLayout; + (void)configBarButtonItem:(UIBarButtonItem *)item tzImagePickerVc:(TZImagePickerController *)tzImagePickerVc; + (BOOL)isICloudSyncError:(NSError *)error; + (BOOL)isAssetNotSelectable:(TZAssetModel *)model tzImagePickerVc:(TZImagePickerController *)tzImagePickerVc; + (UICollectionViewFlowLayout *)tz_rtlFlowLayout; @end @interface TZImagePickerConfig : NSObject + (instancetype)sharedInstance; @property (copy, nonatomic) NSString *preferredLanguage; @property(nonatomic, assign) BOOL allowPickingImage; @property (nonatomic, assign) BOOL allowPickingVideo; @property (strong, nonatomic) NSBundle *languageBundle; @property (assign, nonatomic) BOOL showSelectedIndex; @property (assign, nonatomic) BOOL showPhotoCannotSelectLayer; @property (assign, nonatomic) BOOL notScaleImage; @property (assign, nonatomic) BOOL needFixComposition; /// 默认是50,如果一个GIF过大,里面图片个数可能超过1000,会导致内存飙升而崩溃 @property (assign, nonatomic) NSInteger gifPreviewMaxImagesCount; /// 【自定义GIF播放方案】为了避免内存过大,内部默认限制只播放50帧(平均取),可通过gifPreviewMaxImagesCount属性调整,若对GIF预览有更好的效果要求,可实现这个block采用FLAnimatedImage等三方库来播放,但注意FLAnimatedImage有播放速度较慢问题,自行取舍下。 @property (nonatomic, copy) void (^gifImagePlayBlock)(TZPhotoPreviewView *view, UIImageView *imageView, NSData *gifData, NSDictionary *info); @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImagePickerController.m ================================================ // // TZImagePickerController.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // version 3.8.12 - 2026.2.6 // 更多信息,请前往项目的github地址:https://github.com/banchichen/TZImagePickerController #import "TZImagePickerController.h" #import "TZPhotoPickerController.h" #import "TZPhotoPreviewController.h" #import "TZAssetModel.h" #import "TZAssetCell.h" #import "UIView+TZLayout.h" #import "TZImageManager.h" #import "TZVideoCropController.h" @interface TZImagePickerController () { NSTimer *_timer; UILabel *_tipLabel; UIButton *_settingBtn; BOOL _pushPhotoPickerVc; BOOL _didPushPhotoPickerVc; CGRect _cropRect; UIButton *_progressHUD; UIView *_HUDContainer; UIActivityIndicatorView *_HUDIndicatorView; UILabel *_HUDLabel; UIStatusBarStyle _originStatusBarStyle; } /// Default is 4, Use in photos collectionView in TZPhotoPickerController /// 默认4列, TZPhotoPickerController中的照片collectionView @property (nonatomic, assign) NSInteger columnNumber; @property (nonatomic, assign) NSInteger HUDTimeoutCount; ///< 超时隐藏HUD计数 @end @implementation TZImagePickerController - (instancetype)init { self = [super init]; if (self) { self = [self initWithMaxImagesCount:9 delegate:nil]; } return self; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)viewDidLoad { [super viewDidLoad]; self.needShowStatusBar = ![UIApplication sharedApplication].statusBarHidden; if (@available(iOS 13.0, *)) { self.view.backgroundColor = UIColor.tertiarySystemBackgroundColor; } else { self.view.backgroundColor = [UIColor whiteColor]; } self.navigationBar.barStyle = UIBarStyleBlack; self.navigationBar.translucent = YES; [TZImageManager manager].shouldFixOrientation = NO; // Default appearance, you can reset these after this method // 默认的外观,你可以在这个方法后重置 self.oKButtonTitleColorNormal = [UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:1.0]; self.oKButtonTitleColorDisabled = [UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:0.5]; self.navigationBar.barTintColor = [UIColor colorWithRed:(34/255.0) green:(34/255.0) blue:(34/255.0) alpha:1.0]; self.navigationBar.tintColor = [UIColor whiteColor]; self.automaticallyAdjustsScrollViewInsets = NO; if (self.needShowStatusBar) [UIApplication sharedApplication].statusBarHidden = NO; } - (void)setNaviBgColor:(UIColor *)naviBgColor { _naviBgColor = naviBgColor; self.navigationBar.barTintColor = naviBgColor; [self configNavigationBarAppearance]; } - (void)setNaviTitleColor:(UIColor *)naviTitleColor { _naviTitleColor = naviTitleColor; [self configNaviTitleAppearance]; } - (void)setNaviTitleFont:(UIFont *)naviTitleFont { _naviTitleFont = naviTitleFont; [self configNaviTitleAppearance]; } - (void)configNaviTitleAppearance { NSMutableDictionary *textAttrs = [NSMutableDictionary dictionary]; if (self.naviTitleColor) { textAttrs[NSForegroundColorAttributeName] = self.naviTitleColor; } if (self.naviTitleFont) { textAttrs[NSFontAttributeName] = self.naviTitleFont; } self.navigationBar.titleTextAttributes = textAttrs; [self configNavigationBarAppearance]; } - (void)configNavigationBarAppearance { if (@available(iOS 13.0, *)) { UINavigationBarAppearance *barAppearance = [[UINavigationBarAppearance alloc] init]; if (self.navigationBar.isTranslucent) { UIColor *barTintColor = self.navigationBar.barTintColor; barAppearance.backgroundColor = [barTintColor colorWithAlphaComponent:0.85]; } else { barAppearance.backgroundColor = self.navigationBar.barTintColor; } barAppearance.titleTextAttributes = self.navigationBar.titleTextAttributes; self.navigationBar.standardAppearance = barAppearance; self.navigationBar.scrollEdgeAppearance = barAppearance; } } - (void)setBarItemTextFont:(UIFont *)barItemTextFont { _barItemTextFont = barItemTextFont; [self configBarButtonItemAppearance]; } - (void)setBarItemTextColor:(UIColor *)barItemTextColor { _barItemTextColor = barItemTextColor; [self configBarButtonItemAppearance]; } - (void)setIsStatusBarDefault:(BOOL)isStatusBarDefault { _isStatusBarDefault = isStatusBarDefault; if (isStatusBarDefault) { self.statusBarStyle = UIStatusBarStyleDefault; } else { self.statusBarStyle = UIStatusBarStyleLightContent; } } - (void)configBarButtonItemAppearance { UIBarButtonItem *barItem; if (@available(iOS 9, *)) { barItem = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[TZImagePickerController class]]]; } else { barItem = [UIBarButtonItem appearanceWhenContainedIn:[TZImagePickerController class], nil]; } NSMutableDictionary *textAttrs = [NSMutableDictionary dictionary]; textAttrs[NSForegroundColorAttributeName] = self.barItemTextColor; textAttrs[NSFontAttributeName] = self.barItemTextFont; [barItem setTitleTextAttributes:textAttrs forState:UIControlStateNormal]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; _originStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; [UIApplication sharedApplication].statusBarStyle = self.statusBarStyle; [self configNavigationBarAppearance]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [UIApplication sharedApplication].statusBarStyle = _originStatusBarStyle; [self hideProgressHUD]; } - (UIStatusBarStyle)preferredStatusBarStyle { return self.statusBarStyle; } - (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount delegate:(id)delegate { return [self initWithMaxImagesCount:maxImagesCount columnNumber:4 delegate:delegate pushPhotoPickerVc:YES]; } - (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount columnNumber:(NSInteger)columnNumber delegate:(id)delegate { return [self initWithMaxImagesCount:maxImagesCount columnNumber:columnNumber delegate:delegate pushPhotoPickerVc:YES]; } - (instancetype)initWithMaxImagesCount:(NSInteger)maxImagesCount columnNumber:(NSInteger)columnNumber delegate:(id)delegate pushPhotoPickerVc:(BOOL)pushPhotoPickerVc { _pushPhotoPickerVc = pushPhotoPickerVc; TZAlbumPickerController *albumPickerVc = [[TZAlbumPickerController alloc] init]; albumPickerVc.isFirstAppear = YES; albumPickerVc.columnNumber = columnNumber; self = [super initWithRootViewController:albumPickerVc]; if (self) { self.maxImagesCount = maxImagesCount > 0 ? maxImagesCount : 9; // Default is 9 / 默认最大可选9张图片 self.pickerDelegate = delegate; self.selectedAssets = [NSMutableArray array]; // Allow user picking original photo and video, you also can set No after this method // 默认准许用户选择原图和视频, 你也可以在这个方法后置为NO self.allowPickingOriginalPhoto = YES; self.allowPickingVideo = YES; self.allowPickingImage = YES; self.allowTakePicture = YES; self.allowTakeVideo = YES; self.videoMaximumDuration = 10 * 60; self.sortAscendingByModificationDate = YES; self.columnNumber = columnNumber; [self configDefaultSetting]; if (![[TZImageManager manager] authorizationStatusAuthorized]) { _tipLabel = [[UILabel alloc] init]; _tipLabel.frame = CGRectMake(8, 120, self.view.tz_width - 16, 60); _tipLabel.textAlignment = NSTextAlignmentCenter; _tipLabel.numberOfLines = 0; _tipLabel.font = [UIFont systemFontOfSize:16]; _tipLabel.textColor = [UIColor blackColor]; _tipLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; NSString *appName = [TZCommonTools tz_getAppName]; NSString *tipText = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Allow %@ to access your album in \"Settings -> Privacy -> Photos\""],appName]; _tipLabel.text = tipText; [self.view addSubview:_tipLabel]; _settingBtn = [UIButton buttonWithType:UIButtonTypeSystem]; [_settingBtn setTitle:self.settingBtnTitleStr forState:UIControlStateNormal]; _settingBtn.frame = CGRectMake(0, 180, self.view.tz_width, 44); _settingBtn.titleLabel.font = [UIFont systemFontOfSize:18]; [_settingBtn addTarget:self action:@selector(settingBtnClick) forControlEvents:UIControlEventTouchUpInside]; _settingBtn.autoresizingMask = UIViewAutoresizingFlexibleWidth; [self.view addSubview:_settingBtn]; if ([PHPhotoLibrary authorizationStatus] == 0) { _timer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(observeAuthrizationStatusChange) userInfo:nil repeats:NO]; } } else { [self pushPhotoPickerVc]; } } return self; } /// This init method just for previewing photos / 用这个初始化方法以预览图片 - (instancetype)initWithSelectedAssets:(NSMutableArray *)selectedAssets selectedPhotos:(NSMutableArray *)selectedPhotos index:(NSInteger)index{ TZPhotoPreviewController *previewVc = [[TZPhotoPreviewController alloc] init]; self = [super initWithRootViewController:previewVc]; if (self) { self.selectedAssets = [NSMutableArray arrayWithArray:selectedAssets]; self.allowPickingOriginalPhoto = self.allowPickingOriginalPhoto; [self configDefaultSetting]; previewVc.photos = [NSMutableArray arrayWithArray:selectedPhotos]; previewVc.currentIndex = index; __weak typeof(self) weakSelf = self; [previewVc setDoneButtonClickBlockWithPreviewType:^(NSArray *photos, NSArray *assets, BOOL isSelectOriginalPhoto) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf.autoDismiss) { if (strongSelf.didFinishPickingPhotosHandle) { strongSelf.didFinishPickingPhotosHandle(photos,assets,isSelectOriginalPhoto); } return; } [strongSelf dismissViewControllerAnimated:YES completion:^{ if (!strongSelf) return; if (strongSelf.didFinishPickingPhotosHandle) { strongSelf.didFinishPickingPhotosHandle(photos,assets,isSelectOriginalPhoto); } }]; }]; } return self; } /// This init method for crop photo / 用这个初始化方法以裁剪图片 - (instancetype)initCropTypeWithAsset:(PHAsset *)asset photo:(UIImage *)photo completion:(void (^)(UIImage *cropImage,PHAsset *asset))completion { TZPhotoPreviewController *previewVc = [[TZPhotoPreviewController alloc] init]; self = [super initWithRootViewController:previewVc]; if (self) { self.maxImagesCount = 1; self.allowPickingImage = YES; self.allowCrop = YES; self.selectedAssets = [NSMutableArray arrayWithArray:@[asset]]; [self configDefaultSetting]; previewVc.photos = [NSMutableArray arrayWithArray:@[photo]]; previewVc.isCropImage = YES; previewVc.currentIndex = 0; __weak typeof(self) weakSelf = self; [previewVc setDoneButtonClickBlockCropMode:^(UIImage *cropImage, id asset) { __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf dismissViewControllerAnimated:YES completion:^{ if (completion) { completion(cropImage,asset); } }]; }]; } return self; } - (void)configDefaultSetting { self.autoDismiss = YES; self.autoSelectCurrentWhenDone = YES; self.timeout = 30; self.photoWidth = 828.0; self.photoPreviewMaxWidth = 600; self.naviTitleColor = [UIColor whiteColor]; self.naviTitleFont = [UIFont systemFontOfSize:17]; self.barItemTextFont = [UIFont systemFontOfSize:15]; self.barItemTextColor = [UIColor whiteColor]; self.allowPreview = YES; // 2.2.26版本,不主动缩放图片,降低内存占用 self.notScaleImage = YES; self.needFixComposition = NO; self.statusBarStyle = UIStatusBarStyleLightContent; self.cannotSelectLayerColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8]; #ifdef TZ_HAVE_LOCATION_CODE self.allowCameraLocation = YES; #endif self.presetName = AVAssetExportPresetMediumQuality; self.maxCropVideoDuration = 30; self.iconThemeColor = [UIColor colorWithRed:31 / 255.0 green:185 / 255.0 blue:34 / 255.0 alpha:1.0]; [self configDefaultBtnTitle]; CGFloat cropViewWH = MIN(self.view.tz_width, self.view.tz_height) / 3 * 2; self.cropRect = CGRectMake((self.view.tz_width - cropViewWH) / 2, (self.view.tz_height - cropViewWH) / 2, cropViewWH, cropViewWH); } - (void)configDefaultImageName { self.takePictureImageName = @"takePicture80"; self.photoSelImageName = @"photo_sel_photoPickerVc"; self.photoDefImageName = @"photo_def_photoPickerVc"; self.photoNumberIconImage = [self createImageWithColor:nil size:CGSizeMake(24, 24) radius:12]; // @"photo_number_icon"; self.photoPreviewOriginDefImageName = @"preview_original_def"; self.photoOriginDefImageName = @"photo_original_def"; self.photoOriginSelImageName = @"photo_original_sel"; self.addMorePhotoImage = [UIImage tz_imageNamedFromMyBundle:@"addMore"]; } - (void)setTakePictureImageName:(NSString *)takePictureImageName { _takePictureImageName = takePictureImageName; _takePictureImage = [UIImage tz_imageNamedFromMyBundle:takePictureImageName]; } - (void)setPhotoSelImageName:(NSString *)photoSelImageName { _photoSelImageName = photoSelImageName; _photoSelImage = [UIImage tz_imageNamedFromMyBundle:photoSelImageName]; } - (void)setPhotoDefImageName:(NSString *)photoDefImageName { _photoDefImageName = photoDefImageName; _photoDefImage = [UIImage tz_imageNamedFromMyBundle:photoDefImageName]; } - (void)setPhotoNumberIconImageName:(NSString *)photoNumberIconImageName { _photoNumberIconImageName = photoNumberIconImageName; _photoNumberIconImage = [UIImage tz_imageNamedFromMyBundle:photoNumberIconImageName]; } - (void)setPhotoPreviewOriginDefImageName:(NSString *)photoPreviewOriginDefImageName { _photoPreviewOriginDefImageName = photoPreviewOriginDefImageName; _photoPreviewOriginDefImage = [UIImage tz_imageNamedFromMyBundle:photoPreviewOriginDefImageName]; } - (void)setPhotoOriginDefImageName:(NSString *)photoOriginDefImageName { _photoOriginDefImageName = photoOriginDefImageName; _photoOriginDefImage = [UIImage tz_imageNamedFromMyBundle:photoOriginDefImageName]; } - (void)setPhotoOriginSelImageName:(NSString *)photoOriginSelImageName { _photoOriginSelImageName = photoOriginSelImageName; _photoOriginSelImage = [UIImage tz_imageNamedFromMyBundle:photoOriginSelImageName]; } - (void)setTakePictureImage:(UIImage *)takePictureImage { _takePictureImage = takePictureImage; _takePictureImageName = @""; } - (void)setIconThemeColor:(UIColor *)iconThemeColor { _iconThemeColor = iconThemeColor; [self configDefaultImageName]; } - (void)configDefaultBtnTitle { self.doneBtnTitleStr = [NSBundle tz_localizedStringForKey:@"Done"]; self.cancelBtnTitleStr = [NSBundle tz_localizedStringForKey:@"Cancel"]; self.previewBtnTitleStr = [NSBundle tz_localizedStringForKey:@"Preview"]; self.fullImageBtnTitleStr = [NSBundle tz_localizedStringForKey:@"Full image"]; self.settingBtnTitleStr = [NSBundle tz_localizedStringForKey:@"Setting"]; self.processHintStr = [NSBundle tz_localizedStringForKey:@"Processing..."]; self.editBtnTitleStr = [NSBundle tz_localizedStringForKey:@"Edit"]; } - (void)setShowSelectedIndex:(BOOL)showSelectedIndex { _showSelectedIndex = showSelectedIndex; if (showSelectedIndex) { self.photoSelImage = [self createImageWithColor:nil size:CGSizeMake(24, 24) radius:12]; } [TZImagePickerConfig sharedInstance].showSelectedIndex = showSelectedIndex; } - (void)setShowPhotoCannotSelectLayer:(BOOL)showPhotoCannotSelectLayer { _showPhotoCannotSelectLayer = showPhotoCannotSelectLayer; [TZImagePickerConfig sharedInstance].showPhotoCannotSelectLayer = showPhotoCannotSelectLayer; } - (void)setNotScaleImage:(BOOL)notScaleImage { _notScaleImage = notScaleImage; [TZImagePickerConfig sharedInstance].notScaleImage = notScaleImage; } - (void)setNeedFixComposition:(BOOL)needFixComposition { _needFixComposition = needFixComposition; [TZImagePickerConfig sharedInstance].needFixComposition = needFixComposition; } - (void)observeAuthrizationStatusChange { [_timer invalidate]; _timer = nil; if ([PHPhotoLibrary authorizationStatus] == 0) { _timer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(observeAuthrizationStatusChange) userInfo:nil repeats:NO]; } if ([[TZImageManager manager] authorizationStatusAuthorized]) { [_tipLabel removeFromSuperview]; [_settingBtn removeFromSuperview]; [self pushPhotoPickerVc]; TZAlbumPickerController *albumPickerVc = (TZAlbumPickerController *)self.visibleViewController; if ([albumPickerVc isKindOfClass:[TZAlbumPickerController class]]) { [albumPickerVc configTableView]; } } } - (void)pushPhotoPickerVc { _didPushPhotoPickerVc = NO; // 1.6.8 判断是否需要push到照片选择页,如果_pushPhotoPickerVc为NO,则不push if (!_didPushPhotoPickerVc && _pushPhotoPickerVc) { TZPhotoPickerController *photoPickerVc = [[TZPhotoPickerController alloc] init]; photoPickerVc.isFirstAppear = YES; photoPickerVc.columnNumber = self.columnNumber; [[TZImageManager manager] getCameraRollAlbumWithFetchAssets:NO completion:^(TZAlbumModel *model) { photoPickerVc.model = model; [self pushViewController:photoPickerVc animated:YES]; self->_didPushPhotoPickerVc = YES; }]; } } - (UIAlertController *)showAlertWithTitle:(NSString *)title { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:[NSBundle tz_localizedStringForKey:@"OK"] style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; return alertController; } - (void)showProgressHUD { if (!_progressHUD) { _progressHUD = [UIButton buttonWithType:UIButtonTypeCustom]; [_progressHUD setBackgroundColor:[UIColor clearColor]]; _HUDContainer = [[UIView alloc] init]; _HUDContainer.layer.cornerRadius = 8; _HUDContainer.clipsToBounds = YES; _HUDContainer.backgroundColor = [UIColor darkGrayColor]; _HUDContainer.alpha = 0.7; _HUDIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; _HUDLabel = [[UILabel alloc] init]; _HUDLabel.textAlignment = NSTextAlignmentCenter; _HUDLabel.text = self.processHintStr; _HUDLabel.font = [UIFont systemFontOfSize:15]; _HUDLabel.textColor = [UIColor whiteColor]; [_HUDContainer addSubview:_HUDLabel]; [_HUDContainer addSubview:_HUDIndicatorView]; [_progressHUD addSubview:_HUDContainer]; } [_HUDIndicatorView startAnimating]; UIWindow *applicationWindow; if ([[[UIApplication sharedApplication] delegate] respondsToSelector:@selector(window)]) { applicationWindow = [[[UIApplication sharedApplication] delegate] window]; } else { applicationWindow = [[UIApplication sharedApplication] keyWindow]; } [applicationWindow addSubview:_progressHUD]; [self.view setNeedsLayout]; // if over time, dismiss HUD automatic self.HUDTimeoutCount++; __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.timeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.HUDTimeoutCount--; if (strongSelf.HUDTimeoutCount <= 0) { strongSelf.HUDTimeoutCount = 0; [strongSelf hideProgressHUD]; } }); } - (void)hideProgressHUD { if (_progressHUD) { [_HUDIndicatorView stopAnimating]; [_progressHUD removeFromSuperview]; } } - (void)setMaxImagesCount:(NSInteger)maxImagesCount { _maxImagesCount = maxImagesCount; if (maxImagesCount > 1) { _showSelectBtn = YES; _allowCrop = NO; } } - (void)setShowSelectBtn:(BOOL)showSelectBtn { _showSelectBtn = showSelectBtn; // 多选模式下,不允许让showSelectBtn为NO if (!showSelectBtn && _maxImagesCount > 1) { _showSelectBtn = YES; } } - (void)setAllowCrop:(BOOL)allowCrop { _allowCrop = _maxImagesCount > 1 ? NO : allowCrop; if (allowCrop) { // 允许裁剪的时候,不能选原图和GIF self.allowPickingOriginalPhoto = NO; self.allowPickingGif = NO; self.photoWidth = 1200; self.photoPreviewMaxWidth = 1200; } } - (void)setCircleCropRadius:(NSInteger)circleCropRadius { _circleCropRadius = circleCropRadius; self.cropRect = CGRectMake(self.view.tz_width / 2 - circleCropRadius, self.view.tz_height / 2 - _circleCropRadius, _circleCropRadius * 2, _circleCropRadius * 2); } - (void)setCropRect:(CGRect)cropRect { _cropRect = cropRect; if ([TZCommonTools tz_isLandscape]) { _cropRectLandscape = cropRect; CGFloat widthHeight = cropRect.size.height; _cropRectPortrait = CGRectMake(cropRect.origin.y, (self.view.tz_width - widthHeight) / 2, widthHeight, widthHeight); } else { _cropRectPortrait = cropRect; CGFloat widthHeight = cropRect.size.width; _cropRectLandscape = CGRectMake((self.view.tz_height - widthHeight) / 2, cropRect.origin.x, widthHeight, widthHeight); } } - (CGRect)cropRect { CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; BOOL isFullScreen = self.view.tz_height == screenHeight; if (isFullScreen) { return _cropRect; } else { CGRect newCropRect = _cropRect; newCropRect.origin.y -= ((screenHeight - self.view.tz_height) / 2); return newCropRect; } } - (void)setTimeout:(NSInteger)timeout { _timeout = timeout; if (timeout < 5) { _timeout = 5; } else if (_timeout > 600) { _timeout = 600; } } - (void)setPickerDelegate:(id)pickerDelegate { _pickerDelegate = pickerDelegate; [TZImageManager manager].pickerDelegate = pickerDelegate; } - (void)setColumnNumber:(NSInteger)columnNumber { _columnNumber = columnNumber; if (columnNumber <= 2) { _columnNumber = 2; } else if (columnNumber >= 6) { _columnNumber = 6; } TZAlbumPickerController *albumPickerVc = [self.childViewControllers firstObject]; albumPickerVc.columnNumber = _columnNumber; [TZImageManager manager].columnNumber = _columnNumber; } - (void)setMinPhotoWidthSelectable:(NSInteger)minPhotoWidthSelectable { _minPhotoWidthSelectable = minPhotoWidthSelectable; [TZImageManager manager].minPhotoWidthSelectable = minPhotoWidthSelectable; } - (void)setMinPhotoHeightSelectable:(NSInteger)minPhotoHeightSelectable { _minPhotoHeightSelectable = minPhotoHeightSelectable; [TZImageManager manager].minPhotoHeightSelectable = minPhotoHeightSelectable; } - (void)setHideWhenCanNotSelect:(BOOL)hideWhenCanNotSelect { _hideWhenCanNotSelect = hideWhenCanNotSelect; [TZImageManager manager].hideWhenCanNotSelect = hideWhenCanNotSelect; } - (void)setPhotoPreviewMaxWidth:(CGFloat)photoPreviewMaxWidth { _photoPreviewMaxWidth = photoPreviewMaxWidth; if (photoPreviewMaxWidth > 800) { _photoPreviewMaxWidth = 800; } else if (photoPreviewMaxWidth < 500) { _photoPreviewMaxWidth = 500; } [TZImageManager manager].photoPreviewMaxWidth = _photoPreviewMaxWidth; } - (void)setPhotoWidth:(CGFloat)photoWidth { _photoWidth = photoWidth; [TZImageManager manager].photoWidth = photoWidth; } - (void)setSelectedAssets:(NSMutableArray *)selectedAssets { _selectedAssets = selectedAssets; _selectedModels = [NSMutableArray array]; _selectedAssetIds = [NSMutableArray array]; for (PHAsset *asset in selectedAssets) { TZAssetModel *model = [TZAssetModel modelWithAsset:asset type:[[TZImageManager manager] getAssetType:asset]]; model.isSelected = YES; [self addSelectedModel:model]; } } - (void)setAllowPickingImage:(BOOL)allowPickingImage { _allowPickingImage = allowPickingImage; [TZImagePickerConfig sharedInstance].allowPickingImage = allowPickingImage; if (!allowPickingImage) { _allowTakePicture = NO; } } - (void)setAllowPickingVideo:(BOOL)allowPickingVideo { _allowPickingVideo = allowPickingVideo; [TZImagePickerConfig sharedInstance].allowPickingVideo = allowPickingVideo; if (!allowPickingVideo) { _allowTakeVideo = NO; } } - (void)setPreferredLanguage:(NSString *)preferredLanguage { _preferredLanguage = preferredLanguage; [TZImagePickerConfig sharedInstance].preferredLanguage = preferredLanguage; [self configDefaultBtnTitle]; } - (void)setLanguageBundle:(NSBundle *)languageBundle { _languageBundle = languageBundle; [TZImagePickerConfig sharedInstance].languageBundle = languageBundle; [self configDefaultBtnTitle]; } - (void)setSortAscendingByModificationDate:(BOOL)sortAscendingByModificationDate { _sortAscendingByModificationDate = sortAscendingByModificationDate; [TZImageManager manager].sortAscendingByModificationDate = sortAscendingByModificationDate; } - (void)settingBtnClick { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; } - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { viewController.automaticallyAdjustsScrollViewInsets = NO; [super pushViewController:viewController animated:animated]; } - (void)dealloc { // NSLog(@"%@ dealloc",NSStringFromClass(self.class)); } - (void)addSelectedModel:(TZAssetModel *)model { [_selectedModels addObject:model]; [_selectedAssetIds addObject:model.asset.localIdentifier]; } - (void)removeSelectedModel:(TZAssetModel *)model { [_selectedModels removeObject:model]; [_selectedAssetIds removeObject:model.asset.localIdentifier]; } - (UIImage *)createImageWithColor:(UIColor *)color size:(CGSize)size radius:(CGFloat)radius { if (!color) { color = self.iconThemeColor; } CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height); UIGraphicsBeginImageContextWithOptions(rect.size, NO, [UIScreen mainScreen].scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius]; CGContextAddPath(context, path.CGPath); CGContextFillPath(context); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } #pragma mark - UIContentContainer - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (![UIApplication sharedApplication].statusBarHidden) { if (self.needShowStatusBar) [UIApplication sharedApplication].statusBarHidden = NO; } }); if (size.width > size.height) { _cropRect = _cropRectLandscape; } else { _cropRect = _cropRectPortrait; } } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat progressHUDY = CGRectGetMaxY(self.navigationBar.frame); _progressHUD.frame = CGRectMake(0, progressHUDY, self.view.tz_width, self.view.tz_height - progressHUDY); _HUDContainer.frame = CGRectMake((self.view.tz_width - 120) / 2, (_progressHUD.tz_height - 90 - progressHUDY) / 2, 120, 90); _HUDIndicatorView.frame = CGRectMake(45, 15, 30, 30); _HUDLabel.frame = CGRectMake(0,40, 120, 50); } #pragma mark - Public - (void)cancelButtonClick { if (self.autoDismiss) { [self dismissViewControllerAnimated:YES completion:^{ [self callDelegateMethod]; }]; } else { [self callDelegateMethod]; } } - (void)callDelegateMethod { if ([self.pickerDelegate respondsToSelector:@selector(tz_imagePickerControllerDidCancel:)]) { [self.pickerDelegate tz_imagePickerControllerDidCancel:self]; } if (self.imagePickerControllerDidCancelHandle) { self.imagePickerControllerDidCancelHandle(); } } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { if ([self.topViewController isKindOfClass:TZVideoPlayerController.class] && self.topViewController.presentedViewController) { return UIInterfaceOrientationMaskPortrait; } return UIInterfaceOrientationMaskAll; } @end @interface TZAlbumPickerController () { UITableView *_tableView; } @property (nonatomic, strong) NSMutableArray *albumArr; @end @implementation TZAlbumPickerController - (void)viewDidLoad { [super viewDidLoad]; if ([[TZImageManager manager] authorizationStatusAuthorized] || !SYSTEM_VERSION_GREATER_THAN_15) { [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; } self.isFirstAppear = YES; if (@available(iOS 13.0, *)) { self.view.backgroundColor = UIColor.tertiarySystemBackgroundColor; } else { self.view.backgroundColor = [UIColor whiteColor]; } TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithTitle:imagePickerVc.cancelBtnTitleStr style:UIBarButtonItemStylePlain target:imagePickerVc action:@selector(cancelButtonClick)]; [TZCommonTools configBarButtonItem:cancelItem tzImagePickerVc:imagePickerVc]; self.navigationItem.rightBarButtonItem = cancelItem; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; [imagePickerVc hideProgressHUD]; if (imagePickerVc.allowPickingImage) { self.navigationItem.title = [NSBundle tz_localizedStringForKey:@"Photos"]; } else if (imagePickerVc.allowPickingVideo) { self.navigationItem.title = [NSBundle tz_localizedStringForKey:@"Videos"]; } if (self.isFirstAppear && !imagePickerVc.navLeftBarButtonSettingBlock) { self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle tz_localizedStringForKey:@"Back"] style:UIBarButtonItemStylePlain target:nil action:nil]; } [self configTableView]; } - (void)configTableView { if (![[TZImageManager manager] authorizationStatusAuthorized]) { return; } TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; if (self.isFirstAppear) { [imagePickerVc showProgressHUD]; } dispatch_async(dispatch_get_global_queue(0, 0), ^{ [[TZImageManager manager] getAllAlbumsWithFetchAssets:!self.isFirstAppear completion:^(NSArray *models) { dispatch_async(dispatch_get_main_queue(), ^{ self->_albumArr = [NSMutableArray arrayWithArray:models]; for (TZAlbumModel *albumModel in self->_albumArr) { albumModel.selectedModels = imagePickerVc.selectedModels; } [imagePickerVc hideProgressHUD]; if (self.isFirstAppear) { self.isFirstAppear = NO; [self configTableView]; } if (!self->_tableView) { self->_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; self->_tableView.rowHeight = 70; if (@available(iOS 13.0, *)) { self->_tableView.backgroundColor = [UIColor tertiarySystemBackgroundColor]; } else { self->_tableView.backgroundColor = [UIColor whiteColor]; } self->_tableView.tableFooterView = [[UIView alloc] init]; self->_tableView.dataSource = self; self->_tableView.delegate = self; [self->_tableView registerClass:[TZAlbumCell class] forCellReuseIdentifier:@"TZAlbumCell"]; [self.view addSubview:self->_tableView]; if (imagePickerVc.albumPickerPageUIConfigBlock) { imagePickerVc.albumPickerPageUIConfigBlock(self->_tableView); } } else { [self->_tableView reloadData]; } }); }]; }); } - (void)dealloc { [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; // NSLog(@"%@ dealloc",NSStringFromClass(self.class)); } - (UIStatusBarStyle)preferredStatusBarStyle { TZImagePickerController *tzImagePicker = (TZImagePickerController *)self.navigationController; if (tzImagePicker && [tzImagePicker isKindOfClass:[TZImagePickerController class]]) { return tzImagePicker.statusBarStyle; } return [super preferredStatusBarStyle]; } #pragma mark - PHPhotoLibraryChangeObserver - (void)photoLibraryDidChange:(PHChange *)changeInstance { dispatch_async(dispatch_get_main_queue(), ^{ [self configTableView]; }); } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat top = 0; CGFloat tableViewHeight = 0; CGFloat naviBarHeight = self.navigationController.navigationBar.tz_height; BOOL isStatusBarHidden = [UIApplication sharedApplication].isStatusBarHidden; BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height; if (self.navigationController.navigationBar.isTranslucent) { top = naviBarHeight; if (!isStatusBarHidden && isFullScreen) top += [TZCommonTools tz_statusBarHeight]; tableViewHeight = self.view.tz_height - top; } else { tableViewHeight = self.view.tz_height; } _tableView.frame = CGRectMake(0, top, self.view.tz_width, tableViewHeight); TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; if (imagePickerVc.albumPickerPageDidLayoutSubviewsBlock) { imagePickerVc.albumPickerPageDidLayoutSubviewsBlock(_tableView); } } #pragma mark - UITableViewDataSource && Delegate - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _albumArr.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TZAlbumCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TZAlbumCell" forIndexPath:indexPath]; if (@available(iOS 13.0, *)) { cell.backgroundColor = UIColor.tertiarySystemBackgroundColor; } TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; cell.albumCellDidLayoutSubviewsBlock = imagePickerVc.albumCellDidLayoutSubviewsBlock; cell.albumCellDidSetModelBlock = imagePickerVc.albumCellDidSetModelBlock; cell.selectedCountButton.backgroundColor = imagePickerVc.iconThemeColor; cell.model = _albumArr[indexPath.row]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { TZPhotoPickerController *photoPickerVc = [[TZPhotoPickerController alloc] init]; photoPickerVc.columnNumber = self.columnNumber; TZAlbumModel *model = _albumArr[indexPath.row]; photoPickerVc.model = model; [self.navigationController pushViewController:photoPickerVc animated:YES]; [tableView deselectRowAtIndexPath:indexPath animated:NO]; } #pragma clang diagnostic pop @end @implementation UIImage (MyBundle) + (UIImage *)tz_imageNamedFromMyBundle:(NSString *)name { NSBundle *imageBundle = [NSBundle tz_imagePickerBundle]; name = [name stringByAppendingString:@"@2x"]; NSString *imagePath = [imageBundle pathForResource:name ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; if (!image) { // 兼容业务方自己设置图片的方式 name = [name stringByReplacingOccurrencesOfString:@"@2x" withString:@""]; image = [UIImage imageNamed:name]; } return image; } @end // 适配RTL的UICollectionViewFlowLayout @interface TZRTLLayout : UICollectionViewFlowLayout @end @implementation TZRTLLayout - (BOOL)flipsHorizontallyInOppositeLayoutDirection { return [TZCommonTools tz_isRightToLeftLayout]; } @end @implementation TZCommonTools + (UIEdgeInsets)tz_safeAreaInsets { UIWindow *window = [UIApplication sharedApplication].windows.firstObject; if (![window isKeyWindow]) { UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; if (CGRectEqualToRect(keyWindow.bounds, [UIScreen mainScreen].bounds)) { window = keyWindow; } } if (@available(iOS 11.0, *)) { UIEdgeInsets insets = [window safeAreaInsets]; return insets; } return UIEdgeInsetsZero; } + (BOOL)tz_isIPhoneX { if ([UIWindow instancesRespondToSelector:@selector(safeAreaInsets)]) { return [self tz_safeAreaInsets].bottom > 0; } return (CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(375, 812)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(812, 375)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(414, 896)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(896, 414)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(390, 844)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(844, 390)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(428, 926)) || CGSizeEqualToSize([UIScreen mainScreen].bounds.size, CGSizeMake(926, 428))); } + (BOOL)tz_isLandscape { if ([UIApplication sharedApplication].statusBarOrientation == UIDeviceOrientationLandscapeRight || [UIApplication sharedApplication].statusBarOrientation == UIDeviceOrientationLandscapeLeft) { return true; } return false; } + (CGFloat)tz_statusBarHeight { if ([UIWindow instancesRespondToSelector:@selector(safeAreaInsets)]) { return [self tz_safeAreaInsets].top ?: 20; } return 20; } // 获得Info.plist数据字典 + (NSDictionary *)tz_getInfoDictionary { NSDictionary *infoDict = [NSBundle mainBundle].localizedInfoDictionary; if (!infoDict || !infoDict.count) { infoDict = [NSBundle mainBundle].infoDictionary; } if (!infoDict || !infoDict.count) { NSString *path = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"]; infoDict = [NSDictionary dictionaryWithContentsOfFile:path]; } return infoDict ? infoDict : @{}; } + (NSString *)tz_getAppName { NSDictionary *infoDict = [self tz_getInfoDictionary]; NSString *appName = [infoDict valueForKey:@"CFBundleDisplayName"]; if (!appName) appName = [infoDict valueForKey:@"CFBundleName"]; if (!appName) appName = [infoDict valueForKey:@"CFBundleExecutable"]; if (!appName) { infoDict = [NSBundle mainBundle].infoDictionary; appName = [infoDict valueForKey:@"CFBundleDisplayName"]; if (!appName) appName = [infoDict valueForKey:@"CFBundleName"]; if (!appName) appName = [infoDict valueForKey:@"CFBundleExecutable"]; } return appName; } + (BOOL)tz_isRightToLeftLayout { if (@available(iOS 9.0, *)) { if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:UIView.appearance.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) { return YES; } } else { NSString *preferredLanguage = [NSLocale preferredLanguages].firstObject; if ([preferredLanguage hasPrefix:@"ar-"]) { return YES; } } return NO; } + (void)configBarButtonItem:(UIBarButtonItem *)item tzImagePickerVc:(TZImagePickerController *)tzImagePickerVc { item.tintColor = tzImagePickerVc.barItemTextColor; NSMutableDictionary *textAttrs = [NSMutableDictionary dictionary]; textAttrs[NSForegroundColorAttributeName] = tzImagePickerVc.barItemTextColor; textAttrs[NSFontAttributeName] = tzImagePickerVc.barItemTextFont; [item setTitleTextAttributes:textAttrs forState:UIControlStateNormal]; NSMutableDictionary *textAttrsHighlighted = [NSMutableDictionary dictionary]; textAttrsHighlighted[NSFontAttributeName] = tzImagePickerVc.barItemTextFont; [item setTitleTextAttributes:textAttrsHighlighted forState:UIControlStateHighlighted]; } + (BOOL)isICloudSyncError:(NSError *)error { if (!error) return NO; if ([error.domain isEqualToString:@"CKErrorDomain"] || [error.domain isEqualToString:@"CloudPhotoLibraryErrorDomain"]) { return YES; } return NO; } + (BOOL)isAssetNotSelectable:(TZAssetModel *)model tzImagePickerVc:(TZImagePickerController *)tzImagePickerVc { BOOL notSelectable = tzImagePickerVc.selectedModels.count >= tzImagePickerVc.maxImagesCount; if (tzImagePickerVc.selectedModels && tzImagePickerVc.selectedModels.count > 0 && !tzImagePickerVc.allowPickingMultipleVideo) { if (model.asset.mediaType == PHAssetMediaTypeVideo) { notSelectable = true; } } return notSelectable; } + (UICollectionViewFlowLayout *)tz_rtlFlowLayout { return [[TZRTLLayout alloc] init]; } @end @interface TZImagePickerConfig () @property (strong, nonatomic) NSSet *supportedLanguages; @end @implementation TZImagePickerConfig + (instancetype)sharedInstance { static dispatch_once_t onceToken; static TZImagePickerConfig *config = nil; dispatch_once(&onceToken, ^{ if (config == nil) { config = [[TZImagePickerConfig alloc] init]; config.supportedLanguages = [NSSet setWithObjects:@"zh-Hans", @"zh-Hant", @"en", @"ar", @"de", @"es", @"fr", @"ja", @"ko-KP", @"pt", @"ru", @"vi", nil]; config.preferredLanguage = nil; config.gifPreviewMaxImagesCount = 50; } }); return config; } - (void)setPreferredLanguage:(NSString *)preferredLanguage { _preferredLanguage = preferredLanguage; if (!preferredLanguage || !preferredLanguage.length) { preferredLanguage = [NSLocale preferredLanguages].firstObject; } NSString *usedLanguage = @"en"; for (NSString *language in self.supportedLanguages) { if ([preferredLanguage hasPrefix:language]) { usedLanguage = language; break; } } _languageBundle = [NSBundle bundleWithPath:[[NSBundle tz_imagePickerBundle] pathForResource:usedLanguage ofType:@"lproj"]]; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImageRequestOperation.h ================================================ // // TZImageRequestOperation.h // TZImagePickerControllerFramework // // Created by 谭真 on 2018/12/20. // Copyright © 2018 谭真. All rights reserved. // #import #import NS_ASSUME_NONNULL_BEGIN @interface TZImageRequestOperation : NSOperation typedef void(^TZImageRequestCompletedBlock)(UIImage *photo, NSDictionary *info, BOOL isDegraded); typedef void(^TZImageRequestProgressBlock)(double progress, NSError *error, BOOL *stop, NSDictionary *info); @property (nonatomic, copy, nullable) TZImageRequestCompletedBlock completedBlock; @property (nonatomic, copy, nullable) TZImageRequestProgressBlock progressBlock; @property (nonatomic, strong, nullable) PHAsset *asset; @property (assign, nonatomic, getter = isExecuting) BOOL executing; @property (assign, nonatomic, getter = isFinished) BOOL finished; - (instancetype)initWithAsset:(PHAsset *)asset completion:(TZImageRequestCompletedBlock)completionBlock progressHandler:(TZImageRequestProgressBlock)progressHandler; - (void)done; @end NS_ASSUME_NONNULL_END ================================================ FILE: TZImagePickerController/TZImagePickerController/TZImageRequestOperation.m ================================================ // // TZImageRequestOperation.m // TZImagePickerControllerFramework // // Created by 谭真 on 2018/12/20. // Copyright © 2018 谭真. All rights reserved. // #import "TZImageRequestOperation.h" #import "TZImageManager.h" @implementation TZImageRequestOperation @synthesize executing = _executing; @synthesize finished = _finished; - (instancetype)initWithAsset:(PHAsset *)asset completion:(TZImageRequestCompletedBlock)completionBlock progressHandler:(TZImageRequestProgressBlock)progressHandler { self = [super init]; self.asset = asset; self.completedBlock = completionBlock; self.progressBlock = progressHandler; _executing = NO; _finished = NO; return self; } - (void)start { self.executing = YES; [[TZImageManager manager] getPhotoWithAsset:self.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { dispatch_async(dispatch_get_main_queue(), ^{ if (!isDegraded) { if (self.completedBlock) { self.completedBlock(photo, info, isDegraded); } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self done]; }); } }); } progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.progressBlock) { self.progressBlock(progress, error, stop, info); } }); } networkAccessAllowed:YES]; } - (void)done { self.finished = YES; self.executing = NO; [self reset]; } - (void)reset { self.asset = nil; self.completedBlock = nil; self.progressBlock = nil; } - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } - (BOOL)isAsynchronous { return YES; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZPhotoPickerController.h ================================================ // // TZPhotoPickerController.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @class TZAlbumModel; @interface TZPhotoPickerController : UIViewController @property (nonatomic, assign) BOOL isFirstAppear; @property (nonatomic, assign) NSInteger columnNumber; @property (nonatomic, strong) TZAlbumModel *model; @end @interface TZCollectionView : UICollectionView @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZPhotoPickerController.m ================================================ // // TZPhotoPickerController.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "TZPhotoPickerController.h" #import "TZImagePickerController.h" #import "TZPhotoPreviewController.h" #import "TZAssetCell.h" #import "TZAssetModel.h" #import "UIView+TZLayout.h" #import "TZImageManager.h" #import "TZVideoPlayerController.h" #import "TZGifPhotoPreviewController.h" #import #import "TZImageRequestOperation.h" #import "TZAuthLimitedFooterTipView.h" #import @interface TZPhotoPickerController () { NSMutableArray *_models; UIView *_bottomToolBar; UIButton *_previewButton; UIButton *_doneButton; UIImageView *_numberImageView; UILabel *_numberLabel; UIButton *_originalPhotoButton; UILabel *_originalPhotoLabel; UIView *_divideLine; BOOL _shouldScrollToBottom; BOOL _showTakePhotoBtn; BOOL _authorizationLimited; CGFloat _offsetItemCount; } @property CGRect previousPreheatRect; @property (nonatomic, assign) BOOL isSelectOriginalPhoto; @property (nonatomic, strong) TZCollectionView *collectionView; @property (nonatomic, strong) TZAuthLimitedFooterTipView *authFooterTipView; @property (nonatomic, strong) UILabel *noDataLabel; @property (strong, nonatomic) UICollectionViewFlowLayout *layout; @property (nonatomic, strong) UIImagePickerController *imagePickerVc; @property (strong, nonatomic) CLLocation *location; @property (nonatomic, strong) NSOperationQueue *operationQueue; @property (nonatomic, assign) BOOL isSavingMedia; @property (nonatomic, assign) BOOL isFetchingMedia; @end static CGSize AssetGridThumbnailSize; static CGFloat itemMargin = 5; @implementation TZPhotoPickerController #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (UIImagePickerController *)imagePickerVc { if (_imagePickerVc == nil) { _imagePickerVc = [[UIImagePickerController alloc] init]; _imagePickerVc.delegate = self; // set appearance / 改变相册选择页的导航栏外观 _imagePickerVc.navigationBar.barTintColor = self.navigationController.navigationBar.barTintColor; _imagePickerVc.navigationBar.tintColor = self.navigationController.navigationBar.tintColor; UIBarButtonItem *tzBarItem, *BarItem; if (@available(iOS 9, *)) { tzBarItem = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[TZImagePickerController class]]]; BarItem = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UIImagePickerController class]]]; } else { tzBarItem = [UIBarButtonItem appearanceWhenContainedIn:[TZImagePickerController class], nil]; BarItem = [UIBarButtonItem appearanceWhenContainedIn:[UIImagePickerController class], nil]; } NSDictionary *titleTextAttributes = [tzBarItem titleTextAttributesForState:UIControlStateNormal]; [BarItem setTitleTextAttributes:titleTextAttributes forState:UIControlStateNormal]; } return _imagePickerVc; } - (void)viewDidLoad { [super viewDidLoad]; if ([[TZImageManager manager] authorizationStatusAuthorized] || !SYSTEM_VERSION_GREATER_THAN_15) { [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; } self.isFirstAppear = YES; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; _isSelectOriginalPhoto = tzImagePickerVc.isSelectOriginalPhoto; _shouldScrollToBottom = YES; if (@available(iOS 13.0, *)) { self.view.backgroundColor = UIColor.tertiarySystemBackgroundColor; } else { self.view.backgroundColor = [UIColor whiteColor]; } self.navigationItem.title = _model.name; UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithTitle:tzImagePickerVc.cancelBtnTitleStr style:UIBarButtonItemStylePlain target:tzImagePickerVc action:@selector(cancelButtonClick)]; [TZCommonTools configBarButtonItem:cancelItem tzImagePickerVc:tzImagePickerVc]; self.navigationItem.rightBarButtonItem = cancelItem; if (tzImagePickerVc.navLeftBarButtonSettingBlock) { UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeCustom]; leftButton.frame = CGRectMake(0, 0, 44, 44); [leftButton addTarget:self action:@selector(navLeftBarButtonClick) forControlEvents:UIControlEventTouchUpInside]; tzImagePickerVc.navLeftBarButtonSettingBlock(leftButton); self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:leftButton]; } else if (tzImagePickerVc.childViewControllers.count) { UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:[NSBundle tz_localizedStringForKey:@"Back"] style:UIBarButtonItemStylePlain target:self action:@selector(navLeftBarButtonClick)]; [TZCommonTools configBarButtonItem:backItem tzImagePickerVc:tzImagePickerVc]; [tzImagePickerVc.childViewControllers firstObject].navigationItem.backBarButtonItem = backItem; } _showTakePhotoBtn = _model.isCameraRoll && ((tzImagePickerVc.allowTakePicture && tzImagePickerVc.allowPickingImage) || (tzImagePickerVc.allowTakeVideo && tzImagePickerVc.allowPickingVideo)); _authorizationLimited = _model.isCameraRoll && [[TZImageManager manager] isPHAuthorizationStatusLimited]; // [self resetCachedAssets]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeStatusBarOrientationNotification:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; self.operationQueue = [[NSOperationQueue alloc] init]; self.operationQueue.maxConcurrentOperationCount = 3; } - (void)fetchAssetModels { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (_isFirstAppear && !_model.models.count) { [tzImagePickerVc showProgressHUD]; } dispatch_async(dispatch_get_global_queue(0, 0), ^{ CGFloat systemVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; if (!tzImagePickerVc.sortAscendingByModificationDate && self->_isFirstAppear && self->_model.isCameraRoll) { [[TZImageManager manager] getCameraRollAlbumWithFetchAssets:YES completion:^(TZAlbumModel *model) { self->_model = model; self->_models = [NSMutableArray arrayWithArray:self->_model.models]; [self initSubviews]; }]; } else if (self->_showTakePhotoBtn || self->_isFirstAppear || !self.model.models || systemVersion >= 14.0) { [[TZImageManager manager] getAssetsFromFetchResult:self->_model.result completion:^(NSArray *models) { self->_models = [NSMutableArray arrayWithArray:models]; [self initSubviews]; }]; } else { self->_models = [NSMutableArray arrayWithArray:self->_model.models]; [self initSubviews]; } }); } - (void)initSubviews { dispatch_async(dispatch_get_main_queue(), ^{ TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; [tzImagePickerVc hideProgressHUD]; [self checkSelectedModels]; [self configCollectionView]; self->_collectionView.hidden = YES; [self configBottomToolBar]; [self prepareScrollCollectionViewToBottom]; }); } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; tzImagePickerVc.isSelectOriginalPhoto = _isSelectOriginalPhoto; } - (BOOL)prefersStatusBarHidden { return NO; } - (UIStatusBarStyle)preferredStatusBarStyle { TZImagePickerController *tzImagePicker = (TZImagePickerController *)self.navigationController; if (tzImagePicker && [tzImagePicker isKindOfClass:[TZImagePickerController class]]) { return tzImagePicker.statusBarStyle; } return [super preferredStatusBarStyle]; } - (void)configCollectionView { if (!_collectionView) { _layout = [TZCommonTools tz_rtlFlowLayout]; _collectionView = [[TZCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_layout]; if (@available(iOS 13.0, *)) { _collectionView.backgroundColor = UIColor.tertiarySystemBackgroundColor; } else { _collectionView.backgroundColor = [UIColor whiteColor]; } _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.alwaysBounceHorizontal = NO; _collectionView.contentInset = UIEdgeInsetsMake(itemMargin, itemMargin, itemMargin, itemMargin); [self.view addSubview:_collectionView]; [_collectionView registerClass:[TZAssetCell class] forCellWithReuseIdentifier:@"TZAssetCell"]; [_collectionView registerClass:[TZAssetCameraCell class] forCellWithReuseIdentifier:@"TZAssetCameraCell"]; [_collectionView registerClass:[TZAssetAddMoreCell class] forCellWithReuseIdentifier:@"TZAssetAddMoreCell"]; } else { [_collectionView reloadData]; } if (!_authFooterTipView && _authorizationLimited) { _authFooterTipView = [[TZAuthLimitedFooterTipView alloc] initWithFrame:CGRectMake(0, 0, self.view.tz_width, 80)]; UITapGestureRecognizer *footTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openSettingsApplication)]; [_authFooterTipView addGestureRecognizer:footTap]; [self.view addSubview:_authFooterTipView]; } _collectionView.contentSize = CGSizeMake(self.view.tz_width, (([self getAllCellCount] + self.columnNumber - 1) / self.columnNumber) * self.view.tz_width); if (_models.count == 0) { [_collectionView addSubview:self.noDataLabel]; } else if (_noDataLabel) { [_noDataLabel removeFromSuperview]; _noDataLabel = nil; } } - (UILabel *)noDataLabel { if (!_noDataLabel) { _noDataLabel = [[UILabel alloc] initWithFrame:_collectionView.bounds]; _noDataLabel.textAlignment = NSTextAlignmentCenter; _noDataLabel.text = [NSBundle tz_localizedStringForKey:@"No Photos or Videos"]; CGFloat rgb = 153 / 256.0; _noDataLabel.textColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:1.0]; _noDataLabel.font = [UIFont boldSystemFontOfSize:20]; } return _noDataLabel; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Determine the size of the thumbnails to request from the PHCachingImageManager CGFloat scale = 2.0; if ([UIScreen mainScreen].bounds.size.width > 600) { scale = 1.0; } CGSize cellSize = ((UICollectionViewFlowLayout *)_collectionView.collectionViewLayout).itemSize; AssetGridThumbnailSize = CGSizeMake(cellSize.width * scale, cellSize.height * scale); if (!_models) { [self fetchAssetModels]; } } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; self.isFirstAppear = NO; // [self updateCachedAssets]; } - (void)configBottomToolBar { if (_bottomToolBar) return; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (!tzImagePickerVc.showSelectBtn) return; _bottomToolBar = [[UIView alloc] initWithFrame:CGRectZero]; CGFloat rgb = 253 / 255.0; if (@available(iOS 13.0, *)) { _bottomToolBar.backgroundColor = UIColor.tertiarySystemBackgroundColor; } else { _bottomToolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:1.0]; } _previewButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_previewButton addTarget:self action:@selector(previewButtonClick) forControlEvents:UIControlEventTouchUpInside]; _previewButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_previewButton setTitle:tzImagePickerVc.previewBtnTitleStr forState:UIControlStateNormal]; [_previewButton setTitle:tzImagePickerVc.previewBtnTitleStr forState:UIControlStateDisabled]; if (@available(iOS 13.0, *)) { [_previewButton setTitleColor:UIColor.labelColor forState:UIControlStateNormal]; } else { [_previewButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; } [_previewButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; _previewButton.enabled = tzImagePickerVc.selectedModels.count; if (tzImagePickerVc.allowPickingOriginalPhoto) { _originalPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; _originalPhotoButton.imageEdgeInsets = UIEdgeInsetsMake(0, [TZCommonTools tz_isRightToLeftLayout] ? 10 : -10, 0, 0); [_originalPhotoButton addTarget:self action:@selector(originalPhotoButtonClick) forControlEvents:UIControlEventTouchUpInside]; _originalPhotoButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_originalPhotoButton setTitle:tzImagePickerVc.fullImageBtnTitleStr forState:UIControlStateNormal]; [_originalPhotoButton setTitle:tzImagePickerVc.fullImageBtnTitleStr forState:UIControlStateSelected]; [_originalPhotoButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal]; if (@available(iOS 13.0, *)) { [_originalPhotoButton setTitleColor:[UIColor labelColor] forState:UIControlStateSelected]; } else { [_originalPhotoButton setTitleColor:[UIColor blackColor] forState:UIControlStateSelected]; } [_originalPhotoButton setImage:tzImagePickerVc.photoOriginDefImage forState:UIControlStateNormal]; [_originalPhotoButton setImage:tzImagePickerVc.photoOriginSelImage forState:UIControlStateSelected]; _originalPhotoButton.imageView.clipsToBounds = YES; _originalPhotoButton.imageView.contentMode = UIViewContentModeScaleAspectFit; _originalPhotoButton.selected = _isSelectOriginalPhoto; _originalPhotoButton.enabled = tzImagePickerVc.selectedModels.count > 0; _originalPhotoLabel = [[UILabel alloc] init]; _originalPhotoLabel.textAlignment = NSTextAlignmentNatural; _originalPhotoLabel.font = [UIFont systemFontOfSize:16]; if (@available(iOS 13.0, *)) { _originalPhotoLabel.textColor = [UIColor labelColor]; } else { _originalPhotoLabel.textColor = [UIColor blackColor]; } if (_isSelectOriginalPhoto) [self getSelectedPhotoBytes]; } _doneButton = [UIButton buttonWithType:UIButtonTypeCustom]; _doneButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside]; [_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal]; [_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateDisabled]; [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal]; [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled]; _doneButton.enabled = tzImagePickerVc.selectedModels.count || tzImagePickerVc.alwaysEnableDoneBtn; _numberImageView = [[UIImageView alloc] initWithImage:tzImagePickerVc.photoNumberIconImage]; _numberImageView.hidden = tzImagePickerVc.selectedModels.count <= 0; _numberImageView.clipsToBounds = YES; _numberImageView.contentMode = UIViewContentModeScaleAspectFit; _numberImageView.backgroundColor = [UIColor clearColor]; _numberLabel = [[UILabel alloc] init]; _numberLabel.font = [UIFont systemFontOfSize:15]; _numberLabel.adjustsFontSizeToFitWidth = YES; _numberLabel.textColor = [UIColor whiteColor]; _numberLabel.textAlignment = NSTextAlignmentCenter; _numberLabel.text = [NSString stringWithFormat:@"%zd",tzImagePickerVc.selectedModels.count]; _numberLabel.hidden = tzImagePickerVc.selectedModels.count <= 0; _numberLabel.backgroundColor = [UIColor clearColor]; _numberLabel.userInteractionEnabled = YES; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonClick)]; [_numberLabel addGestureRecognizer:tapGesture]; _divideLine = [[UIView alloc] init]; CGFloat rgb2 = 222 / 255.0; if (@available(iOS 13.0, *)) { UIColor *divideLineDyColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trainCollection) { if ([trainCollection userInterfaceStyle] == UIUserInterfaceStyleLight) { return [UIColor colorWithRed:rgb2 green:rgb2 blue:rgb2 alpha:1.0]; } else { CGFloat lineDarkRgb = 100 / 255.0; return [UIColor colorWithRed:lineDarkRgb green:lineDarkRgb blue:lineDarkRgb alpha:1.0]; } }]; _divideLine.backgroundColor = divideLineDyColor; } else { _divideLine.backgroundColor = [UIColor colorWithRed:rgb2 green:rgb2 blue:rgb2 alpha:1.0]; } [_bottomToolBar addSubview:_divideLine]; [_bottomToolBar addSubview:_previewButton]; [_bottomToolBar addSubview:_doneButton]; [_bottomToolBar addSubview:_numberImageView]; [_bottomToolBar addSubview:_numberLabel]; [_bottomToolBar addSubview:_originalPhotoButton]; [self.view addSubview:_bottomToolBar]; [_originalPhotoButton addSubview:_originalPhotoLabel]; if (tzImagePickerVc.photoPickerPageUIConfigBlock) { tzImagePickerVc.photoPickerPageUIConfigBlock(_collectionView, _bottomToolBar, _previewButton, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel, _divideLine); } } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; CGFloat top = 0; CGFloat collectionViewHeight = 0; CGFloat naviBarHeight = self.navigationController.navigationBar.tz_height; CGFloat footerTipViewH = _authorizationLimited ? 80 : 0; BOOL isStatusBarHidden = [UIApplication sharedApplication].isStatusBarHidden; BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height; BOOL isRTL = [TZCommonTools tz_isRightToLeftLayout]; CGFloat toolBarHeight = 50 + [TZCommonTools tz_safeAreaInsets].bottom; if (self.navigationController.navigationBar.isTranslucent) { top = naviBarHeight; if (!isStatusBarHidden && isFullScreen) top += [TZCommonTools tz_statusBarHeight]; collectionViewHeight = tzImagePickerVc.showSelectBtn ? self.view.tz_height - toolBarHeight - top : self.view.tz_height - top;; } else { collectionViewHeight = tzImagePickerVc.showSelectBtn ? self.view.tz_height - toolBarHeight : self.view.tz_height; } collectionViewHeight -= footerTipViewH; _collectionView.frame = CGRectMake(0, top, self.view.tz_width, collectionViewHeight); _noDataLabel.frame = _collectionView.bounds; CGFloat itemWH = (self.view.tz_width - (self.columnNumber + 1) * itemMargin) / self.columnNumber; _layout.itemSize = CGSizeMake(itemWH, itemWH); _layout.minimumInteritemSpacing = itemMargin; _layout.minimumLineSpacing = itemMargin; [_collectionView setCollectionViewLayout:_layout]; if (_offsetItemCount > 0) { CGFloat offsetY = _offsetItemCount * (_layout.itemSize.height + _layout.minimumLineSpacing); [_collectionView setContentOffset:CGPointMake(0, offsetY)]; } CGFloat toolBarTop = 0; if (!self.navigationController.navigationBar.isHidden) { toolBarTop = self.view.tz_height - toolBarHeight; } else { CGFloat navigationHeight = naviBarHeight + [TZCommonTools tz_statusBarHeight]; toolBarTop = self.view.tz_height - toolBarHeight - navigationHeight; } _bottomToolBar.frame = CGRectMake(0, toolBarTop, self.view.tz_width, toolBarHeight); if (_authFooterTipView) { CGFloat footerTipViewY = _bottomToolBar ? toolBarTop - footerTipViewH : self.view.tz_height - footerTipViewH; _authFooterTipView.frame = CGRectMake(0, footerTipViewY, self.view.tz_width, footerTipViewH);; } CGFloat previewWidth = [tzImagePickerVc.previewBtnTitleStr boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil].size.width + 2; if (!tzImagePickerVc.allowPreview) { previewWidth = 0.0; } previewWidth = !tzImagePickerVc.showSelectBtn ? 0 : previewWidth; if (isRTL) { _previewButton.frame = CGRectMake(self.view.tz_width - previewWidth - 10, 3, previewWidth, 44); } else { _previewButton.frame = CGRectMake(10, 3, previewWidth, 44); } if (tzImagePickerVc.allowPickingOriginalPhoto) { CGFloat fullImageWidth = [tzImagePickerVc.fullImageBtnTitleStr boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:13]} context:nil].size.width; if (isRTL) { _originalPhotoButton.frame = CGRectMake(_previewButton.frame.origin.x - (fullImageWidth + 56), 0, fullImageWidth + 56, 50); _originalPhotoLabel.frame = CGRectMake(-80 + 11, 0, 80, 50); } else { _originalPhotoButton.frame = CGRectMake(CGRectGetMaxX(_previewButton.frame), 0, fullImageWidth + 56, 50); _originalPhotoLabel.frame = CGRectMake(fullImageWidth + 46, 0, 80, 50); } } [_doneButton sizeToFit]; CGFloat donwButtonWidth = MAX(44, _doneButton.tz_width); if (isRTL) { _doneButton.frame = CGRectMake(12, 0, donwButtonWidth, 50); _numberImageView.frame = CGRectMake(_doneButton.tz_right + 5, 13, 24, 24); } else { _doneButton.frame = CGRectMake(self.view.tz_width - donwButtonWidth - 12, 0, donwButtonWidth, 50); _numberImageView.frame = CGRectMake(_doneButton.tz_left - 24 - 5, 13, 24, 24); } _numberLabel.frame = _numberImageView.frame; _divideLine.frame = CGRectMake(0, 0, self.view.tz_width, 1); [TZImageManager manager].columnNumber = [TZImageManager manager].columnNumber; [TZImageManager manager].photoWidth = tzImagePickerVc.photoWidth; [self.collectionView reloadData]; if (tzImagePickerVc.photoPickerPageDidLayoutSubviewsBlock) { tzImagePickerVc.photoPickerPageDidLayoutSubviewsBlock(_collectionView, _bottomToolBar, _previewButton, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel, _divideLine); } } #pragma mark - Notification - (void)didChangeStatusBarOrientationNotification:(NSNotification *)noti { _offsetItemCount = _collectionView.contentOffset.y / (_layout.itemSize.height + _layout.minimumLineSpacing); } #pragma mark - Click Event - (void)navLeftBarButtonClick{ [self.navigationController popViewControllerAnimated:YES]; } - (void)previewButtonClick { TZPhotoPreviewController *photoPreviewVc = [[TZPhotoPreviewController alloc] init]; [self pushPhotoPrevireViewController:photoPreviewVc needCheckSelectedModels:YES]; } - (void)originalPhotoButtonClick { _originalPhotoButton.selected = !_originalPhotoButton.isSelected; _isSelectOriginalPhoto = _originalPhotoButton.isSelected; _originalPhotoLabel.hidden = !_originalPhotoButton.isSelected; if (_isSelectOriginalPhoto) { [self getSelectedPhotoBytes]; } } - (void)doneButtonClick { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; // 1.6.8 判断是否满足最小必选张数的限制 if (tzImagePickerVc.minImagesCount && tzImagePickerVc.selectedModels.count < tzImagePickerVc.minImagesCount) { NSString *title = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Select a minimum of %zd photos"], tzImagePickerVc.minImagesCount]; [tzImagePickerVc showAlertWithTitle:title]; return; } [tzImagePickerVc showProgressHUD]; _doneButton.enabled = NO; self.isFetchingMedia = YES; NSMutableArray *assets = [NSMutableArray array]; NSMutableArray *photos; NSMutableArray *infoArr; if (tzImagePickerVc.onlyReturnAsset) { // not fetch image for (NSInteger i = 0; i < tzImagePickerVc.selectedModels.count; i++) { TZAssetModel *model = tzImagePickerVc.selectedModels[i]; [assets addObject:model.asset]; } } else { // fetch image photos = [NSMutableArray array]; infoArr = [NSMutableArray array]; for (NSInteger i = 0; i < tzImagePickerVc.selectedModels.count; i++) { [photos addObject:@1];[assets addObject:@1];[infoArr addObject:@1]; } __block BOOL havenotShowAlert = YES; [TZImageManager manager].shouldFixOrientation = YES; __block UIAlertController *alertView; for (NSInteger i = 0; i < tzImagePickerVc.selectedModels.count; i++) { TZAssetModel *model = tzImagePickerVc.selectedModels[i]; TZImageRequestOperation *operation = [[TZImageRequestOperation alloc] initWithAsset:model.asset completion:^(UIImage * _Nonnull photo, NSDictionary * _Nonnull info, BOOL isDegraded) { if (isDegraded) return; if (photo) { if (![TZImagePickerConfig sharedInstance].notScaleImage) { photo = [[TZImageManager manager] scaleImage:photo toSize:CGSizeMake(tzImagePickerVc.photoWidth, (int)(tzImagePickerVc.photoWidth * photo.size.height / photo.size.width))]; } [photos replaceObjectAtIndex:i withObject:photo]; } if (info) [infoArr replaceObjectAtIndex:i withObject:info]; [assets replaceObjectAtIndex:i withObject:model.asset]; for (id item in photos) { if ([item isKindOfClass:[NSNumber class]]) return; } if (havenotShowAlert && alertView) { [alertView dismissViewControllerAnimated:YES completion:^{ alertView = nil; [self didGetAllPhotos:photos assets:assets infoArr:infoArr]; }]; } else { [self didGetAllPhotos:photos assets:assets infoArr:infoArr]; } } progressHandler:^(double progress, NSError * _Nonnull error, BOOL * _Nonnull stop, NSDictionary * _Nonnull info) { // 如果图片正在从iCloud同步中,提醒用户 if (progress < 1 && havenotShowAlert && !alertView) { alertView = [tzImagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Synchronizing photos from iCloud"]]; havenotShowAlert = NO; return; } if (progress >= 1) { havenotShowAlert = YES; } }]; [self.operationQueue addOperation:operation]; } } if (tzImagePickerVc.selectedModels.count <= 0 || tzImagePickerVc.onlyReturnAsset) { [self didGetAllPhotos:photos assets:assets infoArr:infoArr]; } } - (void)didGetAllPhotos:(NSArray *)photos assets:(NSArray *)assets infoArr:(NSArray *)infoArr { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; [tzImagePickerVc hideProgressHUD]; _doneButton.enabled = YES; self.isFetchingMedia = NO; if (tzImagePickerVc.autoDismiss) { [self.navigationController dismissViewControllerAnimated:YES completion:^{ [self callDelegateMethodWithPhotos:photos assets:assets infoArr:infoArr]; }]; } else { [self callDelegateMethodWithPhotos:photos assets:assets infoArr:infoArr]; } } - (void)callDelegateMethodWithPhotos:(NSArray *)photos assets:(NSArray *)assets infoArr:(NSArray *)infoArr { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc.allowPickingVideo && tzImagePickerVc.maxImagesCount == 1) { if ([[TZImageManager manager] isVideo:[assets firstObject]]) { BOOL triggered = NO; if ([tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingVideo:sourceAssets:)]) { [tzImagePickerVc.pickerDelegate imagePickerController:tzImagePickerVc didFinishPickingVideo:[photos firstObject] sourceAssets:[assets firstObject]]; triggered = YES; } if (tzImagePickerVc.didFinishPickingVideoHandle) { tzImagePickerVc.didFinishPickingVideoHandle([photos firstObject], [assets firstObject]); triggered = YES; } if (triggered) return; } } if ([tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:)]) { [tzImagePickerVc.pickerDelegate imagePickerController:tzImagePickerVc didFinishPickingPhotos:photos sourceAssets:assets isSelectOriginalPhoto:_isSelectOriginalPhoto]; } if ([tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto:infos:)]) { [tzImagePickerVc.pickerDelegate imagePickerController:tzImagePickerVc didFinishPickingPhotos:photos sourceAssets:assets isSelectOriginalPhoto:_isSelectOriginalPhoto infos:infoArr]; } if (tzImagePickerVc.didFinishPickingPhotosHandle) { tzImagePickerVc.didFinishPickingPhotosHandle(photos,assets,_isSelectOriginalPhoto); } if (tzImagePickerVc.didFinishPickingPhotosWithInfosHandle) { tzImagePickerVc.didFinishPickingPhotosWithInfosHandle(photos,assets,_isSelectOriginalPhoto,infoArr); } } #pragma mark - UICollectionViewDataSource && Delegate - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self getAllCellCount]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; // the cell lead to add more photo / 去添加更多照片的cell if (indexPath.item == [self getAddMorePhotoCellIndex]) { TZAssetAddMoreCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZAssetAddMoreCell" forIndexPath:indexPath]; cell.imageView.image = tzImagePickerVc.addMorePhotoImage; cell.tipLabel.text = [NSBundle tz_localizedStringForKey:@"Add more accessible photos"]; cell.imageView.contentMode = UIViewContentModeScaleAspectFit; cell.imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; return cell; } // the cell lead to take a picture / 去拍照的cell if (indexPath.item == [self getTakePhotoCellIndex]) { TZAssetCameraCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZAssetCameraCell" forIndexPath:indexPath]; cell.imageView.image = tzImagePickerVc.takePictureImage; if ([tzImagePickerVc.takePictureImageName isEqualToString:@"takePicture80"]) { cell.imageView.contentMode = UIViewContentModeCenter; CGFloat rgb = 223 / 255.0; cell.imageView.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:1.0]; } else { cell.imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; } return cell; } // the cell dipaly photo or video / 展示照片或视频的cell TZAssetCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZAssetCell" forIndexPath:indexPath]; cell.allowPickingMultipleVideo = tzImagePickerVc.allowPickingMultipleVideo; cell.photoDefImage = tzImagePickerVc.photoDefImage; cell.photoSelImage = tzImagePickerVc.photoSelImage; cell.assetCellDidSetModelBlock = tzImagePickerVc.assetCellDidSetModelBlock; cell.assetCellDidLayoutSubviewsBlock = tzImagePickerVc.assetCellDidLayoutSubviewsBlock; TZAssetModel *model; if (tzImagePickerVc.sortAscendingByModificationDate) { model = _models[indexPath.item]; } else { NSInteger diff = [self getAllCellCount] - _models.count; model = _models[indexPath.item - diff];; } cell.allowPickingGif = tzImagePickerVc.allowPickingGif; cell.model = model; if (model.isSelected && tzImagePickerVc.showSelectedIndex) { cell.index = [tzImagePickerVc.selectedAssetIds indexOfObject:model.asset.localIdentifier] + 1; } cell.showSelectBtn = tzImagePickerVc.showSelectBtn; cell.allowPreview = tzImagePickerVc.allowPreview; BOOL notSelectable = [TZCommonTools isAssetNotSelectable:model tzImagePickerVc:tzImagePickerVc]; if (notSelectable && tzImagePickerVc.showPhotoCannotSelectLayer && !model.isSelected) { cell.cannotSelectLayerButton.backgroundColor = tzImagePickerVc.cannotSelectLayerColor; cell.cannotSelectLayerButton.hidden = NO; } else { cell.cannotSelectLayerButton.hidden = YES; } __weak typeof(cell) weakCell = cell; __weak typeof(self) weakSelf = self; __weak typeof(_numberImageView.layer) weakLayer = _numberImageView.layer; cell.didSelectPhotoBlock = ^(BOOL isSelected) { __strong typeof(weakCell) strongCell = weakCell; __strong typeof(weakSelf) strongSelf = weakSelf; __strong typeof(weakLayer) strongLayer = weakLayer; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)strongSelf.navigationController; // 1. cancel select / 取消选择 if (isSelected) { strongCell.selectPhotoButton.selected = NO; model.isSelected = NO; NSArray *selectedModels = [NSArray arrayWithArray:tzImagePickerVc.selectedModels]; for (TZAssetModel *model_item in selectedModels) { if ([model.asset.localIdentifier isEqualToString:model_item.asset.localIdentifier]) { [tzImagePickerVc removeSelectedModel:model_item]; [strongSelf setAsset:model_item.asset isSelect:NO]; break; } } [strongSelf refreshBottomToolBarStatus]; if (tzImagePickerVc.showSelectedIndex || tzImagePickerVc.showPhotoCannotSelectLayer) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_PHOTO_PICKER_RELOAD_NOTIFICATION" object:strongSelf.navigationController]; } [UIView showOscillatoryAnimationWithLayer:strongLayer type:TZOscillatoryAnimationToSmaller]; if (strongCell.model.iCloudFailed) { NSString *title = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"]; [tzImagePickerVc showAlertWithTitle:title]; } } else { // 2. select:check if over the maxImagesCount / 选择照片,检查是否超过了最大个数的限制 if (tzImagePickerVc.selectedModels.count < tzImagePickerVc.maxImagesCount) { if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) { return; } if (!tzImagePickerVc.allowPreview) { BOOL shouldDone = tzImagePickerVc.maxImagesCount == 1; if (!tzImagePickerVc.allowPickingMultipleVideo && (model.type == TZAssetModelMediaTypeVideo || model.type == TZAssetModelMediaTypePhotoGif)) { shouldDone = YES; } if (shouldDone) { model.isSelected = YES; [tzImagePickerVc addSelectedModel:model]; [strongSelf doneButtonClick]; return; } } strongCell.selectPhotoButton.selected = YES; model.isSelected = YES; [tzImagePickerVc addSelectedModel:model]; if (tzImagePickerVc.showSelectedIndex || tzImagePickerVc.showPhotoCannotSelectLayer) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_PHOTO_PICKER_RELOAD_NOTIFICATION" object:strongSelf.navigationController]; } [strongSelf setAsset:model.asset isSelect:YES]; [strongSelf refreshBottomToolBarStatus]; [UIView showOscillatoryAnimationWithLayer:strongLayer type:TZOscillatoryAnimationToSmaller]; } else { NSString *title = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Select a maximum of %zd photos"], tzImagePickerVc.maxImagesCount]; [tzImagePickerVc showAlertWithTitle:title]; } } }; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // take a photo / 去拍照 TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (indexPath.item == [self getAddMorePhotoCellIndex]) { [self addMorePhoto]; return; } if (indexPath.item == [self getTakePhotoCellIndex]) { [self takePhoto]; return; } // preview phote or video / 预览照片或视频 NSInteger index = indexPath.item; if (!tzImagePickerVc.sortAscendingByModificationDate) { index -= [self getAllCellCount] - _models.count; } TZAssetModel *model = _models[index]; if (model.type == TZAssetModelMediaTypeVideo && !tzImagePickerVc.allowPickingMultipleVideo) { if (tzImagePickerVc.selectedModels.count > 0) { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; [imagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Can not choose both video and photo"]]; } else { TZVideoPlayerController *videoPlayerVc = [[TZVideoPlayerController alloc] init]; videoPlayerVc.model = model; [self.navigationController pushViewController:videoPlayerVc animated:YES]; } } else if (model.type == TZAssetModelMediaTypePhotoGif && tzImagePickerVc.allowPickingGif && !tzImagePickerVc.allowPickingMultipleVideo) { if (tzImagePickerVc.selectedModels.count > 0) { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; [imagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Can not choose both photo and GIF"]]; } else { TZGifPhotoPreviewController *gifPreviewVc = [[TZGifPhotoPreviewController alloc] init]; gifPreviewVc.model = model; [self.navigationController pushViewController:gifPreviewVc animated:YES]; } } else { TZPhotoPreviewController *photoPreviewVc = [[TZPhotoPreviewController alloc] init]; photoPreviewVc.currentIndex = index; photoPreviewVc.models = _models; [self pushPhotoPrevireViewController:photoPreviewVc]; } } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // [self updateCachedAssets]; } #pragma mark - Private Method - (NSInteger)getAllCellCount { NSInteger count = _models.count; if (_showTakePhotoBtn) { count += 1; } if (_authorizationLimited) { count += 1; } return count; } - (NSInteger)getTakePhotoCellIndex { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (!_showTakePhotoBtn) { return -1; } if (tzImagePickerVc.sortAscendingByModificationDate) { return [self getAllCellCount] - 1; } else { return 0; } } - (NSInteger)getAddMorePhotoCellIndex { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (!_authorizationLimited) { return -1; } if (tzImagePickerVc.sortAscendingByModificationDate) { if (_showTakePhotoBtn) { return [self getAllCellCount] - 2; } return [self getAllCellCount] - 1; } else { return _showTakePhotoBtn ? 1 : 0; } } /// 拍照按钮点击事件 - (void)takePhoto { AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if ((authStatus == AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied)) { // 无权限 做一个友好的提示 NSString *appName = [TZCommonTools tz_getAppName]; NSString *title = [NSBundle tz_localizedStringForKey:@"Can not use camera"]; NSString *message = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Please allow %@ to access your camera in \"Settings -> Privacy -> Camera\""],appName]; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *cancelAct = [UIAlertAction actionWithTitle:[NSBundle tz_localizedStringForKey:@"Cancel"] style:UIAlertActionStyleCancel handler:nil]; [alertController addAction:cancelAct]; UIAlertAction *settingAct = [UIAlertAction actionWithTitle:[NSBundle tz_localizedStringForKey:@"Setting"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; }]; [alertController addAction:settingAct]; [self.navigationController presentViewController:alertController animated:YES completion:nil]; } else if (authStatus == AVAuthorizationStatusNotDetermined) { // fix issue 466, 防止用户首次拍照拒绝授权时相机页黑屏 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { dispatch_async(dispatch_get_main_queue(), ^{ [self pushImagePickerController]; }); } }]; } else { [self pushImagePickerController]; } } - (void)openSettingsApplication { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; } - (void)addMorePhoto { if (@available(iOS 14, *)) { [[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:self]; } } // 调用相机 - (void)pushImagePickerController { // 提前定位 TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; #ifdef TZ_HAVE_LOCATION_CODE if (tzImagePickerVc.allowCameraLocation) { __weak typeof(self) weakSelf = self; [[TZLocationManager manager] startLocationWithSuccessBlock:^(NSArray *locations) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.location = [locations firstObject]; } failureBlock:^(NSError *error) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.location = nil; }]; } #endif UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera; if ([UIImagePickerController isSourceTypeAvailable: sourceType]) { self.imagePickerVc.sourceType = sourceType; NSMutableArray *mediaTypes = [NSMutableArray array]; if (tzImagePickerVc.allowTakePicture) { [mediaTypes addObject:(NSString *)kUTTypeImage]; } if (tzImagePickerVc.allowTakeVideo) { [mediaTypes addObject:(NSString *)kUTTypeMovie]; self.imagePickerVc.videoMaximumDuration = tzImagePickerVc.videoMaximumDuration; } self.imagePickerVc.mediaTypes= mediaTypes; if (tzImagePickerVc.uiImagePickerControllerSettingBlock) { tzImagePickerVc.uiImagePickerControllerSettingBlock(_imagePickerVc); } [self presentViewController:_imagePickerVc animated:YES completion:nil]; } else { NSLog(@"模拟器中无法打开照相机,请在真机中使用"); } } - (void)refreshBottomToolBarStatus { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; _previewButton.enabled = tzImagePickerVc.selectedModels.count > 0; _doneButton.enabled = tzImagePickerVc.selectedModels.count > 0 || tzImagePickerVc.alwaysEnableDoneBtn; _numberImageView.hidden = tzImagePickerVc.selectedModels.count <= 0; _numberLabel.hidden = tzImagePickerVc.selectedModels.count <= 0; _numberLabel.text = [NSString stringWithFormat:@"%zd",tzImagePickerVc.selectedModels.count]; _originalPhotoButton.enabled = tzImagePickerVc.selectedModels.count > 0; _originalPhotoButton.selected = (_isSelectOriginalPhoto && _originalPhotoButton.enabled); _originalPhotoLabel.hidden = (!_originalPhotoButton.isSelected); if (_isSelectOriginalPhoto) [self getSelectedPhotoBytes]; if (tzImagePickerVc.photoPickerPageDidRefreshStateBlock) { tzImagePickerVc.photoPickerPageDidRefreshStateBlock(_collectionView, _bottomToolBar, _previewButton, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel, _divideLine);; } } - (void)pushPhotoPrevireViewController:(TZPhotoPreviewController *)photoPreviewVc { [self pushPhotoPrevireViewController:photoPreviewVc needCheckSelectedModels:NO]; } - (void)pushPhotoPrevireViewController:(TZPhotoPreviewController *)photoPreviewVc needCheckSelectedModels:(BOOL)needCheckSelectedModels { __weak typeof(self) weakSelf = self; photoPreviewVc.isSelectOriginalPhoto = _isSelectOriginalPhoto; [photoPreviewVc setBackButtonClickBlock:^(BOOL isSelectOriginalPhoto) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.isSelectOriginalPhoto = isSelectOriginalPhoto; if (needCheckSelectedModels) { [strongSelf checkSelectedModels]; } [strongSelf.collectionView reloadData]; [strongSelf refreshBottomToolBarStatus]; }]; [photoPreviewVc setDoneButtonClickBlock:^(BOOL isSelectOriginalPhoto) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.isSelectOriginalPhoto = isSelectOriginalPhoto; [strongSelf doneButtonClick]; }]; [photoPreviewVc setDoneButtonClickBlockCropMode:^(UIImage *cropedImage, id asset) { __strong typeof(weakSelf) strongSelf = weakSelf; NSArray *assets = @[]; if (asset) { assets = @[asset]; } NSArray *photos = @[]; if (cropedImage) { photos = @[cropedImage]; } [strongSelf didGetAllPhotos:photos assets:assets infoArr:nil]; }]; [self.navigationController pushViewController:photoPreviewVc animated:YES]; } - (void)getSelectedPhotoBytes { // 越南语 && 5屏幕时会显示不下,暂时这样处理 if ([[TZImagePickerConfig sharedInstance].preferredLanguage isEqualToString:@"vi"] && self.view.tz_width <= 320) { return; } TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; [[TZImageManager manager] getPhotosBytesWithArray:imagePickerVc.selectedModels completion:^(NSString *totalBytes) { self->_originalPhotoLabel.text = [NSString stringWithFormat:@"(%@)",totalBytes]; }]; } - (void)prepareScrollCollectionViewToBottom { if (_shouldScrollToBottom && _models.count > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self scrollCollectionViewToBottom]; // try fix #1562:https://github.com/banchichen/TZImagePickerController/issues/1562 if (@available(iOS 15.0, *)) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self scrollCollectionViewToBottom]; }); } }); } else { _collectionView.hidden = NO; } } - (void)scrollCollectionViewToBottom { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; NSInteger item = 0; if (tzImagePickerVc.sortAscendingByModificationDate) { item = [self getAllCellCount] - 1; } [self->_collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:item inSection:0] atScrollPosition:UICollectionViewScrollPositionBottom animated:NO]; self->_shouldScrollToBottom = NO; self->_collectionView.hidden = NO; } - (void)checkSelectedModels { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; NSArray *selectedModels = tzImagePickerVc.selectedModels; NSMutableSet *selectedAssets = [NSMutableSet setWithCapacity:selectedModels.count]; for (TZAssetModel *model in selectedModels) { [selectedAssets addObject:model.asset]; } // 拿到了最新的models,在此刷新照片选中状态 for (TZAssetModel *model in _models) { model.isSelected = NO; if ([selectedAssets containsObject:model.asset]) { model.isSelected = YES; } } } /// 选中/取消选中某张照片 - (void)setAsset:(PHAsset *)asset isSelect:(BOOL)isSelect { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (isSelect && [tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didSelectAsset:photo:isSelectOriginalPhoto:)]) { [self callDelegate:asset isSelect:YES]; } if (!isSelect && [tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didDeselectAsset:photo:isSelectOriginalPhoto:)]) { [self callDelegate:asset isSelect:NO]; } } /// 调用选中/取消选中某张照片的代理方法 - (void)callDelegate:(PHAsset *)asset isSelect:(BOOL)isSelect { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; __weak typeof(self) weakSelf = self; __weak typeof(tzImagePickerVc) weakImagePickerVc= tzImagePickerVc; [[TZImageManager manager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (isDegraded) return; __strong typeof(weakSelf) strongSelf = weakSelf; __strong typeof(weakImagePickerVc) strongImagePickerVc = weakImagePickerVc; if (isSelect) { [strongImagePickerVc.pickerDelegate imagePickerController:strongImagePickerVc didSelectAsset:asset photo:photo isSelectOriginalPhoto:strongSelf.isSelectOriginalPhoto]; } else { [strongImagePickerVc.pickerDelegate imagePickerController:strongImagePickerVc didDeselectAsset:asset photo:photo isSelectOriginalPhoto:strongSelf.isSelectOriginalPhoto]; } }]; } #pragma mark - UIImagePickerControllerDelegate - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { [picker dismissViewControllerAnimated:YES completion:nil]; NSString *type = [info objectForKey:UIImagePickerControllerMediaType]; if ([type isEqualToString:@"public.image"]) { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; [imagePickerVc showProgressHUD]; UIImage *photo = [info objectForKey:UIImagePickerControllerOriginalImage]; NSDictionary *meta = [info objectForKey:UIImagePickerControllerMediaMetadata]; if (photo) { self.isSavingMedia = YES; [[TZImageManager manager] savePhotoWithImage:photo meta:meta location:self.location completion:^(PHAsset *asset, NSError *error){ self.isSavingMedia = NO; if (!error && asset) { [self addPHAsset:asset]; } else { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; [tzImagePickerVc hideProgressHUD]; } }]; self.location = nil; } } else if ([type isEqualToString:@"public.movie"]) { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; [imagePickerVc showProgressHUD]; NSURL *videoUrl = [info objectForKey:UIImagePickerControllerMediaURL]; if (videoUrl) { self.isSavingMedia = YES; [[TZImageManager manager] saveVideoWithUrl:videoUrl location:self.location completion:^(PHAsset *asset, NSError *error) { self.isSavingMedia = NO; if (!error && asset) { [self addPHAsset:asset]; } else { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; [tzImagePickerVc hideProgressHUD]; } }]; self.location = nil; } } } - (void)addPHAsset:(PHAsset *)asset { TZAssetModel *assetModel = [[TZImageManager manager] createModelWithAsset:asset]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; [tzImagePickerVc hideProgressHUD]; if (tzImagePickerVc.sortAscendingByModificationDate) { [_models addObject:assetModel]; } else { [_models insertObject:assetModel atIndex:0]; } if (tzImagePickerVc.maxImagesCount <= 1) { if (tzImagePickerVc.allowCrop && asset.mediaType == PHAssetMediaTypeImage) { TZPhotoPreviewController *photoPreviewVc = [[TZPhotoPreviewController alloc] init]; if (tzImagePickerVc.sortAscendingByModificationDate) { photoPreviewVc.currentIndex = _models.count - 1; } else { photoPreviewVc.currentIndex = 0; } photoPreviewVc.models = _models; [self pushPhotoPrevireViewController:photoPreviewVc]; } else if (tzImagePickerVc.selectedModels.count < 1) { [tzImagePickerVc addSelectedModel:assetModel]; [self doneButtonClick]; } return; } if (tzImagePickerVc.selectedModels.count < tzImagePickerVc.maxImagesCount) { if (assetModel.type == TZAssetModelMediaTypeVideo && !tzImagePickerVc.allowPickingMultipleVideo) { // 不能多选视频的情况下,不选中拍摄的视频 } else { if ([[TZImageManager manager] isAssetCannotBeSelected:assetModel.asset]) { return; } assetModel.isSelected = YES; [tzImagePickerVc addSelectedModel:assetModel]; [self refreshBottomToolBarStatus]; } } _collectionView.hidden = YES; [_collectionView reloadData]; _shouldScrollToBottom = YES; [self prepareScrollCollectionViewToBottom]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:nil]; } - (void)dealloc { [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; // NSLog(@"%@ dealloc",NSStringFromClass(self.class)); } #pragma mark - PHPhotoLibraryChangeObserver - (void)photoLibraryDidChange:(PHChange *)changeInstance { if (self.isSavingMedia || self.isFetchingMedia) { return; } dispatch_async(dispatch_get_main_queue(), ^{ PHFetchResultChangeDetails *changeDetail = [changeInstance changeDetailsForFetchResult:self.model.result]; if (changeDetail == nil) return; if ([[TZImageManager manager] isPHAuthorizationStatusLimited]) { NSArray *changedObjects = [changeDetail changedObjects]; changeDetail = [PHFetchResultChangeDetails changeDetailsFromFetchResult:self.model.result toFetchResult:changeDetail.fetchResultAfterChanges changedObjects:changedObjects]; if (changeDetail && changeDetail.removedObjects.count) { [self handleRemovedAssets:changeDetail.removedObjects]; } } if (changeDetail.hasIncrementalChanges == NO) { [self.model refreshFetchResult]; [self fetchAssetModels]; } else { NSInteger insertedCount = changeDetail.insertedObjects.count; NSInteger removedCount = changeDetail.removedObjects.count; NSInteger changedCount = changeDetail.changedObjects.count; if (insertedCount > 0 || removedCount > 0 || changedCount > 0) { self.model.result = changeDetail.fetchResultAfterChanges; self.model.count = changeDetail.fetchResultAfterChanges.count; [self fetchAssetModels]; } } }); } - (void)handleRemovedAssets:(NSArray *)removedObjects { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; for (PHAsset *asset in removedObjects) { Boolean isSelected = [tzImagePickerVc.selectedAssetIds containsObject:asset.localIdentifier]; if (!isSelected) continue; NSArray *selectedModels = [NSArray arrayWithArray:tzImagePickerVc.selectedModels]; for (TZAssetModel *model_item in selectedModels) { if ([asset.localIdentifier isEqualToString:model_item.asset.localIdentifier]) { [tzImagePickerVc removeSelectedModel:model_item]; } } [self refreshBottomToolBarStatus]; } } #pragma mark - Asset Caching - (void)resetCachedAssets { [[TZImageManager manager].cachingImageManager stopCachingImagesForAllAssets]; self.previousPreheatRect = CGRectZero; } - (void)updateCachedAssets { BOOL isViewVisible = [self isViewLoaded] && [[self view] window] != nil; if (!isViewVisible) { return; } // The preheat window is twice the height of the visible rect. CGRect preheatRect = _collectionView.bounds; preheatRect = CGRectInset(preheatRect, 0.0f, -0.5f * CGRectGetHeight(preheatRect)); /* Check if the collection view is showing an area that is significantly different to the last preheated area. */ CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect)); if (delta > CGRectGetHeight(_collectionView.bounds) / 3.0f) { // Compute the assets to start caching and to stop caching. NSMutableArray *addedIndexPaths = [NSMutableArray array]; NSMutableArray *removedIndexPaths = [NSMutableArray array]; [self computeDifferenceBetweenRect:self.previousPreheatRect andRect:preheatRect removedHandler:^(CGRect removedRect) { NSArray *indexPaths = [self aapl_indexPathsForElementsInRect:removedRect]; [removedIndexPaths addObjectsFromArray:indexPaths]; } addedHandler:^(CGRect addedRect) { NSArray *indexPaths = [self aapl_indexPathsForElementsInRect:addedRect]; [addedIndexPaths addObjectsFromArray:indexPaths]; }]; NSArray *assetsToStartCaching = [self assetsAtIndexPaths:addedIndexPaths]; NSArray *assetsToStopCaching = [self assetsAtIndexPaths:removedIndexPaths]; // Update the assets the PHCachingImageManager is caching. [[TZImageManager manager].cachingImageManager startCachingImagesForAssets:assetsToStartCaching targetSize:AssetGridThumbnailSize contentMode:PHImageContentModeAspectFill options:nil]; [[TZImageManager manager].cachingImageManager stopCachingImagesForAssets:assetsToStopCaching targetSize:AssetGridThumbnailSize contentMode:PHImageContentModeAspectFill options:nil]; // Store the preheat rect to compare against in the future. self.previousPreheatRect = preheatRect; } } - (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler { if (CGRectIntersectsRect(newRect, oldRect)) { CGFloat oldMaxY = CGRectGetMaxY(oldRect); CGFloat oldMinY = CGRectGetMinY(oldRect); CGFloat newMaxY = CGRectGetMaxY(newRect); CGFloat newMinY = CGRectGetMinY(newRect); if (newMaxY > oldMaxY) { CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY)); addedHandler(rectToAdd); } if (oldMinY > newMinY) { CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY)); addedHandler(rectToAdd); } if (newMaxY < oldMaxY) { CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY)); removedHandler(rectToRemove); } if (oldMinY < newMinY) { CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY)); removedHandler(rectToRemove); } } else { addedHandler(newRect); removedHandler(oldRect); } } - (NSArray *)assetsAtIndexPaths:(NSArray *)indexPaths { if (indexPaths.count == 0) { return nil; } NSMutableArray *assets = [NSMutableArray arrayWithCapacity:indexPaths.count]; for (NSIndexPath *indexPath in indexPaths) { if (indexPath.item < _models.count) { TZAssetModel *model = _models[indexPath.item]; [assets addObject:model.asset]; } } return assets; } - (NSArray *)aapl_indexPathsForElementsInRect:(CGRect)rect { NSArray *allLayoutAttributes = [_collectionView.collectionViewLayout layoutAttributesForElementsInRect:rect]; if (allLayoutAttributes.count == 0) { return nil; } NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:allLayoutAttributes.count]; for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) { NSIndexPath *indexPath = layoutAttributes.indexPath; [indexPaths addObject:indexPath]; } return indexPaths; } #pragma clang diagnostic pop @end @implementation TZCollectionView - (BOOL)touchesShouldCancelInContentView:(UIView *)view { if ([view isKindOfClass:[UIControl class]]) { return YES; } return [super touchesShouldCancelInContentView:view]; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.h ================================================ // // TZPhotoPreviewCell.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @class TZAssetModel; @interface TZAssetPreviewCell : UICollectionViewCell @property (nonatomic, strong) TZAssetModel *model; @property (nonatomic, copy) void (^singleTapGestureBlock)(void); - (void)configSubviews; - (void)photoPreviewCollectionViewDidScroll; @end @class TZAssetModel,TZProgressView,TZPhotoPreviewView; @interface TZPhotoPreviewCell : TZAssetPreviewCell @property (nonatomic, copy) void (^imageProgressUpdateBlock)(double progress); @property (nonatomic, strong) TZPhotoPreviewView *previewView; @property (nonatomic, assign) BOOL allowCrop; @property (nonatomic, assign) CGRect cropRect; @property (nonatomic, assign) BOOL scaleAspectFillCrop; - (void)recoverSubviews; @end @interface TZPhotoPreviewView : UIView @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UIScrollView *scrollView; @property (nonatomic, strong) UIView *imageContainerView; @property (nonatomic, strong) TZProgressView *progressView; @property (nonatomic, strong) UIImageView *iCloudErrorIcon; @property (nonatomic, strong) UILabel *iCloudErrorLabel; @property (nonatomic, copy) void (^iCloudSyncFailedHandle)(id asset, BOOL isSyncFailed); @property (nonatomic, assign) BOOL allowCrop; @property (nonatomic, assign) CGRect cropRect; @property (nonatomic, assign) BOOL scaleAspectFillCrop; @property (nonatomic, strong) TZAssetModel *model; @property (nonatomic, strong) id asset; @property (nonatomic, copy) void (^singleTapGestureBlock)(void); @property (nonatomic, copy) void (^imageProgressUpdateBlock)(double progress); @property (nonatomic, assign) int32_t imageRequestID; - (void)recoverSubviews; @end @class AVPlayer, AVPlayerLayer; @interface TZVideoPreviewCell : TZAssetPreviewCell @property (strong, nonatomic) AVPlayer *player; @property (strong, nonatomic) AVPlayerLayer *playerLayer; @property (strong, nonatomic) UIButton *playButton; @property (strong, nonatomic) UIImage *cover; @property (nonatomic, strong) NSURL *videoURL; @property (nonatomic, strong) UIImageView *iCloudErrorIcon; @property (nonatomic, strong) UILabel *iCloudErrorLabel; @property (nonatomic, copy) void (^iCloudSyncFailedHandle)(id asset, BOOL isSyncFailed); - (void)pausePlayerAndShowNaviBar; @end @interface TZGifPreviewCell : TZAssetPreviewCell @property (strong, nonatomic) TZPhotoPreviewView *previewView; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.m ================================================ // // TZPhotoPreviewCell.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "TZPhotoPreviewCell.h" #import "TZAssetModel.h" #import "UIView+TZLayout.h" #import "TZImageManager.h" #import "TZProgressView.h" #import "TZImageCropManager.h" #import #import "TZImagePickerController.h" @implementation TZAssetPreviewCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor blackColor]; [self configSubviews]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(photoPreviewCollectionViewDidScroll) name:@"photoPreviewCollectionViewDidScroll" object:nil]; } return self; } - (void)configSubviews { } #pragma mark - Notification - (void)photoPreviewCollectionViewDidScroll { } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end @implementation TZPhotoPreviewCell - (void)configSubviews { self.previewView = [[TZPhotoPreviewView alloc] initWithFrame:CGRectZero]; __weak typeof(self) weakSelf = self; [self.previewView setSingleTapGestureBlock:^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf.singleTapGestureBlock) { strongSelf.singleTapGestureBlock(); } }]; [self.previewView setImageProgressUpdateBlock:^(double progress) { __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf.imageProgressUpdateBlock) { strongSelf.imageProgressUpdateBlock(progress); } }]; [self.contentView addSubview:self.previewView]; } - (void)setModel:(TZAssetModel *)model { [super setModel:model]; _previewView.model = model; } - (void)recoverSubviews { [_previewView recoverSubviews]; } - (void)setAllowCrop:(BOOL)allowCrop { _allowCrop = allowCrop; _previewView.allowCrop = allowCrop; } - (void)setScaleAspectFillCrop:(BOOL)scaleAspectFillCrop { _scaleAspectFillCrop = scaleAspectFillCrop; _previewView.scaleAspectFillCrop = scaleAspectFillCrop; } - (void)setCropRect:(CGRect)cropRect { _cropRect = cropRect; _previewView.cropRect = cropRect; } - (void)layoutSubviews { [super layoutSubviews]; self.previewView.frame = self.bounds; } @end @interface TZPhotoPreviewView () @property (assign, nonatomic) BOOL isRequestingGIF; @end @implementation TZPhotoPreviewView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { _scrollView = [[UIScrollView alloc] init]; _scrollView.bouncesZoom = YES; _scrollView.maximumZoomScale = 4; _scrollView.minimumZoomScale = 1.0; _scrollView.multipleTouchEnabled = YES; _scrollView.delegate = self; _scrollView.scrollsToTop = NO; _scrollView.showsHorizontalScrollIndicator = NO; _scrollView.showsVerticalScrollIndicator = YES; _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _scrollView.delaysContentTouches = NO; _scrollView.canCancelContentTouches = YES; _scrollView.alwaysBounceVertical = NO; if (@available(iOS 11, *)) { _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } [self addSubview:_scrollView]; _imageContainerView = [[UIView alloc] init]; _imageContainerView.clipsToBounds = YES; _imageContainerView.contentMode = UIViewContentModeScaleAspectFill; [_scrollView addSubview:_imageContainerView]; _imageView = [[UIImageView alloc] init]; _imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; _imageView.contentMode = UIViewContentModeScaleAspectFill; _imageView.clipsToBounds = YES; [_imageContainerView addSubview:_imageView]; _iCloudErrorIcon = [[UIImageView alloc] init]; _iCloudErrorIcon.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"]; _iCloudErrorIcon.hidden = YES; [self addSubview:_iCloudErrorIcon]; _iCloudErrorLabel = [[UILabel alloc] init]; _iCloudErrorLabel.font = [UIFont systemFontOfSize:10]; _iCloudErrorLabel.textColor = [UIColor whiteColor]; _iCloudErrorLabel.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"]; _iCloudErrorLabel.hidden = YES; [self addSubview:_iCloudErrorLabel]; UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)]; [self addGestureRecognizer:tap1]; UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; tap2.numberOfTapsRequired = 2; [tap1 requireGestureRecognizerToFail:tap2]; [self addGestureRecognizer:tap2]; [self configProgressView]; } return self; } - (void)configProgressView { _progressView = [[TZProgressView alloc] init]; _progressView.hidden = YES; [self addSubview:_progressView]; } - (void)setModel:(TZAssetModel *)model { _model = model; self.isRequestingGIF = NO; [_scrollView setZoomScale:1.0 animated:NO]; if (model.type == TZAssetModelMediaTypePhotoGif) { // 先显示缩略图 [[TZImageManager manager] getPhotoWithAsset:model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (photo) { self.imageView.image = photo; } [self resizeSubviews]; if (self.isRequestingGIF) { return; } // 再显示gif动图 self.isRequestingGIF = YES; [[TZImageManager manager] getOriginalPhotoDataWithAsset:model.asset progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { progress = progress > 0.02 ? progress : 0.02; dispatch_async(dispatch_get_main_queue(), ^{ BOOL iCloudSyncFailed = [TZCommonTools isICloudSyncError:error]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(model.asset, iCloudSyncFailed); } self.progressView.progress = progress; if (progress >= 1) { self.progressView.hidden = YES; } else { self.progressView.hidden = NO; } }); #ifdef DEBUG NSLog(@"[TZImagePickerController] getOriginalPhotoDataWithAsset:%f error:%@", progress, error); #endif } completion:^(NSData *data, NSDictionary *info, BOOL isDegraded) { if (!isDegraded) { self.isRequestingGIF = NO; self.progressView.hidden = YES; if ([TZImagePickerConfig sharedInstance].gifImagePlayBlock) { [TZImagePickerConfig sharedInstance].gifImagePlayBlock(self, self.imageView, data, info); } else { self.imageView.image = [UIImage sd_tz_animatedGIFWithData:data]; } [self resizeSubviews]; } }]; } progressHandler:nil networkAccessAllowed:NO]; } else { self.asset = model.asset; } } - (void)setAsset:(PHAsset *)asset { if (_asset && self.imageRequestID) { [[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID]; } _asset = asset; self.imageRequestID = [[TZImageManager manager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(asset, iCloudSyncFailed); } if (![asset isEqual:self->_asset]) return; if (photo) { self.imageView.image = photo; } [self resizeSubviews]; if (self.imageView.tz_height && self.allowCrop) { CGFloat scale = MAX(self.cropRect.size.width / self.imageView.tz_width, self.cropRect.size.height / self.imageView.tz_height); if (self.scaleAspectFillCrop && scale != 1) { // 如果设置图片缩放裁剪并且图片需要缩放 CGFloat multiple = self.scrollView.maximumZoomScale / self.scrollView.minimumZoomScale; self.scrollView.minimumZoomScale = scale; self.scrollView.maximumZoomScale = scale * MAX(multiple, 2); [self.scrollView setZoomScale:scale animated:YES]; } } self->_progressView.hidden = YES; if (self.imageProgressUpdateBlock) { self.imageProgressUpdateBlock(1); } if (!isDegraded) { self.imageRequestID = 0; } } progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { if (![asset isEqual:self->_asset]) return; self->_progressView.hidden = NO; [self bringSubviewToFront:self->_progressView]; progress = progress > 0.02 ? progress : 0.02; self->_progressView.progress = progress; if (self.imageProgressUpdateBlock && progress < 1) { self.imageProgressUpdateBlock(progress); } if (progress >= 1) { self->_progressView.hidden = YES; self.imageRequestID = 0; } } networkAccessAllowed:YES]; [self configMaximumZoomScale]; } - (void)recoverSubviews { [_scrollView setZoomScale:_scrollView.minimumZoomScale animated:NO]; [self resizeSubviews]; } - (void)resizeSubviews { _imageContainerView.tz_origin = CGPointZero; _imageContainerView.tz_width = self.scrollView.tz_width; UIImage *image = _imageView.image; if (image.size.height / image.size.width > self.tz_height / self.scrollView.tz_width) { CGFloat width = image.size.width / image.size.height * self.scrollView.tz_height; if (width < 1 || isnan(width)) width = self.tz_width; width = floor(width); _imageContainerView.tz_width = width; _imageContainerView.tz_height = self.tz_height; _imageContainerView.tz_centerX = self.scrollView.tz_width / 2; } else { CGFloat height = image.size.height / image.size.width * self.scrollView.tz_width; if (height < 1 || isnan(height)) height = self.tz_height; height = floor(height); _imageContainerView.tz_height = height; _imageContainerView.tz_centerY = self.tz_height / 2; } if (_imageContainerView.tz_height > self.tz_height && _imageContainerView.tz_height - self.tz_height <= 1) { _imageContainerView.tz_height = self.tz_height; } CGFloat contentSizeH = MAX(_imageContainerView.tz_height, self.tz_height); _scrollView.contentSize = CGSizeMake(self.scrollView.tz_width, contentSizeH); [_scrollView scrollRectToVisible:self.bounds animated:NO]; _scrollView.alwaysBounceVertical = _imageContainerView.tz_height <= self.tz_height ? NO : YES; _imageView.frame = _imageContainerView.bounds; [self refreshScrollViewContentSize]; } - (void)configMaximumZoomScale { _scrollView.maximumZoomScale = _allowCrop ? 6.0 : 4.0; if ([self.asset isKindOfClass:[PHAsset class]]) { PHAsset *phAsset = (PHAsset *)self.asset; CGFloat aspectRatio = phAsset.pixelWidth / (CGFloat)phAsset.pixelHeight; // 优化超宽图片的显示 if (aspectRatio > 1.5) { self.scrollView.maximumZoomScale *= aspectRatio / 1.5; } // 优化超长图片的显示 else if (aspectRatio < 0.667) { self.scrollView.maximumZoomScale *= 0.667 / aspectRatio; } } } - (void)refreshScrollViewContentSize { if (_allowCrop) { // 1.7.2 如果允许裁剪,需要让图片的任意部分都能在裁剪框内,于是对_scrollView做了如下处理: // 1.让contentSize增大(裁剪框右下角的图片部分) CGFloat contentWidthAdd = (MIN(_imageContainerView.tz_width, self.scrollView.tz_width) - _cropRect.size.width) / 2; CGFloat contentHeightAdd = (MIN(_imageContainerView.tz_height, self.scrollView.tz_height) - _cropRect.size.height) / 2; CGFloat newSizeW = MAX(self.scrollView.contentSize.width, self.scrollView.tz_width) + contentWidthAdd; CGFloat newSizeH = MAX(self.scrollView.contentSize.height, self.scrollView.tz_height) + contentHeightAdd; _scrollView.contentSize = CGSizeMake(newSizeW, newSizeH); _scrollView.alwaysBounceVertical = YES; // 2.让scrollView新增滑动区域(裁剪框左上角的图片部分) if (contentHeightAdd > 0 || contentWidthAdd > 0) { _scrollView.contentInset = UIEdgeInsetsMake(MAX(contentHeightAdd, 0), MAX(contentWidthAdd, 0), 0, 0); } else { _scrollView.contentInset = UIEdgeInsetsZero; } } } - (void)layoutSubviews { [super layoutSubviews]; _scrollView.frame = CGRectMake(10, 0, self.tz_width - 20, self.tz_height); static CGFloat progressWH = 40; CGFloat progressX = (self.tz_width - progressWH) / 2; CGFloat progressY = (self.tz_height - progressWH) / 2; _progressView.frame = CGRectMake(progressX, progressY, progressWH, progressWH); [self recoverSubviews]; _iCloudErrorIcon.frame = CGRectMake(20, [TZCommonTools tz_statusBarHeight] + 44 + 10, 28, 28); _iCloudErrorLabel.frame = CGRectMake(53, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.tz_width - 63, 28); } #pragma mark - UITapGestureRecognizer Event - (void)doubleTap:(UITapGestureRecognizer *)tap { if (_scrollView.zoomScale > _scrollView.minimumZoomScale) { _scrollView.contentInset = UIEdgeInsetsZero; [_scrollView setZoomScale:_scrollView.minimumZoomScale animated:YES]; } else { CGPoint touchPoint = [tap locationInView:self.imageView]; CGFloat newZoomScale = MIN(_scrollView.maximumZoomScale, 2.5); CGFloat xsize = self.frame.size.width / newZoomScale; CGFloat ysize = self.frame.size.height / newZoomScale; [_scrollView zoomToRect:CGRectMake(touchPoint.x - xsize/2, touchPoint.y - ysize/2, xsize, ysize) animated:YES]; } } - (void)singleTap:(UITapGestureRecognizer *)tap { if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } #pragma mark - UIScrollViewDelegate - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return _imageContainerView; } - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view { scrollView.contentInset = UIEdgeInsetsZero; } - (void)scrollViewDidZoom:(UIScrollView *)scrollView { [self refreshImageContainerViewCenter]; } - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale { [self refreshScrollViewContentSize]; } #pragma mark - Private - (void)refreshImageContainerViewCenter { CGFloat offsetX = (_scrollView.tz_width > _scrollView.contentSize.width) ? ((_scrollView.tz_width - _scrollView.contentSize.width) * 0.5) : 0.0; CGFloat offsetY = (_scrollView.tz_height > _scrollView.contentSize.height) ? ((_scrollView.tz_height - _scrollView.contentSize.height) * 0.5) : 0.0; self.imageContainerView.center = CGPointMake(_scrollView.contentSize.width * 0.5 + offsetX, _scrollView.contentSize.height * 0.5 + offsetY); } @end @implementation TZVideoPreviewCell - (void)configSubviews { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil]; _iCloudErrorIcon = [[UIImageView alloc] init]; _iCloudErrorIcon.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"]; _iCloudErrorIcon.hidden = YES; _iCloudErrorLabel = [[UILabel alloc] init]; _iCloudErrorLabel.font = [UIFont systemFontOfSize:10]; _iCloudErrorLabel.textColor = [UIColor whiteColor]; _iCloudErrorLabel.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"]; _iCloudErrorLabel.hidden = YES; } - (void)configPlayButton { if (_playButton) { [_playButton removeFromSuperview]; } _playButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted]; [_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside]; _playButton.frame = CGRectMake(0, 64, self.tz_width, self.tz_height - 64 - 44); [self.contentView addSubview:_playButton]; [self.contentView addSubview:_iCloudErrorIcon]; [self.contentView addSubview:_iCloudErrorLabel]; } - (void)setModel:(TZAssetModel *)model { [super setModel:model]; [self configMoviePlayer]; } - (void)setVideoURL:(NSURL *)videoURL { _videoURL = videoURL; [self configMoviePlayer]; } - (void)configMoviePlayer { if (_player) { [_playerLayer removeFromSuperlayer]; _playerLayer = nil; [_player pause]; _player = nil; } if (self.model && self.model.asset) { [[TZImageManager manager] getPhotoWithAsset:self.model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(self.model.asset, iCloudSyncFailed); } if (photo) { self.cover = photo; } }]; [[TZImageManager manager] getVideoWithAsset:self.model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ BOOL iCloudSyncFailed = !playerItem && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(self.model.asset, iCloudSyncFailed); } [self configPlayerWithItem:playerItem]; }); }]; } else { AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:self.videoURL]; [self configPlayerWithItem:playerItem]; } } - (void)configPlayerWithItem:(AVPlayerItem *)playerItem { self.player = [AVPlayer playerWithPlayerItem:playerItem]; self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.backgroundColor = [UIColor blackColor].CGColor; self.playerLayer.frame = self.bounds; [self.contentView.layer addSublayer:self.playerLayer]; [self configPlayButton]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; } - (void)layoutSubviews { [super layoutSubviews]; _playerLayer.frame = self.bounds; _playButton.frame = CGRectMake(0, 64, self.tz_width, self.tz_height - 64 - 44); _iCloudErrorIcon.frame = CGRectMake(20, [TZCommonTools tz_statusBarHeight] + 44 + 10, 28, 28); _iCloudErrorLabel.frame = CGRectMake(53, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.tz_width - 63, 28); } - (void)photoPreviewCollectionViewDidScroll { if (_player && _player.rate != 0.0) { [self pausePlayerAndShowNaviBar]; } } #pragma mark - Notification - (void)appWillResignActiveNotification { if (_player && _player.rate != 0.0) { [self pausePlayerAndShowNaviBar]; } } #pragma mark - Click Event - (void)playButtonClick { CMTime currentTime = _player.currentItem.currentTime; CMTime durationTime = _player.currentItem.duration; if (_player.rate == 0.0f) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_VIDEO_PLAY_NOTIFICATION" object:_player]; if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)]; [_player play]; [_playButton setImage:nil forState:UIControlStateNormal]; [UIApplication sharedApplication].statusBarHidden = YES; if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } else { [self pausePlayerAndShowNaviBar]; } } - (void)pausePlayerAndShowNaviBar { [_player pause]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } @end @implementation TZGifPreviewCell - (void)configSubviews { [self configPreviewView]; } - (void)configPreviewView { _previewView = [[TZPhotoPreviewView alloc] initWithFrame:CGRectZero]; __weak typeof(self) weakSelf = self; [_previewView setSingleTapGestureBlock:^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf signleTapAction]; }]; [self.contentView addSubview:_previewView]; } - (void)setModel:(TZAssetModel *)model { [super setModel:model]; _previewView.model = self.model; } - (void)layoutSubviews { [super layoutSubviews]; _previewView.frame = self.bounds; } #pragma mark - Click Event - (void)signleTapAction { if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.h ================================================ // // TZPhotoPreviewController.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @interface TZPhotoPreviewController : UIViewController @property (nonatomic, strong) NSMutableArray *models; ///< All photo models / 所有图片模型数组 @property (nonatomic, strong) NSMutableArray *photos; ///< All photos / 所有图片数组 @property (nonatomic, assign) NSInteger currentIndex; ///< Index of the photo user click / 用户点击的图片的索引 @property (nonatomic, assign) BOOL isSelectOriginalPhoto; ///< If YES,return original photo / 是否返回原图 @property (nonatomic, assign) BOOL isCropImage; /// Return the new selected photos / 返回最新的选中图片数组 @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 *photos,NSArray *assets,BOOL isSelectOriginalPhoto); @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.m ================================================ // // TZPhotoPreviewController.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "TZPhotoPreviewController.h" #import "TZPhotoPreviewCell.h" #import "TZAssetModel.h" #import "UIView+TZLayout.h" #import "TZImagePickerController.h" #import "TZImageManager.h" #import "TZImageCropManager.h" @interface TZPhotoPreviewController () { UICollectionView *_collectionView; UICollectionViewFlowLayout *_layout; NSArray *_photosTemp; NSArray *_assetsTemp; UIView *_naviBar; UIButton *_backButton; UIButton *_selectButton; UILabel *_indexLabel; UIView *_toolBar; UIButton *_doneButton; UIImageView *_numberImageView; UILabel *_numberLabel; UIButton *_originalPhotoButton; UILabel *_originalPhotoLabel; CGFloat _offsetItemCount; BOOL _didSetIsSelectOriginalPhoto; } @property (nonatomic, assign) BOOL isHideNaviBar; @property (nonatomic, strong) UIView *cropBgView; @property (nonatomic, strong) UIView *cropView; @property (nonatomic, assign) double progress; @property (strong, nonatomic) UIAlertController *alertView; @property (nonatomic, strong) UIView *iCloudErrorView; @end @implementation TZPhotoPreviewController - (void)viewDidLoad { [super viewDidLoad]; [TZImageManager manager].shouldFixOrientation = YES; 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]; } - (void)setIsSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto { _isSelectOriginalPhoto = isSelectOriginalPhoto; _didSetIsSelectOriginalPhoto = YES; } - (void)setPhotos:(NSMutableArray *)photos { _photos = photos; _photosTemp = [NSArray arrayWithArray:photos]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.navigationController setNavigationBarHidden:YES animated:YES]; [UIApplication sharedApplication].statusBarHidden = YES; [_collectionView setContentOffset:CGPointMake((self.view.tz_width + 20) * self.currentIndex, 0) animated:NO]; [self refreshNaviBarAndBottomBarState]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc.needShowStatusBar) { [UIApplication sharedApplication].statusBarHidden = NO; } [self.navigationController setNavigationBarHidden:NO animated:YES]; [TZImageManager manager].shouldFixOrientation = NO; } - (BOOL)prefersStatusBarHidden { return YES; } - (void)configCustomNaviBar { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; _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]; _selectButton.hidden = !tzImagePickerVc.showSelectBtn; _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]; } - (void)configBottomToolBar { _toolBar = [[UIView alloc] initWithFrame:CGRectZero]; static CGFloat rgb = 34 / 255.0; _toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7]; TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (_tzImagePickerVc.allowPickingOriginalPhoto) { _originalPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; _originalPhotoButton.imageEdgeInsets = UIEdgeInsetsMake(0, [TZCommonTools tz_isRightToLeftLayout] ? 10 : -10, 0, 0); _originalPhotoButton.backgroundColor = [UIColor clearColor]; [_originalPhotoButton addTarget:self action:@selector(originalPhotoButtonClick) forControlEvents:UIControlEventTouchUpInside]; _originalPhotoButton.titleLabel.font = [UIFont systemFontOfSize:13]; [_originalPhotoButton setTitle:_tzImagePickerVc.fullImageBtnTitleStr forState:UIControlStateNormal]; [_originalPhotoButton setTitle:_tzImagePickerVc.fullImageBtnTitleStr forState:UIControlStateSelected]; [_originalPhotoButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal]; [_originalPhotoButton setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected]; [_originalPhotoButton setImage:_tzImagePickerVc.photoPreviewOriginDefImage forState:UIControlStateNormal]; [_originalPhotoButton setImage:_tzImagePickerVc.photoOriginSelImage forState:UIControlStateSelected]; _originalPhotoLabel = [[UILabel alloc] init]; _originalPhotoLabel.textAlignment = NSTextAlignmentNatural; _originalPhotoLabel.font = [UIFont systemFontOfSize:13]; _originalPhotoLabel.textColor = [UIColor whiteColor]; _originalPhotoLabel.backgroundColor = [UIColor clearColor]; if (_isSelectOriginalPhoto) [self showPhotoBytes]; } _doneButton = [UIButton buttonWithType:UIButtonTypeCustom]; _doneButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside]; [_doneButton setTitle:_tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal]; [_doneButton setTitleColor:_tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal]; _numberImageView = [[UIImageView alloc] initWithImage:_tzImagePickerVc.photoNumberIconImage]; _numberImageView.backgroundColor = [UIColor clearColor]; _numberImageView.clipsToBounds = YES; _numberImageView.contentMode = UIViewContentModeScaleAspectFit; _numberImageView.hidden = _tzImagePickerVc.selectedModels.count <= 0; _numberLabel = [[UILabel alloc] init]; _numberLabel.font = [UIFont systemFontOfSize:15]; _numberLabel.adjustsFontSizeToFitWidth = YES; _numberLabel.textColor = [UIColor whiteColor]; _numberLabel.textAlignment = NSTextAlignmentCenter; _numberLabel.text = [NSString stringWithFormat:@"%zd",_tzImagePickerVc.selectedModels.count]; _numberLabel.hidden = _tzImagePickerVc.selectedModels.count <= 0; _numberLabel.backgroundColor = [UIColor clearColor]; _numberLabel.userInteractionEnabled = YES; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonClick)]; [_numberLabel addGestureRecognizer:tapGesture]; [_originalPhotoButton addSubview:_originalPhotoLabel]; [_toolBar addSubview:_doneButton]; [_toolBar addSubview:_originalPhotoButton]; [_toolBar addSubview:_numberImageView]; [_toolBar addSubview:_numberLabel]; [self.view addSubview:_toolBar]; if (_tzImagePickerVc.photoPreviewPageUIConfigBlock) { _tzImagePickerVc.photoPreviewPageUIConfigBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel); } } - (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"]; TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (_tzImagePickerVc.scaleAspectFillCrop && _tzImagePickerVc.allowCrop) { _collectionView.scrollEnabled = NO; } } - (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]; } } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height; BOOL isRTL = [TZCommonTools tz_isRightToLeftLayout]; 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); if (isRTL) { _backButton.frame = CGRectMake(self.view.tz_width - 54, 10 + statusBarHeightInterval, 44, 44); _backButton.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0); _selectButton.frame = CGRectMake(12, 10 + statusBarHeightInterval, 44, 44); } else { _backButton.frame = CGRectMake(10, 10 + statusBarHeightInterval, 44, 44); _selectButton.frame = CGRectMake(self.view.tz_width - 56, 10 + statusBarHeightInterval, 44, 44); } _indexLabel.frame = _selectButton.frame; _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]; if (_offsetItemCount > 0) { CGFloat offsetX = _offsetItemCount * _layout.itemSize.width; [_collectionView setContentOffset:CGPointMake(offsetX, 0)]; } if (_tzImagePickerVc.allowCrop) { [_collectionView reloadData]; } CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom; CGFloat toolBarTop = self.view.tz_height - toolBarHeight; _toolBar.frame = CGRectMake(0, toolBarTop, self.view.tz_width, toolBarHeight); if (_tzImagePickerVc.allowPickingOriginalPhoto) { CGFloat fullImageWidth = [_tzImagePickerVc.fullImageBtnTitleStr boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:13]} context:nil].size.width; if (isRTL) { _originalPhotoButton.frame = CGRectMake(self.view.tz_width - (fullImageWidth + 56), 0, fullImageWidth + 56, 44); _originalPhotoLabel.frame = CGRectMake(-80 + 15, 0, 80, 44); } else { _originalPhotoButton.frame = CGRectMake(0, 0, fullImageWidth + 56, 44); _originalPhotoLabel.frame = CGRectMake(fullImageWidth + 42, 0, 80, 44); } } [_doneButton sizeToFit]; if (isRTL) { _doneButton.frame = CGRectMake(12, 0, MAX(44, _doneButton.tz_width), 44); _numberImageView.frame = CGRectMake(_doneButton.tz_right + 5, 10, 24, 24); } else { _doneButton.frame = CGRectMake(self.view.tz_width - _doneButton.tz_width - 12, 0, MAX(44, _doneButton.tz_width), 44); _numberImageView.frame = CGRectMake(_doneButton.tz_left - 24 - 5, 10, 24, 24); } _numberLabel.frame = _numberImageView.frame; [self configCropView]; if (_tzImagePickerVc.photoPreviewPageDidLayoutSubviewsBlock) { _tzImagePickerVc.photoPreviewPageDidLayoutSubviewsBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel); } } #pragma mark - Notification - (void)didChangeStatusBarOrientationNotification:(NSNotification *)noti { _offsetItemCount = _collectionView.contentOffset.x / _layout.itemSize.width; } #pragma mark - Click Event - (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) { // 1. select:check if over the maxImagesCount / 选择照片,检查是否超过了最大个数的限制 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; // 2. if not over the maxImagesCount / 如果没有超过最大个数限制 } 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]) { // 1.6.7版本更新:防止有多个一样的model,一次性被移除了 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]; // [_tzImagePickerVc.selectedModels removeObjectAtIndex:i]; break; } } if (self.photos) { // 1.6.7版本更新:防止有多个一样的asset,一次性被移除了 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; } } // [_tzImagePickerVc.selectedAssets removeObject:_assetsTemp[self.currentIndex]]; [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]; } - (void)backButtonClick { if (self.navigationController.childViewControllers.count < 2) { [self.navigationController dismissViewControllerAnimated:YES completion:nil]; if ([self.navigationController isKindOfClass: [TZImagePickerController class]]) { TZImagePickerController *nav = (TZImagePickerController *)self.navigationController; if (nav.imagePickerControllerDidCancelHandle) { nav.imagePickerControllerDidCancelHandle(); } } return; } [self.navigationController popViewControllerAnimated:YES]; if (self.backButtonClickBlock) { self.backButtonClickBlock(_isSelectOriginalPhoto); } } - (void)doneButtonClick { TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; // 如果图片正在从iCloud同步中,提醒用户 if (_progress > 0 && _progress < 1 && (_selectButton.isSelected || !_tzImagePickerVc.selectedModels.count )) { _alertView = [_tzImagePickerVc showAlertWithTitle:[NSBundle tz_localizedStringForKey:@"Synchronizing photos from iCloud"]]; return; } // 如果没有选中过照片 点击确定时选中当前预览的照片 if (_tzImagePickerVc.selectedModels.count == 0 && _tzImagePickerVc.minImagesCount <= 0 && _tzImagePickerVc.autoSelectCurrentWhenDone) { TZAssetModel *model = _models[self.currentIndex]; if ([[TZImageManager manager] isAssetCannotBeSelected:model.asset]) { return; } [self select:_selectButton refreshCount:NO]; } NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.currentIndex inSection:0]; TZPhotoPreviewCell *cell = (TZPhotoPreviewCell *)[_collectionView cellForItemAtIndexPath:indexPath]; 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]; if (self.doneButtonClickBlockCropMode) { TZAssetModel *model = _models[self.currentIndex]; self.doneButtonClickBlockCropMode(cropedImage,model.asset); } } else if (self.doneButtonClickBlock) { // 非裁剪状态 self.doneButtonClickBlock(_isSelectOriginalPhoto); } if (self.doneButtonClickBlockWithPreviewType) { self.doneButtonClickBlockWithPreviewType(self.photos,_tzImagePickerVc.selectedAssets,self.isSelectOriginalPhoto); } } - (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) { // 如果当前已选择照片张数 < 最大可选张数 && 最大可选张数大于1,就选中该张图 TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (_tzImagePickerVc.selectedModels.count < _tzImagePickerVc.maxImagesCount && _tzImagePickerVc.showSelectBtn) { [self select:_selectButton]; } } } } - (void)didTapPreviewCell { self.isHideNaviBar = !self.isHideNaviBar; _naviBar.hidden = self.isHideNaviBar; _toolBar.hidden = self.isHideNaviBar; } #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]; } #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) { cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZGifPreviewCell" forIndexPath:indexPath]; TZGifPreviewCell *currentCell = (TZGifPreviewCell *)cell; currentCell.previewView.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) { model.iCloudFailed = isSyncFailed; [weakSelf didICloudSyncStatusChanged:model]; }; } else { 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]; }]; } } }]; 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; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if ([cell isKindOfClass:[TZPhotoPreviewCell class]]) { [(TZPhotoPreviewCell *)cell recoverSubviews]; } } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if ([cell isKindOfClass:[TZPhotoPreviewCell class]]) { [(TZPhotoPreviewCell *)cell recoverSubviews]; } else if ([cell isKindOfClass:[TZVideoPreviewCell class]]) { TZVideoPreviewCell *videoCell = (TZVideoPreviewCell *)cell; if (videoCell.player && videoCell.player.rate != 0.0) { [videoCell pausePlayerAndShowNaviBar]; } } } #pragma mark - Private Method - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; // NSLog(@"%@ dealloc",NSStringFromClass(self.class)); } - (void)refreshNaviBarAndBottomBarState { TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; TZAssetModel *model = _models[self.currentIndex]; _selectButton.selected = model.isSelected; [self refreshSelectButtonImageViewContentMode]; if (_selectButton.isSelected && _tzImagePickerVc.showSelectedIndex && _tzImagePickerVc.showSelectBtn) { NSString *index = [NSString stringWithFormat:@"%d", (int)([_tzImagePickerVc.selectedAssetIds indexOfObject:model.asset.localIdentifier] + 1)]; _indexLabel.text = index; _indexLabel.hidden = NO; } else { _indexLabel.hidden = YES; } _numberLabel.text = [NSString stringWithFormat:@"%zd",_tzImagePickerVc.selectedModels.count]; _numberImageView.hidden = (_tzImagePickerVc.selectedModels.count <= 0 || _isHideNaviBar || _isCropImage); _numberLabel.hidden = (_tzImagePickerVc.selectedModels.count <= 0 || _isHideNaviBar || _isCropImage); _originalPhotoButton.selected = _isSelectOriginalPhoto; _originalPhotoLabel.hidden = !_originalPhotoButton.isSelected; if (_isSelectOriginalPhoto) [self showPhotoBytes]; // If is previewing video, hide original photo button // 如果正在预览的是视频,隐藏原图按钮 if (!_isHideNaviBar) { if (model.type == TZAssetModelMediaTypeVideo) { _originalPhotoButton.hidden = YES; _originalPhotoLabel.hidden = YES; } else { _originalPhotoButton.hidden = NO; if (_isSelectOriginalPhoto) _originalPhotoLabel.hidden = NO; } } _doneButton.hidden = NO; _selectButton.hidden = !_tzImagePickerVc.showSelectBtn; // 让宽度/高度小于 最小可选照片尺寸 的图片不能选中 if (![[TZImageManager manager] isPhotoSelectableWithAsset:model.asset]) { _numberLabel.hidden = YES; _numberImageView.hidden = YES; _selectButton.hidden = YES; _originalPhotoButton.hidden = YES; _originalPhotoLabel.hidden = YES; _doneButton.hidden = YES; } // iCloud同步失败的UI刷新 [self didICloudSyncStatusChanged:model]; if (_tzImagePickerVc.photoPreviewPageDidRefreshStateBlock) { _tzImagePickerVc.photoPreviewPageDidRefreshStateBlock(_collectionView, _naviBar, _backButton, _selectButton, _indexLabel, _toolBar, _originalPhotoButton, _originalPhotoLabel, _doneButton, _numberImageView, _numberLabel); } } - (void)refreshSelectButtonImageViewContentMode { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (self->_selectButton.imageView.image.size.width <= 27) { self->_selectButton.imageView.contentMode = UIViewContentModeCenter; } else { self->_selectButton.imageView.contentMode = UIViewContentModeScaleAspectFit; } }); } - (void)didICloudSyncStatusChanged:(TZAssetModel *)model{ dispatch_async(dispatch_get_main_queue(), ^{ TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; // onlyReturnAsset为NO时,依赖TZ返回大图,所以需要有iCloud同步失败的提示,并且不能选择, if (_tzImagePickerVc.onlyReturnAsset) { return; } TZAssetModel *currentModel = self.models[self.currentIndex]; if (_tzImagePickerVc.selectedModels.count <= 0) { self->_doneButton.enabled = !currentModel.iCloudFailed; } else { self->_doneButton.enabled = YES; } self->_selectButton.hidden = currentModel.iCloudFailed || !_tzImagePickerVc.showSelectBtn; if (currentModel.iCloudFailed) { self->_originalPhotoButton.hidden = YES; self->_originalPhotoLabel.hidden = YES; } }); } - (void)showPhotoBytes { [[TZImageManager manager] getPhotosBytesWithArray:@[_models[self.currentIndex]] completion:^(NSString *totalBytes) { self->_originalPhotoLabel.text = [NSString stringWithFormat:@"(%@)",totalBytes]; }]; } // TZRTLLayout已经自动适配,无需手动换算 //- (NSInteger)currentIndex { // return [TZCommonTools tz_isRightToLeftLayout] ? self.models.count - _currentIndex - 1 : _currentIndex; //} /// 选中/取消选中某张照片 - (void)setAsset:(PHAsset *)asset isSelect:(BOOL)isSelect { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (isSelect && [tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didSelectAsset:photo:isSelectOriginalPhoto:)]) { [self callDelegate:asset isSelect:YES]; } if (!isSelect && [tzImagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didDeselectAsset:photo:isSelectOriginalPhoto:)]) { [self callDelegate:asset isSelect:NO]; } } /// 调用选中/取消选中某张照片的代理方法 - (void)callDelegate:(PHAsset *)asset isSelect:(BOOL)isSelect { TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; __weak typeof(self) weakSelf = self; __weak typeof(tzImagePickerVc) weakImagePickerVc= tzImagePickerVc; [[TZImageManager manager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (isDegraded) return; __strong typeof(weakSelf) strongSelf = weakSelf; __strong typeof(weakImagePickerVc) strongImagePickerVc = weakImagePickerVc; if (isSelect) { [strongImagePickerVc.pickerDelegate imagePickerController:strongImagePickerVc didSelectAsset:asset photo:photo isSelectOriginalPhoto:strongSelf.isSelectOriginalPhoto]; } else { [strongImagePickerVc.pickerDelegate imagePickerController:strongImagePickerVc didDeselectAsset:asset photo:photo isSelectOriginalPhoto:strongSelf.isSelectOriginalPhoto]; } }]; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZProgressView.h ================================================ // // TZProgressView.h // TZImagePickerController // // Created by ttouch on 2016/12/6. // Copyright © 2016年 谭真. All rights reserved. // #import @interface TZProgressView : UIView @property (nonatomic, assign) double progress; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZProgressView.m ================================================ // // TZProgressView.m // TZImagePickerController // // Created by ttouch on 2016/12/6. // Copyright © 2016年 谭真. All rights reserved. // #import "TZProgressView.h" @interface TZProgressView () @property (nonatomic, strong) CAShapeLayer *progressLayer; @end @implementation TZProgressView - (instancetype)init { self = [super init]; if (self) { self.backgroundColor = [UIColor clearColor]; _progressLayer = [CAShapeLayer layer]; _progressLayer.fillColor = [[UIColor clearColor] CGColor]; _progressLayer.strokeColor = [[UIColor whiteColor] CGColor]; _progressLayer.opacity = 1; _progressLayer.lineCap = kCALineCapRound; _progressLayer.lineWidth = 5; [_progressLayer setShadowColor:[UIColor blackColor].CGColor]; [_progressLayer setShadowOffset:CGSizeMake(1, 1)]; [_progressLayer setShadowOpacity:0.5]; [_progressLayer setShadowRadius:2]; } return self; } - (void)drawRect:(CGRect)rect { CGPoint center = CGPointMake(rect.size.width / 2, rect.size.height / 2); CGFloat radius = rect.size.width / 2; CGFloat startA = - M_PI_2; CGFloat endA = - M_PI_2 + M_PI * 2 * _progress; _progressLayer.frame = self.bounds; UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES]; _progressLayer.path =[path CGPath]; [_progressLayer removeFromSuperlayer]; [self.layer addSublayer:_progressLayer]; } - (void)setProgress:(double)progress { _progress = progress; [self setNeedsDisplay]; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZVideoCropController.h ================================================ // // TZVideoCropController.h // TZImagePickerController // // Created by 肖兰月 on 2021/5/27. // Copyright © 2021 谭真. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class TZAssetModel,TZImagePickerController; @interface TZVideoCropController : UIViewController @property (nonatomic, strong) TZAssetModel *model; @property (nonatomic, weak) TZImagePickerController *imagePickerVc; @end @protocol TZVideoEditViewDelegate - (void)editViewCropRectBeginChange; - (void)editViewCropRectEndChange; @end @interface TZVideoEditView : UIView @property (strong, nonatomic) UIImageView *beginImgView; @property (strong, nonatomic) UIImageView *endImgView; @property (strong, nonatomic) UIView *indicatorLine; @property (assign, nonatomic) CGFloat videoDuration; @property (assign, nonatomic) NSInteger maxCropVideoDuration; @property (assign, nonatomic) CGRect cropRect; @property (assign, nonatomic) CGFloat allImgWidth; @property (assign, nonatomic) CGFloat minCropRectWidth; @property (nonatomic, weak) id delegate; - (void)resetIndicatorLine; - (void)indicatorLineAnimateWithDuration:(NSTimeInterval)duration cropRect:(CGRect)cropRect; @end @interface TZVideoPictureCell : UICollectionViewCell @property (strong, nonatomic) UIImageView *imgView; @end NS_ASSUME_NONNULL_END ================================================ FILE: TZImagePickerController/TZImagePickerController/TZVideoCropController.m ================================================ // // TZVideoCropController.m // TZImagePickerController // // Created by 肖兰月 on 2021/5/27. // Copyright © 2021 谭真. All rights reserved. // #import "TZVideoCropController.h" #import #import "UIView+TZLayout.h" #import "TZImageManager.h" #import "TZAssetModel.h" #import "TZImagePickerController.h" @interface TZVideoCropController () { AVPlayer *_player; AVPlayerLayer *_playerLayer; UIButton *_playButton; UIImage *_cover; NSString *_outputPath; NSString *_errorMsg; UIButton *_cancelButton; UIButton *_doneButton; UIProgressView *_progress; UILabel *_cropVideoDurationLabel; AVAssetImageGenerator *_imageGenerator; AVAsset *_asset; CGFloat _collectionViewBeginOffsetX; BOOL _isPlayed; CGFloat _itemW; BOOL _isDraging; UIStatusBarStyle _originStatusBarStyle; } // iCloud无法同步提示UI @property (nonatomic, strong) UIView *iCloudErrorView; @property (strong, nonatomic) UICollectionView *collectionView; @property (strong, nonatomic) TZVideoEditView *videoEditView; @property (strong, nonatomic) NSMutableArray *videoImgArray; @property (strong, nonatomic) NSArray *imageTimes; @property (strong, nonatomic) NSTimer *timer; @end @implementation TZVideoCropController #define VideoEditLeftMargin 40 #define PanImageWidth 10 #define MinCropVideoDuration 1 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blackColor]; [self configMoviePlayer]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayer) name:UIApplicationWillResignActiveNotification object:nil]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self stopTimer]; } - (void)configMoviePlayer { [[TZImageManager manager] getPhotoWithAsset:_model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorView.hidden = !iCloudSyncFailed; self->_doneButton.enabled = !iCloudSyncFailed; }]; [[TZImageManager manager] getVideoWithAsset:_model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ self->_asset = playerItem.asset; self->_player = [AVPlayer playerWithPlayerItem:playerItem]; self->_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self->_player]; self->_playerLayer.frame = self.view.bounds; [self.view.layer addSublayer:self->_playerLayer]; [self configPlayButton]; [self configBottomToolBar]; if (self.imagePickerVc.allowEditVideo) { [self configVideoImageCollectionView]; [self configVideoEditView]; [self generateVideoImage]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayer) name:AVPlayerItemDidPlayToEndTimeNotification object:self->_player.currentItem]; }); }]; } - (void)configPlayButton { _playButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted]; [_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_playButton]; } - (void)configBottomToolBar { _cropVideoDurationLabel = UILabel.new; _cropVideoDurationLabel.textAlignment = NSTextAlignmentCenter; _cropVideoDurationLabel.textColor = UIColor.whiteColor; _cropVideoDurationLabel.font = [UIFont systemFontOfSize:12]; [self.view addSubview:_cropVideoDurationLabel]; _cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; _cancelButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_cancelButton setTitle:[NSBundle tz_localizedStringForKey:@"Cancel"] forState:0]; [_cancelButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; [_cancelButton addTarget:self action:@selector(cancelButtonClick) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_cancelButton]; _doneButton = [UIButton buttonWithType:UIButtonTypeCustom]; _doneButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside]; [_doneButton setTitle:self.imagePickerVc.doneBtnTitleStr forState:UIControlStateNormal]; [_doneButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; [_doneButton setTitleColor:self.imagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled]; [self.view addSubview:_doneButton]; if (self.imagePickerVc.videoEditViewPageUIConfigBlock) { self.imagePickerVc.videoEditViewPageUIConfigBlock(_playButton, _cropVideoDurationLabel, _cancelButton, _doneButton); } } - (void)configVideoImageCollectionView { _itemW = (self.view.tz_width - VideoEditLeftMargin * 2 - 2 * PanImageWidth) / 10.0; UICollectionViewFlowLayout *layout = UICollectionViewFlowLayout.new; layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; layout.itemSize = CGSizeMake(_itemW, _itemW * 2); layout.minimumLineSpacing = 0; layout.minimumInteritemSpacing = 0; layout.sectionInset = UIEdgeInsetsMake(0, VideoEditLeftMargin + PanImageWidth, 0, VideoEditLeftMargin + PanImageWidth); _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.clipsToBounds = NO; _collectionView.showsHorizontalScrollIndicator = NO; _collectionView.alwaysBounceHorizontal = YES; [_collectionView registerClass:TZVideoPictureCell.class forCellWithReuseIdentifier:@"TZVideoPictureCell"]; [self.view addSubview:_collectionView]; } - (void)configVideoEditView { _videoEditView = TZVideoEditView.new; _videoEditView.backgroundColor = UIColor.clearColor; _videoEditView.delegate = self; _videoEditView.maxCropVideoDuration = self.imagePickerVc.maxCropVideoDuration; [self.view addSubview:_videoEditView]; } - (UIStatusBarStyle)preferredStatusBarStyle { if (self.imagePickerVc && [self.imagePickerVc isKindOfClass:[TZImagePickerController class]]) { return self.imagePickerVc.statusBarStyle; } return [super preferredStatusBarStyle]; } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height; CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0; CGFloat statusBarAndNaviBarHeight = statusBarHeight + self.navigationController.navigationBar.tz_height; CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom; CGFloat doneButtonWidth = [_doneButton.currentTitle boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil].size.width; doneButtonWidth = MAX(44, doneButtonWidth); [_cancelButton sizeToFit]; if ([TZCommonTools tz_isRightToLeftLayout]) { _doneButton.frame = CGRectMake(12, self.view.tz_height - toolBarHeight, doneButtonWidth, 44); _cancelButton.frame = CGRectMake(self.view.tz_width - _cancelButton.tz_width - 12, self.view.tz_height - toolBarHeight, _cancelButton.tz_width, 44); } else { _doneButton.frame = CGRectMake(self.view.tz_width - doneButtonWidth - 12, self.view.tz_height - toolBarHeight, doneButtonWidth, 44); _cancelButton.frame = CGRectMake(12, self.view.tz_height - toolBarHeight, _cancelButton.tz_width, 44); } _playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, self.view.tz_height - statusBarAndNaviBarHeight - toolBarHeight); CGFloat collectionViewH = (self.view.tz_width - VideoEditLeftMargin * 2 - 2 * PanImageWidth) / 10.0 * 2; _collectionView.frame = CGRectMake(0, self.view.tz_height - collectionViewH - toolBarHeight - statusBarHeight, self.view.tz_width, collectionViewH); _videoEditView.frame = _collectionView.frame; _cropVideoDurationLabel.frame = CGRectMake(0, _videoEditView.tz_bottom, self.view.tz_width, 20); CGFloat playerLayerHeight = CGRectGetMinY(_collectionView.frame) - statusBarHeight * 2; CGFloat playerLayerWidth = self.view.tz_width/self.view.tz_height * playerLayerHeight; CGFloat playerLayerLeft = (self.view.tz_width - playerLayerWidth) / 2.0; CGRect playerLayerFrame = CGRectMake(playerLayerLeft, statusBarHeight, playerLayerWidth, playerLayerHeight); _playerLayer.frame = playerLayerFrame; _playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, playerLayerHeight - statusBarAndNaviBarHeight); if (self.imagePickerVc.videoEditViewPageDidLayoutSubviewsBlock) { self.imagePickerVc.videoEditViewPageDidLayoutSubviewsBlock(_playButton, _cropVideoDurationLabel, _cancelButton, _doneButton); } } - (void)generateVideoImage { _imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset]; _imageGenerator.appliesPreferredTrackTransform = YES; _imageGenerator.requestedTimeToleranceBefore = kCMTimeZero; _imageGenerator.requestedTimeToleranceAfter = kCMTimeZero; _imageGenerator.maximumSize = CGSizeMake(100, 100); NSTimeInterval durationSeconds = self.model.asset.duration; self.videoEditView.videoDuration = durationSeconds; NSUInteger imageCount = 10; CGFloat maxCropWidth = self.view.tz_width - (VideoEditLeftMargin + PanImageWidth) * 2; if (durationSeconds <= MinCropVideoDuration) return; if (durationSeconds <= self.imagePickerVc.maxCropVideoDuration) { imageCount = 10; self.videoEditView.allImgWidth = maxCropWidth; _cropVideoDurationLabel.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Selected for %ld seconds"], (NSInteger)durationSeconds]; } else { CGFloat singleWidthSecond = maxCropWidth / self.imagePickerVc.maxCropVideoDuration; CGFloat allImgWidth = singleWidthSecond * durationSeconds; self.videoEditView.allImgWidth = allImgWidth; imageCount = allImgWidth / _itemW; _cropVideoDurationLabel.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Selected for %ld seconds"],(long)self.imagePickerVc.maxCropVideoDuration]; } NSArray *assetTracks = [_asset tracksWithMediaType:AVMediaTypeVideo]; if (!assetTracks.count) { self.iCloudErrorView.hidden = NO; _doneButton.enabled = NO; _cropVideoDurationLabel.hidden = YES; return; }; Float64 frameRate = [[_asset tracksWithMediaType:AVMediaTypeVideo][0] nominalFrameRate];; NSMutableArray *times = NSMutableArray.array; NSTimeInterval intervalSecond = durationSeconds/imageCount; CMTime timeFrame; for (NSInteger i = 0; i < imageCount; i++) { timeFrame = CMTimeMake(intervalSecond * i *frameRate, frameRate); NSValue *timeValue = [NSValue valueWithCMTime:timeFrame]; [times addObject:timeValue]; } self.videoImgArray = NSMutableArray.new; self.imageTimes = times; typeof(self) weakSelf = self; [_imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) { if (image) { UIImage *img = [[UIImage alloc] initWithCGImage:image]; [weakSelf.videoImgArray addObject:img]; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.collectionView reloadData]; }); } }]; } #pragma mark - UICollectiobViewDataSource & UIcollectionViewDelegate - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.videoImgArray.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { TZVideoPictureCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZVideoPictureCell" forIndexPath:indexPath]; // 在RTL环境下,普通的UICollectionViewFlowLayout的cell是从右到左排列的,所以这里【需要】反向取值 if ([TZCommonTools tz_isRightToLeftLayout]) { cell.imgView.image = self.videoImgArray[self.videoImgArray.count - 1 - indexPath.item]; } else { cell.imgView.image = self.videoImgArray[indexPath.item]; } return cell; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (!_isDraging) return; // 在RTL环境下,普通的UICollectionViewFlowLayout的cell虽然是从右到左排列的,但是contentOffset依旧是从左到右的,所以这里【不需要】反向取值 CGFloat offsetX = scrollView.contentOffset.x; if (offsetX - _collectionViewBeginOffsetX >= self.view.tz_width) { [self.collectionView setContentOffset:CGPointMake(self.view.tz_width + _collectionViewBeginOffsetX, 0) animated:NO]; } else if (_collectionViewBeginOffsetX - offsetX >= self.view.tz_width) { [self.collectionView setContentOffset:CGPointMake(_collectionViewBeginOffsetX - self.view.tz_width, 0) animated:NO]; } [self editViewCropRectBeginChange]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { _isDraging = YES; _collectionViewBeginOffsetX = scrollView.contentOffset.x; } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { _isDraging = NO; [self editViewCropRectEndChange]; } #pragma mark - TZVideoEditViewDelegate - (void)editViewCropRectBeginChange { [self stopTimer]; [_playerLayer.player seekToTime:[self getCropStartTime] toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; NSTimeInterval second = [self getCropVideoDuration]; _cropVideoDurationLabel.text = [NSString stringWithFormat:[NSBundle tz_localizedStringForKey:@"Selected for %ld seconds"], (NSInteger)second]; } - (void)editViewCropRectEndChange { if (_isPlayed) { [self starTimer]; } } #pragma mark - Click Event - (void)playButtonClick { CMTime currentTime = _player.currentItem.currentTime; CMTime durationTime = _player.currentItem.duration; if (_player.rate == 0.0f) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_VIDEO_PLAY_NOTIFICATION" object:_player]; if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)]; _isPlayed = YES; [self starTimer]; [_playButton setImage:nil forState:UIControlStateNormal]; } else { _isPlayed = NO; [self stopTimer]; [self pausePlayer]; } } - (void)cancelButtonClick { [self dismissViewControllerAnimated:YES completion:nil]; } - (void)doneButtonClick { if ([[TZImageManager manager] isAssetCannotBeSelected:_model.asset]) { return; } [self stopTimer]; TZImagePickerController *imagePickerVc = self.imagePickerVc; [imagePickerVc showProgressHUD]; [[TZImageManager manager] getVideoOutputPathWithAsset:_model.asset presetName:imagePickerVc.presetName timeRange:[self getCropVideoTimeRange] success:^(NSString *outputPath) { [imagePickerVc hideProgressHUD]; self->_outputPath = outputPath; [self dismissAndCallDelegateMethod]; } failure:^(NSString *errorMessage, NSError *error) { [imagePickerVc hideProgressHUD]; self->_errorMsg = errorMessage; [self dismissAndCallDelegateMethod]; }]; } - (void)dismissAndCallDelegateMethod { [self dismissViewControllerAnimated:NO completion:^{ [self callDelegateMethod]; }]; [self.imagePickerVc dismissViewControllerAnimated:YES completion:nil]; } - (void)callDelegateMethod { if (_outputPath) { NSURL *videoURL = [NSURL fileURLWithPath:_outputPath]; if (self.imagePickerVc.saveEditedVideoToAlbum) { [[TZImageManager manager] saveVideoWithUrl:videoURL completion:^(PHAsset *asset, NSError *error) { if (error) { // 视频保存失败 if ([self.imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFailToSaveEditedVideoWithError:)]) { [self.imagePickerVc.pickerDelegate imagePickerController:self.imagePickerVc didFailToSaveEditedVideoWithError:error]; } } }]; } UIImage *coverImage = [[TZImageManager manager] getImageWithVideoURL:videoURL]; if ([self.imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingAndEditingVideo:outputPath:error:)]) { [self.imagePickerVc.pickerDelegate imagePickerController:self.imagePickerVc didFinishPickingAndEditingVideo:coverImage outputPath:_outputPath error:nil]; } if (self.imagePickerVc.didFinishPickingAndEditingVideoHandle) { self.imagePickerVc.didFinishPickingAndEditingVideoHandle(coverImage, _outputPath, nil); } } else { if ([self.imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingAndEditingVideo:outputPath:error:)]) { [self.imagePickerVc.pickerDelegate imagePickerController:self.imagePickerVc didFinishPickingAndEditingVideo:nil outputPath:nil error:_errorMsg]; } if (self.imagePickerVc.didFinishPickingAndEditingVideoHandle) { self.imagePickerVc.didFinishPickingAndEditingVideoHandle(nil, nil, _errorMsg); } } } #pragma mark - private method - (CMTime)getCropStartTime { NSTimeInterval second = [self getCropVideoStartSecond]; if (second > self.model.asset.duration) { second = roundf(self.model.asset.duration); } return CMTimeMakeWithSeconds(second, _playerLayer.player.currentTime.timescale); } - (CMTimeRange)getCropVideoTimeRange { NSTimeInterval startSecond = [self getCropVideoStartSecond]; CMTime start = CMTimeMakeWithSeconds(startSecond, _playerLayer.player.currentTime.timescale); NSTimeInterval second = [self getCropVideoDuration]; CMTime duration = CMTimeMakeWithSeconds(second, _playerLayer.player.currentTime.timescale); return CMTimeRangeMake(start, duration); } - (NSTimeInterval)getCropVideoDuration { CGFloat rectW = self.videoEditView.cropRect.size.width; CGFloat contentW = self.videoEditView.allImgWidth; CGFloat second = rectW / contentW * roundf(self.model.asset.duration); return roundf(second); } - (NSTimeInterval)getCropVideoStartSecond { CGFloat offsetX = self.collectionView.contentOffset.x; CGFloat contentW = self.videoEditView.allImgWidth; CGFloat cropRectX = self.videoEditView.cropRect.origin.x - VideoEditLeftMargin - PanImageWidth; NSTimeInterval second = (offsetX + cropRectX) / contentW * roundf(self.model.asset.duration); if (second < 0) second = 0; return roundf(second); } - (CMTime)getTimeOfSeek { NSTimeInterval second = [self getCropVideoStartSecond]; if (second > self.model.asset.duration) { second = roundf(self.model.asset.duration); } return CMTimeMakeWithSeconds(second, _playerLayer.player.currentTime.timescale); } - (void)starTimer { [self stopTimer]; NSTimeInterval timeInterval = [self getCropVideoDuration]; self.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(playCropVideo) userInfo:nil repeats:YES]; [self.timer fire]; } - (void)stopTimer { if (self.timer) { [self.videoEditView resetIndicatorLine]; [_player pause]; [self.timer invalidate]; self.timer = nil; } } - (void)playCropVideo { [_player seekToTime:[self getCropStartTime] toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; [_player play]; [self.videoEditView indicatorLineAnimateWithDuration:[self getCropVideoDuration] cropRect:self.videoEditView.cropRect]; } #pragma mark - Notification Method - (void)pausePlayer { [_player pause]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; } #pragma mark - lazy - (UIView *)iCloudErrorView{ if (!_iCloudErrorView) { _iCloudErrorView = [[UIView alloc] initWithFrame:CGRectMake(0, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.view.tz_width, 28)]; UIImageView *icloud = [[UIImageView alloc] init]; icloud.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"]; icloud.frame = CGRectMake(20, 0, 28, 28); [_iCloudErrorView addSubview:icloud]; UILabel *label = [[UILabel alloc] init]; label.frame = CGRectMake(53, 0, self.view.tz_width - 63, 28); label.font = [UIFont systemFontOfSize:10]; label.textColor = [UIColor whiteColor]; label.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"]; [_iCloudErrorView addSubview:label]; [self.view addSubview:_iCloudErrorView]; _iCloudErrorView.hidden = YES; } return _iCloudErrorView; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskPortrait; } - (void)dealloc { NSLog(@"%s",__func__); [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma clang diagnostic pop @end @implementation TZVideoEditView { UILabel *_dragingLabel; CGFloat _itemWidth; CGFloat _beginOffsetX; CGFloat _endOffsetX; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initSubViews]; } return self; } - (void)initSubViews { _indicatorLine = UIView.new; _indicatorLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7]; [self addSubview:_indicatorLine]; _beginImgView = UIImageView.new; _beginImgView.image = [UIImage imageNamed:@"leftVideoEdit"]; _beginImgView.userInteractionEnabled = YES; _beginImgView.tag = 0; UIPanGestureRecognizer *beginPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)]; [_beginImgView addGestureRecognizer:beginPanGesture]; [self addSubview:_beginImgView]; _endImgView = UIImageView.new; _endImgView.image = [UIImage imageNamed:@"rightVideoEdit"]; _endImgView.userInteractionEnabled = YES; _endImgView.tag = 1; UIPanGestureRecognizer *endPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)]; [_endImgView addGestureRecognizer:endPanGesture]; [self addSubview:_endImgView]; } - (void)layoutSubviews { _beginImgView.frame = CGRectMake(VideoEditLeftMargin, 0, PanImageWidth, self.tz_height); _indicatorLine.frame = CGRectMake(_beginImgView.tz_right - 2, 2, 2, self.tz_height - 4); _endImgView.frame = CGRectMake(self.tz_width - PanImageWidth - VideoEditLeftMargin, 0, PanImageWidth, self.tz_height); self.cropRect = CGRectMake(VideoEditLeftMargin + PanImageWidth, 0, self.tz_width - VideoEditLeftMargin * 2 - PanImageWidth * 2, self.tz_height); } - (void)setAllImgWidth:(CGFloat)allImgWidth { _allImgWidth = allImgWidth; if ((NSInteger)roundf(self.videoDuration) <= 0) { self.minCropRectWidth = allImgWidth; return; } CGFloat scale = MinCropVideoDuration / self.videoDuration; self.minCropRectWidth = scale * allImgWidth; } - (void)setCropRect:(CGRect)cropRect { _cropRect = cropRect; self.beginImgView.tz_left = cropRect.origin.x - PanImageWidth; self.indicatorLine.tz_left = cropRect.origin.x - self.indicatorLine.tz_width; self.endImgView.tz_left = CGRectGetMaxX(cropRect); [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextClearRect(context, rect); CGPoint topPoints[] = { CGPointMake(self.cropRect.origin.x, 0), CGPointMake(CGRectGetMaxX(self.cropRect), 0) }; CGPoint bottomPoints[] = { CGPointMake(self.cropRect.origin.x, self.tz_height), CGPointMake(CGRectGetMaxX(self.cropRect), self.tz_height) }; CGContextAddLines(context, topPoints, 2); CGContextAddLines(context, bottomPoints, 2); CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); CGContextSetLineWidth(context, 4.0); CGContextDrawPath(context, kCGPathStroke); } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGRect beginImgViewFrame = self.beginImgView.frame; beginImgViewFrame.origin.x -= PanImageWidth; beginImgViewFrame.size.width += PanImageWidth * 2; if (CGRectContainsPoint(beginImgViewFrame, point)) return self.beginImgView; CGRect endImgViewFrame = self.endImgView.frame; endImgViewFrame.origin.x -= PanImageWidth; endImgViewFrame.size.width += PanImageWidth * 2; if (CGRectContainsPoint(endImgViewFrame, point)) return self.endImgView; return nil; } #pragma mark - private - (void)indicatorLineAnimateWithDuration:(NSTimeInterval)duration cropRect:(CGRect)cropRect { [self resetIndicatorLine]; [UIView animateWithDuration:duration delay:.0 options:UIViewAnimationOptionCurveLinear animations:^{ self.indicatorLine.tz_left = CGRectGetMaxX(cropRect); } completion:nil]; } - (void)resetIndicatorLine { [self.indicatorLine.layer removeAllAnimations]; self.indicatorLine.tz_left = CGRectGetMinX(self.cropRect) - self.indicatorLine.tz_width; } - (void)panGestureAction:(UIGestureRecognizer *)gesture { CGPoint point = [gesture locationInView:self]; CGRect rect = self.cropRect; CGFloat minCropRectLeft = VideoEditLeftMargin + PanImageWidth; switch (gesture.view.tag) { case 0: { // 左边拖拽 CGFloat maxX = self.endImgView.tz_left - self.minCropRectWidth; point.x = MAX(minCropRectLeft, MIN(point.x, maxX)); point.y = 0; rect.size.width = CGRectGetMaxX(rect) - point.x; rect.origin.x = point.x; } break; case 1: { // 右边拖拽 minCropRectLeft = CGRectGetMaxX(self.beginImgView.frame) + self.minCropRectWidth; CGFloat maxX = self.tz_width - VideoEditLeftMargin - PanImageWidth; point.x = MAX(minCropRectLeft, MIN(point.x, maxX)); point.y = 0; rect.size.width = (point.x - rect.origin.x); } break; default:break; } self.cropRect = rect; switch (gesture.state) { case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateChanged: { if (self.delegate && [self.delegate respondsToSelector:@selector(editViewCropRectBeginChange)]) { [self.delegate editViewCropRectBeginChange]; } } break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: { if (self.delegate && [self.delegate respondsToSelector:@selector(editViewCropRectEndChange)]) { [self.delegate editViewCropRectEndChange]; } } break; default: break; } } @end @implementation TZVideoPictureCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initSubViews]; } return self; } - (void)initSubViews { _imgView = [[UIImageView alloc] initWithFrame:self.bounds]; _imgView.contentMode = UIViewContentModeScaleAspectFill; _imgView.clipsToBounds = YES; [self.contentView addSubview:_imgView]; } @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZVideoEditedPreviewController.h ================================================ // // TZVideoEditedPreviewController.h // TZImagePickerController // // Created by 肖兰月 on 2021/5/29. // Copyright © 2021 谭真. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @interface TZVideoEditedPreviewController : UIViewController @property (nonatomic, copy) NSURL *videoURL; @end NS_ASSUME_NONNULL_END ================================================ FILE: TZImagePickerController/TZImagePickerController/TZVideoEditedPreviewController.m ================================================ // // TZVideoEditedPreviewController.m // TZImagePickerController // // Created by 肖兰月 on 2021/5/29. // Copyright © 2021 谭真. All rights reserved. // #import "TZVideoEditedPreviewController.h" #import #import "TZImageManager.h" #import "TZImagePickerController.h" #import "UIView+TZLayout.h" @interface TZVideoEditedPreviewController () { AVPlayer *_player; AVPlayerLayer *_playerLayer; UIButton *_playButton; UIImage *_cover; UIView *_toolBar; UIButton *_doneButton; UIProgressView *_progress; UIStatusBarStyle _originStatusBarStyle; } @end @implementation TZVideoEditedPreviewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blackColor]; [self configMoviePlayer]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:UIApplicationWillResignActiveNotification object:nil]; } - (void)configMoviePlayer { _player = [AVPlayer playerWithURL:self.videoURL]; _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; [self.view.layer addSublayer:_playerLayer]; [self configPlayButton]; [self configBottomToolBar]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:AVPlayerItemDidPlayToEndTimeNotification object:self->_player.currentItem]; } - (void)configPlayButton { _playButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted]; [_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_playButton]; } - (void)configBottomToolBar { _toolBar = [[UIView alloc] initWithFrame:CGRectZero]; CGFloat rgb = 34 / 255.0; _toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7]; _doneButton = [UIButton buttonWithType:UIButtonTypeCustom]; _doneButton.titleLabel.font = [UIFont systemFontOfSize:16]; [_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc) { [_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal]; [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal]; } else { [_doneButton setTitle:[NSBundle tz_localizedStringForKey:@"Done"] forState:UIControlStateNormal]; [_doneButton setTitleColor:[UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:1.0] forState:UIControlStateNormal]; } [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled]; [_toolBar addSubview:_doneButton]; [self.view addSubview:_toolBar]; } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height; CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0; CGFloat statusBarAndNaviBarHeight = statusBarHeight + self.navigationController.navigationBar.tz_height; CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom; _toolBar.frame = CGRectMake(0, self.view.tz_height - toolBarHeight, self.view.tz_width, toolBarHeight); [_doneButton sizeToFit]; CGFloat doneButtonWidth = MAX(44, _doneButton.tz_width); if ([TZCommonTools tz_isRightToLeftLayout]) { _doneButton.frame = CGRectMake(12, 0, doneButtonWidth, 44); } else { _doneButton.frame = CGRectMake(self.view.tz_width - doneButtonWidth - 12, 0, doneButtonWidth, 44); } _playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, self.view.tz_height - statusBarAndNaviBarHeight - toolBarHeight); _playerLayer.frame = self.view.bounds; } #pragma mark - Click Event - (void)playButtonClick { CMTime currentTime = _player.currentItem.currentTime; CMTime durationTime = _player.currentItem.duration; if (_player.rate == 0.0f) { if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)]; [_player play]; _toolBar.hidden = YES; [_playButton setImage:nil forState:UIControlStateNormal]; } else { [self pausePlayerAndShowNaviBar]; } } - (void)doneButtonClick { [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - Notification Method - (void)pausePlayerAndShowNaviBar { [_player pause]; _toolBar.hidden = NO; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma clang diagnostic pop @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZVideoPlayerController.h ================================================ // // TZVideoPlayerController.h // TZImagePickerController // // Created by 谭真 on 16/1/5. // Copyright © 2016年 谭真. All rights reserved. // #import @class TZAssetModel; @interface TZVideoPlayerController : UIViewController @property (nonatomic, strong) TZAssetModel *model; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/TZVideoPlayerController.m ================================================ // // TZVideoPlayerController.m // TZImagePickerController // // Created by 谭真 on 16/1/5. // Copyright © 2016年 谭真. All rights reserved. // #import "TZVideoPlayerController.h" #import #import "UIView+TZLayout.h" #import "TZImageManager.h" #import "TZAssetModel.h" #import "TZImagePickerController.h" #import "TZPhotoPreviewController.h" #import "TZVideoCropController.h" @interface TZVideoPlayerController () { AVPlayer *_player; AVPlayerLayer *_playerLayer; UIButton *_playButton; UIImage *_playButtonNormalImage; UIImage *_cover; NSString *_outputPath; NSString *_errorMsg; UIView *_toolBar; UIButton *_doneButton; UIButton *_editButton; UIProgressView *_progress; UIStatusBarStyle _originStatusBarStyle; } @property (assign, nonatomic) BOOL needShowStatusBar; // iCloud无法同步提示UI @property (nonatomic, strong) UIView *iCloudErrorView; @end #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @implementation TZVideoPlayerController - (void)viewDidLoad { [super viewDidLoad]; self.needShowStatusBar = ![UIApplication sharedApplication].statusBarHidden; self.view.backgroundColor = [UIColor blackColor]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc) { self.navigationItem.title = tzImagePickerVc.previewBtnTitleStr; } [self configMoviePlayer]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:UIApplicationWillResignActiveNotification object:nil]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; _originStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if (self.needShowStatusBar) { [UIApplication sharedApplication].statusBarHidden = NO; } [UIApplication sharedApplication].statusBarStyle = _originStatusBarStyle; } - (void)configMoviePlayer { [[TZImageManager manager] getPhotoWithAsset:_model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { BOOL iCloudSyncFailed = !photo && [TZCommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorView.hidden = !iCloudSyncFailed; if (!isDegraded && photo) { self->_cover = photo; self->_doneButton.enabled = YES; self->_editButton.enabled = YES; } }]; [[TZImageManager manager] getVideoWithAsset:_model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ self->_player = [AVPlayer playerWithPlayerItem:playerItem]; self->_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self->_player]; self->_playerLayer.frame = self.view.bounds; [self.view.layer addSublayer:self->_playerLayer]; [self addProgressObserver]; [self configPlayButton]; [self configBottomToolBar]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:AVPlayerItemDidPlayToEndTimeNotification object:self->_player.currentItem]; }); }]; } /// Show progress,do it next time / 给播放器添加进度更新,下次加上 - (void)addProgressObserver{ AVPlayerItem *playerItem = _player.currentItem; UIProgressView *progress = _progress; [_player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { float current = CMTimeGetSeconds(time); float total = CMTimeGetSeconds([playerItem duration]); if (current) { [progress setProgress:(current/total) animated:YES]; } }]; } - (void)configPlayButton { _playButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; [_playButton setImage:[UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted]; [_playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_playButton]; } - (void)configBottomToolBar { _toolBar = [[UIView alloc] initWithFrame:CGRectZero]; CGFloat rgb = 34 / 255.0; _toolBar.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:0.7]; _doneButton = [UIButton buttonWithType:UIButtonTypeCustom]; _doneButton.titleLabel.font = [UIFont systemFontOfSize:16]; if (!_cover) { _doneButton.enabled = NO; } [_doneButton addTarget:self action:@selector(doneButtonClick) forControlEvents:UIControlEventTouchUpInside]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (tzImagePickerVc) { [_doneButton setTitle:tzImagePickerVc.doneBtnTitleStr forState:UIControlStateNormal]; [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal]; } else { [_doneButton setTitle:[NSBundle tz_localizedStringForKey:@"Done"] forState:UIControlStateNormal]; [_doneButton setTitleColor:[UIColor colorWithRed:(83/255.0) green:(179/255.0) blue:(17/255.0) alpha:1.0] forState:UIControlStateNormal]; } [_doneButton setTitleColor:tzImagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled]; [_toolBar addSubview:_doneButton]; [self.view addSubview:_toolBar]; if (tzImagePickerVc && tzImagePickerVc.allowEditVideo && roundf(self.model.asset.duration) > 1) { _editButton = [UIButton buttonWithType:UIButtonTypeCustom]; _editButton.titleLabel.font = [UIFont systemFontOfSize:16]; if (!_cover) { _editButton.enabled = NO; } [_editButton addTarget:self action:@selector(editButtonClick) forControlEvents:UIControlEventTouchUpInside]; [_editButton setTitle:tzImagePickerVc.editBtnTitleStr forState:UIControlStateNormal]; [_editButton setTitleColor:tzImagePickerVc.oKButtonTitleColorNormal forState:UIControlStateNormal]; [_editButton setTitleColor:tzImagePickerVc.oKButtonTitleColorDisabled forState:UIControlStateDisabled]; [_toolBar addSubview:_editButton]; } if (tzImagePickerVc.videoPreviewPageUIConfigBlock) { tzImagePickerVc.videoPreviewPageUIConfigBlock(_playButton, _toolBar, _editButton, _doneButton); } } - (UIStatusBarStyle)preferredStatusBarStyle { TZImagePickerController *tzImagePicker = (TZImagePickerController *)self.navigationController; if (tzImagePicker && [tzImagePicker isKindOfClass:[TZImagePickerController class]]) { return tzImagePicker.statusBarStyle; } return [super preferredStatusBarStyle]; } #pragma mark - Layout - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; TZImagePickerController *tzImagePickerVc = (TZImagePickerController *)self.navigationController; BOOL isFullScreen = self.view.tz_height == [UIScreen mainScreen].bounds.size.height; BOOL isRTL = [TZCommonTools tz_isRightToLeftLayout]; CGFloat statusBarHeight = isFullScreen ? [TZCommonTools tz_statusBarHeight] : 0; CGFloat statusBarAndNaviBarHeight = statusBarHeight + self.navigationController.navigationBar.tz_height; _playerLayer.frame = self.view.bounds; CGFloat toolBarHeight = 44 + [TZCommonTools tz_safeAreaInsets].bottom; _toolBar.frame = CGRectMake(0, self.view.tz_height - toolBarHeight, self.view.tz_width, toolBarHeight); [_doneButton sizeToFit]; if (isRTL) { _doneButton.frame = CGRectMake(12, 0, MAX(44, _doneButton.tz_width), 44); } else { _doneButton.frame = CGRectMake(self.view.tz_width - _doneButton.tz_width - 12, 0, MAX(44, _doneButton.tz_width), 44); } _playButton.frame = CGRectMake(0, statusBarAndNaviBarHeight, self.view.tz_width, self.view.tz_height - statusBarAndNaviBarHeight - toolBarHeight); if (tzImagePickerVc.allowEditVideo) { [_editButton sizeToFit]; if (isRTL) { _editButton.frame = CGRectMake(self.view.tz_width - _editButton.tz_width - 12, 0, _editButton.tz_width, 44); } else { _editButton.frame = CGRectMake(12, 0, _editButton.tz_width, 44); } } if (tzImagePickerVc.videoPreviewPageDidLayoutSubviewsBlock) { tzImagePickerVc.videoPreviewPageDidLayoutSubviewsBlock(_playButton, _toolBar, _editButton, _doneButton); } } #pragma mark - Click Event - (void)playButtonClick { CMTime currentTime = _player.currentItem.currentTime; CMTime durationTime = _player.currentItem.duration; if (_player.rate == 0.0f) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_VIDEO_PLAY_NOTIFICATION" object:_player]; if (currentTime.value == durationTime.value) [_player.currentItem seekToTime:CMTimeMake(0, 1)]; [_player play]; [self.navigationController setNavigationBarHidden:YES]; _toolBar.hidden = YES; _playButtonNormalImage = [_playButton imageForState:UIControlStateNormal]; [_playButton setImage:nil forState:UIControlStateNormal]; [UIApplication sharedApplication].statusBarHidden = YES; } else { [self pausePlayerAndShowNaviBar]; } } - (void)editButtonClick { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; TZVideoCropController *videoCropVc = [[TZVideoCropController alloc] init]; videoCropVc.model = self.model; videoCropVc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; videoCropVc.modalPresentationStyle = UIModalPresentationFullScreen; videoCropVc.modalPresentationCapturesStatusBarAppearance = YES; videoCropVc.imagePickerVc = imagePickerVc; [self presentViewController:videoCropVc animated:YES completion:nil]; } - (void)doneButtonClick { if ([[TZImageManager manager] isAssetCannotBeSelected:_model.asset]) { return; } TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; if (imagePickerVc.allowEditVideo) { [imagePickerVc showProgressHUD]; [[TZImageManager manager] getVideoOutputPathWithAsset:_model.asset presetName:imagePickerVc.presetName success:^(NSString *outputPath) { [imagePickerVc hideProgressHUD]; self->_outputPath = outputPath; [self dismissAndCallDelegateMethod]; } failure:^(NSString *errorMessage, NSError *error) { [imagePickerVc hideProgressHUD]; self->_errorMsg = errorMessage; [self dismissAndCallDelegateMethod]; }]; } else { [self dismissAndCallDelegateMethod]; } } - (void)dismissAndCallDelegateMethod { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; if (!imagePickerVc) { [self dismissViewControllerAnimated:YES completion:nil]; return; } if (imagePickerVc.autoDismiss) { [imagePickerVc dismissViewControllerAnimated:YES completion:^{ [self callDelegateMethod]; }]; } else { [self callDelegateMethod]; } } - (void)callDelegateMethod { TZImagePickerController *imagePickerVc = (TZImagePickerController *)self.navigationController; if (imagePickerVc.allowEditVideo) { if (_outputPath) { if ([imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingAndEditingVideo:outputPath:error:)]) { [imagePickerVc.pickerDelegate imagePickerController:imagePickerVc didFinishPickingAndEditingVideo:self->_cover outputPath:self->_outputPath error:nil]; } if (imagePickerVc.didFinishPickingAndEditingVideoHandle) { imagePickerVc.didFinishPickingAndEditingVideoHandle(self->_cover, self->_outputPath, nil); } } else { if ([imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingAndEditingVideo:outputPath:error:)]) { [imagePickerVc.pickerDelegate imagePickerController:imagePickerVc didFinishPickingAndEditingVideo:nil outputPath:nil error:self->_errorMsg]; } if (imagePickerVc.didFinishPickingAndEditingVideoHandle) { imagePickerVc.didFinishPickingAndEditingVideoHandle(nil, nil, self->_errorMsg); } } } else { if ([imagePickerVc.pickerDelegate respondsToSelector:@selector(imagePickerController:didFinishPickingVideo:sourceAssets:)]) { [imagePickerVc.pickerDelegate imagePickerController:imagePickerVc didFinishPickingVideo:_cover sourceAssets:_model.asset]; } if (imagePickerVc.didFinishPickingVideoHandle) { imagePickerVc.didFinishPickingVideoHandle(_cover,_model.asset); } } } #pragma mark - Notification Method - (void)pausePlayerAndShowNaviBar { [_player pause]; _toolBar.hidden = NO; [self.navigationController setNavigationBarHidden:NO]; UIImage *normalImage = _playButtonNormalImage ?: [UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"]; [_playButton setImage:normalImage forState:UIControlStateNormal]; if (self.needShowStatusBar) { [UIApplication sharedApplication].statusBarHidden = NO; } } #pragma mark - lazy - (UIView *)iCloudErrorView{ if (!_iCloudErrorView) { _iCloudErrorView = [[UIView alloc] initWithFrame:CGRectMake(0, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.view.tz_width, 28)]; UIImageView *icloud = [[UIImageView alloc] init]; icloud.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"]; icloud.frame = CGRectMake(20, 0, 28, 28); [_iCloudErrorView addSubview:icloud]; UILabel *label = [[UILabel alloc] init]; label.frame = CGRectMake(53, 0, self.view.tz_width - 63, 28); label.font = [UIFont systemFontOfSize:10]; label.textColor = [UIColor whiteColor]; label.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"]; [_iCloudErrorView addSubview:label]; [self.view addSubview:_iCloudErrorView]; _iCloudErrorView.hidden = YES; } return _iCloudErrorView; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma clang diagnostic pop @end ================================================ FILE: TZImagePickerController/TZImagePickerController/UIView+TZLayout.h ================================================ // // UIView+TZLayout.h // TZImagePickerController // // Created by 谭真 on 15/2/24. // Copyright © 2015年 谭真. All rights reserved. // #import typedef enum : NSUInteger { TZOscillatoryAnimationToBigger, TZOscillatoryAnimationToSmaller, } TZOscillatoryAnimationType; @interface UIView (TZLayout) @property (nonatomic) CGFloat tz_left; ///< Shortcut for frame.origin.x. @property (nonatomic) CGFloat tz_top; ///< Shortcut for frame.origin.y @property (nonatomic) CGFloat tz_right; ///< Shortcut for frame.origin.x + frame.size.width @property (nonatomic) CGFloat tz_bottom; ///< Shortcut for frame.origin.y + frame.size.height @property (nonatomic) CGFloat tz_width; ///< Shortcut for frame.size.width. @property (nonatomic) CGFloat tz_height; ///< Shortcut for frame.size.height. @property (nonatomic) CGFloat tz_centerX; ///< Shortcut for center.x @property (nonatomic) CGFloat tz_centerY; ///< Shortcut for center.y @property (nonatomic) CGPoint tz_origin; ///< Shortcut for frame.origin. @property (nonatomic) CGSize tz_size; ///< Shortcut for frame.size. + (void)showOscillatoryAnimationWithLayer:(CALayer *)layer type:(TZOscillatoryAnimationType)type; @end ================================================ FILE: TZImagePickerController/TZImagePickerController/UIView+TZLayout.m ================================================ // // UIView+TZLayout.m // TZImagePickerController // // Created by 谭真 on 15/2/24. // Copyright © 2015年 谭真. All rights reserved. // #import "UIView+TZLayout.h" @implementation UIView (TZLayout) - (CGFloat)tz_left { return self.frame.origin.x; } - (void)setTz_left:(CGFloat)x { CGRect frame = self.frame; frame.origin.x = x; self.frame = frame; } - (CGFloat)tz_top { return self.frame.origin.y; } - (void)setTz_top:(CGFloat)y { CGRect frame = self.frame; frame.origin.y = y; self.frame = frame; } - (CGFloat)tz_right { return self.frame.origin.x + self.frame.size.width; } - (void)setTz_right:(CGFloat)right { CGRect frame = self.frame; frame.origin.x = right - frame.size.width; self.frame = frame; } - (CGFloat)tz_bottom { return self.frame.origin.y + self.frame.size.height; } - (void)setTz_bottom:(CGFloat)bottom { CGRect frame = self.frame; frame.origin.y = bottom - frame.size.height; self.frame = frame; } - (CGFloat)tz_width { return self.frame.size.width; } - (void)setTz_width:(CGFloat)width { CGRect frame = self.frame; frame.size.width = width; self.frame = frame; } - (CGFloat)tz_height { return self.frame.size.height; } - (void)setTz_height:(CGFloat)height { CGRect frame = self.frame; frame.size.height = height; self.frame = frame; } - (CGFloat)tz_centerX { return self.center.x; } - (void)setTz_centerX:(CGFloat)centerX { self.center = CGPointMake(centerX, self.center.y); } - (CGFloat)tz_centerY { return self.center.y; } - (void)setTz_centerY:(CGFloat)centerY { self.center = CGPointMake(self.center.x, centerY); } - (CGPoint)tz_origin { return self.frame.origin; } - (void)setTz_origin:(CGPoint)origin { CGRect frame = self.frame; frame.origin = origin; self.frame = frame; } - (CGSize)tz_size { return self.frame.size; } - (void)setTz_size:(CGSize)size { CGRect frame = self.frame; frame.size = size; self.frame = frame; } + (void)showOscillatoryAnimationWithLayer:(CALayer *)layer type:(TZOscillatoryAnimationType)type{ NSNumber *animationScale1 = type == TZOscillatoryAnimationToBigger ? @(1.15) : @(0.5); NSNumber *animationScale2 = type == TZOscillatoryAnimationToBigger ? @(0.92) : @(1.15); [UIView animateWithDuration:0.15 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:^{ [layer setValue:animationScale1 forKeyPath:@"transform.scale"]; } completion:^(BOOL finished) { [UIView animateWithDuration:0.15 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:^{ [layer setValue:animationScale2 forKeyPath:@"transform.scale"]; } completion:^(BOOL finished) { [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:^{ [layer setValue:@(1.0) forKeyPath:@"transform.scale"]; } completion:nil]; }]; }]; } @end ================================================ FILE: TZImagePickerController/TZImageUploadOperation.h ================================================ // // TZImageUploadOperation.h // TZImagePickerController // // Created by 谭真 on 2019/1/14. // Copyright © 2019 谭真. All rights reserved. // #import "TZImageRequestOperation.h" NS_ASSUME_NONNULL_BEGIN @interface TZImageUploadOperation : TZImageRequestOperation @end NS_ASSUME_NONNULL_END ================================================ FILE: TZImagePickerController/TZImageUploadOperation.m ================================================ // // TZImageUploadOperation.m // TZImagePickerController // // Created by 谭真 on 2019/1/14. // Copyright © 2019 谭真. All rights reserved. // #import "TZImageUploadOperation.h" #import "TZImageManager.h" @implementation TZImageUploadOperation - (void)start { NSLog(@"TZImageUploadOperation start"); self.executing = YES; dispatch_async(dispatch_get_global_queue(0, 0), ^{ #pragma mark - 获取&上传大图 /* [[TZImageManager manager] getPhotoWithAsset:self.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { dispatch_async(dispatch_get_main_queue(), ^{ if (!isDegraded) { if (self.completedBlock) { self.completedBlock(photo, info, isDegraded); } // 在这里上传图片(代码略),图片上传完毕后调用[self done] dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self done]; }); } }); } progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.progressBlock) { self.progressBlock(progress, error, stop, info); } }); } networkAccessAllowed:YES]; */ #pragma mark - 获取&上传原图 [[TZImageManager manager] getOriginalPhotoWithAsset:self.asset progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.progressBlock) { self.progressBlock(progress, error, stop, info); } }); } newCompletion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { dispatch_async(dispatch_get_main_queue(), ^{ if (!isDegraded) { if (self.completedBlock) { self.completedBlock(photo, info, isDegraded); } // 在这里上传图片(代码略),图片上传完毕后调用[self done] dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self done]; }); } }); }]; }); } - (void)done { [super done]; // NSLog(@"TZImageUploadOperation done"); } @end ================================================ FILE: TZImagePickerController/TZTestCell.h ================================================ // // TZTestCell.h // TZImagePickerController // // Created by 谭真 on 16/1/3. // Copyright © 2016年 谭真. All rights reserved. // #import @interface TZTestCell : UICollectionViewCell @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UIImageView *videoImageView; @property (nonatomic, strong) UIButton *deleteBtn; @property (nonatomic, strong) UILabel *gifLable; @property (nonatomic, assign) NSInteger row; @property (nonatomic, strong) id asset; - (UIView *)snapshotView; @end ================================================ FILE: TZImagePickerController/TZTestCell.m ================================================ // // TZTestCell.m // TZImagePickerController // // Created by 谭真 on 16/1/3. // Copyright © 2016年 谭真. All rights reserved. // #import "TZTestCell.h" #import "UIView+TZLayout.h" #import #import "TZImagePickerController/TZImagePickerController.h" @implementation TZTestCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor whiteColor]; _imageView = [[UIImageView alloc] init]; _imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; _imageView.contentMode = UIViewContentModeScaleAspectFit; [self addSubview:_imageView]; self.clipsToBounds = YES; _videoImageView = [[UIImageView alloc] init]; _videoImageView.image = [UIImage tz_imageNamedFromMyBundle:@"MMVideoPreviewPlay"]; _videoImageView.contentMode = UIViewContentModeScaleAspectFill; _videoImageView.hidden = YES; [self addSubview:_videoImageView]; _deleteBtn = [UIButton buttonWithType:UIButtonTypeCustom]; [_deleteBtn setImage:[UIImage imageNamed:@"photo_delete"] forState:UIControlStateNormal]; _deleteBtn.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 0, -10); _deleteBtn.alpha = 0.6; [self addSubview:_deleteBtn]; _gifLable = [[UILabel alloc] init]; _gifLable.text = @"GIF"; _gifLable.textColor = [UIColor whiteColor]; _gifLable.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.8]; _gifLable.textAlignment = NSTextAlignmentCenter; _gifLable.font = [UIFont systemFontOfSize:10]; [self addSubview:_gifLable]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; _imageView.frame = self.bounds; _gifLable.frame = CGRectMake(self.tz_width - 25, self.tz_height - 14, 25, 14); _deleteBtn.frame = CGRectMake(self.tz_width - 36, 0, 36, 36); CGFloat width = self.tz_width / 3.0; _videoImageView.frame = CGRectMake(width, width, width, width); } - (void)setAsset:(PHAsset *)asset { _asset = asset; _videoImageView.hidden = asset.mediaType != PHAssetMediaTypeVideo; _gifLable.hidden = ![[asset valueForKey:@"filename"] containsString:@"GIF"]; } - (void)setRow:(NSInteger)row { _row = row; _deleteBtn.tag = row; } - (UIView *)snapshotView { UIView *snapshotView = [[UIView alloc]init]; UIView *cellSnapshotView = nil; if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)]) { cellSnapshotView = [self snapshotViewAfterScreenUpdates:NO]; } else { CGSize size = CGSizeMake(self.bounds.size.width + 20, self.bounds.size.height + 20); UIGraphicsBeginImageContextWithOptions(size, self.opaque, 0); [self.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage * cellSnapshotImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); cellSnapshotView = [[UIImageView alloc]initWithImage:cellSnapshotImage]; } snapshotView.frame = CGRectMake(0, 0, cellSnapshotView.frame.size.width, cellSnapshotView.frame.size.height); cellSnapshotView.frame = CGRectMake(0, 0, cellSnapshotView.frame.size.width, cellSnapshotView.frame.size.height); [snapshotView addSubview:cellSnapshotView]; return snapshotView; } @end ================================================ FILE: TZImagePickerController/ViewController.h ================================================ // // ViewController.h // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @interface ViewController : UIViewController @end ================================================ FILE: TZImagePickerController/ViewController.m ================================================ // // ViewController.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import "ViewController.h" #import "TZImagePickerController.h" #import "UIView+TZLayout.h" #import "TZTestCell.h" #import #import "LxGridViewFlowLayout.h" #import "TZImageManager.h" #import "TZVideoPlayerController.h" #import "TZPhotoPreviewController.h" #import "TZGifPhotoPreviewController.h" #import "TZAssetCell.h" #import #import "FLAnimatedImage.h" #import "TZImageUploadOperation.h" #import "TZVideoEditedPreviewController.h" @interface ViewController () { NSMutableArray *_selectedPhotos; NSMutableArray *_selectedAssets; BOOL _isSelectOriginalPhoto; BOOL _isAllowEditVideo; CGFloat _itemWH; CGFloat _margin; } @property (nonatomic, strong) UIImagePickerController *imagePickerVc; @property (nonatomic, strong) UICollectionView *collectionView; @property (strong, nonatomic) LxGridViewFlowLayout *layout; @property (strong, nonatomic) CLLocation *location; @property (nonatomic, strong) NSOperationQueue *operationQueue; @property (weak, nonatomic) IBOutlet UIScrollView *scrollView; // 设置开关 @property (weak, nonatomic) IBOutlet UISwitch *showTakePhotoBtnSwitch; ///< 允许拍照 @property (weak, nonatomic) IBOutlet UISwitch *showTakeVideoBtnSwitch; ///< 允许拍视频 @property (weak, nonatomic) IBOutlet UISwitch *sortAscendingSwitch; ///< 照片排列按修改时间升序 @property (weak, nonatomic) IBOutlet UISwitch *allowPickingVideoSwitch; ///< 允许选择视频 @property (weak, nonatomic) IBOutlet UISwitch *allowPickingImageSwitch; ///< 允许选择图片 @property (weak, nonatomic) IBOutlet UISwitch *allowPickingGifSwitch; @property (weak, nonatomic) IBOutlet UISwitch *allowPickingOriginalPhotoSwitch; ///< 允许选择原图 @property (weak, nonatomic) IBOutlet UISwitch *showSheetSwitch; ///< 显示一个sheet,把拍照/拍视频按钮放在外面 @property (weak, nonatomic) IBOutlet UITextField *maxCountTF; ///< 照片最大可选张数,设置为1即为单选模式 @property (weak, nonatomic) IBOutlet UITextField *columnNumberTF; @property (weak, nonatomic) IBOutlet UISwitch *allowCropSwitch; @property (weak, nonatomic) IBOutlet UISwitch *needCircleCropSwitch; @property (weak, nonatomic) IBOutlet UISwitch *allowPickingMuitlpleVideoSwitch; @property (weak, nonatomic) IBOutlet UISwitch *showSelectedIndexSwitch; @end @implementation ViewController #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (UIImagePickerController *)imagePickerVc { if (_imagePickerVc == nil) { _imagePickerVc = [[UIImagePickerController alloc] init]; _imagePickerVc.delegate = self; // set appearance / 改变相册选择页的导航栏外观 _imagePickerVc.navigationBar.barTintColor = self.navigationController.navigationBar.barTintColor; _imagePickerVc.navigationBar.tintColor = self.navigationController.navigationBar.tintColor; UIBarButtonItem *tzBarItem, *BarItem; if (@available(iOS 9, *)) { tzBarItem = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[TZImagePickerController class]]]; BarItem = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UIImagePickerController class]]]; } else { tzBarItem = [UIBarButtonItem appearanceWhenContainedIn:[TZImagePickerController class], nil]; BarItem = [UIBarButtonItem appearanceWhenContainedIn:[UIImagePickerController class], nil]; } NSDictionary *titleTextAttributes = [tzBarItem titleTextAttributesForState:UIControlStateNormal]; [BarItem setTitleTextAttributes:titleTextAttributes forState:UIControlStateNormal]; } return _imagePickerVc; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; _selectedPhotos = [NSMutableArray array]; _selectedAssets = [NSMutableArray array]; [self configCollectionView]; } - (BOOL)prefersStatusBarHidden { return NO; } - (void)configCollectionView { // 如不需要长按排序效果,将LxGridViewFlowLayout类改成UICollectionViewFlowLayout即可 _layout = [[LxGridViewFlowLayout alloc] init]; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_layout]; CGFloat rgb = 244 / 255.0; _collectionView.alwaysBounceVertical = YES; _collectionView.backgroundColor = [UIColor colorWithRed:rgb green:rgb blue:rgb alpha:1.0]; _collectionView.contentInset = UIEdgeInsetsMake(4, 4, 4, 4); _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; [self.view addSubview:_collectionView]; [_collectionView registerClass:[TZTestCell class] forCellWithReuseIdentifier:@"TZTestCell"]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; NSInteger contentSizeH = 14 * 35 + 20; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.scrollView.contentSize = CGSizeMake(0, contentSizeH + 5); }); if (self.scrollView.tz_height + 80 > self.view.tz_height) { self.scrollView.tz_height = self.view.tz_height - 80; } _margin = 4; _itemWH = (self.view.tz_width - 2 * _margin - 4) / 3 - _margin; _layout.itemSize = CGSizeMake(_itemWH, _itemWH); _layout.minimumInteritemSpacing = _margin; _layout.minimumLineSpacing = _margin; [self.collectionView setCollectionViewLayout:_layout]; CGFloat collectionViewY = CGRectGetMaxY(self.scrollView.frame); self.collectionView.frame = CGRectMake(0, collectionViewY, self.view.tz_width, self.view.tz_height - collectionViewY); } #pragma mark UICollectionView - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (_selectedPhotos.count >= self.maxCountTF.text.integerValue) { return _selectedPhotos.count; } if (!self.allowPickingMuitlpleVideoSwitch.isOn) { if (_isAllowEditVideo) { return 1; } else { for (PHAsset *asset in _selectedAssets) { if (asset.mediaType == PHAssetMediaTypeVideo) { return _selectedPhotos.count; } } } } return _selectedPhotos.count + 1; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { TZTestCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZTestCell" forIndexPath:indexPath]; cell.videoImageView.hidden = YES; if (indexPath.item == _selectedPhotos.count) { cell.imageView.image = [UIImage imageNamed:@"AlbumAddBtn.png"]; cell.deleteBtn.hidden = YES; cell.gifLable.hidden = YES; } else { cell.imageView.image = _selectedPhotos[indexPath.item]; if (!_isAllowEditVideo) { cell.asset = _selectedAssets[indexPath.item]; } cell.deleteBtn.hidden = NO; } if (!self.allowPickingGifSwitch.isOn) { cell.gifLable.hidden = YES; } cell.deleteBtn.tag = indexPath.item; [cell.deleteBtn addTarget:self action:@selector(deleteBtnClik:) forControlEvents:UIControlEventTouchUpInside]; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.item == _selectedPhotos.count) { BOOL showSheet = self.showSheetSwitch.isOn; if (showSheet) { NSString *takePhotoTitle = @"拍照"; if (self.showTakeVideoBtnSwitch.isOn && self.showTakePhotoBtnSwitch.isOn) { takePhotoTitle = @"相机"; } else if (self.showTakeVideoBtnSwitch.isOn) { takePhotoTitle = @"拍摄"; } UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; UIAlertAction *takePhotoAction = [UIAlertAction actionWithTitle:takePhotoTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [self takePhoto]; }]; [alertVc addAction:takePhotoAction]; UIAlertAction *imagePickerAction = [UIAlertAction actionWithTitle:@"去相册选择" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [self pushTZImagePickerController]; }]; [alertVc addAction:imagePickerAction]; UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]; [alertVc addAction:cancelAction]; UIPopoverPresentationController *popover = alertVc.popoverPresentationController; UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; if (popover) { popover.sourceView = cell; popover.sourceRect = cell.bounds; popover.permittedArrowDirections = UIPopoverArrowDirectionAny; } [self presentViewController:alertVc animated:YES completion:nil]; } else { [self pushTZImagePickerController]; } } else if (_isAllowEditVideo && [_selectedAssets[indexPath.item] isKindOfClass:[NSURL class]]) { // preview edited video / 预览编辑后的视频 TZVideoEditedPreviewController *vc = [[TZVideoEditedPreviewController alloc] init]; vc.videoURL = _selectedAssets[indexPath.item]; vc.modalPresentationStyle = UIModalPresentationFullScreen; [self presentViewController:vc animated:YES completion:nil]; } else { // preview photos or video / 预览照片或者视频 PHAsset *asset = _selectedAssets[indexPath.item]; BOOL isVideo = NO; isVideo = asset.mediaType == PHAssetMediaTypeVideo; if ([[asset valueForKey:@"filename"] containsString:@"GIF"] && self.allowPickingGifSwitch.isOn && !self.allowPickingMuitlpleVideoSwitch.isOn) { TZGifPhotoPreviewController *vc = [[TZGifPhotoPreviewController alloc] init]; TZAssetModel *model = [TZAssetModel modelWithAsset:asset type:TZAssetModelMediaTypePhotoGif timeLength:@""]; vc.model = model; vc.modalPresentationStyle = UIModalPresentationFullScreen; [self presentViewController:vc animated:YES completion:nil]; } else if (isVideo && !self.allowPickingMuitlpleVideoSwitch.isOn) { // perview video / 预览视频 TZVideoPlayerController *vc = [[TZVideoPlayerController alloc] init]; TZAssetModel *model = [TZAssetModel modelWithAsset:asset type:TZAssetModelMediaTypeVideo timeLength:@""]; vc.model = model; vc.modalPresentationStyle = UIModalPresentationFullScreen; [self presentViewController:vc animated:YES completion:nil]; } else { // preview photos / 预览照片 TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithSelectedAssets:_selectedAssets selectedPhotos:_selectedPhotos index:indexPath.item]; imagePickerVc.maxImagesCount = self.maxCountTF.text.integerValue; imagePickerVc.allowPickingGif = self.allowPickingGifSwitch.isOn; imagePickerVc.autoSelectCurrentWhenDone = NO; imagePickerVc.allowPickingOriginalPhoto = self.allowPickingOriginalPhotoSwitch.isOn; imagePickerVc.allowPickingMultipleVideo = self.allowPickingMuitlpleVideoSwitch.isOn; imagePickerVc.showSelectedIndex = self.showSelectedIndexSwitch.isOn; imagePickerVc.isSelectOriginalPhoto = _isSelectOriginalPhoto; imagePickerVc.modalPresentationStyle = UIModalPresentationFullScreen; [imagePickerVc setDidFinishPickingPhotosHandle:^(NSArray *photos, NSArray *assets, BOOL isSelectOriginalPhoto) { self->_selectedPhotos = [NSMutableArray arrayWithArray:photos]; self->_selectedAssets = [NSMutableArray arrayWithArray:assets]; self->_isSelectOriginalPhoto = isSelectOriginalPhoto; [self->_collectionView reloadData]; self->_collectionView.contentSize = CGSizeMake(0, ((self->_selectedPhotos.count + 2) / 3 ) * (self->_margin + self->_itemWH)); }]; [self presentViewController:imagePickerVc animated:YES completion:nil]; } } } #pragma mark - LxGridViewDataSource /// 以下三个方法为长按排序相关代码 - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath { return indexPath.item < _selectedPhotos.count; } - (BOOL)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath canMoveToIndexPath:(NSIndexPath *)destinationIndexPath { return (sourceIndexPath.item < _selectedPhotos.count && destinationIndexPath.item < _selectedPhotos.count); } - (void)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath didMoveToIndexPath:(NSIndexPath *)destinationIndexPath { UIImage *image = _selectedPhotos[sourceIndexPath.item]; [_selectedPhotos removeObjectAtIndex:sourceIndexPath.item]; [_selectedPhotos insertObject:image atIndex:destinationIndexPath.item]; id asset = _selectedAssets[sourceIndexPath.item]; [_selectedAssets removeObjectAtIndex:sourceIndexPath.item]; [_selectedAssets insertObject:asset atIndex:destinationIndexPath.item]; [_collectionView reloadData]; } #pragma mark - TZImagePickerController - (void)pushTZImagePickerController { if (self.maxCountTF.text.integerValue <= 0) { return; } // 设置languageBundle以使用其它语言,必须在TZImagePickerController初始化前设置 / Set languageBundle to use other language // [TZImagePickerConfig sharedInstance].languageBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"tz-ru" ofType:@"lproj"]]; TZImagePickerController *imagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:self.maxCountTF.text.integerValue columnNumber:self.columnNumberTF.text.integerValue delegate:self pushPhotoPickerVc:YES]; #pragma mark - 五类个性化设置,这些参数都可以不传,此时会走默认设置 imagePickerVc.isSelectOriginalPhoto = _isSelectOriginalPhoto; if (self.maxCountTF.text.integerValue > 1) { // 1.设置目前已经选中的图片数组 imagePickerVc.selectedAssets = _selectedAssets; // 目前已经选中的图片数组 } imagePickerVc.allowTakePicture = self.showTakePhotoBtnSwitch.isOn; // 在内部显示拍照按钮 imagePickerVc.allowTakeVideo = self.showTakeVideoBtnSwitch.isOn; // 在内部显示拍视频按 imagePickerVc.videoMaximumDuration = 10; // 视频最大拍摄时间 imagePickerVc.allowEditVideo = YES; // 允许编辑视频 // imagePickerVc.saveEditedVideoToCollection = YES; // 编辑后的视频是否自动保存到相册 // imagePickerVc.maxCropVideoDuration = 30; // 裁剪视频的最大时长 // imagePickerVc.presetName = AVAssetExportPresetMediumQuality // 编辑后的视频的导出质量 [imagePickerVc setUiImagePickerControllerSettingBlock:^(UIImagePickerController *imagePickerController) { imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; }]; // imagePickerVc.autoSelectCurrentWhenDone = NO; // imagePickerVc.photoWidth = 1600; // imagePickerVc.photoPreviewMaxWidth = 1600; // 2. Set the appearance // 2. 在这里设置imagePickerVc的外观 // imagePickerVc.navigationBar.barTintColor = [UIColor greenColor]; // imagePickerVc.oKButtonTitleColorDisabled = [UIColor lightGrayColor]; // imagePickerVc.oKButtonTitleColorNormal = [UIColor greenColor]; // imagePickerVc.barItemTextColor = [UIColor blackColor]; // imagePickerVc.navigationBar.translucent = NO; // [imagePickerVc.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor blackColor]}]; // imagePickerVc.navigationBar.tintColor = [UIColor blackColor]; // if (@available(iOS 13.0, *)) { // UINavigationBarAppearance *barAppearance = [[UINavigationBarAppearance alloc] init]; // barAppearance.backgroundColor = imagePickerVc.navigationBar.barTintColor; // barAppearance.titleTextAttributes = imagePickerVc.navigationBar.titleTextAttributes; // imagePickerVc.navigationBar.standardAppearance = barAppearance; // imagePickerVc.navigationBar.scrollEdgeAppearance = barAppearance; // } imagePickerVc.iconThemeColor = [UIColor colorWithRed:31 / 255.0 green:185 / 255.0 blue:34 / 255.0 alpha:1.0]; imagePickerVc.showPhotoCannotSelectLayer = YES; imagePickerVc.cannotSelectLayerColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8]; /* [imagePickerVc setPhotoPickerPageUIConfigBlock:^(UICollectionView *collectionView, UIView *bottomToolBar, UIButton *previewButton, UIButton *originalPhotoButton, UILabel *originalPhotoLabel, UIButton *doneButton, UIImageView *numberImageView, UILabel *numberLabel, UIView *divideLine) { [doneButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; }]; */ /* [imagePickerVc setAssetCellDidSetModelBlock:^(TZAssetCell *cell, UIImageView *imageView, UIImageView *selectImageView, UILabel *indexLabel, UIView *bottomView, UILabel *timeLength, UIImageView *videoImgView) { cell.contentView.clipsToBounds = YES; cell.contentView.layer.cornerRadius = cell.contentView.tz_width * 0.5; }]; */ // 3. Set allow picking video & photo & originalPhoto or not // 3. 设置是否可以选择视频/图片/原图 imagePickerVc.allowPickingVideo = self.allowPickingVideoSwitch.isOn; imagePickerVc.allowPickingImage = self.allowPickingImageSwitch.isOn; imagePickerVc.allowPickingOriginalPhoto = self.allowPickingOriginalPhotoSwitch.isOn; imagePickerVc.allowPickingGif = self.allowPickingGifSwitch.isOn; imagePickerVc.allowPickingMultipleVideo = self.allowPickingMuitlpleVideoSwitch.isOn; // 是否可以多选视频 // 4. 照片排列按修改时间升序 imagePickerVc.sortAscendingByModificationDate = self.sortAscendingSwitch.isOn; // imagePickerVc.minImagesCount = 3; // imagePickerVc.alwaysEnableDoneBtn = YES; // imagePickerVc.minPhotoWidthSelectable = 3000; // imagePickerVc.minPhotoHeightSelectable = 2000; /// 5. Single selection mode, valid when maxImagesCount = 1 /// 5. 单选模式,maxImagesCount为1时才生效 imagePickerVc.showSelectBtn = NO; imagePickerVc.allowCrop = self.allowCropSwitch.isOn; imagePickerVc.needCircleCrop = self.needCircleCropSwitch.isOn; // 设置竖屏下的裁剪尺寸 NSInteger left = 30; NSInteger widthHeight = self.view.tz_width - 2 * left; NSInteger top = (self.view.tz_height - widthHeight) / 2; if ([TZCommonTools tz_isLandscape]) { top = 30; widthHeight = self.view.tz_height - 2 * left; left = (self.view.tz_width - widthHeight) / 2; } imagePickerVc.cropRect = CGRectMake(left, top, widthHeight, widthHeight); imagePickerVc.scaleAspectFillCrop = YES; // 设置横屏下的裁剪尺寸 // imagePickerVc.cropRectLandscape = CGRectMake((self.view.tz_height - widthHeight) / 2, left, widthHeight, widthHeight); /* [imagePickerVc setCropViewSettingBlock:^(UIView *cropView) { cropView.layer.borderColor = [UIColor redColor].CGColor; cropView.layer.borderWidth = 2.0; }];*/ // imagePickerVc.allowPreview = NO; // 自定义导航栏上的返回按钮 /* [imagePickerVc setNavLeftBarButtonSettingBlock:^(UIButton *leftButton){ [leftButton setImage:[UIImage imageNamed:@"back"] forState:UIControlStateNormal]; [leftButton setImageEdgeInsets:UIEdgeInsetsMake(0, -10, 0, 20)]; }]; imagePickerVc.delegate = self; */ // Deprecated, Use statusBarStyle // imagePickerVc.isStatusBarDefault = NO; imagePickerVc.statusBarStyle = UIStatusBarStyleLightContent; // 设置是否显示图片序号 imagePickerVc.showSelectedIndex = self.showSelectedIndexSwitch.isOn; // 设置拍照时是否需要定位,仅对选择器内部拍照有效,外部拍照的,请拷贝demo时手动把pushImagePickerController里定位方法的调用删掉 // imagePickerVc.allowCameraLocation = NO; // 自定义gif播放方案 [[TZImagePickerConfig sharedInstance] setGifImagePlayBlock:^(TZPhotoPreviewView *view, UIImageView *imageView, NSData *gifData, NSDictionary *info) { FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:gifData]; FLAnimatedImageView *animatedImageView; for (UIView *subview in imageView.subviews) { if ([subview isKindOfClass:[FLAnimatedImageView class]]) { animatedImageView = (FLAnimatedImageView *)subview; animatedImageView.frame = imageView.bounds; animatedImageView.animatedImage = nil; } } if (!animatedImageView) { animatedImageView = [[FLAnimatedImageView alloc] initWithFrame:imageView.bounds]; animatedImageView.runLoopMode = NSDefaultRunLoopMode; [imageView addSubview:animatedImageView]; } animatedImageView.animatedImage = animatedImage; }]; // 设置首选语言 / Set preferred language // imagePickerVc.preferredLanguage = @"zh-Hans"; #pragma mark - 到这里为止 // You can get the photos by block, the same as by delegate. // 你可以通过block或者代理,来得到用户选择的照片. [imagePickerVc setDidFinishPickingPhotosHandle:^(NSArray *photos, NSArray *assets, BOOL isSelectOriginalPhoto) { }]; imagePickerVc.modalPresentationStyle = UIModalPresentationFullScreen; [self presentViewController:imagePickerVc animated:YES completion:nil]; } /* // 设置了navLeftBarButtonSettingBlock后,需打开这个方法,让系统的侧滑返回生效 - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { navigationController.interactivePopGestureRecognizer.enabled = YES; if (viewController != navigationController.viewControllers[0]) { navigationController.interactivePopGestureRecognizer.delegate = nil; // 支持侧滑 } } */ #pragma mark - UIImagePickerController - (void)takePhoto { AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) { // 无相机权限 做一个友好的提示 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"无法使用相机" message:@"请在iPhone的""设置-隐私-相机""中允许访问相机" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]]; [alertController addAction:[UIAlertAction actionWithTitle:@"设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; }]]; [self presentViewController:alertController animated:YES completion:nil]; } else if (authStatus == AVAuthorizationStatusNotDetermined) { // fix issue 466, 防止用户首次拍照拒绝授权时相机页黑屏 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { dispatch_async(dispatch_get_main_queue(), ^{ [self takePhoto]; }); } }]; // 拍照之前还需要检查相册权限 } else if ([PHPhotoLibrary authorizationStatus] == 2) { // 已被拒绝,没有相册权限,将无法保存拍的照片 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"无法访问相册" message:@"请在iPhone的""设置-隐私-相册""中允许访问相册" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]]; [alertController addAction:[UIAlertAction actionWithTitle:@"设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; }]]; [self presentViewController:alertController animated:YES completion:nil]; } else if ([PHPhotoLibrary authorizationStatus] == 0) { // 未请求过相册权限 [[TZImageManager manager] requestAuthorizationWithCompletion:^{ [self takePhoto]; }]; } else { [self pushImagePickerController]; } } // 调用相机 - (void)pushImagePickerController { #ifdef TZ_HAVE_LOCATION_CODE // 提前定位 __weak typeof(self) weakSelf = self; [[TZLocationManager manager] startLocationWithSuccessBlock:^(NSArray *locations) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.location = [locations firstObject]; } failureBlock:^(NSError *error) { __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.location = nil; }]; #endif UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera; if ([UIImagePickerController isSourceTypeAvailable: UIImagePickerControllerSourceTypeCamera]) { self.imagePickerVc.sourceType = sourceType; NSMutableArray *mediaTypes = [NSMutableArray array]; if (self.showTakeVideoBtnSwitch.isOn) { [mediaTypes addObject:(NSString *)kUTTypeMovie]; } if (self.showTakePhotoBtnSwitch.isOn) { [mediaTypes addObject:(NSString *)kUTTypeImage]; } if (mediaTypes.count) { _imagePickerVc.mediaTypes = mediaTypes; } [self presentViewController:_imagePickerVc animated:YES completion:nil]; } else { NSLog(@"模拟器中无法打开照相机,请在真机中使用"); } } - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { [picker dismissViewControllerAnimated:YES completion:nil]; NSString *type = [info objectForKey:UIImagePickerControllerMediaType]; TZImagePickerController *tzImagePickerVc = [[TZImagePickerController alloc] initWithMaxImagesCount:1 delegate:self]; tzImagePickerVc.sortAscendingByModificationDate = self.sortAscendingSwitch.isOn; [tzImagePickerVc showProgressHUD]; if ([type isEqualToString:@"public.image"]) { UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage]; NSDictionary *meta = [info objectForKey:UIImagePickerControllerMediaMetadata]; // save photo and get asset / 保存图片,获取到asset [[TZImageManager manager] savePhotoWithImage:image meta:meta location:self.location completion:^(PHAsset *asset, NSError *error){ [tzImagePickerVc hideProgressHUD]; if (error) { NSLog(@"图片保存失败 %@",error); } else { TZAssetModel *assetModel = [[TZImageManager manager] createModelWithAsset:asset]; if (self.allowCropSwitch.isOn) { // 允许裁剪,去裁剪 TZImagePickerController *imagePicker = [[TZImagePickerController alloc] initCropTypeWithAsset:assetModel.asset photo:image completion:^(UIImage *cropImage, id asset) { [self refreshCollectionViewWithAddedAsset:asset image:cropImage]; }]; imagePicker.allowPickingImage = YES; imagePicker.needCircleCrop = self.needCircleCropSwitch.isOn; imagePicker.circleCropRadius = 100; [self presentViewController:imagePicker animated:YES completion:nil]; } else { [self refreshCollectionViewWithAddedAsset:assetModel.asset image:image]; } } }]; } else if ([type isEqualToString:@"public.movie"]) { NSURL *videoUrl = [info objectForKey:UIImagePickerControllerMediaURL]; if (videoUrl) { [[TZImageManager manager] saveVideoWithUrl:videoUrl location:self.location completion:^(PHAsset *asset, NSError *error) { [tzImagePickerVc hideProgressHUD]; if (!error) { TZAssetModel *assetModel = [[TZImageManager manager] createModelWithAsset:asset]; [[TZImageManager manager] getPhotoWithAsset:assetModel.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (!isDegraded && photo) { [self refreshCollectionViewWithAddedAsset:assetModel.asset image:photo]; } }]; } }]; } } } - (void)refreshCollectionViewWithAddedAsset:(PHAsset *)asset image:(UIImage *)image { [_selectedAssets addObject:asset]; [_selectedPhotos addObject:image]; [_collectionView reloadData]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { if ([picker isKindOfClass:[UIImagePickerController class]]) { [picker dismissViewControllerAnimated:YES completion:nil]; } } #pragma mark - TZImagePickerControllerDelegate /// User click cancel button /// 用户点击了取消 - (void)tz_imagePickerControllerDidCancel:(TZImagePickerController *)picker { // NSLog(@"cancel"); } // The picker should dismiss itself; when it dismissed these handle will be called. // You can also set autoDismiss to NO, then the picker don't dismiss itself. // If isOriginalPhoto is YES, user picked the original photo. // You can get original photo with asset, by the method [[TZImageManager manager] getOriginalPhotoWithAsset:completion:]. // The UIImage Object in photos default width is 828px, you can set it by photoWidth property. // 这个照片选择器会自己dismiss,当选择器dismiss的时候,会执行下面的代理方法 // 你也可以设置autoDismiss属性为NO,选择器就不会自己dismis了 // 如果isSelectOriginalPhoto为YES,表明用户选择了原图 // 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:] // photos数组里的UIImage对象,默认是828像素宽,你可以通过设置photoWidth属性的值来改变它 - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray *)infos { _selectedPhotos = [NSMutableArray arrayWithArray:photos]; _selectedAssets = [NSMutableArray arrayWithArray:assets]; _isSelectOriginalPhoto = isSelectOriginalPhoto; [_collectionView reloadData]; // _collectionView.contentSize = CGSizeMake(0, ((_selectedPhotos.count + 2) / 3 ) * (_margin + _itemWH)); // 1.打印图片名字 [self printAssetsName:assets]; // 2.图片位置信息 for (PHAsset *phAsset in assets) { NSLog(@"location:%@",phAsset.location); } // 3. 获取原图的示例,用队列限制最大并发为1,避免内存暴增 self.operationQueue = [[NSOperationQueue alloc] init]; self.operationQueue.maxConcurrentOperationCount = 1; for (NSInteger i = 0; i < assets.count; i++) { PHAsset *asset = assets[i]; // 图片上传operation,上传代码请写到operation内的start方法里,内有注释 TZImageUploadOperation *operation = [[TZImageUploadOperation alloc] initWithAsset:asset completion:^(UIImage * photo, NSDictionary *info, BOOL isDegraded) { if (isDegraded) return; NSLog(@"图片获取&上传完成"); } progressHandler:^(double progress, NSError * _Nonnull error, BOOL * _Nonnull stop, NSDictionary * _Nonnull info) { NSLog(@"获取原图进度 %f", progress); }]; [self.operationQueue addOperation:operation]; } } /// 如果用户选择了某张照片下面的代理方法会被执行 /// 如果isSelectOriginalPhoto为YES,表明用户选择了原图 /// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:] - (void)imagePickerController:(TZImagePickerController *)picker didSelectAsset:(PHAsset *)asset photo:(UIImage *)photo isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto { // [_selectedAssets addObject:asset]; // [_selectedPhotos addObject:photo]; // [self.collectionView reloadData]; } /// 如果用户取消选择了某张照片下面的代理方法会被执行 /// 如果isSelectOriginalPhoto为YES,表明用户选择了原图 /// 你可以通过一个asset获得原图,通过这个方法:[[TZImageManager manager] getOriginalPhotoWithAsset:completion:] - (void)imagePickerController:(TZImagePickerController *)picker didDeselectAsset:(PHAsset *)asset photo:(UIImage *)photo isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto { // int index = -1; // for (int i = 0; i < _selectedAssets.count; i++) { // if ([_selectedAssets[i] isEqual:asset]) { // index = i; // } // } // if (index > -1) { // [_selectedAssets removeObjectAtIndex:index]; // [_selectedPhotos removeObjectAtIndex:index]; // [self.collectionView reloadData]; // } } // If user picking a video and allowPickingMultipleVideo is NO, this callback will be called. // If allowPickingMultipleVideo is YES, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: // 如果用户选择了一个视频且allowPickingMultipleVideo是NO,下面的代理方法会被执行 // 如果allowPickingMultipleVideo是YES,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingVideo:(UIImage *)coverImage sourceAssets:(PHAsset *)asset { _selectedPhotos = [NSMutableArray arrayWithArray:@[coverImage]]; _selectedAssets = [NSMutableArray arrayWithArray:@[asset]]; // open this code to send video / 打开这段代码发送视频 [[TZImageManager manager] getVideoOutputPathWithAsset:asset presetName:AVAssetExportPresetLowQuality success:^(NSString *outputPath) { // NSData *data = [NSData dataWithContentsOfFile:outputPath]; NSLog(@"视频导出到本地完成,沙盒路径为:%@",outputPath); // Export completed, send video here, send by outputPath or NSData // 导出完成,在这里写上传代码,通过路径或者通过NSData上传 } failure:^(NSString *errorMessage, NSError *error) { NSLog(@"视频导出失败:%@,error:%@",errorMessage, error); }]; [_collectionView reloadData]; // _collectionView.contentSize = CGSizeMake(0, ((_selectedPhotos.count + 2) / 3 ) * (_margin + _itemWH)); } // If allowEditVideo is YES and allowPickingMultipleVideo is NO, When user picking a video, this callback will be called. // If allowPickingMultipleVideo is YES, video editing is not supported, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: // 当allowEditVideo是YES且allowPickingMultipleVideo是NO是,如果用户选择了一个视频,下面的代理方法会被执行 // 如果allowPickingMultipleVideo是YES,则不支持编辑视频,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingAndEditingVideo:(UIImage *)coverImage outputPath:(NSString *)outputPath error:(NSString *)errorMsg { _isAllowEditVideo = YES; self->_selectedPhotos = [NSMutableArray arrayWithArray:@[coverImage]]; self->_selectedAssets = [NSMutableArray arrayWithArray:@[[NSURL fileURLWithPath:outputPath]]]; if (outputPath) { // NSData *data = [NSData dataWithContentsOfFile:outputPath]; NSLog(@"视频导出到本地完成,outputPath为:%@",outputPath); // Export completed, send video here, send by outputPath or NSData // 导出完成,在这里写上传代码,通过路径或者通过NSData上传 } else { NSLog(@"视频导出失败:%@",errorMsg); } [self.collectionView reloadData]; } // If user fail to save edited, this callback will be called. // 如果用户保存编辑好的视频失败,将会调用 - (void)imagePickerController:(TZImagePickerController *)picker didFailToSaveEditedVideoWithError:(NSError *)error { NSLog(@"编辑后的视频自动保存到相册失败:%@",error.description); } // If user picking a gif image and allowPickingMultipleVideo is NO, this callback will be called. // If allowPickingMultipleVideo is YES, will call imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: // 如果用户选择了一个gif图片且allowPickingMultipleVideo是NO,下面的代理方法会被执行 // 如果allowPickingMultipleVideo是YES,将会调用imagePickerController:didFinishPickingPhotos:sourceAssets:isSelectOriginalPhoto: - (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingGifImage:(UIImage *)animatedImage sourceAssets:(PHAsset *)asset { _selectedPhotos = [NSMutableArray arrayWithArray:@[animatedImage]]; _selectedAssets = [NSMutableArray arrayWithArray:@[asset]]; [_collectionView reloadData]; } // Decide album show or not't // 决定相册显示与否 - (BOOL)isAlbumCanSelect:(NSString *)albumName result:(PHFetchResult *)result { /* if ([albumName isEqualToString:@"个人收藏"]) { return NO; } if ([albumName isEqualToString:@"视频"]) { return NO; }*/ return YES; } // Decide asset show or not't // 决定asset显示与否 - (BOOL)isAssetCanBeDisplayed:(PHAsset *)asset { /* switch (asset.mediaType) { case PHAssetMediaTypeVideo: { // 视频时长 // NSTimeInterval duration = phAsset.duration; return NO; } break; case PHAssetMediaTypeImage: { // 图片尺寸 if (asset.pixelWidth > 3000 || asset.pixelHeight > 3000) { return NO; } return YES; } break; case PHAssetMediaTypeAudio: return NO; break; case PHAssetMediaTypeUnknown: return NO; break; default: break; } */ return YES; } // Decide asset can be selected // 决定照片能否被选中 - (BOOL)isAssetCanBeSelected:(PHAsset *)asset { /* switch (asset.mediaType) { case PHAssetMediaTypeVideo: { // 视频时长 // NSTimeInterval duration = phAsset.duration; return NO; } break; case PHAssetMediaTypeImage: { // 图片尺寸 if (asset.pixelWidth > 3000 || asset.pixelHeight > 3000) { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"不支持选择超大图片" message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [self.presentedViewController presentViewController:alertController animated:YES completion:nil]; return NO; } return YES; } break; case PHAssetMediaTypeAudio: return NO; break; case PHAssetMediaTypeUnknown: return NO; break; default: break; } */ return YES; } #pragma mark - Click Event - (void)deleteBtnClik:(UIButton *)sender { if ([self collectionView:self.collectionView numberOfItemsInSection:0] <= _selectedPhotos.count) { [_selectedPhotos removeObjectAtIndex:sender.tag]; [_selectedAssets removeObjectAtIndex:sender.tag]; [self.collectionView reloadData]; return; } [_selectedPhotos removeObjectAtIndex:sender.tag]; [_selectedAssets removeObjectAtIndex:sender.tag]; [_collectionView performBatchUpdates:^{ NSIndexPath *indexPath = [NSIndexPath indexPathForItem:sender.tag inSection:0]; [self->_collectionView deleteItemsAtIndexPaths:@[indexPath]]; } completion:^(BOOL finished) { [self->_collectionView reloadData]; }]; } - (IBAction)showTakePhotoBtnSwitchClick:(UISwitch *)sender { if (sender.isOn) { [_allowPickingImageSwitch setOn:YES animated:YES]; } } - (IBAction)showTakeVideoBtnSwitchClick:(UISwitch *)sender { if (sender.isOn) { [_allowPickingVideoSwitch setOn:YES animated:YES]; } } - (IBAction)showSheetSwitchClick:(UISwitch *)sender { if (sender.isOn) { [_allowPickingImageSwitch setOn:YES animated:YES]; } } - (IBAction)allowPickingOriginPhotoSwitchClick:(UISwitch *)sender { if (sender.isOn) { [_allowPickingImageSwitch setOn:YES animated:YES]; [self.needCircleCropSwitch setOn:NO animated:YES]; [self.allowCropSwitch setOn:NO animated:YES]; } } - (IBAction)allowPickingImageSwitchClick:(UISwitch *)sender { if (!sender.isOn) { [_allowPickingOriginalPhotoSwitch setOn:NO animated:YES]; [_allowPickingVideoSwitch setOn:YES animated:YES]; [_allowPickingGifSwitch setOn:NO animated:YES]; } } - (IBAction)allowPickingGifSwitchClick:(UISwitch *)sender { if (sender.isOn) { [_allowPickingImageSwitch setOn:YES animated:YES]; } else if (!self.allowPickingVideoSwitch.isOn) { [self.allowPickingMuitlpleVideoSwitch setOn:NO animated:YES]; } } - (IBAction)allowPickingVideoSwitchClick:(UISwitch *)sender { if (!sender.isOn) { [_allowPickingImageSwitch setOn:YES animated:YES]; if (!self.allowPickingGifSwitch.isOn) { [self.allowPickingMuitlpleVideoSwitch setOn:NO animated:YES]; } } } - (IBAction)allowCropSwitchClick:(UISwitch *)sender { if (sender.isOn) { self.maxCountTF.text = @"1"; [self.allowPickingOriginalPhotoSwitch setOn:NO animated:YES]; } else { if ([self.maxCountTF.text isEqualToString:@"1"]) { self.maxCountTF.text = @"9"; } [self.needCircleCropSwitch setOn:NO animated:YES]; } } - (IBAction)needCircleCropSwitchClick:(UISwitch *)sender { if (sender.isOn) { [self.allowCropSwitch setOn:YES animated:YES]; self.maxCountTF.text = @"1"; [self.allowPickingOriginalPhotoSwitch setOn:NO animated:YES]; } } - (IBAction)allowPickingMultipleVideoSwitchClick:(UISwitch *)sender { } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self.view endEditing:YES]; } #pragma mark - Private /// 打印图片名字 - (void)printAssetsName:(NSArray *)assets { NSString *fileName; for (PHAsset *asset in assets) { fileName = [asset valueForKey:@"filename"]; // NSLog(@"图片名字:%@",fileName); } } #pragma clang diagnostic pop @end ================================================ FILE: TZImagePickerController/ar-001.lproj/LaunchScreen.strings ================================================ ================================================ FILE: TZImagePickerController/ar-001.lproj/Main.strings ================================================ /* Class = "UITextField"; text = "9"; ObjectID = "37K-cO-hgf"; */ "37K-cO-hgf.text" = "9"; /* Class = "UILabel"; text = "允许选择照片原图"; ObjectID = "7mi-Lf-2N5"; */ "7mi-Lf-2N5.text" = "允许选择照片原图"; /* Class = "UILabel"; text = "允许多选视频/GIF/图片"; ObjectID = "7q9-aI-kbz"; */ "7q9-aI-kbz.text" = "允许多选视频/GIF/图片"; /* Class = "UILabel"; text = "允许拍视频"; ObjectID = "8H2-cq-aUq"; */ "8H2-cq-aUq.text" = "允许拍视频"; /* Class = "UILabel"; text = "单选模式下允许裁剪"; ObjectID = "Ibb-kN-MWC"; */ "Ibb-kN-MWC.text" = "单选模式下允许裁剪"; /* Class = "UILabel"; text = "允许选择Gif图片"; ObjectID = "Ly2-uJ-7DN"; */ "Ly2-uJ-7DN.text" = "允许选择Gif图片"; /* Class = "UILabel"; text = "允许拍照"; ObjectID = "R92-Zn-NWV"; */ "R92-Zn-NWV.text" = "允许拍照"; /* Class = "UILabel"; text = "使用圆形裁剪框"; ObjectID = "TiG-df-jDs"; */ "TiG-df-jDs.text" = "使用圆形裁剪框"; /* Class = "UILabel"; text = "照片按修改时间升序排列"; ObjectID = "UL3-n3-Hmt"; */ "UL3-n3-Hmt.text" = "照片按修改时间升序排列"; /* Class = "UITextField"; text = "4"; ObjectID = "YR1-nJ-q0v"; */ "YR1-nJ-q0v.text" = "4"; /* Class = "UILabel"; text = "每行展示照片张数"; ObjectID = "fbH-6q-oeE"; */ "fbH-6q-oeE.text" = "每行展示照片张数"; /* Class = "UILabel"; text = "把拍照/拍视频按钮放在外面"; ObjectID = "jop-5R-ioj"; */ "jop-5R-ioj.text" = "把拍照/拍视频按钮放在外面"; /* Class = "UILabel"; text = "右上角显示图片选中序号"; ObjectID = "nm2-mT-siy"; */ "nm2-mT-siy.text" = "右上角显示图片选中序号"; /* Class = "UILabel"; text = "照片最大可选张数"; ObjectID = "rK3-1U-kcW"; */ "rK3-1U-kcW.text" = "照片最大可选张数"; /* Class = "UILabel"; text = "允许选择照片"; ObjectID = "vNN-OI-9z1"; */ "vNN-OI-9z1.text" = "允许选择照片"; /* Class = "UILabel"; text = "允许选择视频"; ObjectID = "xCu-D3-IZg"; */ "xCu-D3-IZg.text" = "允许选择视频"; ================================================ FILE: TZImagePickerController/main.m ================================================ // // main.m // TZImagePickerController // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import #import "AppDelegate.h" int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: TZImagePickerController/zh-Hans.lproj/LaunchScreen.strings ================================================ ================================================ FILE: TZImagePickerController/zh-Hans.lproj/Main.strings ================================================ /* Class = "UILabel"; text = "允许选择照片原图"; ObjectID = "7mi-Lf-2N5"; */ "7mi-Lf-2N5.text" = "允许选择照片原图"; /* Class = "UILabel"; text = "显示内部拍照按钮"; ObjectID = "R92-Zn-NWV"; */ "R92-Zn-NWV.text" = "显示内部拍照按钮"; /* Class = "UILabel"; text = "照片按修改时间升序排列"; ObjectID = "UL3-n3-Hmt"; */ "UL3-n3-Hmt.text" = "照片按修改时间升序排列"; /* Class = "UILabel"; text = "把拍照按钮放在外面"; ObjectID = "jop-5R-ioj"; */ "jop-5R-ioj.text" = "把拍照按钮放在外面"; /* Class = "UILabel"; text = "允许选择照片"; ObjectID = "vNN-OI-9z1"; */ "vNN-OI-9z1.text" = "允许选择照片"; /* Class = "UILabel"; text = "允许选择视频"; ObjectID = "xCu-D3-IZg"; */ "xCu-D3-IZg.text" = "允许选择视频"; ================================================ FILE: TZImagePickerController.podspec ================================================ Pod::Spec.new do |s| s.name = "TZImagePickerController" s.version = "3.8.12" s.summary = "A clone of UIImagePickerController, support picking multiple photos、original photo and video" s.homepage = "https://github.com/banchichen/TZImagePickerController" s.license = "MIT" s.author = { "banchichen" => "tanzhenios@foxmail.com" } s.platform = :ios s.ios.deployment_target = "10.0" s.source = { :git => "https://github.com/banchichen/TZImagePickerController.git", :tag => "3.8.12" } s.requires_arc = true s.resource_bundles = { 'TZImagePickerControllerPrivacy' => ['TZImagePickerControllerFramework/PrivacyInfo.xcprivacy'] } s.subspec 'Basic' do |b| b.resources = "TZImagePickerController/Resources/*.bundle" b.source_files = "TZImagePickerController/TZImagePickerController/*.{h,m}" end s.subspec 'Location' do |l| l.source_files = 'TZImagePickerController/Location/*.{h,m}' end s.frameworks = "Photos", "PhotosUI" end ================================================ FILE: TZImagePickerController.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 441B78D32BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 441B78D22BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy */; }; 441B78D42BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 441B78D22BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy */; }; 441B78D52BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 441B78D22BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy */; }; 441B78D62BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 441B78D22BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy */; }; 570A348C26D7275C000EEF2E /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570A348B26D7275C000EEF2E /* Photos.framework */; }; 570A348E26D72762000EEF2E /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570A348D26D72762000EEF2E /* PhotosUI.framework */; }; 570A349026D72768000EEF2E /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570A348F26D72768000EEF2E /* PhotosUI.framework */; }; 57DD378A26D61592005090FF /* TZAuthLimitedFooterTipView.m in Sources */ = {isa = PBXBuildFile; fileRef = 57DD378926D61592005090FF /* TZAuthLimitedFooterTipView.m */; }; 57DD378E26D619C9005090FF /* TZAuthLimitedFooterTipView.m in Sources */ = {isa = PBXBuildFile; fileRef = 57DD378926D61592005090FF /* TZAuthLimitedFooterTipView.m */; }; 6D32B9BD1CD83640005CE1E0 /* LxGridViewFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D32B9BC1CD83640005CE1E0 /* LxGridViewFlowLayout.m */; }; 6D4608311DFFC60D004FB009 /* TZGifPhotoPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D4608301DFFC60D004FB009 /* TZGifPhotoPreviewController.m */; }; 6D5358CD1D64600F00928CC6 /* NSBundle+TZImagePicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D5358CC1D64600F00928CC6 /* NSBundle+TZImagePicker.m */; }; 6DC358661CC8BAFD00898D29 /* TZImagePickerController.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6DC358651CC8BAFD00898D29 /* TZImagePickerController.bundle */; }; 6DC84D0A1DF5358500A107A9 /* TZImageCropManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6DC84D091DF5358500A107A9 /* TZImageCropManager.m */; }; 6DD0502B1DF659ED0057C78D /* TZProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6DD0502A1DF659ED0057C78D /* TZProgressView.m */; }; 90020E1320F8840800D3AB10 /* TZImagePickerController.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 90020E1220F8840800D3AB10 /* TZImagePickerController.podspec */; }; 90020E1520F8840F00D3AB10 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 90020E1420F8840E00D3AB10 /* README.md */; }; 9005E2B3257A462E002A8682 /* UIView+TZLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 9005E2B1257A462E002A8682 /* UIView+TZLayout.m */; }; 9005E2B4257A462E002A8682 /* UIView+TZLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 9005E2B1257A462E002A8682 /* UIView+TZLayout.m */; }; 9005E2B5257A462E002A8682 /* UIView+TZLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 9005E2B2257A462E002A8682 /* UIView+TZLayout.h */; }; 900E65811C2BB8D5003D9A9E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 900E65801C2BB8D5003D9A9E /* main.m */; }; 900E65841C2BB8D5003D9A9E /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 900E65831C2BB8D5003D9A9E /* AppDelegate.m */; }; 900E65871C2BB8D5003D9A9E /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900E65861C2BB8D5003D9A9E /* ViewController.m */; }; 900E658A1C2BB8D5003D9A9E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 900E65881C2BB8D5003D9A9E /* Main.storyboard */; }; 900E658C1C2BB8D5003D9A9E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 900E658B1C2BB8D5003D9A9E /* Assets.xcassets */; }; 900E658F1C2BB8D5003D9A9E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 900E658D1C2BB8D5003D9A9E /* LaunchScreen.storyboard */; }; 900E659A1C2BB8D5003D9A9E /* TZImagePickerControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 900E65991C2BB8D5003D9A9E /* TZImagePickerControllerTests.m */; }; 900E65A51C2BB8D5003D9A9E /* TZImagePickerControllerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 900E65A41C2BB8D5003D9A9E /* TZImagePickerControllerUITests.m */; }; 900EEFF81C2BBF9500EA709B /* TZImagePickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EEFF71C2BBF9500EA709B /* TZImagePickerController.m */; }; 900EEFFE1C2BD58B00EA709B /* TZPhotoPickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EEFFD1C2BD58B00EA709B /* TZPhotoPickerController.m */; }; 900EF0021C2BD7E400EA709B /* TZAssetCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF0001C2BD7E400EA709B /* TZAssetCell.m */; }; 900EF00F1C2C0B1100EA709B /* TZAssetModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF00E1C2C0B1100EA709B /* TZAssetModel.m */; }; 900EF0121C2C107400EA709B /* TZPhotoPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF0111C2C107400EA709B /* TZPhotoPreviewController.m */; }; 900EF0161C2C134900EA709B /* TZPhotoPreviewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF0141C2C134900EA709B /* TZPhotoPreviewCell.m */; }; 9019FD8D21EC25D7009ADEAE /* TZImageUploadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9019FD8C21EC25D7009ADEAE /* TZImageUploadOperation.m */; }; 901CC3FA21CB757500C55443 /* TZImageRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 901CC3F821CB757500C55443 /* TZImageRequestOperation.h */; }; 901CC3FB21CB757500C55443 /* TZImageRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 901CC3F921CB757500C55443 /* TZImageRequestOperation.m */; }; 901CC3FC21CB758500C55443 /* TZImageRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 901CC3F921CB757500C55443 /* TZImageRequestOperation.m */; }; 901F2295215CABD600F604ED /* FLAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 901F2291215CABD600F604ED /* FLAnimatedImage.m */; }; 901F2296215CABD600F604ED /* FLAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 901F2293215CABD600F604ED /* FLAnimatedImageView.m */; }; 9038D5911C3974F0007DE549 /* TZTestCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 9038D5901C3974F0007DE549 /* TZTestCell.m */; }; 903996931F447604005E77C2 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 903996921F447604005E77C2 /* Photos.framework */; }; 908D11582887ED8000DD212E /* TZLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 908D11562887ED7F00DD212E /* TZLocationManager.m */; }; 908D11592887ED8000DD212E /* TZLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 908D11562887ED7F00DD212E /* TZLocationManager.m */; }; 908D115A2887ED8000DD212E /* TZLocationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 908D11572887ED7F00DD212E /* TZLocationManager.h */; }; 90A74B85203287C200D84C2A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 90A74B83203287C200D84C2A /* Localizable.strings */; }; 90CE84AE1C3A89EF003D0779 /* TZImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 90CE84AD1C3A89EF003D0779 /* TZImageManager.m */; }; 9F763A4A1FA071D000D9E526 /* TZImagePickerController.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9F763A421FA071CF00D9E526 /* TZImagePickerController.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9F763A501FA072E500D9E526 /* TZImagePickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EEFF71C2BBF9500EA709B /* TZImagePickerController.m */; }; 9F763A521FA072E500D9E526 /* TZPhotoPickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EEFFD1C2BD58B00EA709B /* TZPhotoPickerController.m */; }; 9F763A541FA072E500D9E526 /* TZAssetCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF0001C2BD7E400EA709B /* TZAssetCell.m */; }; 9F763A561FA072E500D9E526 /* TZAssetModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF00E1C2C0B1100EA709B /* TZAssetModel.m */; }; 9F763A581FA072E500D9E526 /* TZPhotoPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF0111C2C107400EA709B /* TZPhotoPreviewController.m */; }; 9F763A5A1FA072E500D9E526 /* TZPhotoPreviewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 900EF0141C2C134900EA709B /* TZPhotoPreviewCell.m */; }; 9F763A5C1FA072E500D9E526 /* TZVideoPlayerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 90CE84B61C3BABB6003D0779 /* TZVideoPlayerController.m */; }; 9F763A5E1FA072E500D9E526 /* TZGifPhotoPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D4608301DFFC60D004FB009 /* TZGifPhotoPreviewController.m */; }; 9F763A601FA072E500D9E526 /* TZProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6DD0502A1DF659ED0057C78D /* TZProgressView.m */; }; 9F763A621FA072E500D9E526 /* TZImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 90CE84AD1C3A89EF003D0779 /* TZImageManager.m */; }; 9F763A641FA072E500D9E526 /* TZImageCropManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6DC84D091DF5358500A107A9 /* TZImageCropManager.m */; }; 9F763A6A1FA072E500D9E526 /* NSBundle+TZImagePicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D5358CC1D64600F00928CC6 /* NSBundle+TZImagePicker.m */; }; 9F763A6B1FA0731E00D9E526 /* TZImagePickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = 900EEFF61C2BBF9500EA709B /* TZImagePickerController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A6C1FA0733100D9E526 /* TZAssetModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 900EF00D1C2C0B1100EA709B /* TZAssetModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A6D1FA0733700D9E526 /* NSBundle+TZImagePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = 6D5358CB1D64600F00928CC6 /* NSBundle+TZImagePicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A6E1FA0734800D9E526 /* TZImagePickerController.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6DC358651CC8BAFD00898D29 /* TZImagePickerController.bundle */; }; 9F763A6F1FA0741D00D9E526 /* TZPhotoPickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = 900EEFFC1C2BD58B00EA709B /* TZPhotoPickerController.h */; }; 9F763A701FA0741D00D9E526 /* TZAssetCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 900EEFFF1C2BD7E400EA709B /* TZAssetCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A711FA0741D00D9E526 /* TZPhotoPreviewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 900EF0101C2C107400EA709B /* TZPhotoPreviewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A721FA0741D00D9E526 /* TZPhotoPreviewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 900EF0131C2C134800EA709B /* TZPhotoPreviewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A731FA0741D00D9E526 /* TZVideoPlayerController.h in Headers */ = {isa = PBXBuildFile; fileRef = 90CE84B51C3BABB6003D0779 /* TZVideoPlayerController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A741FA0741D00D9E526 /* TZGifPhotoPreviewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 6D46082F1DFFC60D004FB009 /* TZGifPhotoPreviewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A751FA0741D00D9E526 /* TZProgressView.h in Headers */ = {isa = PBXBuildFile; fileRef = 6DD050291DF659ED0057C78D /* TZProgressView.h */; }; 9F763A761FA0741D00D9E526 /* TZImageManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 90CE84AC1C3A89EF003D0779 /* TZImageManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F763A771FA0741D00D9E526 /* TZImageCropManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6DC84D081DF5358500A107A9 /* TZImageCropManager.h */; }; B606A9C4265FD53200F5493E /* TZVideoCropController.m in Sources */ = {isa = PBXBuildFile; fileRef = B606A9C2265FD53200F5493E /* TZVideoCropController.m */; }; B606A9CE265FD5F700F5493E /* TZVideoPlayerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 90CE84B61C3BABB6003D0779 /* TZVideoPlayerController.m */; }; B662C36F2662914D006A672A /* TZVideoEditedPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B662C36E2662914D006A672A /* TZVideoEditedPreviewController.m */; }; B662C37326629179006A672A /* TZVideoEditedPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B662C36E2662914D006A672A /* TZVideoEditedPreviewController.m */; }; B6977B0A265FDB280028BA31 /* TZVideoCropController.m in Sources */ = {isa = PBXBuildFile; fileRef = B606A9C2265FD53200F5493E /* TZVideoCropController.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 900E65961C2BB8D5003D9A9E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 900E65741C2BB8D5003D9A9E /* Project object */; proxyType = 1; remoteGlobalIDString = 900E657B1C2BB8D5003D9A9E; remoteInfo = TZImagePickerController; }; 900E65A11C2BB8D5003D9A9E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 900E65741C2BB8D5003D9A9E /* Project object */; proxyType = 1; remoteGlobalIDString = 900E657B1C2BB8D5003D9A9E; remoteInfo = TZImagePickerController; }; 9F763A471FA071D000D9E526 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 900E65741C2BB8D5003D9A9E /* Project object */; proxyType = 1; remoteGlobalIDString = 9F763A411FA071CF00D9E526; remoteInfo = TZImagePickerControllerFramework; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9F763A4E1FA071D000D9E526 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 9F763A4A1FA071D000D9E526 /* TZImagePickerController.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 441B78D22BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 570A348B26D7275C000EEF2E /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/System/Library/Frameworks/Photos.framework; sourceTree = DEVELOPER_DIR; }; 570A348D26D72762000EEF2E /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/System/iOSSupport/System/Library/Frameworks/PhotosUI.framework; sourceTree = DEVELOPER_DIR; }; 570A348F26D72768000EEF2E /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; 57DD378826D61592005090FF /* TZAuthLimitedFooterTipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZAuthLimitedFooterTipView.h; sourceTree = ""; }; 57DD378926D61592005090FF /* TZAuthLimitedFooterTipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZAuthLimitedFooterTipView.m; sourceTree = ""; }; 6D12FC191D66B71E00182C44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 6D12FC1A1D66B71E00182C44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; 6D32B9BB1CD83640005CE1E0 /* LxGridViewFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LxGridViewFlowLayout.h; sourceTree = ""; }; 6D32B9BC1CD83640005CE1E0 /* LxGridViewFlowLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LxGridViewFlowLayout.m; sourceTree = ""; }; 6D46082F1DFFC60D004FB009 /* TZGifPhotoPreviewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZGifPhotoPreviewController.h; sourceTree = ""; }; 6D4608301DFFC60D004FB009 /* TZGifPhotoPreviewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZGifPhotoPreviewController.m; sourceTree = ""; }; 6D5358CB1D64600F00928CC6 /* NSBundle+TZImagePicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBundle+TZImagePicker.h"; sourceTree = ""; }; 6D5358CC1D64600F00928CC6 /* NSBundle+TZImagePicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+TZImagePicker.m"; sourceTree = ""; }; 6DC358651CC8BAFD00898D29 /* TZImagePickerController.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = TZImagePickerController.bundle; sourceTree = ""; }; 6DC84D081DF5358500A107A9 /* TZImageCropManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZImageCropManager.h; sourceTree = ""; }; 6DC84D091DF5358500A107A9 /* TZImageCropManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZImageCropManager.m; sourceTree = ""; }; 6DD050291DF659ED0057C78D /* TZProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZProgressView.h; sourceTree = ""; }; 6DD0502A1DF659ED0057C78D /* TZProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZProgressView.m; sourceTree = ""; }; 90020E1220F8840800D3AB10 /* TZImagePickerController.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = TZImagePickerController.podspec; sourceTree = SOURCE_ROOT; }; 90020E1420F8840E00D3AB10 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 9005E2B1257A462E002A8682 /* UIView+TZLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+TZLayout.m"; sourceTree = ""; }; 9005E2B2257A462E002A8682 /* UIView+TZLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+TZLayout.h"; sourceTree = ""; }; 900E657C1C2BB8D5003D9A9E /* TZImagePickerController.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TZImagePickerController.app; sourceTree = BUILT_PRODUCTS_DIR; }; 900E65801C2BB8D5003D9A9E /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 900E65821C2BB8D5003D9A9E /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 900E65831C2BB8D5003D9A9E /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 900E65851C2BB8D5003D9A9E /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 900E65861C2BB8D5003D9A9E /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 900E65891C2BB8D5003D9A9E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 900E658B1C2BB8D5003D9A9E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 900E658E1C2BB8D5003D9A9E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 900E65901C2BB8D5003D9A9E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 900E65951C2BB8D5003D9A9E /* TZImagePickerControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TZImagePickerControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 900E65991C2BB8D5003D9A9E /* TZImagePickerControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TZImagePickerControllerTests.m; sourceTree = ""; }; 900E659B1C2BB8D5003D9A9E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 900E65A01C2BB8D5003D9A9E /* TZImagePickerControllerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TZImagePickerControllerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 900E65A41C2BB8D5003D9A9E /* TZImagePickerControllerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TZImagePickerControllerUITests.m; sourceTree = ""; }; 900E65A61C2BB8D5003D9A9E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 900EEFF61C2BBF9500EA709B /* TZImagePickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZImagePickerController.h; sourceTree = ""; }; 900EEFF71C2BBF9500EA709B /* TZImagePickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZImagePickerController.m; sourceTree = ""; }; 900EEFFC1C2BD58B00EA709B /* TZPhotoPickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZPhotoPickerController.h; sourceTree = ""; }; 900EEFFD1C2BD58B00EA709B /* TZPhotoPickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZPhotoPickerController.m; sourceTree = ""; }; 900EEFFF1C2BD7E400EA709B /* TZAssetCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZAssetCell.h; sourceTree = ""; }; 900EF0001C2BD7E400EA709B /* TZAssetCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZAssetCell.m; sourceTree = ""; }; 900EF00D1C2C0B1100EA709B /* TZAssetModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZAssetModel.h; sourceTree = ""; }; 900EF00E1C2C0B1100EA709B /* TZAssetModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZAssetModel.m; sourceTree = ""; }; 900EF0101C2C107400EA709B /* TZPhotoPreviewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZPhotoPreviewController.h; sourceTree = ""; }; 900EF0111C2C107400EA709B /* TZPhotoPreviewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZPhotoPreviewController.m; sourceTree = ""; }; 900EF0131C2C134800EA709B /* TZPhotoPreviewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZPhotoPreviewCell.h; sourceTree = ""; }; 900EF0141C2C134900EA709B /* TZPhotoPreviewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZPhotoPreviewCell.m; sourceTree = ""; }; 9019FD8B21EC25D7009ADEAE /* TZImageUploadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZImageUploadOperation.h; sourceTree = ""; }; 9019FD8C21EC25D7009ADEAE /* TZImageUploadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZImageUploadOperation.m; sourceTree = ""; }; 901CC3F821CB757500C55443 /* TZImageRequestOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TZImageRequestOperation.h; sourceTree = ""; }; 901CC3F921CB757500C55443 /* TZImageRequestOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TZImageRequestOperation.m; sourceTree = ""; }; 901F2290215CABD600F604ED /* FLAnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLAnimatedImage.h; sourceTree = ""; }; 901F2291215CABD600F604ED /* FLAnimatedImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLAnimatedImage.m; sourceTree = ""; }; 901F2292215CABD600F604ED /* FLAnimatedImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLAnimatedImageView.h; sourceTree = ""; }; 901F2293215CABD600F604ED /* FLAnimatedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLAnimatedImageView.m; sourceTree = ""; }; 9038D58F1C3974F0007DE549 /* TZTestCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZTestCell.h; sourceTree = ""; }; 9038D5901C3974F0007DE549 /* TZTestCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZTestCell.m; sourceTree = ""; }; 903996921F447604005E77C2 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; 903996941F447609005E77C2 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 908D11562887ED7F00DD212E /* TZLocationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZLocationManager.m; sourceTree = ""; }; 908D11572887ED7F00DD212E /* TZLocationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZLocationManager.h; sourceTree = ""; }; 909DBC95216B981B00926570 /* ar-001 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ar-001"; path = "ar-001.lproj/Main.strings"; sourceTree = ""; }; 909DBC96216B981B00926570 /* ar-001 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ar-001"; path = "ar-001.lproj/LaunchScreen.strings"; sourceTree = ""; }; 909DBC97216B981B00926570 /* ar-001 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ar-001"; path = "../ar-001.lproj/Localizable.strings"; sourceTree = ""; }; 90A74B84203287C200D84C2A /* tz-ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tz-ru"; path = Localizable.strings; sourceTree = ""; }; 90CE84AC1C3A89EF003D0779 /* TZImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZImageManager.h; sourceTree = ""; }; 90CE84AD1C3A89EF003D0779 /* TZImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZImageManager.m; sourceTree = ""; }; 90CE84B51C3BABB6003D0779 /* TZVideoPlayerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZVideoPlayerController.h; sourceTree = ""; }; 90CE84B61C3BABB6003D0779 /* TZVideoPlayerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZVideoPlayerController.m; sourceTree = ""; }; 9F763A421FA071CF00D9E526 /* TZImagePickerController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TZImagePickerController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9F763A451FA071D000D9E526 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B606A9C2265FD53200F5493E /* TZVideoCropController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TZVideoCropController.m; sourceTree = ""; }; B606A9C3265FD53200F5493E /* TZVideoCropController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TZVideoCropController.h; sourceTree = ""; }; B662C36D2662914D006A672A /* TZVideoEditedPreviewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TZVideoEditedPreviewController.h; sourceTree = ""; }; B662C36E2662914D006A672A /* TZVideoEditedPreviewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TZVideoEditedPreviewController.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 900E65791C2BB8D5003D9A9E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 570A349026D72768000EEF2E /* PhotosUI.framework in Frameworks */, 903996931F447604005E77C2 /* Photos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 900E65921C2BB8D5003D9A9E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 900E659D1C2BB8D5003D9A9E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 9F763A3E1FA071CF00D9E526 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 570A348E26D72762000EEF2E /* PhotosUI.framework in Frameworks */, 570A348C26D7275C000EEF2E /* Photos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 5B7A0D4F288691E7002B2603 /* Location */ = { isa = PBXGroup; children = ( 908D11572887ED7F00DD212E /* TZLocationManager.h */, 908D11562887ED7F00DD212E /* TZLocationManager.m */, ); path = Location; sourceTree = ""; }; 6DC358641CC8BAE300898D29 /* Resources */ = { isa = PBXGroup; children = ( 6DC358651CC8BAFD00898D29 /* TZImagePickerController.bundle */, ); name = Resources; sourceTree = ""; }; 900E65731C2BB8D5003D9A9E = { isa = PBXGroup; children = ( 900E657E1C2BB8D5003D9A9E /* TZImagePickerController */, 900E65981C2BB8D5003D9A9E /* TZImagePickerControllerTests */, 900E65A31C2BB8D5003D9A9E /* TZImagePickerControllerUITests */, 9F763A431FA071D000D9E526 /* TZImagePickerControllerFramework */, 900E657D1C2BB8D5003D9A9E /* Products */, 903996911F447603005E77C2 /* Frameworks */, ); sourceTree = ""; }; 900E657D1C2BB8D5003D9A9E /* Products */ = { isa = PBXGroup; children = ( 900E657C1C2BB8D5003D9A9E /* TZImagePickerController.app */, 900E65951C2BB8D5003D9A9E /* TZImagePickerControllerTests.xctest */, 900E65A01C2BB8D5003D9A9E /* TZImagePickerControllerUITests.xctest */, 9F763A421FA071CF00D9E526 /* TZImagePickerController.framework */, ); name = Products; sourceTree = ""; }; 900E657E1C2BB8D5003D9A9E /* TZImagePickerController */ = { isa = PBXGroup; children = ( 5B7A0D4F288691E7002B2603 /* Location */, 901F228F215CABD600F604ED /* FLAnimatedImage */, 900EEFF51C2BBF7600EA709B /* TZImagePickerController */, 900E65821C2BB8D5003D9A9E /* AppDelegate.h */, 900E65831C2BB8D5003D9A9E /* AppDelegate.m */, 900E65851C2BB8D5003D9A9E /* ViewController.h */, 900E65861C2BB8D5003D9A9E /* ViewController.m */, 9019FD8B21EC25D7009ADEAE /* TZImageUploadOperation.h */, 9019FD8C21EC25D7009ADEAE /* TZImageUploadOperation.m */, 6D32B9BB1CD83640005CE1E0 /* LxGridViewFlowLayout.h */, 6D32B9BC1CD83640005CE1E0 /* LxGridViewFlowLayout.m */, 9038D58F1C3974F0007DE549 /* TZTestCell.h */, 9038D5901C3974F0007DE549 /* TZTestCell.m */, 90020E1420F8840E00D3AB10 /* README.md */, 90020E1220F8840800D3AB10 /* TZImagePickerController.podspec */, 900E65881C2BB8D5003D9A9E /* Main.storyboard */, 900E658B1C2BB8D5003D9A9E /* Assets.xcassets */, 900E658D1C2BB8D5003D9A9E /* LaunchScreen.storyboard */, 900E65901C2BB8D5003D9A9E /* Info.plist */, 900E657F1C2BB8D5003D9A9E /* Supporting Files */, ); path = TZImagePickerController; sourceTree = ""; }; 900E657F1C2BB8D5003D9A9E /* Supporting Files */ = { isa = PBXGroup; children = ( 90A74B82203287C200D84C2A /* tz-ru.lproj */, 900E65801C2BB8D5003D9A9E /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; 900E65981C2BB8D5003D9A9E /* TZImagePickerControllerTests */ = { isa = PBXGroup; children = ( 900E65991C2BB8D5003D9A9E /* TZImagePickerControllerTests.m */, 900E659B1C2BB8D5003D9A9E /* Info.plist */, ); path = TZImagePickerControllerTests; sourceTree = ""; }; 900E65A31C2BB8D5003D9A9E /* TZImagePickerControllerUITests */ = { isa = PBXGroup; children = ( 900E65A41C2BB8D5003D9A9E /* TZImagePickerControllerUITests.m */, 900E65A61C2BB8D5003D9A9E /* Info.plist */, ); path = TZImagePickerControllerUITests; sourceTree = ""; }; 900EEFF51C2BBF7600EA709B /* TZImagePickerController */ = { isa = PBXGroup; children = ( 900EEFF61C2BBF9500EA709B /* TZImagePickerController.h */, 900EEFF71C2BBF9500EA709B /* TZImagePickerController.m */, 900EEFFC1C2BD58B00EA709B /* TZPhotoPickerController.h */, 900EEFFD1C2BD58B00EA709B /* TZPhotoPickerController.m */, 900EEFFF1C2BD7E400EA709B /* TZAssetCell.h */, 900EF0001C2BD7E400EA709B /* TZAssetCell.m */, 900EF00D1C2C0B1100EA709B /* TZAssetModel.h */, 900EF00E1C2C0B1100EA709B /* TZAssetModel.m */, 900EF0101C2C107400EA709B /* TZPhotoPreviewController.h */, 900EF0111C2C107400EA709B /* TZPhotoPreviewController.m */, 900EF0131C2C134800EA709B /* TZPhotoPreviewCell.h */, 900EF0141C2C134900EA709B /* TZPhotoPreviewCell.m */, 90CE84B51C3BABB6003D0779 /* TZVideoPlayerController.h */, 90CE84B61C3BABB6003D0779 /* TZVideoPlayerController.m */, 6D46082F1DFFC60D004FB009 /* TZGifPhotoPreviewController.h */, 6D4608301DFFC60D004FB009 /* TZGifPhotoPreviewController.m */, B606A9C3265FD53200F5493E /* TZVideoCropController.h */, B606A9C2265FD53200F5493E /* TZVideoCropController.m */, B662C36D2662914D006A672A /* TZVideoEditedPreviewController.h */, B662C36E2662914D006A672A /* TZVideoEditedPreviewController.m */, 6DD050291DF659ED0057C78D /* TZProgressView.h */, 6DD0502A1DF659ED0057C78D /* TZProgressView.m */, 57DD378826D61592005090FF /* TZAuthLimitedFooterTipView.h */, 57DD378926D61592005090FF /* TZAuthLimitedFooterTipView.m */, 90CE84AC1C3A89EF003D0779 /* TZImageManager.h */, 90CE84AD1C3A89EF003D0779 /* TZImageManager.m */, 901CC3F821CB757500C55443 /* TZImageRequestOperation.h */, 901CC3F921CB757500C55443 /* TZImageRequestOperation.m */, 6DC84D081DF5358500A107A9 /* TZImageCropManager.h */, 6DC84D091DF5358500A107A9 /* TZImageCropManager.m */, 9005E2B2257A462E002A8682 /* UIView+TZLayout.h */, 9005E2B1257A462E002A8682 /* UIView+TZLayout.m */, 6D5358CB1D64600F00928CC6 /* NSBundle+TZImagePicker.h */, 6D5358CC1D64600F00928CC6 /* NSBundle+TZImagePicker.m */, 6DC358641CC8BAE300898D29 /* Resources */, ); path = TZImagePickerController; sourceTree = ""; }; 901F228F215CABD600F604ED /* FLAnimatedImage */ = { isa = PBXGroup; children = ( 901F2290215CABD600F604ED /* FLAnimatedImage.h */, 901F2291215CABD600F604ED /* FLAnimatedImage.m */, 901F2292215CABD600F604ED /* FLAnimatedImageView.h */, 901F2293215CABD600F604ED /* FLAnimatedImageView.m */, ); path = FLAnimatedImage; sourceTree = ""; }; 903996911F447603005E77C2 /* Frameworks */ = { isa = PBXGroup; children = ( 570A348D26D72762000EEF2E /* PhotosUI.framework */, 570A348F26D72768000EEF2E /* PhotosUI.framework */, 570A348B26D7275C000EEF2E /* Photos.framework */, 903996941F447609005E77C2 /* AssetsLibrary.framework */, 903996921F447604005E77C2 /* Photos.framework */, ); name = Frameworks; sourceTree = ""; }; 90A74B82203287C200D84C2A /* tz-ru.lproj */ = { isa = PBXGroup; children = ( 90A74B83203287C200D84C2A /* Localizable.strings */, ); path = "tz-ru.lproj"; sourceTree = ""; }; 9F763A431FA071D000D9E526 /* TZImagePickerControllerFramework */ = { isa = PBXGroup; children = ( 9F763A451FA071D000D9E526 /* Info.plist */, 441B78D22BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy */, ); path = TZImagePickerControllerFramework; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 9F763A3F1FA071CF00D9E526 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 901CC3FA21CB757500C55443 /* TZImageRequestOperation.h in Headers */, 9F763A6B1FA0731E00D9E526 /* TZImagePickerController.h in Headers */, 9F763A6D1FA0733700D9E526 /* NSBundle+TZImagePicker.h in Headers */, 9F763A6C1FA0733100D9E526 /* TZAssetModel.h in Headers */, 9F763A731FA0741D00D9E526 /* TZVideoPlayerController.h in Headers */, 9F763A741FA0741D00D9E526 /* TZGifPhotoPreviewController.h in Headers */, 9005E2B5257A462E002A8682 /* UIView+TZLayout.h in Headers */, 9F763A761FA0741D00D9E526 /* TZImageManager.h in Headers */, 908D115A2887ED8000DD212E /* TZLocationManager.h in Headers */, 9F763A711FA0741D00D9E526 /* TZPhotoPreviewController.h in Headers */, 9F763A721FA0741D00D9E526 /* TZPhotoPreviewCell.h in Headers */, 9F763A701FA0741D00D9E526 /* TZAssetCell.h in Headers */, 9F763A6F1FA0741D00D9E526 /* TZPhotoPickerController.h in Headers */, 9F763A751FA0741D00D9E526 /* TZProgressView.h in Headers */, 9F763A771FA0741D00D9E526 /* TZImageCropManager.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 900E657B1C2BB8D5003D9A9E /* TZImagePickerController */ = { isa = PBXNativeTarget; buildConfigurationList = 900E65A91C2BB8D5003D9A9E /* Build configuration list for PBXNativeTarget "TZImagePickerController" */; buildPhases = ( 900E65781C2BB8D5003D9A9E /* Sources */, 900E65791C2BB8D5003D9A9E /* Frameworks */, 900E657A1C2BB8D5003D9A9E /* Resources */, 9F763A4E1FA071D000D9E526 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( 9F763A481FA071D000D9E526 /* PBXTargetDependency */, ); name = TZImagePickerController; productName = TZImagePickerController; productReference = 900E657C1C2BB8D5003D9A9E /* TZImagePickerController.app */; productType = "com.apple.product-type.application"; }; 900E65941C2BB8D5003D9A9E /* TZImagePickerControllerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 900E65AC1C2BB8D5003D9A9E /* Build configuration list for PBXNativeTarget "TZImagePickerControllerTests" */; buildPhases = ( 900E65911C2BB8D5003D9A9E /* Sources */, 900E65921C2BB8D5003D9A9E /* Frameworks */, 900E65931C2BB8D5003D9A9E /* Resources */, ); buildRules = ( ); dependencies = ( 900E65971C2BB8D5003D9A9E /* PBXTargetDependency */, ); name = TZImagePickerControllerTests; productName = TZImagePickerControllerTests; productReference = 900E65951C2BB8D5003D9A9E /* TZImagePickerControllerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 900E659F1C2BB8D5003D9A9E /* TZImagePickerControllerUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 900E65AF1C2BB8D5003D9A9E /* Build configuration list for PBXNativeTarget "TZImagePickerControllerUITests" */; buildPhases = ( 900E659C1C2BB8D5003D9A9E /* Sources */, 900E659D1C2BB8D5003D9A9E /* Frameworks */, 900E659E1C2BB8D5003D9A9E /* Resources */, ); buildRules = ( ); dependencies = ( 900E65A21C2BB8D5003D9A9E /* PBXTargetDependency */, ); name = TZImagePickerControllerUITests; productName = TZImagePickerControllerUITests; productReference = 900E65A01C2BB8D5003D9A9E /* TZImagePickerControllerUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 9F763A411FA071CF00D9E526 /* TZImagePickerControllerFramework */ = { isa = PBXNativeTarget; buildConfigurationList = 9F763A4D1FA071D000D9E526 /* Build configuration list for PBXNativeTarget "TZImagePickerControllerFramework" */; buildPhases = ( 9F763A3D1FA071CF00D9E526 /* Sources */, 9F763A3E1FA071CF00D9E526 /* Frameworks */, 9F763A3F1FA071CF00D9E526 /* Headers */, 9F763A401FA071CF00D9E526 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = TZImagePickerControllerFramework; productName = TZImagePickerControllerFramework; productReference = 9F763A421FA071CF00D9E526 /* TZImagePickerController.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 900E65741C2BB8D5003D9A9E /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0930; ORGANIZATIONNAME = "谭真"; TargetAttributes = { 900E657B1C2BB8D5003D9A9E = { CreatedOnToolsVersion = 7.2; DevelopmentTeam = 3TA49P2Q58; ProvisioningStyle = Automatic; }; 900E65941C2BB8D5003D9A9E = { CreatedOnToolsVersion = 7.2; TestTargetID = 900E657B1C2BB8D5003D9A9E; }; 900E659F1C2BB8D5003D9A9E = { CreatedOnToolsVersion = 7.2; TestTargetID = 900E657B1C2BB8D5003D9A9E; }; 9F763A411FA071CF00D9E526 = { CreatedOnToolsVersion = 9.0.1; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = 900E65771C2BB8D5003D9A9E /* Build configuration list for PBXProject "TZImagePickerController" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, "zh-Hans", "tz-ru", "ar-001", ); mainGroup = 900E65731C2BB8D5003D9A9E; productRefGroup = 900E657D1C2BB8D5003D9A9E /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 900E657B1C2BB8D5003D9A9E /* TZImagePickerController */, 900E65941C2BB8D5003D9A9E /* TZImagePickerControllerTests */, 900E659F1C2BB8D5003D9A9E /* TZImagePickerControllerUITests */, 9F763A411FA071CF00D9E526 /* TZImagePickerControllerFramework */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 900E657A1C2BB8D5003D9A9E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 90020E1320F8840800D3AB10 /* TZImagePickerController.podspec in Resources */, 90A74B85203287C200D84C2A /* Localizable.strings in Resources */, 900E658F1C2BB8D5003D9A9E /* LaunchScreen.storyboard in Resources */, 90020E1520F8840F00D3AB10 /* README.md in Resources */, 6DC358661CC8BAFD00898D29 /* TZImagePickerController.bundle in Resources */, 900E658C1C2BB8D5003D9A9E /* Assets.xcassets in Resources */, 441B78D32BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */, 900E658A1C2BB8D5003D9A9E /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 900E65931C2BB8D5003D9A9E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 441B78D42BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 900E659E1C2BB8D5003D9A9E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 441B78D52BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 9F763A401FA071CF00D9E526 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 441B78D62BBD7E0300A1CB09 /* PrivacyInfo.xcprivacy in Resources */, 9F763A6E1FA0734800D9E526 /* TZImagePickerController.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 900E65781C2BB8D5003D9A9E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B606A9CE265FD5F700F5493E /* TZVideoPlayerController.m in Sources */, B662C36F2662914D006A672A /* TZVideoEditedPreviewController.m in Sources */, 9038D5911C3974F0007DE549 /* TZTestCell.m in Sources */, 900EF00F1C2C0B1100EA709B /* TZAssetModel.m in Sources */, 900EEFFE1C2BD58B00EA709B /* TZPhotoPickerController.m in Sources */, 900EF0021C2BD7E400EA709B /* TZAssetCell.m in Sources */, 6DD0502B1DF659ED0057C78D /* TZProgressView.m in Sources */, 6D32B9BD1CD83640005CE1E0 /* LxGridViewFlowLayout.m in Sources */, 57DD378A26D61592005090FF /* TZAuthLimitedFooterTipView.m in Sources */, 6D4608311DFFC60D004FB009 /* TZGifPhotoPreviewController.m in Sources */, 900EF0161C2C134900EA709B /* TZPhotoPreviewCell.m in Sources */, 900E65871C2BB8D5003D9A9E /* ViewController.m in Sources */, 90CE84AE1C3A89EF003D0779 /* TZImageManager.m in Sources */, 901F2295215CABD600F604ED /* FLAnimatedImage.m in Sources */, 908D11582887ED8000DD212E /* TZLocationManager.m in Sources */, 900E65841C2BB8D5003D9A9E /* AppDelegate.m in Sources */, 9005E2B3257A462E002A8682 /* UIView+TZLayout.m in Sources */, B606A9C4265FD53200F5493E /* TZVideoCropController.m in Sources */, 901F2296215CABD600F604ED /* FLAnimatedImageView.m in Sources */, 9019FD8D21EC25D7009ADEAE /* TZImageUploadOperation.m in Sources */, 900E65811C2BB8D5003D9A9E /* main.m in Sources */, 900EF0121C2C107400EA709B /* TZPhotoPreviewController.m in Sources */, 901CC3FC21CB758500C55443 /* TZImageRequestOperation.m in Sources */, 6D5358CD1D64600F00928CC6 /* NSBundle+TZImagePicker.m in Sources */, 900EEFF81C2BBF9500EA709B /* TZImagePickerController.m in Sources */, 6DC84D0A1DF5358500A107A9 /* TZImageCropManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 900E65911C2BB8D5003D9A9E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 900E659A1C2BB8D5003D9A9E /* TZImagePickerControllerTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 900E659C1C2BB8D5003D9A9E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 900E65A51C2BB8D5003D9A9E /* TZImagePickerControllerUITests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 9F763A3D1FA071CF00D9E526 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 57DD378E26D619C9005090FF /* TZAuthLimitedFooterTipView.m in Sources */, B662C37326629179006A672A /* TZVideoEditedPreviewController.m in Sources */, B6977B0A265FDB280028BA31 /* TZVideoCropController.m in Sources */, 901CC3FB21CB757500C55443 /* TZImageRequestOperation.m in Sources */, 9F763A501FA072E500D9E526 /* TZImagePickerController.m in Sources */, 9F763A521FA072E500D9E526 /* TZPhotoPickerController.m in Sources */, 9F763A541FA072E500D9E526 /* TZAssetCell.m in Sources */, 9F763A561FA072E500D9E526 /* TZAssetModel.m in Sources */, 9F763A581FA072E500D9E526 /* TZPhotoPreviewController.m in Sources */, 9005E2B4257A462E002A8682 /* UIView+TZLayout.m in Sources */, 9F763A5A1FA072E500D9E526 /* TZPhotoPreviewCell.m in Sources */, 9F763A5C1FA072E500D9E526 /* TZVideoPlayerController.m in Sources */, 9F763A5E1FA072E500D9E526 /* TZGifPhotoPreviewController.m in Sources */, 9F763A601FA072E500D9E526 /* TZProgressView.m in Sources */, 908D11592887ED8000DD212E /* TZLocationManager.m in Sources */, 9F763A621FA072E500D9E526 /* TZImageManager.m in Sources */, 9F763A641FA072E500D9E526 /* TZImageCropManager.m in Sources */, 9F763A6A1FA072E500D9E526 /* NSBundle+TZImagePicker.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 900E65971C2BB8D5003D9A9E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 900E657B1C2BB8D5003D9A9E /* TZImagePickerController */; targetProxy = 900E65961C2BB8D5003D9A9E /* PBXContainerItemProxy */; }; 900E65A21C2BB8D5003D9A9E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 900E657B1C2BB8D5003D9A9E /* TZImagePickerController */; targetProxy = 900E65A11C2BB8D5003D9A9E /* PBXContainerItemProxy */; }; 9F763A481FA071D000D9E526 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9F763A411FA071CF00D9E526 /* TZImagePickerControllerFramework */; targetProxy = 9F763A471FA071D000D9E526 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 900E65881C2BB8D5003D9A9E /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 900E65891C2BB8D5003D9A9E /* Base */, 6D12FC191D66B71E00182C44 /* zh-Hans */, 909DBC95216B981B00926570 /* ar-001 */, ); name = Main.storyboard; sourceTree = ""; }; 900E658D1C2BB8D5003D9A9E /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 900E658E1C2BB8D5003D9A9E /* Base */, 6D12FC1A1D66B71E00182C44 /* zh-Hans */, 909DBC96216B981B00926570 /* ar-001 */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; 90A74B83203287C200D84C2A /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( 90A74B84203287C200D84C2A /* tz-ru */, 909DBC97216B981B00926570 /* ar-001 */, ); name = Localizable.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 900E65A71C2BB8D5003D9A9E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.2; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; 900E65A81C2BB8D5003D9A9E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; 900E65AA1C2BB8D5003D9A9E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 3TA49P2Q58; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/TZImagePickerController", ); INFOPLIST_FILE = TZImagePickerController/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 3.8.12; PRODUCT_BUNDLE_IDENTIFIER = tanzhenios2025.TZImagePickerController.www; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 900E65AB1C2BB8D5003D9A9E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = LTFQDC2QVX; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/TZImagePickerController", ); INFOPLIST_FILE = TZImagePickerController/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 3.8.12; PRODUCT_BUNDLE_IDENTIFIER = tanzhenios2022.TZImagePickerController.www; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 900E65AD1C2BB8D5003D9A9E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = TZImagePickerControllerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = tanzhen.TZImagePickerControllerTests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TZImagePickerController.app/TZImagePickerController"; }; name = Debug; }; 900E65AE1C2BB8D5003D9A9E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = TZImagePickerControllerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = tanzhen.TZImagePickerControllerTests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TZImagePickerController.app/TZImagePickerController"; }; name = Release; }; 900E65B01C2BB8D5003D9A9E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = TZImagePickerControllerUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = tanzhen.TZImagePickerControllerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_TARGET_NAME = TZImagePickerController; USES_XCTRUNNER = YES; }; name = Debug; }; 900E65B11C2BB8D5003D9A9E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = TZImagePickerControllerUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = tanzhen.TZImagePickerControllerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_TARGET_NAME = TZImagePickerController; USES_XCTRUNNER = YES; }; name = Release; }; 9F763A4B1FA071D000D9E526 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = TZImagePickerControllerFramework/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 3.8.9; OTHER_LDFLAGS = "-all_load"; PRODUCT_BUNDLE_IDENTIFIER = tanzhen.TZImagePickerControllerFramework; PRODUCT_NAME = TZImagePickerController; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 9F763A4C1FA071D000D9E526 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = TZImagePickerControllerFramework/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 3.8.9; OTHER_LDFLAGS = "-all_load"; PRODUCT_BUNDLE_IDENTIFIER = tanzhen.TZImagePickerControllerFramework; PRODUCT_NAME = TZImagePickerController; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 900E65771C2BB8D5003D9A9E /* Build configuration list for PBXProject "TZImagePickerController" */ = { isa = XCConfigurationList; buildConfigurations = ( 900E65A71C2BB8D5003D9A9E /* Debug */, 900E65A81C2BB8D5003D9A9E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 900E65A91C2BB8D5003D9A9E /* Build configuration list for PBXNativeTarget "TZImagePickerController" */ = { isa = XCConfigurationList; buildConfigurations = ( 900E65AA1C2BB8D5003D9A9E /* Debug */, 900E65AB1C2BB8D5003D9A9E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 900E65AC1C2BB8D5003D9A9E /* Build configuration list for PBXNativeTarget "TZImagePickerControllerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 900E65AD1C2BB8D5003D9A9E /* Debug */, 900E65AE1C2BB8D5003D9A9E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 900E65AF1C2BB8D5003D9A9E /* Build configuration list for PBXNativeTarget "TZImagePickerControllerUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( 900E65B01C2BB8D5003D9A9E /* Debug */, 900E65B11C2BB8D5003D9A9E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 9F763A4D1FA071D000D9E526 /* Build configuration list for PBXNativeTarget "TZImagePickerControllerFramework" */ = { isa = XCConfigurationList; buildConfigurations = ( 9F763A4B1FA071D000D9E526 /* Debug */, 9F763A4C1FA071D000D9E526 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 900E65741C2BB8D5003D9A9E /* Project object */; } ================================================ FILE: TZImagePickerController.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: TZImagePickerController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: TZImagePickerController.xcodeproj/xcshareddata/xcschemes/TZImagePickerControllerFramework.xcscheme ================================================ ================================================ FILE: TZImagePickerControllerFramework/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: TZImagePickerControllerFramework/PrivacyInfo.xcprivacy ================================================ NSPrivacyAccessedAPITypes NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons 3B52.1 NSPrivacyCollectedDataTypes NSPrivacyTrackingDomains NSPrivacyTracking ================================================ FILE: TZImagePickerControllerTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: TZImagePickerControllerTests/TZImagePickerControllerTests.m ================================================ // // TZImagePickerControllerTests.m // TZImagePickerControllerTests // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @interface TZImagePickerControllerTests : XCTestCase @end @implementation TZImagePickerControllerTests - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: TZImagePickerControllerUITests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: TZImagePickerControllerUITests/TZImagePickerControllerUITests.m ================================================ // // TZImagePickerControllerUITests.m // TZImagePickerControllerUITests // // Created by 谭真 on 15/12/24. // Copyright © 2015年 谭真. All rights reserved. // #import @interface TZImagePickerControllerUITests : XCTestCase @end @implementation TZImagePickerControllerUITests - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. // In UI tests it is usually best to stop immediately when a failure occurs. self.continueAfterFailure = NO; // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. [[[XCUIApplication alloc] init] launch]; // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } - (void)testExample { // Use recording to get started writing UI tests. // Use XCTAssert and related functions to verify your tests produce the correct results. } @end