Optional Int property contains nil instead of zero during migration - ios

I am trying to perform Realm.IO migration from the following model:
public class RealmBuddy: Object {
dynamic var id = 0
let contactId = RealmOptional<Int>()
dynamic var name = ""
let topics = List<RealmTopic>()
override static func primaryKey() -> String? {
return "id"
}
}
to the following:
public class RealmBuddy: Object {
dynamic var id = 0
dynamic var contactId: String? = nil
dynamic var name = ""
let topics = List<RealmTopic>()
override public static func primaryKey() -> String? {
return "id"
}
}
As you can see, the single change is in the type of contactId: optional Int should be converted to optional String. I've implemented it like this:
if (oldSchemaVersion < 1) {
migration.enumerate(RealmBuddy.className()) { oldObject, newObject in
if let contactId = oldObject!["contactId"] as? Int {
newObject!["contactId"] = "\(contactId)"
}
}
}
However, I found that after migration my entities which had no contactId (i.e. nil value) now have contactId equal to "0" string. It appears that oldObject!["contactId"] returns 0 instead of nil.
I haven't find any info or examples on how to perform this simple task so I am not sure if I retrieve/cast the value properly (tried to cast to RealmOptional<Int> also), so I decided to ask about it here before filing an issue on github. Is it a bug or expected behavior? If it is expected behavior, how to get value properly?
PS: My project is open-source because I am new to iOS and develop it for self-education, so I pushed it to a repo where the issue can be easily reproduced (please note that it is in migration_issue branch). I also made a unit test using existing .realm file which migrates to new schema during test. Hope that will help.
PPS: I tried 0.102.1 and 0.103.1 versions of the library.
PPPS: It was mentioned that Realm Browser doesn't display any contactId with nil value in my sample .realm file. I think it is just Realm Browser issue, so I made another unit test in separate branch to prove that old database has nil value.

When I open default-v0.realm in the Realm Browser I don't see any rows with a null contactId. If you're expecting there to be one then the problem is in the code that initially populated the Realm and not the migration code.

It was a bug. See https://github.com/realm/realm-cocoa/pull/3643. Fixed in 0.103.2.
Optional properties of scalar types were getting the wrong Objective-C
type code, leading to the dynamic accessors taking the non-optional
code path. This resulted in nil values being returned as 0.

Related

Filter array that contains optional - SearchBar

just a bit of hindsight for you to understand my problem. I am currently coding an iOS app for events where I have a GuestList CoreData saved on device.
On viewDidLoad, it will fetch the coredata object and place it into an array of [GuestDetails]
Now the Guestdetail object struct is as follows:
private(set) public var guestFirstName: String
private(set) public var guestLastName: String?
private(set) public var guestEmail: String?
private(set) public var guestPhone: String?
private(set) public var guestUUID: UUID
private(set) public var guestBarcode: String?
private(set) public var guestCheckedIn: Bool
Such that only first name, UUID and checkinStatus are compulsory. I have already set up adding by JSON but now my issue is on my GuestListViewController, I have a searchbar
I am using the following code and array to make sure I can filter.
I have another array that is called
searchResultArray = [GuestDetails]()
So essentially I would copy all my guest details to searchResultArray and that is the one that the tableView is getting its sources from.
Now as part of the search, I used this code which I found on appcoda
searchResultArray = guestData.filter({guestData -> Bool in
(guestData.guestFirstName.lowercased().contains(searchText.lowercased())) ||
(guestData.guestLastName?.lowercased().contains(searchText.lowercased()))! ||
(guestData.guestBarcode?.contains(searchText))!
})
The issue is that my app is crashing, if I only search by first name it will not crash since those are force unwrapped nicely, but if I add the code to search lastName or Barcode, it will crash. I understand that it is probably because of explicitly unwrapped but xcode would not let me NOT unwrap it.
I have tried using map (which does not help, unless i am not familiar enough with it),
I have tried .compact (but I could not get it to work as i am not sure how it can access the inside of an GuestDetail object to remove nils)
The issue is the array of [GuestDetails] itself will not contain null some details inside a GuestDetails object might, hence causing it to crash.
My question is, how do I get it to search by firstname(already possible), lastname and barcode?
Thanks and I hope the question was elaborate enough.
I would avoid using forced unwrapping like so:
searchResultArray = guestData.filter({guestData -> Bool in
let searchLowercased = searchText.lowercased()
if guestData.guestFirstName.lowercased().contains(searchLowercased) {
return true
}
if let guestLastName = guestData.guestLastName, guestLastName.lowercased().contains(searchLowercased) {
return true
}
if let guestBarcode = guestData.guestBarcode, guestBarcode.lowercased().contains(searchLowercased) {
return true
}
return false
})
Answering your question from comments: You don't have to use ? operator on optionals because we use Optional Binding if let syntax. So for example:
if let guestLastName = guestData.guestLastName if guestData.guestLastName is nil than we will just jump out of this if statement. then you see , in the if statement, we will go pass this comma only if guestData.guestLastName is not nil, that is why we can use the guestLastName variable that is unwrapped String and is no longer optional String?, we than proceed to check if search term matches the guestLastName and return true.
Please read: if let , if var, guard let,guard var and defer statements in swift
It would be even better if you happen to add another property to your Data and avoid doing all this if else you can do something like:
searchResultArray = guestData.filter({guestData -> Bool in
let searchLowercased = searchText.lowercased()
let matches:[String?] = [guestData.guestFirstName, guestData.guestLastName, guestData.guestBarcode]
let nonNilElements = matches.compactMap { $0 }
for element in nonNilElements {
if element.lowercased().contains(searchLowercased) {
return true
}
}
return false
})
It's an interesting little problem, so let's generalize it. Here's our test data, comparable to your array of GuestDetails:
struct S {
var s1 : String
var s2 : String?
var s3 : String?
}
var array = [S]()
array.append(S(s1: "test", s2: "yo", s3: "ha"))
array.append(S(s1: "test", s2: nil, s3: nil))
array.append(S(s1: "Howdy", s2: "Bonjour", s3: "Hello"))
let target = "hello"
Some S properties are Optional, others are not.
So the problem is: Filter array down to only those elements where any S property contains our target string, using case insensitive comparison.
We can do that in one statement:
let filteredArray = array.filter {
[$0.s1,$0.s2,$0.s3].compactMap {$0}
.map {$0.localizedCaseInsensitiveContains(target)}
.contains(true)
}
It's not quite as efficient as what #Ladislav wrote, because we keep looping inside map even after we've found our string. But the inefficiency is probably not significant.

Retrieving object properties with relation to their parent categories

I am new to realm and iOS development so I apologize in advance if something isn’t explained properly or is just incorrect.
I have 2 Realm Object classes:
class Category: Object {
#objc dynamic var name: String = ""
#objc dynamic var color: String = ""
let trackers = List<Tracker>()
}
and
class Tracker: Object {
#objc dynamic var timeSegment: Int = 0
var parentCategory = LinkingObjects(fromType: Category.self, property:
"trackers")
}
I’m able to store new timeSegment properties consistently; however, the issue is that I cannot retrieve & display a collection of timeSegment values relating to their parentCategory. setting
var entries : Results<Tracker>?
produces all results for every category, which is the only result i'm able to pull so far after testing.
Any help is appreciated, and can follow up with any additional details. Thanks
You need to call objects on your Realm object with a filter for fetching only results that match a predicate. The realm object in this code is an instance of the Realm class.
func getTrackersWithName(_ name: String) -> Results<Tracker> {
return realm.objects(Tracker.self).filter("name = \"\(name)\"")
}
This tells Realm to fetch all objects that match the filter predicate. In this case, the filter predicate matches any object where the value of the "name" property matches the string that is passed into the method.

Realm is creating multiple entries for nested objects on update

I'm experiencing trouble understanding how updating objects works in Realm. I'd appreciate help in helping me to understand how updating nested objects work and why it doesn't work the way I expect it.
I started using Realm just recently, and here's what I want to use it for: I have a set of key value pairs stored on my server, that serve as localized values for strings used in my iOS app. On app launch every now and then I want to update my strings, so I pull them from the server and store them locally in realm on my iOS device. I want to have only ONE instance of those strings on my device.
Here are the classes:
import RealmSwift
public class LocalizedStrings: Object {
dynamic var id = 1
dynamic var version: String = ""
let assets = List<LocalizedString>()
override public static func primaryKey() -> String? {
return "id"
}
}
public class LocalizedString: Object {
dynamic var key: String = ""
dynamic var value: String = ""
}
Here's how I update the LocalizedStrings object:
realm.add(localizedStrings, update: true)
Here's how I access my strings:
func getLocalizedString(forKey key: String) -> String {
var result = key
try! realm.write {
let queryResult = realm.objects(LocalizedString.self).filter("key == %#", key)
// print(queryResult)
if queryResult.count == 1 {
result = queryResult[0].value(forKey: "value") as! String
}
}
return result
}
Now, I would expect, that whenever I update my LocalizedStrings, that the localizedStrings.assets list would get updated with new values. But instead, the assets are not updated, the list reference gets updated and I end up having multiple instances of the same string, which is not what I would expect from an update function. When I try to access a particular LocalizedString, it turns out there's multiple instances:
(...)
[19] LocalizedString {
key = update;
value = Update;
},
[20] LocalizedString {
key = update;
value = Update;
}
Perhaps I'm missing something obvious and I would really appreciate if someone could point me in the right direction, so I'd be able to achieve the behavior I'm looking for (which would be having the nested object actually updated, rather than having unnecessary duplicates of my objects).
Thanks!
Ok, so this answer helped me figure out, what was wrong with my setup. I was missing primaryKey in LocalizedString class.
From the answer above on how realm.add(object, update: true) works:
Documentation :
parameter object: The object to be added to this Realm.
parameter update: If true, the Realm will try to find an existing copy of the object (with the same primary
key), and update it. Otherwise, the object will be added.
So the same thing happens with nested objects. They can not be updated unless they have primaryKey.

Computed properties using Realm LinkedObject instances return nil

I'm experiencing slightly unusual behaviour when attempting to use computed properties to access linked Objects in a Realm Object subclass.
final class Patient: Object {
dynamic var name: String = ""
var parameters = List<Parameter>()
}
final class Parameter: Object {
dynamic var name: String = ""
dynamic var patient: Patient? {
return LinkingObjects(fromType: Patient.self, property: "parameters").first
}
}
The patient property on the Parameter class returns nil but, if you replace the code with the following, we get the expected behaviour:
var p = LinkingObjects(fromType: Patient.self, property: "parameters")
var q: Patient? {
return p.first
}
I suspect this is something to do with Realm's internal representation of LinkingObject. The code I used originally was referenced in a previous StackOverflow question and was accepted as a functional solution thus I guess it worked then so perhaps something has changed? Xcode 7, Swift 2.2
When Realm added the ability to query inverse relationships, that became the syntax to specify them. See https://realm.io/news/realm-objc-swift-0.100.0/ and https://realm.io/docs/swift/latest/#inverse-relationships for details.

Realm Swift iOS - Can't Set Primary Key

I'll try to explain my scenario as short as possible, I have read some comments on Realm GitHub Repo about this issue:
Terminating app due to uncaught exception 'RLMException', reason:
'Can't set primary key property 'id' to existing value 'xxxxxxx'.
Here's my issue:
I got two classes.
Appointment Model Class
import Foundation
import RealmSwift
class Appointment: Object {
dynamic var id = 0
dynamic var user_id: String?
dynamic var profile_id: String?
let mainMeeting = List<Meeting>()
let meetingsWithOtherInfo = List<Meeting>()
override static func primaryKey() -> String? {
return "id"
}
}
Meeting Model Class
import Foundation
import RealmSwift
class Meeting: Object {
dynamic var id = 0
dynamic var name: String?
dynamic var created_at: String?
// other info
dynamic var restaurant_venue: String?
override static func primaryKey() -> String? {
return "id"
}
}
I am fetching the Appointments from my server API like this
for fetchedAppointment in allAppointmentsFromAlamofire {
let existingAppointment: Results<(Appointment)>! = realm.objects(Appointment).filter("id = \(fetchedAppointment["id"]!)")
let newAppointment: Appointment = Appointment()
newAppointment.id = fetchedAppointment["id"]! as! Int
....
// add data to Meeting connected to Appointment
let newMeeting = Meeting()
newMeeting.id = fetchedAppointment["meetings"]["id"]! as! Int
...
// update or add new entry
try! realm.write {
print("NEW APPOINTMENT: \(newAppointment)")
realm.add(newAppointment, update: existingAppointment.count == 0 ? false : true)
}
}
The error comes out whenever the program is trying to update existing entry in realm - whenever the existingAppointment is 1. the workaround here, from what I've read from Github Realm is to delete the override static func primaryKey() in Meeting Class.
There is no issue if I am just adding new entries to Appointment, but again, the issue comes out if I will be updating, and the issue goes away if I remove the primaryKey() in Meeting Class ---- BUT, in other screens of my app, I really need to have that primaryKey() in Meeting Class.
My wild guess here is that every time that I need to update entries in Appointment, I should update too the Meeting.
So, the question is: why is this happening? Is my wild guess correct? Any other way to solve this?
It looks like you are trying to update the newAppointment object with a value that is not its primary key.
realm.add(newAppointment, update: existingAppointment.count == 0 ? false : true)
Instead, Realm is expecting you to provide the key for that object so that it can update the specified object.
It looks like you are setting the key value here, which is what you should use for your update.
let newAppointment: Appointment = Appointment()
newAppointment.id = fetchedAppointment["id"]! as! Int
Realm documentation on updating with keys.
You don't need to set the update argument of add(:_, update: _) to false if you provide a new object. If your model has a primary key and you want to create or update, you can pass true and Realm will automatically figure out whether an object with the same primary key is already managed in the database or not.

Resources