Apple’s CoreData is a great choice for a data model on iOS and with SwiftUI. Under the hood it uses SQLite to handle lots of data that can’t fit into memory all at once. I have always wondered about using SQLite directly. It would be a little messy because SQLite is a C library and it would force you to use the unsafe APIs that you would certainly want to wrap. It turns out that you can pretty easily use the ORM from the Vapor project in your SwiftUI project. I prepared this example by looking at test code.

Let’s make this simple list app:

Planets app

Start by creating a vanilla SwiftUI project and importing the fluent-sqlite-driver Swift package. File > Swift Packages and add https://github.com/vapor/fluent-sqlite-driver to the project.

Once that is done you can create a model type:

import FluentSQLiteDriver

final class Planet: Model {
  static let schema = "planets"

  @ID(key: .id) var id: UUID?
  @Field(key: "name") var name: String

  init() {}

  init(id: UUID? = nil, name: String) {
    self.id = id
    self.name = name
  }
}

You will want to create a migration to prepare the tables in the database:

struct CreatePlanetMigration: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    database.schema(Planet.schema)
      .id()
      .field("name", .string, .required)
      .create()
  }

  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema(Planet.schema)
      .delete()
  }
}

For SwiftUI, you will want to creat a view-model:

import SwiftUI

final class AppModel: ObservableObject {
  let databases: Databases

  var logger: Logger = {
    var logger = Logger(label: "database.logger")
    logger.logLevel = .trace
    return logger
  }()

  var database: Database {
    return self.databases.database(
      logger: logger,
      on: self.databases.eventLoopGroup.next()
    )!
  }
  // More to follow...
}

The databases keeps the databases available. (Just like core data there might multiple stores.)

Next add this to the AppModel:

init() {
  let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
  let threadPool = NIOThreadPool(numberOfThreads: 1)
  threadPool.start()

  databases = Databases(threadPool: threadPool, on: eventLoopGroup)
  databases.use(.sqlite(.memory), as: .sqlite)
  databases.default(to: .sqlite)

  do {
    try CreatePlanetMigration().prepare(on: database).wait()
    try ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Ntune"].forEach {
      let item = Planet(name: $0)
      try item.save(on: database).wait()
    }
  }
  catch {
    print("error \(error)")
  }
}

This creates an in-memory database and adds some records to it. It performs the work syncronously on the main thread, but this should happen fast so is probably okay. (Fluent is designed to scale to hundreds or even thousands of concurrent operations.)

If you want the database to use durable storage, you can use .sqlite(.file(path)) instead of .sqlite(.memory)

Now let’s get the UI working. Finally add this to the AppModel:

var planets: [Planet] {
  try! database.query(Planet.self).all().wait()
}

This just syncronously grabs the data. Now create the SwiftUI to show it off.

@main
struct PlanetsApp: App {

  @StateObject var model = AppModel()

  var body: some Scene {
    WindowGroup {
      ContentView(model: model)
    }
  }
}

struct ContentView: View {
  @ObservedObject var model: AppModel
  var body: some View {
    NavigationView {
      List {
        ForEach(model.planets, id: \.id) {
          Text($0.name)
        }
      }.navigationTitle("Planets")
    }
  }
}

You can, of course, do more fancy queries using the Fluent ORM. For example:

var planets: [Planet] {
   try! database.query(Planet.self).filter(\.$name ~~ "a").all().wait()
}

This filters down to all of the planets that contain an “a”. If you look at the console, you can see the SQL this boils down to:

database.logger : database-id=sqlite 
 SELECT "planets"."id" AS "planets_id", 
        "planets"."name" AS "planets_name" FROM "planets" 
 WHERE "planets"."name" LIKE ? ["%a%"]

That’s pretty cool. In a future post, we’ll look at how to interface with Combine better with non-blocking calls.