admaDIC App Development & IT Solutions

Sudoku - Solver View

by Annett Schwarze | 2025-09-26

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()
}
    
Sudoku Solver View

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri Sep 26 07:51:12 2025 GMT