"Correct" Realm Models in swift? [closed] - swift

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 6 years ago.
Improve this question
I'm currently implementing my first app in swift, which uses Realm. I really like it! However, I tried to make my models "good" but I really feel I made them worse for realm. Here's an example model:
import RealmSwift
class Location : Object {
dynamic var ident = ""
dynamic var package = ""
dynamic var title = ""
dynamic var is_selected = false
let contentSets = List<ContentSet>()
convenience init(ident : String, package: String, title : String, is_selected : Bool) {
self.init()
self.ident = ident
self.package = package
self.title = title
self.is_selected = is_selected
}
override static func primaryKey() -> String? {
return "ident"
}
func save() {
let realm = try! Realm()
try! realm.write {
realm.add(self)
}
}
static func findAll() -> Results<Location> {
return try! Realm().objects(Location)
}
static func findByIdent(ident : String) -> Location?{
return try! Realm().objects(Location).filter("ident == %#", ident).first as Location?
}
static func getSelected() -> Location? {
return try! Realm().objects(Location).filter("is_selected == true").first as Location?
}
func hasContentSetByObject(contentSet : ContentSet) -> Bool {
return self.hasContentSetByString(contentSet.ident)
}
func addContentSet(contentSet: ContentSet) {
let realm = try! Realm()
try! realm.write {
self.contentSets.append(contentSet)
}
}
func isSelected(value: Bool) {
let realm = try! Realm()
let selectedLocation = Location.getSelected()
selectedLocation?.isSelected(false)
try! realm.write {
self.is_selected = value
try! realm.commitWrite()
}
}
func hasContentSetByString(ident : String) -> Bool {
let result = self.contentSets.filter{$0.ident == ident}.count > 0 ? true : false
return result
}
}
My idea was, to keep everything realm related out of my controllers. However, in regards to update data on the models this approach is bad I feel, because it erases a lot of Realm's flexibility.
How are you guys doing that sort of stuff? Looking forward to your input.
Regards,
SantoDE

You can preserve your "separation of concerns" without having to sacrifice the whole rationale behind Realm's transactional mutation model, or the internals of its querying syntax.
Here are a few tips:
1. Minimize the number of areas where you need to do error handling.
From your small sample, there are 10 failable calls (try!) with no error handling. This is a code smell. As in, not necessarily incorrect, but should make you take a closer look and reconsider the pattern.
Even if you were handling errors in each case, that would be tedious and error-prone.
Instead, why not keep the error handling as localized as possible, yielding simpler code, less duplication and less opportunity for unhandled errors?
You can do this by using dependency injection, lifting the Realm instance out of the model methods, and passing the Realm instance to the parts of your code that need it.
2. Avoid redundant type casts
Realm Swift makes heavy use of Swift's generics system, yielding dynamic casts like this one redundant:
try! Realm().objects(Location).filter("ident == %#", ident).first as Location?
Because the following is even more type safe, since there's no opportunity to use the wrong type:
try! Realm().objects(Location).filter("ident == %#", ident).first
3. Avoid redundant write commits
this code:
try! realm.write {
self.is_selected = value
try! realm.commitWrite()
}
is incorrect, since a Realm.write(_:) invocation automatically calls Realm.commitWrite() after invoking the passed-in closure. So just replace with this:
try! realm.write {
self.is_selected = value
}
4. Avoid leaving Realm's query system
Code like this forces Realm to materialize all its objects as Swift objects, which performs very poorly:
let result = self.contentSets.filter{$0.ident == ident}.count > 0 ? true : false
return result
Instead, you should prefer Realm's native querying system, which is optimized for Realm data:
return !contentSets.filter("ident == %#", ident).isEmpty
In general, I'd also say that if you find yourself writing lots of code in the name of separation of concerns, what are you really gaining?

Related

Realm and Swift - parameters to pass for the model to be updated

In the beginning of the project, Realm is great and easy to work and the project is getting complicated so I need to figure out how to decouple the realm layer and uiviewcontroller.
There is some awkwardness by writing a realm object with parameters. I would like to have object updated with the parameter then pass to the realm database to update object in (table?). Initially, I have a function to write a realm object by -
func createOrUpdateNote(note : Note, body : String, textSize : Float, toggleColor : Int) {
let realm = try! Realm()
do {
try realm.write {
if note.id == -1 {
note.id = NoteManager.createNotePrimaryId()
}
note.body = body
note.textSize = textSize
note.toggleColor = toggleColor
realm.add(note, update: true)
}
}
catch {
print(error.localizedDescription)
}
}
I would like to have like this function. Hope it clears up my question here.
func createOrUpdateNote(note : Note) {
let realm = try! Realm()
do {
try realm.write {
realm.add(note, update: true)
}
}
catch {
print(error.localizedDescription)
}
}
Now I have another viewcontroller to update the object with their preference of using language below.
func createOrUpdateNote(note : Note, language : String) {
let realm = try! Realm()
do {
try realm.write {
if note.id == -1 {
note.id = NoteManager.createNotePrimaryId()
}
note.language = language
realm.add(note, update: true)
}
}
catch {
print(error.localizedDescription)
}
}
There will be two similar functions (dup functions) in database layer and that will not work well when the project is getting more features so I'm stepping back to see if I can redesign the create or update the object approach.
I googled and there are several solutions like making UI object and copy the ui values over to the realm objects each time I do CRUD, create object with internal realm object (1 to 1 mapping), or I'm thinking about partial update but not sure how can I approach this situation. Ideally, I would prefer only object to carry over to be updated. Any suggestions?
If you have an existing (managed) Realm object, its properties can only be modified within a write block. However, ANY of it's properties can be modified and you really only need one block 'style' to do it.
So for example, if we have a Note Realm object
Note: Object {
#objc dynamic var body = ""
#objc dynamic var textSize = 12
#objc dynamic var language = "English"
}
any time we have access to that Note, we can modify it's properties within a write closure. Let's say a user is editing an existing note and changes the body, then clicks Save.
let realm = try Realm()
try! realm.write {
myNote.body = updatedBodyText
realm.add(myNote, update: true)
}
or they change the text size
let realm = try Realm()
try! realm.write {
myNote.textSize = updatedTextSize
realm.add(myNote, update: true)
}
Notice that those blocks are identical, other than which property is updated. The key is to hang on to a reference to the note when loaded, so you can then modify it's properties in a write block when saving.
There's no problem having multiple write blocks depending on what property you're saving. It really depends on your use case but that's common practice.
Generically speaking it could also be rolled into one function, something like this:
func saveMyNote(myNote: NoteClass, updatedData: String, fieldType: NoteFieldTypes) {
try! realm.write {
switch fieldType:
case .body:
myNote.body = updatedData
case .language:
myNote.language = updatedData
etc etc
realm.add(myNote, update: true)
}
You could also extend the class or a variety of other solutions. In general the write code is so small I would just use it wherever you need to update an objects fields.

Using JSONEncoder for Equatable method in Swift

I know JSON keys don't have any order and can be generated randomly, but there isn't anything preventing me from writing this function and from my tests this works on my every use case I tested.
func ==<T: Encodable> (lhs: T, rhs: T) -> Bool {
let encoder = JSONEncoder()
do {
let leftEncoded = try encoder.encode(lhs)
let rightEncoded = try encoder.encode(rhs)
return leftEncoded == rightEncoded
} catch {
return false
}
}
The problem I want to solve is writing a function for a type which has an array of one protocol with like 20 different implementation, which I have to implement the == function instead of swift auto synthesise. And I know I can switch to JSONSerialization.writeJSONObject with option .sortedKeys to persist the keys order.
What is the downside of this implementation instead of any other method of writing == function?
The answer seems to be in your question: "JSON keys don't have any order." Also, two things that are logically equal might encode differently (if their equality is dependent only on their id for example). Also, reference types that encode the same values may not be equatable at all (UIView for example). Also, this creates a very broad == overload that makes type-checking more expensive (much as + is extremely expensive).
What you're doing is trying to auto-conform to Equatable when it can be synthesized. Swift could have done this; in fact, for most Encodable cases, Equatable can be synthesized just by adding : Equatable. The Swift team explicitly chose not to auto-conform in those cases and force you to request it when you mean it, because it doesn't always mean "all the properties contain the same bits." When the Swift team explicitly chooses to avoid a feature, it is rarely a good idea to re-add it.
func ==<T: Codable> (lhs: T, rhs: T) -> Bool {
let encoder = JSONEncoder()
guard let lhsData = try? encoder.encode(lhs),
let rhsData = try? encoder.encode(lhs),
let left = try? JSONSerialization.jsonObject(with: lhsData) as? [String : Any],
let right = try? JSONSerialization.jsonObject(with: rhsData) as? [String : Any] else { return false }
let leftDictionary = left as NSDictionary
let rightDictionary = right
return leftDictionary.isEqual(to: rightDictionary)
}

RxSwift and control flow - a bad idea?

I have developed a struct that generates random data for testing purposes in my app but only does so if the Realm database is empty. The service class responsible for CRUD operations returns outcomes / results as RxSwift Observables.
Generating new data is predicated on the contents of the Realm - using guard is the logical choice but requires adding a new method to the service that returns Int rather than an Observable which seems to be an unnecessary duplication of code and somewhat inconsistent.
i.e.
class Service {
let realm = try! Realm()
...
// Existing reactive code
func patients() -> Observable<Result<Patient>> {
let results = realm.objects(Patient.self)
return Observable.collection(from: results)
}
func objectCount() -> Int {
let realm = try! Realm()
return realm.objects(Patient.self).count
}
}
struct DataGenerator() {
...
func createRandomPatients() {
let service = Service()
guard service.objectCount() == 0 else { return }
...
//go on to generate patients
The only way I can figure to replicate this control flow using the reactive service method is to bind the Observable to a Variable
i.e.
... service code as above ...
func createRandomPatients() {
let isEmpty = Variable<Bool>(false)
service.patients().map{ $0 == 0 }.bind(to: isEmpty)
guard isEmpty.value else { return }
// etc
It seems like a bit of a fudge as it perhaps isn't quite as clear, but it avoids adding code to the service class that won't be used in the production app. Is this bad practice or just personal preference? I'm open to alternative solutions if anyone has one...
Faking is definitely something that should be handled with polymorphism.
A good design would be to define a common interface; for example it can be PatientsSource:
protocol PatientsSource {
func patients() -> Observable<Result<[Patient]>>
}
Than you can take advantage of polymorphism by defining:
class PatientsService: PatientsSource... {
//w/e
}
struct FakePatients: PatientsSource {
//your random patients generation
}
If you use a Service style architecture then you should pass the Service and the PatientsSource to the user (like your controller) separately even though they might end up to be the same object.

Replaced List<T> object not persisting consistently in Realm

I have a List<Workout> object that occasionally needs to be sorted (e.g., if a user adds a Workout out of order), but I can't seem to get the new sorted List<Workout> to persist. My code works the moment it runs (i.e., it shows up on the view as sorted), but when I exit the ViewController or restart the app, I see nothing. The nothing is due to the exercise.workoutDiary.removeAll() persisting, but apparently the subsequent assignment to the exercise.workoutDiary = sortedWorkoutDiary is not persisting. Any ideas why?
Everything else works just fine. The typical recordWorkout() case works assuming nothing is entered out of order. So the persisting is working in nearly all cases except for this overwrite of the sorted List.
The update happens here:
struct ExerciseDetailViewModel {
private let exercise: Exercise!
func recordWorkout(newWorkout: Workout) {
let lastWorkout = exercise.workoutDiary.last // grab the last workout for later comparison
let realm = try! Realm()
try! realm.write {
exercise.workoutDiary.append(newWorkout) // write the workout no matter what
}
if let secondToLastWorkout = lastWorkout { // only bother checking out of order if there is a last workout...
if newWorkout.date < secondToLastWorkout.date { // ...and now look to see if they are out of order
let sortedWorkoutDiary = exercise.sortedWorkouts
try! realm.write {
exercise.workoutDiary.removeAll()
exercise.workoutDiary = sortedWorkoutDiary
}
}
}
}
}
final class Exercise: Object {
var workoutDiary = List<Workout>()
var sortedWorkouts: List<Workout> {
return List(workoutDiary.sorted("date"))
}
}
final class Workout: Object {
dynamic var date = NSDate()
var sets = List<WorkSet>()
}
List<T> properties in Realm Swift must be mutated in place, not assigned to. The Swift runtime does not provide any way for Realm to intercept assignments to properties of generic types. Instead, you should use methods like appendContentsOf(_:) to mutate the List<T>:
exercise.workoutDiary.removeAll()
exercise.workoutDiary.appendContentsOf(sortedWorkoutDiary)
This limitation on assignment to properties of generic types is why the Realm Swift documentation recommends that you declare such properties using let rather than var. This will allow the Swift compiler to catch these sorts of mistakes.
One further note: for your sortedWorkouts computed property, it'd be preferable for it to return Results<Workout> instead to avoid allocating and populating an intermediate List<Workout>.

How to reduce mutability with nested objects stored in Realm?

Full code on github
I am trying to rewrite my app to reduce mutability and take advantage of functional programming. I am having trouble figuring out where to start, since it seems like my architecture is to use modification in place almost everywhere. I could use some advice on a simple starting point of how to break this down into smaller pieces where I am maintaining immutability at each modification. Should I change my data storage architecture so that I am only storing/modifying/deleting the leaf objects?
Right now, from the root ViewController, I load my one monster object ExerciseProgram (which contains a RealmList of Exercise objects, which contains a RealmList of Workouts, which contains a RealmList of Sets....)
final class ExerciseProgram: Object {
dynamic var name: String = ""
dynamic var startDate = NSDate()
dynamic var userProfile: User?
var program = List<Exercise>()
var count: Int {
return program.count
}
}
Loaded here one time in MasterTableViewController.swift:
func load() -> ExerciseProgram? {
let realm = try! Realm()
return realm.objects(ExerciseProgram).first
}
and then modify the single ExerciseProgram object in place throughout the app, such as when recording a new workout.
To create a new Workout, I instantiate a new Workout object in RecordWorkoutTableViewController.swift:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if doneButton === sender {
if let date = newDate, weight = newWeight, setOne = newSetOne, setTwo = newSetTwo {
let newSets = List<WorkSet>()
newSets.append(WorkSet(weight: weight, repCount: setOne))
newSets.append(WorkSet(weight: weight, repCount: setTwo))
newWorkout = Workout(date: date, sets: newSets)
}
}
}
Which unwinds to ExerciseDetailTableViewController.swift where the storage occurs into the same monster ExerciseProgram object retrieved at the beginning:
#IBAction func unwindToExerciseDetail(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? RecordWorkoutTableViewController, newWorkout = sourceViewController.newWorkout {
let realm = try! Realm()
try! realm.write {
exercise.recordWorkout(newWorkout)
}
}
}
This behavior is replicated all over my app. If I want to edit or delete an existing workout, it's exactly the same.
The Exercise class is just this:
final class Exercise: Object {
dynamic var name = ""
dynamic var notes: String?
var workoutDiary = List<Workout>()
dynamic var goal = 0
...
func recordWorkout(newWorkout: Workout) {
workoutDiary.append(newWorkout)
}
func replaceWorkout(originalWorkout: Workout, newWorkout: Workout) {
workoutDiary[workoutDiary.indexOf(originalWorkout)!] = newWorkout
}
}
From what I can tell, looking at that schema, no, you shouldn't change it. If it's representing the types of information and their relations properly and it's already working in your app, then there's no need to change it.
If you feel it is overly complex or confusing, then it may be necessary to go back and look at your data model design itself before actually doing more work on the code itself. Review each relationship and each property in the linked objects, and make sure that it's absolutely critical that the data is saved at that level. In any case, Realm itself is very good at handling relationships between objects, so it's not 'wrong' to have several layers of nested objects.
Either way, Realm itself lends itself pretty well to functional programming since every property is explicitly immutable out of the box. Functional programming doesn't mean everything has to be immutable always though. Inevitably, you'll have to reach a point where you'll need to save changes to Realm; the mindset behind it is that you're not transforming data as you're working on it, and you minimise the number of points that actually do so.