Intro
I’ve recently released an iOS application Commission. This app’s inception came about from my girl friend’s need to calculate her income coming in from various clinics and multiple product sales at each clinic (this calculation on her side was for when she gets paid from the clinics she has something to compare their numbers to).
Initially I setup an Excel sheet for her to input each product and it’s cost, sale price as well as each clinic’s commission split on profits. This Excel setup definitely worked but it was not very convenient for her due to the input having to be done on her MacBook; as her schedule is so busy she quickly began to miss inputing sales for entire days. Convenience is king these days!
SwiftUI
When starting a new application process I like to figure out the technologies I am going to use by looking at the feature set required to get a first version out the door.
Since I prefer native development when it comes to any project, and since this was going to be an iOS app – it wasn’t a question of using a cross-platform tool (React Native, Flutter etc.) or not – but whether I should use Apple’s tried and true framework UIKit or their newer framework SwiftUI.
I decided to use SwiftUI for this app for a couple of reasons and without justifying my decision too much (as that is not the point of this article) here are some of the deciding factors that lead me to go with SwiftUI:
- I only needed to release this app for iOS 14 or higher
- The user interface was going to be fairly simple consisting of mostly input forms and displaying data figures
- Composable and declarative UI is usually quicker to put together
- It’s new – and I find it fun to learn new things! 🎉
Persistence
I knew this application was going to need to hold some data such as: products, sales, and businesses. A fairly simple data model would suffice for the needs of this app.
Now in my experience you can go very simple for data storage such as serializing/deserializing everything to file up to more complex frameworks like Apple’s CoreData or Realm – each having pros and cons. I am a firm believer that there is no ‘one tool to rule them all’ but rather it’s best to pick the right tool for your job.
With that being said, I had a feeling that our data was going to be accessed quite frequently – to run various calculations i.e. in dashboard screens, aggregating multiple sales from multiple businesses etc. So right away I leaned away from the simple file storage option. I am sure if you wanted to you could get away with keeping some data in memory to run calculations but this would require more effort during development to ensure our data doesn’t get out of sync with our file stores – unnecessary complexity, bye-bye.
The next tool I considered was CoreData. This was almost the tool that I went with as I’ve seen some good examples online where CoreData was successfully used with SwiftUI. But the worry of being too complex a tool for such a simple data model kept me to keep pursuing other options. I actually didn’t even consider Realm for this app as I had no need to sync data )and the last time I used their library with Swift if kind of felt out of place with all of the ‘objc’ keywords littered in your model files) – and why lock yourself to a third party vendor when it’s not needed?
So, the tool that was just right in terms of complexity with just the right amount of features available that I landed on was: SQLite. Internally CoreData also uses SQLite but that is abstracted from us as users of the framework. A relational database matches well with my model needs, there isn’t too much ceremony or boilerplate to get up and running when it comes to fetching and saving data… and the best of all I have many options as to how I want to setup my code when it comes to a SQLite database.
With SQLite I have the freedom to utilize different patterns when it comes to the app’s persistence. I could write a class that handles all of the logic for retrieving certain rows from multiple tables in the database (Repository Pattern) or a data object pattern for more granularity (DAO)… or a bit of both!?
In hindsight, it seems that I favoured flexibility the highest when making my decision for my persistence tool/framework, second to performance.
GRDB
To make my life easier with SQLite I found this awesome open source library called GRDB. It gives you an easy to use toolkit for SQLite databases which streamlines your application development a lot – and if needed, you can still always write and execute raw SQL queries with this library!
On top of GRDB’s SQLite API, they also provide protocols and a class that help manipulating database rows as regular objects named “Records”.
To define your custom records, you subclass the ready-made Record class, or you extend your structs and classes with protocols that come with focused sets of features: fetching methods, persistence methods, record comparison… i.e. FetchableRecord:
struct Place: FetchableRecord { ... }
let places = try dbQueue.read { db in
try Place.fetchAll(db, sql: "SELECT * FROM place")
}
As you will see in GRDB’s documentation: a big difference with Records when compared to something like CoreData’s NSManagedObject is that Records are not uniqued, do not auto-update, and do not lazy load. This has a big impact on how you design your data model. A write-up of good practices when it comes to designing your Record Types can be found here: Good Practices for Designing Record Types
This is a good article to read if you are trying to decide whether or not GRDB is a good idea for your own application How to build an iOS application with SQLite and GRDB.swift
Query
Once of the challenges I found when it came to persistence when using SwiftUI was how quickly things can get messy. For instance I initially was putting @State
vars in all of my views… this quickly turned messy due to the fact that I then had to inject state into every view as well as ensure if an update occurred elsewhere in the app that all view’s refreshed their state with the most up-to-date data.
Luckily, I came across a Query
structure in one of GRDB’s examples that utilizes Combine’s Binding
as well as GRDB’s ValueObservation
which allows us to use a request variable on a view that fetches from the database anytime there’s a change that occurs in the db – automatically refreshing our view with the latest data:
/// The property wrapper that observes a database query
@propertyWrapper
struct Query<Query: Queryable>: DynamicProperty {
/// The database reader that makes it possible to observe the database
@Environment(\.appDatabase?.databaseReader) private var databaseReader: DatabaseReader?
@StateObject private var core = Core()
private var baseQuery: Query
/// The fetched value
var wrappedValue: Query.Value {
core.value ?? Query.defaultValue
}
/// A binding to the query, that lets your views modify it.
var projectedValue: Binding<Query> {
Binding(
get: { core.query ?? baseQuery },
set: {
core.usesBaseQuery = false
core.query = $0
})
}
init(_ query: Query) {
baseQuery = query
}
func update() {
guard let databaseReader = databaseReader else {
core.query = nil
return
}
// Feed core with necessary information, and make sure tracking has started
if core.usesBaseQuery { core.query = baseQuery }
core.startTrackingIfNecessary(in: databaseReader)
}
private class Core: ObservableObject {
private(set) var value: Query.Value?
var databaseReader: DatabaseReader?
var usesBaseQuery = true
var query: Query? {
willSet {
if query != newValue {
// Stop tracking, and tell SwiftUI about the update
objectWillChange.send()
cancellable = nil
}
}
}
private var cancellable: AnyCancellable?
init() { }
func startTrackingIfNecessary(in databaseReader: DatabaseReader) {
if databaseReader !== self.databaseReader {
// Database has changed. Stop tracking.
self.databaseReader = databaseReader
cancellable = nil
}
guard let query = query else {
// No query set
return
}
guard cancellable == nil else {
// Already tracking
return
}
cancellable = ValueObservation
.tracking(query.fetchValue)
.publisher(
in: databaseReader,
scheduling: .async(onQueue: DispatchQueue.global()))
.sink(
receiveCompletion: { _ in
// Ignore errors
},
receiveValue: { [weak self] value in
guard let self = self else { return }
DispatchQueue.main.async {
// Tell SwiftUI about the new value
self.objectWillChange.send()
self.value = value
}
})
}
}
}
That’s a lot of code; but essentially all it’s doing is providing you with a means to break out the logic needed to fetch data from the db in a separate structure and gets executed whenever a ValueObservation’s handler is triggered. An example Request
that conforms to Queryable
looks like:
struct AllAccountsRequest {}
extension AllAccountsRequest: Queryable {
static var defaultValue: [Account] { [] }
func fetchValue(_ db: Database) throws -> [Account] {
let accounts = try Account
.order(Account.Columns.name)
.fetchAll(db)
return accounts
}
}
Where Account
is conforming to GRDB’s appropriate protocols i.e. Codable & FetchableRecord
And within our SwiftUI view the request variable looks like:
struct AccountsScreen: View {
@Query(AllAccountsRequest()) private var accounts
...
// within your view's body 'accounts' can be iterated over
var body: some View {
NavigationView {
List {
ForEach(accounts) { account in
Text(account.name).tag(account.id)
}
}
}
}
}
Not only are the requests reactive now but they are also re-usable and contained in their own struct – making for (in my opinion) a much cleaner code structure that allows for easier testing, development & on-going maintenance.
Conclusion
Persistence is an important aspect for most applications (especially if you are planning for to be offline-first) and since it’s often such a pivotal and low-level aspect of your application choosing the best tool for your scenario up-front is key in avoiding major re-writes of your application in the future.
In this article we went over the process I went through for choosing a persistence tool and how I landed on SQLite along with a great toolkit GRDB. In a future posting I will go over the process for adding data into the database using GRDB.