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")
}
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.
Let's say I have Queue class that has a unique title and can hold a list of objects from my other class Item.
class Queue: Object {
#objc dynamic var title = ""
let items = List<Item>()
override static func primaryKey() -> String? {
return "title"
}
}
I want to have n (probably 3-5) instances of Queue from the time the app gets installed available in the database, so I can access them at any time to add some Items to the list of items. Is there a way to create those queues and save them to the database just once when the app gets first launched and where exactly in the code should I do it?
You can check somewhere at the start of your app how many Queues you have right now:
let realm = try! Realm()
if realm.objects(Queue.self).isEmpty {
// no items, so you should create n items
}
Add new object for Realm
class Task : Object {
#objc dynamic var id : Int = 0
#objc dynamic var name = ""
#objc dynamic var phone = ""
#objc dynamic var address = ""
}
#IBAction func buttonSave(_ sender: Any) {
let realm = try! Realm()
let user = Task()
user.id = 0
user.name = (txtName.text! as NSString) as String
user.phone = (txtPhone.text! as NSString) as String
user.address = (txtAddress.text! as NSString) as String
try! realm.write {
realm.add(user)
print("user:",user.name)
}
}
I am creating a project. I want if the userID already exist, it doesn't add the user. But somehow my code isn't working properly.
This is my Realm Model Object (User.swift):
import Foundation
import RealmSwift
class User: Object {
#objc dynamic var userID = Int()
#objc dynamic var username = ""
#objc dynamic var full_name = ""
#objc dynamic var myBool = Bool()
override static func primaryKey() -> String? {
return "userID"
}
}
And this is the button to add users:
#IBAction func add(_ sender: Any) {
let myUser = User()
let JSON_userID = Int(arc4random_uniform(5)) // This is temporary. I am going to get code from JSON, but using random for testing purpose.
if (myUser.userID != JSON_userID) {
myUser.userID = JSON_userID
myUser.username = "myUsername"
myUser.full_name = "My Name"
let realm = try! Realm()
try! realm.write {
realm.add(myUser)
}
}
else {
print("Already exist")
}
}
Sometimes it runs the code, but most of the times it crashes with error:
libc++abi.dylib: terminating with uncaught exception of type NSException.
As you defined a primary key in your User object, Realm can handle this automatically if you set the update parameter to true inside the write closure.
let realm = try! Realm()
try! realm.write {
realm.add(myUser, update: true)
}
If the update parameter is not set or false, Realm will throw an exception when you try to add an object with an existing primary key.
This makes the if / else condition useless. It can be removed.
If you need to know if the user already exists, you can request the Realm with the primary key value:
realm.object(ofType: User.self, forPrimaryKey: JSON_userID)
The result will be nil if the user does not exist.
I have my Device class defined as such:
class Device: Object {
dynamic var asset_tag = ""
}
I have an array like this ["43", "24", "23", "64"]
and I would like to loop through the array and add each one into the asset_tag attribute of the Device entity in Realm.
To create an array in Realm you use List. According to Realm's docs, List is the container type in Realm used to define to-many relationships. It goes on to say, "Properties of List type defined on Object subclasses must be declared as let and cannot be dynamic." This means you need to define an entirely separate object to create a list in Realm, there are no native types that will allow you to do something like
let assetTagList = List<String>().
You need to create an AssetTags object and make a list of it in your Device object as follows:
class AssetTags: Object {
dynamic var stringValue = ""
}
class Device: Object {
var asset_tags: [String] {
get {
return _assetTags.map { $0.stringValue }
}
set {
_assetTags.removeAll()
_assetTags.append(objectsIn: newValue.map({ AssetTags(value: [$0]) }))
}
}
let _assetTags = List<AssetTags>()
override static func ignoredProperties() -> [String] {
return ["asset_tags"]
}
}
Now you can do this to add asset tags to Device.
//adding asset tags
let realm = try! Realm()
try! realm.write {
let device = Device()
device.asset_tags = ["1", "2"]
realm.add(device)
}
//looking at all device objects and asking for list of asset tags
for thisDevice in realm.objects(Device.self) {
print("\(thisDevice.asset_tags)")
}
SECOND EDIT
This is not the way I would do it but I think this is what you might find easier to understand.
class AssetTags: Object {
dynamic var id = 0
override static func primaryKey() -> String? {
return "id"
}
dynamic var tagValue = ""
}
class Device: Object {
let assetTags = List<AssetTags>()
}
So now Device has a list of asset tags. I added a primary id for Asset Tags because you indicated you might want to add more properties so an id should help make each one unique. It is optional though, so you can remove it if you do not need it.
To add assetTags objects to a Device object in a for-loop style (note: device in this code exampl is the device object you want to save the asset tags to):
var assetTagArray: [String] = ["1", "2"]
let realm = try! Realm()
for assetTag in assetTagArray {
let newAssetTag = AssetTags()
newAssetTag.tagValue = assetTag
newAssetTag.id = (realm.objects(AssetTags.self).max(ofProperty: "id") as Int? ?? 0) + 1
do {
try realm.write {
device?.assetTags.append(newAssetTag)
}
} catch let error as NSError {
print(error.localizedDescription)
return
}
}
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]()