Can't save string to Realm Swift - swift

I'm trying to save a string to Realm. For some reason, a nil value is being saved to Realm. Here is the function where I save to Realm (imageKey is the String)
func saveImageKey(_ imageKey: Data){
do{
try realm.write{
realm.add(imageKey)
}
}
catch{
print("Error saving imageKey: \(error)")
}
}
I'm not sure if this affects Realm, but when initializing the imageKey variable, I used a didSet to reorganize the imageKey objects by date created as follows:
var imageKey: Results<Data>?{
didSet{ //every time imageKey is updated, this will be called
imageKey = imageKey!.sorted(byKeyPath: "date", ascending: false)
print(imageKey)
}
}
This is how I converted the imageKey string into a Data object:
let newData = Data()
newData.imageKey = "item" + String(textCount)
saveImageKey(newData)
This is the class definition for Data:
class Data: Object{
#objc dynamic var text: String = ""
#objc dynamic var imageKey: String = ""
#objc dynamic var date = NSDate().timeIntervalSince1970
}
I have already checked for common errors, such as not initializing Realm, not adding the dynamic to the variable declarations in the Data class.
What could the issue be? Please let me know if you need more code/information.

Related

Realm (Swift): How to get at MutableSet data during migration?

I've got a local Realm (using the RealmSwift API version 10.15.1) that has a Player object that I'm trying to migrate. The Player currently contains field called preferredPositions that is a MutableSet<PositionClass>. The definition of Player looks like this:
#objc final class Player: Object {
#Persisted(primaryKey: true) var playerId: ObjectId
#Persisted var name: String = ""
#Persisted var preferredPositions: MutableSet<PositionClass> = MutableSet<PositionClass>()
...
}
and PositionClass looks like this:
class PositionClass: Object {
#Persisted(primaryKey: true) var positionClassId: String = ""
#Persisted var name: String = ""
#Persisted var order: Int = 0
#Persisted var abbreviation: String = ""
...
}
I want to do a migration that will change preferredPositions from a MutableSet<PositionClass> to a List<PositionClass> since now I want preferredPositions to be ordered.
So the new Player looks like:
#objc final class Player: Object {
#Persisted(primaryKey: true) var playerId: ObjectId
#Persisted var name: String = ""
#Persisted var preferredPositions: List<PositionClass> = List<PositionClass>()
...
}
However, I can't figure out the magic incantation in the migration configuration to get access to the preferredPositions data.
In my migration I have:
let schemaVersion: UInt64 = 22
let config = Realm.Configuration(schemaVersion: schemaVersion,
migrationBlock: { migration, oldSchemaVersion in
...
if (oldSchemaVersion < 22) {
migration.enumerateObjects(ofType: Player.className()) { oldObject, newObject in
if let preferredPositionsSet: MutableSet<PositionClass> = oldObject!["preferredPositions"] as? MutableSet<PositionClass> {
let preferredPositionsList: List<PositionClass> = List()
preferredPositionsSet.forEach { (positionClass: PositionClass) in
preferredPositionsList.append(positionClass)
}
newObject!["preferredPositions"] = preferredPositionsList
} else {
NSLog("preferredPositionsSet is nil.")
}
}
}
})
Realm.Configuration.defaultConfiguration = config
But the line
let preferredPositionsSet: MutableSet<PositionClass> = oldObject!["preferredPositions"] as? MutableSet<PositionClass>
always returns nil. I've looked in the debugger and it seems like oldObject!["preferredPositions"] is a MutableSet<PositionClass>. For example if I add the code:
let preferredPositionsAny = oldObject!["preferredPositions"]
and then look at preferredPositionsAny in the debugger it shows:
So, the underlying type is correct, but I don't know how to get at it properly.
Or am I supposed to do the migration in a different way?
Code TL;DR:
let schemaVersion: UInt64 = 22
let config = Realm.Configuration(schemaVersion: schemaVersion,
migrationBlock: { migration, oldSchemaVersion in
...
if (oldSchemaVersion < 22) {
migration.enumerateObjects(ofType: Player.className()) { oldObject, newObject in
let newRealm = newObject!.realm
let preferredPositionsList: List<PositionClass> = List()
let preferredPositionsSet = oldObject!.dynamicMutableSet("preferredPositions")
preferredPositionsSet.forEach { positionClass in
if let newPositionClass = newRealm?.object(ofType: PositionClass.self, forPrimaryKey: positionClass["positionClassId"]) {
preferredPositionsList.append(newPositionClass)
}
}
newObject!["preferredPositions"] = preferredPositionsList
}
}
More Complete Answer:
There were a couple of things I was getting wrong:
the data type for the "old" object isn't a MutableSet<PositionClass>, but a MutableSet<DynamicObject>. This was why the cast to MutableSet<PositionClass> was always returning nil.
There's also a clearer way to fetch the old MutableSet, so instead of calling
let preferredPositionsSet = oldObject!["preferredPositions"] as! MutableSet<DynamicObject>
from https://stackoverflow.com/a/37588630/1204250 I found
let preferredPositionsSet = oldObject!.dynamicMutableSet("preferredPositions")
Which seems to have the return the same object, but again, a bit more clear
From this answer: https://stackoverflow.com/a/37588630/1204250, to re-populate the new List with the existing PositionClass instances (instead of creating new ones), you need to fetch the data from the Realm. However in order to get a parameter to fetch the data. Since the "old" MutableSet contains DynamicObject, you need to access it using the DynamicObject method of named fields e.g. positionClass["positionClassId"] instead of accessing a property on the actual base object type e.g. positionClass.positionClassId. So that's where
let newRealm = newObject!.realm
if let newPositionClass = newRealm?.object(ofType: PositionClass.self, forPrimaryKey: positionClass["positionClassId"]) {
...
comes from.

How to map a Firestore DocumentID to a RealmObject attribute?

I'm trying to provide some data in the cloud with Firestore that can be downloaded and stored in a Realm database on an iOS device.
The structure of my object that I want to store is:
import Foundation
import RealmSwift
import FirebaseFirestore
import FirebaseFirestoreSwift
#objcMembers class Flashcard: Object, Codable{
#objc dynamic var id: String? = NSUUID().uuidString
#objc dynamic var character: String?
#objc dynamic var title: String?
#objc dynamic var translation: String?
#objc dynamic var created: Date = Date()
let deck = LinkingObjects<FlashcardDeck>(fromType: FlashcardDeck.self, property: "cards")
override class func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case character
case translation
case created
case title
}
If I try to map the documentID to my id attribute with
#DocumentID #objc dynamic var id: String? = NSUUID().uuidString
If get the following error:
'Primary key property 'id' does not exist on object 'Flashcard'
How can I solve this problem?
EDIT: To make it more understandable here is a screenshot of my Firestore database:
The collection "PredifinedDecks" will store many decks. For example the id = DF59B1B3-BD22-47CE-81A6-04E7A274B98F represents one deck. Each deck will store an array/List with cards in it.
Not sure I fully understand the question but let me address this at a high level.
It appears there is a PredefinedDecks (a collection) that contains documents. Each document has a field (an array) of cards and some other field data. If the goal is to read in all of the documents (the decks) and their child data and store them as Realm objects, here's one solution. Start with a Realm object to hold the data from Firestore
class DeckClass: Object {
#objc dynamic var deck_id = ""
#objc dynamic var created = ""
#objc dynamic var title = ""
let cardList = List<CardClass>()
convenience init(withDoc: QueryDocumentSnapshot) {
self.init()
self.deck_id = withDoc.documentID
self.title = withDoc.get("title") as? String ?? "no title"
self.created = withDoc.get("created") as? String ?? "no date"
let cardArray = withDoc.get("cards") as? [String]
for card in cardArray {
let card = CardClass(withCard: card) {
self.cardList.append(card)
}
}
}
}
With this, you simply pass the documentSnapshot from Firestore for each document and the class will populate its properties accordingly.
and the code to read Firestore
func readDecks() {
let decksCollection = self.db.collection("PredefinedDecks")
decksCollection.getDocuments(completion: { documentSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
for doc in documentSnapshot!.documents {
let deck = DeckClass(withDoc: doc)
self.decksList.append(deck) //a Realm List class object? Something else?
}
})
}

Why converting a Firestore querySnapshot into custom objects with compactMap returns empty although the querySnapshot contains documents?

Screenshot of a Firestore Document
I am using Swift, Xcode and a Firestore database.
I created a TableView and a Custom Object Class (MediumSample) with a dictionary and want to load my Firestore documents and show them in the TableView.
The documents (example in the screenshot) are loaded from Firestore correctly but the conversion into the object did not work. The list of objects returned from compactMap is always empty.
Here is my code snippets. It would be great, if someone has a hint on what is going wrong.
Custom Object Class (simplified):
import Foundation
import FirebaseFirestore
protocol MediumSampleDocumentSerializable {
init?(dictionary: [String:Any])
}
struct MediumSample {
var id: String
var field_t: String
var field_i: Int64
var field_b1: Bool
var field_b2: Bool
var field_g: FirebaseFirestore.GeoPoint
var field_d: Date
var field_a: [String]
var usecase: String
var dictionary: [String:Any] {
return [
"id": id,
"field_t": field_t,
"field_i": field_i,
"field_b1": field_b1,
"field_b2": field_b2,
"field_g": field_g,
"field_d": field_d,
"field_a": field_a,
"usecase": usecase
]
}
}
extension MediumSample: MediumSampleDocumentSerializable {
init?(dictionary: [String:Any]) {
guard let id = dictionary ["id"] as? String,
let field_t = dictionary ["field_t"] as? String,
let field_i = dictionary ["field_i"] as? Int64,
let field_b1 = dictionary ["field_b1"] as? Bool,
let field_b2 = dictionary ["field_b2"] as? Bool,
let field_g = dictionary ["field_g"] as? FirebaseFirestore.GeoPoint,
let field_d = dictionary ["field_d"] as? Date,
let field_a = dictionary ["field_a"] as? [String],
let usecase = dictionary ["usecase"] as? String else {return nil}
self.init (id: id, field_t: field_t, field_i: field_i, field_b1: field_b1, field_b2: field_b2, field_g: field_g, field_d: field_d, field_a: field_a, usecase: usecase)
}
}
Declaration of the database and array and calling the loading function:
import UIKit
import FirebaseFirestore
class MediumTableViewController: UITableViewController {
//MARK: Properties
var db: Firestore!
var mediumsamples = [MediumSample]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
loadMediumSamples()
}
Function for loading the Firestore documents to fill the Array:
private func loadMediumSamples() {
//run the Firestore query
db.collection(Constants.MEDIUMS).whereField("usecase", isEqualTo: Constants.USECASE)
.getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
//initialise an array of medium objects with Firestore data snapshots
self.mediumsamples = querySnapshot!.documents.compactMap({MediumSample(dictionary: $0.data())})
//fill the tableView
DispatchQueue.main.async {
self.tableView.reloadData()
print(self.mediumsamples)
}
print("Mediums List", self.mediumsamples) // This line returns: Mediums List []
print("Mediums List size", (self.mediumsamples.count)) // This line returns: Mediums List size 0
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())") // This line returns the snapshot documents correctly!
}
}
}
}
Here is how the screenshot object object is added:
func addMediumSamples() {
let currentDateTime = Date()
let location = FirebaseFirestore.GeoPoint(latitude: 0, longitude: 0)
let mediumsample = MediumSample(id: "an id", field_t: "field_t", field_i: 10, field_b1: true, field_b2: false, field_g: location, field_d: currentDateTime, field_a: [Constants.SAMPLE_DEV], usecase: Constants.SAMPLE_DEV)
var ref: DocumentReference? = nil
ref = self.db.collection(Constants.MEDIUMS).addDocument(data: mediumsample.dictionary) {
error in
if let error = error {
print("Error writing city to Firestore: \(error)")
} else {
print("Document added with id : \(ref!.documentID)")
}
}
}
The problem is in the MediumSample struct, in the field_d type (Date).
The type of that field in your Cloud Firestore database is Timestamp.
The field "field_d" in the MediumSample struct expects a value of type Date.
You can change the type to the FirebaseFirestore.Timestamp, or you can convert it to Date when mapping and before passing to the MediumSample.
eg. for converting Timestamp to Date in Swift
let date = timestamp.dateValue()

Realm Model Conversion Failed

I have a realm db model like:
import RealmSwift
class User: Object {
let isVerified = RealmOptional<Bool>()
#objc dynamic var pk = 0
#objc dynamic var profilePicUrl: String? = nil
}
And I am getting data from service and it returns same name and type like realm model.
I want to save this data to db. But when I try to convert model to realm model it gives error.
let data = [Users(value: serviceUser)] -> serviceUser comes from service.
Before save when I try to convert I get this error:
'RLMException', reason: 'Invalid value 'ServiceUserModel(isVerified: Optional(false), pk: Optional(123456), profilePicUrl: Optional("") of type '__UserDataValue' for 'bool' property 'Users.isVerified'.'
serviceUser Model:
public struct ServiceUserModel: Codable {
public var isVerified: Bool?
public var pk: Int?
public var profilePicUrl: String?
}
I do not want to use for loop because of performance problem. I want to save this data in one time.
Save Method:
func save(users: [Users]){
try! database.write {
database.add(users)
}
}
How can I convert it?
I think the problem is located here:
when I try to convert
When dealing with RealmOptional you have to set it's .value property.
let isVerified = RealmOptional<Bool>()
now the isVerified.value is nil
isVerified.value = true
now it's true
But you can't use the Bool? to set the value.
The same applies to pk.
You should implement the convenience init() and provide some default values for your optionals

How can I save an object of a custom class in Userdefaults in swift 5/ Xcode 10.2

I want to save the array patientList in UserDefaults. Patient is an custom class so I need to transfer it into Data object, but this doesn't work on Swift 5 like it did before.
func addFirstPatient(){
let newPatient = Patient(name: nameField.text!, number: numberField.text!, resultArray: resultArray, diagnoseArray: diagnoseArray)
let patientList: [Patient] = [newPatient]
let encodeData: Data = NSKeyedArchiver.archivedData(withRootObject: patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
UserDefaults.standard.synchronize()
}
struct Patient {
var diagnoseArray: [Diagnose]
var resultArray: [Diagnose]
var name: String
var number: String
init(name: String, number: String, resultArray: [Diagnose], diagnoseArray: [Diagnose]) {
self.diagnoseArray = diagnoseArray
self.name = name
self.number = number
self.resultArray = resultArray
}
}
struct Diagnose{
var name: String
var treatments: [Treatment]
var isPositiv = false
var isExtended = false
init(name: String, treatments: [Treatment]) {
self.name = name
self.treatments = treatments
}
}
struct Treatment {
var name: String
var wasMade = false
init(name: String) {
self.name = name
}
}
This is what the function looks like.
The problem is in the line where I initialize encodeData.
let encodeData: Data = try! NSKeyedArchiver.archivedData(withRootObject: patientList, requiringSecureCoding: false)
This is what Swift suggests but when I try it like this it always crashes and I don't get the error
You cannot use NSKeyedArchiver with structs at all. The objects must be subclasses of NSObject which adopt NSCoding and implement the required methods.
As suggested in the comments Codable is the better choice for example
struct Patient : Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose : Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment : Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
do {
let encodeData = try JSONEncoder().encode(patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
// synchronize is not needed
} catch { print(error) }
If you want to provide default values for the Bool values you have to write an initializer.
Vadian's answer is correct, you cannot use NSKeyedArchiver with structs. Having all your objects conform to Codable is the best way to reproduce the behavior you are looking for. I do what Vadian does, but I you can also use protocol extensions to make this safer.
import UIKit
struct Patient: Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose: Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment: Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
Introduce a protocol to manage the encoding and saving of objects.
This does not have to inherit from Codable but it does for this example for simplicity.
/// Objects conforming to `CanSaveToDisk` have a save method and provide keys for saving individual objects or a list of objects.
protocol CanSaveToDisk: Codable {
/// Provide default logic for encoding this value.
static var defaultEncoder: JSONEncoder { get }
/// This key is used to save the individual object to disk. This works best by using a unique identifier.
var storageKeyForObject: String { get }
/// This key is used to save a list of these objects to disk. Any array of items conforming to `CanSaveToDisk` has the option to save as well.
static var storageKeyForListofObjects: String { get }
/// Persists the object to disk.
///
/// - Throws: useful to throw an error from an encoder or a custom error if you use stage different from user defaults like the keychain
func save() throws
}
Using protocol extensions we add an option to save an array of these objects.
extension Array where Element: CanSaveToDisk {
func dataValue() throws -> Data {
return try Element.defaultEncoder.encode(self)
}
func save() throws {
let storage = UserDefaults.standard
storage.set(try dataValue(), forKey: Element.storageKeyForListofObjects)
}
}
We extend our patient object so it can know what to do when saving.
I use "storage" so that this could be swapped with NSKeychain. If you are saving sensitive data (like patient information) you should be using the keychain instead of UserDefaults. Also, make sure you comply with security and privacy best practices for health data in whatever market you're offering your app. Laws can be a very different experience between countries. UserDefaults might not be safe enough storage.
There are lots of great keychain wrappers to make things easier. UserDefaults simply sets data using a key. The Keychain does the same. A wrapper like https://github.com/evgenyneu/keychain-swift will behave similar to how I use UserDefaults below. I have commented out what the equivalent use would look like for completeness.
extension Patient: CanSaveToDisk {
static var defaultEncoder: JSONEncoder {
let encoder = JSONEncoder()
// add additional customization here
// like dates or data handling
return encoder
}
var storageKeyForObject: String {
// "com.myapp.patient.123"
return "com.myapp.patient.\(number)"
}
static var storageKeyForListofObjects: String {
return "com.myapp.patientList"
}
func save() throws {
// you could also save to the keychain easily
//let keychain = KeychainSwift()
//keychain.set(dataObject, forKey: storageKeyForObject)
let data = try Patient.defaultEncoder.encode(self)
let storage = UserDefaults.standard
storage.setValue(data, forKey: storageKeyForObject)
}
}
Saving is simplified, check out the 2 examples below!
do {
// saving just one patient record
// this saves this patient to the storageKeyForObject
try patientList.first?.save()
// saving the entire list
try patientList.save()
} catch { print(error) }
struct Employee: Codable{
var name: String
}
var emp1 = Employee(name: "John")
let encoder = JSONEncoder()
do {
let data = try encoder.encode(emp1)
UserDefaults.standard.set(data, forKey: "employee")
UserDefaults.standard.synchronize()
} catch {
print("error")
}