Swift Realm - Iterate an array of items to update their relationships - ios

I have a basic many-to-many relation with a relation table on my Realm Schema.
So I have Recipe, Ingredient and RecipeIngredient model.
When I try to create a Recipe, I have to iterate an array of Ingredient to create RecipeIngredient. Once the recipeIngredient is create I want to edit my Recipe and my Ingredient to add a relation to the new RecipeIngredient. But the edition of the Ingredient throws an exception, and I have no idea why...
Here some code
Recipe Model
class RecipeRealm: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = ""
#Persisted var lastEditedTime: Date = Date()
#Persisted var recipeIngredients: List<RecipeIngredient>
convenience init(name: String){
self.init()
self.name = name
}
}
Ingredient Model
class IngredientRealm: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = ""
#Persisted var unit: Unit = Unit.none
#Persisted var lastEditedTime: Date = Date()
#Persisted var recipeIngredients: List<RecipeIngredient>
convenience init(name: String){
self.init()
self.name = name
}
}
RecipeIngredient Model
class RecipeIngredient: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var qty: Int = 0
#Persisted var lasEditedTime: Date = Date()
#Persisted(originProperty: "recipeIngredients") var recipe: LinkingObjects<RecipeRealm>
#Persisted(originProperty: "recipeIngredients") var ingredient: LinkingObjects<IngredientRealm>
}
The recipe form model
struct QuantifiedIngredient {
var ingredient: IngredientRealm
var qty: Int16
}
class RecipeForm: ObservableObject {
#Published var name = ""
#Published var ingredients: [QuantifiedIngredient] = []
var recipeID: String?
var updating: Bool {
recipeID != nil
}
init(){}
init(_ recipe: Recipe){
name = recipe.name ?? ""
ingredients = []
recipeID = recipe.id
}
}
The create Recipe logic
private func createRecipe() {
withAnimation {
do {
let realm = try Realm()
// Create Recipe on RealmDB
let realmRecipe = RecipeRealm(name: form.name)
try realm.write{
realm.add(realmRecipe)
for ingredient in form.ingredients{
let recipeIngredient = RecipeIngredient()
recipeIngredient.qty = Int(ingredient.qty)
realm.add(recipeIngredient)
realmRecipe.recipeIngredients.append(recipeIngredient)
ingredient.ingredient.recipeIngredients.append(recipeIngredient) // <--- this line crash
}
}
} catch {
print(error.localizedDescription)
}
presentationMode.wrappedValue.dismiss()
}
}
An exception is throw by this line on createRecipe method: ingredient.ingredient.recipeIngredients.append(recipeIngredient).
Here the error message: Terminating app due to uncaught exception 'RLMException', reason: 'Cannot modify managed RLMArray outside of a write transaction.'
But I have the write block...
Any ideas ?

It seems that when I tried to add recipeIngredient on the ingredient, I actually tried to put a realm object on a copy of the realm ingredient. So is obviously not working, but the error message is not very clear.
To fix it I used the thaw()function that returns me the good realm object, so I can edit it.
try realm.write{
realm.add(realmRecipe)
for ingredient in form.ingredients{
let recipeIngredient = RecipeIngredient()
recipeIngredient.qty = Int(ingredient.qty)
realm.add(recipeIngredient)
realmRecipe.recipeIngredients.append(recipeIngredient)
guard let rIngredient = ingredient.ingredient.thaw() else {
return
}
rIngredient.recipeIngredients.append(recipeIngredient)
}
}
I don't know if this is the best way to do it, so if someone had a better option I'm listening :)

Related

Correct way of creating reference to object in Realm

I create a fitness app and I use Realm as local database. During first launch I want to replace default realm with realm file which contains initial data (names of exercises, equipment, muscles engaged etc.). This initial data won't change in future. I wonder if exists some way which can help me to create reference in main class to another smaller classes. I need this to make filtering and getting data easier.
It's my main realm class
class Exercise: Object {
#Persisted var exerciseID: Int = 0
#Persisted var name: String = ""
#Persisted var category: Int
#Persisted var equipment: String
#Persisted var instruction: String
#Persisted var muscle: String
#Persisted var gif: String?
#Persisted var image: String? = nil
convenience init(name: String, category: Int, equipment: String, instruction: String, muscle: String, gif: String?, image: String?) {
self.init()
self.name = name
self.category = category
self.equipment = equipment
self.instruction = instruction
self.muscle = muscle
self.gif = gif
self.image = image
}
override static func primaryKey() -> String? {
return "exerciseID"
}
}
When I want to get all exercises and assigned equipment and muscles it is really a lot of code to retrieve this data especially when string contains few references to object.
var exercises = [Exercise]()
var equipments = [Equipment]()
func getAllExercises() {
let data = RealmService.shared.realm.objects(Exercise.self)
exercises = data.compactMap({$0})
let equipment = exercises.compactMap({$0.equipment})
for eq in exercises.compactMap({$0.equipment}) {
let numberOfEquipment = eq.components(separatedBy: ",")
for number in numberOfEquipment {
guard let intNumber = Int(number) else { return }
guard let finalEquipment = RealmService.shared.realm.object(ofType: Equipment.self, forPrimaryKey: intNumber) else { return }
equipments.append(finalEquipment)
}
}
Maybe the better option is to just insert values instead of object references?
You need to set up one-to-many relationships to take advantage of quicker queries and lazy loading.
I've simplified the models, but the magic is in the equipmentObjects property:
class Exercise: Object {
#Persisted(primaryKey: true) var exerciseID = 0
#Persisted var name: String = ""
#Persisted var equipment: String
#Persisted var equipmentObjects: List<Equipment>
convenience init(exerciseID: Int, name: String, equipment: String) {
self.init()
self.exerciseID = exerciseID
self.name = name
self.equipment = equipment
}
}
class Equipment: Object {
#Persisted(primaryKey: true) var equipmentID = 0
#Persisted var equipment: String = ""
convenience init(equipmentID: Int, equipment: String) {
self.init()
self.equipmentID = equipmentID
self.equipment = equipment
}
}
You can go ahead and initialize realm with your csv file. But when the app begins you would want to go ahead and establish the relationships between Exercise, Equipment, and Muscles. You should only do this once.
Here I've created a small utility to link the realm objects. Notice how it uses UserDefaults to check and see if relationships were already built. It is also building the relationships on a specified queue. You would want to pass in a background queue rather than the main queue so the UI doesn't lock up.
struct RealmRelationshipBuilder {
let configuration: Realm.Configuration
let userDefaults: UserDefaults
let queue: DispatchQueue
func buildRelationshipsIfNeeded(completion: #escaping() -> Void) {
guard userDefaults.didBuildRealmRelationships == false else { return completion() }
queue.async {
autoreleasepool {
defer { completion() }
do {
let realm = try Realm(configuration: configuration)
try realm.write {
realm.objects(Exercise.self).forEach { exercise in
let equipment = exercise
.equipment
.components(separatedBy: ",")
.compactMap(Int.init)
.compactMap { realm.object(ofType: Equipment.self, forPrimaryKey: $0) }
exercise.equipmentObjects.append(objectsIn: equipment)
}
}
} catch {
print("RealmRelationshipBuilder error: \(error)")
}
userDefaults.didBuildRealmRelationships = true
}
}
}
}
extension UserDefaults {
enum Key {
static let didBuildRealmRelationships = "didBuildRealmRelationshipsKey"
}
var didBuildRealmRelationships: Bool {
get { bool(forKey: Key.didBuildRealmRelationships) }
set { set(newValue, forKey: Key.didBuildRealmRelationships) }
}
}
Then to test the builder here is a small test case. But in reality you would probably want to show the user an status indicator while the relationships are being built in the background.
enum InitialData {
static let exercises: [Exercise] = {
[
Exercise(exerciseID: 1, name: "Bench press", equipment: "1,3,5"),
Exercise(exerciseID: 2, name: "Butterfly", equipment: "6"),
]
}()
static let equipment: [Equipment] = {
[
Equipment(equipmentID: 1, equipment: "Barbell"),
Equipment(equipmentID: 2, equipment: "Bench"),
Equipment(equipmentID: 3, equipment: "Bodyweight"),
Equipment(equipmentID: 4, equipment: "Cable"),
Equipment(equipmentID: 5, equipment: "Not sure"),
Equipment(equipmentID: 6, equipment: "Unknown"),
]
}()
}
class RealmExerciseTests: XCTestCase {
let realmConfiguration = Realm.Configuration.defaultConfiguration
override func setUpWithError() throws {
let realm = try Realm(configuration: realmConfiguration)
try realm.write {
realm.deleteAll()
realm.add(InitialData.exercises)
realm.add(InitialData.equipment)
}
}
func testInitialize() throws {
let relationshipBuilder = RealmRelationshipBuilder(
configuration: realmConfiguration,
userDefaults: .init(suiteName: UUID().uuidString) ?? .standard,
queue: DispatchQueue(label: "realm.init.background")
)
let expectation = expectation(description: "realm.init")
relationshipBuilder.buildRelationshipsIfNeeded {
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
let realm = try Realm(configuration: realmConfiguration)
realm.refresh()
guard let exercise1 = realm.object(ofType: Exercise.self, forPrimaryKey: 1) else {
return XCTFail("Missing exercise with primary key 1")
}
guard let exercise2 = realm.object(ofType: Exercise.self, forPrimaryKey: 2) else {
return XCTFail("Missing exercise with primary key 2")
}
XCTAssertEqual(exercise1.equipmentObjects.count, 3)
XCTAssertEqual(exercise2.equipmentObjects.count, 1)
}
}

Primary key in realm database

I create a default realm file to use it during first launching app as replacement (I want to have a file with initial data). I start with creating default realm file from csv files. The problem is that I am not sure if my structure is correct. When I import data from csv (in Realm Browser) and try to import next data for next class, I get this error
I have main class called Exercises
class Exercises: Object {
#Persisted var id: Int = 0
#Persisted var name: String = ""
#Persisted var category: Category?
#Persisted var equipment: Equipment?
#Persisted var instruction: String
#Persisted var muscle: List<Muscle>
#Persisted var gif: String?
#Persisted var image: String?
convenience init(id: Int, name: String, category: Category?, equipment: Equipment?, instruction: String, muscle: [Muscle], gif: String?, image: String?) {
self.init()
self.id = id
self.name = name
self.category = category
self.equipment = equipment
self.instruction = instruction
self.muscle.append(objectsIn: muscle)
self.gif = gif
self.image = image
}
}
and other classes for separate things
class Equipment: Object {
#Persisted(primaryKey: true) var equipmentID = 0
#Persisted var equipment: String = ""
convenience init(equipment: String) {
self.init()
self.equipment = equipment
}
}
class Category: Object {
#Persisted(primaryKey: true) var categoryID = 0
#Persisted var category: String = ""
convenience init(category: String) {
self.init()
self.category = category
}
}
class Muscle: Object {
#Persisted(primaryKey: true) var muscleID = 0
#Persisted var muscle: String = ""
convenience init(muscle: String) {
self.init()
self.muscle = muscle
}
}
Finally I want to receive structure like below. I wonder if it is correct way? Maybe better option is just set text in fields instead of reference to class (after all realm is non-relational)?

Swift Inheritance with custom array

class RecipeBrain: NSObject {
var name: String
var pictureUrl: String
var likes = 0
var ingredients: [Ingredient]
var method: [String]
init(name: String, pictureUrl: String, ingredients: [Ingredient], method: [String]) {
self.name = name
self.pictureUrl = pictureUrl
self.ingredients = ingredients
self.method = method
}
}
class Ingredient{
var name: String
var quantity: Double
var unit: String
init(name: String, quantity: Double, unit: String) {
self.name = name
self.quantity = quantity
self.unit = unit
}
}
class AddRecipe {
var recipeBrain = [RecipeBrain]()
var ingredients = [Ingredient]()
var ingredient = Ingredient(name: "apple", quantity: 1.0, unit: "Kg")
ingredients.append(ingredient)
var recipe1 = RecipeBrain(name: "Recipe1", pictureUrl: "nil", ingrexdients: ingredients, method: ["Method"])
recipeBrain.append(recipe1)
}
I am trying to build a recipe app in Swift. Problem is creating the ingredients for it where I require a string,double,string.
How I imagined : an ingredient is an array of Ingredients. and to create a new recipe I just .append it to the recipeBrain
Main problem : when I try to append a new recipe to the recipeBrain array It says that the recipe1 is not declared.
( The AddRecipe class purpose is only testing with static data )
I changed it to recipeBrain.append(recipe1) But I still get the error : expected declaration , same with ingredients when I try to append
You are trying to append recipe and ingredient objects into their arrays in body of AddRecipe class, but you can't.
if you do this in body of a method everything will be ok, for example in init() method:
class AddRecipe {
var recipeBrain = [RecipeBrain]()
var ingredients = [Ingredient]()
init()
{
var ingredient = Ingredient(name: "apple", quantity: 1.0, unit: "Kg")
ingredients.append(ingredient)
var recipe1 = RecipeBrain(name: "Recipe1", pictureUrl: "nil", ingredients: ingredients, method: ["Method"])
recipeBrain.append(recipe1)
}
You are getting the error message because you have tried to write code in a class declaration. You can only declare items such as properties, functions, enumerations and other classes directly inside a class declaration. Code, such as ingredients.append(ingredient) needs to go inside a function.
I would suggest that you move this code into a class function of your RecipeBrain class (or indeed put it somewhere else entirely, such as a view controller, but you haven't shown how your app is structured):
class RecipeBrain: NSObject {
var name: String
var pictureUrl: String
var likes = 0
var ingredients: [Ingredient]
var method: [String]
init(name: String, pictureUrl: String, ingredients: [Ingredient], method: [String]) {
self.name = name
self.pictureUrl = pictureUrl
self.ingredients = ingredients
self.method = method
}
class func addRecipe() -> [RecipeBrain] {
var recipeBrain = [RecipeBrain]()
var ingredients = [Ingredient]()
let ingredient = Ingredient(name: "apple", quantity: 1.0, unit: "Kg")
ingredients.append(ingredient)
let recipe1 = RecipeBrain(name: "Recipe1", pictureUrl: "nil", ingredients: ingredients, method: ["Method"])
recipeBrain.append(recipe1)
return recipeBrain
}
}
Now you can say let recipeBrain = RecipeBrain.addRecipe() and recipeBrain will be an array of RecipeBrain containing a single RecipeBrain

Many-to-one with primary key (unique constraint)

I've got an Article and a Category model linked by a many-to-one relationship. However, the Category model has a unique constraint on the id property because it's the primary key as you can see below.
class Article: Object
{
dynamic var id: String = ""
dynamic var title: String = ""
dynamic var category: Category()
override static func primaryKey() -> String? {
return "id"
}
}
class Category: Object
{
dynamic var id: String = ""
dynamic var title: String = ""
override static func primaryKey() -> String? {
return "id"
}
}
This will work until an Article got the same Category and throw an exception because of the unique constraint.
How am I supposed to implement this kind of relationship ? Is there any built-in way to persist only the Category id and retrieve the corresponding Category ?
Thanks
As you can read in Realm doc (0.92.1), you have to use a List<Object> for a many-to-one relationship.
See this link :
http://realm.io/docs/swift/latest/
class Dog: Object {
dynamic var name = ""
dynamic var owner: Person? // Can be optional
}
class Person: Object {
... // other property declarations
let dogs = List<Dog>()
}
let someDogs = Realm().objects(Dog).filter("name contains 'Fido'")
jim.dogs.extend(someDogs)
jim.dogs.append(rex)
So in your case, I guess it should be something like that :
class Article: Object
{
dynamic var id: String = ""
dynamic var title: String = ""
override static func primaryKey() -> String? {
return "id"
}
}
class Category: Object
{
dynamic var id: String = ""
dynamic var title: String = ""
dynamic var articles = List<Article>()
override static func primaryKey() -> String? {
return "id"
}
}
If your Realm version is older :
class Category: Object
{
...
dynamic var categories = RLMArray(objectClassName: Article.className())
}

Core Data with Swift two entities

I am trying to use Core Data to save some of my application data. I have following classes. Basically I want to store the properties of each job, and use it later on.
Following is the class I currently use in my application.
class Job {
var name:String?
var count = 1
var id:String
var startDate:NSDate?
var finishDate:NSDate?
var expected:NSDate?
var detail:Array<JobDetail> = []
var isFinished:Bool?
var sender:String?
var receiver:String?
init(name:String?, id:String) {
self.name = name
self.id = id
self.isFinished = false
self.startDate = NSDate()
}
func addDetail (message:String?, date:NSDate?, location:String?, status: DetailStatus) {
detail.append(JobDetail(message: message, date: date, location: location, status: status))
if status == DetailStatus.OK {
self.isFinished = true
self.finishDate = date
}
}
}
enum DetailStatus {
case OK
case Error
case Exception
case Unknown
}
class JobDetail {
var message:String?
var date:NSDate?
var location:String?
var status:DetailStatus
init(message:String?, date:NSDate?, location:String?, status: DetailStatus) {
self.message = message
self.date = date
self.location = location
self.status = status
}
}
NSManagedObject sub class I created with Xcode after I create the data model.
class Job: NSManagedObject {
#NSManaged var name: String
#NSManaged var count: NSNumber
#NSManaged var id: String
#NSManaged var startDate: NSDate
#NSManaged var finishDate: NSDate
#NSManaged var expected: NSDate
#NSManaged var isFinished: NSNumber
#NSManaged var sender: String
#NSManaged var receiver: String
#NSManaged var details: NSSet
}
class JobDetail: NSManagedObject {
#NSManaged var message: String
#NSManaged var date: NSDate
#NSManaged var location: String
#NSManaged var status: NSNumber
#NSManaged var parent: Job
}
Here are the screenshots of my data model.
Basically I want to CRUD Job in my application so that I can show them in my tableview. I have everything setup, but because I couldn’t setup Core Data I don’t have persistence. I will appreciate if you can help me to setup Core Data.
Refer this. May be it's useful to you...
http://www.raywenderlich.com/85578/first-core-data-app-using-swift
It seems from the screenshots that your setup is correct. Link details with jobs like this.
detail1.parent = job
detail2.parent = job
context.save(nil)
Get all details for a job like this
job.details
This is unordered, but you can sort them using sortedArrayUsingDescriptors.
let sortedDetails = job.details.sortedArrayUsingDescriptors(
[NSSortDescriptor(key:"date" ascending: false)])

Resources