iOS share extension does not receive data from Safari - ios

I have an iOS share extension that needs the URL of the opened web page. Everything works good, especially in a simulator. But on a real device I have around 20-30% cases where the extension does not receive any data i.e.:
NSExtensionItem *inputItem = self.extensionContext.inputItems.firstObject;
NSItemProvider *item = inputItem.attachments.firstObject;
[item loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
// here the error is sometimes not nil and thus the _baseURI ends up nil
_baseURI = [item[NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:#"baseURI"];
}];
The error code is -100 with description "No item available for requested type identifier.". This happens mainly when I open the extension several times in a row without changing/refreshing the web page in the Safari.
In those situations I see a device log saying "iPhone kernel[0] : Sandbox: MobileSafari(7033) deny(1) file-read-data /private/var/containers/Bundle/Application/.../bundle.js" where the bundle.js is the javascript with the ExtensionPreprocessingJS object. The bundle.js declares the ExtensionPreprocessingJS object like this (extracted the relevant part):
ExtensionPreprocessingJS = {
run: function(arguments){
arguments.completionFunction({
"baseURI": document.baseURI
})
},
finalize: function(arguments){
}
}
In this situation, it could some time happen that when the extension is closed the next time opening the share dialog in Safari shows my extension with no icon. This happens on my testing iPhone 5s and iPhone 6 with iOS 9.3.
I think that the missing data is because of the system could not read the extension's JavaScript file, but why could this happen?

If you read the documentation for:
loadItemForTypeIdentifier(_:options:completionHandler:)
You'll see that:
The type information for the first parameter of your completionHandler
block should be set to the class of the expected type. For example,
when requesting text data, you might set the type of the first
parameter to NSString or NSAttributedString. An item provider can
perform simple type conversions of the data to the class you specify,
such as from NSURL to NSData or NSFileWrapper, or from NSData to
UIImage (in iOS) or NSImage (in OS X). If the data could not be
retrieved or coerced to the specified class, an error is passed to the
completion block’s.
Try this code to see what you recieve:
[item loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(id item, NSError *error) {
// here the error is sometimes not nil and thus the _baseURI ends up nil
_baseURI = [item[NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:#"baseURI"];
}];
Note that item is not set to NSDictionary.

Related

How to get initial link for iOS using react native firebase dynamic links?

I am using react-native-firebase:: 5.6.0, I am having issue while getting initial link for iOS device. On android it's working fine. I am using "Firebase Dynamic Links" to redirect user inside login screen of my app if in case he is not logged in inside app, otherwise just opening app if he is already logged in.
It's working for android app but having an issue with ios app. I have used two function one is the get dynamic link if app is closed "getInitialLink" and another one is to check when app is opened "onLink".
This is function I am using after closing splash screen, only called once when opening app from closing state.
firebase.links().getInitialLink().then((url) => {
if (url && url === 'https://mycustomdomain.co.in') {
navigationToScreen(AUTH, INITIAL_SCREEN);
} else {
// INITIALIZE APP HERE
}
});
If app already opened I am getting dynamic link url value inside this function::
this.unsubscribeHandleOpenAppDynamicLinks = firebase.links().onLink(async (url) => {
let isLoggedIn = await AsyncStorage.getItem(LocalStorageKeys.IS_LOGGEDIN);
if (url) {
if ( isLoggedIn !== 'yes' && url === 'https://mycustomdomain.co.in') {
navigationToScreen(AUTH, INITIAL_SCREEN);
}
}
});
and clearing that listener on componentWillUnmount:: this.unsubscribeHandleOpenAppDynamicLinks();
In case of iOS only "onLink" function is working and I am
getting url value as "undefined". getInitialLink() function will
returns the URL that the app has been launched from. If the app was
not launched from a URL the return value will be null, but I am
getting "undefined" even when launching an app from url in case of iOS
only. I am getting url inside onLink() in case of iOS when app is
launched. Why this is happening??
Please suggest what I am doing wrong here.
If getInitialLink method does not work, it is either because of improper linking or due to Expo runtime. As an alternative, use Linking.getInitialURL method to get the initial URL. This requires a little bit of native code as well. This is because Linking module does not know how to interpret the shortened URL. So, we call the resolveShortLink method of Firebase SDK to get the embedded deep link. Once we receive the embedded deep link, we can handle it as usual in our app.
The native source code is documented in this article. But for completeness, I will post it here.
#import "FDLResolver.h"
#import <React/RCTLog.h>
#implementation FDLResolver
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(resolveShortLink:(NSString *)shortLink :(RCTPromiseResolveBlock)resolve
:(RCTPromiseRejectBlock)reject)
{
id completion = ^(NSURL *_Nullable dynamicLink, NSError *_Nullable error) {
if (!error && dynamicLink) {
resolve(dynamicLink.absoluteString);
} else {
reject(#"Error", #"Error in getting dynamic link", error);
}
};
NSURL *shortLinkURL = [NSURL URLWithString:shortLink];
[[FIRDynamicLinks dynamicLinks] resolveShortLink:shortLinkURL completion:completion];
}
#end
And Linking module code is below.
Linking.getInitialURL().then(url => {
if (url && url.includes('page.link')) {
const shortLink = url.replace('exps', 'https')
NativeModules.FDLResolver.resolveShortLink(shortLink)
.then(link => {
const linkParts = link.split('?')
const query = qs.parse(linkParts[1])
this.parseRouteUrl(query.deep_link_id)
})
}
})

Google API Bug - Attribution Inconsistency

Fetching photo data for a place using Google Places API HTTP web service call URL,
https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=<LOCATION>&radius=<RADIUS>&sensor=false&key=<API_KEY>
yields the expected callback. Here is the response from a request I am using for testing.
2017-09-19 11:20:32.968347-0400 <MY_APP_NAME>[7502:2809094] Places Found: (
{
...
...
photos = (
{
height = 5312;
"html_attributions" = (
"leah steen"
);
"photo_reference" = "CmRaAAAAcppPDrYGBnLqlyYQ_EMmsvgKXXpVjp0drGkJPWLBwlSzrWzuhtFFGCzQPl1pSez92QVktthtaMSsV1IlAWMIMPboycElhruKgUg-KFHWWrhLKOR6h8GGiR349rhokAFJEhApc7ghYIH0guUQBGJi1bKBGhQMcElN7-lSBzvCgAG3vVzANLkf6A";
width = 2988;
}
);
...
...
}
)
As you can see, this place has a photo dictionary containing an html_attributions value for the associated image, which I am handling appropriately throughout my application.
ISSUE:
When I fetch the photo data for the same place, by using the place_id, I get this for the photo and attribution:
2017-09-19 11:22:08.548253-0400 <MY_APP_NAME>[7502:2809094] photo data: (
"<UIImage: 0x17048b720> size {169, 300} orientation 0 scale 2.000000",
"leah steen{\n}"
)
I receive of course the same photo, but an attribution WITHOUT a link. As you can see from my other method using the web service, the photo for this place does indeed have an attribution with a link.
I am at a loss. How am I supposed to get the full attribution from Google if their iOS SDK does not return the same data?
CODE:
Using Google's other suggested method for fetching photo data for a place via the place_id you can obtain the image and the images attribution (if it has one).
I fetch the data using these two methods as Google asks:
- (void)lookUpPhotosForPlaceID:(NSString *)placeID
callback:(GMSPlacePhotoMetadataResultCallback)callback;
- (void)loadPlacePhoto:(GMSPlacePhotoMetadata *)photo
callback:(GMSPlacePhotoImageResultCallback)callback;
Here is the code I use for fetching the photo data.
// Look up photos for the place
[[GMSPlacesClient sharedClient] lookUpPhotosForPlaceID:placeID callback:^(GMSPlacePhotoMetadataList * _Nullable photos, NSError * _Nullable error) {
if (error != nil) {
response(error, NULL);
return;
}
if (photos.results.count > 0) {
GMSPlacePhotoMetadata *metaData = photos.results.firstObject; // Using the first photo
// Load the photo
[[GMSPlacesClient sharedClient] loadPlacePhoto:metaData constrainedToSize:<SOME_SIZE> scale:[UIApplication sharedApplication].keyWindow.screen.scale callback:^(UIImage * _Nullable photo, NSError * _Nullable error) {
if (error) {
response(error, NULL);
return;
}
NSArray *photoData = #[photo, metaData.attributions];
response(nil, photoData);
}];
}
else response(error, NULL);
}];
EDIT:
For reference, as Google does inform it's users that this data may be subject to change, and that multiple callbacks should return the same data. Yet multiple calls using different API's do not...
You can do couple of thing to verify correct behaviour:
You can switch back to first API and see whether it actually returns it again. Because doc says:
Multiple calls of this method will probably return the same photos
each time. However, this is not guaranteed because the underlying data
may have changed.
Second you can check the callback behaviour if you are losing some data when callback is called in line
NSArray *photoData = #[photo, metaData.attributions];
At this point it me be loosing attributed string but only retains plain text(leah steen).
Good way to check this metaData is before loadPlacePhoto:callback:

WatchConnectivity file transfer not working

I am using WatchConnectivity to transfer an image from iOS to Watch OS. When debugging in simulator I am facing a problem
The file is transferred successfully as I can see in (sender side i.e. iOS)
public func session(session: WCSession, didFinishFileTransfer fileTransfer: WCSessionFileTransfer, error: NSError?)
Now from XCode I stop iOS simulator, change target to Watch App, Ctrl+Run Watch App (just run, no build).The below method is called.
public func session(session: WCSession, didReceiveFile file: WCSessionFile)
At last I do
NSFileManager.defaultManager().moveItemAtURL(file.fileURL, toURL: destinationFileURL)
This call throws because there is no file at file.fileURL (which I checked in my MAC also).
The file.fileURL.path! is like this
/Users/<user name>/Library/Developer/CoreSimulator/Devices/DAD8E150-BAA7-43E0-BBDD-58FB0AA74E80/data/Containers/Data/PluginKitPlugin/2CB3D46B-DDB5-480C-ACF4-E529EFBA2657/Documents/Inbox/com.apple.watchconnectivity/979DC929-E1BA-4C24-8140-462EC0B0655C/Files/EC57EBB8-827E-487E-8F5A-A07BE80B3269/image
Any clues?
In actual I am transferring 15-20 images in loop.
Sometime when not debugging I noticed that few image (not all) show
up in watch simulator (also in actual watch). I have no idea what is
happening with WC.
No problem in transferring user info dictionary.
I found the problem. I was dispatching some code to main thread and the file move code was also inside that. WC framework clean up the file just after this method ends so the file must be moved before this function returns. I moved that code outside performInMainThread block and everything is working like charm.
public func session(session: WCSession, didReceiveFile file: WCSessionFile)
{
// Move file here
performInMainThread { () -> Void in
// Not here
}
}
As the Apple documentation of WCSessionDelegate Protocol Reference says regarding
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file
when getting back the (WCSessionFile *)file parameter:
The object containing the URL of the file and any additional
information. If you want to keep the file referenced by this
parameter, you must move it synchronously to a new location during
your implementation of this method. If you do not move the file, the
system deletes it after this method returns.
So the best to move it ASAP to a new location. It's safe as the system keeps the reference and doesn't delete it during the move.
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *cacheDir = [[NSHomeDirectory() stringByAppendingPathComponent:#"Library"] stringByAppendingPathComponent:#"Caches"];
NSURL *cacheDirURL = [NSURL fileURLWithPath:cacheDir];
if ([fileManager moveItemAtURL: file.fileURL toURL:cacheDirURL error: &error]) {
//Store reference to the new URL or do whatever you'd like to do with the file
NSData *data = [NSData dataWithContentsOfURL:cacheDirURL];
}
else {
//Handle the error
}
}
WARNING! You have to be careful with the thread handling as delegates of WCSession run in the background queue so you have to switch to main queue if you'd like to work with UI.

Unpinning HomeKit accessory

The HomeKit Accessory simulator as a wee little botton that allows to unfair the device.
Given it's an accessory simulator I presume that there are some hardware programming specifications that allow to write an un-pairing function.
However, it could make sense to implement this also as part of the HomeKit framework and allow client developers to do it via the HomesController class (and derivates).
--> short version:
Is there any method in HMService or HMCharacteristic or in the HomeKit framework to unpair a characteristic/service from a home?
Service or Characteristic can not be unpair, practically its not require to do.
One can unpair Accessory from home.
You can call method of HMHome class to remove/unpair accessory.
- (void)removeAccessory:(HMAccessory *)accessory
completionHandler:(void (^)(NSError *error))completion;
And Pairing - Reset button is given in Homekit Accessory Simulator for an option, you can always unpair the accessory from an iOS app.
Reset may require if you don't have the same device at a time or the Homekit configuration is reseted from settings in iOS app.
Unpairing is equivalent to essentially removing that accessory from under a HomeKit home (HMHome). That can be achieved by using the api call under HMHome (could be triggered by a button click from your view controller let's say):
Sample code:
[yourHMHome removeAccessory:accessory_ completionHandler:^(NSError *error) {
if (error) {
//Handle your error here.
} else {
//Removing, or here, un-pairing was successful, do anything else
//Example
[[YourExampleHomeStore sharedStore] updateAccessoryDelegates];
YourCompletionHandler(error);
}
}];
That answers your question regarding un-pairing. HMService and HMCharacteristic is conceptually different. If you'd like to update the value of a (writeable) HMCharacteristic, then you'd search for that HMCharacteristic in the available HMServices. Remember, the available HMServices and thus HMCharacteristics on an accessory will be completely different based on its paired state. Most read or writes and 'paired-read' and 'paired-write', and when in un-paired state, you can only read the 'accessory information service' (name, model etc.)
Now, presuming that you'd like to read or change (write) the value of a HMCharacteristic, let's say it's the serial number, the steps would be:
Get the pertinent HMCharacteristic
Sample code for serial number could be something like this:
Note: Returns empty string if accessory is unreachable.
+(NSString *)getSerialNumberFromHMAccessory:(HMAccessory *)accessory
{
if (!accessory || !accessory.reachable) { return #""; }
for (HMService *service in accessory.services) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%K like %#", #"characteristicType", HMCharacteristicTypeSerialNumber];
NSArray *result = [service.characteristics filteredArrayUsingPredicate:predicate];
if (result && [result count] > 0 && [result[0] isKindOfClass:[HMCharacteristic class]]) {
HMCharacteristic *serialNumChar = (HMCharacteristic *)result[0];
NSString *serialNum = [serialNumChar valueForKey:#"value"];
if (serialNum && [serialNum length] > 0) {
NSLog(#"Found serial number: %# for accessory named \"%#\"", serialNum, accessory.name);
return serialNum;
}
}
}
return #"";
}
Once you've found your desired HMCharacteristic, you can make HomeKit api calls to further your interest (like update value etc.). Here, since serial number is not writeable, you need to be content with just reading it.
Anyway, probably ranted a tad too long and digressed from the main question. Hope this helps you man. Cheers.

removeTracksFromPlaylist not removing tracks with ios spotify sdk

I was testing this method to remove tracks from a playlist. Basically I modified the demo project "simple track playback" provided with the SDK. I wanted to remove the track form the playlist when you hit fastForward. I changed the fastForward method this way but it's not doing anything, and error is nil.
-(IBAction)fastForward:(id)sender {
if([self.player isPlaying] && self.currentPlaylistSnapshot){
SPTAuth *auth = [SPTAuth defaultInstance];
[self.currentPlaylistSnapshot removeTracksFromPlaylist:#[self.player.currentTrackURI]
withAccessToken:auth.session.accessToken
callback:^(NSError *error) {
if (error != nil) {
NSLog(#"*** Failed to remove track : %#", self.titleLabel.text);
return;
}
}];
}
[self.player skipNext:nil];
}
self.currentPlaylistSnapshot is the one I've got from the handleNewSession method.
There's also a static method apparently offering something similar which I have't tried yet.
createRequestForRemovingTracks:fromPlaylist:withAccessToken:snapshot:error:
According to the documentation both options are implemented asynchronously and will take seconds to reflect the results in the server but I'm suspecting that there's either something wrong or I'm just missing to do an actual request to push the changes on the local snapshot maybe?
Documentation:
https://developer.spotify.com/ios-sdk-docs/Documents/Classes/SPTPlaylistSnapshot.html#//api/name/removeTracksWithPositionsFromPlaylist:withAccessToken:callback:
ios sdk:
https://github.com/spotify/ios-sdk
I solved my issue by reseting simulator + adding SPTAuthPlaylistModifyPublicScope (which I fogot to do...)
auth.requestedScopes = #[SPTAuthStreamingScope, SPTAuthPlaylistModifyPublicScope];

Resources