ALAssets groups enumeration: how to check for completion? - iphone

ALAssets use a separated thread to manage enumeration, i have to know when enumeration terminate.
The block prototype for group enumeration is :
typedef void (^ALAssetsLibraryGroupsEnumerationResultsBlock)(ALAssetsGroup *group, BOOL *stop);
How can i add a completion block ?

I found a solution that is documented only in part.
When group enumeration is terminated, ALAssetsLibraryGroupsEnumerationResultsBlock is invoked with group=nil. So you can write something like:
void (^groupsEnumerator)(ALAssetsGroup *,BOOL *) = ^(ALAssetsGroup *group, BOOL *stop){
if (group != nil) {
[group enumerateAssetsUsingBlock:assetsEnumerator];
}else {
NSLog(#"group enumeration terminated");
}
};
The same solution is valid for assets enumeration (this is not documented -.- )
void (^assetsEnumerator)(ALAsset *,NSUInteger,BOOL*) = ^(ALAsset *result, NSUInteger index, BOOL *stop){
if (result !=nil) {
//do something with result asset
}else {
NSLog(#"Assets enumeration terminated");
}
};

I'm using this:
[group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result == nil) {
return;
}
if (index + 1 == group.numberOfAssets) {
//Do what you want. Im using delegate to notify my parent class about finish.
[delegate didGroupEnumerated:group];
}
}];

Related

Get last 10 photos from iPhone using AssetLibrary, Corelocation deprecation issue on ios 6.1.3

App tries to access photos from phone library. Works well on ipod (5.1.something) iPhone5 (6.1.4), all simulators, but crashes on iPhone 4S(6.1.3).
All checks (location services, photo library access) are there w.r.t ios version.
Console LOG:
: libMobileGestalt copySystemVersionDictionaryValue: Could not lookup ReleaseType from system version dictionary
Jul 31 12:03:00 ABC's-iPhone awdd[296] : CoreLocation: CLClient is deprecated. Will be obsolete soon.
BTW below code fetches last 10 photos from the photo library. If exist. Before calling this method check for location is made using [CLLocationManager authorizationStatus].
- (void) getRecentPhotos
{
if(! oneTimeFetch) // to prevent location delegate from calling this method.
{
oneTimeFetch = TRUE;
NSLog(#"getRecentPhotos");
recenPicScroll.userInteractionEnabled = FALSE;
[self.recentPicsArr removeAllObjects];
if ([[[UIDevice currentDevice] systemVersion] doubleValue] >= 6.0)
{
NSLog(#"IOS version 6.0 and above, need to check for photo access");
if ([ALAssetsLibrary authorizationStatus] == ALAuthorizationStatusAuthorized)
{
NSLog(#"Photo ACCESS ALLOWED");
// just execute the code after loop ends. Else return :).
}
else
{
recentPicView.hidden = TRUE;
NSLog(#"PLEASE ALLLOW PHOTO ACCESS");
return;
}
}
recentPicView.hidden = FALSE;
loadingAI.hidden = FALSE;
ALAssetsLibrary *al = [[ALAssetsLibrary alloc] init];
[al enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
usingBlock:^(ALAssetsGroup *group, BOOL *stop)
{
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
[group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stop)
{
if([group numberOfAssets] == 0)
{
recentPicView.hidden = TRUE;
return;
}
int startIdx = [group numberOfAssets]- 10; // Start from 10th last
if (asset)
{
//NSLog(#"asset Index: %d",index);
ALAssetRepresentation *rep = [asset defaultRepresentation];
CGImageRef imgRef = [rep fullResolutionImage];
if(group.numberOfAssets > 10) // upto 10
{
if(index >= startIdx)
{
[self.recentPicsArr addObject:[UIImage imageWithCGImage:imgRef]];
if(index == [group numberOfAssets] - 1)
{
[self addPicsToScrollV];
}
}
}
else if (group.numberOfAssets <= 10) // get less than 10 photos
{
[self.recentPicsArr addObject:[UIImage imageWithCGImage:imgRef]];
if(index + 1 == group.numberOfAssets)
{
[self addPicsToScrollV];
}
}
else
{
recentPicView.hidden = TRUE;
}
}
}];
}
failureBlock:^(NSError *error)
{
NSLog(#"You must allow the app to fetch your photos");
}
] ;
}
}
Since AlAsset library requires location services to be enabled, I couldn't ignore the location check.
And Since iOS 6.0 user can even prevent the app from accessing the photo library so AssetLibrary authorisation was also required.
Solution for me was to use [self.locMgr stopUpdatingLocation] in the below delegate:
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
if(status == 1 || status == 2)
{
NO_LOCATION_ALERT // User denied location. Don't start the activity for fetching photos
}
else if (status == kCLAuthorizationStatusAuthorized)
{
//Start fetching fotos
}
else
{
// This is for the first time, when user hasn't made any choice. So leave it blank.
}
[self.locMgr stopUpdatingLocation]; // SOLUTION
}

NSArray enumerateObjectsUsingBlock: delay enumeration

I use fast enumeration and in the enumeration block I send network requests asynchronously.
So what happens is the enumerateObjectsUsingBlock: just call the block super fast and let the enumeration block finish after some time.
This leads to different results, because some requests finish faster than other. So it's not sorted as I wanted.
Is there any way to set the block to freeze, and after the asynchronous network requests is completed, to just tell it to go to the next one?
Here is some code
NSArray *sites = [self.fetchedResultsController.fetchedObjects copy];
NSLog(#"sites - %#",sites);
[sites enumerateObjectsUsingBlock:^(Sites *site, NSUInteger idx, BOOL *stop) {
NSLog(#"site name - %#,",site.name);
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
NSLog(#"site name - %#",site.name);
}];
}];
Thanks!
Is there any way to set the block to freeze, and after the
asynchronous network requests is completed, to just tell it to go to
the next one?
NSArray *sites = [self.fetchedResultsController.fetchedObjects copy];
NSLog(#"sites - %#",sites);
[sites enumerateObjectsUsingBlock:^(Sites *site, NSUInteger idx, BOOL *stop) {
NSLog(#"site name - %#,",site.name);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
NSLog(#"site name - %#",site.name);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
^ Not really the ideal way to do this but if you want to synchronously iterate while waiting for an async request to finish before moving forward, the above will do it via GCD. There are other ways to to where you can iterate and increment a dispatch_group while waiting for all the groups to be left after the async tasks complete such as:
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_enter(downloadGroup);
[self fetchStuffInBackground:background withCompletion:^(NSArray *stuff, NSError *error) {
NSLog(#"leaving stuff");
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchAOtherStuffInBackground:background withCompletion:^(NSArray *stuff, NSError *error) {
NSLog(#"leaving other stuff");
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchLastStuffInBackground:background withCompletion:^(NSArray *lastStuff, NSError *error) {
NSLog(#"leaving last stuff");
dispatch_group_leave(downloadGroup);
}];
}
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
if (callback) {
callback(error);
}
});
I wanted to achieve the same thing but keep using blocks to simplify my code, instead of having the hassle of passing parameters through a recursive method. I came up with this NSArray category:
NS_ASSUME_NONNULL_BEGIN
#interface NSArray(MH)
- (void)mh_asyncEnumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL* stop, dispatch_block_t next))block;
#end
NS_ASSUME_NONNULL_END
#implementation NSArray(MH)
- (void)mh_asyncEnumerateObjectsUsingBlock:(void (^)(id _Nonnull obj, NSUInteger idx, BOOL* stop, dispatch_block_t next))block{
__block NSUInteger index = 0;
__block BOOL stop = NO;
void (^next)();
__block __weak typeof(next) weakNext;
weakNext = next = ^void() {
void (^strongNext)() = weakNext;
// check if finished
if(stop || index == self.count){
return;
}
id obj = self[index];
index++;
block(obj, index - 1, &stop, strongNext);
};
next();
}
#end
It's used like this:
NSArray* a = #[#"Malc", #"Bob", #"Jim"];
[a mh_asyncEnumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *stop, dispatch_block_t next) {
// simulate an async network call
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(#"%#", obj);
next();
});
}];
Output:
2016-01-04 22:41:04.631 Malc
2016-01-04 22:41:05.632 Bob
2016-01-04 22:41:06.720 Jim
As you can see, this demo outputs each string in the array with 1 second delay. You can use it by doing your network call inside the block and then calling next when it's done. If you hit an error and would like to cancel just set *stop = YES; before calling next(), the same as you would do with normal enumeration.
NSArray *sites = [self.fetchedResultsController.fetchedObjects copy];
NSLog(#"sites - %#",sites);
[sites mh_asyncEnumerateObjectsUsingBlock:^(Site *site, NSUInteger idx, BOOL *stop, dispatch_block_t next){
NSLog(#"site name - %#,",site.name);
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
if(error){ // your completion block should have an error param!!!
*stop = YES;
}
NSLog(#"site name - %#",site.name);
next();
}];
}];
Is there any way to set the block to freeze, and after the asynchronous network requests is completed, to just tell it to go to the next one?
You can achieve that result by reorganizing your code: instead of using enumeration, just execute the async request from the completion block, one at the time:
- (void) doRequestAsync:(NSArray*)sites index:(NSUInteger)index {
if (index >= [sites count]) return;
NSString* site = [sites objectAtIndex:index];
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
NSLog(#"site name - %#",site.name);
[self doRequestAsync:sites index:++index];
}];
}
Alternatives to this are modifying your Wrapper class so that it uses async networking (but use it on a secondary thread, then, to avoid blocking the UI).
Or you might implement the Async Completion Token pattern so to be able to reorder the responses when they are received.

AlAssetLibrary returning empty results

I'm trying to pick out some camera roll meta data. When I enumerate through the assets, I cannot seem to retrieve any information and get an empty array. Is there a step I'm missing?
My code:
assets = [[NSMutableArray array] init];
void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if(asset != NULL) {
[assets addObject:asset];
dispatch_async(dispatch_get_main_queue(), ^{
});
}
};
void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
[group enumerateAssetsUsingBlock:assetEnumerator];
}
};
library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
usingBlock:assetGroupEnumerator
failureBlock: ^(NSError *error) {
NSLog(#"Failed.");
}];
NSLog(#"%#", assets); //prints an empty array
Midhun MP is right that you are not waiting for the asynchronous enumeration to complete. In this case, you have asynchronous blocks calling other asynchronous blocks, so it is not simple to know when all enumeration is done.
If you would like to know when this is done, and end up with an array that contains all of the enumerated assets, you could use dispatch_groups. Here is one way you could do that (I've included multiple ALAssetGroup types to show that this can work with multiple albums):
dispatch_group_t loadingGroup = dispatch_group_create();
NSMutableArray * assets = [[NSMutableArray array] init];
NSMutableArray * albums = [[NSMutableArray array] init];
void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if(index != NSNotFound) {
[assets addObject:asset];
dispatch_async(dispatch_get_main_queue(), ^{ });
} else {
dispatch_group_leave(loadingGroup);
}
};
void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
[albums addObject: group];
} else {
NSLog(#"Found %d albums", [albums count]);
// album loading is done
for (ALAssetsGroup * album in albums) {
dispatch_group_enter(loadingGroup);
[album enumerateAssetsUsingBlock: assetEnumerator];
}
dispatch_group_notify(loadingGroup, dispatch_get_main_queue(), ^{
NSLog(#"DONE: ALAsset array contains %d elements", [assets count]);
});
}
};
ALAssetsLibrary * library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos | ALAssetsGroupAlbum
usingBlock:assetGroupEnumerator
failureBlock: ^(NSError *error) {
NSLog(#"Failed.");
}];
(In this example, it is safe to have various blocks adding to assets and albums because the enumeration is all happening on the main thread.)
If you are running this on iOS 6, it may be that the user has denied access for you app to access the asset library. If this is the case, the failureBlock is called.
Also note that the usingBlock is called asynchronously so your attempt to log assets is premature. You should move the NSLog statement into the end of the enumeration block.
From the docs for enumerateGroupsWithTypes:usingBlock:failureBlock:
The results are passed one by one to the caller by executing the enumeration block.
This method is asynchronous. When groups are enumerated, the user may be asked to confirm the application's access to the data; the method, though, returns immediately. You should perform whatever work you want with the assets in enumerationBlock.
If the user denies access to the application, or if no application is allowed to access the data, the failureBlock is called.
Your NSLog will always display an empty array because the NSLog statement will work before completing the asynchronous enumeration block.
Solution:
First check that your photolibrary is not empty.
Then add NSLog in:
void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if(asset != NULL) {
[assets addObject:asset];
NSLog(#"Asset : %#", asset);
dispatch_async(dispatch_get_main_queue(), ^{
});
}
};

Access a single image with ALAssetsLibrary

Allow me to preface this by saying this is my first time using the ALAssetsLibrary. I need to access the most recent photo in the user's saved photo gallery. It seems that to do this, I have to create an ALAssetsLibrary instance and iterate over every item in the user's gallery before selecting the last image. This is always worst-case scenario. Is there a faster/better way to approach this problem?
You don't have to enumerate all the photos in the user's gallery.
The ALAssetsGroup class has a method - (void)enumerateAssetsAtIndexes:(NSIndexSet *)indexSet options:(NSEnumerationOptions)options usingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock which you can use to indicate which assets you want to enumerate.
In your case it's only the last one so set indexSet to [NSIndexSet indexSetWithIndexesInRange:NSMakeRange([group numberOfAssets]-1, [group numberOfAssets]) where group is your ALAssetsGroup.
As #mithuntnt mentioned, you can get the ALAssetsGroup for the photo library using [[assetsLibrary] enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^(ALAssetsGroup *group, BOOL *stop)
What about this:
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
*stop = YES;
//...
}
}];
http://developer.apple.com/library/ios/#documentation/AssetsLibrary/Reference/ALAssetsLibrary_Class/Reference/Reference.html
There is only one enumeration method. So this is the only way.
I needed the last imported photos. You can have some filter similar to this.
[[assetsLibrary] enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if( group )
{
NSString * groupName = [group valueForProperty:ALAssetsGroupPropertyName];
if( [#"Last Import" isEqualToString:groupName] )
{
*stop = true;
...
The accepted answer doesn't appear to work if you're enumerating an ALAssetGroup that you've set a filter on (because [group numberOfAssets] returns the total assets rather then the total assets after filtering).
I used this:
typedef void(^SMKMostRecentPhotoCompletionBlock)(ALAsset *asset);
- (void)mostRecentPhotoWithCompletionBlock:(SMKMostRecentPhotoCompletionBlock)completionBlock
{
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
__block ALAsset *mostRecentPhoto = nil;
if (group)
{
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result != NULL)
{
mostRecentPhoto = result;
*stop = YES;
}
}];
}
if (completionBlock)
{
completionBlock(mostRecentPhoto);
}
} failureBlock:^(NSError *error) {
if (completionBlock)
{
completionBlock(nil);
}
}];
}
In your completionBlock, make sure to check that the returned ALAsset != nil.

objc_setAssociatedObject unavailable in iPhone simulator

In the 3.1 SDk, Apple added support for associated objects.
However, the simulator will not compile code that includes references to objc_setAssociatedObject, objc_getAssociatedObject, et al. (Undeclared errors)
Is there away around this? Can I make the iPhone simulator compile this code? I would hate to have to do all testing on the device.
Update
Bug Filed:
rdar://7477326
I don't think this will be fixed in the 3.x SDKs, so another fix is to just define the functions and call through to the next definition via a dynamic lookup.
Header:
#if TARGET_IPHONE_SIMULATOR
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
typedef uintptr_t objc_AssociationPolicy;
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, void *key);
void objc_removeAssociatedObjects(id object);
#endif
Implementation:
#if TARGET_IPHONE_SIMULATOR
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) {
((void (*)(id, void *, id, objc_AssociationPolicy))
dlsym(RTLD_NEXT, "objc_setAssociatedObject")) (object, key, value, policy);
}
id objc_getAssociatedObject(id object, void *key) {
return ((id (*)(id, void *))
dlsym(RTLD_NEXT, "objc_getAssociatedObject"))(object, key);
}
void objc_removeAssociatedObjects(id object) {
((void (*)(id))
dlsym(RTLD_NEXT, "objc_removeAssociatedObjects"))(object);
}
#endif
A quick and dirty workaround (largely untested, may be buggy):
#if TARGET_IPHONE_SIMULATOR
#import <objc/runtime.h>
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
typedef uintptr_t objc_AssociationPolicy;
#implementation NSObject (OTAssociatedObjectsSimulator)
static CFMutableDictionaryRef theDictionaries = nil;
static void Swizzle(Class c, SEL orig, SEL new) // swizzling by Mike Ash
{
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if (class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
else
method_exchangeImplementations(origMethod, newMethod);
}
- (NSMutableDictionary *)otAssociatedObjectsDictionary
{
if (!theDictionaries)
{
theDictionaries = CFDictionaryCreateMutable(NULL, 0, NULL, &kCFTypeDictionaryValueCallBacks);
Swizzle([NSObject class], #selector(dealloc), #selector(otAssociatedObjectSimulatorDealloc));
}
NSMutableDictionary *dictionary = (id)CFDictionaryGetValue(theDictionaries, self);
if (!dictionary)
{
dictionary = [NSMutableDictionary dictionary];
CFDictionaryAddValue(theDictionaries, self, dictionary);
}
return dictionary;
}
- (void)otAssociatedObjectSimulatorDealloc
{
CFDictionaryRemoveValue(theDictionaries, self);
[self otAssociatedObjectSimulatorDealloc];
}
#end
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
{
NSCAssert(policy == OBJC_ASSOCIATION_RETAIN_NONATOMIC, #"Only OBJC_ASSOCIATION_RETAIN_NONATOMIC supported");
[[object otAssociatedObjectsDictionary] setObject:value forKey:[NSValue valueWithPointer:key]];
}
id objc_getAssociatedObject(id object, void *key)
{
return [[object otAssociatedObjectsDictionary] objectForKey:[NSValue valueWithPointer:key]];
}
void objc_removeAssociatedObjects(id object)
{
[[object otAssociatedObjectsDictionary] removeAllObjects];
}
#endif