Simple Database
An example showing the usage of one of the most popular Swift libraries GRDB.swift for working with SQLite3
Overview
This example demonstrates how to build a simple database application using Swift on Android. It showcases the integration and usage of GRDB.swift, one of the most popular and powerful Swift libraries for working with SQLite databases.
This example implements a basic player management system with CRUD operations (Create, Read, Update, Delete), highlighting how Swift's modern syntax and GRDB's elegant API can be used to build robust database-driven applications that run seamlessly on Android platforms. The example is inspired by the original Demo provided in the GRDB.swift repository.
Additionally, this example demonstrates how easy it is to integrate pure native libraries into Android applications built with Swift. It shows how the SQLite3 native library is seamlessly linked and packaged within the application, illustrating the straightforward process of incorporating native dependencies into your Swift Android projects.
Functionality demonstrated | Technology demonstrated | Github Project |
---|---|---|
• Creating, reading, updating, and deleting player records from a database • Managing a local SQLite database • Integrating native SQLite3 • Using Swift logic inside Android UI | • Swift Android compiler • Swift4J • SwiftPM Gradle for Android • Foundation framework • SQLite3 native library • GRDB.swift | Examples |
It is strongly recommended that you complete the HelloWorld example first, as it covers essential concepts that are only briefly mentioned in this example.
Develop in Xcode
Set up the Swift project as demonstrated in the HelloWorld tutorial
- Create Xcode project
- Modify
Package.swift
file- Add a dependency on the
Swift4J
package, which provides the necessary functionality to expose Swift code as a Java API. - Add a dependency on the
GRDB.swift
package, pointing to a fork that includes necessary adjustments for Android compatibility. This is a temporary solution until the officialGRDB.swift
supports Android.
- Add a dependency on the
- Create class SimpleDB, import
Swift4J
, and add@JVM
annotation
See the HelloWorld for details.
The resulting Package.swift
file:
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import Foundation
import PackageDescription
let package = Package(
// Package.swift
name: "SimpleDB",
products: [
.library(
name: "SimpleDB",
// We use dynamic library to be able to use it on Android
type: .dynamic,
targets: ["SimpleDB"])
],
dependencies: [
// We use the Swift4j package to mark the Swift code that should be available on Android as Java API
.package(url: "https://github.com/scade-platform/swift4j.git", from: "1.2.1")
// We temporarily use a fork of GRDB.swift to ensure compatibility with Android
// This branch includes necessary adjustments for Android compatibility and is wating for the official release
// Once the official GRDB.swift supports Android, we can switch back to the main repository
//.package(url: "https://github.com/groue/GRDB.swift.git", exact: "7.6.0")
.package(url: "https://github.com/scade-platform/GRDB.swift.git", branch: "android_support")
],
targets: [
.target(
name: "WeatherService",
dependencies: [
// We add the Swift4j package as a dependency to our target
.product(name: "Swift4j", package: "swift4j")
]
)
]
)
Implementing the Database Logic
Next, we implement the core logic of our simple database application. Create a class named Database in the file Sources/SimpleDB/Database.swift
. This class will handle:
- Connecting to the SQLite database
- Performing CRUD operations on the player data
Also, create a struct named Player to represent the player data model. The Player struct will include properties such as id, name, and score.
The resulting Sources/SimpleDB/Database.swift
file:
import Foundation
import GRDB
import Swift4j
/// Represents the database for storing Player records.
///
/// Handles database migrations, CRUD operations, and manages the database connection.
@jvm
final class Database {
/// The database writer instance used for read/write operations.
private let dbWriter: any DatabaseWriter
/// Initializes the database with the provided writer and applies migrations.
///
/// - Parameter dbWriter: The database writer instance.
/// - Throws: An error if migration fails.
private init(_ dbWriter: any GRDB.DatabaseWriter) throws {
self.dbWriter = dbWriter
// Apply database migrations
try migrator.migrate(dbWriter)
}
/// The database migrator responsible for handling schema changes.
private var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator()
// Register the initial migration for the player table
migrator.registerMigration("v1") { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer).notNull()
}
}
return migrator
}
/// Creates and returns a Database instance for the given file path.
///
/// - Parameter path: The file path to the database.
/// - Returns: A Database instance.
static func create(path: String) -> Database {
do {
// Create a database queue for the given path
let dbQueue = try DatabaseQueue(path: path)
return try Database(dbQueue)
} catch {
// Terminate if the database cannot be opened
fatalError("Cannot open the db file: \(path). ERROR: \(error.localizedDescription)")
}
}
/// Saves a player to the database. If the player is new, inserts it; otherwise, updates it.
///
/// - Parameter player: The player to save (inout).
func save(player: inout Player) {
do {
// Write the player to the database
try dbWriter.write {
try player.save($0)
}
} catch {
// Terminate if saving fails
fatalError(error.localizedDescription)
}
}
/// Deletes a player from the database.
///
/// - Parameter player: The player to delete.
func delete(player: Player) {
do {
// Delete the player by id
try dbWriter.write {
_ = try Player.deleteAll($0, keys: [player.id])
}
} catch {
// Terminate if deletion fails
fatalError(error.localizedDescription)
}
}
/// Fetches all players from the database.
///
/// - Returns: An array of Player objects.
func fetch() -> [Player] {
do {
// Read all players from the database
return try dbWriter.read {
try Player.fetchAll($0)
}
} catch {
// Terminate if fetching fails
fatalError(error.localizedDescription)
}
}
}
/// Represents a player record in the database.
///
/// Conforms to Codable, FetchableRecord, and MutablePersistableRecord for database operations.
@jvm
struct Player: Codable, FetchableRecord, MutablePersistableRecord {
/// The unique identifier for the player (auto-incremented).
var id: Int64? = nil
/// The name of the player.
var name: String
/// The score of the player.
var score: Int
/// Called after the player is inserted into the database to update the id.
///
/// - Parameter inserted: The insertion result containing the new row ID.
@nonjvm mutating func didInsert(_ inserted: InsertionSuccess) {
// Update the id with the inserted row ID
id = inserted.rowID
}
}
Set up the Android project as demonstrated in the HelloWorld tutorial
- Create a new Android project
- Set up the Gradle config file in the
app
directory- Add the Swift Packages for Gradle plugin to the project.
- Add a configuration block for the plugin.
- Add a dependency to the native SQLite3 library into the plugin's configuration block, which is required by GRDB.swift to build. This library is available on Apple platforms as a binary but not on Android. The Swift PM plugin allows to link binary libraries from AAR archives what make possible to transparently ship all required binaries within AAR archives using the standard Gradle machanisms.
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 "1.1.1"
}
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/SimpleDB")
product = "SimpleDB"
javaVersion = 8
// We link native binaries from the SQLite3 AAR archive to the application
dependencies {
link("io.scade.android.lib:sqlite3:3.49.0")
}
}
Bind Swift logic to Android UI control
Now, we can open the MainActivity.kt
file and implement the database access and user interface logic. For demonstration purposes, the UI will allow adding and removing players from a local database using a Swift-powered backend.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Load the native library containing the Swift code and SQLite3
System.loadLibrary("SimpleDB")
// Get the path for the database file and create the Swift database instance
val dbFile = getDatabasePath("players.db")
val database = Database.create(dbFile.path)
// Enable edge-to-edge UI and set the main Compose screen
enableEdgeToEdge()
setContent {
PlayerEditorScreen(database)
}
}
}
@Composable
fun PlayerEditorScreen(database: Database) {
// State for player name input
var name by remember { mutableStateOf("") }
// State for player score input
var score by remember { mutableStateOf("") }
// List of players fetched from the database
val players = remember { mutableStateListOf(*database.fetch()) }
// State for currently selected player
var selectedPlayer by remember { mutableStateOf<Player?>(null) }
// Main UI layout
Column(modifier = Modifier.padding(start = 16.dp, top = 48.dp, end = 16.dp, bottom = 16.dp)) {
// Title
Text("Team", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
// Input for player name
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Player Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Input for player score (digits only)
OutlinedTextField(
value = score,
onValueChange = { score = it.filter { char -> char.isDigit() } },
label = { Text("Score") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Row with Add and Delete buttons
Row {
Button(onClick = {
// Add new player to database
if (name.isNotBlank() && score.isNotBlank()) {
val newPlayer = Player(name, score.toLong())
database.save(newPlayer)
players.clear()
players.addAll(database.fetch())
name = ""
score = ""
}
}) {
Text("Add Player")
}
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
// Delete selected player from database
selectedPlayer?.let {
database.delete(it)
players.clear()
players.addAll(database.fetch())
}
selectedPlayer = null
},
enabled = selectedPlayer != null
) {
Text("Delete Player")
}
}
Spacer(modifier = Modifier.height(16.dp))
// List of players
LazyColumn {
items(players) { player ->
PlayerItem(player, isSelected = player == selectedPlayer) {
// Select or deselect player
selectedPlayer = if (selectedPlayer == player) null else player
}
}
}
}
}
// Composable for displaying a single player item
@Composable
fun PlayerItem(player: Player, isSelected: Boolean, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
.clickable { onClick() },
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) else MaterialTheme.colorScheme.surface
)
) {
Row(modifier = Modifier.padding(16.dp)) {
// Display player name and score
Text(text = "${player.name} - ${player.score}")
}
}
}
Explanation:
Database
is a Swift class exposed to Kotlin via Swift4J.- Jetpack Compose is used to build the UI in
PlayerEditorScreen
. name
,score
, and selectedPlayer
are state variables for form input and selection.- Players are fetched from the Swift database using
database.fetch()
. - The Add button creates a
Player
and saves it usingdatabase.save(...)
. - The Delete button removes the selected player using
database.delete(...)
. PlayerItem
renders the player list and handles selection.
All logic runs on Android, while the data layer (SQLite via GRDB.swift) runs in native Swift.