Salesforce Demo
Overview
Section titled “Overview”This example demonstrates how to build a Salesforce service application that retrieves a list of Account records from a Salesforce org using the Salesforce REST API. The app showcases real-world usage of authenticated networking, SOQL queries, JSON data retrieval, and asynchronous calls.
| Functionality demonstrated | Technology demonstrated | Github Project |
|---|---|---|
| • Calling Salesforce REST API • Fetching Salesforce data as JSON • Returning data from Swift to Android • Displaying Salesforce data in Android UI | • Swift Android compiler • Swift4j • SwiftPM Gradle plugin • Foundation networking • Salesforce REST API | iOS Example Android Example |
The Salesforce Demo example highlights common features used when integrating with Salesforce in Swift:
- OAuth Authentication: Requesting an access token using the OAuth 2.0 token endpoint (
client_credentialsgrant) - Networking: Performing asynchronous HTTP requests using
URLSessionandURLRequest(async/await) - SOQL Queries: Querying Salesforce objects using the REST API query endpoint
- JSON Decoding: Decoding Salesforce responses using
JSONDecoderinto strongly-typed models (Account,AccountsQueryResponse) - Swift Logic Reuse on Android: Exposing Swift code to Android via Swift4j using the
@jvmbridge class and returning[Account]through a callback
APIs used:
- Salesforce OAuth 2.0 Token Endpoint – retrieves an access token
- Salesforce REST API (Query) – retrieves Account records using SOQL
Architecture: The task is split into two parts:
- Core App Logic: Salesforce integration developed in Swift using Foundation (
URLSession) for OAuth and REST API calls - User Interface: Created in Android Studio to target the Swift logic
Develop in Xcode
Section titled “Develop in Xcode”Set up the Swift project as demonstrated in the HelloWorld tutorial
Section titled “Set up the Swift project as demonstrated in the HelloWorld tutorial”- Create Xcode project
- Modify
Package.swiftfile- Add a dependency on the
Swift4jpackage, which provides the necessary functionality to expose Swift code as a Java API. - Add a dependency on the
SalesforceDemo.swiftpackage.
- Add a dependency on the
- Create class SalesforceDemo, import
Swift4j, and add@JVMannotation
See the HelloWorld for details.
The resulting Package.swift file:
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package( name: "SalesforceDemo", platforms: [ .macOS(.v12), .iOS(.v18) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "SalesforceDemo", type: .dynamic, targets: ["SalesforceDemo"] ), ], dependencies: [ .package(url: "https://github.com/scade-platform/swift4j.git", from: "1.3.1") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "SalesforceDemo", dependencies: [ .product(name: "Swift4j", package: "swift4j") ] ), .testTarget( name: "SalesforceDemoTests", dependencies: ["SalesforceDemo"] ), ])SalesforceDemo class
Section titled “SalesforceDemo class”Create Sources/SalesforceDemo/SalesforceDemo.swift file:
import Foundationimport Swift4jimport Dispatchimport os
// MARK: - Models
public struct TokenResponse: Codable { public let access_token: String}
@jvmpublic struct Account: Identifiable, Codable { public let id: String public let name: String public let accountNumber: String? public let type: String?
enum CodingKeys: String, CodingKey { case id = "Id" case name = "Name" case accountNumber = "AccountNumber" case type = "Type" }}
public struct AccountsQueryResponse: Codable { public let totalSize: Int public let done: Bool public let records: [Account]}
// MARK: - Internal HTTP result holder
final class RequestResult: @unchecked Sendable { var data: Data? var response: URLResponse? var error: Error?}
// MARK: - Salesforce API Client
public final class SalesforceAPI {
private let clientId = "clientId" private let clientSecret = "clientSecret"
private let baseDomain = "orgfarm-c4ae2aaf32-dev-ed.develop.my.salesforce.com" private let apiVersion = "v65.0"
public init() {}
public func loadAccounts() async throws -> [Account] { let token = try await fetchAccessToken() let response = try await fetchAccounts(accessToken: token) return response.records }
// MARK: - HTTP Helper
private func performRequest(_ request: URLRequest) async throws -> Data { os_log("performRequest start");
let (data, response) = try await URLSession.shared.data(for: request);
os_log("performRequest request completed");
guard let httpResponse = response as? HTTPURLResponse else { os_log("performRequest response is not a HTTPURLResponse");
throw NSError( domain: "AccountsError", code: -1, userInfo: [NSLocalizedDescriptionKey: "response is not a HTTPURLResponse"] ) }
guard 200..<300 ~= httpResponse.statusCode else { let text = String(data: data, encoding: .utf8) ?? "" os_log("performRequest received bad status code: \(httpResponse.statusCode), error: \(text)");
throw NSError( domain: "AccountsError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: text] ) }
os_log("performRequest finished");
return data }
// MARK: - Token
private func fetchAccessToken() async throws -> String { var components = URLComponents() components.scheme = "https" components.host = baseDomain components.path = "/services/oauth2/token"
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url) request.httpMethod = "POST"
var body = URLComponents() body.queryItems = [ .init(name: "grant_type", value: "client_credentials"), .init(name: "client_id", value: clientId), .init(name: "client_secret", value: clientSecret) ]
request.httpBody = body.percentEncodedQuery?.data(using: .utf8) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let data = try await performRequest(request) return try JSONDecoder().decode(TokenResponse.self, from: data).access_token }
// MARK: - Accounts
private func fetchAccounts(accessToken: String) async throws -> AccountsQueryResponse { var components = URLComponents() components.scheme = "https" components.host = baseDomain components.path = "/services/data/\(apiVersion)/query" components.queryItems = [ .init(name: "q", value: "SELECT Id, Name, AccountNumber, Type FROM Account") ]
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
let data = try await performRequest(request) return try JSONDecoder().decode(AccountsQueryResponse.self, from: data) }}
// MARK: - JVM Bridge
@jvmpublic final class SalesforceBridge { let api = SalesforceAPI()
public init() {}
public func loadAccounts(callback: ([Account]) -> Void) async { do { os_log("loadAccounts started") let accounts = try await api.loadAccounts() os_log("loadAccounts received accounts: \(accounts)") callback(accounts) os_log("loadAccounts completed") } catch { os_log("loadAccounts error: \(error)") callback([]) } }}Explanation:
- Uses Foundation (
URLSession) for all OAuth and Salesforce REST API requests - Retrieves an OAuth 2.0 access token using the
client_credentialsgrant - Executes a SOQL query to fetch Account records from Salesforce
- Encodes the retrieved Account list as a JSON string
- Exposed to Android via the
@jvmannotation using theSalesforceBridgeclass
Set up the Android project as demonstrated in the HelloWorld tutorial
Section titled “Set up the Android project as demonstrated in the HelloWorld tutorial”- Create a new Android project
- Set up the Gradle config file in the
appdirectory- Add the Swift Packages for Gradle plugin to the project.
- Add a configuration block for the plugin.
See the HelloWorld for details.
The resulting build.gradle.kts file:
plugins { // The first two plugins are generated by the Android Studio wizard alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android)
// We add the Swift Packages for Gradle plugin to the project id("io.scade.gradle.plugins.android.swiftpm") version "latest.release"}
android { // This part remains untouched and is generated by the Android Studio wizard}
dependencies { // We add a dependency to the native SQLite3 library, which is required by GRDB.swift // This library is provided within an AAR archive and is built from official SQLite3 sources implementation("io.scade.android.lib:sqlite3:3.49.0")
// This part remains untouched and is generated by the Android Studio wizard}
swiftpm { path = file("../../../Packages/SalesforceDemo") product = "SalesforceDemo" javaVersion = 8 scdAutoUpdate = true}Bind Swift logic to Android UI control
Section titled “Bind Swift logic to Android UI control”Now, we can open the MainActivity.kt file and implement the user interface logic. For the sake of simplicity, the example displays the list of Salesforce Account records returned from the Swift code as a JSON string.
First, we add an import statement for the SalesforceBridge class exported from the Swift package at the top of the file:
import SalesforceDemo.SalesforceBridgeNext, we add the User-Interface related logic and calling the SalesforceBridge to the MainActivity class. The complete code of the MainActivity.kt file is presented below:
package com.example.salesforcedemo
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.*import androidx.compose.ui.Modifierimport androidx.compose.ui.tooling.preview.Previewimport com.example.salesforcedemo.ui.theme.SalesforceDemoThemeimport SalesforceDemo.SalesforceBridge
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) System.loadLibrary("SalesforceDemo")
enableEdgeToEdge() setContent { SalesforceDemoTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> AccountsScreen(modifier = Modifier.padding(innerPadding)) } } } }}
@Composablefun AccountsScreen(modifier: Modifier = Modifier) { var text by remember { mutableStateOf("Loading..." ) }
LaunchedEffect(Unit) { try { val bridge = SalesforceBridge() val result = bridge.loadAccounts() { accounts -> text = accounts.joinToString("\n") { it.name } } } catch (e: Exception) { text = "Error: ${e.message}" } }
Text( text = text, modifier = modifier )}
@Preview(showBackground = true)@Composablefun AccountsScreenPreview() { SalesforceDemoTheme { AccountsScreen() }}Explanation:
- A mutable state variable holds the Salesforce Account data displayed in the UI.
- The UI is built with Jetpack Compose and renders the Account list in the
AccountsScreencomposable. - The Swift library is loaded via
System.loadLibrary("SalesforceDemo"), andSalesforceBridge.loadAccounts(callback:)is called. - The Swift code performs asynchronous OAuth authentication and Salesforce REST API requests, invoking a Kotlin callback when the Account data is available.
Run the native Swift app on Android
Section titled “Run the native Swift app on Android”