How to reduce mutability with nested objects stored in Realm? - swift

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.

Related

Realm - Update List Order

I've trying to reorder objects from at TableView in a Realm utility class. Many other Stack Overflow questions have said to uses List, however I can't seem to make it work. (Example, example). I'm able to successfully add objects to my list with:
public func addUserSong(song: Song) {
let songList = List<Song>()
songList.append(objectsIn: realm.objects(Song.self))
try! realm.write {
songList.append(song)
realm.add(songList)
}
}
However, I'm not able to preserve the updated order when trying:
public func reorder(from: Int, to: Int) {
let songs = List<Song>()
songs.append(objectsIn: realm.objects(Song.self))
songs.move(from: from, to: to)
try! realm.write {
realm.add(songs)
}
My models are:
class Song: Object {
#Persisted var name: String
#Persisted var key: String
}
class SongList: Object {
let songs = List<Song>()
}
Thanks!
Realm object order is not guaranteed. (unless you specify a sort order)
e.g. if you load 10 songs from Realm, they could come into your app an any order and the order could change between loads. The caveat to that is a Realm List object. Lists always maintain their order.
The problem in the question is you have Song objects stored in Realm but as mentioned above there is no ordering.
So the approach needs to be modified by leveraging a List object for each user to keep track of their songs:
class UserClass: Object {
#Persisted var name: String
#Persisted var songList = List<SongClass>()
}
When adding a song to a user, call it within a write transaction
try! realm.write {
someUser.songList.append(someSong)
}
suppose the user wants to switch the place of song 2 and song 3. Again, within a write transaction:
try! realm.write {
someUser.songList.move(from: 2, to: 3)
}
So then the UI bit - tableViews are backed by a tableView dataSource - this case it would be the songList property. When that dataSource is updated, the tableView should reflect that change.
In this case you would add an observer to the someUser.songList and as the underlying data changes, the observer will react to that change and can then update the UI.
You can do something simple like tableView.reloadData() to reflect the change or if you want fine-grained changes (like row animations for example) you can do that as well. In that same guide, see the code where tableView.deleteRows, .insertRows and .reload is handled. You know what rows were changed in the underlying data there so you can then animate the rows in the tableView.

storing temporary data from user through different controllers before saving them

Best practice for storing temporary data during session? I have an app that allows me to register a customer in a medical service, inserting biometrical and personal data (name, weight, personal measures etch.) once the data are saved, you go to other pages, where you can perform some operation using the data. Only at the very end of this path (after 3-4 controllers), I save the Customer in local database via Realm.
during register phase, I need to store this "TempUser"'s data, with can be accessed from multiple controllers.
I think I could use singletons, but not sure IF and HOW
I fund this but not sure which solution fits better.
my idea:
class AppSession {
static var shared = AppSession()
private init() {}
var currentPatient: Patient?
}
class Patient {
let shared = Patient()
private init() {}
var name: String? = ""
var weight: Double = 0.0
}
and in my textfield delegate:
AppSession.shared.currentPatient?.name = data //text from textfield
otherwise, using Patient as a struct, not a class
It is hard to tell what solution fits for every individual case. It really depends how broadly the data is used in your app. Is it common to every part or just some specific ViewControllers ? How many times it is retrieved and updated ? Also it depends on the architecture pattern you are using and many more factors. So in short, your solution using a singleton pattern may be valid, but can't tell for sure without having a deep look at your project. ​If you decide to stick with your solution, you can omit the AppSession class and directly access your Patient.
class Patient {
static let shared = Patient()
var name: String? = ""
var weight: Double = 0.0
}
And update it:
Patient.shared.name = ""
Even though your solution using a singleton pattern may be valid as mentioned above, you should also consider the user point of view: Do you want the user to be able to continue the flow even after the app was killed ? If yes, than you should save the data after every step(So maybe to KeyChain as what you describe is kind of sensitive data). Or a better solution is to store the data on the server side, which gives the user the flexibility to continue even on an other device(maybe android).
But you can say, that you really do not care about KeyChain neither communicating with a server, and want to do it locally. In this case personally I would pass the data between the controllers, and avoid Singletons. Let's take your Patient object and create a protocol which will require every UIViewController that conforms to it to have a stored property of Patient.
struct Patient {
var name: String? = ""
var weight: Double = 0.0
}
protocol MyOnboardingProtocol: class {
var data: Patient { get set }
}
With the MyOnboardingProtocol you will mark every controller which will require to hold the Patient data. In this way you will force the ViewControllers to hold the Patient object, and will know for the first glance if your ViewController belongs to the "Onboarding" flow.
In your first UIViewController, where your flow begins, you will initialise your Patient object(maybe also make some update on it from user input), and pass along for the next UIViewController:
class MyFirstViewController: UIViewController, MyOnboardingProtocol {
var data: Patient = Patient()
func nextViewController() {
let viewController = MySecondViewController(data: data)
navigationController?.pushViewController(viewController, animated: true)
}
}
For every other UIViewController down the road you will pass the updated object into the init:
class MySecondViewController: UIViewController, MyOnboardingProtocol {
var data: Patient
init(data: Patient) {
self.data = data
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
But as I mentioned before, what I described is just one way of doing it. It will really depend on your individual case and business logic. As I want to keep my answer short, I encourage you to discover the topic a bit more, there has been many great articles about this topic.

Where's the best place to call methods that interact with my database?

I'm creating an app that interacts with a Firestore database. As of now I have a singleton class, DatabaseManager that has all the methods relating to the Firestore database (i.e. get/post methods).
I have a User model called User that has properties such as name, email, photoURL, and some app-specific properties. Any user can edit their profile to update information from a view controller called EditProfileViewController.
Now my question is: is it best to call the DatabaseManager.shared.updateInfo(forUser: user) (where user is a User instance) from EditProfileViewController, User, or some other place?
Sorry if this is an obvious question, but there's going to be a lot of points in the app where I'll need similar logic so I wanted to know what's the best design. Also I'm sure this question has more to with MVC than it does Firebase/Swift.
A couple of thoughts:
Rather than accessing the singleton directly with, DatabaseManager.shared.update(for:), I might instead have a property for the database manager, initialize/inject it with the DatabaseManager.shared, and have whatever needs to interact with the database use that reference, e.g., dataManager.update(for:). The goal would be to allow your unit tests to mock a database manager if and when necessary.
I would not be inclined to have a view controller interact directly with the DatabaseManager. Many of us consider the view controller, which interacts directly with UIKit/AppKit objects, as part of the broader “V” of MVC/MVP/MVVM/whatever. We’d often extricate business logic (including interaction with the database manager) out of the view controller.
I personally wouldn’t bury it under the User object, either. I’d put it in an extension of the database manager, and called from the view model, the presenter, or whatever you personally want to call that object with the business logic.
Is there a reason you're using a singleton to contain all the Firestore logic? User model should contain the method updateInfo.
Here's an example i've used with Firestore:
class Group {
// can read the var anywhere, but an only set value in this class
private(set) var groupName: String!
private(set) var guestsInGroup: Int!
private(set) var joinedGroup: Bool!
private(set) var timeStampGroupCreated: Date!
private(set) var documentId: String!
init(groupName: String, guestsInGroup: Int, joinedGroup: Bool, timeStampGroupCreated: Date, documentId: String) {
self.groupName = groupName
self.guestsInGroup = guestsInGroup
self.joinedGroup = joinedGroup
self.timeStampGroupCreated = timeStampGroupCreated
self.documentId = documentId
}
// method to parse Firestore data to array, that table view will display
class func parseData(snapshot: QuerySnapshot?) -> [Group]{
var groups = [Group]()
guard let snap = snapshot else { return groups }
for document in snap.documents {
let data = document.data()
let groupName = data[GROUP_NAME] as? String ?? "No Group Name"
let guestsInGroup = data[GUESTS_IN_GROUP] as? Int ?? 0
let joinedGroup = data[JOINED_GROUP] as? Bool ?? false
let timeStampGroupCreated = data[TIMESTAMP_GROUP_CREATED] as? Date ?? Date()
let documentId = document.documentID
// add objects with fetched data into thoughts array
let newGroup = Group(groupName: groupName, guestsInGroup: guestsInGroup, joinedGroup: joinedGroup, timeStampGroupCreated: timeStampGroupCreated, documentId: documentId)
groups.append(newGroup)
}
return groups
}
}

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.

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>.