How do I create a leaderboard within app

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.

1 Like

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.

done @Chris_Parker apologies

@stephenb711

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

There it is now @Chris_Parker sorry ,Demo Leaderboard app 4.zip - Google Drive