Objective-C 的 Dropbox
官方 Dropbox Objective-C SDK,用于在 iOS 或 macOS 上与 Dropbox API v2 集成。
完整文档 在此处。
注意:在生产环境中不要依赖 master
,请使用我们标记的版本提交(最好通过 CocoaPods 或 Carthage 获取),因为这些提交已经过更彻底的测试。
目录
系统要求
- iOS 11.0+
- macOS 10.10+
- Xcode 8+ (如果你使用Carthage,则为11.0+)
Xcode 8和iOS 10错误
Keychain错误
Dropbox Objective-C SDK目前支持Xcode 8和iOS 10。然而,在iOS模拟器环境中似乎有一个Keychain的错误,其中数据不能持久地保存到Keychain中。
作为临时解决方案,在项目导航器中,选择你的项目 > 能力 > Keychain共享 > 开启。
你可以在此处了解更多关于错误的信息:这里。
长轮询会话超时错误
目前,iOS 10存在一个错误,我们长轮询请求在约6分钟后超时(而最大支持时间为8分钟(480秒))。
因此,我们建议在苹果解决这个问题之前,使用-listFolderLongpoll:timeout:
进行所有长轮询调用,指定一个timeout
值,不大于300秒(5分钟)。
更多关于这个问题的信息,请访问这里。
开始使用
注册您的应用程序
在开始使用此SDK之前,您应该在Dropbox应用程序控制台中注册您的应用程序。这将创建一个与Dropbox相关联的您的应用程序记录,将关联到您的API调用。
获取OAuth 2.0令牌
所有请求都需要使用OAuth 2.0访问令牌。OAuth令牌代表Dropbox应用程序和Dropbox用户账户或团队之间的认证链接。
创建应用程序后,您可以直接在App控制台中手动生成一个访问令牌,以授权您的应用程序访问您自己的Dropbox账户。否则,您可以使用SDK预定义的认证流程以编程方式获取OAuth令牌。更多信息,请参阅以下内容。
SDK 分发
您可以通过以下几种方法将 Dropbox Objective-C SDK 整合到您的项目中。
CocoaPods
要使用 CocoaPods(一个 Cocoa 项目的依赖管理工具),请先使用以下命令安装它:
$ gem install cocoapods
然后导航到包含您的项目目录,创建一个新的名为 Podfile
的文件。您可以通过 pod init
完成,或者是打开一个现有的 Podfile,然后添加 pod 'ObjectiveDropboxOfficial'
到主循环中。您的 Podfile 应该看起来像这样:
iOS
platform :ios, '9.0'
use_frameworks!
target '<YOUR_PROJECT_NAME>' do
pod 'ObjectiveDropboxOfficial'
end
macOS
platform :osx, '10.10'
use_frameworks!
target '<YOUR_PROJECT_NAME>' do
pod 'ObjectiveDropboxOfficial'
end
然后,在确保您的 Xcode 项目窗口 已关闭 后,运行以下命令安装依赖项:
$ pod install
此命令完成后,打开新创建的 .xcworkspace
文件。此时,您的项目应已成功集成 SDK。
从现在起,您可以使用以下命令拉取 SDK 更新:
$ pod update
常见问题
未定义架构
如果 Xcode 报错信息是关于 Undfined symbols for architecture...
,请尝试以下步骤:
- 项目导航 > 构建目标 > 构建设置 > 其他链接器标志,添加
$(inherited)
和-ObjC
。
Carthage
您还可以通过 Carthage 将 Dropbox Objective-C SDK 集成到项目中,Carthage 是 Cocoa 的分布式依赖管理器。Carthage 比 Cocoapods 提供更多灵活性,但需要一些额外的工作。由于 Xcode 12 上的 XCFramework 需求,需要 Carthage 0.37.0。您可以通过 Homebrew 安装 Carthage(Xcode 11+)。
brew update
brew install carthage
要使用 Carthage 安装 Dropbox Objective-C SDK,您需要在项目中创建一个包含以下内容的 Cartfile
:
# ObjectiveDropboxOfficial
github "https://github.com/dropbox/dropbox-sdk-obj-c" ~> 7.1.1
要将 Dropbox Objective-C SDK 集成到项目中,请按照以下步骤进行:
运行以下命令以更新 Dropbox Objective-C SDK 仓库:
iOS
carthage update --platform iOS --use-xcframeworks
macOS
carthage update --platform Mac --use-xcframeworks
然后,在 Xcode 的项目导航器中,选择您的项目,然后导航到您的项目构建目标 > 常规 > 框架、库和嵌入式内容。将 ObjectiveDropboxOfficial.xcframework
文件从 Carthage/Build
拖动到表格中,并选择 嵌入 & 签章
。
手动添加子项目
最后,您还可以借助 Carthage,手动将 Dropbox Objective-C SDK 集成到您的项目中。请按照以下步骤操作
在您的项目中创建一个与 README 中 Carthage 部分中列出的 Cartfile 内容相同的 Cartfile
。
然后,运行以下命令以检出并构建 Dropbox Objective-C SDK 仓库
iOS
carthage update --platform iOS --use-xcframeworks
macOS
carthage update --platform Mac --use-xcframeworks
通过 Carthage 检出所有必要代码后,将 Carthage/Checkouts/ObjectiveDropboxOfficial/Source/ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.xcodeproj
文件作为子项目拖入您的项目中。
配置您的项目
将Dropbox Objective-C SDK集成到您的项目后,在您可以开始调用API之前需要执行以下额外步骤。
.plist
文件
应用程序 您需要修改应用程序的 .plist
以处理苹果针对 canOpenURL
函数的新安全变更。应该将以下代码添加到您应用程序的 .plist
文件中
<key>LSApplicationQueriesSchemes</key>
<array>
<string>dbapi-8-emm</string>
<string>dbapi-2</string>
</array>
这允许Objective-C SDK确定当前设备上是否安装了官方Dropbox iOS应用程序。如果已安装,则可以使用官方Dropbox iOS应用程序以编程方式获取OAuth 2.0访问令牌。
此外,您的应用程序需要注册以处理OAuth 2.0授权流完成后重定向的独特Dropbox URL方案。该URL方案应采用格式 db-<APP_KEY>
,其中 <APP_KEY>
是您的Dropbox应用程序的应用密钥,可以在App Console中找到。
应该将以下代码添加到您的 .plist
文件中(但请确保将 <APP_KEY>
替换为您的应用密钥)
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>db-<APP_KEY></string>
</array>
<key>CFBundleURLName</key>
<string></string>
</dict>
</array>
在执行上述更改后,您应用程序的 .plist
文件应类似于这样
处理授权流
有三种方法可以以编程方式检索OAuth 2.0访问令牌
- 直接授权(仅限iOS):此操作将启动已安装的官方Dropbox iOS应用程序(如果已安装),通过官方应用程序进行认证,然后重定向回SDK
- Safari视图控制器授权(仅限iOS):此操作将启动一个
SFSafariViewController
来简化认证流程。这很受欢迎,因为它对最终用户来说更安全,并且可以使用现有的会话数据,从而避免需要用户重新输入Dropbox凭据。 - 外部浏览器重定向(仅限macOS):启动用户的默认浏览器以方便身份验证流程。这也是一个优点,因为它更安全,并且可以使用先前的会话数据以避免要求用户重新输入Dropbox凭据。
为方便上述身份验证流程,您应采取以下步骤
DBUserClient
实例
初始化 iOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[DBClientsManager setupWithAppKey:@"<APP_KEY>"];
return YES;
}
macOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[DBClientsManager setupWithAppKeyDesktop:@"<APP_KEY>"];
}
开始身份验证流程
您可以通过在应用程序的视图控制器中调用authorizeFromController:controller:openURL
方法开始身份验证流程。
请确保提供的视图控制器是最顶层的控制器,以便正确显示身份验证视图。
iOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
- (void)myButtonInControllerPressed {
// OAuth 2 code flow with PKCE that grants a short-lived token with scopes, and performs refreshes of the token automatically.
DBScopeRequest *scopeRequest = [[DBScopeRequest alloc] initWithScopeType:DBScopeTypeUser
scopes:@[@"account_info.read"]
includeGrantedScopes:NO];
[DBClientsManager authorizeFromControllerV2:[UIApplication sharedApplication]
controller:[[self class] topMostController]
loadingStatusDelegate:nil
openURL:^(NSURL *url) { [[UIApplication sharedApplication] openURL:url]; }
scopeRequest:scopeRequest];
// Note: this is the DEPRECATED authorization flow that grants a long-lived token.
// If you are still using this, please update your app to use the `authorizeFromControllerV2` call instead.
// See https://dropbox.tech/developers/migrating-app-permissions-and-access-tokens
// [DBClientsManager authorizeFromController:[UIApplication sharedApplication]
// controller:[[self class] topMostController]
// openURL:^(NSURL *url) {
// [[UIApplication sharedApplication] openURL:url];
// }];
}
+ (UIViewController*)topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
macOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
- (void)myButtonInControllerPressed {
// OAuth 2 code flow with PKCE that grants a short-lived token with scopes, and performs refreshes of the token automatically.
DBScopeRequest *scopeRequest = [[DBScopeRequest alloc] initWithScopeType:DBScopeTypeUser
scopes:@[@"account_info.read"]
includeGrantedScopes:NO];
[DBClientsManager authorizeFromControllerDesktopV2:[NSWorkspace sharedWorkspace]
controller:self
loadingStatusDelegate:nil
openURL:^(NSURL *url) { [[NSWorkspace sharedWorkspace] openURL:url]; }
scopeRequest:scopeRequest];
// Note: this is the DEPRECATED authorization flow that grants a long-lived token.
// If you are still using this, please update your app to use the `authorizeFromControllerDesktopV2` call instead.
// See https://dropbox.tech/developers/migrating-app-permissions-and-access-tokens
// [DBClientsManager authorizeFromControllerDesktop:[NSWorkspace sharedWorkspace]
// controller:self
// openURL:^(NSURL *url){ [[NSWorkspace sharedWorkspace] openURL:url]; }];
}
在移动设备上开始认证流程将弹出如下窗口
处理重定向回SDK
为了处理认证流程完成后返回到Objective-C SDK,您应该在应用的代理中添加以下代码
iOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
DBOAuthCompletion completion = ^(DBOAuthResult *authResult) {
if (authResult != nil) {
if ([authResult isSuccess]) {
NSLog(@"\n\nSuccess! User is logged into Dropbox.\n\n");
} else if ([authResult isCancel]) {
NSLog(@"\n\nAuthorization flow was manually canceled by user!\n\n");
} else if ([authResult isError]) {
NSLog(@"\n\nError: %@\n\n", authResult);
}
}
};
BOOL canHandle = [DBClientsManager handleRedirectURL:url completion:completion];
return canHandle;
}
或者如果您的应用是iOS13+版本,或您的应用也支持场景,应将以下代码添加到应用的主场景代理中
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
- (void)scene:(UIScene *)scene
openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
DBOAuthCompletion completion = ^(DBOAuthResult *authResult) {
if (authResult != nil) {
if ([authResult isSuccess]) {
NSLog(@"\n\nSuccess! User is logged into Dropbox.\n\n");
} else if ([authResult isCancel]) {
NSLog(@"\n\nAuthorization flow was manually canceled by user!\n\n");
} else if ([authResult isError]) {
NSLog(@"\n\nError: %@\n\n", authResult);
}
}
};
for (UIOpenURLContext *context in URLContexts) {
if ([DBClientsManager handleRedirectURL:context.URL completion:completion]) {
// stop iterating after the first handle-able url
break;
}
}
}
macOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
// generic launch handler
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
andSelector:@selector(handleAppleEvent:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
}
// custom handler
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
DBOAuthCompletion oauthCompletion = ^(DBOAuthResult *authResult) {
if (authResult != nil) {
if ([authResult isSuccess]) {
NSLog(@"\n\nSuccess! User is logged into Dropbox.\n\n");
} else if ([authResult isCancel]) {
NSLog(@"\n\nAuthorization flow was manually canceled by user!\n\n");
} else if ([authResult isError]) {
NSLog(@"\n\nError: %@\n\n", authResult);
}
// this forces your app to the foreground, after it has handled the browser redirect
[[NSRunningApplication currentApplication]
activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
}
};
[DBClientsManager handleRedirectURL:url completion:oauthCompletion];
}
用户在移动端使用Dropbox登录凭证登录后,将看到一个类似这样的窗口
如果他们点击允许或取消,则从视图控制器启动db-<APP_KEY>
重定向URL,并在你的应用程序代理的application:openURL:options:
方法中处理,从这里可以解析授权的结果。
现在你已经准备好开始制作API请求了!
尝试一些API请求
一旦你获得了OAuth 2.0令牌,你可以尝试使用Objective-C SDK进行一些API v2调用。
Dropbox客户端实例
首先创建一个引用到DBUserClient
或DBTeamClient
实例,这将是你进行API调用的实例。
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
// Reference after programmatic auth flow
DBUserClient *client = [DBClientsManager authorizedClient];
或
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
// Initialize with manually retrieved auth token
DBUserClient *client = [[DBUserClient alloc] initWithAccessToken:@"<MY_ACCESS_TOKEN>"];
处理API响应
Dropbox的用户API和商业API有三种类型的请求:RPC,上传和下载。
每种请求类型的响应处理程序都很相似。处理程序块的定义如下
- 路由结果类型(如果路由没有返回类型,则为
DBNilObject
) - 路由特定的错误(通常是一个联合类型)
- 网络请求错误(适用于所有请求的一般的错误信息,包含如请求ID、HTTP状态代码等信息)
- 输出内容(仅对下载样式端点有效,是对下载输出内容的
NSURL
/NSData
引用)
所有端点都需要响应处理程序。另一方面,进度处理程序对于所有端点是可选的。
注意:Objective-C SDK使用
NSNumber
对象代替布尔值。这样可以表示我们API响应值中的一些null性。因此,在编写如if (myAPIObject.isSomething)
这样的检查时,应小心,这实际上是检查null性而不是值。相反,你应该使用if ([myAPIObject.isSomething boolValue])
,它的作用是在使用前将NSNumber
字段转换为布尔值。
请求类型
RPC风格请求
[[client.filesRoutes createFolder:@"/test/path/in/Dropbox/account"]
setResponseBlock:^(DBFILESFolderMetadata *result, DBFILESCreateFolderError *routeError, DBRequestError *networkError) {
if (result) {
NSLog(@"%@\n", result);
} else {
NSLog(@"%@\n%@\n", routeError, networkError);
}
}];
以下是一个列出文件夹内容的示例。在响应处理程序中,我们会反复调用listFolderContinue:
(对于大型文件夹)直到列出整个文件夹
[[client.filesRoutes listFolder:@"/test/path/in/Dropbox/account"]
setResponseBlock:^(DBFILESListFolderResult *response, DBFILESListFolderError *routeError, DBRequestError *networkError) {
if (response) {
NSArray<DBFILESMetadata *> *entries = response.entries;
NSString *cursor = response.cursor;
BOOL hasMore = [response.hasMore boolValue];
[self printEntries:entries];
if (hasMore) {
NSLog(@"Folder is large enough where we need to call `listFolderContinue:`");
[self listFolderContinueWithClient:client cursor:cursor];
} else {
NSLog(@"List folder complete.");
}
} else {
NSLog(@"%@\n%@\n", routeError, networkError);
}
}];
...
...
...
- (void)listFolderContinueWithClient:(DBUserClient *)client cursor:(NSString *)cursor {
[[client.filesRoutes listFolderContinue:cursor]
setResponseBlock:^(DBFILESListFolderResult *response, DBFILESListFolderContinueError *routeError,
DBRequestError *networkError) {
if (response) {
NSArray<DBFILESMetadata *> *entries = response.entries;
NSString *cursor = response.cursor;
BOOL hasMore = [response.hasMore boolValue];
[self printEntries:entries];
if (hasMore) {
[self listFolderContinueWithClient:client cursor:cursor];
} else {
NSLog(@"List folder complete.");
}
} else {
NSLog(@"%@\n%@\n", routeError, networkError);
}
}];
}
- (void)printEntries:(NSArray<DBFILESMetadata *> *)entries {
for (DBFILESMetadata *entry in entries) {
if ([entry isKindOfClass:[DBFILESFileMetadata class]]) {
DBFILESFileMetadata *fileMetadata = (DBFILESFileMetadata *)entry;
NSLog(@"File data: %@\n", fileMetadata);
} else if ([entry isKindOfClass:[DBFILESFolderMetadata class]]) {
DBFILESFolderMetadata *folderMetadata = (DBFILESFolderMetadata *)entry;
NSLog(@"Folder data: %@\n", folderMetadata);
} else if ([entry isKindOfClass:[DBFILESDeletedMetadata class]]) {
DBFILESDeletedMetadata *deletedMetadata = (DBFILESDeletedMetadata *)entry;
NSLog(@"Deleted data: %@\n", deletedMetadata);
}
}
}
-listFolder:和 -listFolderContinue:
上传风格请求
NSData *fileData = [@"file data example" dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
// For overriding on upload
DBFILESWriteMode *mode = [[DBFILESWriteMode alloc] initWithOverwrite];
[[[client.filesRoutes uploadData:@"/test/path/in/Dropbox/account/my_output.txt"
mode:mode
autorename:@(YES)
clientModified:nil
mute:@(NO)
propertyGroups:nil
inputData:fileData]
setResponseBlock:^(DBFILESFileMetadata *result, DBFILESUploadError *routeError, DBRequestError *networkError) {
if (result) {
NSLog(@"%@\n", result);
} else {
NSLog(@"%@\n%@\n", routeError, networkError);
}
}] setProgressBlock:^(int64_t bytesUploaded, int64_t totalBytesUploaded, int64_t totalBytesExpectedToUploaded) {
NSLog(@"\n%lld\n%lld\n%lld\n", bytesUploaded, totalBytesUploaded, totalBytesExpectedToUploaded);
}];
-uploadData:mode:autorename:clientModified
以下是一个“批量”上传大量文件的复杂上传示例
NSMutableDictionary<NSURL *, DBFILESCommitInfo *> *uploadFilesUrlsToCommitInfo = [NSMutableDictionary new];
DBFILESCommitInfo *commitInfo = [[DBFILESCommitInfo alloc] initWithPath:@"/output/path/in/Dropbox/file.txt"];
[uploadFilesUrlsToCommitInfo setObject:commitInfo forKey:[NSURL fileURLWithPath:@"/local/path/to/file.txt"]];
[client.filesRoutes batchUploadFiles:uploadFilesUrlsToCommitInfo
queue:nil
progressBlock:^(int64_t uploaded, int64_t uploadedTotal, int64_t expectedToUploadTotal) {
NSLog(@"Uploaded: %lld UploadedTotal: %lld ExpectedToUploadTotal: %lld", uploaded, uploadedTotal,
expectedToUploadTotal);
}
responseBlock:^(NSDictionary<NSURL *, DBFILESUploadSessionFinishBatchResultEntry *> *fileUrlsToBatchResultEntries,
DBASYNCPollError *finishBatchRouteError, DBRequestError *finishBatchRequestError,
NSDictionary<NSURL *, DBRequestError *> *fileUrlsToRequestErrors) {
if (fileUrlsToBatchResultEntries) {
NSLog(@"Call to `/upload_session/finish_batch/check` succeeded");
for (NSURL *clientSideFileUrl in fileUrlsToBatchResultEntries) {
DBFILESUploadSessionFinishBatchResultEntry *resultEntry = fileUrlsToBatchResultEntries[clientSideFileUrl];
if ([resultEntry isSuccess]) {
NSString *dropboxFilePath = resultEntry.success.pathDisplay;
NSLog(@"File successfully uploaded from %@ on local machine to %@ in Dropbox.",
[clientSideFileUrl path], dropboxFilePath);
} else if ([resultEntry isFailure]) {
// This particular file was not uploaded successfully, although the other
// files may have been uploaded successfully. Perhaps implement some retry
// logic here based on `uploadNetworkError` or `uploadSessionFinishError`
DBRequestError *uploadNetworkError = fileUrlsToRequestErrors[clientSideFileUrl];
DBFILESUploadSessionFinishError *uploadSessionFinishError = resultEntry.failure;
// implement appropriate retry logic
}
}
}
if (finishBatchRouteError) {
NSLog(@"Either bug in SDK code, or transient error on Dropbox server");
NSLog(@"%@", finishBatchRouteError);
} else if (finishBatchRequestError) {
NSLog(@"Request error from calling `/upload_session/finish_batch/check`");
NSLog(@"%@", finishBatchRequestError);
} else if ([fileUrlsToRequestErrors count] > 0) {
NSLog(@"Other additional errors (e.g. file doesn't exist client-side, etc.).");
NSLog(@"%@", fileUrlsToRequestErrors);
}
}];
注意:上述使用的
batchUploadFiles:
路由方法会自动将大型文件分块上传,这是SDK中其他上传方法所不具备的。此外,使用此路由时,响应和进度处理程序直接作为参数传递给路由,而不是通过setResponseBlock
或setProgressBlock
方法。
-batchUploadFiles:queue:progressBlock:responseBlock
下载风格请求
以下是一个将文件下载到文件(URL)的示例
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *outputDirectory = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
NSURL *outputUrl = [outputDirectory URLByAppendingPathComponent:@"test_file_output.txt"];
[[[client.filesRoutes downloadUrl:@"/test/path/in/Dropbox/account/my_file.txt" overwrite:YES destination:outputUrl]
setResponseBlock:^(DBFILESFileMetadata *result, DBFILESDownloadError *routeError, DBRequestError *networkError,
NSURL *destination) {
if (result) {
NSLog(@"%@\n", result);
NSData *data = [[NSFileManager defaultManager] contentsAtPath:[destination path]];
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@\n", dataStr);
} else {
NSLog(@"%@\n%@\n", routeError, networkError);
}
}] setProgressBlock:^(int64_t bytesDownloaded, int64_t totalBytesDownloaded, int64_t totalBytesExpectedToDownload) {
NSLog(@"%lld\n%lld\n%lld\n", bytesDownloaded, totalBytesDownloaded, totalBytesExpectedToDownload);
}];
-downloadUrl:rev:overwrite:destination
以下是将内容直接下载到内存(NSData)的示例
[[[client.filesRoutes downloadData:@"/test/path/in/Dropbox/account/my_file.txt"]
setResponseBlock:^(DBFILESFileMetadata *result, DBFILESDownloadError *routeError, DBRequestError *networkError,
NSData *fileContents) {
if (result) {
NSLog(@"%@\n", result);
NSString *dataStr = [[NSString alloc] initWithData:fileContents encoding:NSUTF8StringEncoding];
NSLog(@"%@\n", dataStr);
} else {
NSLog(@"%@\n%@\n", routeError, networkError);
}
}] setProgressBlock:^(int64_t bytesDownloaded, int64_t totalBytesDownloaded, int64_t totalBytesExpectedToDownload) {
NSLog(@"%lld\n%lld\n%lld\n", bytesDownloaded, totalBytesDownloaded, totalBytesExpectedToDownload);
}];
关于后台会话的说明
目前,SDK 使用后台 NSURLSession
执行所有下载任务和一些上传任务(包括从文件上传,但不包括从内存或流上传)。背景会话使用单独的进程来处理所有数据传输。这很方便,因为当您的应用程序进入后台时,下载/上传会继续。
然而,后台 NSURLSession
的超时时间是几乎无限的,所以如果您丢失了网络连接,错误处理器将永远不会执行。相反,进程将等待恢复连接,然后从中继续。
如果您希望在丢失连接的情况下获得更多响应的错误反馈,则希望强制要求将所有请求放入前台 NSURLSession
。请参阅 README 文件中关于如何操作的示例。
要了解更多信息,请参阅 Apple 的文档。
注意:您应该在 actually test device 上测试所有后台会话行为,而不是 Xcode 模拟器,因为当它涉及处理后台会话行为时,模拟器有很多错误行为。
处理响应和错误
Dropbox API v2 主要处理两种数据类型:结构体和联合体。广义地说,大多数路由参数是结构体类型,而大多数路由错误是联合体类型。
注意:在这个上下文中,“结构体”和“联合体”是 Dropbox API 专用术语,而不是用来查询 API 的任何语言,因此您应该避免根据 Objective-C 中的定义来思考它们。
结构体类型是“传统”对象类型,即由一个或多个实例字段组成的复合类型。所有公共实例字段在运行时均可用,无论运行时状态如何。
联合体类型另一方面,表示一个可以采取多个值类型的单一值,具体取决于状态。我们将所有这些不同的类型场景归纳为“联合对象”,但该对象仅在运行时作为一种类型存在。每个联合体状态类型,或标签,可能都有一个相关值(如果没有,则该联合体状态类型被认为是空)。相关值类型可以是原始值、结构体或联合体。尽管 Objective-C SDK 将联合体类型表示为具有多个实例字段的对象,但在运行时至多只有一个实例字段可访问,这取决于联合体的标签状态。
例如,/delete 端点返回一个错误 DeleteError
,这是一个联合类型。DeleteError
联合可以具有两种不同的标签状态:path_lookup
(如果在查找路径时出现问题)或path_write
(如果在写入路径或删除存在问题时)。在此,两个标签状态都具有非空相关值(分别为类型 DBFILESLookupError
和 DBFILESWriteError
)。
因此,一个联合对象可以捕获许多场景,每个场景都有自己的值类型。
为了正确处理联合类型,应调用与联合相关联的所有 is<TAG_STATE>
方法。一旦确定联合的当前标签状态,就可以安全地访问与该标签状态相关联的值(只要存在相关联值的类型,即非 void)。如果在运行时尝试访问当前标签状态未关联的联合实例字段,将抛出 异常。请参见下面的内容。
路由特定错误
[[client.filesRoutes delete_:@"/test/path/in/Dropbox/account"]
setResponseBlock:^(DBFILESMetadata *result, DBFILESDeleteError *routeError, DBRequestError *networkError) {
if (result) {
NSLog(@"%@\n", result);
} else {
// Error is with the route specifically (status code 409)
if (routeError) {
if ([routeError isPathLookup]) {
// Can safely access this field
DBFILESLookupError *pathLookup = routeError.pathLookup;
NSLog(@"%@\n", pathLookup);
} else if ([routeError isPathWrite]) {
DBFILESWriteError *pathWrite = routeError.pathWrite;
NSLog(@"%@\n", pathWrite);
// This would cause a runtime error
// DBFILESLookupError *pathLookup = routeError.pathLookup;
}
}
NSLog(@"%@\n%@\n", routeError, networkError);
}
}];
通用网络请求错误
在网络错误的情况下,无论错误是否特定于路由,总将返回一个通用 DBRequestError
类型,它包含诸如 Dropbox 请求 ID 和 HTTP 状态码之类的信息。
DBRequestError
类型是一个特殊的联合类型,类似于标准 API v2 联合类型,但还包括一系列 as<TAG_STATE>
方法,每个方法都返回特定错误子类型的实例。与访问常规联合中的关联值一样,应在相应的 is<TAG_STATE>
方法返回 true 之后调用 as<TAG_STATE>
。请参见以下内容。
[[client.filesRoutes delete_:@"/test/path/in/Dropbox/account"]
setResponseBlock:^(DBFILESMetadata *result, DBFILESDeleteError *routeError, DBRequestError *networkError) {
if (result) {
NSLog(@"%@\n", result);
} else {
if (routeError) {
// see handling above
}
// Error not specific to the route (status codes 500, 400, 401, 403, 404, 429)
else {
if ([networkError isInternalServerError]) {
DBRequestInternalServerError *internalServerError = [networkError asInternalServerError];
NSLog(@"%@\n", internalServerError);
} else if ([networkError isBadInputError]) {
DBRequestBadInputError *badInputError = [networkError asBadInputError];
NSLog(@"%@\n", badInputError);
} else if ([networkError isAuthError]) {
DBRequestAuthError *authError = [networkError asAuthError];
NSLog(@"%@\n", authError);
} else if ([networkError isAccessError]) {
DBRequestAccessError *accessError = [networkError asAccessError];
NSLog(@"%@\n", accessError);
} else if ([networkError isRateLimitError]) {
DBRequestRateLimitError *rateLimitError = [networkError asRateLimitError];
NSLog(@"%@\n", rateLimitError);
} else if ([networkError isHttpError]) {
DBRequestHttpError *genericHttpError = [networkError asHttpError];
NSLog(@"%@\n", genericHttpError);
} else if ([networkError isClientError]) {
DBRequestClientError *genericLocalError = [networkError asClientError];
NSLog(@"%@\n", genericLocalError);
}
}
}
}];
响应处理边缘情况
某些路由返回联合类型作为结果类型,因此您应该准备好以处理联合路由错误相同的方式处理这些结果。请查阅每个端点的文档,以确保您正确处理路由的响应类型。
一些路由返回的结果类型是具有子类型的数据类型,也就是说,可以具有多个状态类型(类似于联合)的结构体。
例如,/delete端点返回一个通用的Metadata
类型,它可以是FileMetadata
结构体、FolderMetadata
结构体或DeletedMetadata
结构体。要确定在运行时Metadata
类型存在哪种子类型,对每个可能的类执行一个isKindOfClass
检查,然后相应地进行类型转换。请参见以下内容
[[client.filesRoutes delete_:@"/test/path/in/Dropbox/account"]
setResponseBlock:^(DBFILESMetadata *result, DBFILESDeleteError *routeError, DBRequestError *networkError) {
if (result) {
if ([result isKindOfClass:[DBFILESFileMetadata class]]) {
DBFILESFileMetadata *fileMetadata = (DBFILESFileMetadata *)result;
NSLog(@"File data: %@\n", fileMetadata);
} else if ([result isKindOfClass:[DBFILESFolderMetadata class]]) {
DBFILESFolderMetadata *folderMetadata = (DBFILESFolderMetadata *)result;
NSLog(@"Folder data: %@\n", folderMetadata);
} else if ([result isKindOfClass:[DBFILESDeletedMetadata class]]) {
DBFILESDeletedMetadata *deletedMetadata = (DBFILESDeletedMetadata *)result;
NSLog(@"Deleted data: %@\n", deletedMetadata);
}
} else {
if (routeError) {
// see handling above
} else {
// see handling above
}
}
}];
在API v2文档中,这个Metadata
对象称为具有子类型的数据类型
。
具有子类型的数据类型是结构和联合的组合方式。具有子类型的数据类型是包含一个标签的结构体对象,该标签指定在运行时对象存在于哪个子类型。我们之所以有这种结构,就像联合一样,是为了能够用一个对象捕获许多场景。
在上面的例子中,Metadata
类型可以是FileMetadata
、FolderMetadata
或DeletedMetadata
。这些类型都有共同的实例字段,如“name”(文件、文件夹或删除类型的名称),但同时也包含特定于特定子类型的实例字段。为了利用继承,我们设置了一个通用的父类型Metadata
,它捕获了所有共同的实例字段,但也具有一个指定对象当前存在的子类型的标签实例字段。
因此,具有子类型的数据类型是结构和联合的混合体。只有少数路由返回这种类型的结果。
统一的全局错误处理
通常,错误是通过对返回的请求任务对象调用setResponseBlock
按请求逐个处理的。然而,有时根据错误类型,不论请求的来源如何,一致地处理错误更有意义。例如,可能每当发生/files/list_folder
错误时,您都希望显示相同的对话框。或者,也许每当发生HTTP认证错误时,您只是希望将用户从您的应用程序中注销。
要实现这些示例,您应该在应用程序的设置逻辑中(可能是在您的应用程序代理中)有如下所示的代码
void (^listFolderGlobalResponseBlock)(DBFILESListFolderError *, DBRequestError *, DBTask *) =
^(DBFILESListFolderError *folderError, DBRequestError *networkError, DBTask *restartTask) {
if (folderError) {
// Display some dialog relating to this error
}
};
void (^networkGlobalResponseBlock)(DBRequestError *, DBTask *) =
^(DBRequestError *networkError, DBTask *restartTask) {
if ([networkError isAuthError]) {
// log the user out of the app, for instance
[DBClientsManager unlinkAndResetClients];
} else if ([networkError isRateLimitError]) {
// automatically retry after backoff period
DBRequestRateLimitError *rateLimitError = [networkError asRateLimitError];
int backOff = [rateLimitError.retryAfter intValue];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, backOff * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[restartTask restart];
});
}
};
// one response block per error type to globally handle
[DBGlobalErrorResponseHandler registerRouteErrorResponseBlock:listFolderGlobalResponseBlock
routeErrorType:[DBFILESListFolderError class]];
// only one response block total to handle all network errors
[DBGlobalErrorResponseHandler registerNetworkErrorResponseBlock:networkGlobalResponseBlock];
该SDK允许您设置一个响应块以处理所有非路线特定(如HTTP认证错误或速率限制错误)的一般网络错误。SDK还允许您设置一个响应块,以便在返回特定错误类型时执行。
这些全局响应块将自动执行,**同时**执行您为特定请求提供的响应块。
自定义网络调用
配置网络客户端
您可以配置SDK使用的网络客户端以进行API请求。您可以提供自定义字段,如自定义用户代理或自定义委托队列来管理响应处理代码。
例如,您可以强制SDK在前台会话中进行所有网络请求。
iOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
DBTransportDefaultConfig *transportConfig =
[[DBTransportDefaultConfig alloc] initWithAppKey:@"<APP_KEY>" forceForegroundSession:YES];
[DBClientsManager setupWithTransportConfig:transportConfig];
macOS
#import <ObjectiveDropboxOfficial/ObjectiveDropboxOfficial.h>
DBTransportDefaultConfig *transportConfig =
[[DBTransportDefaultConfig alloc] initWithAppKey:@"<APP_KEY>" forceForegroundSession:YES];
[DBClientsManager setupWithTransportConfigDesktop:transportConfig];
请查看DBTransportDefaultConfig
类获取所有可自定义的网络参数。
指定API调用响应队列
默认情况下,响应/进度处理代码在主线程上运行。如果您希望响应/进度处理代码在不同的线程上运行,可以通过setResponseBlock
方法为每个API调用设置自定义响应队列。
[[client.filesRoutes listFolder:@""]
setResponseBlock:^(DBFILESListFolderResult *result, DBFILESListFolderError *routeError, DBRequestError *networkError) {
if (result) {
NSLog(@"%@", [NSThread currentThread]); // Output: <NSThread: 0x600000261480>{number = 5, name = (null)}
NSLog(@"%@", [NSThread mainThread]); // Output: <NSThread: 0x618000062bc0>{number = 1, name = (null)}
NSLog(@"%@\n", result);
}
} queue:[NSOperationQueue new]]
DBClientsManager
类
Objective-C SDK包含一个便利类DBClientsManager
,用于将SDK的不同功能集成到一个类中。
单个Dropbox用例
对于大多数应用程序,可以合理假设一次只能管理一个Dropbox帐户(和访问令牌)。在这种情况下,DBClientsManager
流程如下
- 在集成应用程序的应用程序代理中调用
setupWithAppKey
/setupWithAppKeyDesktop
(或setupWithTeamAppKey
/setupWithTeamAppKeyDesktop
)进行集成。 - 如果存储了任何访问令牌,
DBClientsManager
类将确定这一点;如果存在,则任意选择一个令牌,用于共享实例的authorizedClient
/authorizedTeamClient
。 - 如果没有找到令牌,则SDK客户端应调用
authorizeFromController
/authorizeFromControllerDesktop
以初始化OAuth流程。 - 如果启动了认证流程,则SDK客户端应在集成应用程序的应用程序代理中调用
handleRedirectURL
(或handleRedirectURLTeam
)来处理认证重定向回应用程序并存储检索到的访问令牌。 DBClientsManager
类使用特定网络配置设置了DBUserClient
(或DBTeamClient
),该配置由传递的DBTransportDefaultConfig
实例定义(如果没有传递配置实例,则使用标准配置)。
然后使用DBUserClient
(或DBTeamClient
)来执行所有所需的API调用。
- 调用
unlinkAndResetClients
以注销Dropbox用户并清除所有访问令牌。
多个Dropbox用例
对于某些应用程序,需要同时管理多个Dropbox帐户(和访问令牌)。在这种情况下,DBClientsManager
流程如下
- 访问令牌的uid由集成SDK的应用程序管理,以供后续检索。
- 在集成应用程序的应用程序代理中调用
setupWithAppKey
/setupWithAppKeyDesktop
(或setupWithTeamAppKey
/setupWithTeamAppKeyDesktop
)进行集成。 - 如果存储了任何访问令牌,
DBClientsManager
类将确定这一点;如果存在,则任意选择一个令牌,用于共享实例的authorizedClient
/authorizedTeamClient
。 DBClientsManager
类还会从钥匙串中所有存储的令牌中填充authorizedClients
/authorizedTeamClients
共享字典,如果存在任何的话。- 如果没有找到令牌,则SDK客户端应调用
authorizeFromController
/authorizeFromControllerDesktop
以初始化OAuth流程。 - 如果启动了身份验证流程,则在集成应用程序的app delegate中调用
handleRedirectURL
(或handleRedirectURLTeam
)来处理应用程序中的身份验证重定向并将检索到的访问令牌存储起来。 - 在此阶段,与SDK集成的应用程序应持久保存来自
handleRedirectURL
(或handleRedirectURLTeam
)方法返回的DBOAuthResult
对象中DBAccessToken
字段的tokenUid
。 DBClientsManager
类使用传入的DBTransportDefaultConfig
实例定义的特定网络配置(如果没有传入配置实例,则使用标准配置)设置一个DBUserClient
(或DBTeamClient
),并将其保存到授权客户端列表中。
然后使用 authorizedClients
/ authorizedTeamClients
中的 DBUserClient
(或 DBTeamClient
)来执行所有所需的API调用。
- 调用
unlinkAndResetClient
注销特定Dropbox用户并清除其访问令牌。 - 调用
unlinkAndResetClients
注销所有Dropbox用户并清除所有访问令牌。
示例
展示如何将应用程序与SDK集成示例项目可在 Examples/
文件夹中找到。
- DBRoulette - 在您的Dropbox中玩一个有趣的照片轮盘游戏!
从API v1迁移
本节包含有关将应用程序从API v1迁移到API v2(应在API v1于2017年6月28日停用之前完成)的相关信息。
有关API v1迁移的通用指南,请参阅此处。
从较早的SDK迁移OAuth令牌
如果您的应用程序最初使用的是早期API v1 SDK,包括iOS Core SDK、OS X Core SDK、iOS Sync SDK或OS X Sync SDK,那么您可以使用v2 SDK将OAuth 1令牌一次性迁移到OAuth 2.0令牌,这些令牌用于API v2。这样,当您将应用程序从早期SDK迁移到新的API v2 SDK时,用户在您执行此更新后无需重新对Dropbox进行认证。
要执行此认证令牌迁移,在应用程序代理中,您应该调用以下方法
+checkAndPerformV1TokenMigration:queue:appKey:appSecret
BOOL willPerformMigration = [DBClientsManager checkAndPerformV1TokenMigration:^(BOOL shouldRetry, BOOL invalidAppKeyOrSecret,
NSArray<NSArray<NSString *> *> *unsuccessfullyMigratedTokenData) {
if (invalidAppKeyOrSecret) {
// Developers should ensure that the appropriate app key and secret are being supplied.
// If your app has multiple app keys / secrets, then run this migration method for
// each app key / secret combination, and ignore this boolean.
}
if (shouldRetry) {
// Store this BOOL somewhere to retry when network connection has returned
}
if ([unsuccessfullyMigratedTokenData count] != 0) {
NSLog(@"The following tokens were unsucessfully migrated:");
for (NSArray<NSString *> *tokenData in unsuccessfullyMigratedTokenData) {
NSLog(@"DropboxUserID: %@, AccessToken: %@, AccessTokenSecret: %@, StoredAppKey: %@", tokenData[0],
tokenData[1], tokenData[2], tokenData[3]);
}
}
if (!invalidAppKeyOrSecret && !shouldRetry && [unsuccessfullyMigratedTokenData count] == 0) {
[DBClientsManager setupWithAppKey:@"<APP_KEY>"];
}
} queue:nil appKey:@"<APP_KEY>" appSecret:@"<APP_SECRET>"];
if (!willPerformMigration) {
[DBClientsManager setupWithAppKey:@"<APP_KEY>"];
}
该方法应成功地将Dropbox API SDK存储的所有访问令牌从大约2012年至今迁移,适用于iOS和OS X。它将对每个存储在您的应用程序密钥链中的OAuth 1令牌进行一次调用,这些令牌是由v1 SDK存储的。该方法将在主线程之外执行所有网络请求。
在此,将令牌迁移视为原子操作。要么一次性迁移所有可能的令牌,要么一个也不迁移。如果所有令牌转换请求都成功完成,则在responseBlock中的shouldRetry参数将设置为NO。如果某些令牌转换请求成功而某些失败,并且失败的原因不是网络连接问题(例如,令牌已失效),则迁移将继续正常进行,无法迁移的令牌将跳过,shouldRetry将设置为NO。如果任何失败是由于网络连接问题,则不会迁移任何令牌,且shouldRetry将设置为YES。
文档
Stone
我们所有的路由和数据类型都是使用名为Stone的框架自动生成的。
stone
仓库包含所有Objective-C专用生成逻辑,而spec
仓库包含作为特定语言生成器输入的语言无关API端点规范。
修改
如果您想修改 SDK 代码库,请遵循以下步骤
- 将此 GitHub 仓库克隆到您的本地文件系统
- 运行
git submodule init
然后运行git submodule update
- 导航到
TestObjectiveDropbox
并运行pod install
- 在 Xcode 中打开
TestObjectiveDropbox/TestObjectiveDropbox.xcworkspace
- 完善 SDK 源代码中的更改。
为了确保您的更改没有破坏任何现有功能,您可以按照 ViewController.m
文件中列出的说明运行一系列集成测试。
代码生成
如果您对手动生成 SDK 序列化逻辑感兴趣,请执行以下操作
- 将此 GitHub 仓库克隆到您的本地文件系统
- 运行
git submodule init
然后运行git submodule update
- 导航到 Stone GitHub 仓库 并安装所有必要的依赖项
- 运行
./generate_base_client.py
生成代码
为了确保您的更改没有破坏任何现有功能,您可以按照 ViewController.m
文件中列出的说明运行一系列集成测试。
App Store Connect 隐私标签
为了帮助使用 Dropbox SDK 的开发者填写 Apple 的隐私实践调查问卷,我们已提供以下关于 Dropbox 可能收集和使用的数据的信息。
在填写问卷的过程中,请注意以下信息是一般性的。Dropbox SDK 是设计由开发者配置的,以便将 Dropbox 功能与应用程序最佳融合。由于 Dropbox SDK 的可定制性,我们无法提供每个应用程序实际数据收集和使用的具体信息。我们建议开发者参考我们的 Dropbox HTTP 开发者文档,以了解每个 Dropbox API 数据的收集方式。
此外,请注意,以下信息仅标识Dropbox的数据收集和使用情况。您负责识别您应用程序中数据的收集和使用,这可能导致与以下标识不同的调查问卷答案。
数据 | 由Dropbox收集 | 数据使用 | 与用户关联的数据 | 跟踪 |
---|---|---|---|---|
联系信息 | ||||
• 姓名 | 不收集 | 不适用 | 不适用 | 不适用 |
• 电子邮件地址 | 可能收集 (如果您启用使用电子邮件地址进行身份验证的身份验证) |
• 应用程序功能 | 是 | 否 |
健康与健身 | 不收集 | 不适用 | 不适用 | 不适用 |
财务信息 | 不收集 | 不适用 | 不适用 | 不适用 |
位置 | 不收集 | 不适用 | 不适用 | 不适用 |
敏感信息 | 不收集 | 不适用 | 不适用 | 不适用 |
联系人 | 不收集 | 不适用 | 不适用 | 不适用 |
用户内容 | ||||
• 音频数据 | 可能收集 | • 应用程序功能 | 是 | 否 |
• 图片或视频 | 可能收集 | • 应用程序功能 | 是 | 否 |
• 其他用户内容 | 可能收集 | • 应用程序功能 | 是 | 否 |
浏览历史 | 不收集 | 不适用 | 不适用 | 不适用 |
搜索历史 | ||||
• 搜索历史 | 可能收集 (如果使用搜索功能) |
• 应用程序功能 • 分析 |
是 | 否 |
标识符 | ||||
• 用户ID | 收集 | • 应用程序功能 • 分析 |
是 | 否 |
购买 | 不收集 | 不适用 | 不适用 | 不适用 |
使用数据 | ||||
• 产品互动 | 收集 | • 应用程序功能 • 分析 • 产品个性化 |
是 | 否 |
诊断 | ||||
• 其他诊断数据 | 收集 (API调用日志) |
• 应用程序功能 | 是 | 否 |
其他数据 | 不适用 | 不适用 | 不适用 | 不适用 |
错误
请将任何错误发布在项目GitHub页面上的问题跟踪器中。
请随问题附上以下内容
- 关于什么功能无法正常工作的描述
- 示例代码以帮助复制问题
谢谢!