问题
I am using Justin Driscoll's implementaion on Core Data with a Single Shared UIManagedDocument. Everything was fine in my iphone app until I moved it to a iPad storyboard and a splitview controller for the ipad app. The problem is openwithCompletionHandler is being called twice, once from my master view in viewDidLoad and again in my detail view viewWillLoad. The calls are in quick succession and since the document is still in UIDocumentStateClosed when the second call is made to my performWithDocument method (below) of the singleton the app crashes. I looked at e_x_p ' s answer for post iOS5.1: synchronising tasks (wait for a completion) but @sychronized will not work in this case since performWithDocument below is called on the same thread. How would I protect against multiple calls to openwithCompletionHandler? The only way I can think to protect against this is to pause execution of one of the calls above until i am sure UIDocumentStateNormal is true and then release. That though would freeze the main UI thread which is not good. What though would be the best way todo this without freezing up the UI?
From the UIManagedDocumentSingleton code:
- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success)
{
onDocumentReady(self.document);
};
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]])
{
//This should never happen*******************
[self.document saveToURL:self.document.fileURL
forSaveOperation:UIDocumentSaveForCreating
completionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateNormal) {
OnDocumentDidLoad(YES);
}
}
回答1:
I did it as Justin suggested above below. Works fine in one of my apps for two years with ~20k users.
@interface SharedUIManagedDocument ()
@property (nonatomic)BOOL preparingDocument;
@end
- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success) {
onDocumentReady(self.document);
self.preparingDocument = NO; // release in completion handler
};
if(!self.preparingDocument) {
self.preparingDocument = YES; // "lock", so no one else enter here
if(![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateNormal) {
OnDocumentDidLoad(YES);
}
} else {
// try until document is ready (opened or created by some other call)
[self performSelector:@selector(performWithDocument:) withObject:onDocumentReady afterDelay:0.5];
}
}
Swift (not much tested)
typealias OnDocumentReady = (UIManagedDocument) ->()
class SharedManagedDocument {
private let document: UIManagedDocument
private var preparingDocument: Bool
static let sharedDocument = SharedManagedDocument()
init() {
let fileManager = NSFileManager.defaultManager()
let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let documentsDirectory: NSURL = urls.first as! NSURL
let databaseURL = documentsDirectory.URLByAppendingPathComponent(".database")
document = UIManagedDocument(fileURL: databaseURL)
let options = [NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true]
document.persistentStoreOptions = options
preparingDocument = false
}
func performWithDocument(onDocumentReady: OnDocumentReady) {
let onDocumentDidLoad:(Bool) ->() = {
success in
onDocumentReady(self.document)
self.preparingDocument = false
}
if !preparingDocument {
preparingDocument = true
if !NSFileManager.defaultManager().fileExistsAtPath(document.fileURL.path!) {
println("Saving document for first time")
document.saveToURL(document.fileURL, forSaveOperation: .ForCreating, completionHandler: onDocumentDidLoad)
} else if document.documentState == .Closed {
println("Document closed, opening...")
document.openWithCompletionHandler(onDocumentDidLoad)
} else if document.documentState == .Normal {
println("Opening document...")
onDocumentDidLoad(true)
} else if document.documentState == .SavingError {
println("Document saving error")
} else if document.documentState == .EditingDisabled {
println("Document editing disabled")
}
} else {
// wait until document is ready (opened or created by some other call)
println("Delaying...")
delay(0.5, closure: {
self.performWithDocument(onDocumentReady)
})
}
}
private func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
}
回答2:
That's interesting and definitely a flaw in my code (sorry!). My first thought would be to add a serial queue as a property to your document handler class and perform the check on that.
self.queue = dispatch_queue_create("com.myapp.DocumentQueue", NULL);
and then in performWithDocument:
dispatch_async(self.queue, ^{
if (![[NSFileManager defaultManager] fileExistsAtPath... // and so on
});
But that wouldn't work either...
You could set a BOOL flag when you call saveToURL and clear it in the callback. Then you can check for that flag and use performSelectorAfterDelay to call performWithDocument again a little later if the file is being created.
回答3:
The block of code that is shared between numberOfRowsInSection:
and cellForRowAtIndexPath:
should be called only once. numberOfRowsInSection
will always be called before the tableView
tries to render the cells, so you should create an NSArray
object that you can store the results of the fetch request into, and then use this array when rendering your cells:
@implementation FooTableViewController {
NSArray *_privateArray;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
[[UIManagedDocumentSingletonHandler sharedDocumentHandler] performWithDocument:^(FCUIManagedDocumentObject *document) {
NSManagedObjectContext * context = document.managedObjectContext;
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"FCObject"];
NSPredicate * searchStringPredicate = nil;
if (searchFilterString)
{
searchStringPredicate = [NSPredicate predicateWithFormat:@"word BEGINSWITH[c] %@",searchFilterString];
}
request.predicate = searchStringPredicate;
request.shouldRefreshRefetchedObjects = YES;
NSError * error;
_privateArray = [context executeFetchRequest:request error:&error];
}];
return _privateArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"FCCell";
FCardCell *cell = (FCCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
FCManagedObject * fcc = [_privateArray objectAtIndex:indexPath.row];
cell.isWordVisible.on = fcc.isUsed;
cell.fWord.text = fcc.word;
return cell;
}
I'm not sure off the top of my head if you need to do something special with the NSArray
to set it within the block (a la __block
).
The major reason for this is that you need to make sure that 100% of the time the dataset used to determine the # of rows is the same size as when you are creating your cells. If they don't match then you will crash. Also since you don't have a block you don't need to dispatch to make UITableViewCell
updates now.
Finally if UIDocumentStateClosed
is causing problems you should either filter them out of your NSFetch
results (additional predicate, see NSCompoundPredicate
if required) or have code to handle them better in cellForRowAtIndexPath:
来源:https://stackoverflow.com/questions/13480211/uimanageddocument-singleton-code-openwithcompletionhandler-called-twice-and-cras