UIManagedDocument Singleton代码openWithCompletionHandler调用两次并崩溃

时间:2012-11-20 19:00:18

标签: ios singleton uimanageddocument

我在Core Data with a Single Shared UIManagedDocument使用Justin Driscoll的实现。 我的iphone应用程序中的一切都很好,直到我将它移动到iPad故事板和ipad应用程序的splitview控制器。问题是openwithCompletionHandler被调用两次,一次来自viewDidLoad中的主视图,另一次是我的详细视图viewWillLoad。调用是快速连续的,因为当对单例的performWithDocument方法(下面)进行第二次调用时,文档仍然在UIDocumentStateClosed中,应用程序崩溃。我查看了e_x_p对帖子iOS5.1: synchronising tasks (wait for a completion)的回答,但@sychronized在这种情况下不起作用,因为下面的performWithDocument是在同一个线程上调用的。我如何防止多次调用openwithCompletionHandler?我能想到防止这种情况的唯一方法是暂停执行上面的一个调用,直到我确定UIDocumentStateNormal为true然后释放。这虽然会冻结主要的UI线程,这是不好的。尽管如此,如果没有冻结用户界面,最好的办法是什么呢?

来自UIManagedDocumentSingleton代码:

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

3 个答案:

答案 0 :(得分:2)

我这样做是因为贾斯汀建议在以上。在我的一个应用程序中可以正常工作两年,用户数约为20,000。

@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 (测试不多)

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

答案 1 :(得分:1)

这很有趣,而且我的代码中肯定存在缺陷(抱歉!)。我的第一个想法是将一个串行队列作为属性添加到您的文档处理程序类并执行检查。

self.queue = dispatch_queue_create("com.myapp.DocumentQueue", NULL);

然后在performWithDocument中:

dispatch_async(self.queue, ^{
    if (![[NSFileManager defaultManager] fileExistsAtPath... // and so on
});

但那也行不通......

您可以在调用saveToURL并在回调中清除它时设置BOOL标志。然后你可以检查那个标志,如果正在创建文件,请稍后再使用performSelectorAfterDelay调用performWithDocument。

答案 2 :(得分:0)

numberOfRowsInSection:cellForRowAtIndexPath:之间共享的代码块只能调用一次。在numberOfRowsInSection尝试渲染单元格之前将始终调用tableView,因此您应该创建一个NSArray对象,您可以将获取请求的结果存储到该对象中,然后使用此数组渲染细胞时:

@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;
}

如果您需要使用NSArray做一些特别的事情来在块内设置它(la __block),我不确定是不是最重要的。

这样做的主要原因是您需要确保用于确定行数的数据集的100%与创建单元格时的大小相同。如果它们不匹配则会崩溃。此外,由于您没有阻止,因此您无需立即发送UITableViewCell更新。

最后,如果UIDocumentStateClosed导致问题,您应该将其从NSFetch结果中过滤出来(其他谓词,如果需要,请参阅NSCompoundPredicate),或者让代码更好地处理cellForRowAtIndexPath: {{1}} 1}}