I've got an app that I've started adding Realm to and I think I must be doing something wrong because I keep running into EXC_BAD_ACCESS when passing unpersisted objects between view controllers.
Here's a stripped down version of my app.
class TodoTask: Object {
dynamic var name: String = ""
let steps = List<Step>()
convenience init(name: String) {
self.init()
self.name = name
}
}
class Step: Object {
dynamic var name: String = ""
convenience init(name: String) {
self.init()
self.name = name
}
}
class TodoListController: UIViewController {
let todos = List<TodoTask>()
override func viewDidLoad() {
super.viewDidLoad()
var t1 = TodoTask(name: "Todo1")
let steps = [
Step("Do this"),
Step("Do that"),
]
t1.steps.appendContentsOf(steps)
var t2 = TodoTask(name: "Todo2")
let steps = [
Step("Do these"),
Step("Do those"),
]
t2.steps.appendContentsOf(steps)
todos.appendContentsOf([t1, t2])
// Display table of todos.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let detailsController = segue.destinationViewController as? TodoDetailViewController,
let selectedTask = getSelectedTask() {
detailsController.task = selectedTask
}
}
}
class TodoDetailViewController: UIViewController {
var task: TodoTask? // <<< EXC_BAD_ACCESS
// Display the task's steps.
}
Unfortunately, I can't figure out what triggers the EXC_BAD_ACCESS and it happens intermittently. I didn't copy a stacktrace (d'oh!) but I remember it being in the C++ destructor for some sort of Row object. This is odd to me because there doesn't seem to be a database file in my Documents folder.
I'm pretty confident this is a Realm-based error because I experienced no weird crashes until I converted my plain Swift objects to Realm objects.
My only hunch is that I might be using List wrong since the warning This method can only be called during a write transaction. is in the comments of the appendContentsOf method. However, I was under the impression that I could use Realm objects that weren't stored in a Realm just like normal Swift objects.
Am I using Realm objects wrong? Is there anything else I can look into?
I'm on Realm 0.95.2.
Thanks!
Edit:
func getSelectedTask() -> TodoTask? {
if let index = taskTableView.indexPathForSelectedRow {
return tasks[index]
}
return nil
}
The issue is that a Realm List should not be created directly. This class is only used to manage to-many relationships on Object models. Instead, you should use a standard Swift array to store the unpersisted Realm Objects.
So switch:
let todos = List<TodoTask>()
to
let todos = [TodoTask]()
Related
Working Code
Inside my tableView's didSelectRowAt IndexPath method, I have a call that updates the UserSave model, which is in the default realm. The function is structured like so:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let UserRealm = UserSave()
let realm = try! Realm()
try! realm.write {
UserRealm.articleLink = newsLink[indexPath.row]
UserRealm.articleBody = newsData[indexPath.row]
UserRealm.articleTitle = newsTitle[indexPath.row]
UserRealm.articleAuthor = newsSrc[indexPath.row]
}
performSegue(withIdentifier: "webKitSegue", sender: self)
}
When this is run, the realm updates with the new values, as it should.
Problem Code
I have a second model, UserPrefs, which is also a part of the default realm. It is called inside function exrefresh(writeToRealm: String). The function looks like this:
func exrefresh(passed: String) {
let UserRealm = UserPrefs()
let realm = try! Realm()
try! realm.write {
UserRealm.fetchUrl = passed
}
self.refreshControl!.beginRefreshing()
self.refreshControl!.sendActions(for: .valueChanged)
}
When this function runs, however, the realm maintains its default values, and does not update with the new one.
Models
// UserSave
import RealmSwift
class UserSave: Object {
#objc dynamic var articleTitle = "Default Title"
#objc dynamic var articleAuthor = "Default Author"
#objc dynamic var articleLink = "https://example.com"
#objc dynamic var articleBody = "Default Body"
}
// UserPrefs
import RealmSwift
class UserPrefs: Object {
#objc dynamic var applicationDark = false
#objc dynamic var fetchUrl = "https://example.com/"
}
The Issue
I can update the UserSave model just fine, though I am unable to change the values in UserPrefs, even though both are in the default realm. I use the same code (with the names substituted) to update both models, and only one works properly. I have the .realm file pulled up in the Realm Browser, and am able to watch as UserSave changes. I have followed the guide from realm.io, and their code only works on one model.
Thanks for any help in advance.
Your question mentions default values. Assuming the objects already exist, you would need to get that object to then be able to update it. Otherwise Realm would not know what object you're referring to.
Here's the code to write an object.
func exrefresh(passed: String) {
let UserRealm = UserPrefs()
UserRealm.fetchUrl = passed
let realm = try! Realm()
try! realm.write {
realm.add(UserRealm)
}
}
If you are going to only ever have one object of that type, then here's the code to update it.
let realm = try! Realm()
let results = realm.objects(UserPrefs.self)
let theOnlyOne = results.first!
try! realm.write {
theOnlyOne.fetchUrl = passed
}
This assumes you have proper error checking to know they object exists before updating.
I just start to learn Realm data persistence, I start it from a iOS test project.
The realm object is declared like this:
class AchievementRecord: Object {
dynamic var dateID:String = "1111-00-00"
dynamic var date:String = "0000-00-00"
dynamic var apple:Int = Int(0)
override static func primaryKey() -> String? {
return "dateID"
}
}
I initialise the object in View Controller's viewDidLoad() method as this:
class AchievementRecord: Object {
dynamic var dateID:String = "1111-00-00"
dynamic var date:String = "0000-00-00"
dynamic var apple:Int = Int(0)
override static func primaryKey() -> String? {
return "dateID"
}
}
then I declare another function to obtain the save data as:
let appleOn_05 = defaultRealm.objects(AchievementRecord.self).filter("dateID = '05-06-2017'")
print(appleOn_05)
In the console, Xcode says:
Because I need to retrieve the apple's number, which is 22 in the console. How can I retrieve the apple's number to demo it on the screen, how can I do it? Thanks in advance.
Results works like native Swift collections in many ways. If you are fetching a single object, you can just access it with Results.first let appleOn_05 = defaultRealm.objects(AchievementRecord.self).filter("dateID = '05-06-2017'").first
Subclasses of Object work like any other native class instance in Swift, so you can access their properties using the dot syntax.
let apple = appleOn_05.apple
Combining the two:
if let appleOn_05 = defaultRealm.objects(AchievementRecord.self).filter("dateID = '05-06-2017'").first {
let apple = appleOn_05.apple
}
I'm quite familiar with Sqlite, but decided to try using realm for my next project.
I'm having trouble with reading data from the db and deleting objects
as well.
I'm using the default realm path:
let realm = RLMRealm.defaultRealm()
When a button is pressed an RLMObject should either be added or deleted (if already there). This is the IBAction for the button:
#IBAction func addToFavor(sender: UIBarButtonItem) {
// Create RealmTV (RLMObject)
let tvShow = RealmTV(id: id, title: TitleLabel.text!, posterPath: posterUrl)
if favoriteButton.image!.isEqual(UIImage(named: "Favor unfilled")) {
realm.beginWriteTransaction()
// Create or update tv-show in database
RealmTV.createOrUpdateInDefaultRealmWithValue(tvShow)
try! realm.commitWriteTransaction()
// Change button state
favoriteButton.image = UIImage(named: "Favor filled")
}
else
{
realm.beginWriteTransaction()
// Delete tv-show object from database
realm.deleteObject(tvShow) /* RLMException here */
try! realm.commitWriteTransaction()
// Change button state
favoriteButton.image = UIImage(named: "Favor unfilled")
}
}
When I try to delete the object after it has been added to db. I get an RLMExecption saying:
'Can only delete an object from the Realm it belongs to.'
I understand what the above reason mean, but not how to solve it?
And also how do I retrieve only this object from db after it has been added?
EDIT
This is my RealmTv class:
import UIKit
import Realm
class RealmTV: RLMObject {
dynamic var id = ""
dynamic var title = ""
dynamic var posterPath = ""
override class func primaryKey() -> String? {
return "id"
}
override init() {
super.init()
}
init(id: String, title: String, posterPath: String) {
super.init()
self.id = id
self.title = title
self.posterPath = posterPath
}
}
What the error message is trying to convey is that the object you pass to -[RLMRealm deleteObject:] must belong to the Realm that you're trying to delete the object from. In your case you're passing a new object that does not belong to any Realm (such an object is referred to as a standalone or unpersisted object in Realm's documentation). Instead you must pass either an object that you have retrieved from the Realm (using -[RLMRealm objectForPrimaryKey:], +[RLMObject allObjectsInRealm:], etc.), or added to the Realm (using -[RLMRealm addObject:]).
Reworking your code to meet these requirements would look something like:
if favoriteButton.image!.isEqual(UIImage(named: "Favor unfilled")) {
realm.beginWriteTransaction()
// Create or update tv-show in database
let tvShow = RealmTV(id: id, title: TitleLabel.text!, posterPath: posterUrl)
RealmTV.createOrUpdateInDefaultRealmWithValue(tvShow)
try! realm.commitWriteTransaction()
// Change button state
favoriteButton.image = UIImage(named: "Favor filled")
}
else {
realm.beginWriteTransaction()
// Delete tv-show object from database
let tvShow = RealmTV.objectForPrimaryKey(id)
realm.deleteObject(tvShow)
try! realm.commitWriteTransaction()
// Change button state
favoriteButton.image = UIImage(named: "Favor unfilled")
}
I am following the current tutorial on creating a large class to handle seeding a large database with data.
http://www.andrewcbancroft.com/2015/02/25/using-swift-to-seed-a-core-data-database/
My database is populated using JSON, though I am wanting to copy the pattern the author uses in the above article.
During the article he mentions this approach violates the single-use responsibility. I am aware that classes should take a single responsibility, but given a situation such as mine where I will need to seed quite a large dataset when the user logs in for example, is there another approach to take?
I apologise if this comes off as inciting a discussion, that isn't my intention, my question is wether this style of seeding is commonplace in production or if not, what is the best pattern to implement this kind of data seeding.
I don't think it's possible to really answer how everyone imports data in production as everyone could do different things.
Instead, I just want to mention that according to Apple's "Core Data Programming Guide" the most efficient way to import data is via a batch import process. This process is detailed here.
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html
With that said I would store your data in a JSON file that is stored either on a web service, or in the app bundle as a resource, and then use the NSJsonSerialization class to convert it to foundation objects that your code can reason with. Then I would use the principals outlined in the guide above to create a bulk import process to seed your database.
That's pretty much it, and Apple's examples are pretty straight forward. I would also state it would be best to run this process on a background thread as the OS may terminate your application if the import takes a long time to complete.
Hope this helps!
* EDIT *
Here is an example of how you can use protocols and generics to perform tasks against any type of object. You can use this pattern to perform any type of operation, so just take the concept and enter your Core Data logic.
This is merely an example of a pattern one could follow, and shouldn't be considered a plug-in-play implementation. It will need to be adapted to support the Core Data bulk import and saving. However, it does clearly show a way to take a dictionary, or array of dictionaries, and decode them to objects. Then what you do with your objects is completely up to you.
protocol JSONDecodable {
// This is used so you can have a unified way to instantiate an instance without relying on sub-classing NSObject
init()
// This can be implemented so you can manually decode a single object of the type from a dictionary
static func decodeFromJSON(json: AnyObject?) -> AnyObject?
// This is used so that you can manually set the values to the object. This is required as Swift reflection doesn't support dynamic property setting
func setValueForKey(value: AnyObject?, forKey: String)
}
// This class is responsible for decoding a JSON dictionary into an object
class JSONDecoder<T:JSONDecodable>: NSObject {
//MARK: Initialization
required override init() {
// do any initialization here
}
//MARK: Public Methods
/**
Creates a single object from the JSON. This method should only be used if the JSON will only ever contain a single object
:json: A dictionary of data
:returns: An object of the given type
*/
func toSingle(json: AnyObject?) -> T? {
// process single object, and return an array with the one object
if let dict = json as? [NSObject: AnyObject] {
return self.makeObject(dict)
}
return nil
}
/**
Creates a list of objects from the JSON. This method should only be used if the JSON will contain multiple objects
:json: A dictionary of data
:returns: An list of objects of the given type
*/
func toArray(json: AnyObject?) -> [T]? {
// process array
if let arr = json as? [AnyObject] {
return self.makeObjects(arr)
} else if let dict = json as? [NSObject: AnyObject] {
// process single object, and return an array with the one object
var arr = [T]()
arr.append(self.makeObject(dict))
return arr
}
return nil
}
//MARK: The Magic
private func makeObjects(jsonArray: [AnyObject]?) -> [T]? {
var returnArray: [T] = [T]()
if let jArray = jsonArray {
for jObject in jArray {
if let dict = jObject as? [NSObject: AnyObject] {
returnArray.append(self.makeObject(dict))
}
}
}
if returnArray.count > 0 {
return returnArray
} else {
return nil
}
}
private func makeObject(jsonDict: [NSObject: AnyObject]) -> T {
var returnObject = T.self() // this is where the init() function in the protocol comes in handy. It allows us to use generics to create a dynamic instance of our object
for (key, value) in jsonDict {
if let k = key as? String {
returnObject.setValueForKey(value, forKey: k) // this is where the setValueForKey(value: AnyObject?, forKey: String) function in the protocol comes in handy. It allows us to let the object it's self set it's own values.
}
}
return returnObject
}
}
// This is an example class that implements the protocol which means it can be sent through the decoding process
class Employee: NSManagedObject, JSONDecodable {
//MARK: - Properties
var employeID: Int!
var name: Int!
var hireDate: NSDate?
var department: Department?
//MARK: - Initialization
override required init() {
// Necessary to satisfy the JSONDecodable protocol
}
static func decodeFromJSON(json: AnyObject?) -> AnyObject? {
var decoder = JSONDecoder<Employee>()
return decoder.toSingle(json)
}
func setValueForKey(value: AnyObject?, forKey: String) {
switch (forKey) {
case "employeID":
self.employeID = value as! Int
case "name":
self.name = value as! String
case "hireDate":
if let v = value as? String {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
self.hireDate = dateFormatter.dateFromString(v)
}
case "department":
if let v = value as? [NSObject: AnyObject] {
if let dept = Department.decodeFromJSON(dict) as? Department {
self.department = dept
}
}
default:
NSLog("[setValueForKey] Unable to find property \(forKey)")
}
}
}
I am attempting to store data in the local data store using parse.com(1.7.3) in Swift iOS. Following the parse.com docs here, I am able to subclass PFObject to store a simple object. However I am hoping to compose a custom object inside this object and am finding difficulties in doing this.
I have stripped this problem back to its most basic and include my code below. Basically I am storing a PFObject subclass called 'Test', which will store a String(testString), and a custom object called Test2. Test2 in turn will also store a String(testString). I assume that Test2 also needs to subclass PFObject.
Test - testString(String)
- test2(Test2)
- testString(String)
AppDelegate.swift
(Registering subclass in AppDelegate instead of Initialise method - see here for more details.)
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
Test.registerSubclass()
Test2.registerSubclass()
Parse.enableLocalDatastore()
Parse.setApplicationId("...", clientKey: "...")
}
Test2.swift
class Test2:PFObject, PFSubclassing {
#NSManaged var testString:String
static func parseClassName() -> String {
return "Test2"
}
}
Test.swift
class Test:PFObject, PFSubclassing {
#NSManaged var testString:String
#NSManaged var test2:Test2
static func parseClassName() -> String {
return "Test"
}
}
ViewController.swift
override func viewDidLoad() {
saveTest() //toggle between save and load, don't do both!
//loadTest()
}
func saveTest() {
var test2 = Test2()
test2.testString = "I am Test 2"
var test = Test()
test.testString = "I am Test"
test.test2 = test2
test.pinInBackground()
}
func loadTest() {
let query = PFQuery(className:Test.parseClassName())
query.fromLocalDatastore()
query.findObjectsInBackgroundWithBlock({
(objects:[AnyObject]?, error: NSError?) in
if let error = error {
println("THERE WAS AN ERROR")
// There was an error
} else {
if let objects = objects as? [PFObject] {
for object in objects {
//We have an object!
println("We have a PFObject, now to cast as Test")
if let object = object as? Test {
println(object)
}
}
}
}
})
}
So the println in loadTest outputs the following in the console:
<Test: 0x7ff2da75e810, objectId: new, localId: local_15a085d00030543f> {
test2 = "<Test2: 0x7ff2db90fe50, objectId: new>";
testString = "I am Test";
}
If I try to access the test2 property or a property of test2, the app hangs. I also am not able to 'fetch' the Test object. Curiously If I load any Test2 objects stored locally(identical to the loadTest method above with any mention of Test replaced with Test2), I do find the Test2 object:
<Test2: 0x7f94b1f1e850, objectId: new, localId: (null)> {
testString = "I am Test 2";
}
So in pinning the test object, test2 is being pinned too. However the test2 object is not connecting to the test object for some reason.
I'm starting to get lost, and can't seem to find relevant(or accurate) documentation or sample projects which would probably clear all this up in a second... What is the best approach(if any) to compose a custom object inside a PFObject subclass?
Here's an idea which can help you in this case. Your Test class is a subclass of PFObject. And you have a custom object inside Test class, named Test2.
In your Test2.swift file, create a method that would return an NSDictionary object containing all properties of its object.
Somewhat similar to this :
class func objDetail() -> NSDictionary {
return [
"key1": self.property1,
"key2": self.property2,
// ...
] as NSDictionary
}
And you could use this dictionary to store your object details as an object in your Test object. Now, parse supports dictionary and you've converted it in dictionary. It would save the object.
Now add another method in your swift file to create object based on dictionary values.
convenience init(_ dictionary: Dictionary<String, AnyObject>) {
self.init()
title = dictionary["title"] as? NSString
shortDescription = dictionary["shortDescription"] as? NSString
newsDescription = dictionary["newsDescription"] as? NSString
link = dictionary["link"] as? NSURL
}
For retrieving object, in your loadTest method's query.findObjectsInBackgroundWithBlock call this object's init method and create object with values of dictionary you get from parse.
Hope, my idea helps you.. :)