What is the best practice to retrieve Facebook user picture with Firebase in Swift? - swift

I have the following method in a ViewModel to handle user clicks on a Facebook login button:
import Foundation
import FirebaseAuth
import FacebookLogin
/// A view model that handles login and logout operations.
class SessionStore: ObservableObject {
#Published var user: User?
#Published var isAnon = false
private var handle: AuthStateDidChangeListenerHandle?
private let authRef = Auth.auth()
/// A login manager for Facebook.
private let loginManager = LoginManager()
/// Listens to state changes made by Firebase operations.
func listen() {
handle = authRef.addStateDidChangeListener {[self] (auth, user) in
if user != nil {
self.isAnon = false
self.user = User(id: user!.uid, fbId: authRef.currentUser!.providerData[0].uid, name: user!.displayName!, email: user!.email!, profilePicURL: user!.photoURL)
} else {
self.isAnon = true
self.user = nil
}
}
}
/// Logs the user in using `loginManager`.
///
/// If successful, get the Facebook credential and sign in using `FirebaseAuth`.
///
/// - SeeAlso: `loginManager`.
func facebookLogin() {
loginManager.logIn(permissions: [.publicProfile, .email], viewController: nil) { [self] loginResult in
switch loginResult {
case .failed(let error):
print(error)
case .cancelled:
print("User cancelled login.")
case .success:
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
authRef.signIn(with: credential) { (authResult, error) in
if let error = error {
print("Facebook auth with Firebase error: \(error)")
return
}
}
}
}
}
}
In listen(), I'm trying to build my User model whenever Firebase detects state change (i.e., when a user is logged in). My User model is a simple struct like this:
/// A model for the current user.
struct User: Identifiable, Codable {
var id: String
var fbId: String
var name: String
var email: String
var profilePicURL: URL?
}
The Problem
Right now, as you can see in my listen() method, I'm using Firebase's photoURL to get user's profile picture. However, it will only give me a low-quality thumbnail pic.
I would love to fetch normal photos from Facebook.
What I have tried
I tried, in my facebookLogin() to call GraphRequest to get the picture url. However, since my function is synchronous, I couldn't store the result to my User model.
I also tried directly using the Graph API link like "http://graph.facebook.com/user_id/picture?type=normal", but it seems like it's no longer the safe/suggested practice.
Question
Given my ViewModel structure, what is the best way to fetch and store Facebook user picture URL to my User model?

I found out that Firebase's photoURL is Facebook's Graph API URL: http://graph.facebook.com/user_id/picture. So, to get other sizes, all I need to do is to append a query string like ?type=normal to the photoURL.
To use GraphRequest, consider #jnpdx's suggestion:
Seems like you have at least a couple of options: 1) Use the
GraphRequest and don't set your User model until you get the result
back. 2) Set your User model as you are now and then update it with a
new URL once your GraphRequest comes back.

Related

Getting query of documents that another document is referencing

I'm a beginner to Swift and Firebase and I'm creating an app where users can create events and attend events that other users create so I have 2 collections in my Firebase database (users and posts as seen in the image)
Image
When a user joins an event the reference to that event is added to the list "eventsAttending" indicating that they are attending those events. Now I want to fetch the events / posts that are in this eventsAttending array and return them however I'm not quite sure how to do so. To fetch all posts of the same author I did the following
func fetchCurrentUsersPosts() async throws -> [Post] {
return try await fetchPosts(from: postsReference.whereField("author.id", isEqualTo: user.id))
}
Where fetchPosts(from:) is:
func fetchPosts(from query: Query) async throws -> [Post] {
let posts = try await query.getDocuments(as: Post.self)
return posts
}
And getDocuments(as:) is:
extension Query {
func getDocuments<T: Decodable>(as type: T.Type) async throws -> [T] {
let snapshot = try await getDocuments()
return snapshot.documents.compactMap { document in
try! document.data(as: type)
}
}
User model:
struct User: Identifiable, Codable, Equatable {
// unique id of user
var id: String
// name of user
var name: String
// email of user
var email: String
// birthday of user
var birthday: Date
// phone number of user
var phoneNumber: String
// posts that the user has made
var usersPosts: [Post]
// events that the user is attending
var eventsAttending: [Post]
}
So I want a way to fetch the posts stored in the eventsAttending array using these 2 functions if possible.

How to globally store a User Class instance in Swift

I have been coding in swift for a short time now and wish to create my first, properly complete application. My application starts with a UITabController (after the logging in part which I have implemented) will come with a "profile" page, where the user can update information about themselves (username etc).
I have therefore created a User class which holds this information and will in the future, communicate with a server to update the users information.
I only want one User class object to be instantiated throughout the application (yet still accessible everywhere) as only one user can be logged in on the phone, what is considered the best practice to do so? It may also be worth noting that the log in section will remember a user is logged in so they won't have to re-log in (using user defaults Boolean for isLoggedIn)
I was thinking about making the User class as a singleton, or somehow making the class instance global (although I am pretty sure making it global isn't great).
Or is there a way to make the instance accessible for every view controller placed in a UITabController class if I create the User class in the tab controller class? What do you recommend?
Thanks all!
This is how I use a single object for user data that both is available in any view controller I want but also allows for the saving of data. This utilizes Realm for swift for saving data. To call the data you just create the variable let user = User.getCurrentUser()
class User: Object, Decodable {
#objc dynamic var firstName: String? = ""
#objc dynamic var lastName: String? = ""
#objc dynamic var email: String = ""
#objc dynamic var signedIn: Bool = false
override static func primaryKey() -> String? {
return "email"
}
private enum CodingKeys: String, CodingKey {
case email
case firstName
case lastName
}
}
extension User {
func updateUser(block: (User) -> ()) {
let realm = UserRealm.create()
try? realm.write {
block(self)
}
static func getCurrentUser() -> User? {
let realm = UserRealm.create()
return realm.objects(User.self).filter("signedIn == %#", true).first
}
}
fileprivate let currentSchema: UInt64 = 104
struct UserRealm {
static func create() -> Realm {
do {
return try Realm(configuration: config)
} catch {
print(error)
fatalError("Creating User Realm Failed")
}
}
static var config: Realm.Configuration {
let url = Realm.Configuration().fileURL!.deletingLastPathComponent().appendingPathComponent("Users.realm")
return Realm.Configuration(fileURL: url,
schemaVersion: currentSchema, migrationBlock: { migration, oldSchema in
print("Old Schema version =", oldSchema)
print("Current schema version =", currentSchema)
print("")
if oldSchema < currentSchema {
}
}, shouldCompactOnLaunch: { (totalBytes, usedBytes) -> Bool in
let oneHundredMB = 100 * 1024 * 1024
return (totalBytes > oneHundredMB) && (Double(usedBytes) / Double(totalBytes)) < 0.5
})
}
}

Vapor 4/Swift: Middleware development

I'm completely new to Swift and Vapor;
I'm trying to develop an Authorization Middleware that is called from my UserModel (Fluent Model object) and is past in a Required Access Level (int) and Path (string) and
Ensures the user is authenticated,
Checks if the instance of the UserModel.Accesslevel is => the Passed in Required Access level and if not redirects the user to the 'Path'.
I have tried for a couple of days and always get close but never quite get what I'm after.
I have the following (please excuse the poorly named objects):
import Vapor
public protocol Authorizable: Authenticatable {
associatedtype ProfileAccess: UserAuthorizable
}
/// status cached using `UserAuthorizable'
public protocol UserAuthorizable: Authorizable {
/// Session identifier type.
associatedtype AccessLevel: LosslessStringConvertible
/// Identifier identifier.
var accessLevel: AccessLevel { get }
}
extension Authorizable {
/// Basic middleware to redirect unauthenticated requests to the supplied path
///
/// - parameters:
/// - path: The path to redirect to if the request is not authenticated
public static func authorizeMiddleware(levelrequired: Int, path: String) -> Middleware {
return AuthorizeUserMiddleware<Self>(Self.self, levelrequired: levelrequired, path: path)
}
}
// Helper for creating authorization middleware.
///
//private final class AuthRedirectMiddleware<A>: Middleware
//where A: Authenticatable
final class AuthorizeUserMiddleware<A>: Middleware where A: Authorizable {
let levelrequired: Int
let path: String
let authLevel : Int
init(_ authorizableType: A.Type = A.self, levelrequired: Int, path: String) {
self.levelrequired = levelrequired
self.path = path
}
/// See Middleware.respond
public func respond(to req: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
if req.auth.has(A.self) {
print("--------")
print(path)
print(levelrequired)
**// print(A.ProfileAccess.AccessLevel) <- list line fails because the A.ProfileAccess.AccessLevel is a type not value**
print("--------")
}
return next.respond(to: req)
}
}
I added the following to my usermodel
extension UserModel: Authorizable
{
typealias ProfileAccess = UserModel
}
extension UserModel: UserAuthorizable {
typealias AccessLevel = Int
var accessLevel: AccessLevel { self.userprofile! }
}
And Route as such
// setup the authentication process
let session = app.routes.grouped([
UserModelSessionAuthenticator(),
UserModelCredentialsAuthenticator(),
UserModel.authorizeMiddleware(levelrequired: 255, path: "/login"), // this will redirect the user when unauthenticted
UserModel.authRedirectMiddleware(path: "/login"), // this will redirect the user when unauthenticted
])
The Path and required level get passed in correctly but I cannot get the instance of the AccessLevel from the current user. (I'm sure I have my c++ hat on and from what I can 'Guess' the UserModel is not actually a populated instance of a user even though the Authentication has already been completed)
I attempted to merge in the 'SessionAuthenticator' process of using a associatedtype to pass across the account info.
My other thought was to check if the user is authenticated and if so I can safely assume the Session Cookie contains my User Id and therefore I could pull the user from the DB (again) and check the users access level from there.
I could be way off here, after a couple of days I don't know which way is the best approach, any guidance would be greatly appreciated.
Cheers
I use the Vapor 4 approach of conforming my User model to ModelAuthenticatable:
extension User:ModelAuthenticatable
{
static let usernameKey = \User.$email
static let passwordHashKey = \User.$password
func verify(password: String) throws -> Bool
{
try Bcrypt.verify(password, created:self.password)
}
}
At the point where I have checked that the user exists and the password is verified as above, then it logs the user in:
request.auth.login(user)
Once logged in, I use a custom Middleware that checks to see if the user is logged in and a 'superuser', if so then the response is passed to the next Middleware in the chain, otherwise it re-directs to the home/login page if not.
struct SuperUserMiddleware:Middleware
{
func respond(to request:Request, chainingTo next:Responder) -> EventLoopFuture<Response>
{
do
{
let user = try request.auth.require(User.self)
if user.superUser { return next.respond(to:request) }
}
catch {}
let redirect = request.redirect(to:"UNPRIVILEGED")
return request.eventLoop.makeSucceededFuture(redirect)
}
}
I register the SuperUserMiddleware and use with certain groups of routes in configure.swift as:
app.middleware.use(SessionsMiddleware(session:MemorySessions(storage:MemorySessions.Storage())))
app.middleware.use(User.sessionAuthenticator(.mysql))
let superUserMW = SuperUserMiddleware()
let userAuthSessionsMW = User.authenticator()
let events = app.grouped("/events").grouped(userAuthSessionsMW, superUserMW)
try EventRoutes(events)

Firebase Signup - Storing additional data in DB blocks the listener

On Firebase signup, I store additional data in a Realtime database. The problem is that somehow the AuthListener stops working then. Which means users have to restart the app.
This is my code
session.signUp(email: email, password: password) { (authData, error) in
session.createUser()
}
.createUser adds an object to my realtime database. That part works well.
This part below create the problems
func listen() {
_ = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.ref = Database.database().reference(withPath: "\(String(describing: Auth.auth().currentUser?.uid ?? "Error"))")
self.ref.child("user").observe(DataEventType.value) { (snapshot) in
self.user = MyModel(snapshot: snapshot)
}
It looks like self.user doesn't get refreshed and my UI is dependent on this variable.
Thanks so much!

Updating a codable user struct

I'm developing an app where the user can create up to 5 profiles when I came across a problem.
Problem:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file
Info:
The first edition of the app has a struct with these data points in it:
id
profileName
profileIcon
When the app opens it loads the user via func loadUser()
Now, during an update I've added a new data point in the user struct so it now looks like this:
id
profileName
profileIcon
profileSummary
Now when the func loadUser() is called it fails with this statement:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file
How the system is built:
When the app is opened the first time it creates 5 empty profiles. The user can then "activate" and fill out these profiles and he/she likes.
I'm a little uncertain how to deal with this problem. How can I add new data points to my struct without causing the app to crash?
Source code:
struct User: Codable {
// Core user data
let id: Int
var profileName: String
var profileIcon: String
var profileSummary: String
}
class DataManager: NSObject {
/// Used to encode and save user to UserDefaults
func saveUser(_ user: User) {
if let encoded = try? JSONEncoder().encode(user) {
UserDefaults.standard.set(encoded, forKey: "userProfile_\(user.id)")
print("Saved user (ID: \(user.id)) successfully.")
}
}
/// Used to decode and load user from UserDefaults
func loadUser(_ userID: Int) -> User {
var user : User?
if let userData = UserDefaults.standard.data(forKey: "userProfile_\(userID)"),
let userFile = try? JSONDecoder().decode(User.self, from: userData) {
user = userFile
print("Loaded user (ID: \(user!.id)) successfully.")
}
return user!
}
/// Used to create x empty userprofiles ready to be used
func createEmptyProfiles() {
// May be changed, but remember to adjust UI
var profilesAllowed = 5
while profilesAllowed != 0 {
print("Attempting to create empty profile for user with ID \(profilesAllowed)")
let user = User(id: profilesAllowed, profileName: "", profileIcon: "", profileSummary: "Write a bit about your profile here..")
self.saveUser(user)
print("User with ID \(profilesAllowed) was created and saved successfully")
// Substract one
profilesAllowed -= 1
}
}
//MARK: - Delete profile
func deleteUser(user: User) -> Bool {
var userHasBeenDeleted = false
var userToDelete = user
// Reset all values
userToDelete.profileName = ""
userToDelete.profileIcon = ""
userToDelete.profileSummary = ""
// Save the resetted user
if let encoded = try? JSONEncoder().encode(userToDelete) {
UserDefaults.standard.set(encoded, forKey: "userProfile_\(user.id)")
print("User has now been deleted")
userHasBeenDeleted = true
}
return userHasBeenDeleted
}
}
Two options:
Make profileSummary optional
struct User: Codable {
// Core user data
let id: Int
var profileName: String
var profileIcon: String
var profileSummary: String?
}
If the key doesn't exist it will be ignored.
Implement init(from decoder) and decode profileSummary with decodeIfPresent assigning an empty string if it isn't.
Side note:
Never try? in a Codable context. Catch the error and handle it. Your loadUser method crashes reliably if an error occurs. A safe way is to make the method throw and hand over errors to the caller.
enum ProfileError : Error { case profileNotAvailable }
/// Used to decode and load user from UserDefaults
func loadUser(_ userID: Int) throws -> User {
guard let userData = UserDefaults.standard.data(forKey: "userProfile_\(userID)") else { throw ProfileError.profileNotAvailable }
return try JSONDecoder().decode(User.self, from: userData)
}