From the iOS SDK docs, an Action extension might help users edit an image in a document that they’re viewing in a text editor. However I've been trying to google for examples how to do this and can only find articles on how to create app extensions and not how to use them in an app.
Suppose that I'm writing a word processing application for iOS. Picture the user having an embedded image in the app and wants to edit the image (e.g. apply a photo effect). How can the application provides the image to whatever image editing applications that the user has installed in the system, let it do it's thing, and then takes in the result?
I'd imagine the interaction style is pretty similar to LinkBack on the Mac. Except that the image editor is an app extension and displayed as a modal dialog (as per the SDK guide).
However I couldn't find any code example that shows:
How to provide input data (e.g. image) to the action extension.
How to invoke the action extension.
How to get back the output data from the action extension (including whatever additional metadata or editing information that the extension generates).
How to display the output data in a recognizable format (e.g. if I gave out a JPEG, I'd expect another JPEG will be given by the action extension as a result).
In the mean time I've figured out how to do this. It revolves primarily around UIActivityViewController and providing a callback block to the controller to receive the results from the action extension.
The example below works with Skitch as the test action extension.
// get the source image
UIImage* image = ...;
NSArray* activityItems = #[image];
UIActivityViewController* activityCtrl = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
activityCtrl.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
[returnedItems enumerateObjectsUsingBlock:^(NSExtensionItem* _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop1) {
[item.attachments enumerateObjectsUsingBlock:^(NSItemProvider* _Nonnull attachment, NSUInteger idx, BOOL * _Nonnull stop2) {
if ([attachment hasItemConformingToTypeIdentifier:(__bridge id)kUTTypeImage]) {
[attachment loadItemForTypeIdentifier:(__bridge id)kUTTypeImage options:nil completionHandler:^(UIImage* returnedImage, NSError * _Null_unspecified error) {
// returnedImage is the result of the action extension
}];
*stop1 = YES;
*stop2 = YES;
}
}];
}];
};
// assume that `self` is a UIViewController
[self presentViewController:activityCtrl animated:YES completion:nil];
Unfortunately action extension providers are a bit rare, hence I couldn't test out how it actually interacts with other action extensions that can produce images.
Related
To understand this question, return with me now through the WWDC time machine to the distant past, 2014, when Action extensions were introduced and explained in this video:
https://developer.apple.com/videos/play/wwdc2014/217/
About halfway through, in slide 71, about minute 23:30, the presenter gives instructions for returning a value back to the calling app (the app where the user tapped our Action extension's icon in an activity view):
- (IBAction)done:(id)sender {
NSData *data = self.contents;
NSItemProvider *itemProvider =
[[NSItemProvider alloc] initWithItem:data typeIdentifier:MyDocumentUTI];
NSExtensionItem *item = [[NSExtensionItem alloc] init];
item.attachments = #[itemProvider];
}
A moment later, slide 75, about minute 26, we see how the app that put up the activity view controller is supposed to unwrap that envelope to retrieve the result data:
- (void)setupActivityViewController {
UIActivityViewController *controller;
controller.completionWithItemsHandler =
^(NSString *activityType, BOOL completed,
NSArray *returnedItems, NSError *error) {
if (completed && (returnedItems.count > 0)) {
// process the result items
}
}];
}
So my question is: is that for real? Has anyone within the sound of my voice ever done either of those things? Namely:
Does your app have an Action extension that returns a value to the caller?
Does your app put up an activity view controller that receives the result of some arbitrary unknown Action extension and does something with the value?
I ask because (1) I have never seen (on my iPhone) an Action extension that actually returns a value, and (2) the code elided in "process the result items" seems to me to be complete hand-waving, because how would my app even know what kind of data to expect?
I have come to believe that this code is an aspirational pipe dream with no corresponding reality. But I would be delighted to be told I'm wrong.
The Photos app in iOS 7 allows you to select multiple photos, tap "Share" and be presented with a document interaction controller with the appropriate options for multiple items.
The Camera app goes one further and even updates the document interaction controller's options in real time as you select and deselect photos.
However, the UIDocumentInteractionController class seems only to allow for a single URL parameter.
Is it possible to do what the Photos and Camera apps do using public API?
- (void)showShareDialog
{
UIImage *image = [UIImage imageWithCGImage:self.imgView.image.CGImage];
NSArray* dataToShare = #[image, image2, image3]; // ...or whatever pieces of data you want to share.
UIActivityViewController* activityViewController =
[[UIActivityViewController alloc] initWithActivityItems:dataToShare
applicationActivities:nil];
[self presentViewController:activityViewController animated:YES completion:^{
}];
}
I think this should help
Store the URLs of the selected items in an array. You can change the actions displayed depending upon the number of elements in the array. After the user makes his/her selection, you can loop through the URLs and apply the selected action.
This question is kind of solved, but one more request remaining at the bottom of this question. And I will choose an answer after all.
I am creating a static library that is using AssetsLibrary framework. While I am using it in a project to test whether is work. It return nothing while I call the enumerateGroupsWithTypes:usingBlock:failureBlock: method of the instance of AssetsLibrary.
I have trying -
to set breakpoints to see how it running this method. Turns out it did not go into the block that passing to usingBlock:, which is ALAssetsLibraryGroupsEnumerationResultsBlock, and either the failureBlock:. So that I got nothing.
to add the same enumerating code to the project I mentioned at the beginning to try to calling method of AssetsLibrary. it worked perfectly fine.
to test whether it is block by the main thread, then run it in the main thread. Got the same result as before.
For this issue, I have found an answer of other question that is talking about using media in the static library: https://stackoverflow.com/a/15331319/1583560, and I am not sure I have run into the same situation, is the media he/she mentions including accessing AssetsLibrary, I guess no here.
Hope someone can point some directions of this, thank you :)
Update
This is the code I used -
[[[ALAssetsLibrary alloc] init] enumerateGroupsWithTypes:ALAssetsGroupAll
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
NSLog(#"%s",__PRETTY_FUNCTION__);
} failureBlock:^(NSError *error) {
NSLog(#"%s",__PRETTY_FUNCTION__);
}];
Code in the static library is the same as in the test project, the only difference is I make a model class to access AssetsLibrary in the static library.
To be clear, here are few changes I have made in the static library -
Change Build Product Path in Target > Build Settings to $(PROJECT_DIR)/../build
Moving required header file to Project section in Target > Build Phases > Copy Headers
Set Skip Install to YES in Target > Build Settings
Environment related -
OS X 10.9.1
Xcode 5.0.2
Standard architectures (including 64-bit) (both static library and the project)
ARC
More details
Here is my propose to make a assets model for easy accessing in this static library.
Having a group array to store all the albums, which is ALAssetsGroup here, in devices.
Enumerating albums out at the init of this model and storing into the group array.
Enumerating photos, which is ALAssets result, by the group given while needed.
And this model using singleton pattern.
BTW, I have tried to observe the ALAssetsLibraryChangedNotification in this static library. It's not working, too. Are there any potential obstructs at the front of AssetsLibrary?
Update
I have find out that I enumerate the groups while init the model I created. And there are threads make blocks not work. If I trigger the enumerate after the init complete, will work perfectly. And I also found the way to know when it is done enumerating (cf. https://stackoverflow.com/a/13309871/1583560) to get the group array which I stored.
Further, I still cannot find the document, Apple's, that addressing the threading of block, why it will not been call while init, yet. If someone could point out for me, I will appreciate it :).
This is not about "unable to enumerate assets on devices", because of the async of enumerating ALAssetsGroup.
The Async
According to apple's document about enumerateGroupsWithTypes:usingBlock:failureBlock: of ALAssetsLibrary, this is an async method, will not get full data stored in array after it run.
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.
Completion Notification
I do want to know when it is done. so that I have found an answer for knowing when the progress is done, and execute a block (cf. Create my own completion blocks in iOS). Even though this is not about the notification, but still gave me a hint to work it out.
postNotification: while it reach the end of the enumeration
[_assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if(group){
// add into the array you are going to use
}
else
{
// the last one will be nil,
// it also means the enumeration is done
// post notification then
[[NSNotificationCenter defaultCenter] postNotificationName:kEnumerateGroupCompleteNotification];
}
}
failureBlock:nil]; // leave nil here for make subject out
addObserver:selector:name:object: to add observer for reloading data
First, using the empty NSArray, which is the retained array we are using at previous step, in the ~DataSource protocol of UICollectionView or UITableView.
Second, adding an observer to the UIViewController, using #selector pass to this method to trigger the instance's reloadData to get the complete array just enumerating out. And the data will be shown on the view.
// In a UIViewController's implementation
- (void)viewDidLoad{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadTableView:)
name:kEnumerateGroupCompleteNotification
object:nil];
}
-(void)reloadTableView:(NSNotification*) notification {
// reloadData after receive the notification
[_tableView reloadData];
}
Conclusion
This is the way how I achieve my requirement. I have addressed ALAssetsGroup only above, but it will be the same in ALAssets's enumeration.
It is the same way if you want to get ALAssetsLibraryChangedNotification and reload data.
And a great thank for Ramshad's answer, I have accessing ALAssetsLibrary as your suggestion in my static library now.
Add the following method to your class:
+ (ALAssetsLibrary *)defaultAssetsLibrary
{
static dispatch_once_t pred = 0;
static ALAssetsLibrary *library = nil;
dispatch_once(&pred, ^{
library = [[ALAssetsLibrary alloc] init];
});
return library;
}
Then modify your code as
ALAssetsLibrary *assetsLibrary = [ViewController defaultAssetsLibrary];
[assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop)
{
[group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop)
{
if(result)
{
NSLog(#"Success");
}
}];
} failureBlock:^(NSError *error)
{
NSLog(#"Error loading images %#", error);
}];
I'd like to provide different content to the different services in a UIActivityIndicatorView. For example, an HTML string for email, standard string for Facebook/Twitter, and an image for Copy. All of this happens within a single popover window.
I don't think this is possible. As I was playing around with all of the involved classes, I notices that the services that appear in the activity view change as the content type I provided it changed. In other words, when I provided a string, set A of services appeared. When I provided an image, set B of services appeared.
Next I tried adding both string and image thinking that possibly it would provide different content, however this just adds both the string and image to the services (for example an email is created with a string, then the image below).
Here is how I am providing string values (which are html links):
-(void)shareButtonTouchUpInside:(SMActionToolbarViewController*)sender{
// Reposition anchor view for UIPopoverController to point at
[self repositionAnchorViewToButtonFrame:self.actionToolbarViewController.shareButtonFrame];
// Asynch download of image
[SMUtility downloadAsset:self.selectedAsset completion:^(UIImage *image) {
// Create image source
SMActivitySource *activityImageSource = [[SMActivitySource alloc]initWithImage:image];
// Create string source
NSString *assetsString = [SMUtility assetsString:[NSArray arrayWithObject:self.selectedAsset]];
SMActivitySource *activityStringSource = [[SMActivitySource alloc]initWithString:assetsString];
// Present UIActiviyViewController within an UIPopoverController
NSArray *items = [#[activityImageSource, activityStringSource]mutableCopy];
UIActivityViewController *activityViewController = [[UIActivityViewController alloc]initWithActivityItems:items applicationActivities:nil];
[activityViewController setCompletionHandler:^(NSString *activityType, BOOL completed){
// TODO: Populate mixpanel data
[SMMixPanel eventSharePhotoMethod:#"Unknown"];
}];
self.buttonPopoverController = [[UIPopoverController alloc] initWithContentViewController:activityViewController];
[self.buttonPopoverController presentPopoverFromRect:self.anchorView.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}];
}
You should subclass UIActivityItemProvider or create objects that conform to UIActivityItemSource. Pass in an array of those (one for each thing you want to share), and have some of them return nil depending on the chosen activity in the activityViewController:itemForActivityType: call.
This Question has been asked time and time again, the only way to fix it is to make your own UIActivity, see apple documentation on how to do that. Here is a link: http://developer.apple.com/library/ios/#documentation/uikit/reference/UIActivity_Class/Reference/Reference.html
I have implemented UIActivityViewController within my app and can successfully share both strings and images. However, I notice that when you share an image within the iOS Photos app, there are some services that do not appear in my app. Namely Print, Use as Wallpaper, and Assign To Contact, and Photo Stream. My app is able to use Mail, Message, Facebook, Twitter, and Copy just fine.
I am thinking that either:
1.) These extra services have been implemented as custom services within the Photos app using UIActivityItemProvider, UIActivityItemSource, etc..
2.) The data that I am providing is not in the correct format to be used with these services.
I have read through the documentation a few times, but don't seem to see anything about it.
Edit: Showing code as requested:
#define SM_SHARE_IMAGE_AND_STRING 1
-(void)actionToolbarViewControllerUserTappedShareButton:(SMActionToolbarViewController*)sender{
// Reposition anchor view for UIPopoverController to point at
[self repositionAnchorViewToButtonFrame:self.actionToolbarViewController.shareButtonFrame];
// Asynch download of image
[SMUtility downloadAsset:self.selectedAsset completion:^(UIImage *image) {
// Create image source
SMActivitySource *activityImageSource = [[SMActivitySource alloc]initWithImage:image];
#if defined(SM_SHARE_IMAGE_AND_STRING)
// Create string source
NSString *assetsString = [SMUtility assetsString:[NSArray arrayWithObject:self.selectedAsset]];
SMActivitySource *activityStringSource = [[SMActivitySource alloc]initWithString:assetsString];
// Present UIActiviyViewController within an UIPopoverController
NSArray *items = [#[activityImageSource, activityStringSource]mutableCopy];
#else
NSArray *items = [#[activityImageSource]mutableCopy];
#endif
UIActivityViewController *activityViewController = [[UIActivityViewController alloc]initWithActivityItems:items applicationActivities:nil];
[activityViewController setCompletionHandler:^(NSString *activityType, BOOL completed){
[SMMixPanel eventSharePhotoMethod:#"Share"];
}];
self.buttonPopoverController = [[UIPopoverController alloc] initWithContentViewController:activityViewController];
[self.buttonPopoverController presentPopoverFromRect:self.anchorView.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}];
}
The Print, and Assign To Contact activities are standard activities shown by the UIActivityViewController as long as you provide the proper data.
You must provide a UIImage for the Assign To Contact activity. See the docs for UIActivityTypeAssignToContact. See the docs for UIActivityTypePrint for details on what it accepts.
The "Use As Wallpaper" seems to be a custom activity shown only in the Photos app.