Why do I get Unable to Open Database file? - ios

I am a beginner in iOS development and I want to develop a database in my application. However, When I try to create the database it says unable to open database file.
For clarification (https://github.com/stephencelis/SQLite.swift/blob/master/Documentation/Index.md)
My code for the db development is as follow
static func DB(){
do {
let db = try Connection("db.sqlite3")
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
} catch {
print("Dim background error")
}
}
I trigger this method from app delegate file
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
AppCommon.DB()
return true
}
How to fix this issue? Please help me out, I really need your help..!

On iOS, you can't create files where you want. A correct place (among others) is in the documents directory.
Quoting the documentation of the library you are using https://github.com/stephencelis/SQLite.swift/blob/master/Documentation/Index.md#connecting-to-a-database:
On iOS, you can create a writable database in your app’s Documents directory.
let path = NSSearchPathForDirectoriesInDomains(
.DocumentDirectory, .UserDomainMask, true
).first!
let db = try Connection("\(path)/db.sqlite3")

Swift 5
Path of Documents directory is now an array of strings
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true)
let db = try Connection("\(path.first ?? "")/db.sqlite3")

Related

Can I write/read data in application directory in flutter iOS?

Is there anything else on iOS like getExternalStorageDirectory() ?
Is it getApplicationDocumentsDirectory() ?
If so, can the user access it?
The files in getApplicationDocumentsDirectory() can be shown as a list in the flutter iOS app?
use the path package, supported on all main os
https://pub.dev/packages/path
Unfortunately, you cannot access other app directories except for yours in iOS because of sandboxing. You can read it here as well:
https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
By the way, there is a way to get other directories using swift as provided in the documentation, but I did not see any solution for it using flutter.
Hope it helps you.
If I'm not mistaken, you are trying to get another application directory in iOS using flutter.
There is a way to do so.
At first, let me mention that you do not need any permission for writing & reading data in iOS. It is given by default. But, the problem is getting their path. As others already mentioned that, iOS uses sandboxing, you cannot directly get access to all files and folders excluding shared storage.
Steps you need to do for reading and writing directories of other apps.
Install file_picker package. Link: https://pub.dev/packages/file_picker
Using it, popup system directory picker:
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
PS: Users should know which folder they need to get an access.
3. When they select the folder, get the folder path and use it as you want. But there is still one thing to complete. You need to use a little bit Swift code for getting access it.
import UIKit
import Flutter
import Photos
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "example.startAccessingToSharedStorage",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
// This method is invoked on the UI thread.
guard call.method == "startAccessingToSharedStorage" else {
result(FlutterMethodNotImplemented)
return
}
print("\(call.arguments)")
self?.startAccessingToSharedStorage(result: result, call: call)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func startAccessingToSharedStorage(result: FlutterResult, call: FlutterMethodCall) {
let args = call.arguments as? Dictionary<String, Any>
if(args != nil){
let fileUrl = URL(fileURLWithPath: (args!["url"] as? String) ?? "")
// Get bookmark data from the provided URL
let bookmarkData = try? fileUrl.bookmarkData()
if let data = bookmarkData {
// Save data
} else {
result("Some bad thing happened")
}
// Access to an external document by the bookmark data
if let data = bookmarkData {
var stale = false
if let url = try? URL(resolvingBookmarkData: data, bookmarkDataIsStale: &stale),
stale == false,
url.startAccessingSecurityScopedResource()
{
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { readURL in
if let data = try? Data(contentsOf: readURL) {
result("Error occured while getting access")
}
}
result("\(url.startAccessingSecurityScopedResource())\(args!["url"])")
}
}
} else {result("\(args!["url"])")}
}
}
Use method channel for using this function in flutter.
Yes, on iOS in order to get path set import:
import 'package:path_provider/path_provider.dart' as syspath;
then use:
final appDir = await syspath
.getApplicationDocumentsDirectory();
if you save the path, keep in mind that on iOS the path changes every time we run the application.

SQLite.swift (Release 0.11.2) sample code is not working

I am trying to use SQLite.swift sample code but it is throwing an error.
First step I have done is to install it using cocoapods and it was successful. Then I have tried to test it in my app delegate where I declare import SQLite and I have added the below code in didFinishLaunchingWithOptions but it is showing an error.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let db = try Connection("path/to/db.sqlite3")
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
return true
}
The error I am getting from this code line (let db = try Connection("path/to/db.sqlite3")) is "Errors thrown from here are not handled"
Is there anyone experiencing the same issue? I am using XCode 8.2.1 and Swift 3.0.
Your app needs to know what it should do when there is a runtime error. What you're missing is a do/catch clause or the throws keyword at your function declaration. For more details have a look at chapter "Error Handling" of the the Swift book.
Thanks to #martin-r for the solution. Here's the updated code.
Solution:
do {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let db = try Connection("\(documentsPath)/db.sqlite3")
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
} catch {
// Handle error
}

Realm setting custom fileURL confusion

I'm new to realm, and so as I was fooling around with it to learn it, I found something quite interesting. In my appDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let directory: NSURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.Hobo.RealmDatabase")!
var config = Realm.Configuration()
config.fileURL = directory.filePathURL?.URLByAppendingPathComponent("db.realm")
Realm.Configuration.defaultConfiguration = config
let realm = try! Realm()
print("File Location: \(realm.configuration.fileURL!)") // -> Location A
print("NO OF USERS: \(realm.objects(User).count)") // -> 0
return true
}
but in my ViewController:
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
print("NO OF USERS IN VIEWDIDLOAD: \(realm.objects(User).count)") // -> 1
let firstTime = loadFirstTime()
if firstTime {
// configure USER!
let user = User()
user.monthlyIncome = 50000
try! realm.write({
realm.add(user)
})
saveFirstTime(false)
print("First time, user written")
}
dailyLimit.text = String(realm.objects(User).first!.dailyLimit)
}
Notice the returns from the print() functions. In app delegate, the result of the print(number of users:) returns 0, but in the viewController's viewDidLoad, it returned a 1.
Isn't both supposed to return the same value? In this case 1?
Thanks in advance!!
Yes it is the same, I'm guessing you removing by mistake the user, on application load, or something like that, you should use "Realm browser" to check your DB state, that way you can see when an object changes during run time. https://github.com/realm/realm-browser-osx
EDIT
Check your accessing the default configuration. In realm you can have multiple configurations like so:
let config = Realm.Configuration(
// Get the URL to the bundled file
fileURL: NSBundle.mainBundle().URLForResource("MyBundledData", withExtension: "realm"),
// Open the file in read-only mode as application bundles are not writeable
readOnly: true)
// Open the Realm with the configuration
let realm = try! Realm(configuration: config)
depend on documentation of realm 3
https://realm.io/docs/swift/latest/#realm-configuration
func setDefaultRealmForUser(username: String) {
var config = Realm.Configuration()
// Use the default directory, but replace the filename with the username
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(username).realm")
// Set this as the configuration used for the default Realm
Realm.Configuration.defaultConfiguration = config
}

How do you compact a Realm DB on iOS?

I'd like to compact a Realm instance on iOS periodically to recover space. I think the process is to copy the db to a temporary location, then copy it back and use the new default.realm file.
My problem is Realm() acts like a singleton and recycles objects so I can't really close it and tell it to open the new default.realm file.
The docs here (https://realm.io/docs/objc/latest/api/Classes/RLMRealm.html) suggest I wrap all the Realm() calls in autorelease { } but it can't be this complicated.
It can be indeed tricky to completely tear down all retrieved model accessors, but there is unfortunately no other way to close a Realm.
As you wrote "periodically" every app launch might be often enough, depending on your use case.
On the launch of your application, it should be still relatively easy to open Realm in a dedicated autoreleasepool, write a compacted copy to a different path and replace your default.realm file with it.
Swift 2.1
func compactRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultParentURL = defaultURL.URLByDeletingLastPathComponent!
let compactedURL = defaultParentURL.URLByAppendingPathComponent("default-compact.realm")
autoreleasepool {
let realm = try! Realm()
realm.writeCopyToPath(compactedURL)
}
try! NSFileManager.defaultManager().removeItemAtURL(defaultURL)
try! NSFileManager.defaultManager().moveItemAtURL(compactedURL, toURL: defaultURL)
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
compactRealm()
// further setup …
return true
}
Swift 3.0
func compactRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultParentURL = defaultURL.deletingLastPathComponent()
let compactedURL = defaultParentURL.appendingPathComponent("default-compact.realm")
autoreleasepool {
let realm = try! Realm()
try! realm.writeCopy(toFile: compactedURL)
}
try! FileManager.default.removeItem(at: defaultURL)
try! FileManager.default.moveItem(at: compactedURL, to: defaultURL)
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
compactRealm()
// further setup …
return true
}
The answer given by #marius has an issue: the open Realm might still reference the deleted file. This means some writes might end up in the old (deleted) file, causing the app to lose data.
The correct implementation of compactRealm method looks like this (swift 3):
func compactRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultParentURL = defaultURL.deletingLastPathComponent()
let compactedURL = defaultParentURL.appendingPathComponent("default-compact.realm")
autoreleasepool {
let realm = try! Realm()
try! realm.writeCopy(toFile: compactedURL)
}
try! FileManager.default.removeItem(at: defaultURL)
try! FileManager.default.moveItem(at: compactedURL, to: defaultURL)
}
This issue has been driving me crazy until I found an answer here
Well.. it appears as though this issue is mostly obsolete. Realm added an automatic compact feature last fall. Realm Docs / compacting-realms. I think the only reason to do it as described by #marius is if you need to control the user experience and compact in the background.
See this question for more: How to correctly use shouldCompactOnLaunch in RealmSwift

Realm - Add file with initial data to project (iOS/Swift)

I'm developing an application for iOS using swift and chose Realm as a database solution for it. I wrote default data in AppDelegate using write/add function from realm docs and it works just fine. So after first launch I have a *.realm file with my initial data. In Realm documentation I found a section called "Bundling a Realm with an App", I add my *.realm file to project and to Build Phases as it written.
And I can't understand what I should do next (and part about compressing a *.realm file). I've tried to understand a code from Migration Example but I don't know Obj-C well.
Please give as clear steps as you can to add *.realm file with initial data to swift ios project and load this data to the Realm db with the first launch.
Implement this function openRealm in AppDelegate and call it in
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
openRealm()
return true
}
func openRealm() {
let defaultRealmPath = Realm.defaultPath
let bundleReamPath = NSBundle.mainBundle().resourcePath?.stringByAppendingPathComponent("default.realm")
if !NSFileManager.defaultManager().fileExistsAtPath(defaultRealmPath) {
NSFileManager.defaultManager().copyItemAtPath(bundleReamPath!, toPath: defaultRealmPath, error: nil)
}
}
It will copy your realm file that you bundled in the app to the default realm path, if it doesn't exist already. After that you use Realm normally like you used before.
There's also the Migration example that you talked about in Swift.
In Swift 3.0.1 you may prefer this:
let defaultRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
let bundleRealmPath = Bundle.main.url(forResource: "seeds", withExtension: "realm")
if !FileManager.default.fileExists(atPath: defaultRealmPath.absoluteString) {
do {
try FileManager.default.copyItem(at: bundleRealmPath!, to: defaultRealmPath)
} catch let error {
print("error copying seeds: \(error)")
}
}
(but please be careful with the optionals)
Swift version 3, courtesy of Kishikawa Katsumi:
let defaultRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
let bundleReamPath = Bundle.main.path(forResource: "default", ofType:"realm")
if !FileManager.default.fileExists(atPath: defaultRealmPath.path) {
do
{
try FileManager.default.copyItem(atPath: bundleReamPath!, toPath: defaultRealmPath.path)
}
catch let error as NSError {
// Catch fires here, with an NSError being thrown
print("error occurred, here are the details:\n \(error)")
}
}
And for those that need #pteofil's answer in Objective-c
- (void)openRealm {
NSString *defaultRealmPath = [RLMRealm defaultRealm].path;
NSString *bundleRealmPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"default.realm"];
if(![[NSFileManager defaultManager] fileExistsAtPath:defaultRealmPath]) {
[[NSFileManager defaultManager] copyItemAtPath:bundleRealmPath toPath:defaultRealmPath error:nil];
}
}
Updating #pteofil's openRealm function for Swift 2.2/Realm 1.0.2:
func openRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let bundleReamPath = NSBundle.mainBundle().URLForResource("default", withExtension: "realm")
if !NSFileManager.defaultManager().fileExistsAtPath(defaultURL.path!) {
do {
try NSFileManager.defaultManager().copyItemAtURL(bundleReamPath!, toURL: defaultURL)
}
catch {}
}
}
Work in the enterprise space, I need to open a Realm for each application without reusing Realm across all applications so I put this together for Swift 3.0. Add this function to the AppDelegate.
func openRealm()
{
let appName = "ApplcationNameGoesHere"
var rlmConfig = Realm.Configuration()
let defaultRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
let appRealmPath = defaultRealmPath.deletingLastPathComponent().appendingPathComponent("\(appName).realm")
if !FileManager.default.fileExists(atPath: appRealmPath.path) {
// Use the default directory, but replace the filename with the application name: appName
rlmConfig.fileURL = rlmConfig.fileURL!.deletingLastPathComponent().appendingPathComponent("\(appName).realm")
}else
{
rlmConfig.fileURL = appRealmPath
}
// Set this as the configuration used for the default Realm
Realm.Configuration.defaultConfiguration = rlmConfig
}// open the Realm database for the application
The code above opens or creates a Realm with the file name of "ApplicationNameGoesHere.realm" based on the appName variable in this example.
place
openRealm() before return true in application: didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
openRealm()
return true
}
call it in another class like this:
let uiRealm = try! Realm()
If you want to open it straight from the bundle location and not bother copying it to the default Realm path, look at the implementation here
Download Realm Studio in your system. Then print the path from Xcode and copy it:
print(Realm.Configuration.defaultConfiguration.fileURL!)
Then open the terminal and write:
open //file path
It will open the file in Realm Studio and you can see your model data there.

Resources