Path Puzzles

Streaks (beta)

Embed a fully native Streaks path puzzle into your mobile app with minimal code.

StreaksGameView(model: .example)
Streaks
Streaks
Streaks

What's included

Quick integration

Embed a fully native Streaks path puzzle into your mobile app with minimal code.

Customise grid & cells

Swap in your own cell views and control the spacing between cells with a simple modifier.

Path & completion callbacks

React to every step the player draws and get notified the moment the streak is complete.


Quick integration

Pass a model into the view to render a fully playable Streaks grid. The player drags a continuous path through every unblocked cell — each step can move orthogonally or diagonally. If they lift their finger before connecting all cells, the path resets.

StreaksGameView(model: .example)

The model

StreaksModel defines the grid dimensions and which cells are permanently blocked. Blocked cells are skipped by the player's path and do not count toward completion. totalCells gives the exact number the player must connect.

let model = StreaksModel(
    rows: 5,
    columns: 5,
    blockedCells: [
        StreaksCoord(row: 1, col: 1),
        StreaksCoord(row: 3, col: 3)
    ]
)

// model.rows         — number of rows
// model.columns      — number of columns
// model.blockedCells — cells that cannot be visited (optional, defaults to empty)
// model.totalCells   — cells the player must connect (rows × columns − blocked)

Customise grid

Control the spacing between cells and swap in your own custom cell view via the .grid(spacing:cell:) modifier.

StreaksGameView(model: .example)
    .grid(spacing: 8, cell: StreaksCell.self)

Your custom cell receives the row, column, and current state — unselected, part of the active path with its 1-based order, or permanently blocked.

struct MyCell: StreaksCellProtocol {
    var row: Int
    var column: Int
    var state: StreaksCellState

    init(row: Int, column: Int, state: StreaksCellState) {
        self.row = row
        self.column = column
        self.state = state
    }

    private var fillColor: Color {
        switch state {
        case .unselected: return Color(.systemGray5)
        case .selected:   return Color.accentColor
        case .blocked:    return Color(.systemGray2)
        }
    }

    var body: some View {
        RoundedRectangle(cornerRadius: 8)
            .fill(fillColor)
            .overlay {
                switch state {
                case .selected(let order):
                    Text("\(order)")
                        .font(.caption.bold())
                        .foregroundStyle(.white)
                case .blocked:
                    Image(systemName: "xmark")
                        .font(.caption.bold())
                        .foregroundStyle(Color(.systemGray4))
                default:
                    EmptyView()
                }
            }
    }
}

Protocols

StreaksGameView exposes one protocol extension point — the cell view — alongside the StreaksCellState enum that describes every state a cell can be in.

StreaksCellProtocol
.grid(spacing:cell:)

The cell view contract. Implement to control how each grid cell looks across all three states.

StreaksCellState

An enum with three cases passed to every cell: unselected, selected (with 1-based path order), and blocked.

// Renders each grid cell — implement to customise cell appearance
public protocol StreaksCellProtocol: View {
    init(row: Int, column: Int, state: StreaksCellState)
}

// The three states a cell can be in
public enum StreaksCellState {
    case unselected           // not yet part of the path
    case selected(order: Int) // in the active path; order is its 1-based position
    case blocked              // permanently impassable
}

Path callbacks

React to every step the player draws — receive the full ordered path each time it grows by one cell.

StreaksGameView(model: .example)
    .grid(spacing: 8, cell: StreaksCell.self)
    .onInput { path in
        print("Path: \(path.count) / \(StreaksModel.example.totalCells) cells")
    }

Completion callbacks

Get notified the moment the player successfully connects every unblocked cell in the grid.

StreaksGameView(model: .example)
    .grid(spacing: 8, cell: StreaksCell.self)
    .onInput { path in
        print("Path: \(path.count) / \(StreaksModel.example.totalCells) cells")
    }
    .onCompletion { didWin in
        print(didWin ? "Streak complete!" : "")
    }

Explore the examples

Focus

Explore a fully working iOS and Android sample project you can run immediately.