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

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.

Related

Can't save string to Realm 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.

How to save and load GKGameModelPlayer from Realm in Swift?

I am attempting to implement a GKGameModel in my application. In it, it holds variables to a few things, but for the purposes of my question I'm interested in the following two variables:
import GameplayKit
final class GameModel: NSObject, GKGameModel {
var players: [GKGameModelPlayer]?
var activePlayer: GKGameModelPlayer?
}
I do something like this to initialise the game with 3 players (not exact)
let game = GameModel.init()
game.players = [Player(),Player(),Player()] // Create 3 players
guard let firstPlayer = game.players.first else {
return
}
game.activePlayer = firstPlayer
A player class is defined as:
class Player : NSObject, GKGameModelPlayer {
var playerId: Int // GKGameModelPlayer protocol variable
let name: String
var cash: Int = 0
}
In my project I have Realm Entities and the models seperated. So there will be a PlayerEntity and a Player class.
I'm wanting to use RealmSwift to save and load the GKGameModelPlayer data, and more specifically the ability to store/re-store the active player.
I think the key here is the playerId variable; but I am not sure.
But what I'm not sure about is retrieving this information and then re-mapping it into a valid GKGameModelPlayer format
My current idea/theory is that I need to map my model to an entity class and vice-versa.
Ie:
// [REALM] Player entity
class PlayerEntity: Object {
#objc dynamic var id = UUID().uuidString
#objc dynamic var playerId: Int = 0
#objc dynamic var name: String = ""
#objc dynamic var cash: Int = 0
override static func primaryKey() -> String {
return "id"
}
}
And then I extend this class to do some "mapping":
extension PlayerEntity {
// Map model -> entity
convenience init(model: Player) {
self.init()
self.playerId = model.playerId
self.name = model.name
self.cash = model.cash
}
}
extension Player {
// Map entity -> model
convenience init(entity: PlayerEntity) {
let playerId = entity.playerId
let name = entity.name
let cash = entity.cash
self.init(id: playerId, name: name, cash: cash)
}
}
Right now, the playerId is always zero (0) because I'm not really sure how to set it.
I can save a player to realm.
The issue comes from when I try to restore the player, and I want to restore the activePlayer variable in the GameModel
Therefore, my question is:
How would I go about saving and restoring the activePlayer variable so that it continues to comply to GKGameModelPlayer?
I appreciate any assistance on this.
With thanks
While you could use those extensions, sometimes simpler is better. Here's a rough example:
class PlayerEntity: Object {
#objc dynamic var playerId: Int = 0
#objc dynamic var name: String = ""
#objc dynamic var cash: Int = 0
convenience init(withPlayer: PlayerClass) {
self.init()
self.playerId = withPlayer.playerId
self.name = withPlayer.name
self.cash = withPlayer.cash
}
func getPlayer() -> Player {
let p = Player()
p.playerId = self.playerId
p.name = self.name
p.cash = self.cash
return p
}
override static func primaryKey() -> String {
return "playerId"
}
}
to load all the players into an array... this will do it
let playerResults = realm.objects(PlayerEntity.self)
for player in playerResults {
let aPlayer = player.getPlayer()
self.playerArray.append(aPlayer)
}
Notice the removal of
#objc dynamic var id = UUID().uuidString
because it's not really being used to identify the object as a primary key.
The primary key is really
var playerId: Int // GKGameModelPlayer protocol variable
which is fine to use as long as it's unique.

how to get single variable name from struct

I have a core data framework to handle everything you can do with coredata to make it more cooperateable with codable protocol. Only thing i have left is to update the data. I store and fetch data by mirroring the models i send as a param in their functions. Hence i need the variable names in the models if i wish to only update 1 specific value in the model that i request.
public func updateObject(entityKey: Entities, primKey: String, newInformation: [String: Any]) {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityKey.rawValue)
do {
request.predicate = NSPredicate.init(format: "\(entityKey.getPrimaryKey())==%#", primKey)
let fetchedResult = try delegate.context.fetch(request)
print(fetchedResult)
guard let results = fetchedResult as? [NSManagedObject],
results.count > 0 else {
return
}
let key = newInformation.keys.first!
results[0].setValue(newInformation[key],
forKey: key)
try delegate.context.save()
} catch let error {
print(error.localizedDescription)
}
}
As you can see the newInformation param contains the key and new value for the value that should be updated. However, i dont want to pass ("first": "newValue") i want to pass spots.first : "newValue"
So if i have a struct like this:
struct spots {
let first: String
let second: Int
}
How do i only get 1 name from this?
i've tried:
extension Int {
var name: String {
return String.init(describing: self)
let mirror = Mirror.init(reflecting: self)
return mirror.children.first!.label!
}
}
I wan to be able to say something similar to:
spots.first.name
But can't figure out how
Not sure that I understood question, but...what about this?
class Spots: NSObject {
#objc dynamic var first: String = ""
#objc dynamic var second: Int = 0
}
let object = Spots()
let dictionary: [String: Any] = [
#keyPath(Spots.first): "qwerty",
#keyPath(Spots.second): 123,
]
dictionary.forEach { key, value in
object.setValue(value, forKeyPath: key)
}
print(object.first)
print(object.second)
or you can try swift keypath:
struct Spots {
var first: String = ""
var second: Int = 0
}
var spots = Spots()
let second = \Spots.second
let first = \Spots.first
spots[keyPath: first] = "qwerty"
spots[keyPath: second] = 123
print(spots)
however there will be complex (or impossible) problem to solve if you will use dictionary:
let dictionary: [AnyKeyPath: Any] = [
first: "qwerty",
second: 123
]
you will need to cast AnyKeyPath back to WritableKeyPath<Root, Value> and this seems pretty complex (if possible at all).
for path in dictionary.keys {
print(type(of: path).rootType)
print(type(of: path).valueType)
if let writableKeyPath = path as? WritableKeyPath<Root, Value>, let value = value as? Value { //no idea how to cast this for all cases
spots[keyPath: writableKeyPath] = value
}
}

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")
}

How do I remove "Optional()" from object in an array

So im using CloudKit and fetching all the data into an array as [StartDay], my StartDay class looks like this:
import UIKit
import CloudKit
class StartDay {
var recordID: CKRecord.ID!
var wakeUp: String!
var sleptWell: String!
var dNN: String!
var created: String! {
get {
return created
}
}
}`
My function loads get an arraylist, which contains information received from the database. In my database it stands like this: "22.01.09:
func checkIfButtonShouldBeEnabled(startDayList: [StartDay]){
let startDayDates = startDayList.map{$0.created}
for i in 0..<startDayDates.count {
print(startDayDates)
}
}`
OUTPUT:
Optional("22.01.2019")
Optional("22.01.2019")
I want to remove "Optional()", so it only says "22.01.2019", how can I do so?
UPDATE: FETCH FUNC
func loadStartDay() -> [StartDay]{
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "StartDay", predicate: predicate)
let operation = CKQueryOperation(query: query)
var startDays: [StartDay] = []
operation.desiredKeys = ["wakeUp", "wellSlept", "dNN", "recordID", "createdDato"]
operation.recordFetchedBlock = { (record:CKRecord) in
let newStartDay = StartDay()
newStartDay.wakeUp = record.object(forKey: "wakeUP") as? String
newStartDay.sleptWell = record.object(forKey: "sleptWell") as? String
newStartDay.dNN = record.object(forKey: "dNN") as? String
newStartDay.recordID = record.object(forKey: "recordID") as? CKRecord.ID
newStartDay.created = record.object(forKey: "createdDato") as? String
print(newStartDay.created)
startDays.append(newStartDay)
}
You can use print(startDayDates!) or print(startDayDates ?? "default value").
But I recommend usage of startDayList.compactMap() instead of startDayList.map()to ensure your array doesn't contain nil values.
You can also do like this:
startDayList
.compactMap { $0.created }
.forEach { print($0) }
As you designed the database model you exactly know which record attributes always exist. Declaring class properties as implicit unwrapped optional as an alibi not to write an initializer is very bad practice.
Assuming every attribute in a record does have a value declare the properties as non-optional and write an initializer.
At least created and recordID are supposed to have always a value!
import UIKit
import CloudKit
class StartDay {
var recordID: CKRecord.ID
var wakeUp: String
var sleptWell: String
var dNN: String
var created: String
init(record : CKRecord) {
// recordID can be retrieved directly
self.recordID = record.recordID
self.wakeUp = record.object(forKey: "wakeUP") as! String
self.sleptWell = record.object(forKey: "sleptWell") as! String
self.dNN = record.object(forKey: "dNN") as! String
self.created = record.object(forKey: "createdDato") as! String
}
}
and create instances with
operation.recordFetchedBlock = { record in
startDays.append(StartDay(record: record))
}
Now the Optional has gone.
print(startDayList.map{ $0.created })