Sudoku - Solver View
This article is part of a series in which a Sudoku game is implemented. As a fourth step, the views for the solver are built.
An InputView is added so the user can set a number in a cell. The views are adjusted so the list of allowed numbers is shown in the free cells. These allowed numbers are carried into the InputView, so one sees, which numbers are available more easily.
The InputView is shown below:
struct InputView: View {
@ObservedObject var sudokuModel: SudokuModel
@Binding var showInputView: Bool
let editingCell: (Int, Int)
var body: some View {
let (row, col) = editingCell
let allowedNumbers = sudokuModel.allowedNumbers[row][col]
VStack(spacing: 8) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 9), spacing: 8) {
ForEach(1...9, id: \.self) { num in
Button {
let gen = SudokuGenerator()
if gen.canPlace(num, atRow: row, col: col, in: sudokuModel.cells) {
sudokuModel.cellsError[row][col] = false
} else {
sudokuModel.cellsError[row][col] = true
}
sudokuModel.cells[row][col] = num
sudokuModel.cellsFixed[row][col] = false
showInputView = false
gen.updateAllowedNumbers(model: sudokuModel)
sudokuModel.checkIsSolved()
} label: {
Text("\(num)")
.font(.title3)
.frame(width: 24, height: 24)
.background(
allowedNumbers.contains(num) ?
Color.blue.opacity(0.2)
: Color(.systemGray6)
)
.foregroundStyle(
allowedNumbers.contains(num) ?
Color.blue
: Color.gray
)
.cornerRadius(8)
}
}
}
Button {
sudokuModel.cells[row][col] = 0
sudokuModel.cellsFixed[row][col] = false
showInputView = false
SudokuGenerator().updateAllowedNumbers(model: sudokuModel)
} label: {
Text("Clear")
.font(.title3)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.red.opacity(0.2))
.cornerRadius(8)
}
}
.padding()
.background(.white)
.cornerRadius(12)
.shadow(radius: 6)
.padding(10)
.transition(.scale)
}
}
The main view is adjusted for more solver-friendly visualization:
struct SampleSudokuSolverView: View {
@StateObject private var sudokuModel: SudokuModel = SudokuModel()
let color1: Color = Color(.systemGray6)
let color2: Color = Color(.systemGray4)
let fgColor1: Color = Color(.black)
let fgColor2: Color = Color(.systemBlue)
@State private var showInputView: Bool = false
@State private var editingCell: (Int, Int)? = nil
let cellDim: CGFloat = 40
let cellFontSize: CGFloat = 20
let cellFontSizeSmall: CGFloat = 10
var body: some View {
ZStack {
Color(white: 0.99)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
showInputView = false
}
VStack {
Text("Sudoku Solver")
.font(.largeTitle)
.padding(.bottom, 24)
Spacer()
HStack(spacing: 16) {
Button(action: {
let gen = SudokuGenerator()
gen.generate(model: sudokuModel)
gen.makeGaps(model: sudokuModel, count: 30)
gen.updateAllowedNumbers(model: sudokuModel)
}, label: {
Text("Random")
})
.buttonStyle(.bordered)
}
.padding(.bottom, 32)
let isConfigured = sudokuModel.isConfigured
VStack(spacing: 0) {
ForEach(0 ..< SudokuModel.DIM, id: \.self) { row in
HStack(spacing: 0) {
ForEach(0 ..< SudokuModel.DIM, id: \.self) { col in
let isFilled = sudokuModel.isFilled(row: row, col: col)
if isFilled || !isConfigured {
let isError = sudokuModel.cellsError[row][col]
Text(sudokuModel.text(row: row, col: col))
.frame(width: cellDim, height: cellDim)
.background(
sudokuModel.colorIndex(row: row, col: col) == 1 ? color1 : color2
)
.foregroundStyle(
isError ? Color.red :
sudokuModel.foregroundColorIndex(row: row, col: col) == 1 ? fgColor1 : fgColor2
)
.border(Color.gray, width: 1)
.font(.system(size: cellFontSize, weight: .medium, design: .monospaced))
.onTapGesture {
startEdit(row: row, col: col)
}
} else {
Text(sudokuModel.allowedText(row: row, col: col))
.frame(width: cellDim, height: cellDim)
.background(
sudokuModel.colorIndex(row: row, col: col) == 1 ? color1 : color2
)
.foregroundStyle(sudokuModel.foregroundColorIndex(row: row, col: col) == 1 ? fgColor1 : fgColor2)
.border(Color.gray, width: 1)
.font(.system(size: cellFontSizeSmall, weight: .medium, design: .monospaced))
.onTapGesture {
startEdit(row: row, col: col)
}
}
}
}
}
}
HStack(spacing: 16) {
Button(action: {
sudokuModel.reset()
}, label: {
Text("Clear")
})
.buttonStyle(.bordered)
}
.padding(.top, 32)
Spacer()
Spacer()
Spacer()
}
.overlay(alignment: .bottom, content: {
showInputView ?
InputView(sudokuModel: sudokuModel, showInputView: $showInputView, editingCell: editingCell ?? (0,0))
: nil
})
.overlay(alignment: .center, content: {
sudokuModel.isSolved ?
VStack {
Text("You Solved It!")
.padding()
.font(.title)
Button(action: { sudokuModel.reset() }, label: { Text("New Game") })
}
.padding()
.background(content: {
Color(.systemYellow)
})
.clipShape(RoundedRectangle(cornerRadius: 16))
: nil
})
}
}
...
}
For convenience, the full source code is shown below:
import SwiftUI
struct SampleSudokuSolverView: View {
@StateObject private var sudokuModel: SudokuModel = SudokuModel()
let color1: Color = Color(.systemGray6)
let color2: Color = Color(.systemGray4)
let fgColor1: Color = Color(.black)
let fgColor2: Color = Color(.systemBlue)
@State private var showInputView: Bool = false
@State private var editingCell: (Int, Int)? = nil
let cellDim: CGFloat = 40
let cellFontSize: CGFloat = 20
let cellFontSizeSmall: CGFloat = 10
var body: some View {
ZStack {
Color(white: 0.99)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
showInputView = false
}
VStack {
Text("Sudoku Solver")
.font(.largeTitle)
.padding(.bottom, 24)
Spacer()
HStack(spacing: 16) {
Button(action: {
let gen = SudokuGenerator()
gen.generate(model: sudokuModel)
gen.makeGaps(model: sudokuModel, count: 30)
gen.updateAllowedNumbers(model: sudokuModel)
}, label: {
Text("Random")
})
.buttonStyle(.bordered)
}
.padding(.bottom, 32)
let isConfigured = sudokuModel.isConfigured
VStack(spacing: 0) {
ForEach(0 ..< SudokuModel.DIM, id: \.self) { row in
HStack(spacing: 0) {
ForEach(0 ..< SudokuModel.DIM, id: \.self) { col in
let isFilled = sudokuModel.isFilled(row: row, col: col)
if isFilled || !isConfigured {
let isError = sudokuModel.cellsError[row][col]
Text(sudokuModel.text(row: row, col: col))
.frame(width: cellDim, height: cellDim)
.background(
sudokuModel.colorIndex(row: row, col: col) == 1 ? color1 : color2
)
.foregroundStyle(
isError ? Color.red :
sudokuModel.foregroundColorIndex(row: row, col: col) == 1 ? fgColor1 : fgColor2
)
.border(Color.gray, width: 1)
.font(.system(size: cellFontSize, weight: .medium, design: .monospaced))
.onTapGesture {
startEdit(row: row, col: col)
}
} else {
Text(sudokuModel.allowedText(row: row, col: col))
.frame(width: cellDim, height: cellDim)
.background(
sudokuModel.colorIndex(row: row, col: col) == 1 ? color1 : color2
)
.foregroundStyle(sudokuModel.foregroundColorIndex(row: row, col: col) == 1 ? fgColor1 : fgColor2)
.border(Color.gray, width: 1)
.font(.system(size: cellFontSizeSmall, weight: .medium, design: .monospaced))
.onTapGesture {
startEdit(row: row, col: col)
}
}
}
}
}
}
HStack(spacing: 16) {
Button(action: {
sudokuModel.reset()
}, label: {
Text("Clear")
})
.buttonStyle(.bordered)
}
.padding(.top, 32)
Spacer()
Spacer()
Spacer()
}
.overlay(alignment: .bottom, content: {
showInputView ?
InputView(sudokuModel: sudokuModel, showInputView: $showInputView, editingCell: editingCell ?? (0,0))
: nil
})
.overlay(alignment: .center, content: {
sudokuModel.isSolved ?
VStack {
Text("You Solved It!")
.padding()
.font(.title)
Button(action: { sudokuModel.reset() }, label: { Text("New Game") })
}
.padding()
.background(content: {
Color(.systemYellow)
})
.clipShape(RoundedRectangle(cornerRadius: 16))
: nil
})
}
}
struct InputView: View {
@ObservedObject var sudokuModel: SudokuModel
@Binding var showInputView: Bool
let editingCell: (Int, Int)
var body: some View {
let (row, col) = editingCell
let allowedNumbers = sudokuModel.allowedNumbers[row][col]
VStack(spacing: 8) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 9), spacing: 8) {
ForEach(1...9, id: \.self) { num in
Button {
let gen = SudokuGenerator()
if gen.canPlace(num, atRow: row, col: col, in: sudokuModel.cells) {
sudokuModel.cellsError[row][col] = false
} else {
sudokuModel.cellsError[row][col] = true
}
sudokuModel.cells[row][col] = num
sudokuModel.cellsFixed[row][col] = false
showInputView = false
gen.updateAllowedNumbers(model: sudokuModel)
sudokuModel.checkIsSolved()
} label: {
Text("\(num)")
.font(.title3)
.frame(width: 24, height: 24)
.background(
allowedNumbers.contains(num) ?
Color.blue.opacity(0.2)
: Color(.systemGray6)
)
.foregroundStyle(
allowedNumbers.contains(num) ?
Color.blue
: Color.gray
)
.cornerRadius(8)
}
}
}
Button {
sudokuModel.cells[row][col] = 0
sudokuModel.cellsFixed[row][col] = false
showInputView = false
SudokuGenerator().updateAllowedNumbers(model: sudokuModel)
} label: {
Text("Clear")
.font(.title3)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.red.opacity(0.2))
.cornerRadius(8)
}
}
.padding()
.background(.white)
.cornerRadius(12)
.shadow(radius: 6)
.padding(10)
.transition(.scale)
}
}
private func startEdit(row: Int, col: Int) {
if sudokuModel.cellsFixed[row][col] { return }
editingCell = (row, col)
showInputView = true
}
class SudokuModel: ObservableObject {
static let DIM = 9
@Published var cells: [[Int]] = Array(repeating: Array(repeating: 0, count: DIM), count: DIM)
@Published var cellsFixed: [[Bool]] = Array(repeating: Array(repeating: true, count: DIM), count: DIM)
@Published var cellsError: [[Bool]] = Array(repeating: Array(repeating: false, count: DIM), count: DIM)
@Published var allowedNumbers: [[[Int]]] = Array(repeating: Array(repeating: Array(1...DIM), count: DIM), count: DIM)
@Published var isConfigured: Bool = false
@Published var isSolved: Bool = false
func reset() {
cells = Array(repeating: Array(repeating: 0, count: SudokuModel.DIM), count: SudokuModel.DIM)
cellsFixed = Array(repeating: Array(repeating: true, count: Self.DIM), count: Self.DIM)
cellsError = Array(repeating: Array(repeating: false, count: Self.DIM), count: Self.DIM)
allowedNumbers = Array(repeating: Array(repeating: Array(1...Self.DIM), count: Self.DIM), count: Self.DIM)
isConfigured = false
}
func text(row: Int, col: Int) -> String {
let v = cells[row][col]
return v == 0 ? "" : "\(v)"
}
func allowedText(row: Int, col: Int) -> String {
let v = allowedNumbers[row][col]
return v.map({ String($0) }).joined(separator: " ")
}
func isFilled(row: Int, col: Int) -> Bool {
return cells[row][col] != 0
}
func colorIndex(row: Int, col: Int) -> Int {
let subgridRow = row / 3
let subgridCol = col / 3
return ((subgridRow + subgridCol) % 2 == 0) ? 1 : 2
}
func foregroundColorIndex(row: Int, col: Int) -> Int {
return cellsFixed[row][col] == true ? 1 : 2
}
func checkIsSolved() {
isSolved = cells.allSatisfy({ $0.allSatisfy({ $0 != 0 }) }) && cellsError.allSatisfy({ $0.allSatisfy({ $0 == false }) })
}
}
class SudokuGenerator {
func generate(model: SudokuModel) {
model.reset()
_ = fill(0, 0, model: model)
model.isConfigured = true
}
func makeGaps(model: SudokuModel, count: Int) {
var indices = Array(0 ..< SudokuModel.DIM*SudokuModel.DIM)
indices.shuffle()
for i in 0 ..< min(count, indices.count) {
let idx = indices[i]
let row = idx / SudokuModel.DIM
let col = idx % SudokuModel.DIM
model.cells[row][col] = 0
model.cellsFixed[row][col] = false
}
}
private func fill(_ row: Int, _ col: Int, model: SudokuModel) -> Bool {
if row == SudokuModel.DIM {
return true
}
let nextRow = (col == SudokuModel.DIM - 1) ? row + 1 : row
let nextCol = (col + 1) % SudokuModel.DIM
let nums = Array(1 ... SudokuModel.DIM).shuffled()
for num in nums {
if canPlace(num, atRow: row, col: col, in: model.cells) {
model.cells[row][col] = num
if fill(nextRow, nextCol, model: model) {
return true
}
model.cells[row][col] = 0
}
}
return false
}
func canPlace(_ num: Int, atRow row: Int, col: Int, in cells: [[Int]]) -> Bool {
for i in 0 ..< SudokuModel.DIM {
if cells[row][i] == num || cells[i][col] == num {
return false
}
}
let boxRow = (row / 3) * 3
let boxCol = (col / 3) * 3
for i in 0 ..< 3 {
for j in 0 ..< 3 {
if cells[boxRow + i][boxCol + j] == num {
return false
}
}
}
return true
}
func updateAllowedNumbers(model: SudokuModel) {
for row in 0 ..< SudokuModel.DIM {
for col in 0 ..< SudokuModel.DIM {
if model.cells[row][col] != 0 {
model.allowedNumbers[row][col] = []
continue
}
var allowed: [Int] = []
for num in 1 ... SudokuModel.DIM {
if canPlace(num, atRow: row, col: col, in: model.cells) {
allowed.append(num)
}
}
model.allowedNumbers[row][col] = allowed
}
}
}
}
}
#Preview {
SampleSudokuSolverView()
}