TLPhotosPickerViewController.swift 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. //
  2. // TLPhotosPickerViewController.swift
  3. // TLPhotosPicker
  4. //
  5. // Created by wade.hawk on 2017. 4. 14..
  6. // Copyright © 2017년 wade.hawk. All rights reserved.
  7. //
  8. import UIKit
  9. import Photos
  10. import PhotosUI
  11. import MobileCoreServices
  12. public protocol TLPhotosPickerViewControllerDelegate: class {
  13. func dismissPhotoPicker(withPHAssets: [PHAsset])
  14. func dismissPhotoPicker(withTLPHAssets: [TLPHAsset])
  15. func dismissComplete()
  16. func photoPickerDidCancel()
  17. func canSelectAsset(phAsset: PHAsset) -> Bool
  18. func didExceedMaximumNumberOfSelection(picker: TLPhotosPickerViewController)
  19. func handleNoAlbumPermissions(picker: TLPhotosPickerViewController)
  20. func handleNoCameraPermissions(picker: TLPhotosPickerViewController)
  21. }
  22. extension TLPhotosPickerViewControllerDelegate {
  23. public func deninedAuthoization() { }
  24. public func dismissPhotoPicker(withPHAssets: [PHAsset]) { }
  25. public func dismissPhotoPicker(withTLPHAssets: [TLPHAsset]) { }
  26. public func dismissComplete() { }
  27. public func photoPickerDidCancel() { }
  28. public func canSelectAsset(phAsset: PHAsset) -> Bool { return true }
  29. public func didExceedMaximumNumberOfSelection(picker: TLPhotosPickerViewController) { }
  30. public func handleNoAlbumPermissions(picker: TLPhotosPickerViewController) { }
  31. public func handleNoCameraPermissions(picker: TLPhotosPickerViewController) { }
  32. }
  33. //for log
  34. public protocol TLPhotosPickerLogDelegate: class {
  35. func selectedCameraCell(picker: TLPhotosPickerViewController)
  36. func deselectedPhoto(picker: TLPhotosPickerViewController, at: Int)
  37. func selectedPhoto(picker: TLPhotosPickerViewController, at: Int)
  38. func selectedAlbum(picker: TLPhotosPickerViewController, title: String, at: Int)
  39. }
  40. extension TLPhotosPickerLogDelegate {
  41. func selectedCameraCell(picker: TLPhotosPickerViewController) { }
  42. func deselectedPhoto(picker: TLPhotosPickerViewController, at: Int) { }
  43. func selectedPhoto(picker: TLPhotosPickerViewController, at: Int) { }
  44. func selectedAlbum(picker: TLPhotosPickerViewController, collections: [TLAssetsCollection], at: Int) { }
  45. }
  46. public struct TLPhotosPickerConfigure {
  47. public var defaultCameraRollTitle = "Camera Roll"
  48. public var tapHereToChange = "Tap here to change"
  49. public var cancelTitle = "Cancel"
  50. public var doneTitle = "Done"
  51. public var emptyMessage = "No albums"
  52. public var emptyImage: UIImage? = nil
  53. public var usedCameraButton = true
  54. public var usedPrefetch = false
  55. public var allowedLivePhotos = true
  56. public var allowedVideo = true
  57. public var allowedAlbumCloudShared = false
  58. public var allowedVideoRecording = true
  59. public var recordingVideoQuality: UIImagePickerController.QualityType = .typeMedium
  60. public var maxVideoDuration:TimeInterval? = nil
  61. public var autoPlay = true
  62. public var muteAudio = true
  63. public var mediaType: PHAssetMediaType? = nil
  64. public var numberOfColumn = 3
  65. public var singleSelectedMode = false
  66. public var maxSelectedAssets: Int? = nil
  67. public var fetchOption: PHFetchOptions? = nil
  68. public var selectedColor = UIColor(red: 88/255, green: 144/255, blue: 255/255, alpha: 1.0)
  69. public var cameraBgColor = UIColor(red: 221/255, green: 223/255, blue: 226/255, alpha: 1)
  70. public var cameraIcon = TLBundle.podBundleImage(named: "camera")
  71. public var videoIcon = TLBundle.podBundleImage(named: "video")
  72. public var placeholderIcon = TLBundle.podBundleImage(named: "insertPhotoMaterial")
  73. public var nibSet: (nibName: String, bundle:Bundle)? = nil
  74. public var cameraCellNibSet: (nibName: String, bundle:Bundle)? = nil
  75. public var fetchCollectionTypes: [(PHAssetCollectionType,PHAssetCollectionSubtype)]? = nil
  76. public init() {
  77. }
  78. }
  79. public struct Platform {
  80. public static var isSimulator: Bool {
  81. return TARGET_OS_SIMULATOR != 0 // Use this line in Xcode 7 or newer
  82. }
  83. }
  84. open class TLPhotosPickerViewController: UIViewController {
  85. @IBOutlet open var titleView: UIView!
  86. @IBOutlet open var titleLabel: UILabel!
  87. @IBOutlet open var subTitleStackView: UIStackView!
  88. @IBOutlet open var subTitleLabel: UILabel!
  89. @IBOutlet open var subTitleArrowImageView: UIImageView!
  90. @IBOutlet open var albumPopView: TLAlbumPopView!
  91. @IBOutlet open var collectionView: UICollectionView!
  92. @IBOutlet open var indicator: UIActivityIndicatorView!
  93. @IBOutlet open var popArrowImageView: UIImageView!
  94. @IBOutlet open var customNavItem: UINavigationItem!
  95. @IBOutlet open var doneButton: UIBarButtonItem!
  96. @IBOutlet open var cancelButton: UIBarButtonItem!
  97. @IBOutlet open var navigationBarTopConstraint: NSLayoutConstraint!
  98. @IBOutlet open var emptyView: UIView!
  99. @IBOutlet open var emptyImageView: UIImageView!
  100. @IBOutlet open var emptyMessageLabel: UILabel!
  101. public weak var delegate: TLPhotosPickerViewControllerDelegate? = nil
  102. public weak var logDelegate: TLPhotosPickerLogDelegate? = nil
  103. public var selectedAssets = [TLPHAsset]()
  104. public var configure = TLPhotosPickerConfigure()
  105. fileprivate var usedCameraButton: Bool {
  106. get {
  107. return self.configure.usedCameraButton
  108. }
  109. }
  110. fileprivate var allowedVideo: Bool {
  111. get {
  112. return self.configure.allowedVideo
  113. }
  114. }
  115. fileprivate var usedPrefetch: Bool {
  116. get {
  117. return self.configure.usedPrefetch
  118. }
  119. set {
  120. self.configure.usedPrefetch = newValue
  121. }
  122. }
  123. fileprivate var allowedLivePhotos: Bool {
  124. get {
  125. return self.configure.allowedLivePhotos
  126. }
  127. set {
  128. self.configure.allowedLivePhotos = newValue
  129. }
  130. }
  131. @objc open var canSelectAsset: ((PHAsset) -> Bool)? = nil
  132. @objc open var didExceedMaximumNumberOfSelection: ((TLPhotosPickerViewController) -> Void)? = nil
  133. @objc open var handleNoAlbumPermissions: ((TLPhotosPickerViewController) -> Void)? = nil
  134. @objc open var handleNoCameraPermissions: ((TLPhotosPickerViewController) -> Void)? = nil
  135. @objc open var dismissCompletion: (() -> Void)? = nil
  136. fileprivate var completionWithPHAssets: (([PHAsset]) -> Void)? = nil
  137. fileprivate var completionWithTLPHAssets: (([TLPHAsset]) -> Void)? = nil
  138. fileprivate var didCancel: (() -> Void)? = nil
  139. fileprivate var collections = [TLAssetsCollection]()
  140. fileprivate var focusedCollection: TLAssetsCollection? = nil
  141. fileprivate var requestIds = [IndexPath:PHImageRequestID]()
  142. fileprivate var playRequestId: (indexPath: IndexPath, requestId: PHImageRequestID)? = nil
  143. fileprivate var photoLibrary = TLPhotoLibrary()
  144. fileprivate var queue = DispatchQueue(label: "tilltue.photos.pikcker.queue")
  145. fileprivate var thumbnailSize = CGSize.zero
  146. fileprivate var placeholderThumbnail: UIImage? = nil
  147. fileprivate var cameraImage: UIImage? = nil
  148. deinit {
  149. //print("deinit TLPhotosPickerViewController")
  150. PHPhotoLibrary.shared().unregisterChangeObserver(self)
  151. }
  152. required public init?(coder aDecoder: NSCoder) {
  153. fatalError("init(coder:) has not been implemented")
  154. }
  155. public init() {
  156. super.init(nibName: "TLPhotosPickerViewController", bundle: Bundle(for: TLPhotosPickerViewController.self))
  157. }
  158. @objc convenience public init(withPHAssets: (([PHAsset]) -> Void)? = nil, didCancel: (() -> Void)? = nil) {
  159. self.init()
  160. self.completionWithPHAssets = withPHAssets
  161. self.didCancel = didCancel
  162. }
  163. convenience public init(withTLPHAssets: (([TLPHAsset]) -> Void)? = nil, didCancel: (() -> Void)? = nil) {
  164. self.init()
  165. self.completionWithTLPHAssets = withTLPHAssets
  166. self.didCancel = didCancel
  167. }
  168. override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
  169. return UIInterfaceOrientationMask.portrait
  170. }
  171. override open func didReceiveMemoryWarning() {
  172. super.didReceiveMemoryWarning()
  173. self.stopPlay()
  174. }
  175. func checkAuthorization() {
  176. switch PHPhotoLibrary.authorizationStatus() {
  177. case .notDetermined:
  178. PHPhotoLibrary.requestAuthorization { [weak self] status in
  179. switch status {
  180. case .authorized:
  181. self?.initPhotoLibrary()
  182. default:
  183. self?.handleDeniedAlbumsAuthorization()
  184. }
  185. }
  186. case .authorized:
  187. self.initPhotoLibrary()
  188. case .restricted: fallthrough
  189. case .denied:
  190. handleDeniedAlbumsAuthorization()
  191. }
  192. }
  193. override open func viewDidLoad() {
  194. super.viewDidLoad()
  195. makeUI()
  196. checkAuthorization()
  197. }
  198. override open func viewDidLayoutSubviews() {
  199. super.viewDidLayoutSubviews()
  200. if self.thumbnailSize == CGSize.zero {
  201. initItemSize()
  202. }
  203. if #available(iOS 11.0, *) {
  204. } else if self.navigationBarTopConstraint.constant == 0 {
  205. self.navigationBarTopConstraint.constant = 20
  206. }
  207. }
  208. override open func viewWillAppear(_ animated: Bool) {
  209. super.viewWillAppear(animated)
  210. if self.photoLibrary.delegate == nil {
  211. initPhotoLibrary()
  212. }
  213. }
  214. }
  215. // MARK: - UI & UI Action
  216. extension TLPhotosPickerViewController {
  217. @objc public func registerNib(nibName: String, bundle: Bundle) {
  218. self.collectionView.register(UINib(nibName: nibName, bundle: bundle), forCellWithReuseIdentifier: nibName)
  219. }
  220. fileprivate func centerAtRect(image: UIImage?, rect: CGRect, bgColor: UIColor = UIColor.white) -> UIImage? {
  221. guard let image = image else { return nil }
  222. UIGraphicsBeginImageContextWithOptions(rect.size, false, image.scale)
  223. bgColor.setFill()
  224. UIRectFill(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
  225. image.draw(in: CGRect(x:rect.size.width/2 - image.size.width/2, y:rect.size.height/2 - image.size.height/2, width:image.size.width, height:image.size.height))
  226. let result = UIGraphicsGetImageFromCurrentImageContext()
  227. UIGraphicsEndImageContext()
  228. return result
  229. }
  230. fileprivate func initItemSize() {
  231. guard let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
  232. let count = CGFloat(self.configure.numberOfColumn)
  233. let width = (self.view.frame.size.width-(5*(count-1)))/count
  234. self.thumbnailSize = CGSize(width: width, height: width)
  235. layout.itemSize = self.thumbnailSize
  236. self.collectionView.collectionViewLayout = layout
  237. self.placeholderThumbnail = centerAtRect(image: self.configure.placeholderIcon, rect: CGRect(x: 0, y: 0, width: width, height: width))
  238. self.cameraImage = centerAtRect(image: self.configure.cameraIcon, rect: CGRect(x: 0, y: 0, width: width, height: width), bgColor: self.configure.cameraBgColor)
  239. }
  240. @objc open func makeUI() {
  241. registerNib(nibName: "TLPhotoCollectionViewCell", bundle: Bundle(for: TLPhotoCollectionViewCell.self))
  242. if let nibSet = self.configure.nibSet {
  243. registerNib(nibName: nibSet.nibName, bundle: nibSet.bundle)
  244. }
  245. if let nibSet = self.configure.cameraCellNibSet {
  246. registerNib(nibName: nibSet.nibName, bundle: nibSet.bundle)
  247. }
  248. self.indicator.startAnimating()
  249. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(titleTap))
  250. self.titleView.addGestureRecognizer(tapGesture)
  251. self.titleLabel.text = self.configure.defaultCameraRollTitle
  252. self.subTitleLabel.text = self.configure.tapHereToChange
  253. self.cancelButton.title = self.configure.cancelTitle
  254. self.doneButton.title = self.configure.doneTitle
  255. self.doneButton.setTitleTextAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.labelFontSize)], for: .normal)
  256. self.emptyView.isHidden = true
  257. self.emptyImageView.image = self.configure.emptyImage
  258. self.emptyMessageLabel.text = self.configure.emptyMessage
  259. self.albumPopView.tableView.delegate = self
  260. self.albumPopView.tableView.dataSource = self
  261. self.popArrowImageView.image = TLBundle.podBundleImage(named: "pop_arrow")
  262. self.subTitleArrowImageView.image = TLBundle.podBundleImage(named: "arrow")
  263. if #available(iOS 10.0, *), self.usedPrefetch {
  264. self.collectionView.isPrefetchingEnabled = true
  265. self.collectionView.prefetchDataSource = self
  266. } else {
  267. self.usedPrefetch = false
  268. }
  269. if #available(iOS 9.0, *), self.allowedLivePhotos {
  270. }else {
  271. self.allowedLivePhotos = false
  272. }
  273. }
  274. fileprivate func updateTitle() {
  275. guard self.focusedCollection != nil else { return }
  276. self.titleLabel.text = self.focusedCollection?.title
  277. }
  278. fileprivate func reloadCollectionView() {
  279. guard self.focusedCollection != nil else { return }
  280. self.collectionView.reloadData()
  281. }
  282. fileprivate func reloadTableView() {
  283. let count = min(5, self.collections.count)
  284. var frame = self.albumPopView.popupView.frame
  285. frame.size.height = CGFloat(count * 75)
  286. self.albumPopView.popupViewHeight.constant = CGFloat(count * 75)
  287. UIView.animate(withDuration: self.albumPopView.show ? 0.1:0) {
  288. self.albumPopView.popupView.frame = frame
  289. self.albumPopView.setNeedsLayout()
  290. }
  291. self.albumPopView.tableView.reloadData()
  292. self.albumPopView.setupPopupFrame()
  293. }
  294. fileprivate func initPhotoLibrary() {
  295. if PHPhotoLibrary.authorizationStatus() == .authorized {
  296. self.photoLibrary.delegate = self
  297. self.photoLibrary.fetchCollection(configure: self.configure)
  298. }else{
  299. //self.dismiss(animated: true, completion: nil)
  300. }
  301. }
  302. fileprivate func registerChangeObserver() {
  303. PHPhotoLibrary.shared().register(self)
  304. }
  305. fileprivate func getfocusedIndex() -> Int {
  306. guard let focused = self.focusedCollection, let result = self.collections.index(where: { $0 == focused }) else { return 0 }
  307. return result
  308. }
  309. fileprivate func focused(collection: TLAssetsCollection) {
  310. func resetRequest() {
  311. cancelAllImageAssets()
  312. }
  313. resetRequest()
  314. self.collections[getfocusedIndex()].recentPosition = self.collectionView.contentOffset
  315. var reloadIndexPaths = [IndexPath(row: getfocusedIndex(), section: 0)]
  316. self.focusedCollection = collection
  317. self.focusedCollection?.fetchResult = self.photoLibrary.fetchResult(collection: collection, configure: self.configure)
  318. reloadIndexPaths.append(IndexPath(row: getfocusedIndex(), section: 0))
  319. self.albumPopView.tableView.reloadRows(at: reloadIndexPaths, with: .none)
  320. self.albumPopView.show(false, duration: 0.2)
  321. self.updateTitle()
  322. self.reloadCollectionView()
  323. self.collectionView.contentOffset = collection.recentPosition
  324. }
  325. fileprivate func cancelAllImageAssets() {
  326. for (_,requestId) in self.requestIds {
  327. self.photoLibrary.cancelPHImageRequest(requestId: requestId)
  328. }
  329. self.requestIds.removeAll()
  330. }
  331. // User Action
  332. @objc func titleTap() {
  333. guard collections.count > 0 else { return }
  334. self.albumPopView.show(self.albumPopView.isHidden)
  335. }
  336. @IBAction open func cancelButtonTap() {
  337. self.stopPlay()
  338. self.dismiss(done: false)
  339. }
  340. @IBAction open func doneButtonTap() {
  341. self.stopPlay()
  342. self.dismiss(done: true)
  343. }
  344. fileprivate func dismiss(done: Bool) {
  345. if done {
  346. #if swift(>=4.1)
  347. self.delegate?.dismissPhotoPicker(withPHAssets: self.selectedAssets.compactMap{ $0.phAsset })
  348. #else
  349. self.delegate?.dismissPhotoPicker(withPHAssets: self.selectedAssets.flatMap{ $0.phAsset })
  350. #endif
  351. self.delegate?.dismissPhotoPicker(withTLPHAssets: self.selectedAssets)
  352. self.completionWithTLPHAssets?(self.selectedAssets)
  353. #if swift(>=4.1)
  354. self.completionWithPHAssets?(self.selectedAssets.compactMap{ $0.phAsset })
  355. #else
  356. self.completionWithPHAssets?(self.selectedAssets.flatMap{ $0.phAsset })
  357. #endif
  358. }else {
  359. self.delegate?.photoPickerDidCancel()
  360. self.didCancel?()
  361. }
  362. self.dismiss(animated: true) { [weak self] in
  363. self?.delegate?.dismissComplete()
  364. self?.dismissCompletion?()
  365. }
  366. }
  367. fileprivate func canSelect(phAsset: PHAsset) -> Bool {
  368. if let closure = self.canSelectAsset {
  369. return closure(phAsset)
  370. }else if let delegate = self.delegate {
  371. return delegate.canSelectAsset(phAsset: phAsset)
  372. }
  373. return true
  374. }
  375. fileprivate func maxCheck() -> Bool {
  376. if self.configure.singleSelectedMode {
  377. self.selectedAssets.removeAll()
  378. self.orderUpdateCells()
  379. }
  380. if let max = self.configure.maxSelectedAssets, max <= self.selectedAssets.count {
  381. self.delegate?.didExceedMaximumNumberOfSelection(picker: self)
  382. self.didExceedMaximumNumberOfSelection?(self)
  383. return true
  384. }
  385. return false
  386. }
  387. fileprivate func focusFirstCollection() {
  388. if self.focusedCollection == nil, let collection = self.collections.first {
  389. self.focusedCollection = collection
  390. self.updateTitle()
  391. self.reloadCollectionView()
  392. }
  393. }
  394. }
  395. // MARK: - TLPhotoLibraryDelegate
  396. extension TLPhotosPickerViewController: TLPhotoLibraryDelegate {
  397. func loadCameraRollCollection(collection: TLAssetsCollection) {
  398. self.collections = [collection]
  399. self.focusFirstCollection()
  400. self.indicator.stopAnimating()
  401. self.reloadCollectionView()
  402. self.reloadTableView()
  403. }
  404. func loadCompleteAllCollection(collections: [TLAssetsCollection]) {
  405. self.collections = collections
  406. self.focusFirstCollection()
  407. let isEmpty = self.collections.count == 0
  408. self.subTitleStackView.isHidden = isEmpty
  409. self.emptyView.isHidden = !isEmpty
  410. self.emptyImageView.isHidden = self.emptyImageView.image == nil
  411. self.indicator.stopAnimating()
  412. self.reloadTableView()
  413. self.registerChangeObserver()
  414. }
  415. }
  416. // MARK: - Camera Picker
  417. extension TLPhotosPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  418. fileprivate func showCameraIfAuthorized() {
  419. let cameraAuthorization = AVCaptureDevice.authorizationStatus(for: .video)
  420. switch cameraAuthorization {
  421. case .authorized:
  422. self.showCamera()
  423. case .notDetermined:
  424. AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] (authorized) in
  425. DispatchQueue.main.async { [weak self] in
  426. if authorized {
  427. self?.showCamera()
  428. } else {
  429. self?.handleDeniedCameraAuthorization()
  430. }
  431. }
  432. })
  433. case .restricted, .denied:
  434. self.handleDeniedCameraAuthorization()
  435. }
  436. }
  437. fileprivate func showCamera() {
  438. guard !maxCheck() else { return }
  439. let picker = UIImagePickerController()
  440. picker.sourceType = .camera
  441. picker.mediaTypes = [kUTTypeImage as String]
  442. if self.configure.allowedVideoRecording {
  443. picker.mediaTypes.append(kUTTypeMovie as String)
  444. picker.videoQuality = self.configure.recordingVideoQuality
  445. if let duration = self.configure.maxVideoDuration {
  446. picker.videoMaximumDuration = duration
  447. }
  448. }
  449. picker.allowsEditing = false
  450. picker.delegate = self
  451. self.present(picker, animated: true, completion: nil)
  452. }
  453. fileprivate func handleDeniedAlbumsAuthorization() {
  454. self.delegate?.handleNoAlbumPermissions(picker: self)
  455. self.handleNoAlbumPermissions?(self)
  456. }
  457. fileprivate func handleDeniedCameraAuthorization() {
  458. self.delegate?.handleNoCameraPermissions(picker: self)
  459. self.handleNoCameraPermissions?(self)
  460. }
  461. open func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  462. picker.dismiss(animated: true, completion: nil)
  463. }
  464. open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
  465. if let image = (info[.originalImage] as? UIImage) {
  466. var placeholderAsset: PHObjectPlaceholder? = nil
  467. PHPhotoLibrary.shared().performChanges({
  468. let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
  469. placeholderAsset = newAssetRequest.placeholderForCreatedAsset
  470. }, completionHandler: { [weak self] (sucess, error) in
  471. if sucess, let `self` = self, let identifier = placeholderAsset?.localIdentifier {
  472. guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else { return }
  473. var result = TLPHAsset(asset: asset)
  474. result.selectedOrder = self.selectedAssets.count + 1
  475. self.selectedAssets.append(result)
  476. self.logDelegate?.selectedPhoto(picker: self, at: 1)
  477. }
  478. })
  479. }
  480. else if (info[.mediaType] as? String) == kUTTypeMovie as String {
  481. var placeholderAsset: PHObjectPlaceholder? = nil
  482. PHPhotoLibrary.shared().performChanges({
  483. let newAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: info[.mediaURL] as! URL)
  484. placeholderAsset = newAssetRequest?.placeholderForCreatedAsset
  485. }) { [weak self] (sucess, error) in
  486. if sucess, let `self` = self, let identifier = placeholderAsset?.localIdentifier {
  487. guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else { return }
  488. var result = TLPHAsset(asset: asset)
  489. result.selectedOrder = self.selectedAssets.count + 1
  490. self.selectedAssets.append(result)
  491. self.logDelegate?.selectedPhoto(picker: self, at: 1)
  492. }
  493. }
  494. }
  495. picker.dismiss(animated: true, completion: nil)
  496. }
  497. }
  498. // MARK: - UICollectionView Scroll Delegate
  499. extension TLPhotosPickerViewController {
  500. open func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  501. if !decelerate {
  502. videoCheck()
  503. }
  504. }
  505. open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  506. videoCheck()
  507. }
  508. fileprivate func videoCheck() {
  509. func play(asset: (IndexPath,TLPHAsset)) {
  510. if self.playRequestId?.indexPath != asset.0 {
  511. playVideo(asset: asset.1, indexPath: asset.0)
  512. }
  513. }
  514. guard self.configure.autoPlay else { return }
  515. guard self.playRequestId == nil else { return }
  516. let visibleIndexPaths = self.collectionView.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row })
  517. #if swift(>=4.1)
  518. let boundAssets = visibleIndexPaths.compactMap{ indexPath -> (IndexPath,TLPHAsset)? in
  519. guard let asset = self.focusedCollection?.getTLAsset(at: indexPath.row),asset.phAsset?.mediaType == .video else { return nil }
  520. return (indexPath,asset)
  521. }
  522. #else
  523. let boundAssets = visibleIndexPaths.flatMap{ indexPath -> (IndexPath,TLPHAsset)? in
  524. guard let asset = self.focusedCollection?.getTLAsset(at: indexPath.row),asset.phAsset?.mediaType == .video else { return nil }
  525. return (indexPath,asset)
  526. }
  527. #endif
  528. if let firstSelectedVideoAsset = (boundAssets.filter{ getSelectedAssets($0.1) != nil }.first) {
  529. play(asset: firstSelectedVideoAsset)
  530. }else if let firstVideoAsset = boundAssets.first {
  531. play(asset: firstVideoAsset)
  532. }
  533. }
  534. }
  535. // MARK: - Video & LivePhotos Control PHLivePhotoViewDelegate
  536. extension TLPhotosPickerViewController: PHLivePhotoViewDelegate {
  537. fileprivate func stopPlay() {
  538. guard let playRequest = self.playRequestId else { return }
  539. self.playRequestId = nil
  540. guard let cell = self.collectionView.cellForItem(at: playRequest.indexPath) as? TLPhotoCollectionViewCell else { return }
  541. cell.stopPlay()
  542. }
  543. fileprivate func playVideo(asset: TLPHAsset, indexPath: IndexPath) {
  544. stopPlay()
  545. guard let phAsset = asset.phAsset else { return }
  546. if asset.type == .video {
  547. guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return }
  548. let requestId = self.photoLibrary.videoAsset(asset: phAsset, completionBlock: { (playerItem, info) in
  549. DispatchQueue.main.sync { [weak self, weak cell] in
  550. guard let `self` = self, let cell = cell, cell.player == nil else { return }
  551. let player = AVPlayer(playerItem: playerItem)
  552. cell.player = player
  553. player.play()
  554. player.isMuted = self.configure.muteAudio
  555. }
  556. })
  557. if requestId > 0 {
  558. self.playRequestId = (indexPath,requestId)
  559. }
  560. }else if asset.type == .livePhoto {
  561. guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return }
  562. let requestId = self.photoLibrary.livePhotoAsset(asset: phAsset, size: self.thumbnailSize, completionBlock: { [weak cell] (livePhoto,complete) in
  563. cell?.livePhotoView?.isHidden = false
  564. cell?.livePhotoView?.livePhoto = livePhoto
  565. cell?.livePhotoView?.isMuted = true
  566. cell?.livePhotoView?.startPlayback(with: .hint)
  567. })
  568. if requestId > 0 {
  569. self.playRequestId = (indexPath,requestId)
  570. }
  571. }
  572. }
  573. public func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
  574. livePhotoView.isMuted = true
  575. livePhotoView.startPlayback(with: .hint)
  576. }
  577. public func livePhotoView(_ livePhotoView: PHLivePhotoView, willBeginPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
  578. }
  579. }
  580. // MARK: - PHPhotoLibraryChangeObserver
  581. extension TLPhotosPickerViewController: PHPhotoLibraryChangeObserver {
  582. public func photoLibraryDidChange(_ changeInstance: PHChange) {
  583. guard getfocusedIndex() == 0 else { return }
  584. let addIndex = self.usedCameraButton ? 1 : 0
  585. DispatchQueue.main.sync {
  586. guard let changeFetchResult = self.focusedCollection?.fetchResult else { return }
  587. guard let changes = changeInstance.changeDetails(for: changeFetchResult) else { return }
  588. if changes.hasIncrementalChanges {
  589. var deletedSelectedAssets = false
  590. var order = 0
  591. #if swift(>=4.1)
  592. self.selectedAssets = self.selectedAssets.enumerated().compactMap({ (offset,asset) -> TLPHAsset? in
  593. var asset = asset
  594. if let phAsset = asset.phAsset, changes.fetchResultAfterChanges.contains(phAsset) {
  595. order += 1
  596. asset.selectedOrder = order
  597. return asset
  598. }
  599. deletedSelectedAssets = true
  600. return nil
  601. })
  602. #else
  603. self.selectedAssets = self.selectedAssets.enumerated().flatMap({ (offset,asset) -> TLPHAsset? in
  604. var asset = asset
  605. if let phAsset = asset.phAsset, changes.fetchResultAfterChanges.contains(phAsset) {
  606. order += 1
  607. asset.selectedOrder = order
  608. return asset
  609. }
  610. deletedSelectedAssets = true
  611. return nil
  612. })
  613. #endif
  614. if deletedSelectedAssets {
  615. self.focusedCollection?.fetchResult = changes.fetchResultAfterChanges
  616. self.collectionView.reloadData()
  617. }else {
  618. self.collectionView.performBatchUpdates({ [weak self] in
  619. guard let `self` = self else { return }
  620. self.focusedCollection?.fetchResult = changes.fetchResultAfterChanges
  621. if let removed = changes.removedIndexes, removed.count > 0 {
  622. self.collectionView.deleteItems(at: removed.map { IndexPath(item: $0+addIndex, section:0) })
  623. }
  624. if let inserted = changes.insertedIndexes, inserted.count > 0 {
  625. self.collectionView.insertItems(at: inserted.map { IndexPath(item: $0+addIndex, section:0) })
  626. }
  627. changes.enumerateMoves { fromIndex, toIndex in
  628. self.collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
  629. to: IndexPath(item: toIndex, section: 0))
  630. }
  631. }, completion: { [weak self] (completed) in
  632. guard let `self` = self else { return }
  633. if completed {
  634. if let changed = changes.changedIndexes, changed.count > 0 {
  635. self.collectionView.reloadItems(at: changed.map { IndexPath(item: $0+addIndex, section:0) })
  636. }
  637. }
  638. })
  639. }
  640. }else {
  641. self.focusedCollection?.fetchResult = changes.fetchResultAfterChanges
  642. self.collectionView.reloadData()
  643. }
  644. if let collection = self.focusedCollection {
  645. self.collections[getfocusedIndex()] = collection
  646. self.albumPopView.tableView.reloadRows(at: [IndexPath(row: getfocusedIndex(), section: 0)], with: .none)
  647. }
  648. }
  649. }
  650. }
  651. // MARK: - UICollectionView delegate & datasource
  652. extension TLPhotosPickerViewController: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDataSourcePrefetching {
  653. fileprivate func getSelectedAssets(_ asset: TLPHAsset) -> TLPHAsset? {
  654. if let index = self.selectedAssets.index(where: { $0.phAsset == asset.phAsset }) {
  655. return self.selectedAssets[index]
  656. }
  657. return nil
  658. }
  659. fileprivate func orderUpdateCells() {
  660. let visibleIndexPaths = self.collectionView.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row })
  661. for indexPath in visibleIndexPaths {
  662. guard let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { continue }
  663. guard let asset = self.focusedCollection?.getTLAsset(at: indexPath.row) else { continue }
  664. if let selectedAsset = getSelectedAssets(asset) {
  665. cell.selectedAsset = true
  666. cell.orderLabel?.text = "\(selectedAsset.selectedOrder)"
  667. }else {
  668. cell.selectedAsset = false
  669. }
  670. }
  671. }
  672. //Delegate
  673. open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  674. guard let collection = self.focusedCollection, let cell = self.collectionView.cellForItem(at: indexPath) as? TLPhotoCollectionViewCell else { return }
  675. if collection.useCameraButton && indexPath.row == 0 {
  676. if Platform.isSimulator {
  677. print("not supported by the simulator.")
  678. return
  679. }else {
  680. if self.configure.cameraCellNibSet?.nibName != nil {
  681. cell.selectedCell()
  682. }else {
  683. showCameraIfAuthorized()
  684. }
  685. self.logDelegate?.selectedCameraCell(picker: self)
  686. return
  687. }
  688. }
  689. guard var asset = collection.getTLAsset(at: indexPath.row), let phAsset = asset.phAsset else { return }
  690. cell.popScaleAnim()
  691. if let index = self.selectedAssets.index(where: { $0.phAsset == asset.phAsset }) {
  692. //deselect
  693. self.logDelegate?.deselectedPhoto(picker: self, at: indexPath.row)
  694. self.selectedAssets.remove(at: index)
  695. #if swift(>=4.1)
  696. self.selectedAssets = self.selectedAssets.enumerated().compactMap({ (offset,asset) -> TLPHAsset? in
  697. var asset = asset
  698. asset.selectedOrder = offset + 1
  699. return asset
  700. })
  701. #else
  702. self.selectedAssets = self.selectedAssets.enumerated().flatMap({ (offset,asset) -> TLPHAsset? in
  703. var asset = asset
  704. asset.selectedOrder = offset + 1
  705. return asset
  706. })
  707. #endif
  708. cell.selectedAsset = false
  709. cell.stopPlay()
  710. self.orderUpdateCells()
  711. if self.playRequestId?.indexPath == indexPath {
  712. stopPlay()
  713. }
  714. }else {
  715. //select
  716. self.logDelegate?.selectedPhoto(picker: self, at: indexPath.row)
  717. guard !maxCheck() else { return }
  718. guard canSelect(phAsset: phAsset) else { return }
  719. asset.selectedOrder = self.selectedAssets.count + 1
  720. self.selectedAssets.append(asset)
  721. cell.selectedAsset = true
  722. cell.orderLabel?.text = "\(asset.selectedOrder)"
  723. if asset.type != .photo, self.configure.autoPlay {
  724. playVideo(asset: asset, indexPath: indexPath)
  725. }
  726. }
  727. }
  728. open func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  729. if let cell = cell as? TLPhotoCollectionViewCell {
  730. cell.endDisplayingCell()
  731. cell.stopPlay()
  732. if indexPath == self.playRequestId?.indexPath {
  733. self.playRequestId = nil
  734. }
  735. }
  736. guard let requestId = self.requestIds[indexPath] else { return }
  737. self.requestIds.removeValue(forKey: indexPath)
  738. self.photoLibrary.cancelPHImageRequest(requestId: requestId)
  739. }
  740. //Datasource
  741. open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  742. func makeCell(nibName: String) -> TLPhotoCollectionViewCell {
  743. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: nibName, for: indexPath) as! TLPhotoCollectionViewCell
  744. cell.configure = self.configure
  745. cell.imageView?.image = self.placeholderThumbnail
  746. cell.liveBadgeImageView = nil
  747. return cell
  748. }
  749. let nibName = self.configure.nibSet?.nibName ?? "TLPhotoCollectionViewCell"
  750. var cell = makeCell(nibName: nibName)
  751. guard let collection = self.focusedCollection else { return cell }
  752. cell.isCameraCell = collection.useCameraButton && indexPath.row == 0
  753. if cell.isCameraCell {
  754. if let nibName = self.configure.cameraCellNibSet?.nibName {
  755. cell = makeCell(nibName: nibName)
  756. }else{
  757. cell.imageView?.image = self.cameraImage
  758. }
  759. cell.willDisplayCell()
  760. return cell
  761. }
  762. guard let asset = collection.getTLAsset(at: indexPath.row) else { return cell }
  763. if let selectedAsset = getSelectedAssets(asset) {
  764. cell.selectedAsset = true
  765. cell.orderLabel?.text = "\(selectedAsset.selectedOrder)"
  766. }else{
  767. cell.selectedAsset = false
  768. }
  769. if asset.state == .progress {
  770. cell.indicator?.startAnimating()
  771. }else {
  772. cell.indicator?.stopAnimating()
  773. }
  774. if let phAsset = asset.phAsset {
  775. if self.usedPrefetch {
  776. let options = PHImageRequestOptions()
  777. options.deliveryMode = .opportunistic
  778. options.resizeMode = .exact
  779. options.isNetworkAccessAllowed = true
  780. let requestId = self.photoLibrary.imageAsset(asset: phAsset, size: self.thumbnailSize, options: options) { [weak self, weak cell] (image,complete) in
  781. guard let `self` = self else { return }
  782. DispatchQueue.main.async {
  783. if self.requestIds[indexPath] != nil {
  784. cell?.imageView?.image = image
  785. cell?.update(with: phAsset)
  786. if self.allowedVideo {
  787. cell?.durationView?.isHidden = asset.type != .video
  788. cell?.duration = asset.type == .video ? phAsset.duration : nil
  789. }
  790. if complete {
  791. self.requestIds.removeValue(forKey: indexPath)
  792. }
  793. }
  794. }
  795. }
  796. if requestId > 0 {
  797. self.requestIds[indexPath] = requestId
  798. }
  799. }else {
  800. queue.async { [weak self, weak cell] in
  801. guard let `self` = self else { return }
  802. let requestId = self.photoLibrary.imageAsset(asset: phAsset, size: self.thumbnailSize, completionBlock: { (image,complete) in
  803. DispatchQueue.main.async {
  804. if self.requestIds[indexPath] != nil {
  805. cell?.imageView?.image = image
  806. cell?.update(with: phAsset)
  807. if self.allowedVideo {
  808. cell?.durationView?.isHidden = asset.type != .video
  809. cell?.duration = asset.type == .video ? phAsset.duration : nil
  810. }
  811. if complete {
  812. self.requestIds.removeValue(forKey: indexPath)
  813. }
  814. }
  815. }
  816. })
  817. if requestId > 0 {
  818. self.requestIds[indexPath] = requestId
  819. }
  820. }
  821. }
  822. if self.allowedLivePhotos {
  823. cell.liveBadgeImageView?.image = asset.type == .livePhoto ? PHLivePhotoView.livePhotoBadgeImage(options: .overContent) : nil
  824. cell.livePhotoView?.delegate = asset.type == .livePhoto ? self : nil
  825. }
  826. }
  827. cell.alpha = 0
  828. UIView.transition(with: cell, duration: 0.1, options: .curveEaseIn, animations: {
  829. cell.alpha = 1
  830. }, completion: nil)
  831. return cell
  832. }
  833. open func numberOfSections(in collectionView: UICollectionView) -> Int {
  834. return 1
  835. }
  836. open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  837. guard let collection = self.focusedCollection else { return 0 }
  838. return collection.count
  839. }
  840. //Prefetch
  841. open func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
  842. if self.usedPrefetch {
  843. queue.async { [weak self] in
  844. guard let `self` = self, let collection = self.focusedCollection else { return }
  845. var assets = [PHAsset]()
  846. for indexPath in indexPaths {
  847. if let asset = collection.getAsset(at: indexPath.row) {
  848. assets.append(asset)
  849. }
  850. }
  851. let scale = max(UIScreen.main.scale,2)
  852. let targetSize = CGSize(width: self.thumbnailSize.width*scale, height: self.thumbnailSize.height*scale)
  853. self.photoLibrary.imageManager.startCachingImages(for: assets, targetSize: targetSize, contentMode: .aspectFill, options: nil)
  854. }
  855. }
  856. }
  857. open func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
  858. if self.usedPrefetch {
  859. for indexPath in indexPaths {
  860. guard let requestId = self.requestIds[indexPath] else { continue }
  861. self.photoLibrary.cancelPHImageRequest(requestId: requestId)
  862. self.requestIds.removeValue(forKey: indexPath)
  863. }
  864. queue.async { [weak self] in
  865. guard let `self` = self, let collection = self.focusedCollection else { return }
  866. var assets = [PHAsset]()
  867. for indexPath in indexPaths {
  868. if let asset = collection.getAsset(at: indexPath.row) {
  869. assets.append(asset)
  870. }
  871. }
  872. let scale = max(UIScreen.main.scale,2)
  873. let targetSize = CGSize(width: self.thumbnailSize.width*scale, height: self.thumbnailSize.height*scale)
  874. self.photoLibrary.imageManager.stopCachingImages(for: assets, targetSize: targetSize, contentMode: .aspectFill, options: nil)
  875. }
  876. }
  877. }
  878. public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  879. if self.usedPrefetch, let cell = cell as? TLPhotoCollectionViewCell, let collection = self.focusedCollection, let asset = collection.getTLAsset(at: indexPath.row) {
  880. if let selectedAsset = getSelectedAssets(asset) {
  881. cell.selectedAsset = true
  882. cell.orderLabel?.text = "\(selectedAsset.selectedOrder)"
  883. }else{
  884. cell.selectedAsset = false
  885. }
  886. }
  887. }
  888. }
  889. // MARK: - UITableView datasource & delegate
  890. extension TLPhotosPickerViewController: UITableViewDelegate,UITableViewDataSource {
  891. //delegate
  892. open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  893. self.logDelegate?.selectedAlbum(picker: self, title: self.collections[indexPath.row].title, at: indexPath.row)
  894. self.focused(collection: self.collections[indexPath.row])
  895. }
  896. //datasource
  897. open func numberOfSections(in tableView: UITableView) -> Int {
  898. return 1
  899. }
  900. open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  901. return self.collections.count
  902. }
  903. open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  904. let cell = tableView.dequeueReusableCell(withIdentifier: "TLCollectionTableViewCell", for: indexPath) as! TLCollectionTableViewCell
  905. let collection = self.collections[indexPath.row]
  906. cell.titleLabel.text = collection.title
  907. cell.subTitleLabel.text = "\(collection.fetchResult?.count ?? 0)"
  908. if let phAsset = collection.getAsset(at: collection.useCameraButton ? 1 : 0) {
  909. let scale = UIScreen.main.scale
  910. let size = CGSize(width: 80*scale, height: 80*scale)
  911. self.photoLibrary.imageAsset(asset: phAsset, size: size, completionBlock: { [weak cell] (image,complete) in
  912. DispatchQueue.main.async {
  913. cell?.thumbImageView.image = image
  914. }
  915. })
  916. }
  917. cell.accessoryType = getfocusedIndex() == indexPath.row ? .checkmark : .none
  918. cell.selectionStyle = .none
  919. return cell
  920. }
  921. }