UIDocument open/close behavior - ios

I have a UIDocument based app, without iCloud support. The user is able to create documents, and save them to app's Document directory.
I have also created few "sample" documents, and passing them with the app's bundle. I want the user to be able to open sample documents the same way as they can open their own documents:
NSArray *samplesContent = [[NSBundle mainBundle] URLsForResourcesWithExtension:#"doco" subdirectory:#"Samples"];
for (NSURL *sampleURL in samplesContent) {
Doco *doco = [[Doco alloc] initWithFileURL:sampleURL];
[doco disableEditing];
[doco openWithCompletionHandler:^(BOOL success) {
if (success) {
[self.docos addObject:doco];
if (self.docos.count >= samplesContent.count) {
[self.tableView reloadData];
}
} else DLog(#"Failed to open %#", [sampleURL pathExtension]);
}];
}
What I am seeing is this:
On viewDidLoad, docs array gets populated with sample docs and displayed in the tableView the first time app launches. No issues here, all is smooth and nice.
Then I back out of the view with the following code to close all open docs:
int count = 0;
for (Doco *doco in self.docos) {
if (doco.documentState == UIDocumentStateNormal) {
[doco closeWithCompletionHandler:nil];
count++;
}
}
DLog(#"Closed %i docs", count);
When I open the view again, the array of docs should get populated again and tableView re-populated, but nothing happens.
The completion handler below never gets called, although the URL is pointing to the same file and it is valid:
[doco openWithCompletionHandler:^(BOOL success) {}
I do not have this issue for user generated docs stored in Documents, so my assumption is that it has something to do with auto-save, that gets called on read-only bundle and fails
But I am sort of stuck on this part, any help will be appreciated.

The problem has already been identified, but I think it's worth describing a couple of simple solutions since including sample documents in an app's bundle is not uncommon.
So the problem is that the sample document is trying to save changes when it is closed, but saving cannot succeed in a read-only app bundle.
I think there are two main solutions here:
Copy the sample document into the Documents directory, where it can be treated like any other document and saved successfully (if you want user edits to the sample document to be saved, use this approach).
Prevent the document from trying to save (for read-only sample documents).
So here are some simple examples...
1. Copy sample documents into Documents directory
On first launch (or indeed whenever you decide to 'refresh' the sample document), use NSFileManager to copy the file into place:
- (void)refreshSampleDocuments
{
NSArray *sampleFromURLs = [[NSBundle mainBundle] URLsForResourcesWithExtension:#"doc" subdirectory:#"Samples"];
for (NSURL *sampleFromURL in sampleFromURLs) {
NSString *sampleFilename = [sampleFromURL lastPathComponent];
NSURL *sampleToURL = [[self documentsDirectoryURL] URLByAppendingPathComponent:sampleFilename];
// ...
// Do some checks to make sure you won't write over any user documents!
// ....
NSError *error;
BOOL copySuccessful = [[NSFileManager defaultManager] copyItemAtURL:sampleFromURL toURL:sampleToURL error:&error];
if (!copySuccessful) {
// Handle error...
}
}
}
2. Prevent sample documents from trying to save
This approach is much simpler (for read-only documents), and easier than trying to prevent updates wherever they might occur in the document.
When closeWithCompletionHandler: is called on UIDocument, autosaveWithCompletionHandler: is invoked to ensure the document file is saved before closing. This in turn invokes hasUnsavedChanges to decide whether a save is necessary. So if hasUnsavedChanges returns NO, then any invocation of the autosaving mechanism will result in no changes being written out.
(N.B. manually calling saveToURL:forSaveOperation:completionHandler: will still force a save, regardless of what hasUnsavedChanges returns.)
So in your UIDocument subclass, override hasUnsavedChanges to return NO if the document is read-only.
#interface MyDocument : UIDocument
#property(nonatomic, getter = isReadOnly) BOOL readOnly;
#end
#implementation MyDocument
- (BOOL)hasUnsavedChanges
{
return [self isReadOnly] ? NO : [super hasUnsavedChanges];
}
#end

If UIDocument is updated it will try to save changes on close.
Since UIDocument was loaded from read-only bundle, I had to make sure that it does not get updated, otherwise close block returns success=NO, and document is not closed...

Related

Shared Core Data - What am I missing?

I have an app with a Today extension. Using App Groups, I am able to have a single repository for both the app and the extension. However, I must be missing something as the solution only partially works. If I am in the App I can add a record and will see that same record in the widget. However, if a change the value of a column, for example, setting a boolean from true to false. The app won't see the change if it were made in the extension and vice versa. I am saving the change to Core Data:
_record?.managedObjectContext?.save()
Using DB Browser, I am able to verify that the change was made; is in the DB. Clearly, I am missing something. Any ideas would be appreciated.
Make sure you're using the same db in both sides
- (void)viewDidLoad {
[super viewDidLoad];
if ([self.extensionContext respondsToSelector:#selector(setWidgetLargestAvailableDisplayMode:)]) { // iOS 10+
[self.extensionContext setWidgetLargestAvailableDisplayMode:NCWidgetDisplayModeExpanded];
} else {
self.preferredContentSize = CGSizeMake(0, 110.0); // iOS 10-
}
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreAtURL:[MagicalRecord urlAppGroupStore]];
}
As you can see i'm using magical record but I'm specifying to use the share sqlite file
NSString *const kAppGroupIdentifier = #"group.com.carlosduclos.myapp";
+ (NSURL *)urlAppGroupStore {
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kAppGroupIdentifier];
return [groupURL URLByAppendingPathComponent:#"mydb.sqlite"];
}

iMessageExt app Error starting application

I created my iMessage extension, when I try to open it, the first screen appears but it is totally frozen, and it does not react in any way.
I've put logs in the viewDidLoad of that first view and nothing appears there, after a few seconds I can already see those logs.
To make the application freezing lose that status, user has to slide screen left or right and back again.
I've tried looking all over the web for someone who happens to be the same, but I could not find anything.
It does not come to mind more screenshots or portions of code add, if you think I should provide some additional information, just let me know
Any help would be appreciated.
Thank you.
UPDATE:
This is my Project Structure.
This is my viewDidLoad code.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"here viewDidLoad iMessage ext~~~!");
[self applyCornerRadiusToBtn];
[self registerPresentationAction];
NSDictionary *user = [self getUserInfoFromHostApp];
if (user) {
NSLog(#"Here != null user info");
//It is assumed that when you enter this point and run this log, the app should navigate to the next screen, but it does not.
[self performSegueWithIdentifier:#"goToYoutubeListIm" sender:nil];
} else {
NSLog(#"Here userInfo null");
}
}
- (NSDictionary *)getUserInfoFromHostApp
{
NSUserDefaults *myDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.com.xxxxx"];
NSDictionary *userNameSaved = [myDefaults objectForKey:#"userInfoExt"];;
NSLog(#"userNameSaved in xxxx Ext ==> %#",userNameSaved);
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.xxxx"];
NSLog(#"groupURL ==> %#",groupURL);
return userNameSaved;
}
For all concerned I have found the problem or problems to be accurate.
1) I was creating my controllers type MSMessagesAppViewController. Apparently there should only be one controller of this type.
2) I had logic in the viewDidAppear in my MSMessagesAppViewController. For some strange reason this also caused the problem, I had to get the logic out there and force the user to interact with a button to execute the logic that was in the didAppear

How to reinit AVPlayerItem?

I am initializing AVPlayerItem with "initWithUrl:" method.
Issue is that when initialization is interrupted (e.g. connection is lost)
Let's say we have following:
self.avPlayerItem = [[AVPlayerItem alloc] initWithUrl:url];
1.
What should be done? What happens to instance of avPlayerItem when connection is lost meanwhile?
Is there some protocol to implement in order to identify if initialization was successful or not?
2.
What I have noticed is that once initialization is interrupted then next "successful" initialization of avPlayerItem doesn't have tracks at all (they are present when no interruption is done before)
To initialize avPlayerItem with tracks again from the source user needs to close and open the app again.
When dealing with code that relies on internet connection you have to make sure there are failsafes that kick in when something goes wrong, because at some point it will.
What should be done?
1) Perform all code that relies on internet connection on a background thread. You don't want to block up the UI.
2) If possible verify the downloaded file before using.
There are different ways of going about this but off the top of my head I think I would use NSURLConnection or similar API to download the file into a temp folder. ON A BACKGROUND THREAD.
When downloaded I would initialise an AVAsset using the temp URL. AVAsset has some nice properties like playable that will help you check that the file downloaded OK. (NSURLConnectionDelegate also has a method that notifies if there was an error downloading.)
If you've got this far then you can create an AVPlayerItem with your AVAsset and away you go. Remember to wipe the contents of your temp folder at some point if you're not hanging on to the downloaded content.
Remember that you want to play your file on the main thread but all other loading and checking is probably best done on a background thread; you definitely want to use NSURLConnection from a background thread.
Store the AVPlayerItem in an NSPurgeableData object first, and play from that; store the data object in an NSCache object to automatically evict the object from memory after it has played, or when the connection is dropped, and the former AVPlayerItem is replaced by a new one (all of these things you should be doing anyway, regardless of the particular problem you describe). Here's some code to get you started:
void (^cachePlayerItem)(AVPlayerItem *, NSCache *, NSString *) = ^(AVPlayerItem *playerItem, NSCache *cache, NSString *key) {
NSURL *fileURL = [(AVURLAsset *)playerItem.asset URL];
NSPurgeableData *data = [NSPurgeableData dataWithContentsOfURL:fileURL];
[data beginContentAccess];
[cache setObject:data forKey:key];
[data endContentAccess];
[data discardContentIfPossible];
};
Put this block anywhere in an implementation file, defining it in the header file with:
typedef void (^cachePlayerItemBlock)(AVPlayerItem *, NSCache *, NSString *);
Call it within a method with:
cachePlayerItem(playerItem, playerItems, phAsset.localIdentifier);
Whereas, playerItem is the AVPlayerItem, playerItems is the NSCache cache, and, depending on what kind of asset from which you are acquiring the AVPlayerItem, an identifier of some kind unique to it (or, in the example above, its associated asset).
By the way, I set up my caches in AppDelegate accordingly:
- (NSCache *)thumbnailCache {
__block NSCache *t = self->_thumbnailCache;
if (!t) {
t = [[NSCache alloc] init];
[t setName:#"Thumbnail Cache"];
[t setEvictsObjectsWithDiscardedContent:TRUE];
[t setCountLimit:self.assetsFetchResults.count];
self->_thumbnailCache = t;
}
return t;
}
This ensures not only global access to, but also one instance of, the cache, which is particularly important when caching AVPlayerItems, as they can be large in size.
To create a globally accessible cache in AppDelegate, add these to your header and implementation files, respectively:
+ (AppDelegate *)sharedAppDelegate;
...and:
+ (AppDelegate *)sharedAppDelegate
{
return (AppDelegate *)[[UIApplication sharedApplication] delegate];
}
To reference the cache in other class files, import the AppDelegate header file, and, if desired, create a shortcut to the AppDelegate's sharedApplication method:
#import "AppDelegate.h"
#define AppDelegate ((AppDelegate *)[[UIApplication sharedApplication] delegate])
Insodoing, the cache can be referenced by...:
AppDelegate.thumbnailCache
...instead of:
AppDelegate.sharedAppDelegate.thumbnailCache

Unable to enumerate groups/results while using ALAssetsLibrary in static library

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);
}];

iCloud - renaming open documents on another device sometimes fails

The issue: I'm working on an iCloud document on device A, e.g. iPod Touch. Then I change the name of the document on device B, e.g. my Mac (via the Finder). The change goes up to the cloud and after a pause device A gets to hear about it.
And then:
some of the time all is just fine - I pick up the change of name via a changed fileURL property and can update my interface accordingly - the document continues to behave just as it should
some of the time, the document's fileURL is returned as something such as: file://localhost/var/mobile/Library/Mobile%20Documents/.ubd/peer-43A0AEB6-84CE-283E-CA39-FCC4EF3BC8F8-v23/ftr/purg-012fdcfbe3b3bbce6e603fdfd2f000b2cb28649e95 Not surprisingly this file won't save.
Can anyone explain what is going on and how to work around it?
Background
The name change is picked up fine by by NSMetadataQuery. So, for e.g., I can rename documents that are not open and all my iCloud functionality works fine. The issue only seems to occur with open documents.
Other iCloud features are working fine, e.g. I can change content on one device, e.g. my Mac, and detect and then update my interface on another device, e.g. my iPod Touch, that has the relevant iCloud document open.
I first spotted this when I added an override for presentedItemDidMoveToURL: to my UIDocument subclass. The override reliably picks up name changes made in the cloud, e.g. renaming the document on another device. Then sometimes newURL is the final expected URL for the renamed document, i.e. something sensible from which I can extract the new name use `lastPathComponent', update my interface etc. On other occasions newURL is a document in some other directory with a last path component beginning 'purg-', e.g. purg-012fdcfbe3b3bbce6e603fdfd2f000b2cb28649e95.
- (void) presentedItemDidMoveToURL:(NSURL *) newURL;
{
[super presentedItemDidMoveToURL: newURL];
if ([(id)[self delegate] respondsToSelector:#selector(documentNameChanged:)])
{
[[self delegate] documentNameChanged: self];
}
}
The presentedItemDidMoveToURL: method does not seem to be the root cause of the problem. For example, if I don't override that method at all, but periodically check in the viewController that is looking after the open document, then sometimes after a rename fileURL will return the new name and sometimes it will return `purg-.....'. So the issue appears to be to do with how renaming is handled.
Update
As al_lea pointed out, the issue here was related to accommodatePresentedItemDeletionWithCompletionHandler:. Expanding on al_lea's answer, I added the code below to my UIDocument subclass. This fixed the issue.
- (void) accommodatePresentedItemDeletionWithCompletionHandler: (void (^) (NSError *errorOrNil)) completionHandler
{
PresentedDocument* presentedDocument = [self retain];
[presentedDocument closeWithCompletionHandler: ^(BOOL success) {
NSError* error = nil;
if (!success)
{
NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
#"Could not close document that is being deleted on another device",
NSLocalizedDescriptionKey, nil];
error = [NSError errorWithDomain: #"some_suitable_domain"
code: 101
userInfo: userInfo];
}
completionHandler(error); // run the passed in completion handler (required)
dispatch_async(dispatch_get_main_queue(), ^
{
[[NSNotificationCenter defaultCenter] postNotificationName: NOTIFY_presentedDocumentDeletedOnAnotherDevice
object: presentedDocument
userInfo: nil];
[presentedDocument tidyUpAfterDelete]; // app specific tidy up
[presentedDocument release];
});
}];
}
With this code in place, there are no spurious and confusing presentedItemDidMoveToURL: calls made and, in addition, the relevant object can listen for notifications of deletions on other devices.
This type of URL appears when a UIDocument is opened on local and get deleted from a remote device:
file://localhost/var/mobile/Library/Mobile%20Documents/.ubd/peer-43A0AEB6-84CE-283E-CA39-FCC4EF3BC8F8-v23/ftr/purg-
You need to close the document first before it get deleted - detect this in NSFilePresenter's accommodatePresentedItemDeletionWithCompletionHandler:

Resources