Displaying Football Player Statistics

Mansour Mahamat

Mansour Mahamat

9/19/2024

#tutorial#api
Displaying Football Player Statistics

In this tutorial, we'll create a simple iOS app that fetches and displays player statistics using the API-Football API. This project is suitable for beginners and will cover important concepts such as networking, JSON parsing, and basic UI creation.

Project Setup

  1. Open Xcode and create a new iOS project.
  2. Choose "App" as the template.
  3. Name your project "PlayerStats" and select "SwiftUI" as the interface.

Step 1: Create the Data Models

First, let's create the data models to represent the player and statistics information. Create a new Swift file called Models.swift and add the following code:

import Foundation

struct PlayerResponse: Codable {
    let response: [PlayerData]
}

struct PlayerData: Codable {
    let player: Player
    let statistics: [Statistic]
}

struct Player: Codable {
    let id: Int
    let name: String
    let firstname: String
    let lastname: String
    let age: Int
    let nationality: String
    let height: String
    let weight: String
    let photo: String
}

struct Statistic: Codable {
    let team: Team
    let league: League
    let games: Games
    let goals: Goals
    let passes: Passes
}

struct Team: Codable {
    let name: String
    let logo: String
}

struct League: Codable {
    let name: String
    let country: String
    let logo: String
}

struct Games: Codable {
    let appearances: Int
    let minutes: Int
    let position: String

    enum CodingKeys: String, CodingKey {
        case appearances = "appearences"
        case minutes
        case position
    }
}

struct Goals: Codable {
    let total: Int?
    let assists: Int?
}

struct Passes: Codable {
    let total: Int?
    let key: Int?
}

Step 2: Create the Networking Layer

Now, let's create a service to handle API requests. Create a new Swift file called APIService.swift:

import Foundation

class APIService {
    private let apiKey = "YOUR_API_KEY_HERE"
    private let baseURL = "https://api-football-v1.p.rapidapi.com/v3"

    func fetchPlayerStats(playerId: Int, season: Int, completion: @escaping (Result<PlayerResponse, Error>) -> Void) {
        let urlString = "\(baseURL)/players?id=\(playerId)&season=\(season)"
        guard let url = URL(string: urlString) else {
            completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
            return
        }

        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.addValue(apiKey, forHTTPHeaderField: "x-rapidapi-key")
        request.addValue("api-football-v1.p.rapidapi.com", forHTTPHeaderField: "x-rapidapi-host")

        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data else {
                completion(.failure(NSError(domain: "No data received", code: 0, userInfo: nil)))
                return
            }

            do {
                let decoder = JSONDecoder()
                let playerResponse = try decoder.decode(PlayerResponse.self, from: data)
                completion(.success(playerResponse))
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }
}

Remember to replace "YOUR_API_KEY_HERE" with your actual API key.

Step 3: Create the ViewModel

import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = PlayerViewModel()
    @State private var selectedSeason = 2023
      @State private var randomPlayerId = 276

    var body: some View {
        NavigationView {
            VStack {
                seasonPicker

                if viewModel.isLoading {
                    ProgressView()
                        .scaleEffect(1.5)
                        .padding()
                } else if let playerData = viewModel.playerData {
                    ScrollView {
                        VStack(alignment: .leading, spacing: 20) {
                            PlayerInfoView(player: playerData.player)

                            if let stats = playerData.statistics.first {
                                StatisticsView(statistic: stats)
                            }
                        }
                        .padding()
                    }
                } else if let errorMessage = viewModel.errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                        .padding()
                } else {
                    Text("Tap 'Fetch Random Player' to start")
                        .foregroundColor(.secondary)
                }

                Spacer()

                HStack(spacing: 20) {
                    Button(action: fetchRandomPlayer) {
                        Text("Fetch Random Player")
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                    }

                    Button(action: resetToDefaultPlayer) {
                        Text("Reset to Default")
                            .padding()
                            .background(Color.gray)
                            .foregroundColor(.white)
                            .cornerRadius(10)
                    }
                }
                .padding(.bottom)
            }
            .navigationTitle("Football Player Stats")
            .onAppear {
                resetToDefaultPlayer()
            }
        }
    }

    private var seasonPicker: some View {
        Picker("Season", selection: $selectedSeason) {
            ForEach(2010...2023, id: \.self) { year in
                Text(String(year)).tag(year)
            }
        }
        .pickerStyle(MenuPickerStyle())
        .onChange(of: selectedSeason) { _, newValue in
                   viewModel.fetchPlayerStats(playerId: randomPlayerId, season: newValue)
               }
    }

    private func fetchRandomPlayer() {
        let randomId = Int.random(in: 1...1000)
        viewModel.fetchPlayerStats(playerId: randomId, season: selectedSeason)
    }

    private func resetToDefaultPlayer() {
            randomPlayerId = 276 // Reset to default player ID
            viewModel.fetchPlayerStats(playerId: randomPlayerId, season: selectedSeason)
        }
}

struct PlayerInfoView: View {
    let player: Player

    var body: some View {
        VStack(alignment: .leading, spacing: 15) {
            HStack {
                AsyncImage(url: URL(string: player.photo)) { image in
                    image.resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 120, height: 120)
                        .clipShape(Circle())
                        .overlay(Circle().stroke(Color.blue, lineWidth: 2))
                } placeholder: {
                    ProgressView()
                }

                VStack(alignment: .leading) {
                    Text(player.name)
                        .font(.title2)
                        .fontWeight(.bold)
                    Text("Age: \(player.age)")
                    Text("Nationality: \(player.nationality)")
                }
            }

            Divider()

            HStack {
                InfoItem(title: "Height", value: player.height)
                InfoItem(title: "Weight", value: player.weight)
            }
        }
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(15)
        .shadow(radius: 5)
    }
}

struct InfoItem: View {
    let title: String
    let value: String

    var body: some View {
        VStack {
            Text(title)
                .font(.caption)
                .foregroundColor(.secondary)
            Text(value)
                .font(.body)
                .fontWeight(.semibold)
        }
        .frame(maxWidth: .infinity)
    }
}

struct StatisticsView: View {
    let statistic: Statistic

    var body: some View {
        VStack(alignment: .leading, spacing: 15) {
            Text("Season Statistics")
                .font(.headline)

            HStack {
                StatItem(title: "Team", value: statistic.team.name)
                StatItem(title: "League", value: statistic.league.name)
            }

            HStack {
                StatItem(title: "Position", value: statistic.games.position)
                StatItem(title: "Appearances", value: "\(statistic.games.appearances)")
            }

            HStack {
                StatItem(title: "Minutes", value: "\(statistic.games.minutes)")
                StatItem(title: "Goals", value: "\(statistic.goals.total ?? 0)")
            }

            HStack {
                StatItem(title: "Assists", value: "\(statistic.goals.assists ?? 0)")
                StatItem(title: "Passes", value: "\(statistic.passes.total ?? 0)")
            }

            StatItem(title: "Key passes", value: "\(statistic.passes.key ?? 0)")
        }
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(15)
        .shadow(radius: 5)
    }
}

struct StatItem: View {
    let title: String
    let value: String

    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.caption)
                .foregroundColor(.secondary)
            Text(value)
                .font(.body)
                .fontWeight(.semibold)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Step 5: Run the App

Now you can run the app in the simulator. It will fetch and display statistics for Neymar (player ID 276) for the 2023 season.

Store kit Store kit Store kit Store kit

Conclusion

This tutorial has covered several important concepts :

  • Creating data models using Codable
  • Implementing a basic networking layer with URLSession
  • Using a ViewModel to manage data and state
  • Creating a SwiftUI interface to display data
  • Handling asynchronous operations and updating the UI

To improve this app, you could add:

  1. User input for player ID and season
  2. Error handling and retry mechanisms
  3. Caching of player data
  4. More detailed statistics views

Remember to always handle errors gracefully and provide feedback to the user when things go wrong.