Steps to setting up relational database (sqlite) in swift - ios

I am working on an iOS app and I want to set up a relational database for it. I read that SQLite is a good database to use. I'm making the app in Swift. I've looked at several tutorials but it seems like all the ones that I found have the DBManager class or libraries in objective-c. Therefore, I would need a wrapper for the db class. I'm not really sure how the wrapper works and how I would be calling objective-c methods using swift syntax.
I was wondering if someone could help clarify the whole process for this from creating the database file and adding it to your xcode project to using objective-c libraries with swift syntax in order to run queries against the database file.
Also, is it worth it to just use Core Data which seems easier to use instead?

Objective-C tutorials are still more-or-less relevant, but obviously won't bring Swift-specific niceties into the fold (nor does FMDB, currently, as recommended by another commenter). I ended up writing SQLite.swift to utilize some of the more interesting aspects of Swift (type-safety and generics, optionals):
https://github.com/stephencelis/SQLite.swift
SQLite.swift provides compile-time safety/confidence, and removes the need for a lot of error handling. Statements and expressions are built in Swift proper, so runtime SQL syntax errors are unlikely.
See the documentation for more information on creating the database file and where to store it (depending on your needs):
https://github.com/stephencelis/SQLite.swift/blob/master/Documentation/Index.md#connecting-to-a-database
As far as Core Data is concerned, it (like most Apple libraries at the time of this answer) doesn't take advantage of Swift, but has a rich legacy and may be the way to go, especially for a smaller, persistent object graph. However, if you want control over a relational database, or if you plan on storing a large dataset that changes frequently, you may become frustrated with Core Data (and the domain-specific knowledge you'll need to attain),

I have myself followed this step by step and well explained tutorial by techtopia.
http://www.techotopia.com/index.php/Swift_iOS_8_Database_Implementation_using_SQLite
http://www.techotopia.com/index.php/An_Example_SQLite_based_iOS_8_Application_using_Swift_and_FMDB
It uses FMDB wrapper.
Creating the Database and Table
override func viewDidLoad() {
super.viewDidLoad()
let filemgr = NSFileManager.defaultManager()
let dirPaths =
NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
.UserDomainMask, true)
let docsDir = dirPaths[0] as! String
databasePath = docsDir.stringByAppendingPathComponent(
"contacts.db")
if !filemgr.fileExistsAtPath(databasePath as String) {
let contactDB = FMDatabase(path: databasePath as String)
if contactDB == nil {
println("Error: \(contactDB.lastErrorMessage())")
}
if contactDB.open() {
let sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT, PHONE TEXT)"
if !contactDB.executeStatements(sql_stmt) {
println("Error: \(contactDB.lastErrorMessage())")
}
contactDB.close()
} else {
println("Error: \(contactDB.lastErrorMessage())")
}
}
}
The code in the above method performs the following tasks:
-Identifies the application’s Documents directory and constructs a path to the contacts.db database file.
-Creates an NSFileManager instance and subsequently uses it to detect if the database file already exists.
-If the file does not yet exist the code creates the database by creating an FMDatabase instance initialized with the database file path. If the database creation is successful it is then opened via a call to the open method of the new database instance.
-Prepares a SQL statement to create the contacts table in the database and executes it via a call to the FMDB executeStatements method of the database instance.
-Closes the database.
SAVE DATA TO DATABASE
#IBAction func saveData(sender: AnyObject) {
let contactDB = FMDatabase(path: databasePath as String)
if contactDB.open() {
let insertSQL = "INSERT INTO CONTACTS (name, address, phone) VALUES ('\(name.text)', '\(address.text)', '\(phone.text)')"
let result = contactDB.executeUpdate(insertSQL,
withArgumentsInArray: nil)
if !result {
status.text = "Failed to add contact"
println("Error: \(contactDB.lastErrorMessage())")
} else {
status.text = "Contact Added"
name.text = ""
address.text = ""
phone.text = ""
}
} else {
println("Error: \(contactDB.lastErrorMessage())")
}
}
FETCH DATA FROM DATABASE
#IBAction func findContact(sender: AnyObject) {
let contactDB = FMDatabase(path: databasePath as String)
if contactDB.open() {
let querySQL = "SELECT address, phone FROM CONTACTS WHERE name = '\(name.text)'"
let results:FMResultSet? = contactDB.executeQuery(querySQL,
withArgumentsInArray: nil)
if results?.next() == true {
address.text = results?.stringForColumn("address")
phone.text = results?.stringForColumn("phone")
status.text = "Record Found"
} else {
status.text = "Record not found"
address.text = ""
phone.text = ""
}
contactDB.close()
} else {
println("Error: \(contactDB.lastErrorMessage())")
}
}

You could use the sqlite wrapper included in this project. It's written in Objective-C but it can easily be used from within Swift.

Yet another SQLite wrapper for Swift 3: http://github.com/groue/GRDB.swift
It provides:
An API that will look familiar to users of the famous Objective-C FMDB (https://github.com/ccgus/fmdb)
A low-level SQLite API that leverages the Swift standard library.
A pretty Swift query interface for SQL-allergic developers
Support for the SQLite WAL mode, and concurrent database access for extra performance
A Record class that wraps result sets, eats your custom SQL queries for breakfast, provides persistence operations, and changes tracking.
Swift type freedom: pick the right Swift type that fits your data. Use Int64 when needed, or stick with the convenient Int. Store and read NSDate or NSDateComponents. Declare Swift enums for discrete data types. Define your own database-convertible types.
Database migrations
Speed: https://github.com/groue/GRDB.swift/wiki/Performance

Related

How to create a pre bundled realm file and upload data to it?

I am new to Realm and I want to ship a pre bundled Realm file my app, however the realm documentation is unclear to me on how to actually create a .realm file and also upload the desired pre bundled data to it. I have not been able to find any solution via SO, Google, or Youtube. Step-by-step instructions would be very helpful.
We're still looking at ways to officially make generating pre-populated Realms more easy. It's definitely a desired feature for the Realm Browser, but due to the way that Realm files require a migration when changing the schema, it's somewhat tricky to incorporate into a UI. It's definitely something we want to improve down the line.
At the moment, the Browser has a converter in it that can perform minimal data import like CSV files.
The Realm documentation is saying that the easiest way for you to produce a pre-populated Realm specific for your apps needs is to build a separate macOS app whose sole role is to generate the Realm file, pre-populate the data and then expose the resulting Realm file so you can copy it to your app.
The operation to do this is just like any normal Realm interaction (try! Realm() is what causes the file to actually be initially created), except you manually interact with the Realm file on disk upon completion.
I'm working on an app for an iOS conference, and the schedule data for the event is going to be stored in a Realm that is pre-bundled with the app when it ships.
Since I thought creating an extra macOS app would be overkill for an iOS app, I instead used iOS unit tests that would generate the pre-bundled Realm from scratch every time it was executed.
class Tests: XCTestCase {
func testGenerateNewDefaultRealm() {
let sources = [conferences, sponsors, conferenceDays] as [Any]
XCTAssert(generateDefaultRealm(named: "MyConferenceRealm.realm", sources: sources))
}
}
extension Tests {
public func generateDefaultRealm(named name: String, sources: [Any]) -> Bool {
// Create and configure the Realm file we'll be writing to
let realm = generateRealm(named: name)
// Open a Realm write transaction
realm.beginWrite()
// Loop through each source and add it to Realm
for source in sources {
if let objectArray = source as? [Object] {
for object in objectArray {
realm.add(object)
}
}
else if let objectDictionary = source as? [String : Object] {
for (_, object) in objectDictionary {
realm.add(object)
}
}
}
// Commit the write transaction
do {
try realm.commitWrite()
}
catch let error {
print(error.localizedDescription)
return false
}
// Print the file location of the generated Realm
print("=====================================================================")
print(" ")
print("Successfully generated at")
print(realm.configuration.fileURL!.path)
print(" ")
print("=====================================================================")
return true
}
public func generateRealm(named name: String) -> Realm {
let exportPath = NSTemporaryDirectory()
let realmPath = exportPath.appending(name)
// Delete previous Realm file
if FileManager.default.fileExists(atPath: realmPath) {
try! FileManager.default.removeItem(atPath: realmPath)
}
// Create new Realm file at path
let objectTypes: [Object.Type] = [Conference.self, ConferenceDay.self, SessionBlock.self, Event.self, Presentation.self,
Session.self, Location.self, Speaker.self, Sponsor.self, Venue.self]
let configuration = Realm.Configuration(fileURL: URL(string: realmPath), objectTypes: objectTypes)
let realm = try! Realm(configuration: configuration)
return realm
}
}
Running this unit test will generate a new Realm file in the NSTemporaryDirectory() directory of the Mac, and then feed in a set of Array and Dictionary objects of un-persisted Realm Object instances that are created each time the test is run. The location of the final Realm is then printed in the console so I can grab it and move it to the app bundle.

How to encrypt and decrypt using 'FMDB/SQLCipher' in Swift?

I have used FMDB to create a SQLite database in Swift. But now I want to encrypt it. So can anyone please help me with the Swift version of encrypting and decrypting SQLite database using 'FMDB/SQLCipher'? I was not able to find a good tutorial to understand this.
Below is a sample code that sets a key on a database which is a FMDatabase object. You have to use the setKey() method in order to access an encrypted database. I have also written a wrapper over FMDB library which will make your life easier dealing with encrypted databases.
Here it is : https://github.com/SagarSDagdu/SDDatabase/
It also has ample amount of documentation and example code.
func executeUpdate(onDatabase database:FMDatabase, withStatement statement:String, values: [Any]?) -> Bool {
var success:Bool = false
do {
database.logsErrors = self.loggingEnabled
if let key = self.dbPassKey { //Use your key here
database.setKey(key)
}
try database.executeUpdate(statement, values:values)
success = true
}
catch {
print("Error in \(#function) with query: \(statement), error : \(error)")
}
return success
}

How can I take out a value as a string from RealmSwift?

I am developing an iOS app with RealmSwift by referring here. https://realm.io/docs/swift/latest/#in-memory-realms
And, what I don't understand is, how can I indicate the location(record and column) of the data in the realm file.
I've saved a realm file that named "DicData.realm" on the main folder where the same location as ViewController.swift is saved.
The data of DicData.realm is something like this:
1,face,423
2,rain,435
3,airplane,555
If I run the code below, it only printed like this: "results: Results ( )". It seems the filter method is just neglected. When I want to take out the word "airplane" and store in a variable as a string, how should I modify my code?
override func didMoveToView(view: SKView) {
func test()->Int {
let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "DicData"))
let results = realm.objects(DBData).filter("id == 3")
print("results: \(results)")
}
class DBData: Object {
dynamic var id = 0
dynamic var name = ""
dynamic var code = ""
}
You're referring here to the chapter of in-memory Realms and have setup your configuration to use those. An in-memory Realm is not a file. It lives exclusively in memory and is not actually written to disk.
If you've a prepared file, you want to bundle with your app, you need to make sure, that it is part of the Copy-Files Build Phase of the corresponding target of your Xcode project.
You can then copy the file from the app bundle initially via NSFileManager to a path, where the copy can be modified.
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let bundleURL = NSBundle.mainBundle().URLForResource("DicData", withExtension: "realm")!
do {
try NSFileManager.defaultManager().copyItemAtURL(bundleURL, toURL: defaultURL)
} catch {}
}

Prevent Realm from writing JSON twice

I'm new to Realm and iOS. I'm working on an app(written in Swift) that has a feature for the user to search for golf courses. I have a JSON file with roughly 18K courses in it. So I wanted to use Realm so I can quickly search through these courses in my app without it slowing down the user experience. I was able to get my JSON file written to the Realm Browser and can retrieve and search through the items, which has made it a LOT faster.
The problem I'm is I have the code in my App Delegate because I wanted to send the JSON items to my Realm Browser upon the app's launch. But if the app is started again then it writes the JSON file again, which creates duplicate golf courses in my Realm Browser.
Any suggestions on how I should do this so I can write the JSON file to the browser without getting duplicates each time the app is launched?
Thanks!
My code for writing my JSON file to my Realm Browser:
let dataManager = DataManager.getGolfCoursesFromFileWithSuccess { (data) -> Void in
let json = JSON(data: data)
if let courseArray = json.array {
for course in courseArray {
let golfCourseName: String? = course["biz_name"].string
let city: String? = course["e_city"].string
let state: String? = course["e_state"].string
if golfCourseName != nil {
let course = Course()
course.name = golfCourseName!
course.city = city!
course.state = state!
let realm = try! Realm()
try! realm.write {
realm.add(course)
}
}
}
}
}
Just check to see if the data is stored already.
I.e.
if try! Realm().objects(GolfCourse).count == 0 {
// your loading code here.
}
I figured it out. Because this was a data set that I wanted the user to have when they initially start using the app I learned how to bundle a Realm file with this data and put the file directly in my Xcode project. I then configured this file using the method:
"NSBundle.mainBundle().pathForResource("MyBundledData", ofType:"realm")"

SQLITE_BUSY error in iOS application

I am writing an iOS application to allow medical doctors to select diagnoses codes from a database and bill patients that are also in the sqlite3 database. The main page is a form that asks for the patient name and date of birth. Also on the main page is a button to begin the search (drill down through body locations) for the diagnoses code.
I am using a tab bar with a bill tab, patients tab, and a doctors tab. A user can add patients and doctors from their respective pages. This works and shows the patients/doctors in their list unless I have navigated to the diagnoses detail page on the bill tab.
I keep my database connection in a "DatabaseManager" class that opens and closes the database file on the device.
class DatabaseManager {
var db:COpaquePointer!
init(){
}
/**
* Checks that the database file is on the device. If not, copies the database file to the device.
* Connects to the database after file is verified to be in the right spot.
**/
func checkDatabaseFileAndOpen(){
let theFileManager = NSFileManager.defaultManager()
let filePath = dataFilePath()
if theFileManager.fileExistsAtPath(filePath) {
db = openDBPath(filePath)
} else {
let pathToBundledDB = NSBundle.mainBundle().pathForResource("testDML", ofType: "sqlite3")// Copy the file from the Bundle and write it to the Device
let pathToDevice = dataFilePath()
var error:NSError?
if (theFileManager.copyItemAtPath(pathToBundledDB!, toPath:pathToDevice, error: nil)) {
db = openDBPath(pathToDevice)
} else {
println("database failure")
}
}
}
/**
* Gets the path of the database file on the device
**/
func dataFilePath() -> String {
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDirectory = paths[0] as! NSString
return documentsDirectory.stringByAppendingPathComponent("testDML.sqlite3") as String
}
/**
* Makes a connection to the database file located at the provided filePath
**/
func openDBPath(filePath:String) -> COpaquePointer {
var db:COpaquePointer = nil
var result = sqlite3_open(filePath, &db)
println("openResult: \(result)")
if result != SQLITE_OK {
sqlite3_close(db)
println("Failed To Open Database")
return nil
}else {
return db
}
}
func closeDB() {
var closeResult = sqlite3_close_v2(db)
print("closed result:\(closeResult)")
if closeResult == SQLITE_OK {
}
}
Any time I do a query I open the database, do the query, and then close the database. I get SQLITE_OK on closing and opening the database for every query I run, but I get a SQLITE_BUSY result when adding patients and doctors only when I have navigated to the diagnoses detail page on the bill tab. All the detail page does is retrieve the diagnoses code and update some text on the screen. The add functions for patients and doctors are below:
func addPatientToDatabase(inputPatient:String, dateOfBirth:String, email:String){
var (firstName, lastName) = split(inputPatient)
println(dateOfBirth)
let query = "INSERT INTO Patient (pID,date_of_birth,f_name,l_name, email) VALUES (NULL, '\(dateOfBirth)', '\(firstName)', '\(lastName!)', '\(email)')"
var statement:COpaquePointer = nil
if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
var sqliteResult = sqlite3_step(statement)
if sqliteResult == SQLITE_DONE {
println("Saved \(firstName) \(lastName!)")
}else {
println("Add patient failed \(sqliteResult)")
}
}
}
func addDoctorToDatabase(inputDoctor:String, email:String) {
var (firstName, lastName) = split(inputDoctor)
let query = "INSERT INTO Doctor (dID,f_name,l_name, email) VALUES (NULL,'\(firstName)', '\(lastName!)', '\(email)')"
var statement:COpaquePointer = nil
if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
var sqliteResult = sqlite3_step(statement)
if sqliteResult == SQLITE_DONE {
println("Saved \(firstName) \(lastName!)")
}else {
println("Add doctor failed for \(firstName) \(lastName!) with error \(sqliteResult)")
}
}
}
I thought that this wouldn't be a problem because it is impossible for the user to run two queries at once and I have made sure that there is only one connection. Does anyone have any suggestions on what might be happening here?
I believe you forget finalizing your prepared statements using sqlite3_finalize(), unless you have unmatched open/close calls or you access the db connection from multiple threads. According to sqlite guidelines:
If the database connection is associated with unfinalized prepared
statements or unfinished sqlite3_backup objects then sqlite3_close()
will leave the database connection open and return SQLITE_BUSY.
In a Swift project in addition to Mostruash's answer, you may even need to use sqlite3_close_v2() method. sqllite3_close() method not always returns SQLITE_OK.
If the database connection is associated with unfinalized prepared
statements or unfinished sqlite3_backup objects then sqlite3_close()
will leave the database connection open and return SQLITE_BUSY. If
sqlite3_close_v2() is called with unfinalized prepared statements
and/or unfinished sqlite3_backups, then the database connection
becomes an unusable "zombie" which will automatically be deallocated
when the last prepared statement is finalized or the last
sqlite3_backup is finished. The sqlite3_close_v2() interface is
intended for use with host languages that are garbage collected, and
where the order in which destructors are called is arbitrary.
Applications should finalize all prepared statements, close all BLOB
handles, and finish all sqlite3_backup objects associated with the
sqlite3 object prior to attempting to close the object. If
sqlite3_close_v2() is called on a database connection that still has
outstanding prepared statements, BLOB handles, and/or sqlite3_backup
objects then it returns SQLITE_OK and the deallocation of resources is
deferred until all prepared statements, BLOB handles, and
sqlite3_backup objects are also destroyed.

Resources