Skip to content

Commit

Permalink
Merge pull request #566 from leonspok/photo-library-freeze-fix
Browse files Browse the repository at this point in the history
Move initialization of PHPhotoLibrary and PHCachingImageManager to background
  • Loading branch information
leonspok authored Apr 3, 2019
2 parents 5995970 + 8813401 commit ac6fddf
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,44 +60,64 @@ private class PhotosInputDataProviderImageRequest: PhotosInputDataProviderImageR
@objc
final class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol, PHPhotoLibraryChangeObserver {
weak var delegate: PhotosInputDataProviderDelegate?
private var imageManager = PHCachingImageManager()
private var fetchResult: PHFetchResult<PHAsset>!
private var imageManager: PHCachingImageManager?
private var fetchResult: PHFetchResult<PHAsset>?
private var fullImageRequests = [PHAsset: PhotosInputDataProviderImageRequestProtocol]()
override init() {
func fetchOptions(_ predicate: NSPredicate?) -> PHFetchOptions {
let options = PHFetchOptions()
options.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ]
options.predicate = predicate
return options
}

if let userLibraryCollection = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil).firstObject {
self.fetchResult = PHAsset.fetchAssets(in: userLibraryCollection, options: fetchOptions(NSPredicate(format: "mediaType = \(PHAssetMediaType.image.rawValue)")))
} else {
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions(nil))
}
super.init()
PHPhotoLibrary.shared().register(self)
}

deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}

func prepare(_ completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else {
DispatchQueue.main.async(execute: completion)
return
}

func fetchOptions(_ predicate: NSPredicate?) -> PHFetchOptions {
let options = PHFetchOptions()
options.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ]
options.predicate = predicate
return options
}

let fetchResult: PHFetchResult<PHAsset> = {
if let userLibraryCollection = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil).firstObject {
return PHAsset.fetchAssets(in: userLibraryCollection, options: fetchOptions(NSPredicate(format: "mediaType = \(PHAssetMediaType.image.rawValue)")))
} else {
return PHAsset.fetchAssets(with: .image, options: fetchOptions(nil))
}
}()
let imageManager = PHCachingImageManager()
PHPhotoLibrary.shared().register(self)

DispatchQueue.main.async(execute: { [weak self] in
self?.fetchResult = fetchResult
self?.imageManager = imageManager
completion()
})
}
}

var count: Int {
return self.fetchResult.count
return self.fetchResult?.count ?? 0
}

func requestPreviewImage(at index: Int,
targetSize: CGSize,
completion: @escaping PhotosInputDataProviderCompletion) -> PhotosInputDataProviderImageRequestProtocol {
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
let asset = self.fetchResult[index]
guard let fetchResult = self.fetchResult, let imageManager = self.imageManager else {
assertionFailure("PhotosInputDataProvider is not prepared")
return PhotosInputDataProviderImageRequest()
}
assert(index >= 0 && index < self.count, "Index out of bounds")
let asset = fetchResult[index]
let request = PhotosInputDataProviderImageRequest()
request.observeProgress(with: nil, completion: completion)
let options = self.makePreviewRequestOptions()
var requestId: Int32 = -1
requestId = self.imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
requestId = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
let result: PhotosInputDataProviderResult
if let image = image {
result = .success(image)
Expand All @@ -106,8 +126,8 @@ final class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol,
}
request.handleCompletion(with: result)
}
request.cancelBlock = { [weak self] in
self?.imageManager.cancelImageRequest(requestId)
request.cancelBlock = { [weak imageManager] in
imageManager?.cancelImageRequest(requestId)
}
request.requestId = requestId
return request
Expand All @@ -116,11 +136,15 @@ final class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol,
func requestFullImage(at index: Int,
progressHandler: PhotosInputDataProviderProgressHandler?,
completion: @escaping PhotosInputDataProviderCompletion) -> PhotosInputDataProviderImageRequestProtocol {
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
guard let fetchResult = self.fetchResult, let imageManager = self.imageManager else {
assertionFailure("PhotosInputDataProvider is not prepared")
return PhotosInputDataProviderImageRequest()
}
assert(index >= 0 && index < self.count, "Index out of bounds")
if let existedRequest = self.fullImageRequest(at: index) {
return existedRequest
} else {
let asset = self.fetchResult[index]
let asset = fetchResult[index]
let request = PhotosInputDataProviderImageRequest()
request.observeProgress(with: progressHandler, completion: completion)
let options = self.makeFullImageRequestOptions()
Expand All @@ -131,7 +155,7 @@ final class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol,
}
var requestId: Int32 = -1
self.fullImageRequests[asset] = request
requestId = self.imageManager.requestImageData(for: asset, options: options, resultHandler: { [weak self] (data, _, _, info) in
requestId = imageManager.requestImageData(for: asset, options: options, resultHandler: { [weak self] (data, _, _, info) in
guard let sSelf = self else { return }
let result: PhotosInputDataProviderResult
if let data = data, let image = UIImage(data: data) {
Expand All @@ -152,14 +176,22 @@ final class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol,
}

func fullImageRequest(at index: Int) -> PhotosInputDataProviderImageRequestProtocol? {
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
let asset = self.fetchResult[index]
guard let fetchResult = self.fetchResult else {
assertionFailure("PhotosInputDataProvider is not prepared")
return nil
}
assert(index >= 0 && index < self.count, "Index out of bounds")
let asset = fetchResult[index]
return self.fullImageRequests[asset]
}

func cancelFullImageRequest(_ request: PhotosInputDataProviderImageRequestProtocol) {
guard let imageManager = self.imageManager else {
assertionFailure("PhotosInputDataProvider is not prepared")
return
}
assert(Thread.isMainThread, "Cancel function is called not on Main Thread. It's not a thread-safe.")
self.imageManager.cancelImageRequest(request.requestId)
imageManager.cancelImageRequest(request.requestId)
if let assetAndRequestPair = self.fullImageRequests.first(where: { $0.value === request }) {
self.fullImageRequests[assetAndRequestPair.key] = nil
}
Expand All @@ -183,9 +215,9 @@ final class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol,
func photoLibraryDidChange(_ changeInstance: PHChange) {
// Photos may call this method on a background queue; switch to the main queue to update the UI.
DispatchQueue.main.async { [weak self] in
guard let sSelf = self else { return }
guard let sSelf = self, let fetchResult = sSelf.fetchResult else { return }

if let changeDetails = changeInstance.changeDetails(for: sSelf.fetchResult) {
if let changeDetails = changeInstance.changeDetails(for: fetchResult) {
let updateBlock = { () -> Void in
self?.fetchResult = changeDetails.fetchResultAfterChanges
}
Expand Down
19 changes: 12 additions & 7 deletions ChattoAdditions/Source/Input/Photos/PhotosInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,20 @@ public final class PhotosInputView: UIView, PhotosInputViewProtocol {
}

private func replacePlaceholderItemsWithPhotoItems() {
self.collectionViewQueue.addTask { [weak self] (completion) in
let photosDataProvider = PhotosInputDataProvider()
photosDataProvider.prepare { [weak self] in
guard let sSelf = self else { return }

let newDataProvider = PhotosInputWithPlaceholdersDataProvider(photosDataProvider: PhotosInputDataProvider(), placeholdersDataProvider: PhotosInputPlaceholderDataProvider())
newDataProvider.delegate = sSelf
sSelf.dataProvider = newDataProvider
sSelf.cellProvider = PhotosInputCellProvider(collectionView: sSelf.collectionView, dataProvider: newDataProvider)
sSelf.collectionView.reloadData()
DispatchQueue.main.async(execute: completion)
sSelf.collectionViewQueue.addTask { [weak self] (completion) in
guard let sSelf = self else { return }

let newDataProvider = PhotosInputWithPlaceholdersDataProvider(photosDataProvider: photosDataProvider, placeholdersDataProvider: PhotosInputPlaceholderDataProvider())
newDataProvider.delegate = sSelf
sSelf.dataProvider = newDataProvider
sSelf.cellProvider = PhotosInputCellProvider(collectionView: sSelf.collectionView, dataProvider: newDataProvider)
sSelf.collectionView.reloadData()
DispatchQueue.main.async(execute: completion)
}
}
}

Expand Down

0 comments on commit ac6fddf

Please sign in to comment.