I just added MP4/GIF attachments to my push notifications in my iOS app. Everything works fine with respect to playback. The issue I am facing is when MP4 videos are sent, the small thumbnail that is sent in the push looks transparent. However, when I expand it, it looks perfect and I can play it well too inside the push. When I send the same video converted to GIF the thumbnail also looks perfect.
Here is an example:
The example above shows two different apps, just to show how MP4 and GIF thumbnails show up for the same event. If I were to send GIF to the app on the top, the output of the thumbnail looks exactly like the Pushover app thumbnail.
And here is what happens when I slide and view the thumbnail (transparent one). This particular expanded thumbnail is for a different event (I lost that old event). But the point I wanted to make is the expanded view looks perfect. And plays perfectly too.
So in conclusion, in IOS, when I send MP4 files as attachments the small thumbnail looks transparent, but plays back well. expanded thumbnail looks perfect.
This is my client code:
//
// NotificationService.m
// NotificationService
//
//
//
//
// Credit https://github.com/Leanplum/Leanplum-iOS-Samples/blob/master/iOS_basicSetup/basicSetup/richPushExtension/NotificationService.m
#import "NotificationService.h"
#interface NotificationService ()
#property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
#property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
#end
#implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSDictionary *userInfo = request.content.userInfo;
// If there is no image in the payload than
// the code will still show the push notification.
if (userInfo == nil || userInfo[#"image_url_jpg"] == nil) {
NSLog(#"zmNinja Notification: Did not get a payload or image");
[self contentComplete];
return;
}
NSString *mediaUrl = userInfo[#"image_url_jpg"];
// if (mediaType == nil) {
// NSLog(#"zmNinja Notification: No media type specified, assuming .jpg");
// mediaType = #".jpg";
// }
// load the attachment
[self loadAttachmentForUrlString:mediaUrl
completionHandler:^(UNNotificationAttachment *attachment) {
if (attachment) {
self.bestAttemptContent.attachments = [NSArray arrayWithObject:attachment];
}
[self contentComplete];
}];
}
- (NSString*)determineType:(NSString *) fileType {
// Determines the file type of the attachment to append to NSURL.
//return #".gif";
// Determines the file type of the attachment to append to NSURL.
NSLog (#"zmNinja Notification: determineType got filetype=%#",fileType);
if ([fileType isEqualToString:#"image/jpeg"]){
NSLog (#"zmNinja Notification: returning JPG");
return #".jpg";
}
if ([fileType isEqualToString:#"video/mp4"]){
NSLog (#"zmNinja Notification: returning MP4");
return #".mp4";
}
if ([fileType isEqualToString:#"image/gif"]) {
NSLog (#"zmNinja Notification: returning GIF");
return #".gif";
}
if ([fileType isEqualToString:#"image/png"]) {
NSLog (#"zmNinja Notification: returning PNG");
return #".png";
}
NSLog (#"zmNinja Notification: unrecognized filetype, returning JPG");
return #".jpg";
}
- (void)loadAttachmentForUrlString:(NSString *)urlString
completionHandler:(void(^)(UNNotificationAttachment *))completionHandler {
__block UNNotificationAttachment *attachment = nil;
NSURL *attachmentURL = [NSURL URLWithString:urlString];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session downloadTaskWithURL:attachmentURL
completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
if (error != nil) {
NSLog(#"unable to add attachment: %#", error.localizedDescription);
} else {
NSString *fileType = [self determineType: [response MIMEType]];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *localURL = [NSURL fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileType]];
[fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
NSError *attachmentError = nil;
attachment = [UNNotificationAttachment attachmentWithIdentifier:#"" URL:localURL options:nil error:&attachmentError];
if (attachmentError) {
NSLog(#"unable to add attchment: %#", attachmentError.localizedDescription);
}
}
completionHandler(attachment);
}] resume];
}
- (void)contentComplete {
self.contentHandler(self.bestAttemptContent);
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
NSLog (#"zmNinja Notification: Time about to expire, handing off to best attempt");
self.contentHandler(self.bestAttemptContent);
}
#end
The server side uses FCM legacy APIs:
my $ios_message = {
to => $obj->{token},
notification => {
title => $title,
body => $body,
sound => "default",
badge => $badge,
},
data => {
myMessageId => $notId,
mid => $mid,
eid => $eid,
summaryText => $eid
}
};
$ios_message->{data}->{image_url_jpg} = $pic; # $pic is a URL for the mp4
# image_url_jpg is just a field name. It was originally meant for static images
# haven't changed it yet, as you see in client code above, it uses that field.
$json = encode_json($ios_message);
my $req = HTTP::Request->new( 'POST', $uri );
$req->header(
'Content-Type' => 'application/json',
'Authorization' => $key
);
$req->content($json);
my $lwp = LWP::UserAgent->new(%ssl_push_opts);
my $res = $lwp->request($req);
Finally, if you want to take a look at a sample MP4 to rule out any format issue, here is one that I've uploaded to google drive (link). I've extracted frame information with ffshow and it doesn't look out of place to me (plus it plays perfectly).
Can someone help me understand why the initial small thumbnail looks messed up in iOS? (If it helps, I am on iOS 13.x)
Thanks.
Had the same problem. Solved it by creating a notification content extension and sending a thumbnail image URL along with the video URL.
In the service extension, I add both the video and the thumbnail as attachments, with the thumbnail as the first element, which iOS will show in the notification preview:
// mediaAttachment and thumbnailAttachment are UNNotificationAttachments
// that have just been downloaded
if let mediaAttachment = mediaAttachment {
mutableContent.attachments = [mediaAttachment]
}
if let thumbnailAttachment = thumbnailAttachment {
mutableContent.attachments.insert(thumbnailAttachment, at: 0)
}
contentHandler(mutableContent)
The expanded notification UI is handled by the content extension which replaces the default UI, and there I ignore the thumbnail image and just show the video, which will be the last attachment.
Related
I used the Solution form iOS Share Extension issue when sharing images from Photo library to get Images from the Photo App. This works great in the Simulator, but on the Device I get an error that I can't Access the NSURL provided by the itemProvider:
2018-02-18 12:54:09.448148+0100 MyApp[6281:1653186] [default] [ERROR] Failed to determine whether URL /var/mobile/Media/PhotoData/OutgoingTemp/554581B2-950C-4CFD-AE67-A2342EDEA04D/IMG_2784.JPG (s) is managed by a file provider
Caused by the Statment:
[itemProvider loadItemForTypeIdentifier:itemProvider.registeredTypeIdentifiers.firstObject options:nil completionHandler:^(id<NSSecureCoding> item, NSError *error) {
}
Searching PHAsset for the Item Name is not a good solution as the user have to grand access to the photo library again.
In didSelectPost you must not dismiss the ShareExtension ViewController until you have processed all the items in the inputItems array. The ViewController is dismissed using [super didSelectPost] or by calling the completion method of the extension context.
Here is my solution in code:
- (void)didSelectPost {
__block NSInteger itemCount = ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments.count;
__block NSInteger processedCount = 0;
for (NSItemProvider* itemProvider in ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments ) {
if([itemProvider hasItemConformingToTypeIdentifier:#"public.jpeg"]) {
NSLog(#"itemprovider = %#", itemProvider);
[itemProvider loadItemForTypeIdentifier:#"public.jpeg" options:nil completionHandler: ^(id<NSSecureCoding> item, NSError *error) {
NSData *imgData;
if([(NSObject*)item isKindOfClass:[NSURL class]]) {
imgData = [NSData dataWithContentsOfURL:(NSURL*)item];
}
if([(NSObject*)item isKindOfClass:[UIImage class]]) {
imgData = UIImagePNGRepresentation((UIImage*)item);
}
NSDictionary *dict = #{
#"imgData" : imgData,
#"name" : self.contentText
};
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.share.extension1"];
[defaults setObject:dict forKey:#"img"];
[defaults synchronize];
processedCount += 1;
if (processedCount == itemCount)
[super didSelectPost];
}];
}
}
The loadItemForTypeIdentifier or in Swift loadItem method is asynchronous so dismissing the UI must be called as the last thing inside its completionHandler.
For example I have:
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let item = self.extensionContext?.inputItems[0] as? NSExtensionItem, let attachments = item.attachments {
for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: {item, error in
// do all you need to do here, i.e.
if let tmpURL = item as? URL {
// etc. etc.
}
// and, at the end, inside the completionHandler, call
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
}
}
I you dismiss the UI via:
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
or via:
super.didSelectPost()
outside of the completionHandler after the async method loadItem you will get all kind of permission errors, further more this errors could be random, sometimes happen and sometimes don't, this is because sometimes your async call to loadItem gets the chance to terminate before the UI is dismissed and sometimes it doesn't.
Just leaving this here, hoping it helps someone. This issue costed me few hours.
I got a strange problem. iOS Notification Service Extension will delete the attachment from device.
I use SDWebImage to display and cache image, and I implemented a Notification Service Extension to display a image in the notification alert view.
In my case, the image was already cached locally. Then, I click home button, my app was running in background, app scheduled a local notification with the cached image attach into the notification content.
See the code bellow:
1.Schedule a local notification
+ (void)postLocalNotificationGreaterThanOrEqualToiOS10:(LNotification)module body:(NSDictionary *)body {
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.sound = [UNNotificationSound defaultSound];
content.body = #"body";
content.userInfo = #{};
//get the image in device to attach into notification
NSError *error;
NSString* imgURL = [body valueForKey:kLocalNotification_Image];
NSString *filePath = [[SDImageCache sharedImageCache] defaultCachePathForKey:imgURL];
NSURL *url = [NSURL URLWithString:[#"file://" stringByAppendingString:filePath]];
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:#"Image" URL:url options:nil error:&error];
if (attachment) {
content.attachments = #[attachment];
}
UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:#"FiveSecond"
content:content trigger:trigger];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(#"error: %#", error.localizedDescription);
}
}];
}
2.Notification Service Extension (In Fact, for local notification, only didReceiveNotificationRequest:withContentHandler: was called and did nothing.)
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSDictionary *aps = [self.bestAttemptContent.userInfo objectForKey:#"aps"];
if (aps) {
....//For remote notification, modify the notification content here
}
else {
//For local notification, do nothing
}
self.contentHandler(self.bestAttemptContent);
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
}
- (NSString *)downloadImageWithURL:(NSString *)imageURLString imageName:(NSString *)imageName {
....//code will not execute for local notification
}
I found that, the image I attached into the local notification will be deleted from device. I mean, after I click the notification alert to launch app from background to foreground, I try to display the image, unfortunately, SDWebImageCache did not find the cache from neither disk nor memory.
I read the preference of iOS API, and didn't find that the attachment will be deleted. Does anybody know where could I find any clue of this issue? Maybe I just ignored something important refer to Notification Service Extension.
Now, I made a work around to fix this issue temporarily, while scheduling local notification, copy the cached image and save as another name, then attach it into local notification. Even Notification Service Extension will delete the attachment, it will just deleted the copy file, and the app will find the image from cache.
But, I really wanna know why this problem happened. Thanks in advance.
Just found in documentation
UNNotificationAttachment:
The system validates the content of attached files before scheduling
the corresponding notification request. If an attached file is
corrupted, invalid, or of an unsupported file type, the notification
request is not scheduled for delivery. Once validated, attached files
are moved into the attachment data store so that they can be accessed
by the appropriate processes. Attachments located inside an app’s
bundle are copied instead of moved.
In my project wanna have a flow like this:
Users record short videos -> they upload the videos on my channel -> end
To achive this result i'm trying to work with the new Google APIs Client Library for Objective-C for REST. It has a poor documentation and the examples are for mac only. Anyway after many errors this is my code:
- (void)doAuthWithoutCodeExchange:(OIDServiceConfiguration *)configuration
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret {
NSURL *redirectURI = [NSURL URLWithString:kRedirectURI];
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:clientID
clientSecret:clientSecret
scopes:#[ OIDScopeOpenID, OIDScopeProfile ]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
AppDelegate *appDelegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[self logMessage:#"Initiating authorization request %#", request];
appDelegate.currentAuthorizationFlow =
[OIDAuthorizationService presentAuthorizationRequest:request
presentingViewController:self
callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
NSError *_Nullable error) {
if (authorizationResponse) {
OIDAuthState *authState =
[[OIDAuthState alloc] initWithAuthorizationResponse:authorizationResponse];
[self setAuthState:authState];
[self logMessage:#"Authorization response with code: %#",
authorizationResponse.authorizationCode];
// could just call [self tokenExchange:nil] directly, but will let the user initiate it.
OIDTokenRequest *tokenExchangeRequest =
[_authState.lastAuthorizationResponse tokenExchangeRequest];
[self logMessage:#"Performing authorization code exchange with request [%#]",
tokenExchangeRequest];
[OIDAuthorizationService performTokenRequest:tokenExchangeRequest
callback:^(OIDTokenResponse *_Nullable tokenResponse,
NSError *_Nullable error) {
if (!tokenResponse) {
[self logMessage:#"Token exchange error: %#", [error localizedDescription]];
} else {
[self logMessage:#"Received token response with accessToken: %#", tokenResponse.accessToken];
}
[_authState updateWithTokenResponse:tokenResponse error:error];
GTMAppAuthFetcherAuthorization *gtmAuthorization =
[[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
// Sets the authorizer on the GTLRYouTubeService object so API calls will be authenticated.
self.youTubeService.authorizer = gtmAuthorization;
// Serializes authorization to keychain in GTMAppAuth format.
[GTMAppAuthFetcherAuthorization saveAuthorization:gtmAuthorization
toKeychainForName:kGTMAppAuthKeychainItemName];
[self uploadVideoFile];
}];
} else {
[self logMessage:#"Authorization error: %#", [error localizedDescription]];
}
}];
}
This method cause this flow:
app send user to google login page in safari -> user log with his credentials -> after login, user is redirect back to my app -> the block success call the method UploadVideo.
This part of the flow seems to work correctly, i obtain a valid token as the log says. The second part is the video upload that consist in two main methods:
- (void)uploadVideoFile {
// Collect the metadata for the upload from the user interface.
// Status.
GTLRYouTube_VideoStatus *status = [GTLRYouTube_VideoStatus object];
status.privacyStatus = #"public";
// Snippet.
GTLRYouTube_VideoSnippet *snippet = [GTLRYouTube_VideoSnippet object];
snippet.title = #"title";
NSString *desc = #"description";
if (desc.length > 0) {
snippet.descriptionProperty = desc;
}
NSString *tagsStr = #"tags";
if (tagsStr.length > 0) {
snippet.tags = [tagsStr componentsSeparatedByString:#","];
}
GTLRYouTube_Video *video = [GTLRYouTube_Video object];
video.status = status;
video.snippet = snippet;
[self uploadVideoWithVideoObject:video
resumeUploadLocationURL:nil];
}
- (void)uploadVideoWithVideoObject:(GTLRYouTube_Video *)video
resumeUploadLocationURL:(NSURL *)locationURL {
NSURL *fileToUploadURL = [NSURL fileURLWithPath:self.VideoUrlCri.path];
NSError *fileError;
NSLog(#"step");
if (![fileToUploadURL checkPromisedItemIsReachableAndReturnError:&fileError]) {
NSLog(#"exit");
return;
}
// Get a file handle for the upload data.
NSString *filename = [fileToUploadURL lastPathComponent];
NSString *mimeType = [self MIMETypeForFilename:filename
defaultMIMEType:#"video/mp4"];
GTLRUploadParameters *uploadParameters =
[GTLRUploadParameters uploadParametersWithFileURL:fileToUploadURL
MIMEType:mimeType];
uploadParameters.uploadLocationURL = locationURL;
GTLRYouTubeQuery_VideosInsert *query =
[GTLRYouTubeQuery_VideosInsert queryWithObject:video
part:#"snippet,status"
uploadParameters:uploadParameters];
query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket,
unsigned long long numberOfBytesRead,
unsigned long long dataLength) {
NSLog(#"upload progress");
};
GTLRYouTubeService *service = self.youTubeService;
_uploadFileTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRYouTube_Video *uploadedVideo,
NSError *callbackError) {
if (callbackError == nil) {
NSLog(#"uploaded");
} else {
NSLog(#"error %#",callbackError);
}
}];
}
- (NSString *)MIMETypeForFilename:(NSString *)filename
defaultMIMEType:(NSString *)defaultType {
NSString *result = defaultType;
NSString *extension = [filename pathExtension];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
(__bridge CFStringRef)extension, NULL);
if (uti) {
CFStringRef cfMIMEType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
if (cfMIMEType) {
result = CFBridgingRelease(cfMIMEType);
}
CFRelease(uti);
}
return result;
}
I obtain a 403 error in NSLog(#"error %#",callbackError); and i can't see error details because the are something like :
data=<7b226572 726f7222 3a7b2265 72726f72 73223a5b 7b22646f 6d61696e 223a2267 6c6f6261 6c222c22 72656173 6f6e223a 22696e73 75666669 ... 61676522 3a22496e 73756666 69636965 6e742050 65726d69 7373696f 6e227d7d>}
In google api console i have created a Client Oauth for my application bundle and an API key, i use those values for my connection, it seems they works correctly because i obtain a valid token. Anyway, there someone who can help me or point me in the right direction about this error? Or there someone who knows a working example about a video upload for IOS and not for MAC ? There something weird in my code? I can't find any help in documentation or google
I have use White Raccoon and Black Raccoon both to upload zip file on FTP server.
In White Raccoon I was not able to upload zip file, I always get serverTimeout error. So I tried to upload normal xml file with white raccoon, File is uploaded without any data(0 byte size). Here is the code
-(void)upload:(NSData*)data{
//the upload request needs the input data to be NSData
NSData * ourImageData = data;
//we create the upload request
//we don't autorelease the object so that it will be around when the callback gets called
//this is not a good practice, in real life development you should use a retain property to store a reference to the request
WRRequestUpload * uploadImage = [[WRRequestUpload alloc] init];
uploadImage.delegate = self;
//for anonymous login just leave the username and password nil
uploadImage.hostname = #"hostname";
uploadImage.username = #"username";
uploadImage.password = #"password";
//we set our data
uploadImage.sentData = ourImageData;
//the path needs to be absolute to the FTP root folder.
//full URL would be ftp://xxx.xxx.xxx.xxx/space.jpg
uploadImage.path = #"huge_test.zip";
//we start the request
[uploadImage start];
}
I am using this https://github.com/valentinradu/WhiteRaccoon
-As WhiteRaccoon is not working for me I have tried BlackRaccoon but it is not helping me to even upload a normal xml file, it just give me "Stream timed out with no response from server" error.
here is the code
- (IBAction) uploadFile :(NSData *)datas{
self.uploadData = [NSData dataWithData:datas];
//Here I am just Checking that DATA come from another method is proper or not. I got All thedata which I have passed from method
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents/test12121.xml"];
// Write the data to file
[datas writeToFile:path atomically:YES];
self.uploadFile = [[BRRequestUpload alloc] initWithDelegate: self];
//----- for anonymous login just leave the username and password nil
self.uploadFile.path = #"/test.xml";
self.uploadFile.hostname = #"hostname";
self.uploadFile.username = #"username";
self.uploadFile.password = #"password";
//we start the request
[self.uploadFile start];
}
- (long) requestDataSendSize: (BRRequestUpload *) request{
//----- user returns the total size of data to send. Used ONLY for percentComplete
return [self.uploadData length];
}
- (NSData *) requestDataToSend: (BRRequestUpload *) request{
//----- returns data object or nil when complete
//----- basically, first time we return the pointer to the NSData.
//----- and BR will upload the data.
//----- Second time we return nil which means no more data to send
NSData *temp = self.uploadData; // this is a shallow copy of the pointer
self.uploadData = nil; // next time around, return nil...
return temp;
}
-(void) requestFailed:(BRRequest *) request{
if (request == uploadFile)
{
NSLog(#"%#", request.error.message);
uploadFile = nil;
}
NSLog(#"%#", request.error.message);
}
-(BOOL) shouldOverwriteFileWithRequest: (BRRequest *) request
{
//----- set this as appropriate if you want the file to be overwritten
if (request == uploadFile)
{
//----- if uploading a file, we set it to YES
return YES;
}
//----- anything else (directories, etc) we set to NO
return NO;
}
- (void) percentCompleted: (BRRequest *) request
{
NSLog(#"%f completed...", request.percentCompleted);
}
-(void) requestCompleted: (BRRequest *) request
{
//----- handle Create Directory
if (request == uploadFile)
{
NSLog(#"%# completed!", request);
uploadFile = nil;
}
}
I am using https://github.com/lloydsargent/BlackRaccoon.
I have even changed the timeout limit upto 60 but its not working for me. Please anyone can help me?Anyone knows another way to upload zip file to FTP server, then please let me know.
Thanks in advance.
I'm trying to create a sharing extension using the new iOS 8 app extensions. I tried to get the current URL of a Safari site to show it in a UILabel. Simple enough.
I was working trough the official extension guide from apple here https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/Share.html#//apple_ref/doc/uid/TP40014214-CH12-SW1 but some things are not working as expected. I know it is only in beta but maybe I'm just doing something wrong.
Here is my code to get the URL from safari inside the extensions ViewController:
-(void)viewDidAppear:(BOOL)animated{
NSExtensionContext *myExtensionContext = [self extensionContext];
NSArray *inputItems = [myExtensionContext inputItems];
NSMutableString* mutableString = [[NSMutableString alloc]init];
for(NSExtensionItem* item in inputItems){
NSMutableString* temp = [NSMutableString stringWithFormat:#"%#, %#, %lu,
%lu - ",item.attributedTitle,[item.attributedContentText string],
(unsigned long)[item.userInfo count],[item.attachments count]];
for(NSString* key in [item.userInfo allKeys]){
NSArray* array = [item.userInfo objectForKey:#"NSExtensionItemAttachmentsKey"];
[temp appendString:[NSString stringWithFormat:#" in array:%lu#",[array count]]];
}
[mutableString appendString:temp];
}
self.myLabel.text = mutableString;
}
And this is the content of my Info.plist file of my Extension:
<dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>200</integer>
</dict>
</dict>
When I visit apples iPod support page in Safari and try to share it to my extension, I get following values but no URL:
item.attributedTitle = (null)
item.attributedContentText = "iPod - Apple Support"
item.userInfo.count = 2 (two keys: NSExtensionAttributedContentTextKey and
NSExtensionItemAttachmentsKey)
item.attachments.count = 0
The arrays inside the objects of the dictionary are always empty.
When I share the apple site with the system mail app the URL is posted to the message. So why is there no URL in my extension?
Below is how you can get the url. Notice the type identifier is kUTTypeURL and the block argument is NSURL. Also, the plist needs to be correct like mine also. The documentation was lacking and got help from number4 on the Apple dev forums. (you'll need to be registered and logged in to see it).
Code:
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error) {
self.urlString = url.absoluteString;
}];
}
Info.plist
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionPointName</key>
<string>com.apple.share-services</string>
<key>NSExtensionPointVersion</key>
<string>1.0</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
</dict>
I've solved it for myself. I was trying with Sharing Image.
- (void)didSelectPost {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
// Verify that we have a valid NSExtensionItem
NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject];
if(!imageItem){
return;
}
// Verify that we have a valid NSItemProvider
NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject];
if(!imageItemProvider){
return;
}
// Look for an image inside the NSItemProvider
if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) {
if(image){
NSLog(#"image %#", image);
// do your stuff here...
}
}];
}
// this line should not be here. Cos it's called before the block finishes.
// and this is why the console log or any other task won't work inside the block
[self.extensionContext completeRequestReturningItems:nil completionHandler:nil];
}
So what I did is just moved the [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; inside the block at the end of other tasks. The final working version look like this (Xcode 6 beta 5 on Mavericks OS X 10.9.4):
- (void)didSelectPost {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
// Verify that we have a valid NSExtensionItem
NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject];
if(!imageItem){
return;
}
// Verify that we have a valid NSItemProvider
NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject];
if(!imageItemProvider){
return;
}
// Look for an image inside the NSItemProvider
if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){
[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) {
if(image){
NSLog(#"image %#", image);
// do your stuff here...
// complete and return
[self.extensionContext completeRequestReturningItems:nil completionHandler:nil];
}
}];
}
// this line should not be here. Cos it's called before the block finishes.
// and this is why the console log or any other task won't work inside the block
// [self.extensionContext completeRequestReturningItems:nil completionHandler:nil];
}
I hope it'll work for URL sharing as well.
Your extension view controller should be adopting the NSExtensionRequestHandling protocol. One of this protocol's methods is:
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context
You should be waiting for this to be called before you attempt to get the NSExtensionContext. It even provides the context in the method as the context parameter.
This was outlined in this document.
All of those previous answers are really good but I just came accross this issue in Swift and felt it was a little tidious to extract the URL from a given NSExtensionContext especially in the CFString to String conversion process and the fact that the completionHandler in loadItemForTypeIdentifier is not executed in the main thread.
import MobileCoreServices
extension NSExtensionContext {
private var kTypeURL:String {
get {
return kUTTypeURL as NSString as String
}
}
func extractURL(completion: ((url:NSURL?) -> Void)?) -> Void {
var processed:Bool = false
for item in self.inputItems ?? [] {
if let item = item as? NSExtensionItem,
let attachments = item.attachments,
let provider = attachments.first as? NSItemProvider
where provider.hasItemConformingToTypeIdentifier(kTypeURL) == true {
provider.loadItemForTypeIdentifier(kTypeURL, options: nil, completionHandler: { (output, error) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
processed = true
if let url = output as? NSURL {
completion?(url: url)
}
else {
completion?(url: nil)
}
})
})
}
}
// make sure the completion block is called even if no url could be extracted
if (processed == false) {
completion?(url: nil)
}
}
}
That way you can now simply use it like this in your UIViewController subclass:
self.extensionContext?.extractURL({ (url) -> Void in
self.urlLabel.text = url?.absoluteString
println(url?.absoluteString)
})
The other answers are all complicated and incomplete. They only work in Safari and do not work in Google Chrome. This works both in Google Chrome and Safari:
override func viewDidLoad() {
super.viewDidLoad()
for item in extensionContext!.inputItems {
if let attachments = item.attachments {
for itemProvider in attachments! {
itemProvider.loadItemForTypeIdentifier("public.url", options: nil, completionHandler: { (object, error) -> Void in
if object != nil {
if let url = object as? NSURL {
print(url.absoluteString) //This is your URL
}
}
})
}
}
}
}
You need to be looking for an attachment of type kUTTypePropertyList. Do something like this with the first attachment of the first extension item in your extension:
NSExtensionItem *extensionItem = self.extensionContext.extensionItems.firstObject;
NSItemProvider *itemProvider = self.extensionItem.attachments.firstObject;
[itemProvider loadItemForTypeIdentifier:kUTTypePropertyList options:nil
completionHandler:^(NSDictionary *item, NSError *error) {
// Unpack items from "item" in here
}];
You'll also need to setup some JavaScript to pick up all of the data you need from the DOM. See the second extensions session from WWDC 14 for more details.
let itemProvider = item.attachments?.first as! NSItemProvider
itemProvider.loadItemForTypeIdentifier("public.url", options: nil) { (object, error) -> Void in
println(object)
}
so println :
http://detail.m.tmall.com/item.htm?id=38131345289&spm=a2147.7632989.mainList.5