SwiftyDropboxObjC 10.0.7

SwiftyDropboxObjC 10.0.7

Dropbox, Inc. 维护。



  • Stephen Cobbe

Swift 的 Dropbox

10.0.0 版本与 SDK 的先前版本有很大差异。请参阅 10.0.0 版本中的变更,并在需要时,请参阅 从 dropbox-sdk-obj-c 迁移

官方 Dropbox Swift SDK,用于在 iOS 或 macOS 上与 Dropbox API v2 集成。

完整文档 在这里


目录


系统要求

  • iOS 11.0+
  • macOS 10.13+
  • Xcode 13.3+
  • Swift 5.6+

开始使用

注册您的应用

在使用此SDK之前,您应该在Dropbox应用控制台中注册您的应用Dropbox App Console。这将创建一个与Dropbox关联的记录,您的应用将与之相关联的API调用。

获取 OAuth 2.0 令牌

所有请求都需使用 OAuth 2.0 访问令牌。OAuth 令牌代表了一个 Dropbox 应用与 Dropbox 用户账户或团队的认证连接。

创建应用后,您可以访问应用控制台手动生成访问令牌以授权您的应用访问您的 Dropbox 账户。否则,您可以使用 SDK 预定义的认证流程程序化地获取 OAuth 令牌。更多信息,请参阅下面


SDK 分发

您可以使用多种方法之一将 Dropbox Swift SDK 集成到您的项目中。

Swift Package Manager

您可以使用 Swift Package Manager 通过指定 Dropbox Swift SDK 存储库 URL 来将 Dropbox Swift SDK 安装到您的项目中。

https://github.com/dropbox/SwiftyDropbox.git

有关更多信息,请参阅Apple 的“将对您的应用程序添加包依赖”文档

CocoaPods

要使用 CocoaPods(Cocoa项目依赖管理器),您应首先使用以下命令安装它

$ gem install cocoapods

然后切换到包含您的项目的目录,创建一个名为 Podfile 的新文件。您可以使用 pod init 或者打开现有的 Podfile,然后将其添加到主循环中的 pod 'SwiftyDropbox'。您的 Podfile 应该看起来像这样:

use_frameworks!

target '<YOUR_PROJECT_NAME>' do
    pod 'SwiftyDropbox'
end

如果您的项目中包含需要访问 Dropbox SDK 的 Objective-C 代码,则有一个名为 SwiftyDropboxObjC 的单独的 pod,它包含用于 SDK 的 Objective-C 兼容层。将此 pod 添加到您的 Podfile 中(除了 SwiftyDropbox 以外,或者单独添加)。有关更多信息,请参阅本 README 中的 Objective-C 部分。

然后,运行以下命令安装依赖项:

$ pod install

一旦您的项目与 Dropbox Swift SDK 整合,您可以使用以下命令拉取 SDK 更新:

$ pod update

配置您的项目

一旦您将 Dropbox Swift SDK 集成到项目中,在开始使用 API 进行调用之前,还需要执行一些额外步骤。

应用程序 .plist 文件

如果您在 iOS SDK 9.0 上编译,则需要修改您的应用程序的 .plist 来处理 Apple 对 canOpenURL 函数的新安全更改。您应该在应用程序的 .plist 文件中添加以下代码:

<key>LSApplicationQueriesSchemes</key>
    <array>
        <string>dbapi-8-emm</string>
        <string>dbapi-2</string>
    </array>

这允许 Swift 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 文件应类似于以下内容:

Info .plist Example


授予流程处理

有三种方法可以编程获取OAuth 2.0访问令牌

  • 直接认证(仅限iOS):如果已安装,此功能将启动官方Dropbox iOS应用进行认证,然后跳转回SDK
  • Safari视图控制器认证(仅限iOS):启动一个SFSafariViewController以简化认证流程。这很受欢迎,因为它对最终用户更安全,并且可以使用现有的会话数据来避免需要用户重新输入Dropbox凭据。
  • 重定向到外部浏览器(仅限macOS):启动用户的默认浏览器以简化认证流程。这也同样受欢迎,因为它对最终用户更安全,并且可以使用现有的会话数据来避免需要用户重新输入Dropbox凭据。

为了方便上述授权流程,应执行以下步骤


初始化DropboxClient实例

从您的应用程序代理

SwiftUI注意:如果您的应用程序没有应用程序代理,您可能需要创建一个。

iOS
import SwiftyDropbox

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    DropboxClientsManager.setupWithAppKey("<APP_KEY>")
    return true
}
macOS
import SwiftyDropbox

func applicationDidFinishLaunching(_ aNotification: Notification) {
    DropboxClientsManager.setupWithAppKeyDesktop("<APP_KEY>")
}

开始授权流程

您可以通过在应用程序的视图控制器中调用 authorizeFromController:controller:openURL 方法来开始授权流程。请注意,控制器引用将被弱引用。对于 SwiftUI 应用程序,可以将控制器参数传递为 nil,然后使用应用程序的根视图控制器来呈现流程。

从您的视图控制器开始

macOS
import SwiftyDropbox

func myButtonInControllerPressed() {
    // OAuth 2 code flow with PKCE that grants a short-lived token with scopes, and performs refreshes of the token automatically.
    let scopeRequest = ScopeRequest(scopeType: .user, scopes: ["account_info.read"], includeGrantedScopes: false)
    DropboxClientsManager.authorizeFromControllerV2(
        sharedApplication: NSApplication.shared,
        controller: self,
        loadingStatusDelegate: nil,
        openURL: {(url: URL) -> Void in NSWorkspace.shared.open(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
    // DropboxClientsManager.authorizeFromController(sharedApplication: NSApplication.shared,
    //                                               controller: self,
    //                                               openURL: { (url: URL) -> Void in
    //                                                 NSWorkspace.shared.open(url)
    //                                               })
}

通过内联网页开始认证流程将启动一个类似于这样的窗口

Auth Flow Init Example


处理回到 SDK 中的重定向

要处理认证流程完成后返回到 Swift SDK 的重定向,您应该在应用程序的代理中添加以下代码

SwiftUI注意:如果您的应用程序没有应用程序代理,您可能需要创建一个。

iOS
import SwiftyDropbox

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    let oauthCompletion: DropboxOAuthCompletion = {
      if let authResult = $0 {
          switch authResult {
          case .success:
              print("Success! User is logged into DropboxClientsManager.")
          case .cancel:
              print("Authorization flow was manually canceled by user!")
          case .error(_, let description):
              print("Error: \(String(describing: description))")
          }
      }
    }
    let canHandleUrl = DropboxClientsManager.handleRedirectURL(url, includeBackgroundClient: false, completion: oauthCompletion)
    return canHandleUrl
}

或者如果你的应用是iOS13+,或者你的应用也支持场景,请将以下代码添加到应用的主场景代理中

注意:如果你的应用还没有创建场景代理,你可能需要创建一个。

import SwiftyDropbox

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
     let oauthCompletion: DropboxOAuthCompletion = {
      if let authResult = $0 {
          switch authResult {
          case .success:
              print("Success! User is logged into DropboxClientsManager.")
          case .cancel:
              print("Authorization flow was manually canceled by user!")
          case .error(_, let description):
              print("Error: \(String(describing: description))")
          }
      }
    }

    for context in URLContexts {
        // stop iterating after the first handle-able url
        if DropboxClientsManager.handleRedirectURL(context.url, includeBackgroundClient: false, completion: oauthCompletion) { break }
    }
}
macOS
import SwiftyDropbox

func applicationDidFinishLaunching(_ aNotification: Notification) {
    ...... // code outlined above goes here

    NSAppleEventManager.shared().setEventHandler(self,
                                                 andSelector: #selector(handleGetURLEvent),
                                                 forEventClass: AEEventClass(kInternetEventClass),
                                                 andEventID: AEEventID(kAEGetURL))
}

func handleGetURLEvent(_ event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) {
    if let aeEventDescriptor = event?.paramDescriptor(forKeyword: AEKeyword(keyDirectObject)) {
        if let urlStr = aeEventDescriptor.stringValue {
            let url = URL(string: urlStr)!
            let oauthCompletion: DropboxOAuthCompletion = {
                if let authResult = $0 {
                    switch authResult {
                    case .success:
                        print("Success! User is logged into Dropbox.")
                    case .cancel:
                        print("Authorization flow was manually canceled by user!")
                    case .error(_, let description):
                        print("Error: \(String(describing: description))")
                    }
                }
            }
            DropboxClientsManager.handleRedirectURL(url, includeBackgroundClient: false, completion: oauthCompletion)
            // this brings your application back the foreground on redirect
            NSApp.activate(ignoringOtherApps: true)
        }
    }
}

最终用户通过应用内网页使用Dropbox登录凭证登录后,他们将看到如下窗口

Auth Flow Approval Example

如果他们点击允许取消db-<APP_KEY>重定向URL将从网页中启动,并在应用代理的application:handleOpenURL方法中处理,从那里可以解析授权结果。

现在,你可以开始进行API请求了!


尝试进行一些API请求

一旦你获得了OAuth 2.0令牌,你可以尝试使用Swift SDK进行一些API v2调用。

Dropbox客户端实例

首先创建一个对将要使用进行API调用的DropboxClientDropboxTeamClient实例的引用。

import SwiftyDropbox

// Reference after programmatic auth flow
let client = DropboxClientsManager.authorizedClient

或者

import SwiftyDropbox

// Initialize with manually retrieved auth token
let client = DropboxClient(accessToken: "<MY_ACCESS_TOKEN>")

处理API响应

Dropbox的用户API企业API有三种请求类型:RPC、上传和下载。

每种请求类型的响应处理器之间非常相似。处理器块中的参数如下所示

  • 路由结果类型(如果路由没有返回类型则为Void
  • 网络错误(特定路由错误或通用网络错误)
  • 输出内容(仅对下载类型端点,表示下载输出的URL / Data引用)

注意:所有端点都需要响应处理器。另一方面,进度处理器对所有端点都是可选的。

Swift并发

从10.0.0版本开始,所有请求类型也支持通过async response()函数使用Swift并发(async/await)。

let response = try await client.files.createFolder(path: "/test/path/in/Dropbox/account").response()

请求类型

RPC样式请求

client.files.createFolder(path: "/test/path/in/Dropbox/account").response { response, error in
    if let response = response {
        print(response)
    } else if let error = error {
        print(error)
    }
}

上传样式请求

let fileData = "testing data example".data(using: String.Encoding.utf8, allowLossyConversion: false)!

let request = client.files.upload(path: "/test/path/in/Dropbox/account", input: fileData)
    .response { response, error in
        if let response = response {
            print(response)
        } else if let error = error {
            print(error)
        }
    }
    .progress { progressData in
        print(progressData)
    }

// in case you want to cancel the request
if someConditionIsSatisfied {
    request.cancel()
}

下载样式请求

// Download to URL
let fileManager = FileManager.default
let directoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destination = directoryURL.appendingPathComponent("myTestFile")

client.files.download(path: "/test/path/in/Dropbox/account", overwrite: true, destination: destination)
    .response { response, error in
        if let response = response {
            print(response)
        } else if let error = error {
            print(error)
        }
    }
    .progress { progressData in
        print(progressData)
    }


// Download to Data
client.files.download(path: "/test/path/in/Dropbox/account")
    .response { response, error in
        if let response = response {
            let responseMetadata = response.0
            print(responseMetadata)
            let fileContents = response.1
            print(fileContents)
        } else if let error = error {
            print(error)
        }
    }
    .progress { progressData in
        print(progressData)
    }

处理响应和错误

Dropbox API v2 主要处理两种数据类型:结构体联合体。总的来说,大多数路由的参数是结构体类型,而大多数路由的错误是联合体类型。

注意:在此上下文中,“结构体”和“联合体”是特定于Dropbox API的术语,而非任何用于查询API的语言,因此您应避免根据其在Swift中的定义来考虑它们。

结构体类型是“传统”对象类型,即由一个或多个实例字段组成的复合类型。所有公共实例字段在运行时都可用,不受运行时状态的影响。

联合体类型表示可以采用多个值类型的单个值,具体取决于状态。我们把这些不同的类型场景都包含在一个“联合对象”中,但该对象在运行时只存在一个类型。每个联合体状态类型,或称为标签,可能有一个相关值(如果没有,则称联合体状态类型为无值)。相关值类型可以是原始类型、结构体或联合体。尽管Swift SDK将联合体类型表示为具有多个实例字段的对象,但在运行时,只有最多一个实例字段是可访问的,具体取决于标签状态。

例如,/delete 端点返回一个错误,Files.DeleteError,它是一个联合体类型。该 Files.DeleteError 联合体可以采取两种不同的标签状态:path_lookup(如果存在查找路径的问题)或 path_write(如果存在写入路径的问题,在这种情况下删除)。在这里,两种标签状态都具有非无值的相关值(分别为 Files.LookupErrorFiles.WriteError 类型)。

这种方法可以使一个联合对象捕获多种场景,每种场景都有自己的值类型。

为了正确处理联合类型,你应该将每个联合对象通过switch语句传递,并检查与联合相关联的每个可能的标签状态。一旦确定联合当前的标签状态,就可以访问与该标签状态关联的值(前提是存在关联的值类型,即它不是void)。


路由特定错误

client.files.deleteV2(path: "/test/path/in/Dropbox/account").response { response, error in
    if let response = response {
        print(response)
    } else if let error = error {
        switch error as CallError {
        case .routeError(let boxed, let userMessage, let errorSummary, let requestId):
            print("RouteError[\(requestId)]:")

            switch boxed.unboxed as Files.DeleteError {
            case .pathLookup(let lookupError):
                switch lookupError {
                case .notFound:
                    print("There is nothing at the given path.")
                case .notFile:
                    print("We were expecting a file, but the given path refers to something that isn't a file.")
                case .notFolder:
                    print("We were expecting a folder, but the given path refers to something that isn't a folder.")
                case .restrictedContent:
                    print("The file cannot be transferred because the content is restricted...")
                case .malformedPath(let malformedPath):
                    print("Malformed path: \(malformedPath)")
                case .other:
                    print("Unknown")
                }
            case .pathWrite(let writeError):
                print("WriteError: \(writeError)")
                // you can handle each `WriteError` case like the `DeleteError` cases above
            case .tooManyWriteOperations:
                print("Another write operation occurring at the same time prevented this from succeeding.")
            case .tooManyFiles:
                print("There are too many files to delete.")
            case .other:
                print("Unknown")
            }
        case .internalServerError(let code, let message, let requestId):
            ....
            ....
            // a not route-specific error occurred
        ....
        ....
        ....
        }
    }
}

通用网络请求错误

在网络错误的情况下,错误可能是针对端点的(如上所示)或更通用的错误。

为了确定一个错误是否是路由特定错误,应该将错误对象转换为CallError,并根据错误的类型,在适当的switch语句中处理。

client.files.deleteV2(path: "/test/path/in/Dropbox/account").response { response, error in
    if let response = response {
        print(response)
    } else if let error = error {
        switch error as CallError {
        case .routeError(let boxed, let userMessage, let errorSummary, let requestId):
            // a route-specific error occurred
            // see handling above
            ....
            ....
            ....
        case .internalServerError(let code, let message, let requestId):
            print("InternalServerError[\(requestId)]: \(code): \(message)")
        case .badInputError(let message, let requestId):
            print("BadInputError[\(requestId)]: \(message)")
        case .authError(let authError, let userMessage, let errorSummary, let requestId):
            print("AuthError[\(requestId)]: \(userMessage) \(errorSummary) \(authError)")
        case .accessError(let accessError, let userMessage, let errorSummary, let requestId):
            print("AccessError[\(requestId)]: \(userMessage) \(errorSummary) \(accessError)")
        case .rateLimitError(let rateLimitError, let userMessage, let errorSummary, let requestId):
            print("RateLimitError[\(requestId)]: \(userMessage) \(errorSummary) \(rateLimitError)")
        case .httpError(let code, let message, let requestId):
            print("HTTPError[\(requestId)]: \(code): \(message)")
        case .clientError(let error):
            print("ClientError: \(error)")
        }
    }
}

响应处理边缘情况

一些路由以联合类型作为结果类型返回,因此您应该准备好以处理联合路由错误相同的方式处理这些结果。请查阅您使用的每个端点的文档,以确保您正确地处理路由的响应类型。

一些路由返回的结果类型是具有子类型的datatypes,即可以采取多个状态类型(如联合)的结构体。

例如,/delete端点返回了一个通用的Metadata类型,它可以是FileMetadata结构体、FolderMetadata结构体或DeletedMetadata结构体。为了在运行时确定Metadata类型存在的子类型,请将对象通过switch语句传递,并检查每个可能的类,并将结果相应地转换。以下是如何操作的示例

client.files.deleteV2(path: "/test/path/in/Dropbox/account").response { response, error in
    if let response = response {
        switch response {
        case let fileMetadata as Files.FileMetadata:
            print("File metadata: \(fileMetadata)")
        case let folderMetadata as Files.FolderMetadata:
            print("Folder metadata: \(folderMetadata)")
        case let deletedMetadata as Files.DeletedMetadata:
            print("Deleted entity's metadata: \(deletedMetadata)")
        }
    } else if let error = error {
        switch error as CallError {
        case .routeError(let boxed, let userMessage, let errorSummary, let requestId):
            // a route-specific error occurred
            // see handling above
        case .internalServerError(let code, let message, let requestId):
            ....
            ....
            // a not route-specific error occurred
            // see handling above
        ....
        ....
        ....
        }
    }
}

这种Metadata对象在我们的API v2文档中被称为< strong>具有子类型的datatypes

具有子型的数据类型是结合结构体和联合体的一种方式。具有子型的数据类型是包含一个标记的结构体对象,该标记在运行时指定对象存在的是哪种子型。我们拥有这种结构,就像联合体一样,是为了能够使用一个对象捕捉到多种情况。

在上面的例子中,Metadata 类型可以是 FileMetadataFolderMetadataDeleteMetadata。这些类型都包含常见实例字段,如 "name"(文件、文件夹或删除类型的名称),同时也包含特定于特定子型的实例字段。为了利用继承,我们设置了一个共同的超类型 Metadata,它捕获所有常见实例字段,同时还有一个标记实例字段,该字段指定对象当前存在的是哪种子型。

通过这种方式,具有子型的数据类型是结构体和联合体的混合体。只有少数几种方式会返回这种类型的结构。


定制网络调用的

配置网络客户端

可以为 SDK 使用的网络客户端进行配置,以便进行 API 请求。您可以提供自定义字段,例如自定义用户代理或自定义会话配置,或自定义身份验证挑战处理器。以下内容将进行说明:

iOS
import SwiftyDropbox

let transportClient = DropboxTransportClientImpl(accessToken: "<MY_ACCESS_TOKEN>",
                                             baseHosts: nil,
                                             userAgent: "CustomUserAgent",
                                             selectUser: nil,
                                             sessionConfiguration: mySessionConfiguration,
                                             longpollSessionConfiguration: myLongpollSessionConfiguration,
                                             authChallengeHandler: nil)

DropboxClientsManager.setupWithAppKey("<APP_KEY>", transportClient: transportClient)
macOS
import SwiftyDropbox

let transportClient = DropboxTransportClientImpl(accessToken: "<MY_ACCESS_TOKEN>",
                                             baseHosts: nil,
                                             userAgent: "CustomUserAgent",
                                             selectUser: nil,
                                             sessionConfiguration: mySessionConfiguration,
                                             longpollSessionConfiguration: myLongpollSessionConfiguration,
                                             authChallengeHandler: nil)

DropboxClientsManager.setupWithAppKeyDesktop("<APP_KEY>", transportClient: transportClient)

指定API调用响应队列

默认情况下,响应/进度处理代码在主线程上运行。您可以通过调用response方法为每个API调用设置一个自定义响应队列,如果您希望在另一个线程上运行响应/进度处理代码。

let client = DropboxClientsManager.authorizedClient!

client.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in
    if let result = response {
        print(Thread.current)  // Output: <NSThread: 0x61000007bec0>{number = 4, name = (null)}
        print(Thread.main)     // Output: <NSThread: 0x608000070100>{number = 1, name = (null)}
        print(result)
    }
}

在测试中模拟API响应

在测试依赖SDK的代码时,可以使用JSON固定文件模拟任意的API响应,我们建议使用依赖注入而不是通过单例变量访问客户端。注意,模拟结果不是公开的,只有在使用具有@testable属性的SwiftyDropbox进行测试时才可用。

@testable import SwiftyDropbox

let transportClient = MockDropboxTransportClient()
let dropboxClient = DropboxClient(transportClient: transportClient)

// your feature under test
let commentClient = CommentClient(apiClient: dropboxClient)

let expectation = expectation(description: "added comment")

// function of your feature that relies upon Dropbox api response
commentClient.addComment(
    forIdentifier: identifier,
    commentId: "pendingCommentId",
    threadId: nil,
    message: "hello world",
    mentions: [],
    annotation: nil
) { result in
    XCTAssertEqual(result.commentId, "thread-1")
    XCTAssertNil(result.error)
    addCommentExpectation.fulfill()
}

let mockInput: MockInput = .success(
    json: ["id": "thread-1", "status": 1]
)

let request = try XCTUnwrap(transportClient.getLastRequest())
try request.handleMockInput(mockInput)

wait(for: [expectation], timeout: 1.0)

支持后台网络

10.0及以上版本支持iOS应用程序及其扩展的后台网络。

初始化

要创建后台客户端,请提供后台会话标识符。要使用共享容器,也请指定这一点。

import SwiftyDropbox

DropboxClientsManager.setupWithAppKey(
    "<APP_KEY>",
    backgroundSessionIdentifier: "<BACKGROUND_SESSION_IDENTIFIER>"
)

如果您是在应用扩展中设置后台客户端,您需要指定共享容器标识符,并适当地配置应用组或密钥链共享。

DropboxClientsManager.setupWithAppKey(
    "<APP_KEY>",
    backgroundSessionIdentifier: "<BACKGROUND_SESSION_IDENTIFIER>"
    sharedContainerIdentifier: "<SHARED_CONTAINER_IDENTIFIER>"
)

处于同一应用组的应用程序自动具有密钥链共享。使用共享容器需要应用组,这对于要在扩展中使用后台会话下载文件的应用程序是必要的。请参阅

发送请求

像使用前台客户端一样发送请求。

let client = DropboxClientsManager.authorizedBackgroundClient!

client.files.download(path: path, overwrite: true, destination: destinationUrl) {
    if let result = response {
        print(result)
    }
}

定制请求

在请求上设置与后台会话相关的属性,以便进行细粒度控制。

client.files.download(path: path, overwrite: true, destination: destinationUrl)
    .persistingString(string: "<DATA_AVAILABLE_ACROSS_APP_SESSIONS>")
    .settingEarliestBeginDate(date: .addingTimeInterval(fiveSeconds))
    .response { response, error in
{
    if let result = response {
        print(result)
    }
})

在应用程序会话之间重新连接请求

由于后台请求可能跨越应用程序会话,当唤醒以处理事件时,应用将接收到 AppDelegate.application(_:handleEventsForBackgroundURLSession:completionHandler:)。SwiftyDropbox 可以将完成处理程序重新连接到请求。在尝试重新连接之前,必须设置 authorizedBackgroundClient

在重连块中,您正在接收背景会话中请求的路由的异构集合。处理响应可能需要跨会话持续存在的上下文。您可以在请求中使用 .persistingString(string:).clientPersistedString 来存储这个上下文。根据您的使用情况,您可能需要在您的应用程序中持久化额外的上下文。

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        DropboxClientsManager.handleEventsForBackgroundURLSession(
            with: identifier,
            creationInfos: [],
            completionHandler: completionHandler,
            requestsToReconnect: { requestResults in
                processReconnect(requestResults: RequestResults)
            }
        )
    }

    static func processReconnect(requestResults: ([Result<DropboxBaseRequestBox, ReconnectionError>])) {
        let successfulReturnedRequests = requestResults.compactMap { result -> DropboxBaseRequestBox? in
            switch result {
            case .success(let requestBox):
                return requestBox
            case .failure(let error):
                // handle error
                return nil
            }
        }

        for request in successfulReturnedRequests {
            switch request {
            case .download(let downloadRequest):
                downloadRequest.response { response, error in
                    // handle response
                }
            case .upload(let uploadRequest):
                uploadRequest.response { response, error in
                    // handle response
                }
            // or .downloadZip, .paperCreate, .getSharedLinkFile etc.
            }
        }
    }

如果请求源自应用程序扩展,SwiftyDropbox 必须重新创建扩展背景客户端以重新连接请求。

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        let extensionCreationInfo: BackgroundExtensionSessionCreationInfo = .init(defaultInfo: .init(
            backgroundSessionIdentifier: "<EXTENSION_BACKGROUND_SESSION_IDENTIFIER>",
            sharedContainerIdentifier: "<EXTENSION_SHARED_CONTAINER_IDENTIFIER>"
        ))

        DropboxClientsManager.handleEventsForBackgroundURLSession(
            with: identifier,
            creationInfos: [extensionCreationInfo],
            completionHandler: completionHandler,
            requestsToReconnect: { requestResults in
                processReconnect(requestResults: RequestResults)
            }
        )
    }

【调试】

背景会话难以调试。模拟器和 Xcode 调试器会导致应用程序以有意义不同的方式运行—一,正确的重新连接行为应该在未连接调试器的物理设备上始终进行验证。日志可通过 Xcode -> 窗口 -> 设备和模拟器查看,这对于在此处获得见解是一个好方法。强制退出应用程序将使其无法继续在后台执行工作。

关于这一点的良好讨论,请见 https://developer.apple.com/forums/thread/14855

【持久化】

SwiftyDropbox 通过将 JSON 字符串写入 URLSessionTask.taskDescription 属性来跟踪状态并编排重新连接,该属性是 URLSession 实例在应用会话之间持续保持的。

在上述自定义请求中,显示了 SwiftyDropbox 的客户端保留其自身任意字符串的能力,同时还展示了 SwiftyDropbox 对此字段的私人使用。这可以用于存储需要在重新连接后重建任务完成处理器的信息。

根据应用程序的需求,这种轻量级解决方案可能不足以进行持久化记录,应用程序可能会构建独立的私有持久化映射,将状态映射到请求上存储的 id。这可能有助于在发生失败的情况下,URLSession 自身已丢失传输跟踪并需要重新创建时。

关于进一步讨论,请见 https://developer.apple.com/forums/thread/11554


DropboxClientsManager

Swift SDK 包含了一个方便的类,DropboxClientsManager,将 SDK 的不同功能集成到一个类中。

单个 Dropbox 用户用例

对于大多数应用,可以合理假设在一段时间内只需要管理一个 Dropbox 账户(和访问令牌)。在这种情况下,DropboxClientsManager 流程如下:

  • 在集成应用的 app 代理中调用 setupWithAppKey/setupWithAppKeyDesktop(或 setupWithTeamAppKey/setupWithTeamAppKeyDesktop)进行集成。
  • 客户端管理器确定是否存储了任何访问令牌——如果存在,任选一个令牌进行使用。
  • 如果没有找到令牌,调用 authorizeFromController/authorizeFromControllerDesktop 来启动 OAuth 流程。
  • 如果启动了认证流程,在集成应用的 app 代理中调用 handleRedirectURL(或 handleRedirectURLTeam)来处理认证重定向回应用并存储检索到的访问令牌(使用 DropboxOAuthManager 实例)。
  • 客户端管理器实例化一个 DropboxTransportClient(如果用户未提供)。
  • 客户端管理器使用传输客户端作为字段实例化 DropboxClient(或 DropboxTeamClient)。

然后使用 DropboxClient(或 DropboxTeamClient)来执行所有所需的 API 调用。

  • DropboxClientsManager 上调用 unlinkClients 登出 Dropbox 用户并清除所有访问令牌。

多个 Dropbox 用户用例

对于一些应用,有必要同时管理多个 Dropbox 账户(和访问令牌)。在这种情况下,DropboxClientsManager 流程如下:

  • 访问令牌 uids 由与 SDK 集成的应用管理以供以后查找。
  • 在集成应用的 app 代理中调用 setupWithAppKeyMultiUser/setupWithAppKeyMultiUserDesktop(或 setupWithTeamAppKeyMultiUser/setupWithTeamAppKeyMultiUserDesktop)。
    • SwiftUI注意:如果您的应用程序没有应用程序代理,您可能需要创建一个。
  • 客户端管理器确定是否以 tokenUid 作为键存储了访问令牌——如果存在,则选择此令牌进行使用。
  • 如果没有找到令牌,调用 authorizeFromController/authorizeFromControllerDesktop 来启动 OAuth 流程。
  • 如果启动了认证流程,在集成应用的 app 代理中调用 handleRedirectURL(或 handleRedirectURLTeam)来处理认证重定向回应用并存储检索到的访问令牌(使用 DropboxOAuthManager 实例)。
    • SwiftUI注意:如果您的应用程序没有应用程序代理,您可能需要创建一个。
  • 在这个点上,与SDK集成的应用程序应该持续保存从《handleRedirectURL (或 handleRedirectURLTeam)方法返回的《DropboxOAuthResult》对象中的《DropboxAccessToken》字段中的 tokenUid
  • tokenUid》可以用来在应用程序的生命周期中半途重新授权给新用户via《reauthorizeClient (或 reauthorizeTeamClient

    或当应用程序首次启动时via《setupWithAppKeyMultiUser/setupWithAppKeyMultiUserDesktop (或 setupWithTeamAppKeyMultiUser/setupWithTeamAppKeyMultiUserDesktop)。
  • 客户端管理器实例化一个 DropboxTransportClient(如果用户未提供)。
  • 客户端管理器使用传输客户端作为字段实例化 DropboxClient(或 DropboxTeamClient)。

然后使用 DropboxClient(或 DropboxTeamClient)来执行所有所需的 API 调用。

  • 调用《DropboxClientsManager》的《resetClients》以注销Dropbox用户但不清除任何访问令牌
  • 如果需要删除特定的访问令牌,则在《DropboxOAuthManager》中使用《clearStoredAccessToken》方法

Objective-C

如果你需要在Objective-C代码中与Dropbox SDK互动,SDK有一个Objective-C兼容层可以使用。

Objective-C兼容层分发

Swift包管理器

Objective-C兼容层与《SDK分发》中的同一个包相同。添加包后,您将看到一个名为《SwiftyDropboxObjC》的目标,可以像Swift SDK一样添加并替代使用。

CocoaPods

对于CocoaPods,只需在Podfile中指定SwiftyDropboxObjC即可(或者替换或与其一起指定)SwiftyDropbox

use_frameworks!

target '<YOUR_PROJECT_NAME>' do
    pod 'SwiftyDropboxObjC', '~> 10.0.0'
end

使用Objective-C兼容层

Objective-C接口是为了尽可能地模拟Swift接口,同时仍然保持良好的Objective-C模式和最佳实践(例如,使用较长的verbose名称而不是Swift依赖的命名空间)。除了命名和一些其他小的调整以适应Objective-C之外,SDK的使用在两种语言中应该非常相似,因此上述说明同样适用于Objective-C。

Swift和Objective-C之间的差异示例

Swift

import SwiftyDropbox

let userSelectArg = Team.UserSelectorArg.email("[email protected]")
DropboxClientsManager.team.membersGetInfo(members: [userSelectArg]).response { response, error in
    if let result = response {
        // Handle result
    } else {
        // Handle error
    }
}

Objective-C

@import SwiftyDropboxObjC;

DBXTeamUserSelectorArgEmail *selector = [[DBXTeamUserSelectorArgEmail alloc] init:@"[email protected]"]
[[DBXDropboxClientsManager.authorizedTeamClient.team membersGetInfoWithMembers:@[selector]] responseWithCompletionHandler:^(NSArray<DBXTeamMembersGetInfoItem *> * _Nullable result, DBXTeamMembersGetInfoError * _Nullable routeError, DBXCallError * _Nullable error) {
    if (result) {
        // Handle result
    } else {
        // Handle error
    }
}];

迁移自dropbox-sdk-obj-c

如果您之前使用过dropbox-sdk-obj-c,迁移到Swift SDK + Objective-C层将需要一些代码更改,但在大多数情况下应该是相对直接的。

为了在此SDK中尽可能保持Swift和Objective-C接口的一致性,接口不得不在dropbox-sdk-obj-c中进行一些细微的调整。主要差异如下

1.) 类型名称是从SwiftyDropbox类型派生的,前面带有DBX前缀。命名上通常与dropbox-sdk-obj-c有一些差异,并且由于Swift更精细的访问控制,一些以前可访问的类型现在仅限于SDK内部。请参阅常见类型迁移参考

2.) 一些函数名称有所变化,以提供更多关于参数的描述,或更好地匹配Swift接口。在以下示例中,请注意createFolderV2createFolderV2WithPath以及responseWithCompletionHandlersetResponseBlock之间的差异。

dropbox-sdk-obj-c

[[[DBClientsManager authorizedClient].filesRoutes createFolderV2:@"/some/folder/path"]
  setResponseBlock:^(DBFILESCreateFolderResult * _Nullable result, DBFILESCreateFolderError * _Nullable routeError, DBRequestError * _Nullable networkError) {
    // Handle response
}];

SwiftyDropboxObjC

[[DBXDropboxClientsManager.authorizedClient.files createFolderV2WithPath:@"some/folder/path"] responseWithCompletionHandler:^(DBXFilesCreateFolderResult * _Nullable result, DBXFilesCreateFolderError * _Nullable routeError, DBXCallError * _Nullable error) {
    // Handle response
}];

3.) 许多类的首字母大写形式已更改。

dropbox-sdk-obj-c: DBUSERSBasicAccount

SwiftyDropboxObjC: DBXUsersBasicAccount

4.) 传递给服务器或从服务器返回的枚举的表示现在明确地在类名称中类型化

dropbox-sdk-obj-c

DBTEAMUserSelectorArg *userSelectArg = [[DBTEAMUserSelectorArg alloc] initWithEmail:@"[email protected]"];

SwiftyDropboxObjC

DBXTeamUserSelectorArgEmail *userSelectArg = [[DBXTeamUserSelectorArgEmail alloc] init:@"[email protected]"];

5.) 当处理任务时,您不再需要手动start任务。它们在创建时会自动启动。

6.) SwiftyDropbox依赖于泛型在Requests上的类型化完成处理器。这无法转换为Objective-C。相反,对于每个路由,都有一个额外的Request类型,带有正确类型化的完成处理器。例如,DownloadRequestFile<Files.FileMetadataSerializer, Files.DownloadErrorSerializer>在Objective-C中表示为DBXFilesDownloadDownloadRequestFile

通用类型迁移参考

dropbox-sdk-objc-c SwiftyDropbox SwiftyDropboxObjC
DBAppClient DropboxAppClient DBXDropboxAppBase
DBClientsManager DropboxClientsManager DBXDropboxClientsManager
DBTeamClient DropboxTeamClient DBXDropboxTeamClient
DBUserClient DropboxClient DBXDropboxClient
DBRequestErrors CallError DBXCallError
DBRpcTask RpcRequest DBXRpcRequest
DBUploadTask UploadRequest DBXUploadRequest
DBDownloadUrlTask DownloadRequestFile DBXDownloadRequestFile
DBDownloadDataTask DownloadRequestMemory DBXDownloadRequestMemory
DBTransportBaseClient/DBTransportDefaultClient DropboxTransportClientImpl DBXDropboxTransportClient
DBTransportBaseHostnameConfig BaseHosts DBXBaseHosts
DBAccessTokenProvider AccessTokenProvider DBXAccessTokenProvider
DBLongLivedAccessTokenProvider LongLivedAccessTokenProvider DBXLongLivedAccessTokenProvider
DBShortLivedAccessTokenProvider ShortLivedAccessTokenProvider DBXShortLivedAccessTokenProvider
DBLoadingStatusDelegate LoadingStatusDelegate DBXLoadingStatusDelegate
DBOAuthManager DropboxOAuthManager DBXDropboxOAuthManager
DBAccessToken DropboxAccessToken DBXDropboxAccessToken
DBAccessTokenRefreshing AccessTokenRefreshing DBXAccessTokenRefreshing
DBOAuthResult DropboxOAuthResult DBXDropboxOAuthResult
DBOAuthResultCompletion DropboxOAuthCompletion (DBXDropboxOAuthResult?) -> Void
DBScopeRequest ScopeRequest DBXScopeRequest
DBSDKKeychain SecureStorageAccess DBXSecureStorageAccess / DBXSecureStorageAccessDefaultImpl
DBDelegate n/a n/a
DBGlobalErrorResponseHandler n/a n/a
DBSDKReachability n/a n/a
DBSessionData n/a n/a
DBTransportDefaultConfig n/a n/a
DBURLSessionTaskResponseBlockWrapper n/a n/a
DBURLSessionTaskWithTokenRefresh n/a n/a
DBOAuthPKCESession n/a n/a
DBOAuthTokenRequest n/a n/a

版本 10.0.0 的变更

SwiftyDropbox 版本 10.0.0 与版本 9.2.0 相比有显著差异。它旨在支持 Objective-C,移除 AlamoFire 依赖,支持后台网络,替换序列化过程中的致命错误,添加单元测试,并更好地支持测试。

这些新增功能是最大的差异,即使是那些没有利用这些新功能的简单升级也应考虑其他显著的变更。

  • 文件下载的目标必须在调用时指定。现在不再可能提供在请求完成后评估的闭包。

  • 通过AlamoFire提供的旧版SSL证书固定API不再可用。此版本公开了URLSession身份验证挑战API。此外,上一个版本SDK中的可选SessionDelegate已被移除,没有直接替换。如果您的流程依赖于这些特定功能,而且不再可实现,请通知我们,以便我们更好地理解和解决任何潜在问题。

  • 以前导致致命错误的序列化不一致现在表示为通过请求完成处理程序管道的错误。如何处理它们取决于调用应用程序。

  • 不再支持Carthage,请使用Swift包管理器或CocoaPods。

  • SDK类不能再被继承。如果这影响到您的使用,请让我们知道

  • 由于重写范围广泛以及在SDK新版本中引入了新功能,当过渡到SDK新版本时,对您的代码库进行全面测试非常重要。新版本SDK中的重大变化和增强可能引入了新版本SDK中以前版本中没有的微妙行为变化或边缘情况。

新功能

有关对Objective-C支持的说明,请参阅从dropbox-sdk-obj-c迁移

SDK的后台网络支持简化了将完成处理程序重新连接到URLSession任务的重新连接。请参阅TestSwiftyDropbox/DebugBackgroundSessionViewModel中的代码,以了解各种后台网络场景的练习。请参阅TestSwiftDropbox/ActionRequestHandler以了解应用程序扩展的用法。

测试支持

使用MockDropboxTransportClient初始化一个DropboxClient,以在测试中便于路由响应模拟。将此客户端提供给您的测试代码,然后练习代码,然后如以下所示将响应管道输送,并断言您的代码的行为。

let transportClient = MockDropboxTransportClient()

let client = DropboxClient(
    transportClient: transportClient
)

let feature = SystemUnderTest(client: client)

let json: [String: Any] = ["fileNames": ["first"]]

let request = transportClient.getLastRequest()

request.handleMockInput(.success(json: json))

XCTAssert(<state of feature>, <expected state>)


示例

  • PhotoWatch - 查看您的Dropbox中的照片。支持Apple Watch。

文档


Stone

我们所有的路由和数据类型都是使用名为 Stone 的框架自动生成的。

stone 仓库包含所有针对Swift的生成逻辑,而spec仓库包含语言中立的API端点规范,这些规范作为针对特定语言生成器的输入。


修改

如果您有兴趣修改SDK代码库,请执行以下步骤

  • 将此GitHub仓库克隆到您的本地文件系统
  • 运行git submodule init然后运行git submodule update
  • 导航到TestSwifty_[iOS|macOS]
  • 检查安装的CocoaPods版本(通过pod --version)与TestSwifty_[iOS|macOS]/Podfile.lock中的"locked"版本相同
  • 运行pod install
  • 在Xcode中打开TestSwifty_[iOS|macOS]/TestSwifty_[iOS|macOS].xcworkspace
  • 实施对SDK源代码的更改。

为确保您的更改没有破坏现有功能,您可以运行一系列集成测试

  • https://www.dropbox.com/developers/apps/ 上创建一个新应用,具有 "Full Dropbox" 访问权限。注意应用密钥
  • 打开 Info.plist 文件,并配置“URL types > 项0 (编辑器) > URL Schemes > 项0”键为 db-"App密钥"
  • 在 AppDelegate.swift 文件中将 “FULL_DROPBOX_APP_KEY” 替换为 App密钥
  • 在您的设备上运行测试应用程序并按照屏幕上的说明操作

要运行和开发单元测试而不是集成测试,请在 Xcode 中打开克隆仓库的根目录。开发完成后请运行集成测试。


App Store Connect 隐私标签

为了帮助使用 Dropbox SDK 的开发者填写 Apple 的隐私实践调查问卷,我们提供了以下 Dropbox 可能收集和使用的数据信息。

在您完成问卷时,请注意以下信息是通用的。Dropbox SDK 是设计为由开发者配置的,以便将 Dropbox 功能融入他们的应用程序中。因此,由于 Dropbox SDK 的可定制性,我们无法提供每个应用程序实际数据收集和使用的具体信息。我们建议开发者参考 Dropbox 为 HTTP 开发者提供的具体信息,了解每个 Dropbox API 如何收集数据。

此外,请注意以下信息仅标识 Dropbox 收集和使用数据的情况。您负责确定自己的应用程序中收集和使用的数据,这可能导致与以下所述不同的问卷答案。

数据 由 Dropbox 收集 数据使用 与用户关联的数据 跟踪
联系信息
   • 姓名 未收集 不适用 不适用 不适用
   • 电子邮件地址 可能收集
(如果启用通过电子邮件地址进行身份验证)
•应用程序功能
健康与健身 未收集 不适用 不适用 不适用
财务信息 未收集 不适用 不适用 不适用
位置 未收集 不适用 不适用 不适用
敏感信息 未收集 不适用 不适用 不适用
联系人 未收集 不适用 不适用 不适用
用户内容
   • 音频数据 可能收集 •应用程序功能
   • 图片或视频 可能收集 •应用程序功能
   • 其他用户内容 可能收集 •应用程序功能
浏览历史记录 未收集 不适用 不适用 不适用
搜索历史记录
   • 搜索历史记录 可能收集
(如果使用搜索功能)
•应用程序功能
• 分析
标识符
   • 用户 ID 收集 •应用程序功能
• 分析
购买 未收集 不适用 不适用 不适用
使用数据
   • 产品交互 收集 •应用程序功能
• 分析
• 产品个性化
诊断
   • 其他诊断数据 收集
(API 调用日志)
•应用程序功能
其他数据 不适用 不适用 不适用 不适用

错误

请将任何错误发布到项目 GitHub 页面上的 issue tracker。(错误跟踪器)

请包括以下内容与您的问题

  • 不正常工作情况描述
  • 示例代码以帮助复制问题

谢谢!