I have a setup where I fetch some json data from a server to populate a table. There is a chance that the data has changed, so I fetch all the data each time. I map that data into a Realm object and persist it to the database. A primary ID is used to prevent duplication.
I use Realm notifications to keep the tableview/collection view in sync with the datasource. When an server request completes, objects are updated or added to the database, and the views automatically reload.
The problem is that all of the cells reload because all of the objects in the database get updated, even if they aren't actually any different because I'm just blindly using realm.add(object, update:true). Is there is a good way to prevent updating objects that haven't actually changed so that cell's aren't needlessly reloaded?
The solution I tried was to write an extension to Realm's Object class including a function that checks if any objects with the same primary ID exist, compare them, and add/update the Object iff they don't match. However, I have many classes of objects, and I couldn't find a way to get the objects type from the object itself without knowing its class to start with.
// All subclasses of ServerObject have id as their primaryKey
let object = database.objectForPrimaryKey(type:???, key: self.id)
I do not want to copy the same hunk of check-before-add code to every one of my classes because that's asking for trouble, so I need something that can go in a protocol or extension, or just a completely different way to go about handling the server's response.
It sounds like you want something along the lines of:
extension Object {
public func hasChanges(realm: Realm) -> Bool {
guard let obj = realm.objectForPrimaryKey(self.dynamicType, key: self["id"])
else { return true }
for property in obj.objectSchema.properties {
let oldValue = obj[property.name]
let newValue = self[property.name]
if let newValue = newValue {
if oldValue == nil || !newValue.isEqual(oldValue) {
return true
}
} else if oldValue != nil {
return true
}
}
return false
}
}
This would be used as:
let obj = MyObjectType()
obj.id = ...;
obj.field = ...;
if obj.hasChanges(realm) {
realm.add(obj, update: true)
}
For object with nested objects (lists), this modified solution seems to work well.
// This function is general and can be used in any app
private func hasChanges(realm: Realm) -> Bool {
guard let obj = realm.objectForPrimaryKey(self.dynamicType, key: self["id"])
else { return true }
for property in obj.objectSchema.properties {
// For lists and arrays, we need to ensure all the entries don't have changes
if property.type == .Array {
let list = self.dynamicList(property.name)
for newEntry in list {
if newEntry.hasChanges(realm) {
return true
}
}
// For all properties that are values and not lists or arrays we can just compare values
} else {
let oldValue = obj[property.name]
let newValue = self[property.name]
if let newValue = newValue {
if oldValue == nil || !newValue.isEqual(oldValue) {
return true
}
} else if oldValue != nil {
return true
}
}
}
return false
}
Related
I wonder if I should create my own additional layer when updating Realm objects to avoid redundant database writing operations or is it done automatically on a lower level?
Let's take an example:
class SomeEntity: Object {
#Persisted(primaryKey: true) var id = 0
#Persisted var aaa: String?
#Persisted var bbb: Float?
#Persisted var ccc: Int?
}
when doing some batch update:
newDownloadedData.forEach { entry in
guard let id = entry["id"].int else {
return
}
try? localRealm.write {
let entity = existingLocalEntities.first { $0.id == id } ?? SomeEntity(id: id)
localRealm.add(entity, update: .modified) //this makes an 'upsertion' which is automatically an update or insert
if entity.aaa != entry["aaa"].string {
entity.aaa = movieInfo["aaa"].string
}
if entity.bbb != entry["bbb"].float {
entity.bbb = movieInfo["bbb"].float
}
if entity.ccc != entry["ccc"].int {
entity.ccc = movieInfo["ccc"].int
}
}
}
I wonder if these checks necessary or can I just go with:
entity.aaa = movieInfo["aaa"].string
entity.bbb = movieInfo["bbb"].float
entity.ccc = movieInfo["ccc"].int
and not worry that values will be updated and written even if downloaded values are the same as existing local ones?
Your observers will be notified if you update a property on a realm object with the same value. Realm does not care if you use a different value or not.
I'm not sure what your use case is, but it may be a pain in the butt to check every value manually.
You can do something like this though:
protocol UniqueUpdating { }
extension UniqueUpdating where Self: AnyObject {
#discardableResult
func update<Value: Equatable>(
_ keyPath: ReferenceWritableKeyPath<Self, Value>,
to value: Value
) -> Bool {
guard self[keyPath: keyPath] != value else { return false }
self[keyPath: keyPath] = value
return true
}
}
extension Object: UniqueUpdating {}
class Person: Object {
#Persisted(primaryKey: true) var id: Int = 0
#Persisted var name: String = ""
}
Usage would be like this:
let realm = try! Realm()
try! realm.write {
person.update(\.name, to: "BOB")
}
EDIT: things described below do not work like expected.
eg. if name = "Tom" and later the same value is assigned ( self.name = "Tom") it will be treated as modification.
It turns out that YES, Realm can be smart about it!
the clue sits in update parameter in .add function.
using .modified will result in smart data write.
Excerpt from documentation:
/**
What to do when an object being added to or created in a Realm has a primary key that already exists.
*/
#frozen public enum UpdatePolicy: Int {
/**
Throw an exception. This is the default when no policy is specified for `add()` or `create()`.
This behavior is the same as passing `update: false` to `add()` or `create()`.
*/
case error = 1
/**
Overwrite only properties in the existing object which are different from the new values. This results
in change notifications reporting only the properties which changed, and influences the sync merge logic.
If few or no of the properties are changing this will be faster than .all and reduce how much data has
to be written to the Realm file. If all of the properties are changing, it may be slower than .all (but
will never result in *more* data being written).
*/
case modified = 3
/**
Overwrite all properties in the existing object with the new values, even if they have not changed. This
results in change notifications reporting all properties as changed, and influences the sync merge logic.
This behavior is the same as passing `update: true` to `add()` or `create()`.
*/
case all = 2
}
I have a Models class defined like this:
class BaseModel: Object {
var data: JSON = JSON.null
convenience init(_ data: JSON) {
self.init()
self.data = data
}
override static func ignoredProperties() -> [String] {
return ["data"]
}
}
class RecipeModel: BaseModel {
dynamic var title: String {
get { return data["fields"]["title"].stringValue }
set { self.title = newValue }
}
... more vars ...
var ingredients: List<IngredientsModel> {
get {
let ingredients = List<IngredientsModel>()
for item in data["fields"]["ingredients"] {
ingredients.append(IngredientsModel(item.1))
}
return ingredients
}
set { self.ingredients = newValue }
}
}
class IngredientsModel: BaseModel {
dynamic var text: String {
get { return data["text"].stringValue }
set { self.text = newValue }
}
... more vars ...
}
And I would like to use it something like this:
Api.shared.fetchAllEntries().call(onSuccess: {response in
print(response.json)
let realm = try! Realm()
try! realm.write {
realm.deleteAll()
}
for item in response.json["items"].arrayValue {
let recipe = RecipeModel(item)
try! realm.write {
realm.add(recipe)
}
}
}, onError: {
print("error")
})
So basically the idea is to just pass the whole JSON to the initial RecipeModel class, and it should parse it out and create the objects I need in the Realm database. It works quite well except for the nested list of IngredientsModel. They do not get added to the realm database.
What I see as a potential problem is that I call self.init() before I call self.data in the convenience init, but I do not see any way to work around this. Do you guys please know how I could achieve that also the IngredientsModel would have its contents set up properly and I would have a list of ingredients in the RecipeModel?
Your current implementation doesn't work, because you are not calling the getter/setter of ingredients in the init method of RecipeModel and hence the IngredientsModel instances are never persisted in Realm.
Moreover, using a computed property as a one-to-many relationship (Realm List) is a really bad idea, especially if you are parsing the results inside the getter for this property. Every time you call the getter of ingredients, you create new model objects instead of just accessing the existing ones that are already stored in Realm, but you are never deleting the old ones. If you were actually saving the IngredientsModel instances to Realm (which you don't do at the moment as mentioned above) you would see that your database is full of duplicate entries.
Your whole approach seems really suboptimal. You shouldn't store the unparsed data object in your model class and use computed properties to parse it. You should parse it when initializing your models and shouldn't store the unparsed data at all. You can use the ObjectMapper library for creating Realm objects straight away from the JSON response.
My application code gets JSON data from a server, converts it to a Dictionary and then uses that to hydrate and attempt to save a RealmSwift object with a matching schema.
I ran into a crash caused by a Float value coming into a field that was declared as Int in the model. RLMException was thrown.
As suggested in this thread, one doesn't simply try to catch RLMException.
My question is, is that answer correct? And, if so, what is the correct way to prevent a crash when an unexpected value is found? Isn't there some sort of validation mechanism that I can run on all of the values before attempting to set them?
You can use third party frameworks for mapping JSON to Realm objects, like ObjectMapper and use it's failable initializer to validate your JSON.
This failable initializer can be used for JSON validation prior to object serialization. Returning nil within the function will prevent the mapping from occuring. You can inspect the JSON stored within the Map object to do your validation.
You can use this to see if everything is ok with json parameters
static func createFromJSONDictionary(json: JSONDictionary) throws -> MyObject {
guard let
property1 = json["property1"] as? Int,
property2 = json["property1"] as? String else {
// Throw error.... or create with default values
throw NSError(domain: "", code: 0, userInfo: nil)
}
// Everything is fine, create object and return it
let object = MyObject()
object.something = property1
return object
}
I ended up writing an extension to handle this. At the moment I'm only validating numbers and dates, but the switch statement could check every single property type easily.
extension Object {
public convenience init( withDictionary rawData: [String:AnyObject] ) {
var sanitizedData = rawData
let dynamicSelf = self.dynamicType
let schema = dynamicSelf.sharedSchema()
for property in schema.properties {
guard let value = sanitizedData[property.name] else { continue }
var isValid = true
switch property.type {
case .Double:
let val = Double(String(value))
if val == nil || val!.isNaN {
isValid = false
}
case .Float:
let val = Float(String(value))
if val == nil || val!.isNaN {
isValid = false
}
case .Int:
if Int(String(value)) == nil {
isValid = false
}
case .Date:
if Int64(String(value)) == nil {
isValid = false
} else {
sanitizedData[property.name] = NSDate(timeIntervalSince1970: value.doubleValue / 1000)
}
default:
break
}
if !isValid {
log.error( "Found invalid value: \(value) for property \(property.name) of \(dynamicSelf)" )
sanitizedData.removeValueForKey(property.name)
}
}
self.init( value: sanitizedData )
}
}
I'm trying to understand some code in a project I'm working on. I have an array property of strings:
var names: [String]!
func findName(name: String?) -> [Name]? {
if name != nil {
return nameManager.namesForSearchString(name)?.filter({self.names.contains($0.name)})
} else {
return nameManager.allNames.filter({self.names.contains($0.name)}) //<-what get's returned here?
}
}
What I don't understand is if the name is nil, what happens when .contains is called, and with that, what happens when .filter gets called? This is implemented in a Favorites class, and I need to call this function to return all favorites if a button is tapped, so what would I pass to this function to ensure that all the contents of Names: [Name] are returned?
On a lower level, I want to understand how .contains and .filter work and what gets returned if nil is passed to them.
Another version of the same method from a different commit (that I also did not write) is this:
func findFavorites(name: String?) -> [Station]? {
if name != nil {
return nameManager.namesForSearchString(name)!.filter({contains(self.names, $0.objectId)})
} else {
return nameManager.allNames.filter({contains(self.names, $0.objectId)})
}
}
I don't want to post a non-answer, but I do want this to be properly formatted so I guess a comment won't do. This might help you understand what's going on, and what happens with filter/contains. If you have any more questions, let me know, and I'll answer the question. If I'm completely off-base, let me know as well!
// I don't know why this is implicitely unwrapped, as a nil in this Array crashes Playground execution
var localNames: [String!] = ["Troy", "Bob", "Donald"]
// I'm just modelling what I know about NameManager
struct NameManager {
var allNames = [Name(name: "Bob"), Name(name: "Liz"), Name(name: "Anastasia")]
}
// I also assume the `name` in Name is a non-optional.
struct Name {
var name: String = "some name"
}
var nameManager = NameManager()
func findName(name: String?) -> [Name]? {
// Case where `name` is non-nil is excluded for demonstration purposes
// I have expanded all the closure short-hands so we always see what we're doing.
let allNames = nameManager.allNames
// namesMatchingName is of type [Name], that we get by applying a filter.
// `filter` works on a predicate basis: it goes through each element, one at a time,
// and checks if it meets the "predicate", that is, a boolean
// condition that returns true or false. If it DOES meet the criteria, it will be included in
let namesMatchingName = allNames.filter { (currentName) -> Bool in
// Now we're inside the filter-predicate. What we do here is check if the `currentName`
// is in `localNames`.
let namesHasCurrentName = localNames.contains(currentName.name)
// If the name IS in `localNames` we return true to the filter,
// which means it will be included in the final array, `namesMatchingName`.
return namesHasCurrentName
}
// So now we have all the names that appear in both `nameManager.allNames` and `localNames`
return namesMatchingName
}
findName(nil) // returns [{name: "Bob"}]
I have a function that gets and parses data from Firebase. I have a validation "parseUserModel) that returns "Bool" type if the model meets the requirements to pass. I set this value to validUser which is of Bool type. The model I'm pulling actually passes and returns a boolean of true.
Here's where the issue happens.
If(validUser) { ... code ... } fails even though the validUser is "true".
I've played around with the actual return type and I've tried these things:
validUser.boolValue and setting this inside the if(validUser.boolValue)
this fails
setting the if(validUser) { ... code ... }
this fails
setting the if(validUser == true) {..code..}
this fails
setting the if(validUser.boolValue == true) {code}
this fails
I'm all out of ideas as to why exactly this is failing. I'm comparing a primitive type (I'm assuming) to another primitive type.
My Questions
is the 'true' keyword in Swift not actually a Bool object type?
why is this failing when I'm explicitly asking for the boolValue?
is their a Swift bug I should know about?
is the if statement not doing what I think it should be doing?
Retrieve Data Function
public static func retrieveUserData(user:UserModel, completion:(success:Bool)->Void){
if(user.username != nil){
let fbRef = Firebase(url: GlobalVariables().FB_BASE_URL+"users/\(user.username)")
fbRef.observeSingleEventOfType(FEventType.Value, withBlock: { (snapshot) -> Void in
if(snapshot.value != nil){
let validUser:Bool = UserModel.sharedInstance.parseUserData(JSON(snapshot.value))
println(validUser)
println(validUser.boolValue)
if(validUser.boolValue){
completion(success: true)
} else {
completion(success: false)
}
}
})
}
}
Parse Model and Validate
public func parseUserData(data:JSON) -> Bool {
var uName:String!
var uFullName:String!
if let username = data["username"].string {
UserModel.sharedInstance.username = username
}
if let fullName = data["fullName"].string {
UserModel.sharedInstance.fullName = fullName
} else {
return false
}
if let biography = data["biography"].string {
UserModel.sharedInstance.biography = biography
}
if let email = data["email"].string {
UserModel.sharedInstance.email = email
}
if let plistID = data["playlistCollectionID"].string {
UserModel.sharedInstance.playlistCollectionID = plistID
} else {
//generate a playlistCollectionID because the user doesn't have one
UserModel.sharedInstance.playlistCollectionID = NSUUID().UUIDString
}
if(UserModel.validateUserObject(UserModel.sharedInstance)) {
return true
} else {
return false
}
}
Println() log from Xcode
true
true
the above two true are coming from the retrieval code
println(validUser) and println(validUser.boolValue)
The Bool type in Swift conforms to BooleanType, which has the member:
var boolValue: Bool { get }
The value stored in validUser is definitely a Bool, because otherwise you wouldn't be able to put it in a variable of the Bool type.
The most likely situation here is that there is a bug in your code. Go over it again and make sure that it's actually entering the else block instead of the if block.
As a side note, the Swift formatting convention for if statements looks like this:
if condition {
...
} else {
...
}
as opposed to if(condition){...
Due to my own idiocy I was sending the shared user instance UserModel into itself.
private static func validateUserObject(object:UserModel) -> Bool {
println(object.username)
println(object.fullName)
if(object.username != nil && object.fullName != nil){
return true
} else {
return false
}
}
So I would call the method like this...
UserModel.sharedInstance.validateUserObject(self)
This would cause the Function specialization explosion. This would not crash the app (which is the strange part). It would continue and return the true boolean (because the model was valid).