Skip to content

Instantly share code, notes, and snippets.

@ewieberappc
Last active September 14, 2018 16:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ewieberappc/cd79eb35dc615dd5f118bec6d51de395 to your computer and use it in GitHub Desktop.
Save ewieberappc/cd79eb35dc615dd5f118bec6d51de395 to your computer and use it in GitHub Desktop.
A basic Share Extension Test created by combining two simple test cases. Contains the ShareViewController for the extension and the index controller and view for the Titanium Project
var appGroupId = 'group.com.YourGroupId';
var props = Ti.App.iOS.createUserDefaults({
suiteName: appGroupId
});
var score = props.getInt('score');
if (!score)
score = 1;
props.setInt('score', score);
// Get the shared container path
var sharedPath = Ti.Filesystem.directoryForSuite(appGroupId);
// Copy a file from the application to the shared container
var appFile = Ti.Filesystem.getFile(sharedPath, 'somesharefile.txt');
Ti.API.error(appFile);
function doClick(e) {
score = props.getInt('score');
Ti.API.error("++++++++++ score was: "+score);
score += 1;
props.setInt('score', score);
Ti.API.error("++++++++++ score is: "+props.getInt('score'));
if (appFile.createFile())
appFile.write("Hello");
Ti.API.error("++++++++++ data was: "+appFile.read());
appFile.write($.text.value);
Ti.API.error("++++++++++ data is: "+appFile.read());
}
function getImages(e) {
var dir = Ti.Filesystem.getFile(sharedPath);
var dir_files = dir.getDirectoryListing();
var views = [];
for (var i=0;i<dir_files.length;i++){
if (dir_files[i].indexOf("image") == -1)
continue;
var view = Ti.UI.createImageView({
image: Ti.Filesystem.getFile(sharedPath, dir_files[i])
});
views.push(view);
}
$.scroll.views = views;
}
$.index.open();
<Alloy>
<Window class="container">
<TextField id="text" borderColor="black" top="30%" value="This is from the app" />
<Button onClick="doClick" top="40%">OK</Button>
<Button onClick="getImages" top="50%">Get Images</Button>
<ScrollableView id="scroll" showPagingControl="true" top="60%" height="40%"/>
</Window>
</Alloy>
A basic Share Extension Test created by combining two simple test cases. Contains the ShareViewController for the extension and the index controller and view for the Titanium Project
#import "ShareViewController.h"
@import MobileCoreServices;
static NSString *const AppGroupId = @"group.com.YourGroupId";
static NSString *const AppName = @"YourAppName";
@interface ShareViewController ()
@end
@implementation ShareViewController
NSUInteger m_inputItemCount = 0;
NSString * m_invokeArgs = NULL;
NSUserDefaults *sharedUserDefaults;
NSURL *groupURL;
NSExtensionItem *inputItem;
- (BOOL)isContentValid
{
// Do validation of contentText and/or NSExtensionContext attachments here
return YES;
}
-(void)viewDidLoad
{
// Get and print the Score key from user defaults
sharedUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroupId];
NSNumber *score = [NSNumber numberWithDouble:[sharedUserDefaults doubleForKey:@"score"]];
NSLog(@"++++++++++ score was: %@", score);
// Read and print the contents of 'somesharefile.txt' in the shared container
groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroupId];
NSString *docPath = [groupURL.path stringByAppendingPathComponent:@"somesharefile.txt"];
NSString *oldData = [[NSString alloc] initWithData:[NSData dataWithContentsOfFile:docPath] encoding:NSUTF8StringEncoding];
NSLog(@"++++++++++ data was: %@", oldData);
}
- (void)didSelectPost
{
// Write and print the contents of 'somesharefile.txt' in the shared container
NSString *docPath = [groupURL.path stringByAppendingPathComponent:@"somesharefile.txt"];
NSString *data = @"I came from the extension";
[data writeToFile:docPath atomically:YES encoding:NSUTF8StringEncoding error:NULL];
data = [[NSString alloc] initWithData:[NSData dataWithContentsOfFile:docPath] encoding:NSUTF8StringEncoding];
NSLog(@"++++++++++ data is: %@", data);
// Check the attachment type
inputItem = self.extensionContext.inputItems.firstObject;
NSItemProvider *urlItemProvider = [[inputItem.userInfo valueForKey:NSExtensionItemAttachmentsKey] objectAtIndex:0];
if ([urlItemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeURL])
{
// Handle a URL attachment. Write it to user defaults
NSLog(@"++++++++++ Attachment is a URL");
[urlItemProvider loadItemForTypeIdentifier:(__bridge NSString *)kUTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error)
{
NSLog(@"Error occured");
}
else
{
NSMutableArray *arrSites;
if ([sharedUserDefaults valueForKey:@"SharedExtension"])
arrSites = [sharedUserDefaults valueForKey:@"SharedExtension"];
else
arrSites = [[NSMutableArray alloc] init];
NSDictionary *dictSite = [NSDictionary dictionaryWithObjectsAndKeys:self.contentText, @"Text", url.absoluteString, @"URL",nil];
[arrSites addObject:dictSite];
[sharedUserDefaults setObject:arrSites forKey:@"SharedExtension"];
[sharedUserDefaults synchronize];
}
}];
} else {
NSLog(@"++++++++++ Attachment is not a URL");
[ self passSelectedItemsToApp ];
}
}
- (NSArray *)configurationItems {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return @[];
}
- ( void ) addImagePathToArgumentList: ( NSString * ) imagePath
{
assert( NULL != imagePath );
// The list of arguments we will pass to the AIR app when we invoke it.
// It will be a comma-separated list of file paths: /path/to/image1.jpg,/path/to/image2.jpg
if ( NULL == m_invokeArgs )
{
m_invokeArgs = imagePath;
}
else
{
m_invokeArgs = [ NSString stringWithFormat: @"%@,%@", m_invokeArgs, imagePath ];
}
}
- ( NSString * ) saveImageToAppGroupFolder: ( UIImage * ) image
imageIndex: ( int ) imageIndex
{
assert( NULL != image );
NSData * jpegData = UIImageJPEGRepresentation( image, 1.0 );
NSString * documentsPath = groupURL.path;
// Note that we aren't using massively unique names for the files in this example:
NSString * fileName = [ NSString stringWithFormat: @"image%d.jpg", imageIndex ];
NSString * filePath = [ documentsPath stringByAppendingPathComponent: fileName ];
[ jpegData writeToFile: filePath atomically: YES ];
[sharedUserDefaults synchronize];
return filePath;
}
- ( void ) passSelectedItemsToApp
{
NSExtensionItem * item = self.extensionContext.inputItems.firstObject;
// Reset the counter and the argument list for invoking the app:
m_invokeArgs = NULL;
m_inputItemCount = item.attachments.count;
// Iterate through the attached files
for ( NSItemProvider * itemProvider in item.attachments )
{
// Check if we are sharing a Image
if ( [ itemProvider hasItemConformingToTypeIdentifier: ( NSString * ) kUTTypeImage ] )
{
NSLog(@"++++++++++ Attachment is an Image");
// Load it, so we can get the path to it
[ itemProvider loadItemForTypeIdentifier: ( NSString * ) kUTTypeImage
options: NULL
completionHandler: ^ ( UIImage * image, NSError * error )
{
static int itemIdx = 0;
if ( NULL != error )
{
NSLog( @"There was an error retrieving the attachments: %@", error );
return;
}
// The app won't be able to access the images by path directly in the Camera Roll folder,
// so we temporary copy them to a folder which both the extension and the app can access:
NSString * filePath = [ self saveImageToAppGroupFolder: image imageIndex: itemIdx ];
// Now add the path to the list of arguments we'll pass to the app:
[ self addImagePathToArgumentList: filePath ];
// If we have reached the last attachment, it's time to hand control to the app:
if ( ++itemIdx >= m_inputItemCount )
{
[ self invokeApp: m_invokeArgs ];
}
} ];
}
else
{
NSLog(@"++++++++++ Attachment is not an Image");
}
}
}
- ( void ) invokeApp: ( NSString * ) invokeArgs
{
// Prepare the URL request
// this will use the custom url scheme of your app
// and the paths to the photos you want to share:
NSString * urlString = [ NSString stringWithFormat: @"%@://%@", AppName,( NULL == invokeArgs ? @"" : invokeArgs ) ];
NSURL * url = [ NSURL URLWithString: urlString ];
NSString *className = @"UIApplication";
if ( NSClassFromString( className ) )
{
id object = [ NSClassFromString( className ) performSelector: @selector( sharedApplication ) ];
[ object performSelector: @selector( openURL: ) withObject: url ];
}
// Now let the host app know we are done, so that it unblocks its UI:
[ super didSelectPost ];
}
@end
<!-- Replace items in [] -->
<ios>
<team-id>[TEAM ID HERE]</team-id>
<capabilities>
<app-groups>
<group>[group.com.YourGroupId]</group>
</app-groups>
</capabilities>
<extensions>
<extension projectPath="[Path to extension's .xcodeproj]">
<target name="[Target Name]">
<provisioning-profiles>
<device>[Device-UDID]</device>
<dist-appstore/>
<dist-adhoc/>
</provisioning-profiles>
</target>
</extension>
</extensions>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<plist>
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>[com.Your.AppId]</string>
</dict>
</array>
</dict>
</plist>
</ios>
@Nostariel
Copy link

Hi,

I am trying to implement this, and I already got it working if the app is not in background mode, but otherwise, I am getting an error. There is something about the code that I don't really get. When you call the performSelector function, where is that openURL handler?

I think that my error has something to do with this. This is the error log:

[ERROR] : The application has crashed with an uncaught exception 'NSInvalidArgumentException'.
[ERROR] : Reason:
[ERROR] : Expected 'sourceApplication' to be NSString. Please verify you are passing in 'sourceApplication' from your app delegate (not the UIApplication* parameter). If your app delegate implements iOS 9's application:openURL:options:, you should pass in options[UIApplicationOpenURLOptionsSourceApplicationKey].
[ERROR] : Stack trace:
[ERROR] : 0 CoreFoundation 0x0000000116043af3 __exceptionPreprocess + 147
[ERROR] : 1 libobjc.A.dylib 0x00000001155df141 objc_exception_throw + 48
[ERROR] : 2 Caramba 0x000000010ebf869f __77-[FBSDKApplicationDelegate application:openURL:sourceApplication:annotation:]_block_invoke + 0
[ERROR] : 3 CoreFoundation 0x0000000115fe2c2c CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER + 12
[ERROR] : 4 CoreFoundation 0x0000000115fe2b29 _CFXRegistrationPost + 425
[ERROR] : 5 CoreFoundation 0x0000000115fe2892 ___CFXNotificationPost_block_invoke + 50
[ERROR] : 6 CoreFoundation 0x0000000115fa6162 -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1826
[ERROR] : 7 CoreFoundation 0x0000000115fa52c1 _CFXNotificationPost + 673
[ERROR] : 8 Foundation 0x000000010fb26b24 -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
[ERROR] : 9 Caramba 0x000000010eb249c3 -[TiApp application:openURL:options:] + 403
[ERROR] : 10 UIKit 0x0000000111462a1d __45-[UIApplication _applicationOpenURL:payload:]_block_invoke + 813
[ERROR] : 11 UIKit 0x0000000111462456 -[UIApplication _applicationOpenURL:payload:] + 707
[ERROR] : 12 UIKit 0x000000011146f6c5 -[UIApplication _handleNonLaunchSpecificActions:forScene:withTransitionContext:completion:] + 6705
[ERROR] : 13 UIKit 0x0000000111474383 -[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:] + 2406
[ERROR] : 14 UIKit 0x0000000111473786 -[UIApplication _handleApplicationLifecycleEventWithScene:transitionContext:completion:] + 461
[ERROR] : 15 UIKit 0x0000000111455921 __70-[UIApplication scene:didUpdateWithDiff:transitionContext:completion:]_block_invoke + 159
[ERROR] : 16 UIKit 0x0000000111455599 -[UIApplication scene:didUpdateWithDiff:transitionContext:completion:] + 978
[ERROR] : 17 UIKit 0x0000000111973d9c -[UIApplicationSceneClientAgent scene:handleEvent:withCompletion:] + 509
[ERROR] : 18 FrontBoardServices 0x000000011d955f81 __80-[FBSSceneImpl updater:didUpdateSettings:withDiff:transitionContext:completion:]_block_invoke.376 + 205
[ERROR] : 19 FrontBoardServices 0x000000011d9835f6 FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK + 24
[ERROR] : 20 FrontBoardServices 0x000000011d98346d -[FBSSerialQueue _performNext] + 186
[ERROR] : 21 FrontBoardServices 0x000000011d9837f6 -[FBSSerialQueue _performNextFromRunLoopSource] + 45
[ERROR] : 22 CoreFoundation 0x0000000115fe9c01 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION + 17
[ERROR] : 23 CoreFoundation 0x0000000115fcf0cf __CFRunLoopDoSources0 + 527
[ERROR] : 24 CoreFoundation 0x0000000115fce5ff __CFRunLoopRun + 911
[ERROR] : 25 CoreFoundation 0x0000000115fce016 CFRunLoopRunSpecific + 406
[ERROR] : 26 GraphicsServices 0x000000011796fa24 GSEventRunModal + 62
[ERROR] : 27 UIKit 0x00000001114580d4 UIApplicationMain + 159
[ERROR] : 28 Caramba 0x000000010e94fd44 main + 100
[ERROR] : 29 libdyld.dylib 0x000000011a1af65d start + 1

In a native code, usually this error would be related to the app delegate openURL method, but in Titanium I don't know how to solve this, I have spent 2 days already.

Could you help me, please?

Thank you very much!

@qasim90
Copy link

qasim90 commented Jun 4, 2018

Great. Would have been good if tiapp.xml was included too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment