© Kevin Languedoc 2016

Kevin Languedoc, Build iOS Database Apps with Swift and SQLite, 10.1007/978-1-4842-2232-4_5

5. Inserting Records

Kevin Languedoc

(1)Montreal, Canada

The SQL Insert statement in SQLite has some interesting features, such as being able to replace an existing record, just like the Upsert statement in Oracle’s PL/SQL. The expression parameter can either be a literal or the return value from a function. We will explore these interesting features together.

This chapter will demonstrate how to insert data into a SQLite database. I will cover all the supported variations of the API, including:

  • The data-binding functions

  • Inserting records

  • Inserting or replacing records

  • The Insert or Rollback option

  • The Insert or Ignore option

  • The Insert or Abort option

  • The Insert or Fail option

  • Inserting blobs

  • Building an iOS app to insert records

The Data-Binding Functions

The SQLite C API has specific functions for working with data, allowing an application to bind data with the Cocoa Touch corresponding data types. Bound data types are used with prepared statements. The following functions work with all the major primitive data types in regards to text and numbers. For blobs, images, videos, and audio the API provides blob and blob64 as well as zero and zeroblob64.

int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*));
int sqlite3_bind_blob64(sqlite3_stmt*, int, const void*, sqlite3_uint64, void(*)(void*));
int sqlite3_bind_double(sqlite3_stmt*, int, double);
int sqlite3_bind_int(sqlite3_stmt*, int, int);
int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
int sqlite3_bind_null(sqlite3_stmt*, int);
int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*));
int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int, void(*)(void*));
int sqlite3_bind_text64(sqlite3_stmt*, int, const char*, sqlite3_uint64, (*)(void*), unsigned char encoding);
int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*);
int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);
int sqlite3_bind_zeroblob64(sqlite3_stmt*, int, sqlite3_uint64);

For the basic primitive types, the first parameter of the SQLite binding function is the statement that you create using the sqlite3_stmt function and that is returned from the sqlite3_prepare_v2 function. The second parameter is the column number in the table where you want to insert records, which corresponds to a zero-based array, and the last parameter is the value to be inserted. The columns are a 0 (zero)-based array. So column 1 would be 0 and column 2 would be 1, and so on.

For blobs and 64-bit-length functions, the fourth parameter is the number of bytes in the value to be inserted, not the characters. You use blobs to store images, videos, and audio data. The last argument is the destructor.

The SQLite INSERT function

The INSERT function is the SQLite implementation of the SQL INSERT statement . The SQLite INSERT statement comes in three variations:

  • You can insert values into a database table without specifying the column names. However, the values being inserted must match the number of columns in the table. This type of insert takes the following form:

INSERT into main.table VALUES(values list items)
  • The second type of insert uses a SELECT statement as the list of rows to insert. If you omit the column names in the INSERT statement then the SELECT statement must have the exact same number of columns as in the target table, unless the table columns have default values. Here are three examples of the second type:

INSERT INTO main.table
        SELECT * FROM main.otherTable WHERE clause
INSERT INTO main.table
        SELECT column list FROM main.otherTable (with or without WHERE clause)
INSERT INTO main.table(column list)
        SELECT column list FROM main.otherTable (with or without WHERE clause)

Here is a quick, boilerplate example of how to insert records using the SQLite C API and Swift:

internal let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self)
internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)


func sample(){

        var sqlite3_stmt:COpaquePointer?=nil;
        var sqlite3_db:COpaquePointe?r=nil;
        var txt:String = "some text";
        let integer:Int32 = 500;
        let dbl:Double = 10.45;
        var dbPath:URL = URL()
        var sqlStatement:COpaquePointer?=nil
        var dbErr: UnsafeMutablePointer<UnsafeMutablePointer<Int8>>? = nil
        var errmsg:String=""
        let dbName = "winery.sqlite"


        //insert query
        let sql:String = "INSERT INTO table(coltext, colint, coldouble) VALUES(?,?,?)";


        let dirManager = FileManager.default()
        //Open db assuming there are no subfolders
        do {
            let documentDirectoryURL = try dirManager.urlForDirectory(FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)


            dbPath = documentDirectoryURL.urlLByAppendingPathComponent(dbName)
        } catch let err as NSError {
            print("Error: (err.domain)")
        }


        sqlite3_open(dbPath.path!, &sqlite3_db);
        sqlite3_prepare_v2(sqlite3_db, sql, 1, &sqlite3_stmt, nil);
        sqlite3_bind_text(sqlite3_stmt, 1, txt.cString(using: String.encoding.ut)!, -1, SQLITE_TRANSIENT);
        sqlite3_bind_int(sqlite3_stmt, 2, integer);
        sqlite3_bind_double(sqlite3_stmt, 3, dbl);
        sqlite3_step(sqlite3_stmt);
        sqlite3_finalize(sqlite3_stmt);
        sqlite3_close(sqlite3_db);


    }

In the preceding code, SQLITE_STATIC is an immutable sqlite3_static value pointer and SQLITE_TRANSIENT is a pointer that will change in the future, but it is SQLite that will move the pointer as needed.

Within the confines of a function, I create a SQLite statement variable called sqlite3_stmt using a COPaquePointer. This will be initialized with the string query, sql, through the sqlite3_prepare_v2 function, along with the SQLite database engine variable, db, which is created using the COPaquePointer. The next three lines of code create some test variables to allow the code to insert values in the database. For the sake of clarity and simplicity, I am creating a String variable, an Int32, and a Double. I will also hard code some values. However, these can be dynamically set using a block, parameters in a method, or as a result of some process or calculation.

The actual SQL INSERT query statement is pretty standard, as you can see from the sql String variable, which must be converted to a C char (string of chars) later using cUsingStringEncoding and NSUTF8Encoding. I find this statement works best. I have seen some developers trying to replace the interrogation symbols with actual variable names, but the code can get very messy and hard to maintain, and often SQLite will complain that there are issues with the statement because you need to convert and bind the input values.

The next piece of the puzzle is the path to the database file. In this example, I get the path and appended file name by using the NSSearchPathDirectory. DocumentDirectoryand GetDirectory functions in the NSFileManagerclass. I append the database file name using the URLByAppendingPathComponent function, which was changed from the stringByAppendingPathComponent method. The preceding code assumes that there are no subfolders, otherwise I would need to store the Document folder and subfolders in an array using the NSSearchPathForDirectoriesInDomainsfunction.

The other piece of important information is the fact that you need to create your database in the Documents directory in your app’s sandbox. It is the only useful writable directory. If you place the database in the Resources folder, you won’t get any errors, but the data will not be written to the database because the folder is read-only.

Insert or Replace

SQLite also has a special clause that allows you to insert/update using the INSERT statement. With SQLite you use the INSERT OR REPLACE statements . If an insert constraint is encountered, the REPLACE clause will delete the existing record and replace it with the new record. In cases where there is a NOT NULL, REPLACE will attempt to replace it with a default value if the REPLACE is trying to insert a NULL value. If no default value is available, the ABORT clause is used instead on that row. The remaining rows aren’t affected unless similar conditions are encountered. The general syntax is as follows:

INSERT OR REPLACE into schema.table_name(Id, ColText, ColInt, ColDouble) VALUES(1, ‘Kevin’, 20, 53.6)

If the index value, Id = 1, doesn’t exist, it will be inserted. Likewise, if it does exist, the record will be updated. You can use Swift data types instead of the primitives when inserting records, as the following example demonstrates:

    func replace(){
        let index:Int32 = 1
        let name:String = "kevin"
        let Dbl:Double = 1000.99
        var dbPath:URL = URL();


        let dirManager = FileManager.default()
        do {
            let directoryURL = try dirManager.urlForDirectory(FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)


            dbPath = directoryURL.urlByAppendingPathComponent("database.sqlite")
            if( sqlite3_open(dbPath.absoluteString?.cString(using: String.Encoding.utf8)!,&sqlite3_db) == 0){
                let sql:String = "INSERT OR REPLACE INTO schema.simpletable (id, name, colDouble)VALUES((index),(name.cString(using: String.Encoding.utf8)), (Dbl))”


                if(sqlite3_prepare_v2(sqlite3_db, sql.cString(using: String.Encoding.utf8)!, -1, &sqlite3_stmt, nil) != SQLITE_OK)
                {
                    print("Problem with prepared statement")


                }else{
                    sqlite3_finalize(sqlite3_stmt);
                    sqlite3_close(sqlite3_db);
                }
            }


        } catch let err as NSError {
            print("Error: (err.domain)")
        }


    }

This query is similar to the previous one, except that I am using interpolation to construct a new query String.

Insert or Rollback

The rollback option provides you with an elegant way to back out of a transaction if things don’t go your way, like, for instance, if there is an issue with the data being inserted into the column.

The OR REPLACE is shorthand for the ON CONFLICT REPLACE clause, just like the other conflict-handling clauses: ABORT, REPLACE, IGNORE, FAIL. For instance, you may not want to replace an existing record. If a duplicate record already exists in the database, you may want to roll back that transaction without generating an error. For general syntax, it is as follows:

let sql:String = "INSERT or ROLLBACK INTO table(coltext, colint, coldouble) VALUES(?,?,?)";

Again, the query assumes that you will be using SQLite3 data binding to bind data to the values, which is the safest way or pattern to use when working with SQLite3.

Insert or Ignore

The IGNORE clause handles the insert constraint by skipping over the problematic row altogether and generates an SQLITE_CONSTRAINT error like the other constraint clauses, except for REPLACE. The rows before and after are treated normally unless another constraint is encountered. See the following example:

let sql:String = "INSERT or IGNORE INTO table(coltext, colint, coldouble) VALUES(?,?,?)";

Insert or Abort

The ABORT clause aborts the current operation and backs out of the current transaction, allowing your program to continue to handle the other potential inserts or to continue with the runtime logic. The syntax is the same as for the other constraint clauses:

let sql:String = "INSERT or ABORT INTO table(coltext, colint, coldouble) VALUES(?,?,?)";

Insert or Fail

The last clause is FAIL. This clause is triggered when a constraint is encountered and SQLITE_CONSTRAINT is called. The previous transactions are maintained, but the current one is cancelled, and subsequent transactions do not occur. Here is an example:

let sql:String = "INSERT or FAIL INTO table(coltext, colint, coldouble) VALUES(?,?,?)";

Inserting Blobs

Working with binary data in Swift and the Cocoa Touch framework implies that you will be using the NSData class on the iOS side and blob on the SQLite side. In this section, we will look at two functions that will allow you to insert any binary data into a SQLite database. While this is technically possible, I wouldn’t recommend using this in a production app, because it would use up a tremendous amount of valuable storage. There might be some instances where you need to provide binary data as part of your part, and this data wouldn’t increase in size over time.

The following two functions can be used to insert binary data like videos, images, and audio. Of the two, the sqlite3_bind_blob function will be most always be used. The sqlite3_bind_zeroblob is used to write blobs incrementally into the database instead of storing the full size of the blob into memory all at once. We will demonstrate both:

int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*));
int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);

The first parameter is the SQLite INSERT statement, the second parameter is the index of the parameter to be set, and the third is the value to bind to the statement. If this value is nil, then the fourth parameter is ignored; otherwise, this parameter is the amount of bytes in the buffer, which represents the length of the binary data. The last, or fifth, parameter is a destructor to dispose of the blob once it has been inserted into the database. The following code provides a reference implementation:

func insertBlob(){
        var dbPath:URL = URL()
        var bindata:Data = Data()
        var imagePath:String = ""
        var urlObj:URL = URL()
        let binName:String = "Test image"
        let dirManager = FileManager.default()
        do {
            let directoryURL = try dirManager.urlForDirectory(FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)


            dbPath = directoryURL.urlByAppendingPathComponent("database.sqlite")
            if( sqlite3_open(dbPath.absoluteString?.cString(using: String.Encoding.utf8)!,&sqlite3_db) == 0){
                let sql:String = "Insert into binaryTbl(PictureName, ImageData) VALUES(?,?)"


                if(sqlite3_prepare_v2(sqlite3_db, sql.cString (using: String.Encoding.utf8)!, -1, &sqlite3_stmt, nil) != SQLITE_OK)
                {
                    print("Problem with prepared statement")
                }else{
                    imagePath = Bundle.main.pathForResource("IMG_0095", ofType: "JPG")!
                    urlObj = URL(fileURLWithPath: imagePath)
                    bindata = try! Data(contentsOf: urlObj)!
                    sqlite3_bind_text(sqlite3_stmt, 1, binName.cString (using: String.Encoding.utf8)!, -1, SQLITE_TRANSIENT);
                    sqlite3_bind_blob(sqlite3_stmt, 2, bindata.bytes,Int32(bindata.count), SQLITE_TRANSIENT);
                   if(sqlite3_step(sqlite3_stmt)==SQLITE_DONE){
                        sqlite3_finalize(sqlite3_stmt);
                        sqlite3_close(sqlite3_db);
                    }
        }
            }
} catch let err as NSError {
            print("Error: (err.domain)")
        }
     }

As you can see from the preceding code, the pattern is similar to the other SQLite code snippets. First, we create either our variables using the var keyword or a constant using the let keyword. If you make a mistake, Xcode will let you know and offer a suggestion to fix it. The visibility of the variable (or constant) depends on your needs.

Once the variables and constants are created, you attempt to open the SQLite database as usual and set up your SQL query. You execute the query using the sqlite3_prepare_v2 function, and you bind your data to the right columns.

Binding binary is similar to binding other data types, with a few extra steps. To work with binary data (images, videos, compiled files), you need to use NSData, which is a class in the Foundation framework that provides wrappers for the bytes buffers.

When you are storing binary data, you are actually storing bytes. To get the bytes, use the NSURL class to retrieve the path to the file and read in the bytes from the file using the NSData contentsOfURL. The sqlite3_bind_blob needs the bytes contents of the file as well as the length of the bytes buffer in addition to a pointer to the sqlite3_statement, column position, and the SQLITE_TRANSIENT pointer.

Creating the Winery App

The Winery iPhone app will allow a user to enter information about their favorite bottles of wine and take a photo of the label or bottle. The information will be stored in a SQLite database. In later chapters, we will expand on the app to include other CRUD (Create or Insert, Read or Select, Update, and Delete) operations.

In this chapter, I will create the initial app and add the insert capabilities for the wine information and wineries information. The app will be based on the Tabbed Application template.

Create the Project

From Xcode, create a new project and select the Tabbed Application template under the iOS Application category.

Note

If you are new to Xcode, you can create a project from the launcher if no projects are currently open, or from File menu ➤ New ➤ Project.

Add the Bridge

As always, the first step after creating the project is to create an Objective-C bridge. You could create a bridge individually for each project, or you can use create a share resource copy it to work project. You can also use CocoaPods (CocoaPods.org) to install the sqlite3 library and the bridge in your new project. You could also use a project from the CocoaPods repository.

Within the scope of this project, I will simply add the sqlite3 library to my project and manually create a bridge file.

To add the sqlite3 iOS-supported library, follow these steps:

  1. From the General Settings tab, locate the Linked Framework and Libraries section.

  2. Click the “+” button and enter “sqlite3” in the Search field.

  3. Select the libsqlite3.tbd library.

  4. Click Add to add the library to your project.

  5. Next, add a new Objective-C header file using the Header File template.

  6. Add an #import <sqlite3.h > statement.

  7. Under Build Settings, search for Swift.

  8. Under the Objective-C Bridge Header entry, add the name of the bridge file preceded by the project folder name, unless you place the file in the root directory of the project (Figure 5-1).

    A427374_1_En_5_Fig1_HTML.jpg
    Figure 5-1. Bridge file Location

Alternatively, if there is no bridge file in your project, do the following:

  1. Select the Objective-C File template under the iOS Source category (Figure 5-2).

    A427374_1_En_5_Fig2_HTML.jpg
    Figure 5-2. Objective-C File template
  2. Provide a name, such as SQLiteBridge (Figure 5-3).

    A427374_1_En_5_Fig3_HTML.jpg
    Figure 5-3. Name and file type
  3. Select the Extension file type and NSObject as the class.

  4. Once you click Next and Add, Xcode will ask you to create a bridge file (Figure 5-4).

    A427374_1_En_5_Fig4_HTML.jpg
    Figure 5-4. Create Bridge popup
  5. When you click on the Create Bridging Header button, Xcode will generate the header file and add the reference to the Build Settings for you (Figure 5-5). You can discard the NSObject_SQLiteBridge.h file, as it is not needed. The actual bridge file is TheWinery-Bridging-Header.h, and it is this file that is included in the Swift Compiler settings for the Objective-C header mapping.

    A427374_1_En_5_Fig5_HTML.jpg
    Figure 5-5. Xcode-created bridge header

Creating the UI View for Inserting

Before getting into the data model and controllers, I will build the UI and add the IBOutlets and IBActions to the FirstViewController and SecondViewController.

Without adding any other code, you can run the app as is and test Swift between the first and second scenes. Xcode provides a lot of boilerplate code through the template for us.

Figure 5-6 provides a view of the layout of the view controllers and navigation controller along with the components that we will add next.

A427374_1_En_5_Fig6_HTML.jpg
Figure 5-6. Winery UI in Xcode IB

The first view controller will include the following UI components :

Element

Name

Connection Type

UIImageView

imageView

IBOutlet

UITextField

wineNameField

IBOutlet

UITextField

countryNameField

IBOutlet

UISlider

wineRating

IBAction

UITextField

selectWineryField

IBOutlet

UIButton

InsertRecordAction

IBAction

UIButton

selectPhoto

IBAction

UIButton

selectWinery

IBAction

UILabel

Enter Wine Name

 

UILabel

Enter Country

 

UILabel

Select Winery

 

UILabel

Select Rating

 

To get started, select the FirstViewController, and from the Component Pallet on the lower right side of Xcode, select the ImageViewer (UIImageView) and drop it on the IB canvas. Next, select and open the Constraints tool in the bottom right of the IB window and click on the top and left beams; select both the width and height constraints (Figure 5-7). Click on the Add 4 Components button to set the constraints.

A427374_1_En_5_Fig7_HTML.jpg
Figure 5-7. Constraints in IB for ImageViewer

Continue to build the layout by adding labels (UILabel) and fields (UITextField) to the UI, like in Figure 5-6. Also add a slider to set the rating, and finish off by adding three buttons (UIButton): one to take the photo, one to display a UIPickerView of possible wineries, which will have to populate first before adding wines, and one button to insert the record.

As before, select the various components and set the constraints so that they will adapt to the target device. You will need to repeat the process for the SecondViewController. This second scene will allow a user to enter new wineries to the database.

The second view controller will include the following elements:

Element

Name

Connection Type

UITextField

EnterWineryField

IBOutlet

UITextField

EnterCountryField

IBOutlet

UITextField

EnterRegionField

IBOutlet

UILabel

Enter Winery Name

 

UILabel

Enter Country

 

UILabel

Enter Region

 

UIButton

InsertWineryBtn

IBAction

Before moving on to the data model , we need to create connections for the UI IBOutlet and IBAction elements. To create the connections, first select the FirstViewController scene and click on the Identity inspector, which will open the FirstViewController file next to the IB window (Figure 5-8).

A427374_1_En_5_Fig8_HTML.jpg
Figure 5-8. Adding an IBOutlet to the FirstViewController

Select the imageViewelement and hold down the Control key while dragging it to the open file. When you release the mouse, a popup will appear allowing you to enter the name of the imageView element. Click the Connect button, which will create the property.

Repeat the same process for the remaining UITextFields. For the selectWineryField UITextField, select Attribute inspector and unselect the “Enable” property for the Read Only field. The buttons are IBAction so you need to perform an extra step in the Connection popup. For the IBAction, you need to change the connection type from outlet to action.

Once you have finished the connections for the first view controller, repeat the process with the second view controller. Select the SecondViewController and open the Identity inspector corresponding to the scene and drag and drop the connections. I will add the logic later in the chapter.

Creating the Data Model

Creating our data model requires a series of steps, which are outlined in the following sections.

Add the Wineries Database

With the bridge set up, I will add the Wineries.sqlite database. I will create this database through the AppDelegate class in the didFinishWithOptions function. The code is provided later. The pattern is quite simple, and we have seen this code at several points in the preceding chapters.

I get a handle on the Documents directory using the FileManager.SearchPathDirectory. documentDirectoryproperty through the URLForDirectory function of the FileManager class. I then append the wineries.sqlite file name to the dbPath variable and pass this value to the sqlite3_open function along with the pointer to the sqlite3 database engine.

Each time the app runs after the first time, sqlite3 will simply open the database and establish a connection.

var srcPath:URL = URL.init(fileURLWithPath: "")
        var destPath:String = ""
        let dirManager = FileManager.default
        let projectBundle = Bundle.main


        do {
            let resourcePath = projectBundle.path(forResource: "thewinery", ofType: "sqlite")
            let documentURL = try dirManager.urls(for: .documentDirectory, in: .userDomainMask)


            srcPath = URL(fileURLWithPath: resourcePath!)

            destPath = String(describing: documentURL)

            if !dirManager.fileExists(atPath: destPath) {

                try dirManager.copyItem(at: srcPath, to: URL(fileURLWithPath: destPath))

            }

        } catch let err as NSError {
            print("Error: (err.domain)")
        }

Add the Wine Type

import Foundation

class Wine: NSObject {
    var id:Int32 = 0
    var name:String = ""
    var rating:String = ""
    var image:Data = Data()
    var producer:Int32 = 0


    override init(){

    }
}

Add the Wineries Type

import Foundation

public class Wineries:NSObject{
    var id:Int32 = 0
    var name:String = ""
    var country:String = ""
    var region:String = ""
    var volume:Double = 0.0
    var uom:String = ""


    override init(){

    }
}

Add the Database Schema

For the schema proper, I will use a script file, wineries.sql, that I created using the Empty file template under the iOS category from the Other section. I added the following SQL script to create two tables:

  • Wine

  • Wineries

CREATE TABLE IF NOT EXISTS main.wineries(
    id integer primary key autoincrement not null,
    name varchar,
    country varchar,
    region varchar,
    volume float,
    uom varchar
)


CREATE TABLE IF NOT EXISTS main.wine(

    id integer primary key autoincrement not null,
    name varchar,
    rating integer
    producer_id integer foreign key references wineries(id)
)

To build the table schema for inserts, I will read the file and execute the query using the sqlite3_prepare_v2 function along with the sqlite3_step and sqlite3_finalize functions. I will provide the code to execute these SQL scripts in the next section for the WineryDAO controller class.

Creating the Controllers

In this section, I will create the controllers.

Add the WineryDAO Class

To create this class, select the Swift file template from the New File interface under the iOS > Source category. Name the file WineryDAO and add the file to the project. In the class, add the NSObject subclass and the following functions:

  • buildSchema

  • createOrOpenDatabase

  • insertWineRecord

  • insertWineryRecord

The class signature should resemble this:

class WineryDAO: NSObject{}

Before getting to the functions in this class, we need to define some variables, which are listed here. The dbName is the SQLite database file name; the db is the pointer to the SQLite instance; the sqlStatement is the pointer for the sqlite3_statement instance; errMsg is an UnsafeMutablePointer to capture any operational errors thrown by SQLite; sqlite_static and sqlite_transient are unsafeBitCast pointers; dbPath is the path to the SQLite database file in the sandbox; and errStr is a String variable to manage error messages.

let dbName:String=" winery.sqlite "
var db:COpaquePointer?=nil
var sqlStatement:COpaquePointer?=nil
var errMsg: UnsafeMutablePointer<UnsafeMutablePointer<Int8>>? = nil
internal let SQLITE_STATIC = unsafeBitCast(0, to:sqlite3_destructor_type.self)
internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to:sqlite3_destructor_type.self)
var dbPath:NSURL = URL()
var errStr:String = ""

The init() function init()

The init function is a standard initializer in the Swift language. As you might imagine, it allows the program to set up any variables as the instance of the class is created and loaded into memory. To use the init function , you need to override it. In this class, I use the init to ensure that the database path is set. I could have also added sqlite3_open to actually open the database, but opted instead to place that operation in its own function.

Remembering the corruption issue I mentioned in chapter 3, if you open the same database file more than once at the same time, hence having different threads, you risk corrupting the database. The safest way to ensure that there are no corruption issues is to open and close the database after each operation. The other way is to use sqlite3_open_v2, which allows you to set some additional parameters. Using this variation of the function, you can specify if the database will be opened in read mode or read/write mode, and you can use the SQL_OPEN_FULLMUTEX flag to open the database in serialized mode, which offers the most protection against file corruption. The other multi-threading option is SQLITE_OPEN_MUTEX, which also opens the database in a multi-thread mode as long as the single-thread mode was not set when the database was first created. The default is SQLITE_OPEN_NOMUTIEX, which is single-thread operation.

For this app, I will open and close the database with each operation.

  override init() {
        /*
         Create SQLite Winery.sqlite database in Documents directory


         */
let dirManager = FileManager.default()
        do {
            let directoryURL = try dirManager.urlForDirectory(FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
dbPath = try! directoryURL.urlByAppendingPathComponent(dbName)
} catch let err as NSError {
            print("Error: (err.domain)")
        }
    }

The buildSchema Function

With this app, I wanted to try a different approach to building a database schema, so I decided to add the schema definition to a file that is loaded when the app is loaded. This function retrieves the .sql file from the Resource directory and executes the queries, which were provided in the previous section. I am using the sqlite3_exec function as it nicely encapsulates the different operations to execute a query, namely sqlite3_prepare_v2, sqlite3_step, and sqlite3_finalize. Once the query is executed, the database is closed.

The function is called from the AppDelegate when the app is loaded:

func buildSchema()->Void{
        if let filepath = Bundle.main.pathForResource("wineries", ofType: "sql") {
            do {
                let script = try NSString(contentsOfFile: filepath, usedEncoding: nil) as String
                print(script)
                if sqlite3_open(dbName, &db)==SQLITE_OK {
                    if sqlite3_exec(db, script.cString(using: String.Encoding.utf8)!, nil, nil, errMsg) != SQLITE_OK{
                        print( errStr = String (cString: sqlite3_errmsg(db))!)
                    }
                }else{
                    print("Could not open database " + String(cString.sqlite3_errmsg(db))!)
                }
            } catch let error as NSError {
                print(error.localizedDescription)
            }
        } else {
            print("file not found")
        }
sqlite3_close(db)
}

The createOrOpenDatabase Function

The createOrOpenDatabase function is called from the AppDelegate function when the app is loaded to ensure that the database is present and the schema script is executed. We could encapsulate this operation with "file exists" check to not unnecessarily open and execute the schema script every time the app is loaded.

func createOrOpenDatabase()->Enums.SQLiteStatusCode{
 return Enums.SQLiteStatusCode(rawValue: sqlite3_open(dbPath.absoluteString!.cString (using: String.Encoding.utf8)!, &db))!
    }

The insertWineRecord Function

This function is called from the FirstViewController through the insertRecordAction IBAction. First, we define an insert query and assign it to a constant called sql. Next, we open the database that is needed and ensure that it is open with the SQLITE_OK status code before executing the sqlite3_prepare_v2 function, which takes the sqlite pointer, the sql query, and the sqlStatement pointer as arguments.

The special feature of this function is wine.image, which converts NSData into a blob for insertion into the database. To convert it, we need to get the bytes from the NSData and then supply the length of the bytes and convert this to an Int32.

After opening the database and preparing the query statement, we bind the input values to the columns using the appropriate binding functions for the required data type. We execute the query using the sqlite3_step function and then clean up the operation using the sqlite3_finalize function. All that remains is to close the database using the sqlite3_close function:

    func insertWineRecord(_ wine:Wine)->Enums.SQLiteStatusCode{
        let sql:String = "INSERT INTO main.wine VALUES(?, ?, ?, ?)"


        if(sqlite3_open(dbPath.path!, &db)==SQLITE_OK){
            if(sqlite3_prepare_v2(db, sql.cString (using:String.Encoding.utf8)!, -1, &sqlStatement, nil)==SQLITE_OK){
                sqlite3_bind_value(sqlStatement, 1, nil)
                sqlite3_bind_text(sqlStatement, 2, wine.name.cString(using: String.Encoding.utf8)!, -1, SQLITE_TRANSIENT)
                sqlite3_bind_int(sqlStatement, 3, wine.rating)
                sqlite3_bind_int(sqlStatement, 4, wine.producer)
                sqlite3_bind_blob(sqlStatement, 5, wine.image.bytes,Int32(wine.image.count), SQLITE_TRANSIENT)
                sqlite3_step(sqlStatement)
                sqlite3_finalize(sqlStatement)
            }
}else{
            print(String(cString: sqlite3_errmsg(db))!)
            return Enums.SQLiteStatusCode.error
        }
         sqlite3_close(db)
        return Enums.SQLiteStatusCode.ok
    }

The insertWineryRecord Function

This function inserts winery records from the SecondViewController via the insertWineryBtn. The pattern of this function is identical to the function to insert wines, except for the binding columns, which include binding values for integers as well as for doubles and strings. As we have seen before, the strings need to be converted to string chars using the cStringUsingEncoding property and NSUTF8StringEncoding:

        func insertWineryRecord(_ vintner:Wineries) -> Enums.SQLiteStatusCode {
        let sql:String = "INSERT INTO main.winery VALUES(?, ?, ?, ?, ?)"
        if(sqlite3_open(dbPath.path!, &db)==SQLITE_OK){
            if(sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &sqlStatement, nil)==SQLITE_OK){
                sqlite3_bind_value(sqlStatement, 1, nil)
                sqlite3_bind_text(sqlStatement, 2, vintner.name.cString(using: String.Encoding.utf8)!, -1, SQLITE_TRANSIENT)
                sqlite3_bind_text(sqlStatement, 3, vintner.country. cString(using: String.Encoding.utf8)!, -1, SQLITE_TRANSIENT)
                sqlite3_bind_text(sqlStatement, 4, vintner.region. cString(using: String.Encoding.utf8)!, -1, SQLITE_TRANSIENT)
                sqlite3_bind_double(sqlStatement, 5, vintner.volume)
                sqlite3_bind_text(sqlStatement, 6, vintner.uom. cString(using: String.Encoding.utf8)!, -1, SQLITE_TRANSIENT)
                sqlite3_step(sqlStatement)
                sqlite3_finalize(sqlStatement)
            }
        }else{
            print(String(cString: sqlite3_errmsg(db))!)
            return Enums.SQLiteStatusCode.error
        }
        sqlite3_close(db)
        return Enums.SQLiteStatusCode.ok
    }

The FirstViewController

All iOS UIs have a view controller associated with the scenes in the storyboard. When the app was created, the template created a view controller for each of the scenes in the storyboard.

To implement the UIImagePickerController and the UIPickerView, the app will need to implement UIImagePickerControllerDelegate, UIPickerViewDelegate, and UIPickerViewDataSource delegates and data sources. With each of these, the app needs to implement a certain number of required functions, which we will look at later.

From the code that follows, you can see that we implement several variables and constants. The imageSelector variable is a UIImagePickerController type. It is used to present the UIImagePicker, which will use the building camera as its data source. The imageData is a variable that holds the image data from the camera. It is an NSData type that handles binary data on the iOS platform. The dbDAO is an instance of the WineryDAO that the app will use to interface with the database. wine and vintner are instances of the Wine and Wineries classes that represent the data. The wineriesArray is a mutable array of the Wineries type. This array is the data source for the UIPickerView. Finally, the IBOutlets were discussed earlier and are the connections in the UI.

class FirstViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIPickerViewDelegate, UIPickerViewDataSource
var imageSelector: UIImagePickerController!
var imageData:Data = Data()
var dbDAO:WineryDAO = WineryDAO()
var wine: Wine = Wine()
var wineriesArray = [Wineries]()
var wineriesPickerView: UIPickerView = UIPickerView()
var vintnor:Wineries = Wineries()
@IBOutlet weak var selectWineryField: UITextField!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var wineNameField: UITextField!
@IBOutlet weak var countryNameField: UITextField!

Add Photo Capture Functionality

As we have seen in the "Creating the UI for Inserting" section earlier, we need to be able to capture and insert images (preferably wine-related ones). We have set up the UI and added the IBActions, and now we need to add the logic to the FirstViewController.

We start by initializing the imageSelector object and setting its delegate to the FirstViewController, specifying that its source will be the building camera. Finally, the picker is loaded onto the view stack using the presentViewController:

@IBAction func takePhoto(_ sender: AnyObject) {
        imageSelector =  UIImagePickerController()
        imageSelector.delegate = self
        imageSelector.sourceType = .camera
        present(imageSelector, animated: true, completion: nil)
    }

The imagePickerController function is part of the image-picker protocol. It tells the delegate that a picture was selected. We need to call the dismissViewControllerAnimated to close the picker and return the selected image, which we store in the imageView.image property:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]){
        imageSelector.dismiss(animated: true, completion: nil)
        imageView.image = info[UIImagePickerControllerOriginalImage] as? UIImage
        imageData = UIImagePNGRepresentation(imageView.image!)!
}

Once we have the image stored in the imageView, it is passed to the imageData using UIImagePNGRepresentation, which takes imageView.image as a parameter. The image data is passed and stored in the database using insertRecordAction function, which we will look at next.

Add the Insert Function

The insertRecordActioncalls the dbDAO object, passing in the required parameters to insert it into the database. Most of the data used for the function comes from other functions and is assigned to the Wine instance class’s properties.

    @IBAction func insertRecordAction(_ sender: AnyObject) {

        dbDAO.insertWineRecord(wine))

    }

The viewDidLoad Function

This standard ViewController function is used after the scene is loaded onto the view stack or, in our case, when the app is launched. The function provides a channel to set up the wineriesPickerView by setting up the delegate and assigning the wineriesArray data source. Once the setup is complete, the UIPicker is added as a subview to the current view controller.

override func viewDidLoad() {
        super.viewDidLoad()
        //build data source


        self.wineriesPickerView.isHidden = true
        self.wineriesPickerView.dataSource = self
        self.wineriesPickerView.delegate = self
        self.wineriesPickerView.frame = CGRect(x:100, y:100, width: 100, height: 162)
        self.wineriesPickerView.backgroundColor = UIColor.black()
        self.wineriesPickerView.layer.borderColor = UIColor.white().cgColor
        self.wineriesPickerView.layer.borderWidth = 1
        self.wineriesArray = dbDAO.selectWineriesList()


        //other pickerView code like dataSource and delegate
        self.view.addSubview(wineriesPickerView)


    }

Add the Rating UISlider Functionality

The wineRating function manages the interaction with the UISlider. The sender argument returns the float value based on the user’s selection, and then this value is converted to an Int32 and assigned to the wine.rating property:

@IBAction func wineRatingSlider(_ sender: AnyObject) {
        let ratingValue:float_t = sender.value
          wine.rating  = Int32(ratingValue)    
}

The SecondViewController

The second view controller manages the entry of the wine producers. A user will need to add wineries before wines when the app is running. It is much simpler than the first view controller, as we have to deal with just five UITextFields that we defined with the UI. We call the insertWineryRecord method of the dbDAO class from the insertWineryBtn IBAction function, passing the values from the UITextFields. We create the winery instance and assign the values before passing the object as the only argument for the insertwineryRecord method. We could have simply passed the values for the UITextFields directly, but I will need to share those values later.

@IBOutlet weak var wineryNameField: UITextField!
@IBOutlet weak var countryNameField: UITextField!
@IBOutlet weak var regionNameField: UITextField!
@IBOutlet weak var enterVolume: UITextField!
@IBOutlet weak var enterUoM: UITextField!


var dbDAO:WineryDAO = WineryDAO()
var winery:Wineries = Wineries()


@IBAction func insertWineryBtn(_ sender: AnyObject) {
        winery.name = wineryNameField.text!
        winery.country = countryNameField.text!
        winery.region = regionNameField.text!
        winery.volume = Double(enterVolume.text!)!
        winery.uom = enterUoM.text!
        dbDAO.insertWineryRecord(winery)
    }
...
}

All that is left to do is run the app and insert some records.

Running the App

To finish up this chapter, let's fire up the app. To properly test this app and the camera, you need to deploy it on an iPhone. I am using iPhone 6 Plus. To deploy to an iPhone, you need to plug in your phone, provision it on the Apple Developer website, then open the settings on the iPhone. Under the General section, look for the Device Management section and click on "Trust link and enable the app for development."

Inserting Records

It is best to start by entering wineries so that you have some data to choose from in the wineries picker in scene one. With the app running, tap the Winery button in the menu bar.

Inserting Wineries

Figure 5-9 is the Wineries scene. Enter a winery and tap the Insert Winery button.

A427374_1_En_5_Fig9_HTML.jpg
Figure 5-9. The Winery scene

Inserting Wines

When using the camera, the device will ask you for permission to access the camera. Click OK to proceed (Figure 5-10).

A427374_1_En_5_Fig10_HTML.jpg
Figure 5-10. Access camera from app

Once you have accessed the camera for the first time, you will be presented with the camera as usual. I took a photo of a Chateauneuf du Pape—la fiole du pape (Figure 5-11). Click on the shutter button. You can cancel to opt to keep the photo. If you accept the photo, it will appear in the UIImage viewer. Enter the remaining information and click the Insert Wine button.

A427374_1_En_5_Fig11_HTML.jpg
Figure 5-11. Entering wine information with image

Summary

That’s it for this chapter. We explored the SQLite INSERT API including the OR clause. I provided sample code to demonstrate how to handle binary data like images, videos, and audio files, and we started to build the Winery app, which will demonstrate the general SQL CRUD operations in SQLite.

In the next chapter, we will continue to build the Winery app by adding SELECT operations to the app and testing the selection process.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset