Shikaku
Embed a fully native Shikaku (Rectangles) partition puzzle into your mobile app with minimal code.
@State private var model = ShikakuModel.example
ShikakuGameView(model: $model)What's included
Quick integration
Drop in a ShikakuModel and ShikakuGameView — drag-to-draw, tap-to-remove, violation detection, and completion are all handled automatically.
Drag-to-draw input
Drag from any cell to any other cell to draw a rectangle preview. Releasing commits it. Tapping a covered cell removes its rectangle. No buttons needed.
Any grid size
Works with any rows × columns grid. Use the built-in 9×6 example or supply your own puzzle — from a compact 4×4 to a large 12×8 or beyond.
Customise cells
Swap in your own cell view via the .grid(spacing:cell:) modifier. Receive clue, rect, isViolation, isPreview, isOverlap, and colorIndex to drive any visual design.
Stable rectangle colours
Each rectangle gets a stable colour index keyed to its clue coordinate — colours never shift when new rectangles are placed or removed.
State & reset
All live state lives in model.rects. Read it any time, persist it to restore a saved game, or call model.reset() to start over.
The rules
Shikaku has three constraints. The SDK checks all of them when evaluating model.isComplete.
Partition
The rectangles must cover the entire grid with no gaps and no overlaps. Every cell belongs to exactly one rectangle.
One clue per rectangle
Each rectangle must contain exactly one numbered clue cell. A rectangle with zero or two or more clues is invalid.
Area must match
The rectangle's area — rowSpan × colSpan — must equal its clue value. A cell labelled "6" must be inside a 1×6, 2×3, 3×2, or 6×1 rectangle.
// Shikaku has three constraints — all checked live inside model.isComplete:
// 1. Partition — the rectangles must cover the entire grid with no gaps or overlaps.
// 2. One clue per rectangle — each rectangle contains exactly one numbered clue cell.
// 3. Area must match — the rectangle's area (rowSpan × colSpan) must equal its clue.
// A cell labelled "6" must be covered by a 1×6, 2×3, 3×2, or 6×1 rectangle.
// Read live puzzle state:
print(model.rects.count) // rectangles placed so far
print(model.isSolved) // true when all constraints are satisfiedQuick integration
Pass a model binding into the view to render a fully playable Shikaku grid. The view owns the drag-to-draw interaction — no extra controls needed.
@State private var model = ShikakuModel.example
var body: some View {
// Pass a $Binding — every drag gesture is written back into model.rects
ShikakuGameView(model: $model)
}The model
ShikakuModel describes the puzzle and owns all live player state — the grid dimensions, the clue dictionary, the player's placed rectangles, and computed helpers for completion and violation detection. Pass it as a @Binding so the view writes mutations back to your state layer.
// Provide rows, columns, and a sparse clue dictionary (coord → required area).
let model = ShikakuModel(
rows: 9,
columns: 6,
clues: [
ShikakuCoord(row: 0, col: 1): 6,
ShikakuCoord(row: 1, col: 2): 4,
// …
]
)
// ── Configuration ─────────────────────────────────────────────────────
// model.rows — number of rows in the grid
// model.columns — number of columns in the grid
// model.clues — [ShikakuCoord: Int] sparse map of coord → required rect area
// ── Live state (mutated during play) ──────────────────────────────────
// model.rects — [ShikakuRect] the rectangles placed so far
// model.isSolved — true once the puzzle is solved
// ── Computed ──────────────────────────────────────────────────────────
// model.isComplete — true when the grid is fully and correctly partitioned
// model.rect(at:) — returns the ShikakuRect covering a given coord, or nil
// ── Methods ────────────────────────────────────────────────────────────
// model.place(_ rect:) — places a rectangle, removing any it overlaps first
// model.removeRect(at:) — removes the rectangle covering a coordinate
// model.reset() — clears all placed rectangles and resets isSolvedProviding a puzzle
Supply rows, columns, and a sparse clues dictionary mapping each ShikakuCoord to its required rectangle area. ShikakuModel.example is a ready-to-use 9×6 puzzle you can drop in immediately.
// Supply rows, columns, and a clue dictionary.
// The SDK validates nothing at init time; rule checking happens lazily via isComplete.
let model = ShikakuModel(
rows: 9,
columns: 6,
clues: [
ShikakuCoord(row: 0, col: 1): 6,
ShikakuCoord(row: 1, col: 2): 4,
ShikakuCoord(row: 2, col: 4): 6,
ShikakuCoord(row: 4, col: 3): 6,
ShikakuCoord(row: 3, col: 0): 6,
ShikakuCoord(row: 5, col: 5): 6,
ShikakuCoord(row: 7, col: 2): 6,
ShikakuCoord(row: 6, col: 1): 6,
ShikakuCoord(row: 7, col: 4): 4,
ShikakuCoord(row: 8, col: 4): 4,
]
)
// Use ShikakuModel.example for the same ready-to-use 9×6 puzzle.
let example = ShikakuModel.exampleDrag-to-draw interaction
The player draws rectangles by dragging from one corner to the opposite corner. Releasing commits the placement. Tapping a covered cell removes its rectangle. Use .onMove(_:) to react to each placement.
// Drag across cells to draw a rectangle — releasing commits the placement.
// Tapping a covered cell removes its rectangle.
ShikakuGameView(model: $model)
.onMove { rect in
print("Placed \(rect.rowSpan)×\(rect.colSpan) rect at (\(rect.row),\(rect.col)) area=\(rect.area)")
}
// ShikakuRect exposes:
// rect.row — top-left row
// rect.col — top-left column
// rect.rowSpan — height in cells
// rect.colSpan — width in cells
// rect.area — rowSpan × colSpan
// rect.contains(_ coord:) — true if coord falls inside this rect
// rect.isDisjoint(from: other) — true if the two rects do not overlapViolation highlighting
By default the view passes isViolation: true to cells in any rectangle that breaks a rule — wrong area, zero clues, or multiple clues. Toggle the feedback with .showViolations(_:). Cells that will be overwritten by the active drag are highlighted with isOverlap: true.
// Violation highlighting is ON by default.
// Any rectangle that breaks a rule (wrong area, zero or multiple clues, out of bounds)
// receives isViolation: true on every cell it covers.
ShikakuGameView(model: $model)
.showViolations(true) // default — highlight rule-breaking rectangles
// Disable for a cleaner look (no real-time feedback)
ShikakuGameView(model: $model)
.showViolations(false)
// Overlapping cells are highlighted in orange during an active drag.
// isOverlap: true is set on cells covered by both a placed rect and the drag preview —
// so the player sees exactly what will be overwritten before releasing.State management
Because ShikakuGameView accepts a Binding<ShikakuModel>, all live game state — placed rectangles, solved status — is written back into the model on every gesture. Hold the model in @State, read state directly, and call reset() to restart.
// Hold the model in @State — the view writes every gesture back into model.rects
struct ContentView: View {
@State private var model = ShikakuModel.example
var body: some View {
VStack {
ShikakuGameView(model: $model)
// Read live state directly from the binding's wrapped value
if model.isSolved {
Text("Puzzle solved!")
}
// Inspect placed rectangles any time
Text("\(model.rects.count) rectangles placed")
Button("Restart") { model.reset() }
}
}
}Customise cells
Control cell spacing and replace the built-in ShikakuCell with any view that conforms to ShikakuCellProtocol via the .grid(spacing:cell:) modifier.
ShikakuGameView(model: $model)
.grid(spacing: 3, cell: MyShikakuCell.self)Your custom cell receives a ShikakuCellState containing the clue value, the covering rectangle, and all visual state flags — violation, preview, overlap, and colour index.
struct MyShikakuCell: ShikakuCellProtocol {
let row: Int
let column: Int
let state: ShikakuCellState
init(row: Int, column: Int, state: ShikakuCellState) {
self.row = row; self.column = column; self.state = state
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 6)
.fill(background)
.overlay(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(state.isViolation ? Color.red : Color.clear, lineWidth: 2)
)
if state.isPreview {
RoundedRectangle(cornerRadius: 6).fill(Color.accentColor.opacity(0.3))
}
if let clue = state.clue {
Text("\(clue)")
.font(.system(size: 14, weight: .bold, design: .rounded))
.foregroundStyle(state.colorIndex != nil ? Color.white : Color(.label))
}
}
.aspectRatio(1, contentMode: .fit)
.animation(.easeInOut(duration: 0.12), value: state)
}
private var background: Color {
if state.isOverlap { return Color.orange.opacity(0.5) }
if let index = state.colorIndex {
return palette[index % palette.count].opacity(0.6)
}
return Color(.systemGray5)
}
private let palette: [Color] = [.blue, .green, .orange, .purple, .pink, .teal]
}
// Register the custom cell
ShikakuGameView(model: $model)
.grid(spacing: 3, cell: MyShikakuCell.self)Protocols
ShikakuGameView exposes one protocol extension point — the cell view.
ShikakuCellProtocolThe cell view contract. Implement to control how each grid cell looks. Receives a ShikakuCellState with clue, rect, isViolation, isPreview, isOverlap, and colorIndex.
ShikakuCoordA zero-based (row, col) coordinate used in the clues dictionary and to identify cells when calling model.rect(at:) or model.removeRect(at:).
// Renders each grid cell — implement to customise cell appearance
public protocol ShikakuCellProtocol: View {
init(row: Int, column: Int, state: ShikakuCellState)
}
// ShikakuCellState — passed to every cell on every render
public struct ShikakuCellState {
let clue: Int? // clue number at this cell, or nil
let rect: ShikakuRect? // rectangle covering this cell, or nil
let isRectOrigin: Bool // true if this is the top-left cell of its rectangle
let isViolation: Bool // true when the covering rectangle breaks a rule
let isPreview: Bool // true while a drag is active and this cell is inside the preview
let isOverlap: Bool // true when a drag preview would overwrite an existing rectangle
let colorIndex: Int? // stable palette index for the covering rectangle, or nil
}Hint
Wire up a hint button by passing a Binding<Int> to .hint(trigger:). Each increment activates hint mode — all uncovered cells are highlighted with an orange tint. Tap any highlighted cell to reveal its solution rectangle. Hint mode ends after one tap. Supply a solution array when creating the model — ShikakuModel.example already includes one.
// Hold an integer counter in @State — increment it to request a hint.
@State private var hintCount = 0
ShikakuGameView(model: $model)
.hint(trigger: $hintCount)
// Each increment activates hint mode — all uncovered cells are highlighted with an orange tint.
// Tap any highlighted cell to reveal the solution rectangle that should cover it.
// Hint mode ends after one tap. Has no effect when no solution was provided or the puzzle is solved.
Button("Hint") { hintCount += 1 }
// Supply the solution when creating the model so hints know what to reveal:
let model = ShikakuModel(
rows: 9,
columns: 6,
clues: [ ShikakuCoord(row: 0, col: 1): 6, /* … */ ],
solution: [
ShikakuRect(row: 0, col: 0, rowSpan: 3, colSpan: 2),
// …
]
)
// ShikakuModel.example already includes a verified solution — hints work out of the box.
ShikakuGameView(model: $model)
.hint(trigger: $hintCount)
.onComplete {
print("Solved — possibly with hints!")
}Completion callbacks
Get notified the moment the player solves the puzzle. .onComplete() fires once when model.isSolved becomes true after a placement. Pair it with .onMove(_:) to track every step of the player's progress.
ShikakuGameView(model: $model)
.grid(spacing: 2, cell: ShikakuCell.self)
.showViolations(true)
.onMove { rect in
print("Placed \(rect.rowSpan)×\(rect.colSpan) at (\(rect.row),\(rect.col))")
}
.onComplete {
print("Puzzle solved!")
}Explore the examples
Explore a fully working iOS sample project you can run immediately.