86

作为初学者,我在 iCloud 上苦苦挣扎。有一些示例,但它们通常非常详细(在开发者论坛上有一个用于 iCloud 和 CoreData 的大量示例)。苹果文档还可以,但我仍然看不到大局。所以请耐心等待,其中一些问题非常基础,但可能很容易回答。

上下文:我有一个非常简单的 iCloud 应用程序正在运行(下面的完整示例代码)。只有一个 UITextView 显示给用户,他/她的输入保存在一个名为 text.txt 的文件中。

在此处输入图像描述

txt 文件被推送到云端并可供所有设备使用。完美运行,但是:

主要问题:不使用 iCloud 的用户怎么办?

当我启动我的应用程序(见下面的代码)时,我会检查用户是否启用了 iCloud。如果启用了 iCloud,一切都很好。该应用程序继续并在云中查找 text.txt。如果找到,它将加载并显示给用户。如果在云中找不到 text.txt,它将简单地创建一个新的 text.txt 并将其显示给用户。

如果用户没有启用 iCloud,什么都不会发生。如何让非 iCloud 用户仍然可以使用我的文本应用程序?还是我只是忽略它们?我需要为非 iCloud 用户编写单独的函数吗?即,我只需从文档文件夹中加载 text.txt 的功能?

苹果写道

对待 iCloud 中的文件就像对待应用沙箱中的所有其他文件一样。

但是,就我而言,不再有“正常”的应用程序沙箱。它在云端。还是我总是先从磁盘加载我的 text.txt,然后再通过 iCloud 检查是否有更新的内容?

相关问题:文件结构 - 沙盒与云

也许我的主要问题是对 iCloud 应该如何工作的根本误解。当我创建一个 UIDocument 的新实例时,我将不得不覆盖两个方法。首先- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError从云中获取文件,然后-(id)contentsForType:(NSString *)typeName error:(NSError **)outError将文件获取到云中。

我是否必须合并单独的函数,这些函数也会将 text.txt 的本地副本保存到我的沙箱中?这对非 iCloud 用户有用吗?据我了解 iCloud,它会自动保存 text.txt 的本地副本。因此,我不需要将任何内容保存到我的应用程序的“旧”沙箱中(即,它曾经在旧的、前 iCloud 时代)。现在,我的沙箱完全是空的,但我不知道这是否正确。我应该在其中保留另一份 text.txt 吗?这感觉就像把我的数据结构弄得一团糟……因为云中有一个 text.txt,我设备上的 iCloud 沙箱中有一个(即使我离线也可以工作),第三个在旧的沙箱中我的应用...


我的代码:一个简单的 iCloud 示例代码

这大致基于我在开发者论坛和 WWDC 会议视频中找到的示例。我把它剥离到最低限度。我不确定我的 MVC 结构是否良好。该模型位于 AppDelegate 中,这并不理想。欢迎提出任何使它变得更好的建议。


编辑:我试图提取主要问题并将其发布 [here]。4


概述:

概述

从云端加载 text.txt 的最重要部分:

//  AppDelegate.h
//  iCloudText

#import <UIKit/UIKit.h>

@class ViewController;
@class MyTextDocument;

@interface AppDelegate : UIResponder <UIApplicationDelegate> {
    NSMetadataQuery *_query;
}

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@property (strong, nonatomic) MyTextDocument *document;

@end

//  AppDelegate.m
//  iCloudText

#import "AppDelegate.h"
#import "MyTextDocument.h"
#import "ViewController.h"

@implementation AppDelegate

@synthesize window = _window;
@synthesize viewController = _viewController;
@synthesize document = _document;

- (void)dealloc
{
    [_window release];
    [_viewController release];
    [super dealloc];
}

- (void)loadData:(NSMetadataQuery *)query {

    // (4) iCloud: the heart of the load mechanism: if texts was found, open it and put it into _document; if not create it an then put it into _document

    if ([query resultCount] == 1) {
        // found the file in iCloud
        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];

        MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:url];
        //_document = doc;
        doc.delegate = self.viewController;
        self.viewController.document = doc;

        [doc openWithCompletionHandler:^(BOOL success) {
            if (success) {
                NSLog(@"AppDelegate: existing document opened from iCloud");
            } else {
                NSLog(@"AppDelegate: existing document failed to open from iCloud");
            }
        }];
    } else {
        // Nothing in iCloud: create a container for file and give it URL
        NSLog(@"AppDelegate: ocument not found in iCloud.");

        NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
        NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:@"Documents"] URLByAppendingPathComponent:@"text.txt"];

        MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:ubiquitousPackage];
        //_document = doc;
        doc.delegate = self.viewController;
        self.viewController.document = doc;

        [doc saveToURL:[doc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            NSLog(@"AppDelegate: new document save to iCloud");
            [doc openWithCompletionHandler:^(BOOL success) {
                NSLog(@"AppDelegate: new document opened from iCloud");
            }];
        }];
    }
}

- (void)queryDidFinishGathering:(NSNotification *)notification {

    // (3) if Query is finished, this will send the result (i.e. either it found our text.dat or it didn't) to the next function

    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    [self loadData:query];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query];
    _query = nil; // we're done with it
}

-(void)loadDocument {

    // (2) iCloud query: Looks if there exists a file called text.txt in the cloud

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    //SCOPE
    [query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
    //PREDICATE
    NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemFSNameKey, @"text.txt"];
    [query setPredicate:pred];
    //FINISHED?
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:query];
    [query startQuery];

}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSLog(@"AppDelegate: app did finish launching");
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // Override point for customization after application launch.
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        self.viewController = [[[ViewController alloc] initWithNibName:@"ViewController_iPhone" bundle:nil] autorelease];
    } else {
        self.viewController = [[[ViewController alloc] initWithNibName:@"ViewController_iPad" bundle:nil] autorelease];
    }

    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];

    // (1) iCloud: init

    NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    if (ubiq) {
        NSLog(@"AppDelegate: iCloud access!");
        [self loadDocument];
    } else {
        NSLog(@"AppDelegate: No iCloud access (either you are using simulator or, if you are on your phone, you should check settings");
    }


    return YES;
}

@end

用户界面文档

//  MyTextDocument.h
//  iCloudText

#import <Foundation/Foundation.h>
#import "ViewController.h"

@interface MyTextDocument : UIDocument {

    NSString *documentText;
    id delegate;

}

@property (nonatomic, retain) NSString *documentText;
@property (nonatomic, assign) id delegate;

@end

//  MyTextDocument.m
//  iCloudText

#import "MyTextDocument.h"
#import "ViewController.h"

@implementation MyTextDocument

@synthesize documentText = _text;
@synthesize delegate = _delegate;

// ** READING **

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError
{
    NSLog(@"UIDocument: loadFromContents: state = %d, typeName=%@", self.documentState, typeName);

    if ([contents length] > 0) {
        self.documentText = [[NSString alloc] initWithBytes:[contents bytes] length:[contents length] encoding:NSUTF8StringEncoding];
    }
    else {
        self.documentText = @"";
    }

    NSLog(@"UIDocument: Loaded the following text from the cloud: %@", self.documentText);


    // update textView in delegate...
    if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
        [_delegate noteDocumentContentsUpdated:self];
    }

    return YES;

}

// ** WRITING **

-(id)contentsForType:(NSString *)typeName error:(NSError **)outError
{
    if ([self.documentText length] == 0) {
        self.documentText = @"New Note";
    }

    NSLog(@"UIDocument: Will save the following text in the cloud: %@", self.documentText);

    return [NSData dataWithBytes:[self.documentText UTF8String] length:[self.documentText length]];
}
@end

视图控制器

//
//  ViewController.h
//  iCloudText

#import <UIKit/UIKit.h>

@class MyTextDocument;

@interface ViewController : UIViewController <UITextViewDelegate> {

    IBOutlet UITextView *textView;

}

@property (nonatomic, retain) UITextView *textView;
@property (strong, nonatomic) MyTextDocument *document;

-(void)noteDocumentContentsUpdated:(MyTextDocument *)noteDocument;

@end

//  ViewController.m
//  iCloudText

#import "ViewController.h"
#import "MyTextDocument.h"

@implementation ViewController

@synthesize textView = _textView;
@synthesize document = _document;

-(IBAction)dismissKeyboard:(id)sender {

    [_textView resignFirstResponder];

}

-(void)noteDocumentContentsUpdated:(MyTextDocument *)noteDocument
{
    NSLog(@"VC: noteDocumentsUpdated");
    _textView.text = noteDocument.documentText;
}

-(void)textViewDidChange:(UITextView *)theTextView {

     NSLog(@"VC: textViewDidChange");
    _document.documentText = theTextView.text;
    [_document updateChangeCount:UIDocumentChangeDone];

}
4

5 回答 5

22

我只是重新阅读了文档,看来我的一般方法是错误的。我应该首先在沙箱中创建文件,然后将其移动到云端。换句话说,Apple 似乎建议我应该始终拥有同一个文件的三个版本:一个在我的应用程序目录中,一个在我设备的 iCloud 恶魔目录中(如果离线也可以访问),另一个在云端:

应用程序使用与管理本地文件和目录相同的技术来管理 iCloud 中的文件和目录。iCloud 中的文件和目录仍然只是文件和目录。您可以打开它们、创建它们、移动它们、复制它们、从中读取和写入、删除它们或您可能想要执行的任何其他操作。本地文件和目录与 iCloud 文件和目录之间的唯一区别是您用来访问它们的 URL。iCloud 文件和目录的 URL 不是相对于您的应用程序的沙箱的 URL,而是相对于相应的 iCloud 容器目录。

要将文件或目录移动到 iCloud:

在您的应用沙箱中本地创建文件或目录。在使用时,文件或目录必须由文件呈现器管理,例如 UIDocument 对象。

使用 URLForUbiquityContainerIdentifier: 方法检索要在其中存储项目的 iCloud 容器目录的 URL。使用容器目录 URL 构建一个新 URL,指定项目在 iCloud 中的位置。调用 NSFileManager 的 setUbiquitous:itemAtURL:destinationURL:error: 方法将项目移动到 iCloud。永远不要从你的应用程序的主线程调用这个方法;这样做可能会长时间阻塞您的主线程或导致与您的应用程序自己的文件呈现器之一发生死锁。当您将文件或目录移动到 iCloud 时,系统会将该项目从您的应用程序沙箱中复制到私有本地目录中,以便 iCloud 守护程序对其进行监控。即使该文件不再在您的沙箱中,您的应用程序仍然可以完全访问它。尽管该文件的副本保留在当前设备的本地,但该文件也会发送到 iCloud,以便可以分发到其他设备。iCloud 守护进程处理所有确保本地副本相同的工作。因此,从您的应用程序的角度来看,该文件只是在 iCloud 中。

您对 iCloud 中的文件或目录所做的所有更改都必须使用文件协调器对象进行。这些更改包括移动、删除、复制或重命名项目。文件协调器确保 iCloud 守护进程不会同时更改文件或目录,并确保将您所做的更改通知其他相关方。

但是,如果您更深入地研究有关 setUbiquitous 的文档,您会发现:

使用此方法将文件从其当前位置移动到 iCloud。对于位于应用程序沙箱中的文件,这涉及从沙箱目录中物理删除文件。(系统扩展了您的应用程序的沙箱权限,使其能够访问它移动到 iCloud 的文件。)您还可以使用此方法将文件移出 iCloud 并移回本地目录。

所以这似乎意味着文件/目录从本地沙箱中删除并移动到云中。

于 2011-10-24T08:46:17.360 回答
5

我一直在使用您的示例,我喜欢它帮助我掌握 iCloud 的基础知识。现在,我正在为我自己的应用程序解决您的问题,该应用程序必须支持应用程序的现有用户使用本地存储的内容,据我所知,这些用户可能会或可能不会使用 iCloud 创建这些案例:

案例:

  1. 新的用户
    • 拥有 icloud - 在 icloud 中创建文档
    • 没有 icloud - 在本地创建文档
  2. 现有用户
    • 有 icloud
      • 刚刚添加 - 将本地文档迁移到 icloud
      • 不只是添加 - 打开/保存文档到 icloud
    • 没有 icloud
      • 刚刚删除 - 将以前的 icloud 文档迁移到本地
      • 不只是删除 - 打开/保存文档到本地

如果有人删除了 iCloud——对无处不在的 URL 的调用不会返回 nil 吗?如果是这种情况,我该如何将文档迁移回本地存储?我现在将创建一个用户偏好,但似乎有点解决方法。

我觉得我在这里遗漏了一些明显的东西,所以如果有人能看到它,请插话。

于 2011-10-25T18:08:09.663 回答
4

如果您希望用户能够在 iOS 5.0 之前的设备之间共享文本,您将不得不做在 iCloud 之前每个人都必须做的事情,并将信息移动到您自己的服务器上。

您真正需要的只是一个服务器,它可以让您的应用程序保存其文本文件并将它们与用户帐户相关联。

您需要用户创建一个帐户,并且您需要自己管理流程,将一台设备上的新信息移动到您自己的“云”中。

用户将在其他设备上使用相同的帐户注册,您需要注意检测其他设备何时将数据移动到您自己的云中,并使用新信息更新当前设备。

显然,对于 iOS 5.0 设备,您可能希望在自己的云中检测 iOS 5.0 之前设备的更改文件,并且还能够与 iCloud 通信。

于 2011-10-19T21:47:17.023 回答
3

您似乎没有像 iOS5/notIOS5 问题那样为 iCloud/notICloud 问题苦苦挣扎。

如果您的部署目标是 iOS5,那么只需始终使用 UIDocument 结构。如果它无处不在,那么你的 NSMetaDataQuery 会在云端找到它;如果没有,它将在设备上找到它。

另一方面,如果您想为您的应用程序提供 5.0 之前的访问权限,那么您需要有条件地检查正在运行的 iOS 是否为 5.0 或更高版本。如果是,则使用 UIDocument;如果不是,则以旧方式读取/写入数据。

我的方法是编写一个检查 iOS5 的条件 saveData 方法。如果存在,我会更新更改计数(或使用撤消管理器)。在您的情况下, textViewDidChange 将调用此方法。如果没有,那么它将以旧方式保存到磁盘。在加载时,会发生相反的情况。

于 2011-10-20T15:53:07.830 回答
1

“像对待应用沙箱中的所有其他文件一样对待 iCloud 中的文件”让你感到困惑。这适用于像 Keynote 和 Numbers 这样保存一堆文件的东西,如果你有 iCloud,它们就会开始神奇地同步。

但是,您正在构建的东西依赖于类似 iCloud 的功能。你不能坚持这样的说法,因为你的应用程序依赖于 iCloud 的存在,任何东西都可以按预期的方式工作。您将不得不关闭您的应用程序并简单地说“请设置 iCloud 以使其正常工作”或复制您始终可以使用的类似 iCloud 的功能(您自己的或其他人的),无论如何。

于 2011-10-24T09:09:58.893 回答