Realm Update Deleting existing data - ios

Using Swift 5 for iOS13.
I am trying to update an existing Realm record with a Contact Picker result. The function deletes all the object content except for the new content.
My code
class Person: Object {
#objc dynamic var personId = UUID().uuidString
#objc dynamic var firstName: String = ""
#objc dynamic var surname: String = ""
#objc dynamic var mobileNumber: Int = 0
#objc dynamic var password: String = ""
#objc dynamic var myContactID: String = ""
override static func primaryKey() -> String? {
return "personId"
}
}
extension HomeController: CNContactPickerDelegate {
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
picker.dismiss(animated: true, completion: nil)
let person = Person()
let me = realm.objects(Person.self).filter("mobileNumber == %#", mobileNumber)
person.myContactID = contact.identifier
person.personId = me.first!.personId
try! realm.write {
realm.add(person, update: .modified)
}
self.viewWillAppear(true)
}
}
All the existing content of the Person class in the Realm database disappears except for myContactID and personID.

That is because you are updating the Person with new data.
The line let person = Person() creates a new instance with all the default values. (firstName: String = "" etc..)
So, when you assign myContactID and personId to this newly created person, it will look like this:
Person {
personId = FAE4C224-D37E-4C77-B6F1-C60A92F188D0;
firstName = ;
surname = ;
mobileNumber = ;
password = ;
myContactID = contactIdentifier;
}
And when you call realm.add(person, update: .modified), it will overwrite the record associated with the primary key with this newly created person.
You want to fetch an existing person and modify it. You can do something like this:
guard let me = realm.objects(Person.self).filter("mobileNumber == %#", mobileNumber).first else { return }
try! realm.write {
me.myContactID = contact.identifier
}

Related

Set Table Section Titles from Realm Query

I've searched for the answer to this (I'm sure it's there somewhere) but can't find it.
I am trying to populate a UITableView's section headers from a Realm database, where the section title is in a related class.
My Realm classes:
class Person: Object {
#objc dynamic var personId = UUID().uuidString
#objc dynamic var firstName: String = ""
#objc dynamic var surname: String = ""
#objc dynamic var mobileNumber: Int = 0
#objc dynamic var password: String = ""
override static func primaryKey() -> String? {
return "personId"
}
}
class Group: Object {
#objc dynamic var groupId = UUID().uuidString
#objc dynamic var person: Person?
#objc dynamic var groupName: String = ""
let groupContent = List<String>()
override static func primaryKey() -> String? {
return "groupId"
}
}
I want to retrieve the groupName results for the current user and use them as table section headers. The number of groupNames is dynamic for each user.
My current code, which doesn't work at all is:
func getGroupNames() {
let mobileNumber = UserDefaults.standard.integer(forKey: "mobileNumber")
let personResult = realm.objects(Person.self).filter("mobileNumber == %#", mobileNumber)
let groupNames = realm.objects(Group.self).filter("person == %#", personResult.self.first)
return (groupNames)
}
I can't get groupNames to be useful as section headers.
Help will be appreciated!
Thanks.
UPDATE
I now have:
func getGroupNames() -> [String] {
let mobileNumberInt = mobileNumber
let groupNames = realm.objects(Group.self).filter("person.mobileNumber == %#", mobileNumberInt).map({$0.groupName})
return Array(groupNames)
}
This returns ["Group Name 1", "Group Name 2"] twice (no matter how many objects are in the results). Why twice, and now how do I get these into my section headers? I've tried:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> [String] {
return getGroupNames()
}
The quantity of sections works, but the headers are not showing.
Assuming you already know the mobile number of a person, you can use following code to fetch groups a person belongs to.
func getGroupNames() -> [Group]? {
let realm = try Realm()
let mobileNumber = UserDefaults.standard.integer(forKey: "mobileNumber")
let groupNames = realm.objects(Group.self).filter("person.mobileNumber == %#", mobileNumber)
return Array(groupNames)
}

how to properly update the array list data on realm database in swift?

I have product realm object like this:
class Product : Object {
#objc dynamic var productID : String = ""
#objc dynamic var name : String = ""
#objc dynamic var unitPrice: Double = 0.0
#objc dynamic var quantity = 0
#objc dynamic var descriptionProduct : String = ""
#objc dynamic var hasBeenAddedToWishList : Bool = false
#objc dynamic var hasBeenAddedToCart : Bool = false
#objc dynamic var isNewProduct : Bool = false
var imagePaths = List<String>()
override static func primaryKey() -> String? {
return "productID"
}
func toDictionary() -> [String:Any] {
return [
"productID" : productID,
"name" : name,
"unitPrice": unitPrice,
"quantity": quantity,
"descriptionProduct": descriptionProduct,
"hasBeenAddedToWishList": hasBeenAddedToWishList,
"hasBeenAddedToCart": hasBeenAddedToCart,
"isNewProduct": isNewProduct,
"imagePaths": imagePaths
]
}
}
I want to update data of a product using this function:
static func updateProductDataInRealmDatabase(oldProductData: Product, newProductData: Product) {
guard let matchingProduct = RealmService.shared.realm.objects(Product.self).filter("productID == %#", oldProductData.productID).first else {return}
var newDataProductDictionary = newProductData.toDictionary() // the definition of toDictionary is in above code
newDataProductDictionary["productID"] = nil // to avoid updating the primary key
RealmService.shared.update(object: matchingProduct, for: newDataProductDictionary)
}
and here is the definition of realm service to update data:
class RealmService {
private init() {}
static let shared = RealmService()
var realm = try! Realm()
func update<T: Object>(object: T, for dictionary: [String: Any]) {
do {
try realm.write {
for (key,value) in dictionary {
object.setValue(value, forKey: key)
}
}
} catch {
post(error)
}
}
}
I actually can update some of the data to be the new one like the productName, productPrice etc, But I can't update the imagePaths property which is a List<String>
the array will always to be zero after updating the data like the image below, the imagePaths array become zero :
so how to update the product data properly ? especially to update the List data in my realm object. what went wrong in here ? I use the code below to update the realm database :
func update<T: Object>(object: T, for dictionary: [String: Any]) {
do {
try realm.write {
for (key,value) in dictionary {
object.setValue(value, forKey: key)
}
}
} catch {
post(error)
}
}
Since Product class already has the primary key, no need to search for the existing realm object using productID.
Realm will add new object or update the old object with this query mentioned below.
You can query like this.
let realm = try! Realm()
try! realm.write {
realm.add(anyProductObject,update: true)
}
// You can customize this using generics and also use try catch to handle the errors in case you pass the non realm defined class object

how to avoid adding the same data in Realm database in many to many relationship?

I have wishlistVC that has collection view like the picture below:
I have product realm model like this:
class Product : Object {
#objc dynamic var productID : String = ""
#objc dynamic var name : String = ""
#objc dynamic var unitPrice: Double = 0.0
#objc dynamic var quantity = 0
#objc dynamic var descriptionProduct : String = ""
#objc dynamic var hasBeenAddedToWishList : Bool = false
#objc dynamic var hasBeenAddedToCart : Bool = false
#objc dynamic var isNewProduct : Bool = false
var imagePaths = List<String>()
}
and WishList realm model like this:
class WishList : Object {
#objc dynamic var userID: String = ""
var products = List<Product>() // many to many relationship
static func getWishListFromRealmDatabase() -> WishList {
let userID = "1"
let allWishList = RealmService.shared.realm.objects(WishList.self)
let theWishList = allWishList.filter("userID CONTAINS[cd] %#", userID).first
if let userWishList = theWishList {
return userWishList
} else {
// WishList never setted up before in Realm database container
// then create WishList in realm database
let newWishList = WishList()
newWishList.userID = userID
newWishList.products = List<Product>()
RealmService.shared.save(object: newWishList)
return newWishList
}
}
static func addProductToWishListRealmDatabase(userWishList: WishList, selectedProduct: Product) {
// to check wheter the selected product from user is already in WishList or not
if userWishList.products.filter("productID == %#", selectedProduct.productID).first == nil {
RealmService.shared.save(expression: {
selectedProduct.hasBeenAddedToWishList = true
userWishList.products.append(selectedProduct)
})
}
}
}
when the user tap that love button, here is the code used to add product to WishList:
func addProductToWishListRealmDatabase(userWishList: WishList, selectedProduct: Product) {
// to check wheter the selected product from user is already in WishList.products or not
if userWishList.products.filter("productID == %#", selectedProduct.productID).first == nil {
// write in realm database
RealmService.shared.save(expression: { // <-- this is just a wrapper to avoid write do try catch block all over the place
selectedProduct.hasBeenAddedToWishList = true
userWishList.products.append(selectedProduct)
})
}
}
and here is the full simplified code of my WishListVC:
class WishListVC : UIViewController {
#IBOutlet weak var wishListCollectionView: UICollectionView!
private var userWishList : WishList?
private var products = List<Product>()
private var selectedProduct : Product?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
userWishList = WishList.getWishListFromRealmDatabase() // the definition is in the code above
guard let userWishList = userWishList else {return}
products = userWishList.products
}
}
extension WishListVC : ListProductCellDelegate {
func likeButtonDidTapped(at selectedIndexPath: IndexPath, productHasBeenLiked: Bool, collectionView: UICollectionView) {
guard let userWishList = userWishList else {return}
let selectedProduct = products[selectedIndexPath.item]
if productHasBeenLiked {
WishList.removeProductFromWishListRealmDatabase(userWishList: userWishList, selectedProduct: selectedProduct)
} else {
WishList.addProductToWishListRealmDatabase(userWishList: userWishList, selectedProduct: selectedProduct)
}
wishListCollectionView.reloadData()
self.wishListCollectionView.isHidden = userWishList.products.isEmpty
}
}
if I append a product to the wishlist model like the code above, it will affect to the Product.self in realm database, it will keep adding the product in 'Product Realm Database', as you can see in the image below, there are 9 data in Product, but as you can see some of the product have the same productID, there are 3 products that have 'a' as the productID .
so how to avoid adding the product that has the same productID in 'Product Realm Database' (Product.self) when I modify the WishList by appending the product to wishlist.products ?
I have also tried to add primary key using:
override static func primaryKey() -> String? {
return "productID"
}
but It makes crash with message:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to create an object of type 'Product' with an existing
primary key value 'a'.'
it will throw error because I add productID = 'a'
what should I do ? how to append product to the WishList model but I also can avoid adding the same product that has the same productID to the Product Realm database model (Product.self) ?
do I use the wrong approach to add the product to the wishlist?

Adding an array of Json Data to Realm

I'm making an app for airports and I'm getting an array of data from one api, like so:
"data":[
{"id":"001","code":"ABZ","name":"Aberdeen","country":"United Kingdom"},
{"id":"002","code":"AUH","name":"Abu Dhabi","country":"United Arab Emirates"},
.
.
.
]
AND :
"airports":[
{"from":"001",
"to":["1","3","11","13","12","20","23","27","29","31","33"]
},
.
.
.
]
I have created realm model classes:
class AirportsDataRealm: Object {
#objc dynamic var name: String = ""
#objc dynamic var id: Int = 0
#objc dynamic var code: String = ""
#objc dynamic var country: String = ""
override static func primaryKey() -> String? {
return "id"
}
}
class AirportsFromToRealm: Object {
#objc dynamic var fromID: Int = 0
var toID = List<Int>()
override static func primaryKey() -> String? {
return "fromID"
}
}
now I want to save it into realm, I'm using swiftyJSON and I have used for-loop to do it and it is working fine but I think it's taking long time since the array is very long, here is what I've done:
// Airports Data
let countData = json["data"].count
for i in 0...countData - 1{
let airportsDataModel = AirportsDataRealm()
airportsDataModel.code = json["data"][i]["code"].stringValue
airportsDataModel.name = json["data"][i]["name"].stringValue
airportsDataModel.country = json["data"][i]["country"].stringValue
airportsDataModel.id = Int(json["data"][i]["id"].stringValue)!
try! realm.write {
realm.add(airportsDataModel, update: true)
}
}
//Airports FROM-TO
let countFromTo = json["airports"].count
for i in 0...countFromTo - 1{
let fromToDataModel = AirportsFromToRealm()
fromToDataModel.fromID = Int(json["airports"][i]["from"].stringValue)!
let arrayTo = json["airports"][i]["to"].arrayValue.map{ $0.intValue }
fromToDataModel.toID.append(objectsIn: arrayTo)
try! realm.write {
realm.add(fromToDataModel, update: true)
}
}
is there any way to save the whole array in realm in one shot without for-loop?
P.S
"there should be a relation between the two tables because each from 'id' has a list of 'to' id's and the id's are from the data table, for now I managed to create this relations when fetching the data using filters ,, so just ignore this"
Thank you
Simply use map method,
First I needed to add initializers to my object classes and pass json array as a parameter, like so:
class AirportsDataRealm: Object {
#objc dynamic var name: String = ""
#objc dynamic var id: Int = 0
#objc dynamic var code: String = ""
#objc dynamic var country: String = ""
convenience required init(withJSON json : JSON) {
self.init()
self.name = json["name"].stringValue
self.id = json["id"].intValue
self.code = json["code"].stringValue
self.country = json["country"].stringValue
}
override static func primaryKey() -> String? {
return "id"
}
}
class AirportsFromToRealm: Object {
#objc dynamic var fromID: Int = 0
var toID = List<Int>()
convenience required init(withJSON json : JSON) {
self.init()
self.fromID = json["from"].intValue
let toArray = json["to"].arrayValue.map{ $0.intValue }
self.toID.append(objectsIn: toArray)
}
override static func primaryKey() -> String? {
return "fromID"
}
}
Then by using map method the code will look like this:
func updateAirport(json: JSON) {
// Airports Data
let airportsData : [AirportsDataRealm]
let airportsDataJsonArray = json["data"].array
airportsData = airportsDataJsonArray!.map{AirportsDataRealm(withJSON: $0)}
//Airports FROM-TO
let airportsFromTo : [AirportsFromToRealm]
let airportsFromToJsonArray = json["airports"].array
airportsFromTo = airportsFromToJsonArray!.map{AirportsFromToRealm(withJSON: $0)}
//Write To Realm
try! realm.write {
realm.add(airportsData, update: true)
realm.add(airportsFromTo, update: true)
}
}
No for loops anymore ^_^

Preventing duplicate storage to realm

I am using realm to store and persist my data. Everything works fine and I just discovered that users can actually store duplicate item which is bad. I would am unable to create a check to prevent duplicate items, any help would be appreciated
Function
func addData(object: OfflineModel) {
try! database.write {
database.add(object, update: true)
}
}
//MARK:- Get Offline
func getDataFromDB() -> Results<OfflineModel> {
offlineItems = database.objects(OfflineModel.self)
return offlineItems!
}
//MARK:- Create Offline
func createOfflineList(createdDate: Date, photo: Data, title: String, completion: #escaping CompletionHandler) -> Void {
REALM_QUEUE.sync {
let offlineList = OfflineModel()
offlineList.createdDate = createdDate
offlineList.photo = photo
offlineList.title = title
OfflineFunctions.instance.addData(object: offlineList)
completion(true, nil)
}
}
Model
#objc dynamic var id = UUID().uuidString
#objc dynamic var photo: Data? = nil
#objc dynamic var title : String = ""
#objc dynamic var createdDate: Date?
override static func primaryKey() -> String {
return "id"
}
The issue is that in the createOfflineList method you create a new OfflineModel, which generates a random id using UUID().uuidString and hence you cannot have duplicate models from Realm's point of view, since id, which is used as the primary key will always be different. You'll need to use title (or any other non-random property that you actually want to use to identify your model instances) as the primary key.
class OfflineModel: Object {
#objc dynamic var photo: Data? = nil
#objc dynamic var title : String = ""
#objc dynamic var createdDate: Date?
override static func primaryKey() -> String {
return "title"
}
}

Resources