Skip to content

Commit

Permalink
Updated to Swift 2 and add Objective-C
Browse files Browse the repository at this point in the history
  • Loading branch information
mickmaccallum committed Sep 15, 2015
1 parent 31db86c commit 4222708
Showing 1 changed file with 182 additions and 32 deletions.
214 changes: 182 additions & 32 deletions 2014-09-16-phimagemanager.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ author: Mattt Thompson
category: Cocoa
excerpt: "Yesterday's article described various techniques for resizing images using APIs from the UIKit, Core Graphics, Core Image, and Image I/O frameworks. However, that article failed to mention some rather extraordinary functionality baked into the new Photos framework which takes care of all of this for you."
status:
swift: 1.0
reviewed: January 28, 2015
swift: 2.0
reviewed: September 15, 2015
---

[Yesterday's article](http://nshipster.com/image-resizing/) described various techniques for resizing images using APIs from the UIKit, Core Graphics, Core Image, and Image I/O frameworks. However, that article failed to mention some rather extraordinary functionality baked into the new Photos framework which takes care of all of this for you.
Expand All @@ -25,10 +25,12 @@ A great example of this is `PHImageManager`, which acts as a centralized coordin
But first, here's a simple example of how a table view might asynchronously load cell images with asset thumbnails:

~~~{swift}
import Photos
var assets: [PHAsset]
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let manager = PHImageManager.defaultManager()
Expand All @@ -38,19 +40,63 @@ func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexP
let asset = assets[indexPath.row]
cell.textLabel?.text = NSDateFormatter.localizedStringFromDate(asset.creationDate, dateStyle: .MediumStyle, timeStyle: .MediumStyle)
if let creationDate = asset.creationDate {
cell.textLabel?.text = NSDateFormatter.localizedStringFromDate(creationDate,
dateStyle: .MediumStyle,
timeStyle: .MediumStyle
)
} else {
cell.textLabel?.text = nil
}
cell.tag = Int(manager.requestImageForAsset(asset, targetSize: CGSize(width: 100.0, height: 100.0), contentMode: .AspectFill, options: nil) { (result, _) in
// this result handler is called on the main thread for asynchronous requests
if let cell = tableView.cellForRowAtIndexPath(indexPath) {
cell.tag = Int(manager.requestImageForAsset(asset,
targetSize: CGSize(width: 100.0, height: 100.0),
contentMode: .AspectFill,
options: nil) { (result, _) in
cell.imageView?.image = result
}
})
return cell
}
~~~

~~~{objective-c}
@import Photos;
@property (nonatomic, strong) NSArray<PHAsset *> *assets;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"
forIndexPath:indexPath];
PHImageManager *manager = [PHImageManager defaultManager];
if (cell.tag) {
[manager cancelImageRequest:(PHImageRequestID)cell.tag];
}
PHAsset *asset = self.assets[indexPath.row];
if (asset.creationDate) {
cell.textLabel.text = [NSDateFormatter localizedStringFromDate:asset.creationDate
dateStyle:NSDateFormatterMediumStyle
timeStyle:NSDateFormatterMediumStyle];
} else {
cell.textLabel.text = nil;
}
cell.tag = [manager requestImageForAsset:asset
targetSize:CGSizeMake(100.0, 100.0)
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
cell.imageView.image = result;
}];
return cell;
}
~~~

API usage is pretty straightforward: the `defaultManager` asynchronously requests an image for the asset corresponding to the cell at a particular index path, and the cell image view is set whenever the result comes back. The only tricky part is handling cell reuse—(1) before assigning the resulting image to the cell's image view, we call `cellForRowAtIndexPath` to be sure we're working with the right cell, and (2) we use the cell's `tag` to keep track of image requests, in order to cancel any pending requests when a cell is reused.

## Batch Pre-Caching Asset Images
Expand All @@ -68,16 +114,42 @@ options.sortDescriptors = [
NSSortDescriptor(key: "creationDate", ascending: true)
]
if let results = PHAsset.fetchAssetsWithMediaType(.Image, options: options) {
var assets: [PHAsset] = []
results.enumerateObjectsUsingBlock { (object, idx, _) in
if let asset = object as? PHAsset {
assets.append(asset)
}
let results = PHAsset.fetchAssetsWithMediaType(.Image, options: options)
var assets: [PHAsset] = []
results.enumerateObjectsUsingBlock { (object, _, _) in
if let asset = object as? PHAsset {
assets.append(asset)
}
cachingImageManager.startCachingImagesForAssets(assets, targetSize: PHImageManagerMaximumSize, contentMode: .AspectFit, options: nil)
}
cachingImageManager.startCachingImagesForAssets(assets,
targetSize: PHImageManagerMaximumSize,
contentMode: .AspectFit,
options: nil
)
~~~

~~~{objective-c}
PHCachingImageManager *cachingImageManager = [[PHCachingImageManager alloc] init];
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"favorite == YES"];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"" ascending:YES]];
PHFetchResult *results = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage
options:nil];
NSMutableArray<PHAsset *> *assets = [[NSMutableArray alloc] init];
[results enumerateObjectsUsingBlock:^(id _Nonnull object, NSUInteger idx, BOOL * _Nonnull stop) {
if ([object isKindOfClass:[PHAsset class]]) {
[assets addObject:object];
}
}];
[cachingImageManager startCachingImagesForAssets:assets
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeAspectFit
options:nil];
~~~

Alternatively, Swift `willSet` / `didSet` hooks offer a convenient way to automatically start pre-caching assets as they are loaded:
Expand All @@ -90,7 +162,11 @@ var assets: [PHAsset] = [] {
}
didSet {
cachingImageManager.startCachingImagesForAssets(self.assets, targetSize: PHImageManagerMaximumSize, contentMode: .AspectFit, options: nil)
cachingImageManager.startCachingImagesForAssets(self.assets,
targetSize: PHImageManagerMaximumSize,
contentMode: .AspectFit,
options: nil
)
}
}
~~~
Expand Down Expand Up @@ -137,27 +213,101 @@ override func viewDidLoad() {
initialRequestOptions.resizeMode = .Fast
initialRequestOptions.deliveryMode = .FastFormat
manager.requestImageForAsset(asset, targetSize: CGSize(width: 250.0, height: 250.0), contentMode: .AspectFit, options: initialRequestOptions) { (initialResult, _) in
let finalRequestOptions = PHImageRequestOptions()
finalRequestOptions.progressHandler = { (progress, _, _, _) in
self.progressView.progress = Float(progress)
}
manager.requestImageForAsset(asset,
targetSize: CGSize(width: 250.0, height: 250.0),
contentMode: .AspectFit,
options: initialRequestOptions) { (initialResult, _) in
guard let ciImage = initialResult?.CIImage else {
return
}
let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyLow])
let features = detector.featuresInImage(CIImage(CGImage: initialResult.CGImage)) as [CIFeature]
if features.count > 0 {
var rect = CGRectZero
for feature in features {
rect = CGRectUnion(rect, feature.bounds)
let finalRequestOptions = PHImageRequestOptions()
finalRequestOptions.progressHandler = { (progress, _, _, _) in
self.progressView.progress = Float(progress)
}
finalRequestOptions.normalizedCropRect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1.0 / initialResult.size.width, 1.0 / initialResult.size.height))
let detector = CIDetector(
ofType: CIDetectorTypeFace,
context: nil,
options: [CIDetectorAccuracy: CIDetectorAccuracyLow]
)
let features = detector.featuresInImage(ciImage)
if features.count > 0 {
var rect = CGRectZero
features.forEach {
rect.unionInPlace($0.bounds)
}
let transform = CGAffineTransformMakeScale(1.0 / initialResult!.size.width, 1.0 / initialResult!.size.height)
finalRequestOptions.normalizedCropRect = CGRectApplyAffineTransform(rect, transform)
}
manager.requestImageForAsset(self.asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .AspectFit,
options: finalRequestOptions) { (finalResult, _) in
self.imageView.image = finalResult
}
}
}
~~~

~~~{objective-c}
@property (nonatomic, strong) PHAsset *asset;
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, weak) IBOutlet UIProgressView *progressView;
- (void)viewDidLoad {
[super viewDidLoad];
PHImageManager *manager = [PHImageManager defaultManager];
PHImageRequestOptions *initialRequestOptions = [[PHImageRequestOptions alloc] init];
initialRequestOptions.synchronous = true;
initialRequestOptions.resizeMode = PHImageRequestOptionsResizeModeFast;
initialRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat;
void (^resultHandler)(UIImage *, NSDictionary *) = ^(UIImage * _Nullable initialResult, NSDictionary * _Nullable info) {
if (!initialResult.CIImage) {
return;
}
manager.requestImageForAsset(asset, targetSize: PHImageManagerMaximumSize, contentMode: .AspectFit, options: finalRequestOptions) { (finalResult, _) in
self.imageView.image = finalResult
PHImageRequestOptions *finalRequestOptions = [[PHImageRequestOptions alloc] init];
finalRequestOptions.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
self.progressView.progress = progress;
};
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeFace
context:nil
options:@{CIDetectorAccuracy : CIDetectorAccuracyLow}];
NSArray<CIFeature *> *features = [detector featuresInImage:initialResult.CIImage];
if (features.count) {
CGRect rect;
for (CIFeature *feature in features) {
CGRectUnion(rect, feature.bounds);
}
CGAffineTransform transform = CGAffineTransformMakeScale(1.0 / initialResult.size.width, 1.0 / initialResult.size.height);
finalRequestOptions.normalizedCropRect = CGRectApplyAffineTransform(rect, transform);
}
}
[manager requestImageForAsset:self.asset
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeAspectFit
options:finalRequestOptions
resultHandler:^(UIImage * _Nullable finalResult, NSDictionary * _Nullable info) {
self.imageView.image = finalResult;
}];
};
// typedef void (^ PHAssetImageProgressHandler)(double progress, NSError *__nullable error, BOOL *stop, NSDictionary *__nullable info) NS_AVAILABLE_IOS(8_0);
[manager requestImageForAsset:self.asset
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeAspectFit
options:initialRequestOptions
resultHandler:resultHandler];
}
~~~

Expand Down

0 comments on commit 4222708

Please sign in to comment.