ICloud 基础知识和代码示例

作为一个初学者,我在 iCloud 上苦苦挣扎。有一些示例,但它们通常都非常详细(在开发人员论坛上有一个针对 iCloud 和 CoreData 的示例,它们非常庞大)。苹果医生还可以,但我还是看不到全局。所以请容忍我,这些问题中的一些是相当基本的,但可能很容易回答。

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

enter image description here

文本文件被推送到云端,并且可以在所有设备上使用:

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

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

如果用户没有启用 iCloud,那么什么都不会发生。我将如何使非 iCloud 用户仍然可以使用我的文本应用程序?或者我只是无视他们?我是否需要为非 iCloud 用户编写单独的函数?例如,在函数中,我只需从 document 文件夹加载 text.txt?

苹果写道:

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

然而,在我的情况下,没有“正常”的应用程序沙箱了。在云端。或者我总是先从磁盘加载 text.txt,然后再用 iCloud 检查是否有更新的内容?

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

也许我的主要问题是对 iCloud 应该如何工作的根本性误解。当我创建一个 UIDdocument 的新实例时,我必须重写两个方法。首先 - (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 结构是否好。这个模型位于不理想的应用委托中。任何改进的建议都是受欢迎的。


编辑: 我试图摘录主要问题并发表在这里


概述:

Overview

从云中加载 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

UID 文件

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


}
53356 次浏览

If you want users to be able to share text between devices that are pre-iOS 5.0, you are going to have to do what everyone had to do before iCloud and move information to your own server.

All you really need is a server somewhere that lets your app save its text files and associate them with a user account.

You'll need users to create an account and you'll need to manage the process yourself, of moving new information on one device into your own 'cloud'.

Users will register with the same account on other devices and you'll need to take care of detecting when another device has moved data onto your own cloud, and update the current device with the new info.

Obviously, for iOS 5.0 devices, you'll probably want to detect changed files for pre-iOS 5.0 devices in your own cloud, and also be able to talk to iCloud.

It doesn't seem that you are struggling with a iCloud/notICloud issue as much as a iOS5/notIOS5 issue.

If your deployment target is iOS5, then simply always use the UIDocument structure. If it is ubiquitous, then your NSMetaDataQuery will find it in the cloud; if not it will find it on the device.

If, on the other hand, you want to provide pre 5.0 access to your app, then you will need to conditionally check to see if if the running iOS is 5.0 or greater. If it is then use UIDocument; if not then read/write data the old way.

My approach was to write a conditional saveData method that checks for iOS5. If it exists I update the change count (or use an undo manager). In your case the textViewDidChange would call this method. If not, then it saves to disk the old way. On loading, the opposite happens.

I just re-read the docs and it appears that my general approach is wrong. I should first create the file in the sandbox and then move it to the cloud. In other words, Apple seems to suggest that I should have three versions of the same file at all times: one in the directory of my app, one in the iCloud demon directory of my device (which is also accessible if offline) and one in the cloud:

Apps use the same technologies to manage files and directories in iCloud that they do for local files and directories. Files and directories in iCloud are still just files and directories. You can open them, create them, move them, copy them, read and write from them, delete them, or any of the other operations you might want to do. The only differences between local files and directories and iCloud files and directories is the URL you use to access them. Instead of URLs being relative to your app’s sandbox, URLs for iCloud files and directories are relative to the corresponding iCloud container directory.

To move a file or directory to iCloud:

Create the file or directory locally in your app sandbox. While in use, the file or directory must be managed by a file presenter, such as a UIDocument object.

Use the URLForUbiquityContainerIdentifier: method to retrieve a URL for the iCloud container directory in which you want to store the item. Use the container directory URL to build a new URL that specifies the item’s location in iCloud. Call the setUbiquitous:itemAtURL:destinationURL:error: method of NSFileManager to move the item to iCloud. Never call this method from your app’s main thread; doing so could block your main thread for an extended period of time or cause a deadlock with one of your app’s own file presenters. When you move a file or directory to iCloud, the system copies that item out of your app sandbox and into a private local directory so that it can be monitored by the iCloud daemon. Even though the file is no longer in your sandbox, your app still has full access to it. Although a copy of the file remains local to the current device, the file is also sent to iCloud so that it can be distributed to other devices. The iCloud daemon handles all of the work of making sure that the local copies are the same. So from the perspective of your app, the file just is in iCloud.

All changes you make to a file or directory in iCloud must be made using a file coordinator object. These changes include moving, deleting, copying, or renaming the item. The file coordinator ensures that the iCloud daemon does not change the file or directory at the same time and ensures that other interested parties are notified of the changes you make.

However, if you dig a little deeper into the docs concerning setUbiquitous, you'll find:

Use this method to move a file from its current location to iCloud. For files located in an application’s sandbox, this involves physically removing the file from the sandbox directory. (The system extends your application’s sandbox privileges to give it access to files it moves to iCloud.) You can also use this method to move files out of iCloud and back into a local directory.

So this appears to mean that a file / directory gets deleted form the local sandbox and moved into the cloud.

You are befuddled by "Treat files in iCloud the same way you treat all other files in your app sandbox." This holds true for something like Keynote and Numbers where you keep a bunch of files, and if you have iCloud, they start syncing magically.

However, you're building something that depends on iCloud-like functionality. You can't hold onto that statement because your app depends on iCloud to be present for anything to work the way it's meant to. You will either have to close your app down and simply say "please setup iCloud for this to work" or duplicate iCloud-like functionality (your own or someone else's) that you can always use, regardless.

I've been using your example and I like it for helping me grasp the basics of iCloud. Now I'm wrangling with your question for my own app which has to support existing users of the app with locally stored content who may or may not be using iCloud creating these cases as far as I can tell:

Cases:

  1. New user
    • has icloud - create documents in icloud
    • no icloud - create documents locally
  2. Existing user
    • has icloud
      • just added - migrate local docs to icloud
      • not just added - open/save docs to icloud
    • no icloud
      • just removed - migrate former icloud docs to local
      • not just removed - open/save docs to local

If someone removes iCloud - wouldn't the calls to ubiquitous URL return nil? If that's the case how do I migrate the docs back to local storage? I'll create a user pref for now but seems a bit of a workaround.

I feel like I'm missing something obvious here so if anyone can see it, please chime in.