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
- Open Xcode and create a new iOS project.
- Choose "App" as the template.
- 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.
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:
- User input for player ID and season
- Error handling and retry mechanisms
- Caching of player data
- More detailed statistics views
Remember to always handle errors gracefully and provide feedback to the user when things go wrong.