I'm having an issue with Realm Swift. I have an object which is suppose to store information about the user created character. However certain properties are not saving. If I switch the name of the object just by one letter it saves and reads back correctly. The first example refuses to save anything but the default value for the race property, but the second example saves any value to the racea property with no issue. What is causing this?
Example 1
class Character: Object {
//MARK: Properties
dynamic var id: Int = 1
dynamic var name: String = "John Appleseed"
dynamic var level: Int = 1
dynamic var exp: Int = 0
dynamic var race: Int = 0
dynamic var career: Int = 0
dynamic var currentHealth: Int = 100
dynamic var inventory: Inventory? = Inventory()
//MARK: Realm
override static func primaryKey() -> String? {
return "id"
}
}
Example 2
class Character: Object {
//MARK: Properties
dynamic var id: Int = 1
dynamic var name: String = "John Appleseed"
dynamic var level: Int = 1
dynamic var exp: Int = 0
dynamic var racea: Int = 0
dynamic var career: Int = 0
dynamic var currentHealth: Int = 100
dynamic var inventory: Inventory? = Inventory()
//MARK: Realm
override static func primaryKey() -> String? {
return "id"
}
}
Extension
extension Character {
func getRace() -> String {
return Fitventure.species[race]
}
}
If the getRace() function was causing the issue, then it sounds like to me that this might be an unfortunate collision between Swift and the Objective-C runtime reflection features that Realm uses to work.
Realm dynamically generates its own accessors for each of its properties in order to explicitly manage how data is saved and retrieved from disk. As a result, it's not possible to override the accessors of Realm properties, and doing so will create strange behavior.
Best practice is what you've already discovered: when creating another method that transforms the Realm property somehow, you need to make sure the function doesn't have a name that might have been implicitly generated by the Objective-C runtime.
Related
in realm i given id = 0 and it is as primary key and it will be auto increment, but problem is while updating it is saving in the index path 0 as declare as id : Int = 0.
Where ever i update also it is only updating in 0th index only.
i want to update as per selected object.
What to do?
Program :-
class Discount: Object {
#objc dynamic var id : Int = 0
#objc dynamic var offerName : String = ""
#objc dynamic var percentage: Float = 0.00
#objc dynamic var segmentIndex : Int = 0
#objc dynamic var dateWise: Date?
override class func primaryKey() -> String? {
return "id"
}
//Incrementa ID
func IncrementaID() -> Int{
let realm = try! Realm()
if let retNext = realm.objects(Discount.self).sorted(byKeyPath: "id").last?.id {
return retNext + 1
}else{
return 1
}
}
}
Generally speaking, auto-incrementing primary keys are challenging to deal with and can cause headaches long term.
What's generally most important is ensuring primary keys are unique and using UUID strings is ideally suited for that.
class Discount: Object {
#objc dynamic var discount_id = UUID().uuidString
override static func primaryKey() -> String? {
return "discount_id"
}
}
There may be concern about ordering and often times that managed by either adding a class var to determine ordering; like a timestamp for example or if you want to preserve ordering, objects can be added to a List, which keeps the order, like an array.
To answer your specific question, the code in your question is not complete (it was partially pulled from another question). The reason is that for each object that's created, it must be written to realm first, then the next object's primary key is based on the prior object.
Here's an example.
#objcMembers class User: Object {
dynamic var uid: Int = 0
dynamic var username: String?
func getNextUid() -> Int {
let realm = try! Realm()
if let lastObject = realm.objects(User.self).sorted(byKeyPath: "uid").first {
let lastUid = lastObject.uid
let nextUid = lastUid + 1
return nextUid
}
return 1
}
override static func primaryKey() -> String? {
return "uid"
}
}
now the sequence to use this is as follows
let u0 = User()
u0.uid = u0.getNextUid()
u0.username = "User 0"
let realm = try! Realm()
try! realm.write {
realm.add(u0)
}
let u1 = User()
u1.uid = u1.getNextUid()
u1.username = "User 1"
try! realm.write {
realm.add(u1)
}
as you can see, each object needs to be written to realm in order to the next object to be queried to get the prior objects primary key.
It's a whole lot of potentially unnecessary work and code.
My advice: Stick with the UUID().uuidString for primary keys.
I have created a Realm object that needs to store an enum value. To do that I use a method outlined in this question which involves declaring a private property of type String, and then declaring another property of type Enum that sets/reads the private property using getters and setters.
For ease of reference here is the code for that:
#objcMembers
class PlaylistRealmObject: Object {
dynamic var id: String = UUID().uuidString
dynamic var created: Date = Date()
dynamic var title: String = ""
private dynamic var revisionTypeRaw: String = RevisionType.noReminder.rawValue
var revisionType: RevisionType {
get { return RevisionType(rawValue: revisionTypeRaw)! }
set { revisionTypeRaw = newValue.rawValue }
}
let reminders = List<ReminderRealmObject>()
let cardsInPlaylist = List<CardRealmObject>()
override static func primaryKey() -> String? {
return "id"
}
}
I have noticed though that if I add a convenience init to the class declaration (to make it a bit easier to initialise the object) the revisionType properties on the objects I end up with adopt the default value declared in the class, and NOT the revision type value passed to the class using the convenience init.
Here is the class declaration with a convenience init
#objcMembers
class PlaylistRealmObject: Object {
dynamic var id: String = UUID().uuidString
dynamic var created: Date = Date()
dynamic var title: String = ""
private dynamic var revisionTypeRaw: String = RevisionType.noReminder.rawValue
var revisionType: RevisionType {
get { return RevisionType(rawValue: revisionTypeRaw)! }
set { revisionTypeRaw = newValue.rawValue }
}
let reminders = List<ReminderRealmObject>()
let cardsInPlaylist = List<CardRealmObject>()
convenience init(title: String, revisionType: RevisionType) {
self.init()
self.title = title
self.revisionType = revisionType
}
override static func primaryKey() -> String? {
return "id"
}
}
And - to make things even more perplexing - if I simply remove the word 'private' from the revisionTypeRaw property, everything works fine!
I am confused. 1) Why does adding a convenience init have this effect? 2) Why does making the property 'public' resolve the issue?
I have created a demo Xcode project to illustrate the issue and can share it if anyone needs it.
Update:
I found the problem. It has nothing to do with the convenience init. I am using #objcMembers at the top of the class as per the Realm docs: https://realm.io/docs/swift/latest/#property-attributes
If you remove this and place #objc in front of the private keyword, everything works as would be expected. I guess the question then is: what explains this behaviour?
This is a good question but I think the issue is elsewhere in the code. Let's test it.
I created a TestClass that has a Realm managed publicly visible var, name, as well as a non-managed public var visibleVar which is backed by a Realm managed private var, privateVar. I also included a convenience init per the question. The important part is the privateVar is being set to the string "placeholder" so we need to see if that is overwritten.
class TestClass: Object {
#objc dynamic var name = ""
#objc private dynamic var privateVar = "placeholder"
var visibleVar: String {
get {
return self.privateVar
}
set {
self.privateVar = newValue
}
}
convenience init(aName: String, aString: String) {
self.init()
self.name = aName
self.visibleVar = aString
}
}
We then create two instances and save them in Realm
let a = TestClass(aName: "some name", aString: "some string")
let b = TestClass(aName: "another name", aString: "another string")
realm.add(a)
realm.add(b)
Then a button action to load the two objects from Realm and print them.
let testResults = realm.objects(TestClass.self)
for test in testResults {
print(test.name, test.visibleVar)
}
and the output:
some name some string
another name another string
So in this case, the default value of "placeholder" is being overwritten correctly when the instances are being created.
Edit:
A bit more info.
By defining your entire class with #objMembers, it exposes your propertites to Objective-C, but then private hides them again. So that property is not exposed to ObjC. To reverse that hiding, you have to say #objc explicitly. So, better practice is to define the managed Realm properties per line with #objc dynamic.
I thought this would be pretty straightforward after reading here and here but I'm a bit stuck.
I have a 'favouriteWorkout' object that looks like this :
class FavouriteObject: Object {
#objc dynamic var favouriteWorkoutName = ""
#objc dynamic var workoutReference = WorkoutSessionObject()
override class func primaryKey() -> String? {
return "favouriteWorkoutName"
}
}
What I'm trying to do here is reference a WorkoutSessionObject in Realm that links from a WorkoutName when a workout is saved as a favourite.
My WorkoutSessionObject has a primary key of workoutID which is a UUID string. It looks like this :
class WorkoutSessionObject: Object {
#objc dynamic var workoutID = UUID().uuidString
#objc dynamic var workoutType = ""
let exercises = List<WorkoutExercise>()
#objc dynamic var totalExerciseCount = 0
#objc dynamic var rounds = 0
#objc dynamic var favourite : Bool = false
override class func primaryKey() -> String? {
return "workoutID"
}
}
I've then tried to save using this :
let favouriteWorkout = FavouriteObject()
favouriteWorkout.favouriteWorkoutName = favouriteName
favouriteWorkout.workoutReference = (realm.object(ofType: WorkoutSessionObject.self, forPrimaryKey: self.workoutID))!
do {
try realm.write {
realm.add(favouriteWorkout)
}
} catch {
print ("Error adding favourite")
}
but i get a crash when I run of :
'RLMException', reason: 'The FavouriteObject.workoutReference property must be marked as being optional.
However, when I then try to make it optional (by adding ?) it says
"Cannot use optional chaining on non-optional value of type 'WorkoutSessionObject"!
Summary
I want to save a reference of the workoutID of a WorkoutSessionObject in my FavouriteObject which is an actual link to the WorkoutSessionObject (so the properties can be accessed from favourites)
Update
using the answers below I've now sorted the problem of the workout reference. This is now showing in Realm as the proper format () under "workoutReference". However, I'm now getting "nil" in "workoutReference" when trying to save. I know the workoutID is coming through correctly as I am printing it in the console.
You need to change the declaration of workoutReference. First of all, you need to make it Optional by writing ? after the type. Secondly, you shouldn't assign a default value to it, it needs to be Optional for a reason. The linked docs clearly state that
to-one relationships must be optional
, and workoutReference is clearly a to-one relationship.
class FavouriteObject: Object {
#objc dynamic var favouriteWorkoutName = ""
#objc dynamic var workoutReference:WorkoutSessionObject?
override class func primaryKey() -> String? {
return "favouriteWorkoutName"
}
}
In property-cheatsheet you can see that a non-optional Object-property is not allowed, so you have to change it like the following:
class FavouriteObject: Object {
#objc dynamic var favouriteWorkoutName = ""
// here you have to make the property optional
#objc dynamic var workoutReference: WorkoutSessionObject?
override class func primaryKey() -> String? {
return "favouriteWorkoutName"
}
}
In a situation where a random 'workout' might have any number of exercises from say, 5 - 20, is there a way to save a 'workout object' in Realm where there is a variable number of values (exercises)?
e.g. I currently have something like this as my object model:
#objc dynamic var workoutID = UUID().uuidString
#objc dynamic var workoutName = ""
#objc dynamic var totalExercisesCount = 0
#objc dynamic var exerciseOne = ""
#objc dynamic var repsExerciseOne = 0
#objc dynamic var exerciseTwo = ""
#objc dynamic var repsExerciseTwo = 0
#objc dynamic var exerciseThree = ""
#objc dynamic var repsExerciseThree = 0
#objc dynamic var exerciseFour = ""
#objc dynamic var repsExerciseFour = 0
#objc dynamic var exerciseFive = ""
#objc dynamic var repsExerciseFive = 0
#objc dynamic var exerciseSix = ""
#objc dynamic var repsExerciseSix = 0
This works fine but it also frequently populates many fields with 'null' (for example if there are only 3 exercises in a specific workout and 6 dynamic var's as per above).
It just feels like quite an inelegant solution but not sure if there is a better way?
You should be using a List to store a dynamic number of variables attached to your workout session. You should create a WorkoutExercise class that contains the reps and name of the exercise and store a list of WorkoutExercises in your WorkoutSession model.
class WorkoutExercise: Object {
#objc dynamic var name = ""
#objc dynamic var reps = 0
}
class WorkoutSession: Object {
#objc dynamic var workoutID = UUID().uuidString
#objc dynamic var workoutName = ""
let exercises = List<WorkoutExercise>()
var totalExerciseCount: Int {
return exercises.count
}
}
I'd also recommend changing totalExerciseCount to a computed property (which will also make it ignored, since you can't store computed properties in Realm), since its value should always reflect the number of elements in exercises and there's no need to make it a persisted property, because you can always recompute it without a big computational cost.
Problem
I need to save a List in Realm, which is made up of a the properties of an Array of Struct Objects (which has been passed through a Segue and is popualating a tableview). This is in the form of an 'exercise name' and 'number of reps' on each row.
What have I tried?
I have matched the Realm Object with the Struct in terms of fields and format and attempted to save the array as a list e.g. "=List< array >" but this doesn't work ("use of undeclared type"). I've also tried various methods of trying to save the properties of each table row but again, couldn't get that to work (e.g. = cell.workoutname)
Research I found this How to save a struct to realm in swift? however, this isn't for saving arrays of objects I don't think. This did however (first answer), give me the idea of potentially saving the values contained within each row to Realm instead of the actual Struct array. I also found this Saving Array to Realm in Swift? but I think this is for when the array is already made up of Realm Objects, not Struct instances like in my case.
Code and details
Structs
I have a Struct as per below. Another struct, (Workout Generator) has a function which generates x number of instances of these objects. These are then passed via a Segue to a new VC TableView (each row displays a workout name and number of reps):
struct WorkoutExercise : Hashable, Equatable{
let name : String
let reps : Int
var hashValue: Int {
return name.hashValue
}
static func == (lhs: WorkoutExercise, rhs: WorkoutExercise) -> Bool {
return lhs.name == rhs.name
}
}
I then have the following Realm Objects. One is for saving a 'WorkoutSession'. This will contain a Realm List of WorkoutExercise Realm objects.
class WorkoutSessionObject: Object {
#objc dynamic var workoutID = UUID().uuidString
#objc dynamic var workoutName = ""
let exercises = List<WorkoutExerciseObject>()
var totalExerciseCount: Int {
return exercises.count
}
}
class WorkoutExerciseObject: Object {
#objc dynamic var name = ""
#objc dynamic var reps = 0
}
I have tried the following code when trying to save the Workout details to Realm :
func saveToRealm() {
let workoutData = WorkoutSessionObject()
workoutData.workoutName = "test"
workoutData.workoutID = UUID().uuidString
workoutData.exercises = List<selectedWorkoutExerciseArray>
}
What I think I need to do from reading the other answers
Option 1 - instead of trying to save the actual array, save the 'name' and 'reps' from each table row instead?
Option 2 - somehow convert the 'selectedWorkoutExerciseArray' into a list of realm objects?
of course there might be other options! Any help/ideas appreciated!
Why populate 2 separate lists if it needs to be persistent anyway? Just use the list in Realm to populate your table view. Here's a simple example of populating the list using append (just like any array):
class SomeClass: Object {
#objc dynamic var id: String = ""
var someList = List<SomeOtherClass>()
convenience init(id: String) {
self.init()
self.id = id
}
}
#objcMembers class SomeOtherClass: Object {
dynamic var someValue: String = ""
convenience init(value: String) {
self.init()
someValue = value
}
}
func addToList(someOtherClass: SomeOtherClass) {
let realm = try! Realm()
if let someClass = realm.objects(SomeClass.self).last {
do {
try realm.write({
someClass.someList.append(someOtherClass)
})
} catch {
print("something went wrong")
}
}
}
I have a very similar functionality, that allow the user to select from a table view. What I do is create a List from the selection like:
var arrayForSelectedObjects = [CustomObject]()
...
let aList = List<CustomObject>()
aList.append(objectsIn: arrayForSelectedObjects)
//I then assign the created list to the main object and save it.
let realmObject = MainObject()
realmObject.list = aList
My CustomObject is also stored on the realm db.
My MainObject is defined like so:
class MainObject : Object {
#objc dynamic var title: String?
var list = List<CustomObject>()
}