Dealing with sports betting live updates on iOS 🏀 🏈 💎

Tales Silveira
Poatek
Published in
6 min readNov 9, 2023

--

[Views + Network] — Intermediate

I’ve worked on a large sports betting application before, and the first thing every iOS developer would notice is the abundance of live updates — network requests happening all the time. In this article, I’ll discuss how to handle multiple requests simultaneously while independently updating numerous views.

Notes

  • I'll use a free API from this site: https://api.the-odds-api.com;
  • Since my main goal is to show how it works AFTER the requests are made, I'll not create a complex Network layer for this;
  • Also, the views are designed just to show what's needed (Beware, designer on the loose!);
  • SwiftUI really helps on this kind of project, because view updates are its strength. UIKit wouldn't be hard, you could use Combine/view states or delegate methods to update your views;
  • You can download the full project here.

In a large or real project, we would have an optimized way to update each game’s odds, perhaps by splitting responsibilities into various view models. There are many ways to achieve this, and that’s what I love about coding :)

Step 1 — Handling the data

You can register on https://api.the-odds-api.com and get your free API Key by email.

In order to understand how the data is structured to make your Codable models, I used https://www.postman.com/.

After doing that, here're the models I've made:

struct Outcome: Codable, Hashable {
var name: String
var price: Double
}

struct Market: Codable, Hashable {
var key: String?
var last_update: Date
var outcomes: [Outcome]
}

struct Bookmakers: Codable {
var key: String?
var title: String?
var last_update: Date?
var markets: [Market]
}

struct Game: Codable, Identifiable {
var id: String?
var sport_key: String?
var sport_title: String
var commence_time: Date
var home_team: String
var away_team: String
var bookmakers: [Bookmakers]
}

I created a simple network layer with a static property for the API and a fetch method to call it in the view model to retrieve the list of games.

struct Network {

// MARK: - Stored Properties
static private var apiKey = "yourKey"
static var api = "https://api.the-odds-api.com/v4/sports/upcoming/odds/?regions=eu&markets=h2h&apiKey=\(apiKey)"
}
// MARK: - Wrapped Properties
@Published var updatedGames: [Game] = []

// MARK: - Public Methods
func fetchData() async {
guard let url = URL(string: Network.api) else { return }

do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decodedGames = try decoder.decode([Game].self, from: data)

DispatchQueue.main.async {
self.updatedGames = decodedGames
}
} catch {
print("Error fetching or decoding data: \(error)")
}
}

We want to continuously call this method to keep our data up-to-date, right?

Remember, the odds are always changing!

I’ve implemented a simple Timer and a method to handle this for us.

// MARK: - Stored Properties
private var timer: Timer?

// MARK: - Initializers
init() {
Task { await fetchData() }
scheduleDataFetch()
}

deinit {
timer?.invalidate()
}

private func scheduleDataFetch() {
timer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { [weak self] _ in
guard let self else { return }
Task {
await self.fetchData()
}
}
}

That's basically all we need for now around the data!

Step 2— Creating your views

Of course, this varies significantly for each project, so I’ll demonstrate how I created and configured mine to detect changes in odd values.

I’ve created an enum to clearly distinguish between the old and new values. Additionally, I’ve added the desired color for each case through a computed property

enum OutcomeViewStatus {

case up
case down
case none

// MARK: - Computed Properties
var color: Color {
switch self {
case .up:
return Color.green
case .down:
return Color.red
case .none:
return Color.gray
}
}
}

In my project, I utilized a border color to indicate to users when an odd has changed. However, you can certainly implement your own system

What’s important to note here is that we’re defining our status stored property within our initializer. Why? In SwiftUI views, whenever a published property changes (in this case, the viewModel’s game list), all linked views are reloaded. That’s why I’m also setting it in the onAppear lines

struct OutcomeView: View {

// MARK: - Wrapped Properties
@State private var borderColor: Color?
@State private var lineWidth: CGFloat = 1

// MARK: - Stored Properties
let outcome: Outcome
let status: OutcomeViewStatus

// MARK: - Actions
var didTapButtonAction: (Outcome) -> Void

// MARK: - Initializers
init(outcome: Outcome,
status: OutcomeViewStatus,
didTapButtonAction: @escaping (Outcome) -> Void) {
self.outcome = outcome
self.status = status
self.didTapButtonAction = didTapButtonAction
}

var body: some View {
Button(action: {
didTapButtonAction(outcome)
}, label: {
VStack {
Text(outcome.name)
.lineLimit(2)
.font(.callout)

Text(String(format: "%.2f", outcome.price))
}
.foregroundStyle(.black)
})
.padding(8)
.frame(maxWidth: .infinity, maxHeight: 80)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(borderColor ?? Color.gray, lineWidth: lineWidth)
)
.onAppear {
borderColor = status.color
lineWidth = 2
withAnimation(.easeOut(duration: 3)) {
borderColor = .gray
lineWidth = 1
}
}
}
}

You would need to download my project to fully understand how I’ve composed the other views. However, if you’re still here, you probably have an idea of how they’re structured

Step 3 — Comparing and updating the odds

This is the stage that allows for various interpretations and different approaches. I opted for a simple solution, but keep in mind that it may not be the most scalable. It’s advisable to schedule a discussion with the nearest tech lead!

In our viewModel, I’ve created two lists of games and a method to update them within the DispatchQueue of the fetchData

// MARK: - Wrapped Properties
@Published var updatedGames: [Game] = []
@Published var oldGames: [Game] = []

// MARK: - Private Methods
private func updateGames(with newGames: [Game]) {
oldGames = updatedGames
updatedGames = newGames
}

Then, I start to pass this data through our views, like this:

//First View
ForEach(viewModel.updatedGames) { game in
let oldGame = viewModel.oldGames.first(where: { $0.id == game.id })
GameView(currentGame: game, oldGame: oldGame)
}

//Second view
//for this example I'm using only the first market
if let currentMarket = currentGame.bookmakers.first?.markets.first {
let oldMarket = oldGame?.bookmakers.first?.markets.first
MarketView(market: currentMarket, oldMarket: oldMarket)
}

//Third and last view
ForEach(market.outcomes, id: \.self) { outcome in
let oldOutcomes = oldMarket?.outcomes.first(where: { $0.name == outcome.name })
OutcomeView(outcome: outcome,
status: compareOutcomes(current: outcome, old: oldOutcomes),
didTapButtonAction: { outcome in
//do what you need with the outcome
})
}

For this final view, I’ve created a method to compare the values and return the status based on their differences

private func compareOutcomes(current: Outcome, old: Outcome? = nil) -> OutcomeViewStatus {
guard let oldPrice = old?.price else {
return .none
}
let difference = current.price - oldPrice
switch difference {
case let x where x > 0:
return .up
case let x where x < 0:
return .down
default:
return .none
}
}

And that's it!

It’s a truly basic project aimed at illustrating how to approach the logic and views when working with sports betting apps. As mentioned earlier, larger projects, such as the one I’ve previously worked on, often involve a different architecture and considerations for various aspects.

Nonetheless, I hope this has been helpful to you in some way :)

P.S

  • If you intend to use real data from the API, it might take a considerable amount of time to see different odd values. Therefore, I recommend increasing the interval time value;
  • However, if you wish to test and observe changes more quickly, I’ve created a mock method for this purpose. You can simply replace the fetchData call in the initializer with this one:
private func mockRequestChanges() {
oldGames = updatedGames
updatedGames.forEach { game in
if let currentGameIndex = updatedGames.firstIndex(where: { $0.id == game.id }) {
if currentGameIndex%2 == 0 {
updatedGames[currentGameIndex].bookmakers[0].markets[0].outcomes[0].price -= 0.1
updatedGames[currentGameIndex].bookmakers[0].markets[0].outcomes[1].price += 0.6
} else {
updatedGames[currentGameIndex].bookmakers[0].markets[0].outcomes[0].price += 0.6
updatedGames[currentGameIndex].bookmakers[0].markets[0].outcomes[1].price -= 0.2
}
}
}
}

--

--