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

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

Related

How to match 2 users if they both pressed 'Heart' button

My app is very similar to Tinder, and only got the "matching" part left for it to be finished. When the user touches the 'Heart' button, a card with their profile is displayed on the Notifications View to the other user, for them to accept it or not.
In case of yes, the will both 'match' as in Tinder. My problem is in how to make that happen.
Person is presented to the User, and the home view displays the people, that UserManager holds.
struct Person: Identifiable, Hashable {
var username: String
var age: Int
}
struct User {
let username: String // These are let because they're being saved on UserManager
let age: Int
}
class UserManager: ObservableObject {
#Published var userInfo: UserInfo?
#Published var people: [Person] = [] // All People
#Published var matches: [Person] = [] // Matched people
Now, here in UserManager, I fetchAllUsers from Firebase and basically init the people's data by the documentSnapshot.
ref.getDocuments { documentsSnapshot, error in
if let error = error { }
documentsSnapshot?.documents.forEach({ snapshot in
let data = snapshot.data()
self.people.append(.init(data: data))
})
}
And finally, in HomeView, the user taps the button and appends the other person in the matches array, which doesn't make sense cause the other person hasn't even accepted them yet.
struct HomeView: View {
var body: some View {
CircleButtonView(type: .heart) {
if let person = userMng.people.last {
userMng.swipe(person, _direction: .like)
userMng.matches.append(person)
// Should change this to another array?
}
}
}
}
Tried saving it to Firebase and then retrieving the data from the users once matched but I can't especify what person the user has liked, for me to make that network call.
How can I append the liked person to another array and then append it to 'matches' once confirmed that they both like each other?
You need to make a new table in firebase with the name of firendRequest, where table child id will be userID and below it, we will have ids of all users who has sent him the friends request. Please have a look the the schema in below image which will make your concept more clear.
The way, you are getting all the user will add up more computation when number of users start increasing and its not recommend to do such large computation on mobile devices. One more thing, Firesbase is NO-SQL database so you need to duplicate your data to avoid computations. Making duplicate date does not mean you are not implementing the thing right :)

Global Realm Object: Singleton or Fetch It Every Time?

In my app I need to have global access to a currentUser which is an instance of a User class defined like this:
class User: Object{
#objc dynamic var recordName = UUID().uuidString
#objc dynamic var name = ""
#objc dynamic var email = ""
#objc dynamic var photo: Data? = nil
override static func primaryKey() -> String? {
return "recordName"
}
}
The currentUser is established when the app launches, and I refer to it frequently almost everywhere in my app.
I've noticed that from time-to-time, I get an error that appears to be caused by referencing this currentUser in different places:
Realm accessed from incorrect thread
I'm able to keep track of which thread Realm is on most of the time, but it's difficult to cover all cases. So this leads me to my question.
Is there a safe way to set the currentUser object once as a singleton? Or should I save their ID to disk and then fetch the object from Realm every time I need it (something like below)?
let realm = try! Realm()
if let currentUserId = defaults.string(forKey: "currentUserId"), let user = realm.object(ofType: User.self, forPrimaryKey: currentUserId){
currentUser = user
}
I am using Swift 4.2 on Xcode 10. Thanks!
As long as you can make sure that you always access your currentUser object from the same thread, it's fine to set it up as a globally accessible object once and use that auto-updating reference to it instead of re-fetching it every time from Realm.
You can achieve this by either creating a dedicated thread to Realm and always dispatching to that thread before accessing Realm/currentUser or simply doing it from a system thread, such as DispatchQueue.main.

Query where list has item/object in Realm

Using Realm in Swift (I am still using the version just before v1.x):
I have this class
class Event: Object {
dynamic var id: String = ""
dynamic var title: String? = nil
dynamic var creator: User?
let members = List<User>()
}
How can I find all events with the member "User A" (I have the id of the User A).
I tried something like this but doesn't really work:
let predicate = NSPredicate(format: "ANY members.id == %#", userA.id)
eventsWithUserA = realm.objects(Event).filter(predicate)
If you're not using the latest version, hopefully you're at least using version 0.100 or higher. If so, you can use Realm's inverse relationships feature to do this:
class User: Object {
let events = LinkingObjects(fromType: Event.self, property: "members")
}
After implementing this, user.events will return a List of every Event object in which the user object is in its members property.
let eventsWithUserA = userA.events
Hopefully this should eliminate the entire need to manually perform a query for what you're trying to achieve here.
Let me know if that doesn't work for you!

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.

User Class Design

I am very new to design patterns, so I am in need of help determining a good way of designing a specific part of an iOS app I am working on.
I have a User object, that represents the user that is logged in. It is very simple, and looks like this:
class User {
var firstName: String
var lastName: String
var photo: String //must be stored as a string for the filename.
var listOfJoinedOpportunities = [Opportunity]()
var listOfJoinedOpportunitiesKeys = [String]()
var listOfFriendsOnTheApp: NSArray
var bio: String
var email: String
var userID: String //A unique ID that is used to persist data about the user to the database (Firebase).
init(firstName: String, lastName: String, photo: String, email:String, userID: String, joinedEvents: [Opportunity],joinedStrings: [String]){
self.firstName = firstName
self.lastName = lastName
self.photo = photo
self.listOfJoinedOpportunities = joinedEvents
self.listOfFriendsOnTheApp = []
self.listOfJoinedOpportunitiesKeys = []
self.bio = ""
self.email = email
self.userID = userID
}
}
Currently I am only in need of an instance that represents the logged in user, but I could foresee needing this class if I add features to interact with other users of the app.
A total of 5 views include interactions that need to read and write to the user object that represents the logged in user.
Currently, I am passing around the object from controller to controller by creating new references. OR, I am creating a new object that represents the same logged-in user from data that was saved to the database (Firebase).
Please help!
Passing the object to the destination view is actually the preferred way to handle this (prepareForSegue). That being said, you can also simply store the User object instance in your AppDelegate and reference it from any view easily if that fits your model.
The drawback to storing references in the App Delegate is clutter and it may be unnecessary in more complex cases since they'll always be allocated (lookup lazy loading as well). Generally you want to pass objects along so the previous views can dealloc which in turn lowers your memory footprint. This way if you receive a memory warning you can handle it appropriately to recover properly.
// Set a var in your appDelegate
var user:User?
// Accessing it from any view
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let loggedInUser = appDelegate.user