In my app, I am thousands of records in each table. I have multiple table insertions. I am using fmdb. And for inserting data, I am using FMDatabaseQueue:
DBManager.GetQueue().inTransaction { (db, rollback) in
var query: String = String()
for obj in chatThreads
{
query += "insert or replace into \(TABLE_NAME) (\(COLUMN_THREAD_ID), \(COLUMN_CHAT_NAME), \(COLUMN_DATE_TIME)) values (\(obj.threadId), '\(obj.chat_name)', '\(obj.date_time)');"
}
if !(db.executeStatements(query)) {
}
}
I am inserting data, in the above mentioned way, to around 15 tables.
Now, the problem is, FMDatabaseQueue inserts data one table after another, which is the way it is supposed to work. I am using FMDatabaseQueue for thread safety as I am inserting the data in the first two classes, and dont want any db lock issues to appear.
My question is, is there any way to make this a parallel process, so that I can insert data to different tables at the same time. If not, could you please direct me in correct path to achieve a better performance for the same.
I have also tried batch insertions, but multiple insertions at the same time might cause db lock.
Thanks.
You can keep your own singleton class and method to insert data in table should return bool value to know data is been inserted or not.
If data is been stored in table then you can fire another method to insert data, ideally you can make chain to store data in table.
Singleton class like this
import Foundation
class LocalDatabase: NSObject {
//sharedInstance
static let sharedInstance = LocalDatabase()
func methodToCreateDatabase() -> NSURL? {
let fileManager = NSFileManager.defaultManager()
let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
if let documentDirectory:NSURL = urls.first { // No use of as? NSURL because let urls returns array of NSURL
// exclude cloud backup
do {
try documentDirectory.setResourceValue(true, forKey: NSURLIsExcludedFromBackupKey)
} catch _{
print("Failed to exclude backup")
}
// This is where the database should be in the documents directory
let finalDatabaseURL = documentDirectory.URLByAppendingPathComponent("contact.db")
if finalDatabaseURL.checkResourceIsReachableAndReturnError(nil) {
// The file already exists, so just return the URL
return finalDatabaseURL
} else {
// Copy the initial file from the application bundle to the documents directory
if let bundleURL = NSBundle.mainBundle().URLForResource("contact", withExtension: "db") {
do {
try fileManager.copyItemAtURL(bundleURL, toURL: finalDatabaseURL)
} catch _ {
print("Couldn't copy file to final location!")
}
} else {
print("Couldn't find initial database in the bundle!")
}
}
} else {
print("Couldn't get documents directory!")
}
return nil
}
func methodToInsertUpdateDeleteData(strQuery : String) -> Bool
{
// print("%#",String(methodToCreateDatabase()!.absoluteString))
let contactDB = FMDatabase(path: String(methodToCreateDatabase()!.absoluteString) )
if contactDB.open() {
let insertSQL = strQuery
let result = contactDB.executeUpdate(insertSQL,
withArgumentsInArray: nil)
if !result {
print("Failed to add contact")
print("Error: \(contactDB.lastErrorMessage())")
return false
} else {
print("Contact Added")
return true
}
} else {
print("Error: \(contactDB.lastErrorMessage())")
return false
}
}
func methodToSelectData(strQuery : String) -> NSMutableArray
{
let arryToReturn : NSMutableArray = []
print("%#",String(methodToCreateDatabase()!.absoluteString))
let contactDB = FMDatabase(path: String(methodToCreateDatabase()!.absoluteString) )
if contactDB.open() {
let querySQL = strQuery
let results:FMResultSet? = contactDB.executeQuery(querySQL,
withArgumentsInArray: nil)
while results?.next() == true
{
arryToReturn.addObject(results!.resultDictionary())
}
// NSLog("%#", arryToReturn)
if arryToReturn.count == 0
{
print("Record Not Found")
}
else
{
print("Record Found")
}
contactDB.close()
} else {
print("Error: \(contactDB.lastErrorMessage())")
}
return arryToReturn
}
}
And method to call from your viewcontroller like this
if LocalDatabase.sharedInstance.methodToInsertUpdateDeleteData("INSERT INTO CONTACTS_TABLE (name, address, phone) VALUES ('Demo1', 'Demo2', 123)")
{
NSLog("Store Successfully.")
}
else
{
NSLog("Failled to store in database.")
}
This is wrapper singleton class connected with FMDB
https://github.com/hasyapanchasara/SQLite_SingleManagerClass
Related
I want to retrieve usernames from a users collection and save in an array. I use this:
var usernames:[String] = []
override func viewDidLoad() {
super.viewDidLoad(
populateUsernames()
}
func populateUsernames() {
let db = Firestore.firestore()
db.collection("users").getDocuments() { [self] (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let username = document.get("username") as! String
usernames.append(username)
print(usernames) //THIS PRINTS ["user1", "user2"] WHICH IS CORRECT
}
print(usernames) // THIS PRINTS [] WHICH IS FALSE
}
}
}
Why does the array reset to [] after the for loop?
There is nothing in your code that would cause this behavior. You're either printing the wrong array or something else is overwriting it, which doesn't seem likely. I notice that you aren't referring to the array with self which you would need to do in this closure. Therefore, rename the array for testing purposes.
var usernames2: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
populateUsernames()
}
func populateUsernames() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
self.usernames2.append(username)
print(username)
} else {
print("username not found")
}
}
print(self.usernames2)
} else {
if let error = error {
print(error)
}
}
}
}
You also crudely parse these documents which may not be harmful but is nonetheless unsafe, which this code addresses.
I'm still new to Swift and i'm trying archive and unarchive an array of UIColours to NSUserDefaults. I'm aware that in ios 12 i need to use unarchivedObject(ofClass:from:) - but i'm not sure how to use that.
I've tried to follow this question: Unarchive Array with NSKeyedUnarchiver unarchivedObject(ofClass:from:)
but i think i'm doing something wrong.
Here is the code i am trying:
let faveColoursArray = [colour1, colour2]
private func archiveColours() -> Data {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: faveColoursArray, requiringSecureCoding: false)
return data
} catch {
fatalError("can't encode data.")
}
}
func loadColours() -> [UIColor]? {
guard let unarchivedObject = UserDefaults.standard.data(forKey: "faveColours") else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: unarchivedObject) else {
fatalError("Can't load colours.")
}
return array
} catch {
fatalError("Can't load colours.")
}
}
Thankyou
You can use unarchiveTopLevelObjectWithData(_:):
func loadColours() -> [UIColor]? {
guard let unarchivedObject = UserDefaults.standard.data(forKey: "faveColours") else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? [UIColor] else {
fatalError("Can't load colours.")
}
return array
} catch {
fatalError("Can't load colours.")
}
}
Since upgrading to Swift 4.2 I've found that many of the NSKeyedUnarchiver and NSKeyedArchiver methods have been deprecated and we must now use the type method static func unarchivedObject<DecodedObjectType>(ofClass: DecodedObjectType.Type, from: Data) -> DecodedObjectType? to unarchive data.
I have managed to successfully archive an Array of my bespoke class WidgetData, which is an NSObject subclass:
private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> NSData {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: widgetDataArray as Array, requiringSecureCoding: false) as NSData
else { fatalError("Can't encode data") }
return data
}
The problem comes when I try to unarchive this data:
static func loadWidgetDataArray() -> [WidgetData]? {
if isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA) {
if let unarchivedObject = UserDefaults.standard.object(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) as? Data {
//THIS FUNCTION HAS NOW BEEN DEPRECATED:
//return NSKeyedUnarchiver.unarchiveObject(with: unarchivedObject as Data) as? [WidgetData]
guard let nsArray = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: unarchivedObject as Data) else {
fatalError("loadWidgetDataArray - Can't encode data")
}
guard let array = nsArray as? Array<WidgetData> else {
fatalError("loadWidgetDataArray - Can't get Array")
}
return array
}
}
return nil
}
But this fails, as using Array.self instead of NSArray.self is disallowed. What am I doing wrong and how can I fix this to unarchive my Array?
You can use unarchiveTopLevelObjectWithData(_:) to unarchive the data archived by archivedData(withRootObject:requiringSecureCoding:). (I believe this is not deprecated yet.)
But before showing some code, you should better:
Avoid using NSData, use Data instead
Avoid using try? which disposes error info useful for debugging
Remove all unneeded casts
Try this:
private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> Data {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: widgetDataArray, requiringSecureCoding: false)
return data
} catch {
fatalError("Can't encode data: \(error)")
}
}
static func loadWidgetDataArray() -> [WidgetData]? {
guard
isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA), //<- Do you really need this line?
let unarchivedObject = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA)
else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? [WidgetData] else {
fatalError("loadWidgetDataArray - Can't get Array")
}
return array
} catch {
fatalError("loadWidgetDataArray - Can't encode data: \(error)")
}
}
But if you are making a new app, you should better consider using Codable.
unarchiveTopLevelObjectWithData(_:)
is deprecated as well. So to unarchive data without secure coding you need to:
Create NSKeyedUnarchiver with init(forReadingFrom: Data)
Set requiresSecureCoding of created unarchiver to false.
Call decodeObject(of: [AnyClass]?, forKey: String) -> Any? to get your object, just use proper class and NSKeyedArchiveRootObjectKeyas key.
As unarchiveTopLevelObjectWithData is also deprecated after iOS 14.3 only the Hopreeeenjust's answer is correct now.
But if you don't need NSSecureCoding you also can use answer of Maciej S
It is very easy to use it, by adding extension to NSCoding protocol:
extension NSCoding where Self: NSObject {
static func unsecureUnarchived(from data: Data) -> Self? {
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let obj = unarchiver.decodeObject(of: self, forKey: NSKeyedArchiveRootObjectKey)
if let error = unarchiver.error {
print("Error:\(error)")
}
return obj
} catch {
print("Error:\(error)")
}
return nil
}
}
With this extension to unarchive e.g. NSArray you only need:
let myArray = NSArray.unsecureUnarchived(from: data)
For Objective C use NSObject category:
+ (instancetype)unsecureUnarchivedFromData:(NSData *)data {
NSError * err = nil;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData: data error: &err];
unarchiver.requiresSecureCoding = NO;
id res = [unarchiver decodeObjectOfClass:self forKey:NSKeyedArchiveRootObjectKey];
err = err ?: unarchiver.error;
if (err != nil) {
NSLog(#"NSKeyedUnarchiver unarchivedObject error: %#", err);
}
return res;
}
Note that if the requiresSecureCoding is false, class of unarchived object is not actually checked and objective c code returns valid result even if it is called from wrong class.
And swift code when called from wrong class returns nil (because of optional casting), but without error.
Swift 5- IOS 13
guard let mainData = UserDefaults.standard.object(forKey: "eventDetail") as? NSData
else {
print(" data not found in UserDefaults")
return
}
do {
guard let finalArray =
try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(mainData as Data) as? [EventDetail]
else {
return
}
self.eventDetail = finalArray
}
You are likely looking for this:
if let widgetsData = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) {
if let widgets = (try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, WidgetData.self], from: widgetsData)) as? [WidgetData] {
// your code
}
}
if #available(iOS 12.0, *) {
guard let unarchivedFavorites = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(favoritesData!)
else {
return
}
self.channelFavorites = unarchivedFavorites as! [ChannelFavorite]
} else {
if let unarchivedFavorites = NSKeyedUnarchiver.unarchiveObject(with: favoritesData!) as? [ChannelFavorite] {
self.channelFavorites = unarchivedFavorites
}
// Achieving data
if #available(iOS 12.0, *) {
// use iOS 12-only feature
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: channelFavorites, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "channelFavorites")
} catch {
return
}
} else {
// handle older versions
let data = NSKeyedArchiver.archivedData(withRootObject: channelFavorites)
UserDefaults.standard.set(data, forKey: "channelFavorites")
}
This is the way I have updated my code and its working for me
I'm trying to save my json file and show it to offline. I'm trying this code but it is not working for me ..
let myData = NSKeyedArchiver.archivedData(withRootObject: self.data)
UserDefaults.standard.set(myData, forKey: "userJson")
UserDefaults.standard.synchronize()
Can any one suggest me better way to save data and show off line ?
You should not save JSON in the UserDefault, Instead save it in file in document directory
I have created generic class which allows to do it easily
//
// OfflineManager.swift
//
//
// Created by Prashant on 01/05/18.
// Copyright © 2018 Prashant. All rights reserved.
//
import UIKit
class OfflineManager: NSObject {
static let sharedManager = OfflineManager()
let LocalServiceCacheDownloadDir = "LocalData"
enum WSCacheKeys {
case CampignList
case CampignDetail(id:String)
case ScreenShotList
var value:String {
switch self {
case .CampignList:
return "CampignList"
case .CampignDetail(let id):
return id
case .ScreenShotList :
return "ScreenShotList"
}
}
}
func getBaseForCacheLocal(with fileName:String) -> String? {
let filePath = FileManager.default.getDocumentPath(forItemName: self.LocalServiceCacheDownloadDir)
if FileManager.default.directoryExists(atPath: filePath) {
return filePath.stringByAppendingPathComponent(fileName)
} else {
if FileManager.default.createDirectory(withFolderName: self.LocalServiceCacheDownloadDir) {
return filePath.stringByAppendingPathComponent(fileName)
}
}
return nil
}
//------------------------------------------------------------
#discardableResult
func cacheDataToLocal<T>(with Object:T,to key:WSCacheKeys) -> Bool {
let success = NSKeyedArchiver.archiveRootObject(Object, toFile: getBaseForCacheLocal(with: key.value)!)
if success {
print( "Local Data Cached\(String(describing: getBaseForCacheLocal(with: key.value)))")
} else {
print("Error")
}
return success
}
//------------------------------------------------------------
func loadCachedDataFromLocal<T>(with key:WSCacheKeys ) -> T? {
return NSKeyedUnarchiver.unarchiveObject(withFile: getBaseForCacheLocal(with: key.value)!) as? T
}
//------------------------------------------------------------
func removeAllCacheDirs () {
do {
try FileManager.default.removeItem(atPath: self.getBaseForCacheLocal(with: "")!)
} catch {
print("error in remove dir \(error.localizedDescription)")
}
}
//--------------------------------------------------------------------------------
}
Here is some helper methods of extension FileManager
public var getDocumentDirectoryPath: String {
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
return documentDirectory
}
public func getDocumentPath(forItemName name: String)-> String {
return getDocumentDirectoryPath.stringByAppendingPathComponent(name)
}
public func directoryExists(atPath filePath: String)-> Bool {
var isDir = ObjCBool(true)
return FileManager.default.fileExists(atPath: filePath, isDirectory: &isDir )
}
public func createDirectory(withFolderName name: String)-> Bool {
let finalPath = getDocumentDirectoryPath.stringByAppendingPathComponent(name)
return createDirectory(atPath: finalPath)
}
Here Is String extension's method
public func stringByAppendingPathComponent(_ path: String) -> String {
let fileUrl = URL.init(fileURLWithPath: self)
let filePath = fileUrl.appendingPathComponent(path).path
return filePath
}
How to use it ?
To save
OfflineManager.sharedManager.cacheDataToLocal(with: object as! [String:Any], to: .CampignList)
To read data
DispatchQueue.global().async {
// GET OFFLINE DATA
if let object:[String:Any] = OfflineManager.sharedManager.loadCachedDataFromLocal(with: .CampignList) {
do {
let data = try JSONSerialization.data(withJSONObject: object, options: [])
let object = try CampaignListResponse.init(data: data)
self.arrCampignList = object.data ?? []
DispatchQueue.main.async {
self.tableVIew.reloadData()
}
} catch {
}
}
}
Note: You can define your own WSCacheKeys for type of your json like i am fetching some campaign list
You can use Realm or CoraData for saving data and showing it when you are offline.
Here is the official link for Realm.You can learn from here.
https://realm.io/docs/swift/latest
I've already created sqlite tables for my app, but now I want to add a new column in table to the database. ALTER TABLE will help me in this problem but first i want to check the database version.
i am using PRAGMA user_version to check the user version and update the user_version but it always returning user_version as 0.
var database: FMDatabase? = nil
class func getInstance() -> ModelManager{
if(sharedInstance.database == nil){
sharedInstance.database = FMDatabase(path: Util.getPath("XXXX.sqlite"))
}
return sharedInstance
}
func userVersion(){
sharedInstance.database!.open()
var userVer = Int()
let resultSet = sharedInstance.database?.executeQuery("pragma user_version", withArgumentsInArray: nil)
userVer = Int(resultSet!.intForColumn("user_version"))
print("user version : ",userVer)
sharedInstance.database!.close()
}
func updateUserVersion(){
sharedInstance.database!.open()
sharedInstance.database?.executeUpdate("PRAGMA user_version=1", withArgumentsInArray: nil)
sharedInstance.database!.close()
}
The code below works fine with Swift 4
import UIKit
import FMDB
class DataConnection: NSObject {
static let databaseVersion = 2
static var isDatabaseUpdated = false
static var database: FMDatabase? = nil
class func databaseSetup() {
if database == nil {
let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let dpPath = docsDir.appendingPathComponent("database.sqlite")
let file = FileManager.default
if(!file.fileExists(atPath: dpPath.path)) {
copyDatabase(file: file, dpPath: dpPath)
database = FMDatabase(path: dpPath.path)
do {
database!.open()
try database!.executeUpdate("PRAGMA user_version = \(databaseVersion)", values: nil)
database!.close()
isDatabaseUpdated = true
}catch {
print("Error on updating user_version")
}
}else {
database = FMDatabase(path: dpPath.path)
if !isDatabaseUpdated {
var currentVersion = 0
do {
database!.open()
let resultSet: FMResultSet! = try database!.executeQuery("pragma user_version", values: nil)
while resultSet.next() {
currentVersion = Int(resultSet.int(forColumn: "user_version"))
}
database!.close()
}catch {
print("Error on getting user_version")
}
if databaseVersion > currentVersion {
do {
try file.removeItem(at: dpPath)
}catch {
print("Error on getting user_version")
}
copyDatabase(file: file, dpPath: dpPath)
database = FMDatabase(path: dpPath.path)
do {
database!.open()
try database!.executeUpdate("PRAGMA user_version = \(databaseVersion)", values: nil)
database!.close()
isDatabaseUpdated = true
}catch {
print("Error on updating user_version")
}
}else {
isDatabaseUpdated = true
}
}
}
}
}
private class func copyDatabase(file: FileManager, dpPath: URL){
let dpPathApp = Bundle.main.path(forResource: "database", ofType: "sqlite")
print("resPath: "+String(describing: dpPathApp))
do {
try file.copyItem(atPath: dpPathApp!, toPath: dpPath.path)
print("copyItemAtPath success")
} catch {
print("copyItemAtPath fail")
}
}
}
You need to call the next() method on your resultSet so that it loads the first row, before you access intForColumn.
Besides, since you use FMDB, take a look at https://github.com/groue/GRDB.swift: it's a Swift wrapper for SQLite that will look familiar to FMDB users. But you'd simply write let userVer = Int.fetchOne(db, "pragma user_version") this time.