Streaks (beta)
Embed a fully native Streaks path puzzle into your mobile app with minimal code.
StreaksGameView(model: .example)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.
StreaksCellProtocolThe cell view contract. Implement to control how each grid cell looks across all three states.
StreaksCellStateAn 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
Explore a fully working iOS and Android sample project you can run immediately.