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
}
}
Related
I'm using ReamSwift to store data in my iOS application and I have troubles to figure out, why sometimes objects are duplicated in my application.
I use an UUID as primary key on every object. This property is defined in a base class and all the others are subclasses of that.
I use the update approach with a dictionary. So new objects are created and existing should be updated.
I use a central class which holds the Realm object and handles the update. This method could be called from multiple different threads.
This is my base class
import RealmSwift
open class RealmObject : Object {
static let ID_ATTRIBUTE : String = "id"
#objc dynamic var id : String = ""
override public static func primaryKey() -> String? {
return "id"
}
}
Central class managing swift.
This class holds the Realm Object
public var realm: Realm {
var tempRealm: Realm?
do {
tempRealm = try Realm(configuration: configuration)
}
catch let err {
print("------ERROR----- initializing realm\n \(err)")
}
if let tempRealm = tempRealm{
return tempRealm
}
return self.realm
}
This is the update method in the class. As a fallback if the id property is not set, it will be set as it is the primary key
func update<T: RealmObject>(_ obj : T, values : [String:Any?] ) -> T? {
var vals = values
do {
var res : T?
try realm.write {
if let idattr = vals[T.ID_ATTRIBUTE] as? String {
if(idattr.isEmpty) {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
} else {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
res = realm.create(T.self, value: vals, update: .modified)
}
return res
} catch {
return nil
}
}
Could calling the update method cause in any case the duplication as I set the primary key of the object? The problem is, I cannot reproduce to find the problem, i just encounter the issue from time to time and in the field from users.
One interesting thing, when a copy is deleted, also the other object will be deleted.
This is the method which deletes objects by id and type
func delete<T: RealmObject>(_ id : String, _ type : T.Type) -> Bool {
do {
let obj = get(id, T.self)
if let obj = obj {
try realm.write {
realm.delete(obj)
}
return true
} else {
return false
}
} catch {
return false
}
}
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>()
}
Below is my custom object class.
class UserGroups: NSObject {
let groupName: String
let users: [CheckIn]?
init(json:JSON) {
self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
}
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn]{
return jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
return CheckIn(json: jsonItem)
})
}
}
I've an array of above custom objects. How can I combine 2 or more custom objects into a single object by merging users of every object having same groupName.
Below is my CheckIn Model:
class CheckIn: NSObject {
let id: String
let firstName: String
let lastName: String
let latitude: String
let longitude: String
let hint: String
init(json: JSON) {
self.id = json[Constants.Models.CheckIn.id].stringValue
self.firstName = json[Constants.Models.CheckIn.firstName].stringValue
self.lastName = json[Constants.Models.CheckIn.lastName].stringValue
self.hint = json[Constants.Models.CheckIn.hint].stringValue
self.latitude = json["location"][Constants.Models.CheckIn.latitude].stringValue
self.longitude = json["location"][Constants.Models.CheckIn.longitude].stringValue
}
}
id field is not unique in CheckIn.
Here's a slightly simplified example that shows how to combine groups that have the same group name.
Here is the UserGroup class. users is now a variable (var) because we will be adding elements to groups to combine them.
class UserGroups: NSObject {
let groupName: String
var users: [String]?
init(groupName: String, users: [String]?) {
self.groupName = groupName
self.users = users
}
}
Here are three groups, two of the share the same group name, Blues.
let group1 = UserGroups(groupName: "Blues", users: ["Tom", "Huck", "Jim"])
let group2 = UserGroups(groupName: "Reds", users: ["Jo", "Ben", "Tommy"])
let group3 = UserGroups(groupName: "Blues", users: ["Polly", "Watson", "Douglas"])
Next, we'll put all the groups in an array.
let allGroups = [group1, group2, group3]
Here, we use Swift's reduce function to allow us to reduce the array to only groups with unique group names.
let compacted = allGroups.reduce([UserGroups](), { partialResult, group in
var dupe = partialResult.filter {$0.groupName == group.groupName }.first
if let dupeGroup = dupe {
dupeGroup.users?.append(contentsOf: group.users ?? [])
return partialResult
} else {
var newPartialResult = partialResult
newPartialResult.append(group)
return newPartialResult
}
})
The array is now reduced to unique groups, we print out all the groups and their users with the help of Swift's map function.
print(compacted.map { $0.users })
// Prints [
Optional(["Tom", "Huck", "Jim", "Polly", "Watson", "Douglas"]),
Optional(["Jo", "Ben", "Tommy"])
]
The Solution
You did not include the CheckIn model, but I will assume that it has some sort of an id field unique to each user. We will use this to make the object Hashable:
// Add this to your file outside of the UserGroups class
extension CheckIn: Hashable {
var hashValue: Int { return self.id }
}
Making it Hashable allows you to convert the Array to a Set, which does not allow duplicates and will remove them in a very efficient way.
// Change getUserGroupsList as follows
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
return Array(Set(jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
return CheckIn(json: jsonItem)
})))
}
Optional Considerations
As an aside, in case you're coming from another language, Swift gives you nice type inference and default names for closure arguments ($0 is the first argument). You can probably make the code a little less verbose, but it's a matter of taste which is preferred.
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
return Array(Set(jsonArray.flatMap { CheckIn(json: $0) }))
}
Also consider whether you really want the return value to be an array. If you want the list to always have unique users, it is a bit more efficient to use a Set as your return type and forgo the conversion back to an Array like this:
class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
return Set(jsonArray.flatMap { CheckIn(json: $0) })
}
Finally, consider whether you really need the users property to be optional. With sequence types, it is often sufficient to use an empty sequence to denote absence of a value. Depending on your situation, this may simplify your code. The final version looks like this:
class UserGroups: NSObject {
let groupName: String
let users: Set<CheckIn>
init(json:JSON) {
self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
}
class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
return Set(jsonArray.flatMap { CheckIn(json: $0) })
}
}
Maintaining Order
The caveat is that Set does not maintain the order of the items. If the order of the groups does matter, we can use this solution instead:
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
var encountered: Set<CheckIn> = []
return jsonArray.flatMap { CheckIn(json: $0) }.filter { encountered.update(with: $0) == nil }
}
In this version, we still use a set, but only to maintain a set of items we've encountered already. The update method on a set returns the same value if it's already in the set or returns nil if it's being inserted for the first time. We use that to filter our array to those items being encountered for the first time while adding them to the set of encountered items to filter them out when they are subsequently encountered again.
I have an issue about Inheritance with my Objects in Realm.
Could you please have a look a it. I have :
an Object Activity
an Object Sport which I want to be a subclass of Activity
an Object Seminar which I want to be a subclass of Activity
To make this happen I write, according to the documentation, the following code :
// Base Model
class Activity: Object {
dynamic var id = ""
dynamic var date = NSDate()
override static func primaryKey() -> String? {
return "id"
}
}
// Models composed with Activity
class Nutrition: Object {
dynamic var activity: Activity? = nil
dynamic var quantity = 0
}
class Sport: Object {
dynamic var activity: Activity? = nil
dynamic var quantity = 0
dynamic var duration = 0
}
Now I have an Model Category which I want it to hold the activities, doesn’t matter if it’s an Nutrition or Sport.
Here is my code :
class Categorie: Object {
let activities = List<Activitie>()
dynamic var categoryType: String = ""
override static func primaryKey() -> String? {
return "categoryType"
}
}
Now I try to add a Nutrition object to my List<Activitie> by doing this :
let nutrition = Nutrition(value: [ "activity": [ "date": NSDate(), "id": "0" ], "quantity": 12 ])
try! realm.write {
realm.add(nutrition, update: true)
}
It doesn’t work because List<Activitie> expect an Activity Object and not a Nutrition Object. Where am I wrong ?
Thanks a lot for the help.
You encountered one of the big problems of Realm : there is no complete polymorphism.
This github post gives a big highlight on what is possible or not, and a few possible solutions that you can use.
Quick quote from jpsim from the link above:
Inheritance in Realm at the moment gets you:
Class methods, instance methods and properties on parent classes are
inherited in their child classes.
Methods and functions that take
parent classes as arguments can operate on subclasses.
It does not get you:
Casting between polymorphic classes (subclass->subclass,
subclass->parent, parent->subclass, etc.).
Querying on multiple classes simultaneously.
Multi-class container (RLMArray/List and RLMResults/Results).
According to the article about type erased wrappers in swift and the #5 option I have ended up with something more flexible, here is my solution.
( please note that the solution #5 need to be updated for Swift 3, my solution is updated for Swift 3 )
My main Object Activity
class Activity: Object {
dynamic var id = ""
override static func primaryKey() -> String? {
return "id"
}
}
and my inheritance : Nutrition and Sport
class Nutrition: Activity { }
class Sport: Activity { }
The solution according to the solution #5 option : Using a type-erased wrapper for polymorphic relationships.
If you want to store an instance of any subclass of Activity, define a type-erased wrapper that stores the type's name and the primary key.
class AnyActivity: Object {
dynamic var typeName: String = ""
dynamic var primaryKey: String = ""
// A list of all subclasses that this wrapper can store
static let supportedClasses: [Activity.Type] = [
Nutrition.self,
Sport.self
]
// Construct the type-erased activity from any supported subclass
convenience init(_ activity: Activity) {
self.init()
typeName = String(describing: type(of: activity))
guard let primaryKeyName = type(of: activity).primaryKey() else {
fatalError("`\(typeName)` does not define a primary key")
}
guard let primaryKeyValue = activity.value(forKey: primaryKeyName) as? String else {
fatalError("`\(typeName)`'s primary key `\(primaryKeyName)` is not a `String`")
}
primaryKey = primaryKeyValue
}
// Dictionary to lookup subclass type from its name
static let methodLookup: [String : Activitie.Type] = {
var dict: [String : Activity.Type] = [:]
for method in supportedClasses {
dict[String(describing: method)] = method
}
return dict
}()
// Use to access the *actual* Activitie value, using `as` to upcast
var value: Activitie {
guard let type = AnyActivity.methodLookup[typeName] else {
fatalError("Unknown activity `\(typeName)`")
}
guard let value = try! Realm().object(ofType: type, forPrimaryKey: primaryKey) else {
fatalError("`\(typeName)` with primary key `\(primaryKey)` does not exist")
}
return value
}
}
Now, we can create a type that stores an AnyActivity!
class Category: Object {
var categoryType: String = ""
let activities = List<AnyActivity>()
override static func primaryKey() -> String? {
return "categoryType"
}
}
and to store the data :
let nutrition = Nutrition(value : [ "id" : "a_primary_value"] )
let category = Category(value: ["categoryType" : "0"])
category.activities.append(AnyActivity(tree))
To read the data we want to check the activity method, use the value property on AnyActivity
for activity in activities {
if let nutrition = activity.value as? Nutrition {
// cool it's a nutrition
} else if let sport = activity.value as? Sport {
// cool it's a Sport
} else {
fatalError("Unknown payment method")
}
}
Owen is correct, in regarding OO principles, and I noticed that also, that you are not truly doing inheritance.
When an object uses another as an attribute or property, it is Association, not inheritance. I too am reviewing whether Realm supports Table/Object level inheritance like Java does with Hibernate ... but not expecting it.
This framework while still young but powerful, is good enough for me to avoid using SQLite ... very fast, easy to use and much easier with data model migrations !
In your code Nutrition and Sport don't inherit Activity, they inherit Object and composite an Activity instance. To inherit Activity, you should do
class Nutrition: Activity {
dynamic var quantity = 0
}
I think maybe you worried if you did above, your code did not inherit Object any more. But it is not true. Nutrition is still inherited from Object as Object is inherited by Activity.
Their relations are Nutrition: Activity: Object.
As Yoam Farges pointed it out, Realm doesn't support :
Casting between polymorphic classes (subclass->subclass,
subclass->parent, parent->subclass, etc.).
Which is what I was trying to do.
You guys are right when saying that my Inheritance is not an Inheritance, but as you can see in the Realm documentation it's how you achieve it.
Thanks to the informations I got in this github post I could achieved what I wanted and could keep a easy readability.
Use an option type for polymorphic relationships :
class PolyActivity: Object {
dynamic var nutrition: Nutrition? = nil
dynamic var sport: Sport? = nil
dynamic var id = ""
override static func primaryKey() -> String? {
return "id"
}
}
Create my main Object Activity
class Activity: Object {
dynamic var date = NSDate()
}
and have my Nutrition and Sport object inherited properly to Activity
class Nutrition: Activity {
dynamic var quantity = 0
}
class Sport: Activity {
dynamic var quantity = 0
dynamic var duration = 0
}
My Category object can now hold a List
class Categorie: Object {
let activities = List<PolyActivity>()
var categoryType: String = ""
override static func primaryKey() -> String? {
return "categoryType"
}
}
And this is how I create my Nutrition object :
let polyActivity = PolyActivity(value : [ "id": primaryKey ] )
poly.nutrition = Nutrition(value: [ "date": NSDate(), "quantity": 0, "duration": 0 ])
let category = Category(value: ["categoryType" : "0"])
category.activities.append(polyActivity)
And to retrieve just use Optional Binding :
if let nutrition = category.activities[0].nutrition { }
If you guys have a better, clearer, easier solution please go head !
I'm facing an issue where a Realm object has another Realm object as member which is always nil after adding to the database.
class MedPack: Object {
dynamic var uuid = NSUUID().UUIDString
dynamic var medicine: Medicine?
convenience init(medicine: Medicine) {
self.init()
self.medicine = medicine
}
override static func primaryKey() -> String? {
return "uuid"
}
}
The reference to the object Medicine is always nil after adding.
class Medicine: Object {
var uuid = NSUUID().UUIDString
var name: String?
override static func primaryKey() -> String? {
return "uuid"
}
}
Creation of object
let medPack = MedPack(medicine: med)
Adding to database
static let sharedInstance = DBHelper()
var realmDb: Realm!
private init() {
realmDb = try! Realm()
}
func store(object: Object) {
try! self.realmDb.write {
self.realmDb.add(object)
}
}
After comparing this code to one of the Realm sample projects, it would appear that simply setting an Object as a child of another does not implicitly write it to the database as well.
Instead, you may need to refactor your code slightly, but make sure you explicitly add your Medicine object to Realm in a write transaction, before you set its relation to MedPack and then write MedPack to the database.