how do I create a leaderboard view , could I use firebase which I’m not familiar with . it would need to be a leaderboard that other user can join using leaderboard name and password- the leaderboard would display users within the leaderboard in order of rank along with their user name because of the achievements made in the app where these ranks are acquired
Hi Stephen,
Yes you could but you have quite a learning curve ahead of you to become familiar with Firebase.
That’s easy enough to do with Firebase Authentication where users can sign in (existing users) or sign up (new users).
You could store and/or update the users rank in a database collection (table) which could store the users username and their current score.
Like I said, you have a steep learning curve ahead of you but that’s pretty normal in iOS Development.
Or the best way to do this is integrating with Game Center
So you recommend firebase, I’m efficient at the front end concepts and I am now finalising the bank end for an app that needs a leaderboard data storage, username, password, email address etc, I have been working on for the past year. I have heard people recommend Mongodb, realm, and others is Firebase my best option
Really why would you recommend this over firebase out of curiosity ?
Firebase is one that I have used a lot so it’s very familiar to me. MongoDb and Realm are two that I am not familiar with. It’s not to say that either MongDb and Realm are any less of a solution but you would have to come up with your own authentication process in each of those two whereas Firebase already has that in place and it’s easy to integrate… well easy when you understand how to set it up.
Your choice.
It’s literally what it’s for, GameCenter is for a leaderboard and tracking achievements
It’s first party by Apple, no 3rd party authentication needed, users just use their Apple ID
It really depends on what you want, any solution technically works.
Thank you Chris I have one more question if you could answer it. Ive looked at videos with Chris on cocoapods as a dependency package to centralise or manage firebase however, he has videos on YouTube now saying from a 1year ago might I add on Swift package manager. Im using Xcode 14.3 on a MacBook Air and I cant download/install cocoa pods in my terminal . Im wondering if you could give me a quick explanation. Do I use Swift package manager instead or what do you recommend I don’t want to link my app bundle to project in firebase until I know what to do on that field.
Yes, installing cocoapods is difficult these days on an M1 or M2 chipped Mac. But in any case the preferred method of installing 3rd party frameworks is using Swift Package Manager.
Chris Ching published a video on YouTube on how to do that:
This one is related to installing Firebase so it’s perfect to get you on the way.
and its safe to go ahead with that process as your starting point as in just get stuck in and learn as you go or experiment ? im trying to do this all fast without errors
Don’t be afraid of making a mistake. That’s all part of the learning process. Using SPM is so much easier than using cocoapods anyway so it’s a no brainer.
I’m in a position now where my code for the user is done however to create a leaderboard im having a lot of problems in creating one that works if @Chris_Parker or @Chris_Ching could give a quick look at it I would be very grateful:import SwiftUI
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
// About the app, This will be a leaderboard view that users can create a leaderboard with name and password joing once authenticated and once the leaderboard name and password was correct, When i press create a leadernoard it states that the document is missing . Try to fix this but it wont upload a leaderbaord to that users dtabase on firestore. Can you see the issues?
// I know the code is messy but i wanted it in once file . Im also using a users collection and basically that holds the following values and stores them perfectly struct DBUser: Codable { let userId :String
//let Reading: Int
// let Score: Int etc and basically the score and rank value in DBLeaderboards must be the same as these values
struct DBLeaderboard: Codable, Identifiable {
var id: String? // Firestore document ID
let userId: String
var Leaderboard: [DBLeaderboards]
init(auth: AuthDataResultModel) {
self.userId = auth.uid
self.Leaderboard = []
}
init(userId: String, Leaderboard: [DBLeaderboards]) {
self.userId = userId
self.Leaderboard = Leaderboard
}
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case Leaderboard = "leader_board"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.userId = try container.decode(String.self, forKey: .userId)
self.Leaderboard = try container.decode([DBLeaderboards].self, forKey: .Leaderboard)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.userId, forKey: .userId)
try container.encode(self.Leaderboard, forKey: .Leaderboard)
}
}
struct DBLeaderboards: Codable {
let Rank: Int
let Score: Int
let Password: String
let Leaderboard: String
}
final class LeaderBoardManager {
static let shared = LeaderBoardManager()
private init() {}
private let firestore = Firestore.firestore()
// Firestore collections
private let leaderboardsCollection = Firestore.firestore().collection("leaderboards")
private let usersCollection = Firestore.firestore().collection("users")
private func userDocument(userId: String) -> DocumentReference? {
// Check if userId is not empty
guard !userId.isEmpty else {
// Handle the case where userId is empty (e.g., return nil or throw an error)
return nil
}
// Construct and return the document reference
return usersCollection.document(userId)
}
private func createUserDocument(userId: String) throws -> DocumentReference {
guard !userId.isEmpty else {
throw NSError(domain: "YourAppDomain", code: 1, userInfo: [
NSLocalizedDescriptionKey: "User ID is empty."
])
}
return usersCollection.document(userId)
}
func createNewLeaderboard(user: DBLeaderboard) async throws {
do {
let userDocumentRef = try createUserDocument(userId: user.userId)
try await userDocumentRef.setData(from: user, merge: false)
} catch {
throw error
}
}
func getUserLeaderboards(userId: String) async throws -> DBLeaderboard? {
do {
let userDocumentRef = try createUserDocument(userId: userId)
let document = try await userDocumentRef.getDocument(as: DBLeaderboard.self)
return document
} catch {
throw error
}
}
func deleteLeaderboard(leaderboardId: String, completion: @escaping (Error?) -> Void) {
// Construct the reference to the leaderboard document
let leaderboardRef = leaderboardsCollection.document(leaderboardId)
// Delete the leaderboard document
leaderboardRef.delete { error in
if let error = error {
// Handle errors
completion(error)
} else {
// Deletion successful
completion(nil)
}
}
}
func joinLeaderboard(
leaderboardId: String,
userId: String,
password: String,
completion: @escaping (Error?) -> Void
) {
// Construct the reference to the leaderboard document
let leaderboardRef = leaderboardsCollection.document(leaderboardId)
// Fetch the leaderboard data
leaderboardRef.getDocument { document, error in
if let error = error {
// Handle errors when fetching the leaderboard document
completion(error)
return
}
guard let document = document, document.exists else {
// Leaderboard not found
let error = NSError(
domain: "YourAppDomain",
code: 2,
userInfo: [NSLocalizedDescriptionKey: "Leaderboard not found"]
)
completion(error)
return
}
do {
// Decode the leaderboard data
let leaderboard = try document.data(as: DBLeaderboard.self)
// Check if the provided password matches
if leaderboard.Leaderboard.first?.Password == password {
// Add the user to the leaderboard
// You can implement this part as needed for your use case
// For example, updating the leaderboard with the user's information
// or storing the joined leaderboard in the user's profile.
// Call a completion handler with nil to indicate success
completion(nil)
} else {
// Incorrect password
let error = NSError(
domain: "YourAppDomain",
code: 3,
userInfo: [NSLocalizedDescriptionKey: "Incorrect password"]
)
completion(error)
}
} catch {
// Handle decoding errors
completion(error)
}
}
}
}
@MainActor
final class LeaderBViewModel: ObservableObject {
@Published private(set) var user: DBLeaderboard? = nil
@Published private(set) var leaderboards: [DBLeaderboard] = []
func loadCurrentUser() async throws {
let authDataResult = try AuthenticationManager.shared.getAuthenticatedUser()
self.user = try await LeaderBoardManager.shared.getUserLeaderboards(userId: authDataResult.uid)
}
func loadLeaderboards() async {
do {
let authDataResult = try AuthenticationManager.shared.getAuthenticatedUser()
let leaderboards: DBLeaderboard = try await LeaderBoardManager.shared.getUserLeaderboards(userId: authDataResult.uid) ?? DBLeaderboard(userId: "", Leaderboard: [])
// Update the leaderboards property
self.leaderboards = [leaderboards] // Convert the single leaderboard to an array
} catch {
// Handle the error
print("Error loading leaderboards: \(error.localizedDescription)")
// Assign an empty array to leaderboards when an error occurs
self.leaderboards = [DBLeaderboard]()
}
}
func joinLeaderboard(leaderboardId: String, password: String, completion: @escaping (Error?) -> Void) async {
do {
// Check if user and leaderboards are available
guard let userId = user?.userId, !leaderboards.isEmpty else {
let error = NSError(
domain: "YourAppDomain",
code: 1,
userInfo: [
NSLocalizedDescriptionKey: "Invalid user or leaderboards."
]
)
completion(error)
return
}
// Call the joinLeaderboard function in LeaderBoardManager
LeaderBoardManager.shared.joinLeaderboard(
leaderboardId: leaderboardId,
userId: userId,
password: password
) { error in
if let error = error {
// Handle the error
print("Error joining leaderboard: \(error.localizedDescription)")
completion(error)
} else {
// Handle successful join (e.g., update UI or navigate to a new screen)
completion(nil)
}
}
} catch {
// Handle join errors (e.g., show an error message)
print("Error joining leaderboard: \(error.localizedDescription)")
completion(error)
}
}
}
struct LeaderboardView: View {
@StateObject private var viewPro = LeaderBViewModel()
@State private var isCreateLeaderboardViewPresented = false
@State private var isJoinLeaderboardViewPresented = false
@State private var isDeleteAlertPresented = false
// Add a @State variable to store leaderboards
@State private var leaderboards: [DBLeaderboard] = []
@State private(set) var selectedLeaderboard: DBLeaderboard? = nil
var body: some View {
NavigationView {
List {
ForEach(leaderboards, id: \.id) { leaderboard in
NavigationLink(destination: LeaderboardDetailView(leaderboard: leaderboard)) {
HStack {
Text(leaderboard.userId)
Text("\(leaderboard.Leaderboard.first?.Score ?? 0)") // Ensure you're using a non-optional value here
Text("\(leaderboard.Leaderboard.first?.Rank ?? 0)")
}
}
}
.onDelete { indexSet in
if let index = indexSet.first, let leaderboardToDelete = leaderboards[index].id {
LeaderBoardManager.shared.deleteLeaderboard(leaderboardId: leaderboardToDelete) { error in
if let error = error {
// Handle errors
print("Error deleting leaderboard: \(error.localizedDescription)")
} else {
// Deletion successful
leaderboards.remove(at: index)
}
}
}
}
}
.navigationBarTitle("Leaderboards")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
isCreateLeaderboardViewPresented = true
}) {
Text("Create").font(.headline).foregroundColor(.blue)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Spacer()
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isJoinLeaderboardViewPresented = true
}) {
Image(systemName: "person.crop.circle.badge.plus")
}
}
}
}
.sheet(isPresented: $isCreateLeaderboardViewPresented) {
NavigationView {
CreateLeaderboardView(leaderboards: $leaderboards)
}
}
.sheet(isPresented: $isJoinLeaderboardViewPresented) {
NavigationView {
JoinLeaderboardView(leaderboards: $leaderboards)
}
}
.alert(isPresented: $isDeleteAlertPresented) {
Alert(
title: Text("Delete Leaderboard"),
message: Text("Are you sure you want to delete this leaderboard?"),
primaryButton: .destructive(Text("Delete")) {
if let leaderboardToDelete = selectedLeaderboard?.id,
let index = leaderboards.firstIndex(where: { $0.id == leaderboardToDelete }) {
LeaderBoardManager.shared.deleteLeaderboard(leaderboardId: leaderboardToDelete) { error in
if let error = error {
// Handle errors
print("Error deleting leaderboard: \(error.localizedDescription)")
} else {
// Deletion successful
leaderboards.remove(at: index)
}
}
}
selectedLeaderboard = nil
},
secondaryButton: .cancel()
)
}.onAppear {
Task {
do {
await viewPro.loadLeaderboards()
} catch {
// Handle the error
print("Error loading leaderboards: \(error.localizedDescription)")
}
}
}
}
}
struct CreateLeaderboardView: View {
@Binding var leaderboards: [DBLeaderboard]
@StateObject private var viewPro = LeaderBViewModel()
@State private var isCreateLeaderboardViewPresented = false
@State private var isJoinLeaderboardViewPresented = false
@State private(set) var selectedLeaderboard: DBLeaderboard? = nil
@State private var isDeleteAlertPresented = false
@State private var newLeaderboardName = ""
@State private var newPassword = ""
@Environment(\.presentationMode) var presentationMode
@State private var isPasswordValid = false
@State private var isPasswordVisible = false // Added state to toggle password visibility
var body: some View {
Form {
Section(header: Text("Create Leaderboard")) {
TextField("Enter leaderboard name", text: $newLeaderboardName)
HStack {
if isPasswordVisible {
TextField("Enter password", text: $newPassword)
} else {
SecureField("Enter password", text: $newPassword)
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(.secondary)
}
}
.onChange(of: newPassword) { newValue in
isPasswordValid = newPassword.count >= 6 && newPassword.containsNumber && newPassword.containsUppercase
}
if !isPasswordValid {
Text("Password must be at least 6 characters long and contain at least one digit and one uppercase letter.")
.foregroundColor(.red)
}
Button(action: {
if isPasswordValid {
// Create a new leaderboard
let newLeaderboard = DBLeaderboard(userId: viewPro.user?.userId ?? "", Leaderboard: [])
// Add the new leaderboard to Firestore using a Task
Task {
do {
try await LeaderBoardManager.shared.createNewLeaderboard(user: newLeaderboard)
// Add the user to the leaderboard
do {
LeaderBoardManager.shared.joinLeaderboard(
leaderboardId: newLeaderboard.id ?? "",
userId: viewPro.user?.userId ?? "",
password: newPassword
) { error in
if let error = error {
// Handle errors (e.g., show an alert)
print("Error joining leaderboard: \(error.localizedDescription)")
} else {
// Dismiss the sheet
presentationMode.wrappedValue.dismiss()
}
}
} catch {
// Handle errors
print("Error joining leaderboard: \(error.localizedDescription)")
}
} catch {
// Handle errors
print("Error creating leaderboard: \(error.localizedDescription)")
}
}
}
}) {
Text("Create")
}
.disabled(!isPasswordValid)
}
}
.navigationBarTitle("Create Leaderboard")
}
}
struct JoinLeaderboardView: View {
@StateObject private var viewPro = LeaderBViewModel()
// @State private var leaderboards: [DBLeaderboard] =
@State private var isCreateLeaderboardViewPresented = false
@State private var isJoinLeaderboardViewPresented = false
@Binding var leaderboards: [DBLeaderboard]
@State private(set) var selectedLeaderboard: DBLeaderboard? = nil
@State private var isDeleteAlertPresented = false
@State private var newLeaderboardName = ""
@State private var newPassword = ""
@State private var inputPassword = ""
@Environment(\.presentationMode) var presentationMode
@State private var isPasswordValid = false
@State private var isPasswordVisible = false // Added state to toggle password visibility
var body: some View {
Form {
Section(header: Text("Join Leaderboard")) {
TextField("Enter leaderboard name", text: $newLeaderboardName)
HStack {
if isPasswordVisible {
TextField("Enter password", text: $newPassword)
} else {
SecureField("Enter password", text: $newPassword)
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(.secondary)
}
}
.onChange(of: newPassword) { newValue in
isPasswordValid = newPassword.count >= 6 && newPassword.containsNumber && newPassword.containsUppercase
}
if !isPasswordValid {
Text("Password must be at least 6 characters long and contain at least one digit and one uppercase letter.")
.foregroundColor(.red)
}
Button(action: {
if isPasswordValid {
// Create a new leaderboard
let newLeaderboard = DBLeaderboard(userId: viewPro.user?.userId ?? "", Leaderboard: [])
// Add the new leaderboard to Firestore using a Task
Task {
// Add the user to the leaderboard
do {
LeaderBoardManager.shared.joinLeaderboard(
leaderboardId: newLeaderboard.id ?? "",
userId: viewPro.user?.userId ?? "",
password: newPassword
) { error in
if let error = error {
// Handle errors (e.g., show an alert)
print("Error joining leaderboard: \(error.localizedDescription)")
} else {
// Dismiss the sheet
presentationMode.wrappedValue.dismiss()
}
}
} catch {
// Handle errors
print("Error creating leaderboard: \(error.localizedDescription)")
}
}
}
}) {
Text("Join")
}
.disabled(!isPasswordValid)
} }
.navigationBarTitle("Join Leaderboard")
}
}
extension String {
var containsNumber: Bool {
return rangeOfCharacter(from: .decimalDigits, options: .numeric, range: nil) != nil
}
var containsUppercase: Bool {
return rangeOfCharacter(from: .uppercaseLetters, options: .numeric, range: nil) != nil
}
}
struct LeaderboardDetailView: View {
let leaderboard: DBLeaderboard
var body: some View {
Text("Leaderboard Detail View for \(leaderboard.userId)")
.navigationBarTitle(leaderboard.userId)
}
}
Hi Stephen,
Can you share your project with us rather than one of us having to create a project and add all those files to it. There is a lot of work in that process and it would be easier if you shared your entire project. It’s pretty easy to do that.
To compress the project, locate the project folder in Finder and then right click on the top level folder and select Compress ProjectName. The top level folder is the one that contains the file named ProjectName.xcodeproj and the folder named ProjectName.
Upload that zip file to either Dropbox or Google Drive and then create a share link then copy that link and paste that in a reply to this thread.
If you choose to use Google Drive then when you create the Share link, ensure that the “General Access” option is set to “Anyone with the Link” rather than “Restricted”.
Thanks Chris I don’t feel comfortable to upload the whole project but basically here is a demo of what im trying to fix, the errors are appearing coz I just made this project so you can view it here is the link :https://drive.google.com/file/d/1HWg9E48aV8KkaOqrH82b5_WWf9lL_3i4/view?usp=share_link
There’s a description in LeaderboardView of what is wrong if its possible for you to fix it
or point me in the right direction
On Google Drive, can you modify the “General Access” option for that file so that it is set to “Anyone with the Link” rather than “Restricted”.
Currently access to that sample project is denied as you have it set to restricted.
Hi Stephen,
Did you get that demo project you sent me to run?
I tried to compile the project you sent me and these are the errors I have got so far.
let me try now again