SwiftlySalesforce 7.1.6

SwiftlySalesforce 7.1.6

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布上次发布2019 年 1 月
SPM支持 SPM

Mike EpsteinMichael Epstein 维护。



    

使用 Swiftly Salesforce 在 Salesforce 平台上快速构建 iOS 应用程序

  • 完全使用 Swift 编写。
  • 使用 promises 简化复杂的异步 Salesforce API 交互。
  • 自动管理 Salesforce OAuth2 用户身份验证和授权过程(“OAuth 舞蹈”)。
  • 比 Salesforce iOS 移动 SDK 简单、轻量级的替代方案。
  • 易于安装和更新。
  • 兼容 Realm,提供完整的离线移动解决方案。
  • 查看最新内容.

快速开始

按照以下步骤,您只需几分钟即可开始使用

  1. 获取免费的Salesforce开发者版
  2. 在您的全新开发者版中创建一个Salesforce 连接应用
  3. 将Swiftly Salesforce添加到您的Xcode项目中
  4. 配置您的应用代理(示例

最小要求

  • iOS 11.3
  • Swift 4
  • Xcode 9

文档

文档在此
特别关注 Salesforce 类的公共方法 - 这些可能是您从代码中需要调用的所有内容。

示例

以下是一些示例,说明了如何使用Swiftly Salesforce,以及如何串联复杂异步调用。您也可以在这里找到完整的示例应用程序 示例;它从Salesforce检索登录用户的任务记录,并允许用户更新任务的状态。

Swiftly Salesforce会自动管理整个Salesforce OAuth2进程(“OAuth舞蹈”)。如果Swiftly Salesforce有一个有效的访问令牌,它将在每个API请求的头部包含该令牌。如果令牌已过期,且Salesforce拒绝请求,那么Swiftly Salesforce将尝试刷新访问令牌,而无需打扰用户重新输入用户名和密码。如果Swiftly Salesforce没有有效的访问令牌,或无法刷新它,那么Swiftly Salesforce将引导用户到Salesforce提供的登录表单。

在幕后,Swiftly Salesforce使用了PromiseKit,这是一个用于优雅处理异步操作非常广泛采用的框架。

示例:配置您的应用代理

import UIKit
import SwiftlySalesforce

// Global Salesforce variable - in your real-world app
// you could 'inject' it into view controllers instead
var salesforce: Salesforce!

@UIApplicationMain
class AppDelegate: UIApplicationDelegate {

    let consumerKey = "YOUR CONNECTED APP'S CONSUMER KEY HERE"
    let callbackURL = URL(string: "YOUR CONNECTED APP'S CALLBACK URL HERE")!

    var window: UIWindow?
	
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        salesforce = try! Salesforce(consumerKey: consumerKey, callbackURL: callbackURL)
        return true
    }
}

在上面的示例中,我们使用连接应用的消费者密钥和回调URL创建了一个 Salesforce 实例。 salesforce 是一个可选的全局变量,但没有展开,你也可以将 Salesforce 实例注入到根视图中,例如,而不是使用全局变量。

示例:检索Salesforce记录

以下示例将检索账户记录的所有字段

salesforce.retrieve(type: "Account", id: "0013000001FjCcF")

为了指定应检索哪些字段

let fields = ["AccountNumber", "BillingCity", "MyCustomField__c"]
salesforce.retrieve(type: "Account", id: "0013000001FjCcF", fields: fields)

请注意,retrieve 是一个异步函数,其返回值是一个“promise”,将最终在未来的某个时刻得到满足。

let promise: Promise<SObject> = salesforce.retrieve(type: "Account", id: "0013000001FjCcF")

您可以在稍后当 promise 被满足时添加一个闭包

salesforce.retrieve(type: "Account", id: "0013000001FjCcF").done { (queryResult: QueryResult<SObject>) -> () in
    for record: SObject in queryResult.records {
        // Do something more interesting with each record
        debugPrint(record.type)
    }
}.catch { (error: Error) in
    // Do something with the error
}

您可以在并行检索多个记录后等待所有这些记录,然后再继续

first {
    // (Enclosing this in a ‘first’ block is optional; it keeps things neat.)
    let ids = ["001i0000020i19F", "001i0000034i18A", "001i0000020i22B"]
    return salesforce.retrieve(type: "Account", ids: ids)
}.done { (records: [Record]) -> () in
    for record in records {
        if let name = record.string(forField: "Name"), let modifiedDate = record.date(forField: "LastModifiedDate") {
            debugPrint(name)
            debugPrint(modifiedDate)
        }
    }
}.catch { error in
    // Handle error...
}

示例:自定义模型对象

您可以使用通用 SObject 而不是使用自定义模型对象。Swiftly Salesforce 将自动将 Salesforce 响应解码到您的模型对象中,前提是它们实现了 Swift 的 Decodable 协议。

struct MyAccountModel: Decodable {

    var id: String
    var name: String
    var createdDate: Date
    var billingAddress: Address?
    var website: URL?

    enum CodingKeys: String, CodingKey {
        case id = "Id"
        case name = "Name"
        case createdDate = "CreatedDate"
        case billingAddress = "BillingAddress"
        case website = "Website"
    }
}

//...
first {
    // (Enclosing this in a ‘first’ block is optional; it keeps things neat.)
    let ids = ["001i0000020i19F", "001i0000034i18A", "001i0000020i22B"]
    return salesforce.retrieve(type: "Account", ids: ids)
}.done { (records: [MyAccountModel]) -> () in
    for record in records {
        // Do something more interesting with record data
        let id = record.id
        let name = record.name
        let createdDate = record.createdDate
        let billingAddress = record.billingAddress
        let website = record.website
    }
}.catch { error in
    // Handle error...
}

示例:更新Salesforce记录

salesforce.update(type: "Task", id: "00T1500001h3V5NEAU", fields: ["Status": "Completed"]).done { (_) -> () in
    // Update the local model
}.finally {
    // Update the UI
}

finally 闭包将在 promise 链中的成功或失败在任何其他地方被调用。

您也可以使用 SObject 类型来更新 Salesforce 中的记录。例如

// `account` is an SObject we retrieved earlier...
account.setValue("My New Corp.", forField: "Name")
account.setValue(URL(string: "https://www.mynewcorp.com")!, forField: "Website")
account.setValue("123 Main St.", forField: "BillingStreet")
account.setValue(nil, forField: "Sic")
salesforce.update(record: account).done {
    print("Account updated...")
}.catch {
    error in
    // Handle error
}

示例:查询Salesforce

let soql = "SELECT Id,Name FROM Account WHERE BillingPostalCode = '10024'"
salesforce.query(soql: soql).done { (queryResult: QueryResult) -> () in
    for record in queryResult.records {
        // Do something more interesting with each record
        if let name = record.string(forField: "Name") {
            print("Account name: \(name)")
        }
    }
}.catch { error in
    // Handle the error
}

您还可以一次执行多个查询,并等待它们全部完成后再继续

first {
    let queries = ["SELECT Name FROM Account", "SELECT Id FROM Contact", "Select Owner.Name FROM Lead"]
    return salesforce.query(soql: queries)
}.done { (queryResults: [QueryResult<SObject>]) -> () in
    // Results are in the same order as the queries
}.catch { error in
    // Handle the error
}

示例:将查询结果解码为自定义模型对象

您可以轻松执行复杂的查询,遍历对象关系,并将所有结果自动解码为您的自定义模型对象,该模型对象实现了 Decodable 协议

struct Account: Decodable {

    var id: String
    var name: String
    var lastModifiedDate: Date

    enum CodingKeys: String, CodingKey {
        case id = "Id"
        case name = "Name"
        case lastModifiedDate = "LastModifiedDate"
    }
}

struct Contact: Decodable {

    var id: String
    var firstName: String
    var lastName: String
    var createdDate: Date
    var account: Account?

    enum CodingKeys: String, CodingKey {
        case id = "Id"
        case firstName = "FirstName"
        case lastName = "LastName"
        case createdDate = "CreatedDate"
        case account = "Account"
    }
}

func getContactsWithAccounts() -> () {
    let soql = "SELECT Id, FirstName, LastName, CreatedDate, Account.Id, Account.Name, Account.LastModifiedDate FROM Contact"
    salesforce.query(soql: soql).done { (queryResult: QueryResult<Contact>) -> () in
        for contact in queryResult.records {
            // Do something more interesting with each Contact record
            debugPrint(contact.lastName)
            if let account = contact.account {
                // Do something more interesting with each Account record
                debugPrint(account.name)
            }
        }
    }.catch { error in
        // Handle error
    }
}

示例:连续执行异步请求

假设我们想从一个 自定义 Apex REST 资源中检索一个随机的邮政编码,然后在一个查询中使用该邮政编码

// Chained asynch requests
first {
    // Make GET request of custom Apex REST resource that returns a zip code as a string
    return salesforce.apex(path: "/MyApexResourceThatEmitsRandomZip")
}.then { (result: Data) -> Promise<QueryResult<SObject>> in
    // Query accounts in that zip code
    guard let zip = String(data: result, encoding: .utf8) else {
        throw NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil)
    }
    let soql = "SELECT Id,Name FROM Account WHERE BillingPostalCode = '\(zip)'"
    return salesforce.query(soql: soql)
}.done { queryResult -> () in
    for record in queryResult.records {
        if let name = record.string(forField: "Name") {
            print("Account name = \(name)")
        }
    }
}.catch { error in
    // Handle error
}

您可以多次重复此连续执行,将一个异步操作的输出作为下一个操作的输入。或者,您可以同时启动多个操作,并方便地指定逻辑,以便在所有操作都完成后执行,或仅当第一个操作完成后执行,或任何操作失败时执行等。PromiseKit 是一个功能强大的框架,用于处理多个异步操作,否则这些操作将很难协调。请参阅 PromiseKit 文档 以获取更多示例。

示例:检索用户的照片

// "first" block is an optional way to make chained calls easier to read...
first {
    salesforce.identity()
}.then { (identity) -> Promise<UIImage> in
    if let photoURL = identity.photoURL {
        return salesforce.fetchImage(url: photoURL)
    }
    else {
        // Return the default image instead
        return Promise(value: defaultImage)
    }
}.done { image in
    self.photoView.image = image
}.catch { (error) -> () in
    // Handle any errors
}.finally {
    self.refreshControl?.endRefreshing()
}

示例:检索联系人的照片

first {
    salesforce.retrieve(type: "Contact", id: "003f40000027GugAAE")
}.then { (record: Record) -> Promise<UIImage> in
    if let photoPath = record.string(forField: "PhotoUrl") {
        // Fetch image
        return salesforce.fetchImage(path: photoPath)
    }
    else {
        // Return a pre-defined default image
        return Promise(value: self.defaultImage)
    }
}.done { (image: UIImage) -> () in
    // Do something interesting with the image, e.g. display in a view:
    // self.photoView.image = image
}.catch { (error) -> () in
    // Handle any errors
}.finally {
    self.refreshControl?.endRefreshing()
}

示例:检索账户的账单地址

标准对象(例如账户和联系人)的地址存储在组合地址字段中,如果您在组织中启用了地理编码数据集成规则,Salesforce将自动为这些地址进行地理编码,从而为您提供可用于地图标记的纬度和经度值。

first {
    salesforce.retrieve(type: "Account", id: "001f40000036J5mAAE")
}.then { (record: Record) -> () in
    if let address = record.address(forField: "BillingAddress"), let lon = address.longitude, let lat = address.latitude {
	// You could put a marker on a map...
        print("LAT/LON: \(lat)/\(lon)")
    }
}.catch { (error) -> () in
    // Handle any errors
}

或者使用您自己的自定义Decodable模型类,而不是默认的Record.

struct MyAccountModel: Decodable {
			
    var id: String
    var name: String
    var billingAddress: Address?
			
    enum CodingKeys: String, CodingKey {
        case id = "Id"
        case name = "Name"
        case billingAddress = "BillingAddress"
    }
}
// ...
first {
    salesforce.retrieve(type: "Account", id: "001f40000036J5mAAE")
}.then { (record: MyAccountModel) -> () in
    if let address = record.billingAddress, let lon = address.longitude, let lat = address.latitude {
        // You could put a marker on a map...
        print("LAT/LON: \(lat)/\(lon)")
    }
}.catch { (error) -> () in
    // Handle any errors
}

示例:处理错误

func loadUserInfo() {
    salesforce.identity().compactMap { (identity) -> URL? in
        self.nameLabel.text = identity.displayName
        return identity.photoURL
    }.then { (url) -> Promise<UIImage> in
        salesforce.fetchImage(url: url)
    }.done { image -> () in
        self.photoView.image = image
    }.catch {
        debugPrint("Unable to load user photo! (\($0.localizedDescription))")
    }
}

您还可以使用一个recover闭包从错误中恢复,并继续使用链,以下代码片段来自PromiseKit的文档

CLLocationManager.promise().recover { err in
    guard !err.fatal else { throw err }
    return CLLocationChicago
}.done { location in
    // the user’s location, or Chicago if an error occurred
}.finally { err in
    // the error was fatal
}

示例:检索对象元数据

例如,如果您想确定用户是否有权限更新或删除记录,以便在UI中禁用编辑,或者如果您想检索选择列表中的所有选项,而不是在移动应用中硬编码它们,则可以调用salesforce.describe(type:)来检索对象的元数据

first {
    salesforce.describe(type: "Account")
}.done { (accountMetadata) -> () in
    self.saveButton.isEnabled = accountMetadata.isUpdateable
    if let fields = accountMetadata.fields {
        let fieldDict = Dictionary(items: fields, key: { $0.name })
        let industryOptions = fieldDict["Industry"]?.picklistValues
        // Populate a drop-down menu with the picklist values...
    }
}.catch { error in
    debugPrint(error)
}

您可以并行检索多个对象的元数据,并在检索所有内容后再继续

first {
    salesforce.describe(types: ["Account", "Contact", "Task", "CustomObject__c"])
}.then { results -> () in
    // results is an array of ObjectMetadatas, in the same order as requested
}.catch { error in
    // Handle the error
}

示例:注销

如果您想注销当前Salesforce用户并清除任何本地缓存的数据,您可以调用以下操作。Swiftly Salesforce将吊销并移除任何存储的凭据。

@IBAction func logoutButtonPressed(sender: AnyObject) {
    salesforce.revoke().done {
        debugPrint("Access token revoked.")
    }.ensure {
        self.tasks.removeAll()
        self.tableView.reloadData()
    }.catch {
        debugPrint("Unable to revoke user access token: \($0.localizedDescription)")
    }
}

示例:使用Salesforce对象搜索语言(SOSL)进行搜索

了解更多关于SOSL的信息

let sosl = """
    FIND {"A*" OR "B*" OR "C*"} IN Name Fields RETURNING lead(name,phone,Id), contact(name,phone)
"""
salesforce.search(sosl: sosl).done { result in
    debugPrint("Search result count: \(result.searchRecords.count)")
    for record in result.searchRecords {
        // Do something with each record in the search result
    }
}.catch { error in
    // Handle error
}

示例:将 Swiftly Salesforce 添加到您的 CocoaPods Podfile

target 'MyApp' do
  use_frameworks!
  pod 'SwiftlySalesforce'
  # Another pod here
end

依赖框架

Swiftly Salesforce 依赖于 PromiseKit:“不仅仅是一个 promise 实现,它还包含一系列帮助函数,使 iOS 开发者常用的典型异步模式更加愉快。”

资源

如果您是 Salesforce 平台或 Salesforce REST API 的新手,以下资源可能会有所帮助:

联系

欢迎提问、建议、错误报告和代码贡献