How do you compact a Realm DB on iOS? - 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

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.

Seed Data From Realm Sync Configuration in UICollectionView

So, here's my issue:
The local path for realms on iOS are located in the Documents Directory. I can open them with:
let realm = try! Realm()
Opening a sync realm is different as they are located by URLs
https://realm.io/docs/swift/latest/#realms
I have a UICollectionView with a Results<Object> I can render default data to called in the AppDelegate from a separate file by writing to the Realm on launch
Separate File
class SetUpData {
// MARK: - Seed Realm
static func defaults() {
let realm = try! Realm()
guard realm.isEmpty else { return }
try! realm.write {
realm.add(List.self())
}
}
}
App Delegate
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// MARK: - Set Up Realm If Deleted
var config = Realm.Configuration()
config.deleteRealmIfMigrationNeeded = true
Realm.Configuration.defaultConfiguration = config
SetUpData.defaults()
return true
}
From here, client-side (iOS), I am able to successfully log in (rolled my own log in but the values correspond to the Realm Object Server (ROS) admin user) and retrieve the default values from List.cell and begin writing "Lists" to my application.
However, when I configure my realm with a Sync Configuration par opening a synchronized Realm requires a User that’s been authenticated to the Object Server and that’s authorized to open that Realm, I reasonably crash in my cellForItemAtIndexPath return lists.count fatal error: unexpectedly found nil while unwrapping an Optional value because there is no initial data to return.
That makes sense. But what do I do?
Do I need to create a Realm file in the default config and migrate it to the server? I attempted to change my config to a Sync object in the App Delegate with the code below (which is what I am using in ListViewController). No dice.
private func setUpRealm() {
let username = "\(LoginViewController().username.text!)"
let password = "\(LoginViewController().password.text!)"
SyncUser.logIn(with: SyncCredentials.usernamePassword(username: username, password: password, register: true), server: URL(string: "http://000.000.000.000:9080")!) { (user, error) in
guard let user = user else {
fatalError(String(describing: error))
}
DispatchQueue.main.async {
let configuration = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: URL(string: "realm://000.000.000.000:9080/~/realmList")!))
let realm = try! Realm(configuration: configuration)
self.lists = realm.objects(List.self).sorted(byKeyPath: "created", ascending: false)
self.notificationToken = self.lists.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard (self?.collectionView) != nil else { return }
switch changes {
case .initial:
self?.collectionView.reloadData()
break
case .update(_, let deletions, let insertions, let modifications):
self?.collectionView.performBatchUpdates({
self?.collectionView.insertItems(at: insertions.map({ IndexPath(row: $0, section: 0)}))
self?.collectionView.deleteItems(at: deletions.map({ IndexPath(row: $0, section: 0)}))
self?.collectionView.reloadItems(at: modifications.map({ IndexPath(row: $0, section: 0)}))
}, completion: nil)
break
case .error(let error):
print(error.localizedDescription)
break
}
}
}
}
}
Realm does not provide an API to convert a standalone Realm to a synced Realm currently. If my understanding is correct, it is necessary to copy the data from the seed Realm to synced Realm when opening the synced Realm.

How to set the default SyncConfiguration for Realm, so I can get it in multiple ViewControlllers without redundant code?

According to the:
Proper Realm usage patterns/best practices
What is the best practice or design pattern to maintain sync activity across multiple views
Design Pattern for Realm Database Swift 3.1 - Singleton
my approach is like:
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
DispatchQueue.main.async {
let username = "test#test.com"
let password = "Test123"
let serverUrl = URL(string: "http://test.com:9080")
let realmUrl = URL(string: "realm://test.com:9080/~/realmtest")
if let user = SyncUser.current {
Realm.Configuration.defaultConfiguration.syncConfiguration = SyncConfiguration(user: user, realmURL: realmUrl!)
} else {
SyncUser.logIn(with: .usernamePassword(username: username, password: password, register: false), server: serverUrl!, onCompletion: { (user, error) in
guard let user = user else {
print("Error: \(String(describing: error?.localizedDescription))")
return
}
Realm.Configuration.defaultConfiguration.syncConfiguration = SyncConfiguration(user: user, realmURL: realmUrl!)
})
}
}
return true
}
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
print("SyncConfiguration: \(String(describing: Realm.Configuration.defaultConfiguration.syncConfiguration))")
self.realm = try! Realm()
}
When I open app for the first time nothing happens but when I open app the second time, Realm works fine.
Whenever I open app, the printed SyncConfiguration is nil. No errors!
Searched here and there and can't find an answer...
The problem is that you are using an async method to configure your Realm, but you don't call the print inside the completion handler of your method. You should only present your viewcontoller once your asynchronous call has finished.

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
}

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