Accessible iOS design: how Forza Football included blind users

In my early teens, when my sight was slowly entering the low-vision territory, I loved football (soccer, if you’re American). These days, despite being blind, I still follow scores and a few leagues. My favorite iOS app for keeping up with football is Forza Football, thanks to its outstanding support for VoiceOver, Apple’s built-in screen reader for blind and low-vision users.

Forza Football line up of Real Madrid against FC Barcelona.

An unexpected accessibility champion

If you haven’t seen it, Forza Football doesn’t just show match scores and league tables — it also presents lineups in a clean view that’s fully accessible to VoiceOver users.

In this video you can hear VoiceOver navigating Forza’s lineup view — first Real Madrid’s starting lineup from goalkeeper to forward, then Barcelona’s, from goalkeeper to forward in reverse vertical order.

Team lineups are one of the most visual parts of any football app, but Forza has made them incredibly accessible to VoiceOver users like me. It’s so well done that I, as an iOS engineer, just had to replicate their navigation logic to understand how they did it!

How VoiceOver Reads a Football Pitch

As you may know, in left-to-right languages, VoiceOver typically navigates from bottom to top, and from left to right. In football commentary, lineups are usually described from defense to attack (goalkeeper to forwards) and from right to left.

Forza represents the pitch vertically — one goal at the top and one at the bottom. Since lineups show pre-match positions, the topmost player represents the home team goalkeeper, and the bottommost player is the away team goalkeeper, with both teams’ forwards being on either side of the center of the pitch.

For the home team, VoiceOver’s default navigation naturally follows football’s logic: it reads the goalkeeper first, then moves across defenders, midfielders, and forwards — from left to right on screen (which is right to left from the team’s perspective).

However, for the away team, things get trickier. The goalkeeper is at the bottom and the forwards at the top. VoiceOver, by default, reads in the opposite order to what a football fan would expect — forwards first, goalkeeper last. A user could adapt by finding the away goalkeeper and then swipe left, but that would be poor usability. Thankfully, Forza’s developers went a step further.

In this video, you can hear VoiceOver navigating my app’s initial lineup page — reading the home team correctly, but the away team from forwards to goalkeeper.

The Trick: Accessibility Priorities

iOS provides a property called accessibilitySortPriority. This accepts a numeric value — the higher the priority, the earlier VoiceOver reads that element during normal swipe navigation. By default, each view has a priority of 0 (zero). It doesn’t affect where items appear on screen (exploring by touch still works), but it changes the logical order in which VoiceOver moves through the elements.

I don’t know exactly how Forza implemented its lineup logic, but here’s how I replicated it.

  1. My implementation starts with an array of players ordered “footballistically” (goalkeeper to forward, right to left);
  2. This array is divided into subarrays, one per sector: goalkeeper, defenders, midfielders, etc;
  3. In SwiftUI, I render each sector vertically and each player horizontally within it — left to right.

That works perfectly for the home team — once each player’s position is added to their accessibility label, VoiceOver reads the lineup exactly as it should.

But for the away team, since SwiftUI can’t reverse the view creation order, I invert the formation (for example, 4-5-1 becomes 1-5-4) before creating the sector arrays. The result is a correct visual layout — but VoiceOver now reads from forwards to goalkeeper. To fix that, I assign an increasing priority value to each player while iterating in reverse order. The goalkeeper ends up with the highest priority, ensuring VoiceOver reads from goalkeeper to forwards, just as football fans would expect.

// Setting the reversed priorities

func playersByPosition() -> [[PlayerPrioritized]] {
  var players = awayLineup.reversed() // from forward to goalkeeper
  var sectors = [[PlayerPrioritized]]()
  var priority = viewSortingPriority

  for (i, sector) in formation.reversed().enumerated() {
    // creating inner array per formation sector
    sectors.append([PlayerPrioritized]())
    for _ in 0..<sector {
      // get and remove first player from list for next iteration
      let player = players.removeFirst()
      sectors[i].append(PlayerPrioritized(player: player, priority: priority))
      priority += 0.08 // increase priority to next player
     }
   }
  return sectors
}

// AwayTeamView

var body: some View {
  VStack {
    Text(away.lineUpHeading)
    ...
    // will be higher than first player
    .accessibilitySortPriority(viewSortingPriority + 0.95)
    ForEach(playersByPosition(), id: \.self) { sector in
      HStack {
        ForEach(sector, id: \.self) { player in 
          PlayerView(model: player)
          .accessibilitySortPriority(player.priority)
        }
      }
    }
  }
}

Since this property affects the entire view, I also adjusted priorities for the home team and bench sections. To keep things simple, I used a small increment value (0.08) for each away player’s priority.

In this video, you can hear VoiceOver navigating my app’s fixed lineup page, finishing the home team lineup, then reading the away team from bottom to top — goalkeeper to forwards, as intended.

Handling Substitutes

Unlike Forza, I decided to display both teams’ substitutes side by side. Without custom priorities, VoiceOver would alternate between home and away substitutes, reading one from each side.

Here, you can hear VoiceOver reading substitutes alternately — one from the home team, then one from the away team.

By assigning a higher priority to the home team substitutes view, VoiceOver reads the entire home bench first, then the away bench. Within each group, it uses the default navigation order.

// LineUps View

var body: some View {
  ScrollView {
    HomeTeamView(home: homeTeam)
    .accessibilitySortPriority(3)

    AwayTeamView(away: awayTeam,
    sortingPriority: 2)

    Text("Substitutes")
    .accessibilitySortPriority(1)

    HStack {
      BenchView(team: homeTeam)
      .accessibilitySortPriority(0.9)

      BenchView(team: awayTeam)
      .accessibilitySortPriority(0.8)
     }
  }
}

In this last video, VoiceOver reads all home substitutes first, then the away substitutes, in the correct order.

Conclusion

If Forza can make something as visual and complex as football lineups both accessible and fun to explore, then surely simpler apps can do the same — with much less effort. Accessibility isn’t just a technical checkbox — it’s a chance to make more people feel included, informed, and part of the game.

I’m deeply passionate about both iOS development and accessibility. If your iOS team needs someone who truly cares about accessibility and great user experience, I am opened to new assignments. Drop me an email at diogo@axesslab.com.

Get notified when we write new stuff

About once a month we write an article about accessibility or usability, that's just as awesome as this one! #HumbleBrag

Simply drop your email below.

We work world wide, if you need help send us an email!
hello@axesslab.com