Realm not working after migration - ios

This is the relevant code in my UIViewController:
class HabitTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak var habitTableView: UITableView!
private var _numOfRowsInSects: [Int] = []
private var _allSections = Set<Int>() //_[0] = 1 -> Morning
private let _timeInDay = [0: "Morning", 1: "Afternoon", 2:"Evening", 3:"Anytime"]
private var _habitsBySection:[[Habit]] = []
private var _whatIsToday = -1 //means no button other than today has been pressed
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
habitTableView.delegate = self
habitTableView.dataSource = self
var error: NSError?
NSFileManager.defaultManager().removeItemAtPath(Realm.defaultPath, error:&error)
let realm = Realm()
//for testing purposes, preload some habits
let habit1 = Habit()
let habit2 = Habit()
let habit3 = Habit()
let habit4 = Habit()
//set up code -- assigning properties and etc.
realm.write{realm.add(habit1)}
realm.write{realm.add(habit2)}
realm.write{realm.add(habit3)}
realm.write{realm.add(habit4)}
}
#IBAction func reloadTableForDay(sender: DayButton){
if sender.tag != getDayOfWeek(-1){
_whatIsToday = sender.tag
_habitsBySection = []
_allSections = []
habitTableView.reloadData()
}
else{
_whatIsToday = -1
}
}
func getHabitsForDay(daySelected: Int) -> Results<Habit> {
let daySelected = String(daySelected)
let habitsOfDay = Realm().objects(Habit).filter("durationByDay_days contains %#", "7")
return habitsOfDay
}
}
I set up the data to be persisted in viewDidLoad(), for testing purposes. However my getHabitsForDay(daySelected: Int) function only returns query result when the program first runs, i.e. when I click the buttons that call the reloadTableForDay(sender: DayButton) function, which in turn calls reload to the UITable, nothing happens and in my console I can see the query returned an empty Result<Habit>. This all happened after I changed my data model (added a property and a class) and performed the migration.
I also suspect that
var error: NSError?
NSFileManager.defaultManager().removeItemAtPath(Realm.defaultPath, error:&error)
could be messing things up, but I'm not sure.
EDIT: Now i'm sure this was caused by migration, as I started a new project and copied over the code. Everything was working fine until I did a migration.
This is the migration code in my AppDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let config = Realm.Configuration(
//You need to increment the version everytime you change your object schema (starts at 0)
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
//If you want to preserve any data, you can do it here, otherwise just leave it blank.
}
)
Realm.Configuration.defaultConfiguration = config
let realm = Realm()
return true
}

I agree that the line of code in your view controller where you're deleting the Realm file would most likely be the cause of the problem. If you want to delete the default Realm file, it would be much safer to do it before Realm() is called anywhere for the first time.
Realm retains references to itself in memory (So it doesn't need to continually set itself up each time you call Realm() on separate threads), so I'd say it's safe to assume that it's state in memory might be getting confused with the file getting deleted after it was already opened.
If you're deleting the file, simply for testing reasons, I'd recommend deleting it before you set the migration block and call Realm() for the first time.

Related

How to access a realm database from multiple controllers - swift

I am attempting to make a to-do app with a controller for both "this week" and "next week." (Objects with a date in the current week will be shown in the thisWeek view controller, while objects with a date next week will be shown in the nextWeek view controller. However, sometimes objects will not appear in the table until the app restarts, and I'm not sure why this is happening or how to fix it.
In the thisWeek class (which is also my initial view controller), I initialize my database like so:
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: ToDoListItem.className()) { (_, newItem) in
newItem?["notes"] = ""
newItem?["isReoccuring"] = false
}
}
})
lazy var realm = try! Realm(configuration: config)
This worked fine and there weren't any issues. However, I don't know how best to access this db from another controller (nextWeek). This was my first attempt: (which is what I also use in the object entry controller, where objects are first added to the db)
lazy var realm = try! Realm()
I also added a refresh() to each controller's viewDidLoad() with the intention of refreshing the table every time the view is loaded. But it didn't help every situation.
func refresh(){
data = sortContent(arg: realm.objects(ToDoListItem.self).map({ $0 }))
thisTable.reloadData()
}
The issue with this is that some objects do not appear in their proper place until I restart the app. Only then do all objects appear properly. For example:
something created in the nextWeek controller with a date in the current week will not appear until app restarts
when an object in created in nextWeek controller, then date is changed from next week to this week, again, needs an app restart
Things that do work include:
something created in the thisWeek controller with a date in the next week will appear immediately
when an object is created in thisWeek controller, then date is changed from this week to next week works immediately
I thought I needed a way to sync the different "instances" of the db, but there may be another issue that I just don't know about. Any insight would be helpful
Would be happy to share more code snippets if more information is needed.
UPDATE
This is in AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: ToDoListItem.className()) { (_, newItem) in
newItem?["notes"] = ""
newItem?["isReoccuring"] = false
}
}
})
Realm.Configuration.defaultConfiguration = config
let realm = try! Realm()
return true
}
And everywhere I need to use realm (thisWeek and nextWeek controllers, etc), I use:
let realm = RealmService.shared.realm
Again, only certain functionalities are properly working
First the code in the section I initialize my database initializes that database var with a config.
lazy var realm = try! Realm(configuration: config)
The next section could be a 'different' realm as no config is specified
lazy var realm = try! Realm()
Please review Local Migrations, noting that a migration should occur when the app is first launched, not every time realm is accessed
Inside your application(application:didFinishLaunchingWithOptions:)
In other words, perform the migration on app launch (ensuring to include Realm.Configuration.defaultConfiguration = config) and then after that use
let realm = try! Realm()
to access a local realm.
You will probably not want to do this:
data = sortContent(arg: realm.objects(ToDoListItem.self).map({ $0 }))
Realm objects are live updating and that code casts the realm objects to an array which not only 'breaks' the link between the objects and their backing data, it also loads ALL of the objects into memory, defeating their lazily loading nature. This can overwhelm a device and cause a crash. See Realm Key Concepts and Read Characteristics
To answer the question;
I thought I needed a way to sync the different "instances" of the db
You don't. You should access a realm the same way every time. Objects created on different realms are tied to that realm and trying to access them from a different realm will cause a crash.
One solution is to use a Singleton pattern, sometimes called a RealmManager or RealmService (search here on SO for those key words). While there is some debate about singletons in Swift, it works for this case.
Another solution is to have a master controller which 'talks' to realm and then sub viewControllers get their connection to realm from the master viewController. The would be a simple matter of instantiating the sub viewController from the master viewController and passing and/or assigning a property in the sub viewController. So conceptually something like this.
MasterVC
func getRealm() -> Realm {
//return a realm instance
}
func createSubVC {
let mySubVC = SubViewController()
mySubVC.realm = gGetRealm()
}
But for simplicity and maintenance, I would suggest a singleton pattern for this use case. You could do something like this; create a file and put this code in it
import RealmSwift
func gGetRealm() -> Realm? {
do {
let realm = try Realm()
return realm
} catch let error as NSError {
print("Error!")
print(" " + error.localizedDescription)
let err = error.code
print(err)
let t = type(of: err)
print(t)
return nil
}
}
then whenever you need a realm instance in your app
func saveData() {
if let realm = gGetRealm() {
try! realm.write {
//save some data
}
}
or a RealmService singleton
import RealmSwift
class RealmService {
private init() {}
static let shared = RealmService()
lazy var realm: Realm = {
let realm = try! Realm()
return realm
}()
}
accessed like this
let realm = RealmService.shared.realm

Back button and creating new entry causes app to crash (UUID cannot be amended) Realm Swift

When I go back to VC1 (which allows the user to input a title for a book and create an entry in realm including the title and a UUID for that book) from VC2 (using the provided back button as part of the navigation controller not a custom segue) and then create a new book object in Realm (by adding another title to the text field in VC1), the app crashes saying I cannot amend the primary key once set.
I am intending to create a new entry (in theory I could add one, go back, add another etc) rather than try to overwrite an existing entry.
I've read the docs (and even looked at an old project where a similar thing is working) but I can't understand why it isn't just creating a new entry. I looked at the Realm docs (e.g. referenced in this answer Realm in IOS: Primary key can't be changed after an object is inserted)
Code here is VC1 allowing the user to create a new novel (by adding a title into a text field which is earlier in the code)
func createNewNovel() {
let realm = try! Realm()
novelCreated.novelID = UUID().uuidString
novelCreated.novelTitle = novelTitleInput.text!
novelCreated.createdDate = Date()
do {
try realm.write {
realm.add(novelCreated)
print ("Novel Created Successfully")
}
} catch {
print("error adding novel")
}
Then I prepare to pass the novelID to VC2 :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let novelData = NovelObject()
novelData.novelID = novelCreated.novelID
if let destVC = segue.destination as? WriteVC {
destVC.novelIDPassed = novelData.novelID
}
}
This works fine and the segue triggers from a button press on VC1. I can also print the ID of the novel in VC2 so that's working fine.
BUT... when I go back from VC2 and input a new title in the text field in VC1 it should create a new NovelObject, not try to update the existing one (which is causing it to crash). You could even theoretically have the same book title twice but they should have a different UUID (which is the primary key)
I must be missing something trivial I suppose but can't work it out!
The novel is created as at the top :
class NewNovelVC: UIViewController {
let novelCreated = NovelObject()
#IBOutlet weak var novelTitleInput: UITextField!
#IBOutlet weak var createButtonOutlet: UIButton!
then it is populated with variables
This is due to classic issue of same object being in the memory re-instantiated which points to the same value of primary key. Creating a singleton class can be very handy here as well.
Create a service file. This will keep your RealSwift initialized on a specific thread as your current block.
RealService.swift
import RealmSwift
class RealmService {
static let uirealm = RealmService()
private var _initRS = try! Realm()
var realm: Realm! {
return _initRS
}
}
Novel.swift :
import RealmSwift
class Novel : Object {
#objc dynamic var uid : String? = nil
#objc dynamic var title: String? = nil
override static func primaryKey() -> String {
return "uid"
}
}
extension Novel {
func writeToRealm(){
try? RealmService.uirealm.realm.write {
print("Creating the Novel Object")
RealmService.uirealm.realm.add(self, update: true)
}
}
func DeleteFromRealm(object: Results<Novel>){
try? RealmService.uirealm.realm.write {
print("Deleting the Novel Object")
RealmService.uirealm.realm.delete(object)
}
}
}
and just implement as #Don mentioned, write in the block where you are setting the title.
var novel: Novel! // Declare this in above in the class.
novel = Novel()
novel.title = title
novel.uid = uid
novel.writeToRealm()
Hope that helped.

Realm database difficulties in IOS application

I am currently developing an application, and I am facing some problems I can not solve.
I currently have two views:
First view is a tableView representing my Realm objects.
Second view is for adding objects to my Realm database.
My problem is that, after adding a new object to my Realm database and immediately returning to the first view, sometimes the tableView does not recognize the newly added object but sometimes it does.
I hope this is information enough - ask me questions if needed.
override func viewWillAppear(_ animated: Bool) {
setupDataForViewTable()
tableView.delegate = self
tableView.dataSource = self
tableView.reloadData()
}
func setupDataForViewTable() {
let realmm = try! Realm()
let cryptocurrenciesFromDatabase = realmm.objects(crypto.self)
for crypto in cryptocurrenciesFromDatabase{
//For debugging
if crypto.name.count <= 0{
continue
}
let ROI = 0.0
let change24h = 0.0
let totalHolding = Double(crypto.buyingPrice) * Double(crypto.amount)
cryptoCurrencies.append((name : crypto.name, amount : crypto.amount, ROI : ROI, change24h : change24h, total : totalHolding))
}
}
Then there is the code from the second view, where i add stuff:
func updateCryptoInRealmDatabase(name : String, buyingPrice : Double, amount : Double){
DispatchQueue(label: "background").async {
autoreleasepool {
let realm = try! Realm()
let objectToUpdate = realm.objects(crypto.self).filter("name CONTAINS %#", name).first
try! realm.write {
objectToUpdate!.buyingPrice = (objectToUpdate!.buyingPrice + buyingPrice) / 2
objectToUpdate!.amount += amount
}
}
}
}
You need to make sure that tableView is updated when you navigate back to the first view. If that view is holding reference to Realm object and second view edited List in that object then it is enough to reload table. If you are holding reference to query of Realm objects then it will be refreshed automatically by Realm after commit update.
You can simply reload the view after it is shown again or register for Realm notifications and update the view as the second view does changes to it.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
If first view showing (same) Realm object that second view is editing then data is already up to date. We will get fresh count of related objects here:
func titleTableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return realmObject.relatedObjects.count
}
Update: Fixed refresh need for Realm Result and added link to notifications in Realm documentation.

Set UILabel Text at Completion of CloudKit Asyncronous Operation in Swift

I'm new to Swift, and asynchronous code in general, so tell me if this is way off. Basically I want to:
Open the App
That triggers a read of CloudKit records
Once the read is complete a UILabel will display the number of records retrieved
This clearly isn't useful in itself, but as a principle it will help me to understand the asynchronous code operation, and how to trigger actions on their completion.
// In ViewController Swift file:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
readDatabase()
}
#IBOutlet weak var myLabel: UILabel!
}
let VC=ViewController()
//In Another Swift file:
func readDatabase() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "myRecord", predicate: predicate)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
privateDB.perform(query, inZoneWith:nil) { (allRecs, err) in
VC.myLabel.text = ("\(allRecs?.count) records retreived")
/*
ERROR OCCURS IN LINE ABOVE:
CONSOLE: fatal error: unexpectedly found nil while unwrapping an Optional value
BY CODE LINE: Thread 8:EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
*/
}
}
I'm able to set the text field from within the viewDidLoad function, so why not from a function called within that function?
A few other things I've tried:
Use async dispatch to put it on thread 1
Implement a var with within the ViewController class, with a didSet that sets the text, set the var to the desired value in the privateDB.perform code to trigger the change
These both create the same problem as above.
Yes, I know there isn't any error handling in .perform, and yes there are records. If I trigger the setting of the UILabel text to the record count manually a few seconds after the view has loaded, it works fine.
So the question is...
How do I use the completion of the database read as a trigger to load attributes of the records to the view?
Thanks
Edit
What actually happened here was that VC was created globally, but never presented - since loadView was never called for it, myLabel didn't exist and, being a force unwrapped property, caused a crash when it was referenced
The problem is with this line: let VC=ViewController(). Here you instantiate a new instance of your ViewController class and try to set the label on that newly created instance. However, you would want to set the label on your viewController instance that is currently displayed.
Just change this line VC.myLabel.text = ("\(allRecs?.count) records retreived") to self.myLabel.text = ("\(allRecs?.count) records retreived") and it should work fine.
Got it, the solution looks like this:
// In ViewController Swift file:
typealias CompletionHandler = (_ recCount:Int,_ err:Error?) -> Void
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
readDatabase(completionHandler: { (recCount,success) -> Void in
if err == nil {
self.myLabel.text = "\(recCount) records loaded"
} else {
self.myLabel.text = "load failed: \(err)"
}
})
}
#IBOutlet weak var myLabel: UILabel!
}
//In Another Swift file:
func readDatabase() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "myRecord", predicate: predicate)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
privateDB.perform(query, inZoneWith:nil) { (allRecs, err) in
if let recCount = allRecs?.count {
completionHandler(recCount,err)
} else {
completionHandler(0,err)
}
}
}
The difference between this and the original is that this uses the CompletionHandler typealias in the function call for loading the database records, which returns the count of records and an optional error.
The completion operation can now live in the ViewController class and access the UILabel using self.myLabel, which solves the error that was occurring earlier, while keeping the database loading code separate to the ViewController class.
This version of the code also has basic error handling.

Firebase removeAllObservers() keeps making calls when switching views -Swift2 iOS

I read other stack overflow q&a's about this problem but it seems to be a tabBarController issue which I haven't found anything on.
I have a tabBarController with 3 tabs. tab1 is where I successfully send the info to Firebase. In tab2 I have tableView which successfully reads the data from tab1.
In tab2 my listener is in viewWillAppear. I remove the listener in viewDidDisappear (I also tried viewWillDisappear) and somewhere in here is where the problem is occurring.
override func viewDidDisappear(animated: Bool) {
self.sneakersRef!.removeAllObservers()
//When I tried using a handle in viewWillAppear and then using the handle to remove the observer it didn't work here either
//sneakersRef!.removeObserverWithHandle(self.handle)
}
Once I switch from tab2 to any other tab, the second I go back to tab2 the table data doubles, then if I switch tabs again and come back it triples etc. I tried setting the listener inside viewDidLoad which prevents the table data from duplicating when I switch tabs but when I send new info from tab1 the information never gets updated in tab2.
According to the Firebase docs and pretty much everything else I read on stackoverflow/google the listener should be set in viewWillAppear and removed in viewDidDisappear.
Any ideas on how to prevent my data from duplicating whenever I switch between back forth between tabs?
tab1
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
class TabOneController: UIViewController{
var ref:FIRDatabaseReference!
#IBOutlet weak var sneakerNameTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.ref = FIRDatabase.database().reference()
}
#IBAction func sendButton(sender: UIButton) {
let dict = ["sneakerName":self.sneakerNameTextField.text!]
let usersRef = self.ref.child("users")
let sneakersRef = usersRef.child("sneakers").childByAutoID()
sneakersRef?.updateChildValues(dict, withCompletionBlock: {(error, user) in
if error != nil{
print("\n\(error?.localizedDescription)")
}
})
}
}
tab2
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
class TabTwoController: UITableViewController{
#IBOutlet weak var tableView: UITableView!
var handle: Uint!//If you read the comments I tried using this but nahhh didn't help
var ref: FIRDatabaseReference!
var sneakersRef: FIRDatabaseReference?
var sneakerArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.ref = FIRDatabase.database().reference()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let usersRef = self.ref.child("users")
//Listener
self.sneakersRef = usersRef.child("sneakers")
//I tried using the handle
//---self.handle = self.sneakersRef!.observeEventType(.ChildAdded...
self.sneakersRef!.observeEventType(.ChildAdded, withBlock: {(snapshot) in
if let dict = snapshot.value as? [String:AnyObject]{
let sneakerName = dict["sneakerName"] as? String
self.sneakerArray.append(sneakerName)
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
}), withCancelBlock: nil)
}
//I also tried viewWillDisappear
override func viewDidDisappear(animated: Bool) {
self.sneakersRef!.removeAllObservers()
//When I tried using the handle to remove the observer it didn't work either
//---sneakersRef!.removeObserverWithHandle(self.handle)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sneakerArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("sneakerCell", forIndexPath: indexPath)
cell.textLabel?.text = sneakerArray[indexPath.row]
return cell
}
}
tab3 does nothing. I just use it as an alternate tab to switch back and forth with
The removeAllObservers method is working correctly. The problem you're currently experiencing is caused by a minor missing detail: you never clear the contents of sneakerArray.
Note: there's no need for dispatch_async inside the event callback. The Firebase SDK calls this function on the main queue.
The official accepted Answer was posted by #vzsg. I'm just detailing it for the next person who runs into this issue. Up vote his answer.
What basically happens is when you use .childAdded, Firebase loops around and grabs each child from the node (in my case the node is "sneakersRef"). Firebase grabs the data, adds it to the array (in my case self.sneakerArray), then goes goes back to get the next set of data, adds it to the array etc. By clearing the array on the start of the next loop, the array will be empty once FB is done. If you switch scenes, you'll always have an empty array and the only data the ui will display is FB looping the node all over again.
Also I got rid of the dispatch_async call because he said FB calls the function in the main queue. I only used self.tableView.reloadData()
On a side note you should subscribe to firebase-community.slack.com. That's the official Firebase slack channel and that's where I posted this question and vzsg answered it.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
//CLEAR THE ARRAY DATA HERE before you make your Firebase query
self.sneakerArray.removeAll()
let usersRef = self.ref.child("users")
//Listener
self.sneakersRef = usersRef.child("sneakers")
//I tried using the handle
//---self.handle = self.sneakersRef!.observeEventType(.ChildAdded...
self.sneakersRef!.observeEventType(.ChildAdded, withBlock: {(snapshot) in
if let dict = snapshot.value as? [String:AnyObject]{
let sneakerName = dict["sneakerName"] as? String
self.sneakerArray.append(sneakerName)
//I didn't have to use the dispatch_async here
self.tableView.reloadData()
}
}), withCancelBlock: nil)
}
Thank for asking this question.
I have also faced same issue in past. I have solved this issue as follow.
1) Define array which contain list of observer.
var aryObserver = Array<Firebase>()
2) Add observer as follow.
self.sneakersRef = usersRef.child("sneakers")
aryObserver.append( self.sneakersRef)
3) Remove observer as follow.
for fireBaseRef in self.aryObserver{
fireBaseRef.removeAllObservers()
}
self.aryObserver.removeAll() //Remove all object from array.

Resources