J.BLOG

Exposing 81 Sudoku cells to VoiceOver and TalkBack

Jaemyeong Jin···8 min read
한국어

The DailySudoku board is a 9×9 square that shows digits and notes. For someone who looks at the screen and taps with a finger, turning a touch position into a cell number is enough. Someone who uses VoiceOver or TalkBack needs more: which cell has focus, what the cell contains, whether it is selected, and how to activate it.

This post compares the iOS and Android implementations based on the source code as of 2026-08-06. It is not a report that accessibility is done. It records the design rules I used and the parts that are still unverified.

What each cell has to say

On screen, the board is a single UIView on iOS and a single gesture container on Android. Assistive technology still receives 81 separate cells. Each cell needs this information:

  • Position: row and column numbers, starting from 1
  • Content: empty, a given digit, a value the user entered, an error, or notes
  • State: selected or not
  • Action and area: selecting the cell, and a rectangle that matches the cell on screen

An identifier or test tag such as game.cell.r0c0 is only for automated tests to find a cell. The description the user hears has to be separate from that identifier: a localized sentence that carries the position and the content. An example is a sentence saying that row 2, column 7 holds the digit 4.

One model for state, one function for input

The core game state is projected into models in the CellUi family. The model holds five things.

CellUi = value + given + error + notes + highlight

The colors and digits on screen, the calculation from coordinates to an index, and the description, state, and action for assistive technology all come from this model. The two platforms do not need the same view hierarchy. They need the same information from the same model.

Input also goes to one place. A tap, a drag, a VoiceOver activation, and a TalkBack double-tap all end in selectCell(index). I avoided separate accessibility-only logic because it could drift away from the state on screen.

iOS: one view, 81 accessibility elements

On iOS, a single BoardView draws the background, the grid, the digits, and the notes. Instead of one UIView per cell, it creates 81 UIAccessibilityElement objects. Apple’s documentation describes this as the way to expose items that are not views.

Each element gets these values:

  • Array order: row-major (left to right, then top to bottom)
  • accessibilityLabel: the coordinates plus whichever of empty, given, error, value, or notes applies
  • accessibilityTraits: button, with selected added for the selected cell
  • accessibilityFrameInContainerSpace: the rectangle of the cell
  • accessibilityActivate(): calls onSelect(index)

When the size changes after rotation or Split View, the frames are calculated again. The values are set directly in container coordinates.

Two things are unverified. I still need to check whether the order in which indexes 0 through 80 are created matches the real VoiceOver swipe order and read-all order. I also have not verified that VoiceOver speaks the new value right away when the focused cell changes.

Android: pointerInput has no button meaning

On Android, container coordinates are converted to a 9×9 index, and tap and drag use the same hit test. Pointer changes that the board handles are consumed so they do not interfere with outer scrolling.

The problem is that pointerInput alone does not make TalkBack treat a cell as a button. The Compose guide to gesture handling levels explains that default semantics and focus support shrink as you go down from Button to clickable to pointerInput. So each BoardCell gets its description, role, and click action by hand.

These are the semantics attached to each BoardCell.

Modifier.semantics {
    contentDescription = cellDescription
    role = Role.Button
    onClick {
        onSelect(index)
        true
    }
}

The board handles continuous drags from ordinary touch, and semantics handles activation of the cell that TalkBack has focused. Putting clickable on each cell would make it compete with the board’s pointerInput for the same events. This setup avoids that.

Where the two platforms still differ

Here is the source comparison as a table.

Item iOS Android
Each cell exposed separately Yes Yes
Position and content description Yes Yes
Button activation Yes Yes
Separate automation identifier Yes Yes
User-entered value Read as a digit only Distinguished as input
Selected state .selected None
Collection structure None None

Android has no selected or stateDescription. The background color of the selected cell changes, but the selected state does not reach TalkBack. Android also does not use collectionInfo or collectionItemInfo. The collection information described in the Compose semantics documentation tells the user the overall size and the current position. For now, both platforms convey the 9-row, 9-column structure only through the description text of each cell.

I set the next steps in this order. First, add the selected state on Android and listen to the speech output on both platforms. The 9×9 collection information comes after checking that it is not read twice together with the current row and column description. A custom rotor is worth considering only when there is evidence that moving through 81 cells in a line is a real problem.

The cells are small

The board size has an upper limit: 560pt on iOS and 520dp on Android. On Android, when the width is 840dp or more, the board and the controls sit side by side. In a foldable tabletop posture, the hinge splits the screen into top and bottom areas.

At any size, the drawing, the hit test, and the accessibility cells must point to the same index. They are recalculated on rotation, window size change, and foldable posture change. While the pause or completion modal is up, the board behind it is removed from navigation. iOS makes the overlay a modal accessibility area, and Android clears the background semantics.

The concern is cell size. The accessibility section of the Human Interface Guidelines gives a default control size of 44×44pt and a minimum of 28×28pt for iOS and iPadOS. The Compose documentation on default accessibility behavior recommends at least 48dp for interactive elements. With nine cells per row, a cell on iOS is narrower than 44pt when the board is narrower than 396pt. On Android, the cell computed from the code under a compact 360dp condition is about 34dp. That number is calculated, not measured on a device.

Because the Android cells carry hand-written semantics, they do not rely on the automatic target expansion that clickable provides. Still, growing all 81 areas to 44pt or 48dp at once would make them overlap, and it could become unclear which cell was chosen. I can only judge this after measuring focus areas, touch exploration, and finger mis-taps on real devices. The alternatives are enlarging the board, navigating by row or box, or a separate input method. I do not yet have grounds to pick one.

Text size is also open. Digits and notes on iOS are drawn at a fixed ratio of the cell, so they do not follow Dynamic Type. Where and how to show larger text is left for later verification.

What to lock down with tests

The existing checks cover cell state projection, coordinate hit testing, drag boundaries, part of the color contrast, and foldable areas. No test locks down the virtual accessibility elements or the cell semantics themselves. These are the items I plan to automate:

  • 81 cells in the playing state
  • First and last coordinates (1,1) and (9,9)
  • A description for each of given, error, notes, empty, and an ordinary value
  • Selected state exposed only on the selected cell
  • One selectCell(index) call per activation
  • Areas that match after a resize
  • The board excluded after a modal appears
  • The 9×9 information and the current row and column in Compose

The UI tests described in the Compose accessibility testing documentation can check semantics properties and actions. In UIKit, unit tests can check the label, traits, frame, and activation. Being able to find an element with a selector does not prove accessibility.

I also listed what to check on devices. On an iPhone and an Android phone: the point where a swipe moves to the next row, whether the spoken cell matches the finger position, whether focus stays after a double-tap, and whether changes to digits, notes, and errors are noticed. The last check is to select, enter, correct, and finish without looking at the screen.

Apple’s guide to supporting VoiceOver and the VoiceOver section of the Human Interface Guidelines recommend turning VoiceOver on and auditing element access, traversal, and tasks that depend on sight. The automated checks introduced in the Compose accessibility overview help find small targets and traversal order problems, but hands-on checking is still needed.

What the current source supports is this much: both platforms expose the cells separately and route selection to the same function. The selected state on Android, grid information on both platforms, whether the target size is adequate, speech output on state changes, and a full traversal on real devices are not verified. So I do not claim that the two platforms have reached the same level of accessibility.

광고Coupang Partners

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.