iCloud基础知识和代码示例


85

作为一个初学者,我正在为iCloud奋斗。有一些示例,但是它们通常都非常详细(在开发人员论坛上,有一个针对iCloud和CoreData的示例非常庞大)。在苹果的文档都OK,但我仍然无法看到大局。因此,请允许我,其中一些问题是非常基本的,但可能很容易回答。

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

在此处输入图片说明

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

主要问题:不使用iCloud的用户呢?

启动应用程序时(请参见下面的代码),我会检查用户是否启用了iCloud。如果启用了iCloud,则一切正常。该应用程序继续运行,并在云中查找text.txt。如果找到,它将加载它并显示给用户。如果在云中找不到text.txt,它将仅创建一个新的text.txt并将其显示给用户。

如果用户未启用iCloud,则不会发生任何事情。如何使非iCloud用户仍然可以使用我的文本应用程序?还是我只是忽略它们?我是否需要为非iCloud用户编写单独的功能?即我仅从documents文件夹中加载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沙箱中有一个text.txt(即使我离线也可以使用),而旧的沙箱中的第三个则是。我的应用程式...


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

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


编辑:我试图提取主要问题并将其发布[此处]。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

UIDocument

//  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

//
//  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
我真的建议将其分解为几个问题。我看到这里隐藏了一些不同的问题,很难在这里的文字墙上找到它们。我将把这个问题回过头来,只是询问如何为未启用iCloud的人们做些什么,然后将其他问题(仅包含示例代码的相关部分)分解为单独的问题。他们是很好的问题,但我认为应该分开。
布拉德·拉尔森

@BradLarson谢谢您的评论。如果问题混在一起,我感到很抱歉,但是我认为主要问题(正如我试图指出的)是应用程序沙箱与iCloud沙箱问题。我提供了完整的代码(这是最短的iCloud代码示例,btw),因为我认为整个上下文对于了解正在发生的事情至关重要。但是我可能只想提出另一个问题,并将其链接回该问题,以得到更大的画面。
n.evermind 2011年

@BradLarson OK,这里我打开了一个新的问题:stackoverflow.com/questions/7798555/...
n.evermind

对于那些依然试图让核心数据和iCloud的手柄尝试此链接ossh.com.au/design-and-technology/software-development/...
邓肯Groenewald

不应该关闭这其实是我见过的在iCloud上的更具建设性的职位之一..
安德鲁·史密斯

Answers:


22

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

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

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

在应用程序沙箱中本地创建文件或目录。在使用时,文件或目录必须由文件演示者(例如UIDocument对象)管理。

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

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

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

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

因此,这似乎意味着文件/目录将从本地沙箱中删除,并移至云中。


1
url链接已损坏...
ngb

5

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

情况:

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

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

我觉得我这里缺少明显的东西,所以如果有人可以看到它,请发出提示。


我应该补充一点,我想知道是否有一个处理这些情况的类,所以我只用它,而不必担心将其保存在哪里。
earnshavian时间

看看developer.apple.com/library/ios/#documentation/DataManagement/…,其中提供了一些示例代码来确定是否应将某些内容放入本地沙箱或云中。
n.evermind 2011年

感谢那。我看过该文档,但是在iCloud任务的早期,所以我忘记了它提供的代码。我将尝试调整您的示例以支持本地和远程。我仍然不清楚如何处理禁用iCloud的用户,因为我们丢失了无处不在的URL,但是我将进行破解并分享更新。
Earnshavian,时间

1
因此,从某种意义上说,我们必须将URL用于云,将PATH用于本地沙箱,这有点愚蠢。如果iCloud可以为我们处理所有事情,那就太好了……但是通过这种方式,我们基本上需要为打开的每个文件编写两种不同的方法。
n.evermind 2011年

我只是重新阅读了您的帖子。我现在在NSUserDefaults中保存用户的首选项(即用户想要/不想使用iCloud)。这也是苹果公司的建议。我总是检查是否可以访问iCloud。如果无法访问它,我会告诉用户打开它-但前提是他们没有明确告知应用程序他们不希望使用它。否则,对于那些不想使用iCloud的人来说会很烦。一旦确定是否启用了iCloud,就可以按照无处不在的URL路由并使用UIDocument,或者像过去那样从沙箱中打开文件。
n.evermind 2011年

4

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

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

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

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

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


谢谢。因此,换句话说,如果我不想支持iOS 5之前的设备,则只需使用UIDocument,而无需考虑应用程序沙箱中doc目录的内容。
n.evermind 2011年

差不多,尽管据我所知,您仍然在沙箱中有一个文档,UIDocument将帮助您与iCloud进行中介,但是会告诉您何时可以访问它……我仍然自己来处理这些东西!
乔纳森·沃特莫夫

3

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

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

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

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


1

您“对iCloud中的文件进行处理的方式与对待应用程序沙箱中所有其他文件的方式相同”感到困惑。对于诸如Keynote和Numbers之类的东西,您可以保留一堆文件,并且如果您拥有iCloud,它们就可以神奇地同步了。

但是,您正在构建的东西依赖于类似iCloud的功能。您无法坚持该说法,因为您的应用程序依赖于iCloud才能使一切正常工作。您将不得不关闭您的应用程序,然后简单地说“请设置iCloud使其正常工作”,或者重复使用类似iCloud的功能(您自己或他人的功能),无论您何时使用。


谢谢。因此,我想我必须选择只为iCloud应用程序还是为使用iCloud功能的人们创建某种混合应用程序。由于iCloud非常复杂,因此我倾向于选择仅iCloud的应用程序。谢谢。
n.evermind 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.