Build iOS apps fast on the Salesforce Platform with Swiftly Salesforce:
- Written entirely in Swift.
- Uses Swift's new Combine framework to simplify complex , asynchronous Salesforce API interactions.
- Works with SwiftUI
- Manages the Salesforce OAuth2 user authentication and authorization process (the "OAuth dance") automatically.
- Simpler and lighter alternative to the Salesforce Mobile SDK for iOS.
- Easy to install and update with Swift Package Manager (SPM)
- Compatible with Realm for a complete, offline mobile solution.
- See what's new.
Quick Start
You can be up and running in a few minutes by following these steps:
- Get a free Salesforce Developer Edition
- Create a Salesforce Connected App in your new Developer Edition
- Add Swiftly Salesforce to your Xcode project as a package dependency, with URL https://github.com/mike4aday/SwiftlySalesforce.git.
Minimum Requirements
- iOS 13.0
- Swift 5.1
- Xcode 11
Examples
Below are some examples that illustrate how to use Swiftly Salesforce. Swiftly Salesforce will automatically manage the entire Salesforce OAuth2 process (the "OAuth dance"). If Swiftly Salesforce has a valid access token, it will include that token in the header of every API request. If the token has expired, and Salesforce rejects the request, then Swiftly Salesforce will attempt to refresh the access token, without bothering the user to re-enter the username and password. If Swiftly Salesforce doesn't have a valid access token, or is unable to refresh it, then Swiftly Salesforce will direct the user to the Salesforce-hosted login form.
Example: Setup
You can create a re-usable reference to Salesforce in your SceneDelegate.swift
file:
import UIKit
import SwiftUI
import SwiftlySalesforce
import Combine
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var salesforce: Salesforce!
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//...
// Copy consumer key and callback URL from your Salesforce connected app definition
let consumerKey = "<YOUR CONNECTED APP'S CONSUMER KEY HERE>"
let callbackURL = URL(string: "<YOUR CONNECTED APP'S CALLBACK URL HERE>")!
let connectedApp = ConnectedApp(consumerKey: consumerKey, callbackURL: callbackURL)
salesforce = Salesforce(connectedApp: connectedApp)
}
//...
}
In the example above, we created a Salesforce
instance with the Connected App's consumer key and callback URL. salesforce
is an implicitly-unwrapped, optional, global variable, but you could also inject a Salesforce
instance into your root view controller, for example, instead of using a global variable.
Example: Retrieve Salesforce Records
The following will retrieve all the fields for an account record:
salesforce.retrieve(type: "Account", id: "0013000001FjCcF")
To specify which fields should be retrieved:
let fields = ["AccountNumber", "BillingCity", "MyCustomField__c"]
salesforce.retrieve(type: "Account", id: "0013000001FjCcF", fields: fields)
Note that retrieve
is an asynchronous function, whose return value is a Combine publisher:
let pub: AnyPublisher<SObject, Error> = salesforce.retrieve(type: "Account", id: "0013000001FjCcF")
And you could use the sink
subscriber to handle the result:
let subscription = salesforce.retrieve(object: "Account", id: "0013000001FjCcF")
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Done")
case let .failure(error):
//TODO: handle the error
print(error)
}
}, receiveValue: { (record) in
//TODO: something more interesting with the result
if let name = record.string(forField: "Name") {
print(name)
}
})
You can retrieve multiple records in parallel, and wait for them all before proceeding:
var subscriptions = Set<AnyCancellable>()
//...
let pub1 = salesforce.retrieve(object: "Account", id: "0013000001FjCcF")
let pub2 = salesforce.retrieve(object: "Contact", id: "0034000002AdCdD")
let pub3 = salesforce.retrieve(object: "Opportunity", id: "0065000002AdNdH")
pub1.zip(pub2, pub3).sink(receiveCompletion: { (completion) in
//TODO:
}) { (account, contact, opportunity) in
//TODO
}.store(in: &subscriptions)
Example: Custom Model Objects
Instead of using the generic SObject
, you could define your own model objects. Swiftly Salesforce will automatically decode the Salesforce response into your model objects, as long as they implement Swift's Decodable
protocol:
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"
}
}
//...
let pub: AnyPublisher<MyAccountModel, Error> = salesforce.retrieve(object: "Account", id: "0013000001FjCcF")
Example: Update a Salesforce Record
salesforce.update(object: "Task", id: "00T1500001h3V5NEAU", fields: ["Status": "Completed"])
.sink(receiveCompletion: { (completion) in
//TODO: handle completion
}) { _ in
//TODO: successfully updated
}.store(in: &subscriptions)
You could also use the generic SObject
(typealias for SwiftlySalesforce.Record
) to update a record in Salesforce. For example:
// `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)
.sink(receiveCompletion: { (completion) in
//TODO: handle completion
}) { _ in
//TODO: successfully updated
}
.store(in: &subscriptions)
Example: Query Salesforce
let soql = "SELECT Id,Name FROM Account WHERE BillingPostalCode = '10024'"
salesforce.query(soql: soql)
.sink(receiveCompletion: { (completion) in
//TODO: completed
}) { (queryResult: QueryResult<SObject>) in
for record in queryResult.records {
if let name = record.string(forField: "Name") {
print(name)
}
}
}
.store(in: &subscriptions)
Example: Decode Query Results as Your Custom Model Objects
You can easily perform complex queries, traversing object relationships, and have all the results decoded automatically into your custom model objects that implement the Decodable
protocol:
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)
.sink(receiveCompletion: { (completion) in
//TODO: completed
}) { (queryResult: QueryResult<Contact>) in
for contact in queryResult.records {
print(contact.lastName)
if let account = contact.account {
print(account.name)
}
}
}
.store(in: &subscriptions)
}
Example: Retrieve Object Metadata
If, for example, you want to determine whether the user has permission to update or delete a record so you can disable editing in your UI, or if you want to retrieve all the options in a picklist, rather than hardcoding them in your mobile app, then call salesforce.describe(type:)
to retrieve an object's metadata:
salesforce.describe(object: "Account")
.sink(receiveCompletion: { (completion) in
//TODO: completed
}) { (acctMetadata) in
//TODO: update UI
let editable = acctMetadata.isUpdateable
}
.store(in: &subscriptions)
Example: Log Out
If you want to log out the current Salesforce user, and then clear any locally-cached data, you could call the following. Swiftly Salesforce will revoke and remove any stored credentials.
let pub: AnyPublisher<Void, Error> = salesforce.logOut()
//TODO: Connect to UI element with SwiftUI
Example: Search with Salesforce Object Search Language (SOSL)
let sosl = """
FIND {"A*" OR "B*" OR "C*" OR "D*"} IN Name Fields RETURNING Lead(name,phone,Id), Contact(name,phone)
"""
salesforce.search(sosl: sosl)
.sink(receiveCompletion: { (completion) in
//TODO: completed
}) { (results: [SObject]) in
for result in results {
print("Object type: \(result.object)")
}
}
.store(in: &subscriptions)
Resources
If you're new to the Salesforce Platform or the Salesforce REST API, you might find the following resources useful:
- Salesforce REST API Developer's Guide
- Salesforce Platform
- Salesforce Developers: official Salesforce developers' site; training, documentation, SDKs, etc.
- Salesforce Partner Community: "Innovate, grow, connect" with Salesforce ISVs. Join the Salesforce + iOS Mobile Chatter group
- Salesforce Mobile SDK for iOS: Salesforce-supported SDK for developing mobile apps. Written in Objective-C. Available for Android, too
- Use of Salesforce for Android and iOS platform versus creation of custom app
Contact
Questions, suggestions, bug reports and code contributions welcome:
- Open a GitHub issue
- Twitter @mike4aday
- Join the Salesforce Partner Community and post to the 'Salesforce + iOS Mobile' Chatter group