使用 Swiftly Salesforce 在 Salesforce 平台上快速构建 iOS 应用程序
- 完全使用 Swift 编写。
- 使用 promises 简化复杂的异步 Salesforce API 交互。
- 自动管理 Salesforce OAuth2 用户身份验证和授权过程(“OAuth 舞蹈”)。
- 比 Salesforce iOS 移动 SDK 简单、轻量级的替代方案。
- 易于安装和更新。
- 兼容 Realm,提供完整的离线移动解决方案。
- 查看最新内容.
快速开始
按照以下步骤,您只需几分钟即可开始使用
- 获取免费的Salesforce开发者版
- 在您的全新开发者版中创建一个Salesforce 连接应用
- 将Swiftly Salesforce添加到您的Xcode项目中
- 配置您的应用代理(示例)
最小要求
- 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)进行搜索
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 的新手,以下资源可能会有所帮助:
- Salesforce REST API 开发者指南
- Salesforce 平台
- Salesforce 开发者:官方 Salesforce 开发者网站;培训、文档、SDK 等。
- Salesforce 伙伴社区:“创新、成长、连接” with Salesforce ISVs. 加入 Salesforce + iOS 移动 Chatter 群组。
- Salesforce iOS 移动 SDK:Salesforce 支持的用于开发移动应用的 SDK。使用 Objective-C 编写。也适用于 Android。
- 何时使用 Salesforce1 平台与创建自定义应用程序相比
联系
欢迎提问、建议、错误报告和代码贡献
- 打开 GitHub 事项
- Twitter @mike4aday
- 加入 Salesforce 伙伴社区 并在 'Salesforce + iOS 移动' Chatter 群组中发表帖子