神刀安全网

Managing SQLite Database with SwiftyDB

Choosing a way for storing data permanently is something that is always needed when developing applications. There are various options we can pick from: To create single files, to use CoreData or create a SQLite database. The last option includes some extra hassle as the database must be created first, and all tables and fields to be already defined before an app uses them. Furthermore, and from the programming point of view, it’s not that easy to manage a SQLite database when data needs to be stored, updated or retrieved.

Those problems seem to disappear when using a relatively new library that was popped on the GitHub called SwiftyDB . It’s a third-party library, which, as the creator says, is a plug-and-play component indeed. SwiftyDB reliefs developers from the hassle of creating a SQLite database manually, and from defining all required tables and fields. That happens automatically by the library using the properties of the class (or classes) used as data models. Besides that, all database operations are performed under the hood, so developers can focus on the implementation of the app logic only. A simple yet powerful API makes it really a piece of cake when dealing with the database.

It’s necessary to mention though that you shouldn’t expect SwiftyDB to make miracles. It’s a reliable library that can do pretty well what it’s supposed to do, but there are still some missing features that are probably meant to be added in the (near or distant) future. However, it’s still remaing a great tool that deserves some special attention, therefore in this tutorial we’ll go through the basics of SwiftyDB.

For your reference, you can find its documentation here , which you should definitely see after having gone through this text. If you were always willing to work with SQLite databases but hesitated to do so, I believe that SwiftyDB is a good start to do that.

With the above said, let’s get going with the exploration of a new, quite promising tool.

About the Demo App

In our post today we are going to create a really simple note taking app capable of doing all the basic operations one would expect:

  • List notes
  • Create new notes
  • Update existing notes
  • Delete notes

Apparently, SwiftyDB is going to take charge of managing the data into an SQLite database. All the listed operations above are good enough to demonstrate everything you need so can get started working with SwiftyDB easily.

To make it possible for us to stay into the point, I’ve created a starter project which you should download and begin with. When you get it, open it in Xcode and navigate around so you get acquainted with it. As you’ll see, all the basic functionality is there, with the most of the data-related features to be missing. It would be great if you would also run the project at least once, so you see what is all about.

The app is navigation based, and in the first view controller there’s a tableview where all notes are meant to be listed.

Managing SQLite Database with SwiftyDB

By tapping on an existing note we’ll be able to edit it and update it, and when swiping to the left we’ll be able to delete it:

Managing SQLite Database with SwiftyDB

Composing a new note is possible by tapping on the plus (+) button on the navigation bar. In order to have a good enough working example, here’s a list of actions that we can perform when editing a note (new or existing one):

  1. Set title and body for the note.
  2. Change the font name.
  3. Change the font size.
  4. Change the text color.
  5. Import images to the note.
  6. Move the images around and position them to a different place.

All values matching to the above operations will be stored to the database. To make it a bit more clear for the last two parts especially, the actual images are going to be stored to the documents directory of the app, while we are going to save just the name and frame for each image to the database. But not just that; furthermore we are going to create a new class for managing images (with the details are coming later).

Managing SQLite Database with SwiftyDB

A last but important detail I have to say about is that even though you’re downloading a starter project to work on, by the end of the next part you’ll be having a workspace. That’s because we’ll use CocoaPods to download the SwiftyDB library, and any other dependencies that come along.

Please get going when you’re up to it, but first, close the starter project if you have already opened it in Xcode.

Installing SwiftyDB

The first thing we have to do is to download the SwiftyDB library and use it in our project. Simply getting the library files and adding them to the project isn’t going to work, so we have to perform the installation using the CocoaPods dependency manager. The process is simple, and you’ll be able to do it in no time at all, even if you have never used CocoaPods before. For you reference however, take a look to the previous link.

Installing CocoaPods

We are going to begin by installing CocoaPods to our system. If you have installed CocoaPods already please skip this step. If not then go ahead and open Terminal . Type the following command to install CocoaPods:

sudo gem install cocoapods
sudogeminstallcocoapods 

Press Return, provide your password and sit back while the download process is taking place. Don’t close Terminal once it’s finished, we are still needing it.

Installing SwiftyDB and Other Dependencies

Navigate to the folder where the starter project exists by using the cd command (still in Terminal):

cd PATH_TO_THE_STARTER_PROJECT_DIRECTORY
cdPATH_TO_THE_STARTER_PROJECT_DIRECTORY 

It’s now time to create a Podfile that describes the library (or libraries) we want to download to CocoaPods. The easiest way to do that is by typing the following command and let CocoaPods create one for us:

pod init
podinit 

A new file named Podfile will be created to the project folder. Open it using a text editor (preferably not TextEdit), and modify it according to the next snippet:

use_frameworks!  target 'NotesDB' do     pod "SwiftyDB" end
use_frameworks!   target 'NotesDB' do     pod "SwiftyDB" end 

Managing SQLite Database with SwiftyDB

The line that will actually do the whole job is the pod "SwiftyDB" . CocoaPods will download SwiftyDB library and all of its dependencies by using that line, and it will create some new subfolders along with an Xcode workspace.

Once you finish editing the file, save and close it. Then, make sure that you’ve closed the starter project on Xcode and return to Terminal. Type the next command:

pod install
podinstall 

Managing SQLite Database with SwiftyDB

Wait again for a few moments, and then you’re ready to go. Instead of opening the starter project, open the NotesDB.xcworkspace on Xcode this time.

Managing SQLite Database with SwiftyDB

Beginning With SwiftyDB – Our Model

Inside the NotesDB project there’s a file called Note.swift , but it’s currently empty. This is our entry point today, as we’re going to create a couple of classes that will represent a note entity programmatically. In a more theoretical level, our upcoming work here consists of the Model part in the iOS MVC pattern.

What we need initially is to import the SwiftyDB library, so as you guess go to the top of the file and add this line:

import SwiftyDB
import SwiftyDB 

Now, let’s declare our most important class in this project:

class Note: NSObject, Storable {  }
class Note: NSObject, Storable {   } 

When working with SwiftyDB there are a few but specific rules to follow, and in the above class header line you can see two of them:

  1. A class with properties meant to be stored in a database using SwiftyDB must be a subclass of the NSObject class.
  2. A class with properties meant to be stored in a database using SwiftyDB must adopt the Storable protocol (it’s a SwiftyDB protocol).

Now we have to think of the properties we want to have in this class, and here it comes again a new rule from SwiftyDB: The datatypes of the properties must be any of those listed here in order to be able to load entire Note objects, instead of simple data (array with dictionaries) when retrieving data from the database. If there are properties with “incompatible” datatypes, then we’ll have to take extra actions so we convert them into the suggested ones (we’ll see that in details in just a while). By default, properties with such datatypes are simply ignored by SwiftyDB when it’s about time to save to database, and no respective fields are created to the table. Also, we’ll give special treatment to any other properties in the class that we don’t really want to be stored to the database.

The last rule for now says that a class that conforms to the Storable protocol must necessarily implement the init method:

class Note: NSObject, Storable {          override required init() {         super.init()              } }
class Note: NSObject, Storable {          override requiredinit() {         super.init()              } } 

Now that we have all the info we need, let’s start declaring the properties of our class. Not all for now, as some of them require additional discussion. However, here are the basics:

class Note: NSObject, Storable {     let database: SwiftyDB! = SwiftyDB(databaseName: "notes")     var noteID: NSNumber!     var title: String!         var text: String!         var textColor: NSData!         var fontName: String!         var fontSize: NSNumber!         var creationDate: NSDate!         var modificationDate: NSDate!          ... }
class Note: NSObject, Storable {     let database: SwiftyDB! = SwiftyDB(databaseName: "notes")     var noteID: NSNumber!     var title: String!         var text: String!         var textColor: NSData!         var fontName: String!         var fontSize: NSNumber!         var creationDate: NSDate!         var modificationDate: NSDate!          ... } 

Needless to comment any of them, except for the first one. When that object gets initialised it will create a new database (named notes.sqlite ) if it doesn’t exist and it will create a table automatically. The table fields will match to the properties having a proper datatype. On the other hand, if the database already exists, it will just open.

As you might notice, the above properties describe a note and all the attributes we want to save (title, text, text color, font name and size, creation and modification dates), but there’s nothing there regarding the images that a note can possibly have. Intentionally, we are going to create a new class for images, where we’ll store two properties only: The frame and the image name.

So, still being in the Note.swift file, create the following class above or below the existing one:

class ImageDescriptor: NSObject, NSCoding {     var frameData: NSData!         var imageName: String! }
class ImageDescriptor: NSObject, NSCoding {     var frameData: NSData!         var imageName: String! } 

Note that the frame is represented as a NSData object in this class, and not as a CGRect . It’s necessary to do it that way, so we can easily store that value to the database later. You’ll see in a while how we are going to handle it, and you’ll also understand why we adopt the NSCoding protocol.

Back to the Note class, let’s declare an ImageDescriptor array as shown next:

class Note: NSObject, Storable {     ...              var images: [ImageDescriptor]!          ... }
class Note: NSObject, Storable {     ...              var images: [ImageDescriptor]!          ... } 

There’s a limitation that now is the best time to mention about, and that is the fact that SwiftyDB doesn't store collections to the database . In simple words, that means that our images array will never be stored to the database, so we have to figure out how to deal with this. What we are allowed to do is to use one of the supported datatypes (see the link I provided a little after the beginning of this part), and the most suitable datatype is NSData . So, instead of saving the images array to the database, we’ll be saving the following (new) property:

class Note: NSObject, Storable {     ...              var imagesData: NSData!          ... }
class Note: NSObject, Storable {     ...              var imagesData: NSData!          ... } 

But how are we supposed to go from the images array with ImageDescriptor objects to the imagesData NSData object? Well, the answer is by archiving the images array using the NSKeyedArchiver class and producing that way a NSData object. We’ll see later how this is really being done in code, but now that we know what we have to do, we must go back to the ImageDescriptor class and have some additions.

As you know, a class can be archived (in other languages also known as serialized ) if only all of its properties can be serialised too, and in our case this is possible, as the datatypes ( NSData and String ) of the two properties in the ImageDescriptor class are serialisable. However that’s not enough, as we also have to encode and decode them in order to successfully archive and unarchive respectively, and that’s why we actually need the NSCoding protocol. By using it we’ll implement the methods shown next (one of them is an init method), and we’ll properly encode and decode our two properties:

class ImageDescriptor: NSObject, NSCoding {     ...          required init?(coder aDecoder: NSCoder) {         frameData = aDecoder.decodeObjectForKey("frameData") as! NSData         imageName = aDecoder.decodeObjectForKey("imageName") as! String     }          func encodeWithCoder(aCoder: NSCoder) {         aCoder.encodeObject(frameData, forKey: "frameData")         aCoder.encodeObject(imageName, forKey: "imageName")     } }
class ImageDescriptor: NSObject, NSCoding {     ...          requiredinit?(coderaDecoder: NSCoder) {         frameData = aDecoder.decodeObjectForKey("frameData") as! NSData         imageName = aDecoder.decodeObjectForKey("imageName") as! String     }          func encodeWithCoder(aCoder: NSCoder) {         aCoder.encodeObject(frameData, forKey: "frameData")         aCoder.encodeObject(imageName, forKey: "imageName")     } } 

For more information about the NSCoding protocol and the NSKeyedArchiver class take a look here and here , it’s pointless to discuss more about them here and now.

In addition to all the above, let’s define a quite handy custom init method. It’s really simple, so no need to make any comments:

class ImageDescriptor: NSObject, NSCoding {     ...              init(frameData: NSData!, imageName: String!) {         super.init()         self.frameData = frameData         self.imageName = imageName     } }
class ImageDescriptor: NSObject, NSCoding {     ...              init(frameData: NSData!, imageName: String!) {         super.init()         self.frameData = frameData         self.imageName = imageName     } } 

At this point our first quick meeting with the SwiftyDB library comes to its end. Even though we didn’t do much SwiftyDB stuff, this part was necessary for three reasons:

  1. To create a class that will be used from the SwiftyDB.
  2. To learn about some rules applied when using SwiftyDB.
  3. To see some important limitations regarding the datatypes that can be saved in the database using SwiftyDB.

Note : If you’re seeing some errors right now on Xcode, then build the project once (Command-B) to get rid of them.

Primary Keys and Ignored Properties

It’s always recommended to use primary keys when dealing with databases, as such keys can help you uniquely identify records in the database tables and perform various operations by using them (for example, update a specific record). You can find here a good definition about what a primary key is.

It is easy to specify one or more properties of a class as the primary key (or keys) for the respective table in the database in SwiftyDB. The library provides the PrimaryKeys protocol, which should be implemented by all classes that the respective tables should have a primary key so their objects to be uniquely identified. The way to do that is quite straightforward and standard, so let’s get into the point straight away:

In the NotesDB project you’ll find a file named Extensions.swift . Click it on the Project Navigator so you open it. Add the following lines there:

extension Note: PrimaryKeys {     class func primaryKeys() -> Set<String> {         return ["noteID"]     } }
extension Note:PrimaryKeys {     class func primaryKeys() -> Set<String> {         return ["noteID"]     } } 

In our demo, we want the noteID property to be the only primary key in the respective table in the sqlite database. However, if more primary keys are required, then you just have to write them in the row separated by comma (for example, return ["key1", "key2", "key3"] ).

Besides that, not all properties of a class should always be stored to the database, and you should explicitly order SwiftyDB not to include them. For example, in the Note class we have two properties that are not going to be stored to the database (either because they cannot or we don’t want so): The images array and the database object. How do we explicitly exclude those two? By implementing another protocol that the SwiftyDB library provides called IgnoredProperties :

extension Note: IgnoredProperties {     class func ignoredProperties() -> Set<String> {         return ["images", "database"]     } }
extension Note:IgnoredProperties {     class func ignoredProperties() -> Set<String> {         return ["images", "database"]     } } 

If there were more properties we wouldn’t like to have to the database they should be added above too. For example, let’s say that we have the following property:

var noteAuthor: String!
var noteAuthor: String! 

… and that we don’t want it to be saved to the database. In that case, we should add it to the IgnoredProperties protocol implementation too:

extension Note: IgnoredProperties {     class func ignoredProperties() -> Set<String> {         return ["images", "database", "noteAuthor"]     } }
extension Note:IgnoredProperties {     class func ignoredProperties() -> Set<String> {         return ["images", "database", "noteAuthor"]     } } 

Note: Import the MARKDOWN_HASH6211c316cc840902a4df44c828a26fbeMARKDOWN_HASH library to the MARKDOWN_HASH1dbda56f2122b1744ebf59bb64bbffdfMARKDOWN_HASH file if you see any errors.

Saving a New Note

Having done the bare minimum implementation in the Note class, it’s time to turn to the functionalities of the demo app. We still haven’t added any methods to our new class; we’ll do so by following the implementation flow of all the missing functionalities.

So, the first thing we need is having notes, therefore the app must be told how to save them properly using the SwiftyDB and our two new classes. This is going to take place mostly in the EditNoteViewController class, so it’s about time to open the respective file using the Project Navigator. Before we write the first line of code here, I consider quite important to highlight the most of the properties found there:

  • imageViews : This array holds all the image view objects that contain images added to a note. Please don’t forget that this array exists; it’ll become handy in a while.
  • currentFontName : It holds the name of the currently applied font to the textview.
  • currentFontSize : It’s the font size of the text in the textview.
  • editedNoteID : The noteID value (primary key) of a note that is about to be updated. We’ll use it later.

Since the general functionality already exists in the starter project, what we have to do is to implement the missing logic in the saveNote() method. We’ll begin by doing two things: First we won’t allow saving if there is no text in the title or the body of the note. Second, we’ll dismiss the keyboard if it’s appeared by the time of saving:

func saveNote() {     if txtTitle.text?.characters.count == 0 || tvNote.text.characters.count == 0 {         return     }          if tvNote.isFirstResponder() {         tvNote.resignFirstResponder()     }    }
func saveNote() {     if txtTitle.text?.characters.count == 0 || tvNote.text.characters.count == 0 {         return     }          if tvNote.isFirstResponder() {         tvNote.resignFirstResponder()     }   } 

We’ll continue now by initializing a new Note object, and by assigning the right values to the proper properties. The images need special treatment, and we’ll do it right after.

func saveNote() {     ...          let note = Note()     note.noteID = Int(NSDate().timeIntervalSince1970)     note.creationDate = NSDate()     note.title = txtTitle.text     note.text = tvNote.text!     note.textColor = NSKeyedArchiver.archivedDataWithRootObject(tvNote.textColor!)     note.fontName = tvNote.font?.fontName     note.fontSize = tvNote.font?.pointSize     note.modificationDate = NSDate()     }
func saveNote() {     ...          let note = Note()     note.noteID = Int(NSDate().timeIntervalSince1970)     note.creationDate = NSDate()     note.title = txtTitle.text     note.text = tvNote.text!     note.textColor = NSKeyedArchiver.archivedDataWithRootObject(tvNote.textColor!)     note.fontName = tvNote.font?.fontName     note.fontSize = tvNote.font?.pointSize     note.modificationDate = NSDate()     } 

A few comments now:

  • The noteID property expects any Int number that will work as the primary key. You can create or generate any value you want as long as it’s unique. In this case we set the integer part of the current timestamp as our primary key, but generally this is not a good idea in real world applications as the timestamp contains too many digits. However for our demo application it’s just fine, as it also consists of the easiest option for having a unique int value.
  • As we save a note for first time, we set the current timestamp (expressed as a NSDate object) to both creation and modification date properties.
  • The only special action we definitely have to take is to convert the text color of the textview into a NSData object. This is achieved by archiving the color object using the NSKeyedArchiver class.

Let’s focus on how to save the images now. We will create a new method which will be fed with the image views array. We’ll perform two things in it: We’ll save the actual image of each image view to the documents directory of the app, and we’ll create an ImageDescriptor object for each one. Each such object will be appended to the images array.

In order to create this new method we are going to make a small detour, and to go back to the Note.swift file again. Let’s see the implementation first, and then we’ll discuss about it.

<

pre lang=”swift”>

func storeNoteImagesFromImageViews(imageViews: [PanningImageView]) {

if imageViews.count > 0 {

if images == nil {

images = ImageDescriptor

}

else {

images.removeAll()

}

for i in 0..&lt;imageViews.count {         let imageView = imageViews[i]         let imageName = "img_/(Int(NSDate().timeIntervalSince1970))_/(i)"          images.append(ImageDescriptor(frameData: imageView.frame.toNSData(), imageName: imageName))          Helper.saveImage(imageView.image!, withName: imageName)     }      imagesData = NSKeyedArchiver.archivedDataWithRootObject(images) } else {     imagesData = NSKeyedArchiver.archivedDataWithRootObject(NSNull()) }
    for i in 0..<imageViews.count {         let imageView = imageViews[i]         let imageName = "img_/(Int(NSDate().timeIntervalSince1970))_/(i)"           images.append(ImageDescriptor(frameData: imageView.frame.toNSData(), imageName: imageName))           Helper.saveImage(imageView.image!, withName: imageName)     }       imagesData = NSKeyedArchiver.archivedDataWithRootObject(images) } else {     imagesData = NSKeyedArchiver.archivedDataWithRootObject(NSNull()) }   

}

Here’s what is taking place in the above method:

  1. For starters we check if the images array is initialised or not. If it’s nil we initialise it, if not, we just remove any existing data from it. The second will become useful later, when we’ll be updating an existing note.
  2. Then for each image view we create a unique name for its image. Each name will be similar to this: “img_12345679_1”.
  3. We initialise a new ImageDescriptor object by using our custom init method and by providing the image view frame and the image name as parameters. The toNSData() method has been implemented as an extension of the CGRect and you can find it in the Extensions.swift file. Its purpose is to convert a frame to a NSData object. Once the new ImageDescriptor object is ready, it’s appended to the images array.
  4. We save the actual image to the documents directory. The saveImage(_: withName:) class method can be found in the Helper.swift file, along with couple more useful class methods.
  5. Lastly, when all image views have been processed, we convert the images array to a NSData object by archiving it and we assign it to the imagesData property. The last line above is the actual reason that the NSCoding protocol and the implementation of its methods are required in the ImageDescriptor class.

The else case above might seem reduntant, but it’s required. By default, the imagesData property is nil and it will remain like that if no images get added to the note. However, “nil” is not recognized by SQLite. What SQLite understands is the equivalent of NSNull , and that’s what we provide converted to a NSData object.

Back to the EditNoteViewController.swift file again to use what we just created:

func saveNote() {     ...          note.storeNoteImagesFromImageViews(imageViews) }
func saveNote() {     ...          note.storeNoteImagesFromImageViews(imageViews) } 

Let’s return now to the Note.swift file, and let’s implement the method that will perform the actual saving to the database. There’s something important you should know at this point: SwiftyDB provides the option to perform any database-related operation synchronously or asynchronously. Which method you should select depends on the nature of the app you build. However, I’d suggest to use the asynchronous method, as this won’t block the main thread while a database operation is in progress, and it won’t affect the user experience by freezing (even instantly) the UI. But I’m saying again, it’s totally up to you.

We’ll use the asynchronous way to save our data in here. As you’ll see, the respective SwiftyDB method contains a closure that returns the result of the operation. You can read details about that result object here , and actually I’m recommending to do so now.

Let’s implement now our new method so we can discuss more about it:

func saveNote(shouldUpdate: Bool = false, completionHandler: (success: Bool) -> Void) {     database.asyncAddObject(self, update: shouldUpdate) { (result) -> Void in         if let error = result.error {             print(error)             completionHandler(success: false)         }         else {             completionHandler(success: true)         }     } }
func saveNote(shouldUpdate: Bool = false, completionHandler: (success: Bool) -> Void) {     database.asyncAddObject(self, update: shouldUpdate) { (result) -> Void in         if let error = result.error {             print(error)             completionHandler(success: false)         }         else {             completionHandler(success: true)         }     } } 

It’s easy to understand from the above implementation that we’re going to use the same method for updating notes too. We make sure to set the shouldUpdate Bool value as a parameter to the method in advance, and then depending on its value the asyncDataObject(...) will either create a new record or it’ll update an existing one.

Furthermore, you see that the second parameter in our method is a completion handler. We call it with the proper parameter value, depending on whether the saving is successful or not. I’d suggest you to always use completion handlers when you have tasks running asynchronously on the background. That way, you’ll be able to notify the caller methods when the background task has finished, and transfer any possible results or data back.

What you see happening above is what you’ll see happening in other database-related methods too. We’ll always be checking for an error in the result, and we’ll proceed accordingly depending on whether there’s any or not. In the above case if there’s an error, we call our completion handler passing the false value, meaning that the saving was failed. If the opposite case, we pass true to indicate a successful operation.

Back to the EditNoteViewController class once again, let’s get finished with the saveNote() method. We’ll call the one created right above, and if the note saving has been successful we’ll just pop the view controller. If there’s an error, then we’ll display a message.

func saveNote() {     ...          let shouldUpdate = (editedNoteID == nil) ? false : true          note.saveNote(shouldUpdate) { (success) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if success {                 self.navigationController?.popViewControllerAnimated(true)             }             else {                 let alertController = UIAlertController(title: "NotesDB", message: "An error occurred and the note could not be saved.", preferredStyle: UIAlertControllerStyle.Alert)                 alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: { (action) -> Void in                                      }))                 self.presentViewController(alertController, animated: true, completion: nil)             }         })     } }
func saveNote() {     ...          let shouldUpdate = (editedNoteID == nil) ? false : true          note.saveNote(shouldUpdate) { (success) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if success {                 self.navigationController?.popViewControllerAnimated(true)             }             else {                 let alertController = UIAlertController(title: "NotesDB", message: "An error occurred and the note could not be saved.", preferredStyle: UIAlertControllerStyle.Alert)                 alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: { (action) -> Void in                                      }))                 self.presentViewController(alertController, animated: true, completion: nil)             }         })     } } 

Notice the shouldUpdate variable in the above implementation. It gets the proper value depending on whether the editedNoteID property is nil or not, meaning whether the note is being updated or not.

At this point you can run the app and try to save new notes. If you went step by step all along the way up to this point, then you’ll be able to save your notes without any problems.

Loading and Listing Notes

With the creation and saving of new notes being functioning, we can move on and make our app capable of loading saved notes from the database. The loaded notes are meant to be listed in the NoteListViewController class. However, before we start working in this class, let’s create first a new method in the Note.swift file for loading our data.

func loadAllNotes(completionHandler: (notes: [Note]!) -> Void) {     database.asyncObjectsForType(Note.self) { (result) -> Void in         if let notes = result.value {             completionHandler(notes: notes)         }                  if let error = result.error {             print(error)             completionHandler(notes: nil)         }     } }
func loadAllNotes(completionHandler: (notes: [Note]!) -> Void) {     database.asyncObjectsForType(Note.self) { (result) -> Void in         if let notes = result.value {             completionHandler(notes: notes)         }                  if let error = result.error {             print(error)             completionHandler(notes: nil)         }     } } 

The SwiftyDB method that performs the actual loading is the asyncObjectsForType(...) , and it works asynchronously. The result will contain either an error, or a collection with note objects (an array) loaded from the database. In the first case, we call the completion handler passing the nil value so as to indicate to the caller that there was a problem while loading the data. In the second case, we pass the Note objects to the completion handler so we can use them out of this method.

Let’s head to the NoteListViewController.swift file now. Initially, we must declare an array that will contain Note objects (those loaded from the database). It’s going to be the datasource of our tableview (obviously). So, at the top of the class add the following line:

var notes = [Note]()
var notes = [Note]() 

Besides that, initialize a new Note object as well, so we can use the loadAllNotes(...) method we created earlier:

var note = Note()
var note = Note() 

Time to write a really simple new method that will call the one above and load all stored objects from the database to the notes array:

func loadNotes() {     note.loadAllNotes { (notes) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if notes != nil {                 self.notes = notes                 self.tblNotes.reloadData()             }         })     } }
func loadNotes() {     note.loadAllNotes { (notes) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if notes != nil {                 self.notes = notes                 self.tblNotes.reloadData()             }         })     } } 

Notice that after all notes get loaded we use the main thread to reload the tableview. Prior to that of course we hold them to the notes array.

The above two methods are all we need for getting the stored notes from the database. That simple! Don’t forget though that the loadNotes() must be called somewhere, and this will happen in the viewDidLoad() method:

override func viewDidLoad() {     ...          loadNotes() }
override func viewDidLoad() {     ...          loadNotes() } 

Loading the notes is not enough, as we must use them once they’re fetched. So, let’s start updating the tableview methods, starting by the total number of rows:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {     return notes.count }
func tableView(tableView: UITableView, numberOfRowsInSectionsection: Int) -> Int {     return notes.count } 

Next, let’s display some note data to the tableview. To be specific, we’ll display the title, and the creation and modification dates for each note:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {     let cell = tableView.dequeueReusableCellWithIdentifier("idCellNote", forIndexPath: indexPath) as! NoteCell          let currentNote = notes[indexPath.row]          cell.lblTitle.text = currentNote.title!     cell.lblCreatedDate.text = "Created: /(Helper.convertTimestampToDateString(currentNote.creationDate!))"     cell.lblModifiedDate.text = "Modified: /(Helper.convertTimestampToDateString(currentNote.modificationDate!))"          return cell      }
func tableView(tableView: UITableView, cellForRowAtIndexPathindexPath: NSIndexPath) -> UITableViewCell {     let cell = tableView.dequeueReusableCellWithIdentifier("idCellNote", forIndexPath: indexPath) as! NoteCell          let currentNote = notes[indexPath.row]          cell.lblTitle.text = currentNote.title!     cell.lblCreatedDate.text = "Created: /(Helper.convertTimestampToDateString(currentNote.creationDate!))"     cell.lblModifiedDate.text = "Modified: /(Helper.convertTimestampToDateString(currentNote.modificationDate!))"          return cell      } 

If you run the app now, all notes that you have created so far will be listed to the tableview.

An Alternative Way To Fetch Data

Just a bit earlier we used the asyncObjectsForType(...) method of the SwiftyDB library to load our notes from the database. This method returns an array of objects (in our case Note objects) as you’ve seen, and I consider this to be quite handy. However, it’s not always that useful to retrieve objects from the database; there are cases where fetching an array with the actual data values would be more convenient.

SwiftyDB can help you on that, as it provides an alternative way for retrieving data. There’s a method called asyncDataForType(...) (or dataForType(...) if you want to make synchronous operations), and it returns a colletion of dictionaries in this form: [[String: SQLiteValue]] (where SQLiteValue is any of the allowed datatypes).

You can find more here and here . I leave it for you as an exercise to enrich the Note class and load simple data as well, instead of objects only.

Updating A Note

One of the features we want our demo app to have is the capability to edit and update an existing note. In other words, we need to present the EditNoteViewController with the details of a note that is being selected simply by tapping to the respective cell, and store to the database its modified data once it gets saved again.

Starting in the NoteListViewController.swift file, we need a new property for storing the ID of the selected note, so go to the top of the class and add the following line:

var idOfNoteToEdit: Int!
var idOfNoteToEdit: Int! 

Now, let’s implement the next UITableViewDelegate method, where we find the noteID value based on the selected row, and then we perform the segue to show the EditNoteViewController :

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {     idOfNoteToEdit = notes[indexPath.row].noteID as Int     performSegueWithIdentifier("idSegueEditNote", sender: self) }
func tableView(tableView: UITableView, didSelectRowAtIndexPathindexPath: NSIndexPath) {     idOfNoteToEdit = notes[indexPath.row].noteID as Int     performSegueWithIdentifier("idSegueEditNote", sender: self) } 

In the prepareForSegue(...) method let’s pass the value of the idOfNoteToEdit to the next view controller:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {         if let identifier = segue.identifier {         if identifier == "idSegueEditNote" {             let editNoteViewController = segue.destinationViewController as! EditNoteViewController                          if idOfNoteToEdit != nil {                 editNoteViewController.editedNoteID = idOfNoteToEdit                 idOfNoteToEdit = nil             }         }     } }
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {         if let identifier = segue.identifier {         if identifier == "idSegueEditNote" {             let editNoteViewController = segue.destinationViewController as! EditNoteViewController                          if idOfNoteToEdit != nil {                 editNoteViewController.editedNoteID = idOfNoteToEdit                 idOfNoteToEdit = nil             }         }     } } 

The half job has been done now. Before we switch to the EditNoteViewController class and continue there, let’s make another quick detour by paying a visit to the Note class in order to implement a simple new method that will retrieve just a single note using the ID value that is being given with. Here’s the implementation:

func loadSingleNoteWithID(id: Int, completionHandler: (note: Note!) -> Void) {     database.asyncObjectsForType(Note.self, matchingFilter: Filter.equal("noteID", value: id)) { (result) -> Void in         if let notes = result.value {             let singleNote = notes[0]                          if singleNote.imagesData != nil {                 singleNote.images = NSKeyedUnarchiver.unarchiveObjectWithData(singleNote.imagesData) as? [ImageDescriptor]             }                          completionHandler(note: singleNote)         }                  if let error = result.error {             print(error)             completionHandler(note: nil)         }     } }
func loadSingleNoteWithID(id: Int, completionHandler: (note: Note!) -> Void) {     database.asyncObjectsForType(Note.self, matchingFilter: Filter.equal("noteID", value: id)) { (result) -> Void in         if let notes = result.value {             let singleNote = notes[0]                          if singleNote.imagesData != nil {                 singleNote.images = NSKeyedUnarchiver.unarchiveObjectWithData(singleNote.imagesData) as? [ImageDescriptor]             }                          completionHandler(note: singleNote)         }                  if let error = result.error {             print(error)             completionHandler(note: nil)         }     } } 

The new thing here is that for first time we use a filter in order to apply limitations to the results that will be returned. Using the equal(...) class method of the Filter class is just a way to set the filter we want. Don’t miss to visit this link to see more ways for applying filters when fetching data or objects from the database.

By using the filter in the fashion shown above, we actually ask from SwiftyDB to load only those records where the noteID equals to the value given as a parameter to the method. Of course, just one record will be returned because we know that this field is the primary key and there’s no way to have more than one records with the same key.

The found results will be returned as an array of Note objects, so it’s necessary to get the first (and only) item from that collection. After that, we definitely have to convert the image data (if exists) to an array of ImageDescriptor objects, and assign it to the images property. That’s important, because if we skip it any images originally added to the loaded note won’t be shown.

At the end we call the completion handler according to whether the note fetching was successful or not. In the first case we pass the fetched object to the completion handler so it can be used by the caller, while in the second case we just pass nil as there’s no object.

Now we can head to the EditNoteViewController.swift file, and declare and initialize at the same time a new Note property to the class:

var editedNote = Note()
var editedNote = Note() 

This object will be used firstly for calling the new method we implemented above, and then to contain the loaded data from the database.

We’re just about to load the note specified by the editedNoteID property using the loadSingleNote(...) method. For our purpose, we are going to define the viewWillAppear(_:) method, and in there we’ll expand our logic.

As you will see in the following code snippet, all values will be populated properly once the loadSingleNoteWithID(...) method returns the fetched note through the completion handler. That means that we start setting the note title, body, text color, font, etc, but not only. If there are images included to the note, we’ll be creating images views for each one, using of course the frames specified in the ImageDescriptor objects.

override func viewWillAppear(animated: Bool) {     super.viewWillAppear(animated)          if editedNoteID != nil {         editedNote.loadSingleNoteWithID(editedNoteID, completionHandler: { (note) -> Void in             dispatch_async(dispatch_get_main_queue(), { () -> Void in                 if note != nil {                     self.txtTitle.text = note.title!                     self.tvNote.text = note.text!                     self.tvNote.textColor = NSKeyedUnarchiver.unarchiveObjectWithData(note.textColor!) as? UIColor                     self.tvNote.font = UIFont(name: note.fontName!, size: note.fontSize as CGFloat)                                          if let images = note.images {                         for image in images {                             let imageView = PanningImageView(frame: image.frameData.toCGRect())                             imageView.image = Helper.loadNoteImageWithName(image.imageName)                             imageView.delegate = self                             self.tvNote.addSubview(imageView)                             self.imageViews.append(imageView)                             self.setExclusionPathForImageView(imageView)                         }                     }                                          self.editedNote = note                                          self.currentFontName = note.fontName!                     self.currentFontSize = note.fontSize as CGFloat                 }             })         })     } }
override func viewWillAppear(animated: Bool) {     super.viewWillAppear(animated)          if editedNoteID != nil {         editedNote.loadSingleNoteWithID(editedNoteID, completionHandler: { (note) -> Void in             dispatch_async(dispatch_get_main_queue(), { () -> Void in                 if note != nil {                     self.txtTitle.text = note.title!                     self.tvNote.text = note.text!                     self.tvNote.textColor = NSKeyedUnarchiver.unarchiveObjectWithData(note.textColor!) as? UIColor                     self.tvNote.font = UIFont(name: note.fontName!, size: note.fontSize as CGFloat)                                          if let images = note.images {                         for image in images {                             let imageView = PanningImageView(frame: image.frameData.toCGRect())                             imageView.image = Helper.loadNoteImageWithName(image.imageName)                             imageView.delegate = self                             self.tvNote.addSubview(imageView)                             self.imageViews.append(imageView)                             self.setExclusionPathForImageView(imageView)                         }                     }                                          self.editedNote = note                                          self.currentFontName = note.fontName!                     self.currentFontSize = note.fontSize as CGFloat                 }             })         })     } } 

Don’t miss that after having populated all values, we assign the note to the editedNote object, so we can use it later on.

There is one last step required: To update the saveNote() method, so when a note is being updated to avoid creating a new Note object, and not to set a new primary key and creation date.

So, find those three lines (inside the saveNote() method):

let note = Note() note.noteID = Int(NSDate().timeIntervalSince1970) note.creationDate = NSDate()
let note = Note() note.noteID = Int(NSDate().timeIntervalSince1970) note.creationDate = NSDate() 

… and replace them with the following snippet:

let note = (editedNoteID == nil) ? Note() : editedNote  if editedNoteID == nil {     note.noteID = Int(NSDate().timeIntervalSince1970)     note.creationDate = NSDate() }
let note = (editedNoteID == nil) ? Note() : editedNote   if editedNoteID == nil {     note.noteID = Int(NSDate().timeIntervalSince1970)     note.creationDate = NSDate() } 

The rest part of the method remains as is (at least for now).

Updating the Notes List

If you tested the app up to this point, then you would have definitely realized that the notes list is not updated when you create a new note or when you update an existing one. That’s reasonable to happen because the app isn’t capable of that yet, however in this part we’re about to fix this unwanted behaviour.

As you may guess, we are going to use the Delegation pattern to notify the NoteListViewController class about changes made to notes in the EditNoteViewController . Our starting point is to create a new protocol in the EditNoteViewController with two required methods, those shown below:

protocol EditNoteViewControllerDelegate {     func didCreateNewNote(noteID: Int)          func didUpdateNote(noteID: Int) }
protocol EditNoteViewControllerDelegate {     func didCreateNewNote(noteID: Int)          func didUpdateNote(noteID: Int) } 

In both cases we provide to the delegate methods the ID value of the new or edited note. Now, go to EditNoteViewController class and add the following property:

var delegate: EditNoteViewControllerDelegate!
var delegate: EditNoteViewControllerDelegate! 

Finally, let’s revisit one last time the saveNote() method. At first locate the next line inside the completion handler block:

self.navigationController?.popViewControllerAnimated(true)
self.navigationController?.popViewControllerAnimated(true) 

Replace that single line with the following bunch of code:

if self.delegate != nil {     if !shouldUpdate {         self.delegate.didCreateNewNote(note.noteID as Int)     }     else {         self.delegate.didUpdateNote(self.editedNoteID)     } } self.navigationController?.popViewControllerAnimated(true)
if self.delegate != nil {     if !shouldUpdate {         self.delegate.didCreateNewNote(note.noteID as Int)     }     else {         self.delegate.didUpdateNote(self.editedNoteID)     } } self.navigationController?.popViewControllerAnimated(true) 

The proper delegate function will be called every time that a new note is created or an existing one is being updated from now on. But what we just did consists of the half job only. Let’s switch to the NoteListViewController.swift file, and first of all let’s adopt the new protocol to the header line of the class:

class NoteListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, EditNoteViewControllerDelegate {    ... }
class NoteListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, EditNoteViewControllerDelegate {   ... } 

Next, in the prepareForSegue(...) method let’s make this class the delegate of the EditNoteViewController . Right below the let editNoteViewController = segue.destinationViewController as! EditNoteViewController line add the next one as shown to this snippet:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {     if let identifier = segue.identifier {         if identifier == "idSegueEditNote" {             let editNoteViewController = segue.destinationViewController as! EditNoteViewController                          editNoteViewController.delegate = self  // Add this line.                          ...         }     } }
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {     if let identifier = segue.identifier {         if identifier == "idSegueEditNote" {             let editNoteViewController = segue.destinationViewController as! EditNoteViewController                          editNoteViewController.delegate = self  // Add this line.                          ...         }     } } 

Pretty nice, as the most of the job has been done. What we’re still missing is the implementation of the two delegate methods. First, let’s handle the situation where a new note has been created:

func didCreateNewNote(noteID: Int) {     note.loadSingleNoteWithID(noteID) { (note) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if note != nil {                 self.notes.append(note)                 self.tblNotes.reloadData()             }         })     } }
func didCreateNewNote(noteID: Int) {     note.loadSingleNoteWithID(noteID) { (note) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if note != nil {                 self.notes.append(note)                 self.tblNotes.reloadData()             }         })     } } 

As you see, we just fetch from the database the object specified by the noteID parameter value, and (if exists) we append it to the notes array and reload the tableview.

Let’s see now the next one:

func didUpdateNote(noteID: Int) {     var indexOfEditedNote: Int!          for i in 0..<notes.count {         if notes[i].noteID == noteID {             indexOfEditedNote = i             break         }     }          if indexOfEditedNote != nil {         note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in             if note != nil {                 self.notes[indexOfEditedNote] = note                 self.tblNotes.reloadData()             }         })     } }
func didUpdateNote(noteID: Int) {     var indexOfEditedNote: Int!          for i in 0..<notes.count {         if notes[i].noteID == noteID {             indexOfEditedNote = i             break         }     }          if indexOfEditedNote != nil {         note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in             if note != nil {                 self.notes[indexOfEditedNote] = note                 self.tblNotes.reloadData()             }         })     } } 

In this case we first find the index of the updated note in the notes collection. Once that happens, we load the updated note from the database and we replace the old object with the new one. By refreshing the tableview, the new modification date of the updated note will be shown instantly.

Deleting Records

The last major feature that is still missing from our demo app is the note deletion. It’s easy to understand that we need one last method implemented in the Note class that will be called evey time we want to delete a note, so open the Note.swift file.

The only new thing in this part is the SwiftyDB method that performs the actual deletion from the database, as you will see in the following implementation. Like before, this is one more operation executed asynchronously, and we have a completion handler to call once the execution is over again. Lastly, there’s a filter to specify the row that should be deleted from the database.

func deleteNote(completionHandler: (success: Bool) -> Void) {     let filter = Filter.equal("noteID", value: noteID)          database.asyncDeleteObjectsForType(Note.self, matchingFilter: filter) { (result) -> Void in         if let deleteOK = result.value {             completionHandler(success: deleteOK)         }                  if let error = result.error {             print(error)             completionHandler(success: false)         }     } }
func deleteNote(completionHandler: (success: Bool) -> Void) {     let filter = Filter.equal("noteID", value: noteID)          database.asyncDeleteObjectsForType(Note.self, matchingFilter: filter) { (result) -> Void in         if let deleteOK = result.value {             completionHandler(success: deleteOK)         }                  if let error = result.error {             print(error)             completionHandler(success: false)         }     } } 

Let’s open the NoteListViewController.swift now, and let’s define the next UITableViewDataSource method:

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {     if editingStyle == UITableViewCellEditingStyle.Delete {              } }
func tableView(tableView: UITableView, commitEditingStyleeditingStyle: UITableViewCellEditingStyle, forRowAtIndexPathindexPath: NSIndexPath) {     if editingStyle == UITableViewCellEditingStyle.Delete {              } } 

By having added the above method to our code, each time you swipe towards left on a note cell the default Delete button will appear to the right side. Moreover, the code that will be executed when the Delete button is tapped is the one that will be defined in the body of the if statement above. Let’s do so:

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {     if editingStyle == UITableViewCellEditingStyle.Delete {         let noteToDelete = notes[indexPath.row]                  noteToDelete.deleteNote({ (success) -> Void in             dispatch_async(dispatch_get_main_queue(), { () -> Void in                 if success {                     self.notes.removeAtIndex(indexPath.row)                     self.tblNotes.reloadData()                 }             })         })     } }
func tableView(tableView: UITableView, commitEditingStyleeditingStyle: UITableViewCellEditingStyle, forRowAtIndexPathindexPath: NSIndexPath) {     if editingStyle == UITableViewCellEditingStyle.Delete {         let noteToDelete = notes[indexPath.row]                  noteToDelete.deleteNote({ (success) -> Void in             dispatch_async(dispatch_get_main_queue(), { () -> Void in                 if success {                     self.notes.removeAtIndex(indexPath.row)                     self.tblNotes.reloadData()                 }             })         })     } } 

At first, we find the proper note object matching to the selected cell in the notes collection. Next, we call our new method in the Note class to delete it, and if that operation is successful we remove the Note object from the notes array and reload the tableview so we update the UI.

It was just that!

And What About Sorting?

Possibly you’re wondering how we can sort our data while fetching them from the database. Sorting is quite useful, as it can be based on one or more fields, to be performed in ascending or descending order and eventually change the order of the returned data. For example, we could sort our notes in a way that the most recent modified notes appear to the top.

Unfortunately, SwiftyDB doesn’t support data sorting at the time of writing this tutorial. This is a disadvantage indeed, but there’s a solution: To manually sort the data when you need so. To demonstrate this, let’s create one last method in the NoteListViewController.swift file called sortNotes() . In this one we’ll use the Swift’s default sort() function:

func sortNotes() {     notes = notes.sort({ (note1, note2) -> Bool in         let modificationDate1 = note1.modificationDate.timeIntervalSinceReferenceDate         let modificationDate2 = note2.modificationDate.timeIntervalSinceReferenceDate                  return modificationDate1 > modificationDate2     }) }
func sortNotes() {     notes = notes.sort({ (note1, note2) -> Bool in         let modificationDate1 = note1.modificationDate.timeIntervalSinceReferenceDate         let modificationDate2 = note2.modificationDate.timeIntervalSinceReferenceDate                  return modificationDate1 > modificationDate2     }) } 

Since NSDate objects cannot be compared directly, we convert them to timestamp values (double values) first. Then we perform the comparison and we return the result. The above code leads to note sorting where the most recent modified notes are in the first positions of the notes array.

The above method must be called whenever the notes array gets changed. First, let’s update the loadNotes method as shown next:

func loadNotes() {     note.loadAllNotes { (notes) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if notes != nil {                 self.notes = notes                                  self.sortNotes()  // Add this line to sort notes.                                  self.tblNotes.reloadData()             }         })     } }
func loadNotes() {     note.loadAllNotes { (notes) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if notes != nil {                 self.notes = notes                                  self.sortNotes()  // Add this line to sort notes.                                  self.tblNotes.reloadData()             }         })     } } 

Then, we must do the same to the two following delegate methods:

func didCreateNewNote(noteID: Int) {     note.loadSingleNoteWithID(noteID) { (note) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if note != nil {                 self.notes.append(note)                  self.sortNotes() // Add this line to sort notes.                                  self.tblNotes.reloadData()             }         })     } }
func didCreateNewNote(noteID: Int) {     note.loadSingleNoteWithID(noteID) { (note) -> Void in         dispatch_async(dispatch_get_main_queue(), { () -> Void in             if note != nil {                 self.notes.append(note)                   self.sortNotes() // Add this line to sort notes.                                  self.tblNotes.reloadData()             }         })     } } 

func didUpdateNote(noteID: Int) {     ...          if indexOfEditedNote != nil {         note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in             if note != nil {                 self.notes[indexOfEditedNote] = note                                  self.sortNotes()  // Add this line to sort notes.                                  self.tblNotes.reloadData()             }         })     } }
func didUpdateNote(noteID: Int) {     ...          if indexOfEditedNote != nil {         note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in             if note != nil {                 self.notes[indexOfEditedNote] = note                                  self.sortNotes()  // Add this line to sort notes.                                  self.tblNotes.reloadData()             }         })     } } 

By running the app again now, you’ll see all notes listed on the tableview based on their modification date.

Summary

Undoubtably, SwiftyDB is a great tool that can be used in a variety of applications without much effort. It’s really fast and reliable to what is made to do, and we can all agree that can cover several needs when a database must be used in our apps. In this demo tutorial we went through the basics of this library, but this is what you pretty much need to know. Of course, there’s always the official documentation you can refer to for further assistance. In our example today, and for the sake of the tutorial, we created a database with one table only matching to the Note class. In real-world apps though, you can have as many tables as you want, as long as you create the respective models in code (let’s say the respective classes). Personally, I would definitely use SwiftyDB in my projects, and as a matter of fact, I’m planning to do so. In any case, now you know about it, you’ve seen how it works and how it can be used. It’s up to you to decide if this is one more tool in your toolbox or not. Anyway, I hope the time you spent reading isn’t wasted, and you’ve learned another new thing, or two. And until our next tutorial comes, have all a wonderful time!

For your reference, you can download the full project on GitHub .

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Managing SQLite Database with SwiftyDB

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮