├── demo
├── dark
│ ├── home.png
│ ├── board.png
│ └── settings.png
└── light
│ ├── board.png
│ ├── home.png
│ └── settings.png
├── app
├── Sudoku
│ ├── fonts
│ │ ├── CaviarDreams.ttf
│ │ └── CaviarDreams_Bold.ttf
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── Golden.colorset
│ │ │ └── Contents.json
│ │ ├── ActiveBlue.colorset
│ │ │ └── Contents.json
│ │ ├── LightBlue.colorset
│ │ │ └── Contents.json
│ │ ├── GridBackground.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Sudoku.entitlements
│ ├── Seeds.swift
│ ├── Views
│ │ ├── StatisticsView.swift
│ │ ├── StrategiesView.swift
│ │ ├── RootView.swift
│ │ ├── SettingsView.swift
│ │ ├── GameView.swift
│ │ └── MenuView.swift
│ ├── Models
│ │ ├── Record.swift
│ │ ├── Sudoku.xcdatamodeld
│ │ │ └── Record.xcdatamodel
│ │ │ │ └── contents
│ │ ├── Settings.swift
│ │ └── Grid.swift
│ ├── Support Views
│ │ ├── GenericTopBarView.swift
│ │ ├── TimerView.swift
│ │ ├── GridView.swift
│ │ └── KeyboardView.swift
│ ├── Constant.swift
│ ├── en.lproj
│ │ └── Localizable.strings
│ ├── pt-PT.lproj
│ │ └── Localizable.strings
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ └── AppDelegate.swift
└── Sudoku.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ └── xcschemes
│ │ └── Sudoku.xcscheme
│ └── project.pbxproj
├── .gitignore
├── README.md
├── LICENSE
└── generator
└── generate.swift
/demo/dark/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/demo/dark/home.png
--------------------------------------------------------------------------------
/demo/dark/board.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/demo/dark/board.png
--------------------------------------------------------------------------------
/demo/light/board.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/demo/light/board.png
--------------------------------------------------------------------------------
/demo/light/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/demo/light/home.png
--------------------------------------------------------------------------------
/demo/dark/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/demo/dark/settings.png
--------------------------------------------------------------------------------
/demo/light/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/demo/light/settings.png
--------------------------------------------------------------------------------
/app/Sudoku/fonts/CaviarDreams.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/app/Sudoku/fonts/CaviarDreams.ttf
--------------------------------------------------------------------------------
/app/Sudoku/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/app/Sudoku/fonts/CaviarDreams_Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pgalhardo/Sudoku/HEAD/app/Sudoku/fonts/CaviarDreams_Bold.ttf
--------------------------------------------------------------------------------
/app/Sudoku/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/app/Sudoku.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS generated
2 | .DS_Store
3 | .DS_Store?
4 | ._*
5 | .Spotlight-V100
6 | .Trashes
7 | ehthumbs.db
8 | Thumbs.db
9 |
10 | xcuserdata/
11 | Sudoku.xcodeproj/project.xcworkspace/xcuserdata/pgalhardo.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/app/Sudoku.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/Sudoku/Sudoku.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About
2 |
3 | A Sudoku for iOS built with SwiftUI. SwiftUI also allows easy iPadOS and macOS deployments, but the current code base doesn't quite support them yet.
4 |
5 | # Screenshots
6 |
7 | ## Dark
8 | | **Welcome** | **Settings** | **Board** |
9 | |:---:|:---:|:---:|
10 | | [](demo/dark/home.png) | [](demo/dark/settings.png) | [](demo/dark/board.png) |
11 |
12 | ## Light
13 | | **Welcome** | **Settings** | **Board** |
14 | |:---:|:---:|:---:|
15 | | [](demo/light/home.png) | [](demo/light/settings.png) | [](demo/light/board.png) |
16 |
--------------------------------------------------------------------------------
/app/Sudoku/Seeds.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Seeds.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 03/06/2020.
6 | // Copyright © 2020 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum Puzzles {
12 | static let easy: [String] = [
13 | "020590030070000100000000070610000007500000090000000302000010000860300050103400900",
14 | "010006008062000705400000200000400000380000001000700536500064090000000000270030050",
15 | "008070200900501008000800700050000040000687103000000800029005000000000030010900006",
16 | "028000090000000004309800000050080040900072600706000010000040000275000000000030187",
17 | "000004090020890000500002008000300001200009040050010030080000006900000500000460070"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/app/Sudoku/Views/StatisticsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatisticsView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 02/11/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct StatisticsView: View {
12 |
13 | @EnvironmentObject var viewRouter: ViewRouter
14 |
15 | var body: some View {
16 | ScrollView {
17 | VStack(spacing: 0) {
18 | GenericTopBarView(title: "main.stats",
19 | destination: Pages.home)
20 |
21 | Spacer()
22 | Text("Soon...")
23 | .font(.custom("CaviarDreams-Bold", size: 15))
24 | }
25 | }
26 | }
27 | }
28 |
29 | struct StatisticsView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | SettingsView()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Sudoku/Views/StrategiesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StrategiesView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 04/11/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct StrategiesView: View {
12 |
13 | @EnvironmentObject var viewRouter: ViewRouter
14 |
15 | var body: some View {
16 | ScrollView {
17 | VStack(spacing: 0) {
18 | GenericTopBarView(title: "main.strategies",
19 | destination: Pages.home)
20 |
21 | Spacer()
22 | Text("Soon...")
23 | .font(.custom("CaviarDreams-Bold", size: 15))
24 | }
25 | }
26 | }
27 | }
28 |
29 | struct StrategiesView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | StrategiesView()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Sudoku/Models/Record.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Record.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 08/08/2020.
6 | // Copyright © 2020 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | public class Record: NSManagedObject, Identifiable {
13 | @NSManaged public var date : Date?
14 | @NSManaged public var timeElapsed : Int32
15 | @NSManaged public var errors : Int16
16 | @NSManaged public var victory : Bool
17 | }
18 |
19 | extension Record {
20 | static func getAllRecords() -> NSFetchRequest {
21 | let request: NSFetchRequest = Record.fetchRequest() as!
22 | NSFetchRequest
23 |
24 | let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
25 |
26 | request.sortDescriptors = [sortDescriptor]
27 |
28 | return request
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Sudoku/Assets.xcassets/Golden.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.629",
9 | "green" : "0.379",
10 | "red" : "0.263"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.260",
27 | "green" : "0.617",
28 | "red" : "0.758"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Sudoku/Assets.xcassets/ActiveBlue.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.994",
9 | "green" : "0.860",
10 | "red" : "0.745"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.466",
27 | "green" : "0.228",
28 | "red" : "0.081"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Sudoku/Assets.xcassets/LightBlue.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.927",
9 | "green" : "0.898",
10 | "red" : "0.883"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.150",
27 | "green" : "0.113",
28 | "red" : "0.114"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Sudoku/Assets.xcassets/GridBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.169",
27 | "green" : "0.138",
28 | "red" : "0.144"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Sudoku/Models/Sudoku.xcdatamodeld/Record.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Pedro Galhardo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/Sudoku/Support Views/GenericTopBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenericTopBarView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 03/11/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct GenericTopBarView: View {
12 | private var title: String!
13 | private var destination: Int!
14 |
15 | @EnvironmentObject var viewRouter: ViewRouter
16 |
17 | private let frameSize: CGFloat = Screen.cellWidth / 2
18 |
19 | init(title: String, destination: Int) {
20 | self.title = title
21 | self.destination = destination
22 | }
23 |
24 | var body: some View {
25 | ZStack {
26 | Text(LocalizedStringKey(title))
27 | .font(.custom("CaviarDreams-Bold", size: 20))
28 |
29 | HStack {
30 | Button(
31 | action: {
32 | withAnimation(.easeIn) {
33 | self.viewRouter.setCurrentPage(page: self.destination)
34 | }
35 | },
36 | label: {
37 | Image(systemName: "chevron.left")
38 | Text("main.back")
39 | .font(.custom("CaviarDreams-Bold", size: 15))
40 | }
41 | )
42 | .foregroundColor(Color(.label))
43 | Spacer()
44 | }
45 | }
46 | .padding(.top)
47 | .padding(.leading)
48 | .padding(.trailing)
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/app/Sudoku/Constant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constant.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 29/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | let UNDEFINED: Int = 0
13 |
14 | enum Screen {
15 | static let size: CGRect = UIScreen.main.bounds
16 | static let width: CGFloat = UIScreen.main.bounds.width
17 | static let height: CGFloat = UIScreen.main.bounds.height
18 | static let cellWidth: CGFloat = UIScreen.main.bounds.size.width * 0.95 / 9
19 | static let lineThickness: CGFloat = 2
20 | }
21 |
22 | enum Pages {
23 | static let home: Int = 0
24 | static let game: Int = 1
25 | static let settings: Int = 2
26 | static let statistics: Int = 3
27 | static let strategies: Int = 4
28 | }
29 |
30 | enum InputType {
31 | static let system: Int = 0
32 | static let user: Int = 1
33 | static let error: Int = 2
34 | }
35 |
36 | enum Colors {
37 | static let DeepBlue: Color = Color(red: 45 / 255,
38 | green: 75 / 255,
39 | blue: 142 / 255)
40 | static let ActiveBlue: Color = Color(UIColor(named: "ActiveBlue")!)
41 | static let LightBlue: Color = Color(UIColor(named: "LightBlue")!)
42 | static let MatteBlack: Color = Color(red: 27 / 255,
43 | green: 27 / 255,
44 | blue: 27 / 255)
45 | static let Golden: Color = Color(UIColor(named: "Golden")!)
46 | }
47 |
--------------------------------------------------------------------------------
/app/Sudoku/Models/Settings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Settings.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 03/11/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class Settings: ObservableObject {
12 | @Published var highlightAreas: Bool! {
13 | didSet {
14 | UserDefaults.standard.set(highlightAreas,
15 | forKey: "highlightAreas")
16 | }
17 | }
18 | @Published var highlightSimilar: Bool! {
19 | didSet {
20 | UserDefaults.standard.set(highlightSimilar,
21 | forKey: "highlightSimilar")
22 | }
23 | }
24 | @Published var hideUsed: Bool! {
25 | didSet {
26 | UserDefaults.standard.set(hideUsed,
27 | forKey: "highlightUsed")
28 | }
29 | }
30 | @Published var enableTimer: Bool! {
31 | didSet {
32 | UserDefaults.standard.set(enableTimer,
33 | forKey: "enableTimer")
34 | }
35 | }
36 | @Published var fontSize: Float! {
37 | didSet {
38 | UserDefaults.standard.set(fontSize,
39 | forKey: "fontSize")
40 | }
41 | }
42 |
43 | init() {
44 | highlightAreas = UserDefaults.standard.bool(forKey: "highlightAreas")
45 | highlightSimilar = UserDefaults.standard.bool(forKey: "highlightSimilar")
46 | hideUsed = UserDefaults.standard.bool(forKey: "highlightUsed")
47 | enableTimer = UserDefaults.standard.bool(forKey: "enableTimer")
48 | fontSize = UserDefaults.standard.float(forKey: "fontSize")
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/Sudoku/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Sudoku
4 |
5 | Created by Pedro Galhardo on 28/05/2020.
6 | Copyright © 2020 Pedro Galhardo. All rights reserved.
7 | */
8 |
9 | "main.new" = "New game";
10 | "main.resume" = "Resume";
11 | "main.stats" = "Statistics";
12 | "main.strategies" = "Strategies";
13 | "main.settings" = "Settings";
14 | "main.back" = "Back";
15 |
16 | "settings.areas" = "Highlight areas";
17 | "settings.areas.descript" = "Highlight the selected cell column and row";
18 | "settings.twins" = "Highlight identical numbers";
19 | "settings.twins.descript" = "Highlight numbers identical to those in the selected cell";
20 | "settings.used" = "Hide used numbers";
21 | "settings.used.descript" = "Hide numbers that are no longer available to be placed";
22 | "settings.timer" = "Timer";
23 | "settings.text.size" = "Text size";
24 |
25 | "alert.progress.title" = "Current progress will be lost!";
26 | "alert.progress.continue" = "Continue?";
27 | "alert.progress.yes" = "Yes";
28 | "alert.progress.no" = "No";
29 | "alert.default.remove" = "Cannot remove default value";
30 | "alert.default.overwrite" = "Cannot overwrite default value";
31 |
32 | "game.errors: %lld" = "Errors: %lld / 3";
33 |
34 | "banners.congrats.title" = "Well done!";
35 | "banners.congrats.stats: %lld" = "Finished with %lld errors";
36 | "banners.pause.title" = "Paused";
37 | "banners.pause.stats: %lld" = "%lld%% completed";
38 |
39 | "keyboard.undo" = "Undo";
40 | "keyboard.delete" = "Delete";
41 | "keyboard.notes" = "Notes";
42 | "keyboard.sugestion" = "Sugestion";
43 |
44 | "button.leave" = "Leave";
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/Sudoku/pt-PT.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Sudoku
4 |
5 | Created by Pedro Galhardo on 28/05/2020.
6 | Copyright © 2020 Pedro Galhardo. All rights reserved.
7 | */
8 |
9 | "main.new" = "Novo jogo";
10 | "main.resume" = "Continuar";
11 | "main.stats" = "Estatísticas";
12 | "main.strategies" = "Estratégias";
13 | "main.settings" = "Definições";
14 | "main.back" = "Voltar";
15 |
16 | "settings.areas" = "Destacar áreas";
17 | "settings.areas.descript" = "Destacar a coluna e fila da célula selecionada";
18 | "settings.twins" = "Destacar números idênticos";
19 | "settings.twins.descript" = "Destacar números idênticos aos da célula selecionada";
20 | "settings.used" = "Ocultar números usados";
21 | "settings.used.descript" = "Ocultar números que já não estão disponíveis para serem colocados";
22 | "settings.timer" = "Temporizador";
23 | "settings.text.size" = "Tamanho do texto";
24 |
25 | "alert.progress.title" = "O progresso atual será perdido!";
26 | "alert.progress.continue" = "Continuar?";
27 | "alert.progress.yes" = "Sim";
28 | "alert.progress.no" = "Não";
29 | "alert.default.remove" = "Impossível remover valor pré-definido";
30 | "alert.default.overwrite" = "Impossível sobrescrever valor pré-definido";
31 |
32 | "game.errors: %lld" = "Erros: %lld / 3";
33 |
34 | "banners.congrats.title" = "Parabéns!";
35 | "banners.congrats.stats: %lld" = "Terminado com %lld erros";
36 | "banners.pause.title" = "Em pausa";
37 | "banners.pause.stats: %lld" = "%lld%% completo";
38 |
39 | "keyboard.undo" = "Desfazer";
40 | "keyboard.delete" = "Apagar";
41 | "keyboard.notes" = "Notas";
42 | "keyboard.sugestion" = "Sugestão";
43 |
44 | "button.leave" = "Sair";
45 |
--------------------------------------------------------------------------------
/app/Sudoku/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/Sudoku/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/app/Sudoku/Support Views/TimerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimerView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 31/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct TimerView : View {
13 |
14 | @EnvironmentObject var grid: Grid
15 | @EnvironmentObject var pauseHolder: PauseHolder
16 | @EnvironmentObject var timerHolder: TimerHolder
17 |
18 | private let labelSize: CGFloat = Screen.cellWidth / 2
19 |
20 | var body: some View {
21 | Text(String(format: "%@%02d:%02d", arguments: [
22 | self.hours(), self.minutes(), self.sec()
23 | ]))
24 | .font(.custom("CaviarDreams-Bold", size: min(Screen.cellWidth / 3, 15)))
25 | }
26 |
27 | func sec() -> Int {
28 | return self.timerHolder.count % 60
29 | }
30 |
31 | func minutes () -> Int {
32 | return (self.timerHolder.count / 60) % 60
33 | }
34 |
35 | func hours() -> String {
36 | if self.timerHolder.count >= 3600 {
37 | return String(format: "%02d:", (self.timerHolder.count / 3600))
38 | }
39 | return String()
40 | }
41 | }
42 |
43 | class TimerHolder : ObservableObject {
44 | private var timer : Timer!
45 |
46 | @Published var count = 0
47 |
48 | init() {
49 | if UserDefaults.standard.object(forKey: "time") != nil {
50 | let value: Int = UserDefaults.standard.integer(forKey: "time")
51 | self.count = value
52 | }
53 | start()
54 | }
55 |
56 | func start() -> Void {
57 | self.timer?.invalidate()
58 | self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
59 | _ in
60 | self.count += 1
61 | }
62 | }
63 |
64 | func stop() -> Void {
65 | self.timer?.invalidate()
66 | self.storeCounterValue()
67 | }
68 |
69 | func reset() -> Void {
70 | self.count = 0
71 | }
72 |
73 | func storeCounterValue() -> Void {
74 | UserDefaults.standard.set(self.count, forKey: "time")
75 | }
76 | }
77 |
78 | class PauseHolder : ObservableObject {
79 |
80 | @Published var paused: Bool = false
81 |
82 | func toggle() -> Void {
83 | self.paused.toggle()
84 | }
85 |
86 | func isPaused() -> Bool {
87 | return paused
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/Sudoku/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UIAppFonts
53 |
54 | CaviarDreams.ttf
55 | CaviarDreams_Bold.ttf
56 |
57 | UISupportedInterfaceOrientations~ipad
58 |
59 | UIInterfaceOrientationPortrait
60 | UIInterfaceOrientationPortraitUpsideDown
61 | UIInterfaceOrientationLandscapeLeft
62 | UIInterfaceOrientationLandscapeRight
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/Sudoku/Views/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 30/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | import SwiftUI
12 |
13 | class ViewRouter: ObservableObject {
14 | let objectWillChange = PassthroughSubject()
15 |
16 | var currentPage: Int = Pages.home {
17 | didSet {
18 | objectWillChange.send(self)
19 | }
20 | }
21 |
22 | func getCurrentPage() -> Int {
23 | return self.currentPage
24 | }
25 |
26 | func setCurrentPage(page: Int) -> Void {
27 | self.currentPage = page
28 | }
29 |
30 | func currentlyAt(page: Int) -> Bool {
31 | return self.currentPage == page
32 | }
33 | }
34 |
35 | struct RootView: View {
36 |
37 | @EnvironmentObject var viewRouter: ViewRouter
38 |
39 | private var grid: Grid = Grid()
40 | private var settings: Settings = Settings()
41 |
42 | var body: some View {
43 | VStack {
44 | if viewRouter.currentlyAt(page: Pages.home) {
45 | MenuView()
46 | .environmentObject(grid)
47 | .environmentObject(settings)
48 | .transition(AnyTransition.asymmetric(
49 | insertion: AnyTransition.opacity.combined(
50 | with: .move(edge: .leading)),
51 | removal: AnyTransition.opacity.combined(
52 | with: .move(edge: .trailing))
53 | )
54 | )
55 | } else if viewRouter.currentlyAt(page: Pages.game) {
56 | GameView()
57 | .transition(.slideAndFadeIn)
58 | .environmentObject(grid)
59 | .environmentObject(settings)
60 | .environmentObject(PauseHolder())
61 | .environmentObject(TimerHolder())
62 | } else if viewRouter.currentlyAt(page: Pages.statistics) {
63 | StatisticsView()
64 | .transition(.slideAndFadeIn)
65 | } else if viewRouter.currentlyAt(page: Pages.strategies) {
66 | StrategiesView()
67 | .transition(.slideAndFadeIn)
68 | } else if viewRouter.currentlyAt(page: Pages.settings) {
69 | SettingsView()
70 | .transition(.slideAndFadeIn)
71 | .environmentObject(settings)
72 | }
73 | }
74 | }
75 | }
76 |
77 | extension AnyTransition {
78 | static var slideAndFadeIn: AnyTransition {
79 | AnyTransition.asymmetric(
80 | insertion: AnyTransition.opacity.combined(
81 | with: .move(edge: .trailing)),
82 | removal: AnyTransition.opacity.combined(
83 | with: .move(edge: .leading)
84 | )
85 | )
86 | }
87 | }
88 |
89 | extension UIView {
90 | func asImage(rect: CGRect) -> UIImage {
91 | let renderer = UIGraphicsImageRenderer(bounds: rect)
92 | return renderer.image { rendererContext in
93 | layer.render(in: rendererContext.cgContext)
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/Sudoku/Support Views/GridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 27/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct GridView: View {
12 |
13 | @EnvironmentObject var grid: Grid
14 | @EnvironmentObject var settings: Settings
15 | @EnvironmentObject var viewRouter: ViewRouter
16 | @EnvironmentObject var pauseHolder: PauseHolder
17 |
18 | private let frameSize: CGFloat = min(Screen.cellWidth, 45) * 9
19 |
20 | @ViewBuilder
21 | var body: some View {
22 | ZStack {
23 | self.renderStructure(width: frameSize / 9)
24 | self.renderOverlayLines(width: frameSize / 9)
25 | }
26 | .disabled(self.isPaused() || grid.full())
27 | .opacity(self.isPaused() || grid.full() ? 0 : 1)
28 | .frame(width: frameSize,
29 | height: frameSize,
30 | alignment: .center)
31 | }
32 |
33 | private func renderStructure(width: CGFloat) -> some View {
34 | VStack(spacing: -1) {
35 | ForEach(0 ..< 9) { row in
36 | HStack(spacing: -1) {
37 | ForEach(0 ..< 9) { col in
38 | self.grid.render(
39 | row: row,
40 | col: col,
41 | fontSize: self.fontSize()
42 | )
43 | .frame(
44 | width: width,
45 | height: width
46 | )
47 | .border(Color.black, width: 1)
48 | .padding(.all, 0)
49 | .background(
50 | self.grid.colorAt(
51 | row: row,
52 | col: col
53 | )
54 | )
55 | .onTapGesture {
56 | self.grid.objectWillChange.send()
57 | self.grid.setActive(
58 | row: row,
59 | col: col,
60 | areas: self.settings.highlightAreas,
61 | similar: self.settings.highlightSimilar
62 | )
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | private func renderOverlayLines(width: CGFloat) -> some View {
71 | GeometryReader { geometry in
72 | Path { path in
73 | let factor: CGFloat = width * 3
74 | let lines: [CGFloat] = [1, 2]
75 |
76 | for i: CGFloat in lines {
77 | let vpos: CGFloat = i * factor
78 | path.move(to: CGPoint(x: vpos, y: 4))
79 | path.addLine(to: CGPoint(x: vpos, y: geometry.size.height - 4))
80 | }
81 |
82 | for i: CGFloat in lines {
83 | let hpos: CGFloat = i * factor
84 | path.move(to: CGPoint(x: 4, y: hpos))
85 | path.addLine(to: CGPoint(x: geometry.size.width - 4, y: hpos))
86 | }
87 | }
88 | .stroke(lineWidth: Screen.lineThickness)
89 | .foregroundColor(.black)
90 | }
91 | }
92 |
93 | func fontSize() -> CGFloat {
94 | let size: Float = self.settings.fontSize
95 | return CGFloat(size as Float)
96 | }
97 |
98 | func isPaused() -> Bool {
99 | return self.pauseHolder.isPaused()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/Sudoku.xcodeproj/xcshareddata/xcschemes/Sudoku.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/Sudoku/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 27/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 |
21 | // Use a UIHostingController as window root view controller.
22 | if let windowScene = scene as? UIWindowScene {
23 | let window = UIWindow(windowScene: windowScene)
24 |
25 | let managedObjectContent = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
26 |
27 | let rootView = RootView()
28 | .environmentObject(ViewRouter())
29 | .environment(\.managedObjectContext, managedObjectContent)
30 |
31 | window.rootViewController = UIHostingController(rootView: rootView)
32 | self.window = window
33 | window.makeKeyAndVisible()
34 | }
35 | }
36 |
37 | func sceneDidDisconnect(_ scene: UIScene) {
38 | // Called as the scene is being released by the system.
39 | // This occurs shortly after the scene enters the background, or when its session is discarded.
40 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
41 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
42 | }
43 |
44 | func sceneDidBecomeActive(_ scene: UIScene) {
45 | // Called when the scene has moved from an inactive state to an active state.
46 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
47 | }
48 |
49 | func sceneWillResignActive(_ scene: UIScene) {
50 | // Called when the scene will move from an active state to an inactive state.
51 | // This may occur due to temporary interruptions (ex. an incoming phone call).
52 | }
53 |
54 | func sceneWillEnterForeground(_ scene: UIScene) {
55 | // Called as the scene transitions from the background to the foreground.
56 | // Use this method to undo the changes made on entering the background.
57 | }
58 |
59 | func sceneDidEnterBackground(_ scene: UIScene) {
60 | // Called as the scene transitions from the foreground to the background.
61 | // Use this method to save data, release shared resources, and store enough scene-specific state information
62 | // to restore the scene back to its current state.
63 | }
64 |
65 | func logFontFamilies() {
66 | for family: String in UIFont.familyNames {
67 | print(family)
68 | for names: String in UIFont.fontNames(forFamilyName: family) {
69 | print("== \(names)")
70 | }
71 | }
72 | }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/app/Sudoku/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 27/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import CoreData
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 |
17 | // Override point for customization after application launch.
18 | UserDefaults.standard.register(defaults: [
19 | "highlightAreas": true,
20 | "highlightSimilar": false,
21 | "hideUsed": true,
22 | "enableTimer": true,
23 | "fontSize": 28.0
24 | ])
25 | return true
26 | }
27 |
28 | // MARK: UISceneSession Lifecycle
29 |
30 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
31 | // Called when a new scene session is being created.
32 | // Use this method to select a configuration to create the new scene with.
33 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
34 | }
35 |
36 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
37 | // Called when the user discards a scene session.
38 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
39 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
40 | }
41 |
42 | lazy var persistentContainer: NSPersistentContainer = {
43 | /*
44 | The persistent container for the application. This implementation
45 | creates and returns a container, having loaded the store for the
46 | application to it. This property is optional since there are legitimate
47 | error conditions that could cause the creation of the store to fail.
48 | */
49 | let container = NSPersistentContainer(name: "Sudoku")
50 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in
51 | if let error = error as NSError? {
52 | // Replace this implementation with code to handle the error appropriately.
53 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
54 |
55 | /*
56 | Typical reasons for an error here include:
57 | * The parent directory does not exist, cannot be created, or disallows writing.
58 | * The persistent store is not accessible, due to permissions or data protection when the device is locked.
59 | * The device is out of space.
60 | * The store could not be migrated to the current model version.
61 | Check the error message to determine what the actual problem was.
62 | */
63 | fatalError("Unresolved error \(error), \(error.userInfo)")
64 | }
65 | })
66 | return container
67 | }()
68 |
69 | // MARK: - Core Data Saving support
70 |
71 | func saveContext () {
72 | let context = persistentContainer.viewContext
73 | if context.hasChanges {
74 | do {
75 | try context.save()
76 | } catch {
77 | // Replace this implementation with code to handle the error appropriately.
78 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
79 | let nserror = error as NSError
80 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
81 | }
82 | }
83 | }
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/app/Sudoku/Views/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 02/11/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SettingsView: View {
12 |
13 | @EnvironmentObject var settings: Settings
14 | @EnvironmentObject var viewRouter: ViewRouter
15 |
16 | private let incrButtonHeight: CGFloat = Screen.cellWidth / 2
17 |
18 | var body: some View {
19 | ScrollView {
20 | VStack(spacing: 0) {
21 | GenericTopBarView(title: "main.settings",
22 | destination: Pages.home)
23 |
24 | self.general
25 | self.timer
26 | self.fontSize
27 | self.eraseBoard
28 |
29 | Spacer()
30 | }
31 | }
32 | }
33 |
34 | var general: some View {
35 | Section {
36 | VStack(alignment: .leading) {
37 | Toggle(isOn: $settings.highlightAreas) {
38 | Image(systemName: "square.stack.fill")
39 | Text("settings.areas")
40 | .font(.custom("CaviarDreams-Bold", size: 20))
41 | }
42 |
43 | Text("settings.areas.descript")
44 | .font(.custom("CaviarDreams-Bold", size: 12))
45 | .foregroundColor(Color(.systemGray))
46 | }
47 |
48 | VStack(alignment: .leading) {
49 | Toggle(isOn: $settings.highlightSimilar) {
50 | Image(systemName: "square.fill.on.square.fill")
51 | Text("settings.twins")
52 | .font(.custom("CaviarDreams-Bold", size: 20))
53 | }
54 |
55 | Text("settings.twins.descript")
56 | .font(.custom("CaviarDreams-Bold", size: 12))
57 | .foregroundColor(Color(.systemGray))
58 | }
59 |
60 | VStack(alignment: .leading) {
61 | Toggle(isOn: $settings.hideUsed) {
62 | Image(systemName: "eye.slash.fill")
63 | Text("settings.used")
64 | .font(.custom("CaviarDreams-Bold", size: 20))
65 | }
66 |
67 | Text("settings.used.descript")
68 | .font(.custom("CaviarDreams-Bold", size: 12))
69 | .foregroundColor(Color(.systemGray))
70 | }
71 | }
72 | .padding(.top)
73 | .padding(.leading)
74 | .padding(.trailing)
75 | }
76 |
77 | var timer: some View {
78 | VStack(alignment: .leading) {
79 | Toggle(isOn: $settings.enableTimer) {
80 | Image(systemName: "timer")
81 | Text("settings.timer")
82 | .font(.custom("CaviarDreams-Bold", size: 20))
83 | }
84 | }
85 | .padding(.top, 60)
86 | .padding(.leading)
87 | .padding(.trailing)
88 | }
89 |
90 | var fontSize: some View {
91 | HStack {
92 | Image(systemName: "textformat.size")
93 | Text("settings.text.size")
94 | .font(.custom("CaviarDreams-Bold", size: 20))
95 |
96 | Spacer()
97 | Button(
98 | action: {
99 | if self.canDecrement() {
100 | self.settings.fontSize -= 1
101 | }
102 | },
103 | label: {
104 | Text("-")
105 | .font(.custom("CaviarDreams-Bold", size: 20))
106 | }
107 | )
108 | .frame(width: 40,
109 | height: 40)
110 | .background(Color(UIColor.label))
111 | .foregroundColor(Color(UIColor.systemBackground))
112 | .cornerRadius(40)
113 | .padding(.all, 7)
114 |
115 | Text(String(format: "%02.0f", self.settings.fontSize))
116 | .font(.custom("CaviarDreams-Bold", size: 20))
117 | .padding(.trailing)
118 | .padding(.leading)
119 | Button(
120 | action: {
121 | if self.canIncrement() {
122 | self.settings.fontSize += 1
123 | }
124 | },
125 | label: {
126 | Text("+")
127 | .font(.custom("CaviarDreams-Bold", size: 20))
128 | }
129 | )
130 | .frame(width: 40,
131 | height: 40)
132 | .background(Color(UIColor.label))
133 | .foregroundColor(Color(UIColor.systemBackground))
134 | .cornerRadius(40)
135 | .padding(.all, 7)
136 | }
137 | .padding(.top, 60)
138 | .padding(.leading)
139 | .padding(.trailing)
140 | }
141 |
142 | var eraseBoard: some View {
143 | HStack {
144 | Image(systemName: "trash.fill")
145 | Text("Erase current board")
146 | .font(.custom("CaviarDreams-Bold", size: 20))
147 |
148 | Spacer()
149 | Button(
150 | action: {
151 | UserDefaults.standard.set(nil,
152 | forKey: "savedBoard")
153 | UserDefaults.standard.set(nil,
154 | forKey: "time")
155 | },
156 | label: {
157 | Text("Erase")
158 | .frame(width: 100,
159 | height: 50)
160 | .font(.custom("CaviarDreams-Bold", size: 20))
161 | .background(Color(UIColor.label))
162 | .foregroundColor(Color(UIColor.systemBackground))
163 | .cornerRadius(40)
164 | }
165 | )
166 | }
167 | .padding(.top, 60)
168 | .padding(.leading)
169 | .padding(.trailing)
170 | }
171 |
172 | func canIncrement() -> Bool {
173 | let factor: Float = 0.9
174 | let size: Float = self.settings.fontSize
175 | let fontmax: Float = min(Float(Screen.cellWidth), 45) * factor
176 | return size < fontmax
177 | }
178 |
179 | func canDecrement() -> Bool {
180 | let factor: Float = 0.5
181 | let size: Float = self.settings.fontSize
182 | let fontmin: Float = min(Float(Screen.cellWidth), 45) * factor
183 | return size > fontmin
184 | }
185 | }
186 |
187 | struct SettingsView_Previews: PreviewProvider {
188 | static var previews: some View {
189 | SettingsView()
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/app/Sudoku/Support Views/KeyboardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 30/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct KeyboardView: View {
13 |
14 | @Binding var alertText: String
15 | @Binding var displayAlert: Bool
16 |
17 | @EnvironmentObject var grid: Grid
18 | @EnvironmentObject var settings: Settings
19 | @EnvironmentObject var pauseHolder: PauseHolder
20 |
21 | @State private var task: DispatchWorkItem = DispatchWorkItem { }
22 |
23 | var body: some View {
24 | VStack {
25 | optionsRow
26 | numbersRow
27 | }
28 | .blur(radius: self.blurRadius())
29 | .opacity(self.opacity())
30 | .disabled(self.disabled())
31 | .animation(.spring())
32 | .frame(maxWidth: 500.0)
33 | }
34 |
35 | let buttonSize: CGFloat = min(Screen.cellWidth / 2, 22.5)
36 |
37 | var optionsRow: some View {
38 | HStack {
39 | Spacer()
40 |
41 | // Undo Button
42 | Button(
43 | action: {
44 | // TODO
45 | },
46 | label: {
47 | VStack {
48 | Image(systemName: "gobackward")
49 | .frame(width: buttonSize)
50 | //.foregroundColor(Color(.systemGray))
51 | .foregroundColor(Colors.Golden)
52 | Text("keyboard.undo")
53 | .foregroundColor(Color(.label))
54 | .font(.custom("CaviarDreams-Bold",
55 | size: buttonSize))
56 | }
57 | }
58 | )
59 | Spacer()
60 |
61 | // Delete Button
62 | Button(
63 | action: {
64 | self.insert(value: 0, alertText: "alert.default.remove")
65 | },
66 | label: {
67 | VStack {
68 | Image(systemName: "xmark.circle")
69 | .frame(width: buttonSize)
70 | //.foregroundColor(Color(.systemGray))
71 | .foregroundColor(Colors.Golden)
72 | Text("keyboard.delete")
73 | .foregroundColor(Color(.label))
74 | .font(.custom("CaviarDreams-Bold",
75 | size: buttonSize))
76 | }
77 | }
78 | )
79 | Spacer()
80 |
81 | // Notes Button
82 | Button(
83 | action: {
84 | // TODO
85 | },
86 | label: {
87 | VStack {
88 | Image(systemName: "square.and.pencil")
89 | .frame(width: buttonSize)
90 | .foregroundColor(Colors.Golden)
91 | //.foregroundColor(Color(.systemGray))
92 | Text("keyboard.notes")
93 | .foregroundColor(Color(.label))
94 | .font(.custom("CaviarDreams-Bold",
95 | size: buttonSize))
96 | }
97 | }
98 | )
99 | Spacer()
100 |
101 | // Sugestion Button
102 | Button(
103 | action: {
104 | // TODO
105 | },
106 | label: {
107 | VStack {
108 | Image(systemName: "lightbulb")
109 | .frame(width: buttonSize)
110 | .foregroundColor(Colors.Golden)
111 | //.foregroundColor(Color(.systemGray))
112 | Text("keyboard.sugestion")
113 | .foregroundColor(Color(.label))
114 | .font(.custom("CaviarDreams-Bold",
115 | size: buttonSize))
116 | }
117 | }
118 | )
119 | Spacer()
120 | }
121 | .padding(.top)
122 | .padding(.bottom)
123 | }
124 |
125 | var numbersRow: some View {
126 | HStack {
127 | ForEach(1 ..< 10) { number in
128 | if self.displayNumber(number: number) {
129 | Spacer()
130 | Button(
131 | action: {
132 | self.insert(value: number,
133 | alertText: "alert.default.overwrite")
134 | },
135 | label: {
136 | Text("\(number)")
137 | .foregroundColor(Color(.label))
138 | .font(.custom("CaviarDreams-Bold",
139 | size: min(Screen.cellWidth, 45)))
140 | }
141 | )
142 | }
143 | }
144 | Spacer()
145 | }
146 | .padding(.top)
147 | .padding(.bottom)
148 | }
149 |
150 | func insert(value: Int, alertText: String) -> Void {
151 | guard let active: [Int] = self.grid.getActive()
152 | else {
153 | return
154 | }
155 |
156 | if self.grid.setValue(row: active[0],
157 | col: active[1],
158 | value: value) == false {
159 | displayError(alertText: alertText)
160 | } else {
161 | self.grid.objectWillChange.send()
162 | }
163 | }
164 |
165 | func displayError(alertText: String) -> Void {
166 | // setup delayed action
167 | self.task.cancel()
168 | self.task = DispatchWorkItem {
169 | self.displayAlert = false
170 | }
171 |
172 | // display error message
173 | self.alertText = alertText
174 | self.displayAlert = true
175 | DispatchQueue.main.asyncAfter(
176 | deadline: DispatchTime.now() + 2,
177 | execute: self.task
178 | )
179 |
180 | // haptic feedback
181 | let generator: UINotificationFeedbackGenerator
182 | = UINotificationFeedbackGenerator()
183 | generator.notificationOccurred(.error)
184 | }
185 |
186 | func displayNumber(number: Int) -> Bool {
187 | return !self.settings.hideUsed
188 | || (self.grid.getNumberFrequency()[number - 1] < 9
189 | && self.settings.hideUsed)
190 | }
191 |
192 | func isPaused() -> Bool {
193 | return self.pauseHolder.isPaused()
194 | }
195 |
196 | func blurRadius() -> CGFloat {
197 | return self.isPaused() || self.grid.full() ? 5 : 0
198 | }
199 |
200 | func opacity() -> Double {
201 | return self.isPaused() || self.grid.full() ? 0.7 : 1
202 | }
203 |
204 | func disabled() -> Bool {
205 | return self.isPaused() || self.grid.full()
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/app/Sudoku/Views/GameView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GameView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 29/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct GameView: View {
13 |
14 | @State private var displayAlert: Bool = false
15 | @State private var alertText: String = String()
16 |
17 | @EnvironmentObject var grid: Grid
18 | @EnvironmentObject var settings: Settings
19 | @EnvironmentObject var viewRouter: ViewRouter
20 | @EnvironmentObject var pauseHolder: PauseHolder
21 | @EnvironmentObject var timerHolder: TimerHolder
22 |
23 | private let labelSize: CGFloat = 15.0
24 | private let pauseButtonSize: CGFloat = min(Screen.cellWidth / 3, 15)
25 |
26 | var body: some View {
27 | VStack(spacing: 0) {
28 | self.topbar
29 | self.screen
30 | }
31 | }
32 |
33 | @ViewBuilder
34 | private var screen: some View {
35 | VStack(spacing: 0) {
36 |
37 | self.infoBar
38 | .padding(.top)
39 | .padding(.leading)
40 | .padding(.trailing)
41 |
42 | ZStack {
43 | GridView()
44 | .environmentObject(pauseHolder)
45 |
46 | VStack {
47 | Text("banners.pause.title")
48 | .font(.custom("CaviarDreams-Bold", size: 50))
49 | Text("banners.pause.stats: \(self.grid.completion())")
50 | .font(.custom("CaviarDreams-Bold", size: 20))
51 | }
52 | .foregroundColor(Color(.label))
53 | .shadow(radius: 10)
54 | .opacity(pauseHolder.isPaused() ? 1 : 0)
55 | .animation(.spring())
56 |
57 | VStack {
58 | Text("banners.congrats.title")
59 | .font(.custom("CaviarDreams-Bold", size: 50))
60 | Text("banners.congrats.stats: \(self.grid.getErrorCount())")
61 | .font(.custom("CaviarDreams-Bold", size: 20))
62 |
63 | Button(
64 | action: {
65 | withAnimation(.easeIn) {
66 | UserDefaults.standard.set(nil,
67 | forKey: "savedBoard")
68 | UserDefaults.standard.set(nil,
69 | forKey: "time")
70 | self.viewRouter.setCurrentPage(page: Pages.home)
71 | }
72 | },
73 | label: {
74 | HStack {
75 | Spacer()
76 | Text("button.leave")
77 | .font(.custom("CaviarDreams-Bold", size: 20))
78 | Spacer()
79 | }
80 | }
81 | )
82 | .frame(width: Screen.width * 0.55,
83 | height: 50)
84 | .background(Colors.MatteBlack)
85 | .cornerRadius(40)
86 | .padding(.all, 7)
87 | .foregroundColor(.white)
88 | .shadow(radius: 20)
89 | .padding(.top, 20)
90 | }
91 | .shadow(radius: 10)
92 | .opacity(self.grid.full() ? 1 : 0)
93 | .animation(.spring())
94 |
95 | Text(LocalizedStringKey(alertText))
96 | .font(.custom("CaviarDreams-Bold", size: 15))
97 | .padding()
98 | .background(Colors.LightBlue)
99 | .cornerRadius(20)
100 | .overlay(RoundedRectangle(cornerRadius: 20)
101 | .stroke(Color(.systemGray), lineWidth: 2))
102 | .shadow(radius: 10)
103 | .blur(radius: displayAlert ? 0 : 50)
104 | .opacity(displayAlert ? 1 : 0)
105 | .animation(.spring())
106 | .onTapGesture {
107 | self.displayAlert = false
108 | }
109 | }
110 |
111 | Spacer()
112 | KeyboardView(alertText: $alertText, displayAlert: $displayAlert)
113 | .environmentObject(pauseHolder)
114 | Spacer()
115 | }
116 | }
117 |
118 | private var infoBar: some View {
119 | HStack {
120 | Text("game.errors: \(grid.getErrorCount())")
121 | .font(.custom("CaviarDreams-Bold", size: min(Screen.cellWidth / 3, 15)))
122 |
123 | Spacer()
124 |
125 | if self.settings.enableTimer == true {
126 | TimerView()
127 | .environmentObject(pauseHolder)
128 | .environmentObject(timerHolder)
129 | }
130 | }
131 | .blur(radius: self.pauseHolder.isPaused() || self.grid.full() ? 5 : 0)
132 | .animation(.spring())
133 | }
134 |
135 | private var topbar: some View {
136 | HStack {
137 | self.backButton
138 | Spacer()
139 | self.pauseButton
140 | }
141 | .padding(.top)
142 | .padding(.leading)
143 | .padding(.trailing)
144 | }
145 |
146 | private var backButton: some View {
147 | Button(
148 | action: {
149 | withAnimation(.easeIn) {
150 | UserDefaults.standard.set(self.grid.toString(),
151 | forKey: "savedBoard")
152 | UserDefaults.standard.set(self.grid.getDifficulty(),
153 | forKey: "savedDifficulty")
154 | self.timerHolder.stop()
155 | self.viewRouter.setCurrentPage(page: Pages.home)
156 | }
157 | },
158 | label: {
159 | Image(systemName: "chevron.backward")
160 | Text("main.back")
161 | .font(.custom("CaviarDreams-Bold", size: labelSize))
162 | }
163 | )
164 | .foregroundColor(Color(.label))
165 | }
166 |
167 | @ViewBuilder
168 | private var pauseButton: some View {
169 | if self.settings.enableTimer == true {
170 | Button(
171 | action: {
172 | withAnimation {
173 | self.pauseHolder.toggle()
174 |
175 | if self.pauseHolder.isPaused() {
176 | self.timerHolder.stop()
177 | } else {
178 | self.timerHolder.start()
179 | }
180 | }
181 | },
182 | label: {
183 | Image(systemName: self.pauseIconName())
184 | .resizable()
185 | .frame(width: self.pauseButtonSize,
186 | height: self.pauseButtonSize)
187 | }
188 | )
189 | .foregroundColor(Color(.label))
190 | }
191 | }
192 |
193 | func opacity() -> Double {
194 | return grid.full() ? 0.0 : 1.0
195 | }
196 |
197 | func pauseIconName() -> String {
198 | return self.pauseHolder.paused ? "play.fill" : "pause"
199 | }
200 |
201 | func safeAreaHeight() -> CGFloat {
202 | let window = UIApplication.shared.windows[0]
203 | let safeFrame = window.safeAreaLayoutGuide.layoutFrame
204 | return safeFrame.minY
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/app/Sudoku/Views/MenuView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuView.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 27/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct MenuView: View {
12 |
13 | @State private var generating: Bool = false
14 | @State private var displayWarning: Bool = false
15 |
16 | @EnvironmentObject var grid: Grid
17 | @EnvironmentObject var viewRouter: ViewRouter
18 |
19 | private let popupWidth: CGFloat = Screen.width * 0.65
20 | private let popupHeight: CGFloat = Screen.height * 0.35
21 | private let popupPadding: CGFloat = Screen.height * 0.10
22 |
23 | var body: some View {
24 | ZStack {
25 | GeometryReader { geometry in
26 | if geometry.size.width > 1.5 * geometry.size.height {
27 | VStack {
28 | Spacer()
29 |
30 | HStack {
31 | Spacer()
32 |
33 | VStack {
34 | Text("Sudoku")
35 | .font(.custom("CaviarDreams-Bold", size: 80))
36 | .foregroundColor(Color(.label))
37 | .shadow(radius: 10)
38 |
39 | if self.activeBoard() {
40 | ContinueButtonView(width: geometry.size.width)
41 | }
42 | }
43 | .opacity(self.groupOpacity())
44 |
45 | Spacer()
46 |
47 | self.buttons(width: geometry.size.width)
48 |
49 | Spacer()
50 | }
51 | .shadow(radius: 5)
52 |
53 | Spacer()
54 | }
55 | }
56 | else {
57 | HStack {
58 | Spacer()
59 |
60 | VStack {
61 | Spacer()
62 |
63 | Text("Sudoku")
64 | .font(.custom("CaviarDreams-Bold", size: 80))
65 | .foregroundColor(Color(.label))
66 | .shadow(radius: 10)
67 |
68 | if self.activeBoard() {
69 | ContinueButtonView(width: geometry.size.width)
70 | }
71 | self.buttons(width: geometry.size.width)
72 |
73 | Spacer()
74 | }
75 | .opacity(self.groupOpacity())
76 | .shadow(radius: 5)
77 |
78 | Spacer()
79 | }
80 | }
81 | }
82 |
83 | VStack {
84 | Spacer()
85 |
86 | ZStack {
87 | VStack {
88 | Spacer()
89 |
90 | Group {
91 | Text("alert.progress.title")
92 | .font(.custom("CaviarDreams-Bold", size: 15))
93 | .lineLimit(nil)
94 | .padding(.leading)
95 | .padding(.trailing)
96 |
97 | Spacer()
98 |
99 | Text("alert.progress.continue")
100 | .font(.custom("CaviarDreams-Bold", size: 15))
101 | .padding(.leading)
102 | .padding(.trailing)
103 |
104 | HStack {
105 | Spacer()
106 | Button(
107 | action: {
108 | withAnimation {
109 | self.displayWarning = false
110 | }
111 | },
112 | label: {
113 | Image(systemName: "xmark.circle.fill")
114 | .foregroundColor(Color(.systemRed))
115 | Text("alert.progress.no")
116 | .font(.custom("CaviarDreams-Bold", size: 20))
117 | }
118 | )
119 |
120 | Spacer()
121 | Button(
122 | action: {
123 | withAnimation {
124 | UserDefaults.standard.set(nil,
125 | forKey: "savedBoard")
126 | UserDefaults.standard.set(nil,
127 | forKey: "time")
128 | self.generating = true
129 | self.execute()
130 | }
131 | },
132 | label: {
133 | Image(systemName: "checkmark.circle.fill")
134 | .foregroundColor(Color(.systemGreen))
135 | Text("alert.progress.yes")
136 | .font(.custom("CaviarDreams-Bold", size: 20))
137 | }
138 | )
139 | Spacer()
140 | }
141 | }
142 | .opacity(controlsOpacity())
143 | .animation(.spring())
144 |
145 | Spacer()
146 | }
147 | .frame(
148 | width: popupWidth,
149 | height: popupHeight
150 | )
151 | .background(Color(UIColor.systemBackground))
152 | .foregroundColor(Color(UIColor.label))
153 | .cornerRadius(40)
154 | .shadow(radius: 10)
155 | .blur(radius: popupBlur())
156 | .opacity(popupOpacity())
157 | .animation(.spring())
158 |
159 |
160 | ActivityIndicator()
161 | .frame(width: 200, height: 200)
162 | .foregroundColor(spinnerColor())
163 | .opacity(spinnerOpacity())
164 | }
165 | Spacer()
166 | }
167 | .padding(.top, popupPadding)
168 | }
169 | }
170 |
171 | func buttons(width: CGFloat) -> some View {
172 | VStack {
173 | Group {
174 | PlayButtonView(width: width,
175 | generating: self.$generating,
176 | displayWarning: self.$displayWarning)
177 | .modifier(DefaultButton(width: width))
178 | HomeButtonView(label: "main.stats",
179 | imageName: "chart.bar.fill",
180 | page: Pages.statistics,
181 | width: width)
182 | .modifier(DefaultButton(width: width))
183 | HomeButtonView(label: "main.strategies",
184 | imageName: "lightbulb.fill",
185 | page: Pages.strategies,
186 | width: width)
187 | .modifier(DefaultButton(width: width))
188 | HomeButtonView(label: "main.settings",
189 | imageName: "gear",
190 | page: Pages.settings,
191 | width: width)
192 | .modifier(DefaultButton(width: width))
193 | }
194 | .opacity(self.groupOpacity())
195 | }
196 | }
197 |
198 | func activeBoard() -> Bool {
199 | return UserDefaults.standard.string(forKey: "savedBoard") != nil
200 | }
201 |
202 | func groupOpacity() -> Double {
203 | return self.displayWarning || self.generating ? 0 : 1
204 | }
205 |
206 | func popupOpacity() -> Double {
207 | return self.displayWarning ? 1 : 0
208 | }
209 |
210 | func popupBlur() -> CGFloat {
211 | return self.displayWarning ? 0 : 50
212 | }
213 |
214 | func controlsOpacity() -> Double {
215 | return self.generating ? 0 : 1
216 | }
217 |
218 | func spinnerOpacity() -> Double {
219 | return self.generating ? 1 : 0
220 | }
221 |
222 | func spinnerColor() -> Color {
223 | return self.displayWarning ? Colors.LightBlue : Colors.MatteBlack
224 | }
225 |
226 | func execute() -> Void {
227 | DispatchQueue.main.async {
228 | self.grid.reset()
229 | self.grid.generate()
230 | self.generating = false
231 | self.viewRouter.setCurrentPage(page: Pages.game)
232 | self.displayWarning = false
233 | }
234 | }
235 | }
236 |
237 | struct HomeButtonView: View {
238 |
239 | @EnvironmentObject var viewRouter: ViewRouter
240 |
241 | private let page: Int!
242 | private let label: String!
243 | private let imageName: String!
244 | private let buttonWidth: CGFloat!
245 | private let labelOffset: CGFloat!
246 | private let iconPosition: [CGFloat]!
247 |
248 | init (label: String, imageName: String, page: Int, width: CGFloat) {
249 | self.label = label
250 | self.imageName = imageName
251 | self.page = page
252 | self.buttonWidth = min(width * 0.55, 350.0)
253 | self.labelOffset = self.buttonWidth * 0.35
254 | self.iconPosition = [self.buttonWidth * 0.2, 25]
255 | }
256 |
257 | var body: some View {
258 | Button(
259 | action: {
260 | withAnimation {
261 | self.viewRouter.setCurrentPage(page: self.page)
262 | }
263 | },
264 | label: {
265 | ZStack(alignment: .leading) {
266 | Image(systemName: self.imageName)
267 | .position(x: self.iconPosition[0],
268 | y: self.iconPosition[1])
269 | .foregroundColor(Color(UIColor.systemBackground))
270 | Text(LocalizedStringKey(self.label))
271 | .font(.custom("CaviarDreams-Bold", size: 20))
272 | .offset(x: self.labelOffset)
273 | }
274 | }
275 | )
276 | }
277 | }
278 |
279 | struct ContinueButtonView: View {
280 |
281 | @EnvironmentObject var grid: Grid
282 | @EnvironmentObject var viewRouter: ViewRouter
283 | @EnvironmentObject var settings: Settings
284 |
285 | private var buttonWidth: CGFloat
286 | private var frameSize: [CGFloat]
287 | private var labelOffset: CGFloat
288 | private var iconPosition: [CGFloat]
289 | private var savedDifficulty: String
290 | private var savedTime: Int
291 |
292 | init(width: CGFloat) {
293 | self.buttonWidth = min(width * 0.55, 350.0)
294 | self.frameSize = [self.buttonWidth, 50]
295 | self.labelOffset = self.buttonWidth * 0.35
296 | self.iconPosition = [self.buttonWidth * 0.2, 25]
297 |
298 | if UserDefaults.standard.object(forKey: "time") != nil {
299 | self.savedTime = UserDefaults.standard.integer(forKey: "time")
300 | }
301 | else {
302 | self.savedTime = 0
303 | }
304 |
305 | if UserDefaults.standard.object(forKey: "time") != nil {
306 | self.savedTime = UserDefaults.standard.integer(forKey: "time")
307 | }
308 | else {
309 | self.savedTime = 0
310 | }
311 |
312 | self.savedDifficulty = UserDefaults.standard.string(forKey: "savedDifficulty") ?? "NULL"
313 | }
314 |
315 | var body: some View {
316 | Button(
317 | action: {
318 | withAnimation {
319 | if let board: String = UserDefaults.standard.string(forKey: "savedBoard"),
320 | let difficulty: String = UserDefaults.standard.string(forKey: "savedDifficulty") {
321 | self.grid.reset()
322 | self.grid.load(puzzle: board, difficulty: difficulty)
323 | self.viewRouter.setCurrentPage(page: Pages.game)
324 | }
325 | }
326 | },
327 | label: {
328 |
329 | ZStack(alignment: .leading) {
330 | Image(systemName: "hourglass.bottomhalf.fill")
331 | .position(x: self.iconPosition[0],
332 | y: self.iconPosition[1])
333 |
334 | VStack() {
335 | HStack {
336 | Text("main.resume")
337 | .font(.custom("CaviarDreams-Bold", size: 17))
338 | }
339 | .offset(x: self.labelOffset)
340 |
341 | if self.settings.enableTimer == true {
342 | HStack {
343 | Text(String(format: "%@%02d:%02d - %@", arguments: [
344 | self.hours(), self.minutes(), self.sec(), self.savedDifficulty
345 | ]))
346 | .font(.custom("CaviarDreams-Bold", size: 14))
347 | }
348 | .opacity(0.75)
349 | .offset(x: self.labelOffset)
350 | }
351 | }
352 | }
353 | }
354 | )
355 | .frame(width: self.frameSize[0],
356 | height: self.frameSize[1],
357 | alignment: .leading)
358 | .background(Colors.LightBlue)
359 | .foregroundColor(Color(.label))
360 | .cornerRadius(40)
361 | .padding(.all, 7)
362 | .shadow(radius: 20)
363 | }
364 |
365 | func sec() -> Int {
366 | return self.savedTime % 60
367 | }
368 |
369 | func minutes () -> Int {
370 | return (self.savedTime / 60) % 60
371 | }
372 |
373 | func hours() -> String {
374 | if self.savedTime >= 3600 {
375 | return String(format: "%02d:", (self.savedTime / 3600))
376 | }
377 | return String()
378 | }
379 | }
380 |
381 | struct PlayButtonView: View {
382 |
383 | @Binding var generating: Bool
384 | @Binding var displayWarning: Bool
385 |
386 | @EnvironmentObject var grid: Grid
387 | @EnvironmentObject var viewRouter: ViewRouter
388 |
389 | private var buttonWidth: CGFloat
390 | private var labelOffset: CGFloat
391 | private var iconPosition: [CGFloat]
392 |
393 | init(width: CGFloat,
394 | generating: Binding, displayWarning: Binding) {
395 |
396 | self._generating = generating
397 | self._displayWarning = displayWarning
398 | self.buttonWidth = min(width * 0.55, 350.0)
399 | self.labelOffset = self.buttonWidth * 0.35
400 | self.iconPosition = [self.buttonWidth * 0.2, 25]
401 | }
402 |
403 | var body: some View {
404 | Button(
405 | action: {
406 | withAnimation {
407 | if self.activeBoard() {
408 | self.displayWarning = true
409 | } else {
410 | UserDefaults.standard.set(nil,
411 | forKey: "savedBoard")
412 | UserDefaults.standard.set(nil,
413 | forKey: "time")
414 | self.generating = true
415 | self.execute()
416 | }
417 | }
418 | },
419 | label: {
420 | ZStack(alignment: .leading) {
421 | Image(systemName: "gamecontroller.fill")
422 | .position(x: self.iconPosition[0],
423 | y: self.iconPosition[1])
424 | Text("main.new")
425 | .font(.custom("CaviarDreams-Bold", size: 20))
426 | .offset(x: self.labelOffset)
427 | }
428 | }
429 | )
430 | }
431 |
432 | func activeBoard() -> Bool {
433 | return UserDefaults.standard.string(forKey: "savedBoard") != nil
434 | }
435 |
436 | func execute() -> Void {
437 | DispatchQueue.main.async {
438 | self.grid.reset()
439 | self.grid.generate()
440 | self.generating = false
441 | self.viewRouter.setCurrentPage(page: Pages.game)
442 | }
443 | }
444 | }
445 |
446 | struct DefaultButton: ViewModifier {
447 | private var buttonWidth: CGFloat
448 |
449 | init(width: CGFloat) {
450 | self.buttonWidth = min(width * 0.55, 350.0)
451 | }
452 |
453 | func body(content: Content) -> some View {
454 | content
455 | .frame(width: buttonWidth,
456 | height: 50,
457 | alignment: .leading)
458 | .background(Color(UIColor.label))
459 | .cornerRadius(40)
460 | .padding(.all, 7)
461 | .foregroundColor(Color(UIColor.systemBackground))
462 | .shadow(radius: 20)
463 | }
464 | }
465 |
466 | struct ActivityIndicator: View {
467 |
468 | @State private var isAnimating: Bool = false
469 |
470 | var body: some View {
471 | GeometryReader { (geometry: GeometryProxy) in
472 | ForEach(0 ..< 5) { index in
473 | Group {
474 | Circle()
475 | .frame(width: geometry.size.width / 5,
476 | height: geometry.size.height / 5)
477 | .scaleEffect(self.scaleEffect(index: index))
478 | .offset(y: self.offset(geometry: geometry))
479 | }
480 | .frame(width: geometry.size.width, height: geometry.size.height)
481 | .rotationEffect(self.rotation())
482 | .animation(Animation
483 | .timingCurve(0.5, 0.15 + Double(index) / 5, 0.25, 1, duration: 1.5)
484 | .repeatForever(autoreverses: false)
485 | )
486 | }
487 | }
488 | .aspectRatio(1, contentMode: .fit)
489 | .onAppear {
490 | self.isAnimating = true
491 | }
492 | }
493 |
494 | func scaleEffect(index: Int) -> CGFloat {
495 | let animatedEffect: CGFloat = 1 - CGFloat(index) / 5
496 | let frozenEffect: CGFloat = 0.2 + CGFloat(index) / 5
497 | return !self.isAnimating ? animatedEffect : frozenEffect
498 | }
499 |
500 | func offset(geometry: GeometryProxy) -> CGFloat {
501 | return geometry.size.width / 10 - geometry.size.height / 2
502 | }
503 |
504 | func rotation() -> Angle {
505 | return !self.isAnimating ? .degrees(0) : .degrees(360)
506 | }
507 | }
508 |
--------------------------------------------------------------------------------
/generator/generate.swift:
--------------------------------------------------------------------------------
1 | let UNDEFINED: Int = 0
2 | var grid: [[Int]] = Array(repeating: Array(repeating: UNDEFINED, count: 9),
3 | count: 9)
4 | var solution: [[Int]] = Array(repeating: Array(repeating: UNDEFINED, count: 9),
5 | count: 9)
6 | var tokenFrequency: [Int] = Array(repeating: 0,
7 | count: 9)
8 | var solutions: Int = 0
9 |
10 | let easy: String = "000105000140000670080002400063070010900000003010090520007200080026000035000409000"
11 | let hard: String = "000000000000003085001020000000507000004000100090000000500000073002010000000040009"
12 | let moderate: String = "720096003000205000080004020000000060106503807040000000030800090000702000200430018"
13 |
14 | let puzzles: [String] = [easy, moderate, hard]
15 |
16 | /*============================================================================
17 | Make up a Sudoku puzzle.
18 | ------------------------
19 | generate():
20 | fill() -> fill the board with a valid sudoku puzzle.
21 | randomize() -> assign each token to a random number.
22 | remove_numbers() -> removes clues to create a playable board.
23 | ==========================================================================*/
24 |
25 | func generate() -> Void {
26 | fill()
27 | randomize()
28 | removeNumbers()
29 | //computeTokenFrequency()
30 | }
31 |
32 |
33 | @discardableResult func fill() -> Bool {
34 | var tokens: [String] = ["a", "b", "c", "d", "e", "f", "g", "h", "i"]
35 | var row: Int = 0
36 | var col: Int = 0
37 |
38 | for i in (0 ..< 81) {
39 | row = i / 9
40 | col = i % 9
41 |
42 | if grid[row][col] == UNDEFINED {
43 | tokens = tokens.shuffled()
44 |
45 | for token in tokens {
46 | let token2Int: Int = Int(Unicode.Scalar(token)!.value)
47 |
48 | if possible(token: token2Int, row: row, col: col) {
49 | grid[row][col] = token2Int
50 | // self.inputType[row][col] = InputType.system
51 |
52 | if full() {
53 | return true
54 | }
55 | else if fill() {
56 | return true
57 | }
58 | }
59 | }
60 | break
61 | }
62 | }
63 | grid[row][col] = UNDEFINED
64 | return false
65 | }
66 |
67 |
68 | func randomize() -> Void {
69 | var tokens: [String] = ["a", "b", "c", "d", "e", "f", "g", "h", "i"]
70 | tokens = tokens.shuffled()
71 |
72 | for i in (0 ..< 81) {
73 | let row: Int = i / 9
74 | let col: Int = i % 9
75 | let int2Token: String = String(Character(UnicodeScalar(grid[row][col])!))
76 | grid[row][col] = tokens.firstIndex(of: int2Token)! + 1
77 | }
78 | }
79 |
80 |
81 | func removeNumbers() -> Void {
82 | var removed: Int = 0
83 |
84 | // Make a list of all 81 cell positions and shuffle it randomly.
85 | var pos: [Int] = Array(0 ..< 81)
86 | pos = pos.shuffled()
87 |
88 | // As long as the list is not empty,
89 | // take the next position from the list
90 | // and remove the number from the related cell.
91 |
92 | while pos.count > 0 {
93 | let nextpos: Int = pos.removeFirst()
94 | let row: Int = nextpos / 9
95 | let col: Int = nextpos % 9
96 |
97 | let prev: [[Int]] = grid
98 |
99 | grid[row][col] = UNDEFINED
100 | let solutions: Int = solve()
101 |
102 | grid = prev
103 |
104 | if (solutions != 1) { continue }
105 |
106 | // self.inputType[row][col] = InputType.user
107 | grid[row][col] = UNDEFINED
108 | removed += 1
109 | }
110 |
111 | print("Clues + Solved:", 81 - removed, "/ 81")
112 | }
113 |
114 |
115 | func computeTokenFrequency() -> Void {
116 | for row in grid {
117 | for val in row {
118 | if val != UNDEFINED {
119 | tokenFrequency[val - 1] += 1
120 | }
121 | }
122 | }
123 | }
124 |
125 | /*==============================================================================
126 | Solve a Sudoku puzzle.
127 | ==========================================================================*/
128 |
129 | func solve() -> Int {
130 | solutions = 0
131 |
132 | basicTechniques()
133 | if full() {
134 | solutions = 1
135 | }
136 | else {
137 | backtrack(prev: -1, pos: nextEmptyPos(ref: -1))
138 | }
139 |
140 | return solutions
141 | }
142 |
143 |
144 | func basicTechniques() -> Void {
145 | func nakedSingles(possibles: [[[Int]]]) -> Int {
146 | var solved: Int = 0
147 |
148 | for row in (0 ..< 9) {
149 | for col in (0 ..< 9) {
150 | if possibles[row][col].count == 1 {
151 | solved += 1
152 | grid[row][col] = possibles[row][col][0]
153 | }
154 | }
155 | }
156 | return solved
157 | }
158 |
159 | func hiddenSingles(possibles: [[[Int]]]) -> Int {
160 |
161 | func uniqueRow(row: Int, value: Int, possibles: [[Int]]) -> Bool {
162 | var count: Int = 0
163 | var index: Int = 0
164 |
165 | for col in (0 ..< 9) {
166 | for inner in (0 ..< possibles[col].count) {
167 | if possibles[col][inner] == value {
168 | count += 1
169 | index = col
170 | }
171 | }
172 | }
173 |
174 | if count == 1 {
175 | grid[row][index] = value
176 | }
177 | return count == 1
178 | }
179 |
180 | func uniqueCol(col: Int, value: Int, possibles: [[Int]]) -> Bool {
181 | var count: Int = 0
182 | var index: Int = 0
183 |
184 | for row in (0 ..< 9) {
185 | for inner in (0 ..< possibles[row].count) {
186 | if possibles[row][inner] == value {
187 | count += 1
188 | index = row
189 | }
190 | }
191 | }
192 |
193 | if count == 1 {
194 | grid[index][col] = value
195 | }
196 | return count == 1
197 | }
198 |
199 | func uniqueSquare(row: Int, col: Int, value: Int, possibles: [[[Int]]]) -> Bool {
200 | var square: [[Int]] = [[Int]]()
201 | for i in (row ..< row + 3) {
202 | for j in (col ..< col + 3) {
203 | square.append(possibles[i][j])
204 | }
205 | }
206 |
207 | var count: Int = 0
208 | var index: Int = 0
209 | for outter in (0 ..< 9) {
210 | for inner in (0 ..< square[outter].count) {
211 | if square[outter][inner] == value {
212 | count += 1
213 | index = outter
214 | }
215 | }
216 | }
217 |
218 | if count == 1 {
219 | let rowOffset: Int = index / 3
220 | let colOffset: Int = index % 3
221 | grid[row + rowOffset][col + colOffset] = value
222 | }
223 | return count == 1
224 | }
225 |
226 | var found: Int = 0
227 |
228 | // detect hidden singles in rows
229 | for row in (0 ..< 9) {
230 | for value in (1 ... 9) {
231 | if uniqueRow(row: row,
232 | value: value,
233 | possibles: possibles[row]
234 | ) {
235 | found += 1
236 | }
237 | }
238 | }
239 |
240 | // detect hidden singles in columns
241 | for col in (0 ..< 9) {
242 | for value in (1 ... 9) {
243 | if uniqueCol(col: col,
244 | value: value,
245 | possibles: possibles.map { $0[col] }
246 | ) {
247 | found += 1
248 | }
249 | }
250 | }
251 |
252 | // detect hidden singles in boxes
253 | let delim: [Int] = [0, 3, 6]
254 | for row in delim {
255 | for col in delim {
256 | for value in (1 ... 9) {
257 | if uniqueSquare(row: row,
258 | col: col,
259 | value: value,
260 | possibles: possibles
261 | ) {
262 | found += 1
263 | }
264 | }
265 | }
266 | }
267 | return found
268 | }
269 |
270 | while true {
271 | let possibles: [[[Int]]] = computeGridPossibles()
272 |
273 | if nakedSingles(possibles: possibles) > 0 { continue }
274 | else if hiddenSingles(possibles: possibles) > 0 { continue }
275 |
276 | solution = grid
277 | return
278 | }
279 | }
280 |
281 |
282 | @discardableResult func backtrack(prev: Int, pos: Int) -> Bool {
283 | let row: Int = pos / 9
284 | let col: Int = pos % 9
285 |
286 | var possibles: [Int] = getPossibles(row: row, col: col)
287 |
288 | while possibles.count > 0 {
289 | let first: Int = possibles.removeFirst()
290 | grid[row][col] = first
291 |
292 | let nextpos: Int = nextEmptyPos(ref: pos)
293 | if nextpos == -1 {
294 |
295 | // Solution found. We must check if it is the only one (so far).
296 | solutions += 1
297 | if solutions > 1 {
298 | solution = Array(repeating: Array(repeating: UNDEFINED, count: 9), count: 9)
299 | return false
300 | }
301 | else {
302 | solution = grid
303 | }
304 | }
305 | else {
306 | backtrack(prev: pos, pos: nextpos)
307 | if solutions > 1 {
308 | return false
309 | }
310 | }
311 |
312 | // If we got here, everything after us failed.
313 | // We need to try another possible number.
314 | }
315 |
316 | grid[row][col] = UNDEFINED
317 | return false
318 | }
319 |
320 |
321 | func nextEmptyPos(ref: Int) -> Int {
322 | var nextpos: Int = ref + 1
323 |
324 | while true {
325 | let nextrow: Int = nextpos / 9
326 | let nextcol: Int = nextpos % 9
327 | if nextpos > 80 {
328 | // Board is filled
329 | return -1
330 | }
331 | else if grid[nextrow][nextcol] == UNDEFINED {
332 | return nextpos
333 | }
334 | nextpos += 1
335 | }
336 | }
337 |
338 |
339 | func getPossibles(row: Int, col: Int) -> [Int] {
340 | var possibles: [Int] = []
341 |
342 | for i in (1 ... 9) {
343 | if possible(token: i, row: row, col: col) {
344 | possibles.append(i)
345 | }
346 | }
347 | return possibles
348 | }
349 |
350 |
351 | func computeGridPossibles() -> [[[Int]]] {
352 | var possibles: [[[Int]]] = Array(repeating: Array(repeating: [],
353 | count: 9),
354 | count: 9)
355 |
356 | for row in (0 ..< 9) {
357 | for col in (0 ..< 9) {
358 | if grid[row][col] == UNDEFINED {
359 | possibles[row][col] = getPossibles(row: row, col: col)
360 | }
361 | }
362 | }
363 | return possibles
364 | }
365 |
366 | /*==============================================================================
367 | Grid utils.
368 | ==========================================================================*/
369 |
370 | func print_grid(grid: [[Int]]) -> Void {
371 | print(String(repeating: "-", count: 37))
372 |
373 | for (i, row) in grid.enumerated() {
374 | var line: String = ""
375 | var count: Int = 0
376 |
377 | for x in row {
378 | count += 1
379 |
380 | if count % 3 == 0 {
381 | if x != UNDEFINED {
382 | line.append(" " + String(x) + " |")
383 | } else {
384 | line.append(" |")
385 | }
386 | }
387 | else {
388 | if x != UNDEFINED {
389 | line.append(" " + String(x) + " ")
390 | } else {
391 | line.append(" ")
392 | }
393 | }
394 | }
395 | print("|" + line)
396 |
397 | if i == 8 {
398 | print(String(repeating: "-", count: 37))
399 | }
400 | else if i % 3 == 2 {
401 | print("|" + String(repeating: "---+", count: 8) + "---|")
402 | }
403 | else {
404 | print("|" + String(repeating: " +", count: 8) + " |")
405 | }
406 | }
407 | }
408 |
409 |
410 | func possible(token: Int, row: Int, col: Int) -> Bool {
411 | return !tokenInRow(token: token, row: row)
412 | && !tokenInCol(token: token, col: col)
413 | && !tokenInSquare(token: token, row: row, col: col)
414 | }
415 |
416 |
417 | func full() -> Bool {
418 | for row in (0 ..< 9) {
419 | for col in (0 ..< 9) {
420 | if grid[row][col] == UNDEFINED {
421 | return false
422 | }
423 | }
424 | }
425 | return true
426 | }
427 |
428 |
429 | func getSquare(row: Int, col: Int) -> [[Int]] {
430 | // this points to upper left corner
431 | let row: Int = (row / 3) * 3
432 | let col: Int = (col / 3) * 3
433 | var square: [[Int]] = [[Int]]()
434 |
435 | for i in (row ..< row + 3) {
436 | square.append([grid[i][col],
437 | grid[i][col + 1],
438 | grid[i][col + 2]])
439 | }
440 | return square
441 | }
442 |
443 |
444 | func tokenInRow(token: Int, row: Int) -> Bool {
445 | return grid[row].filter { $0 == token }.count > 0
446 | }
447 |
448 |
449 | func tokenInCol(token: Int, col: Int) -> Bool {
450 | return grid.filter { $0[col] == token }.count > 0
451 | }
452 |
453 |
454 | func tokenInSquare(token: Int, row: Int, col: Int) -> Bool {
455 | let square: [[Int]] = getSquare(row: row, col: col)
456 |
457 | return square[0].contains(token)
458 | || square[1].contains(token)
459 | || square[2].contains(token)
460 | }
461 |
462 | /*==============================================================================
463 | ImpExp utils.
464 | ==========================================================================*/
465 |
466 | func load(puzzle: String) -> Void {
467 | var str: String = puzzle
468 | var count: Int = 0
469 | //var user: Bool = false
470 |
471 | while !str.isEmpty {
472 | let row: Int = count / 9
473 | let col: Int = count % 9
474 | let char: Character = str.removeFirst()
475 |
476 | if "0" <= char && char <= "9" {
477 | let value: Int = Int(String(char)) ?? 0
478 |
479 | grid[row][col] = value
480 |
481 | /*
482 | user == true
483 | ? (self.inputType[row][col] = InputType.user)
484 | : (self.inputType[row][col] = InputType.system)
485 | */
486 |
487 | //user = false
488 |
489 | count += 1
490 | if (value > 0) {
491 | tokenFrequency[value - 1] += 1
492 | }
493 | }
494 | /*
495 | else if char == "u" {
496 | user = true
497 | }
498 | */
499 | }
500 | }
501 |
502 |
503 | func store() -> String {
504 | var str: String = String()
505 |
506 | for row in (0 ..< 9) {
507 | for col in (0 ..< 9) {
508 | /*
509 | if (self.inputType[row][col] == InputType.user) {
510 | str.append("u")
511 | }
512 | */
513 | str.append(String(grid[row][col]))
514 | }
515 | }
516 | return str
517 | }
518 |
519 | /*==============================================================================
520 | Tests.
521 | ==========================================================================*/
522 |
523 | func solve_test() {
524 | print("\n#####################################\n")
525 | print(" Solve Test \n")
526 |
527 | var score: Int = 0
528 | for puzzle in puzzles {
529 |
530 | load(puzzle: puzzle)
531 | score += solve()
532 | print_grid(grid: solution)
533 |
534 | print("\n#####################################\n")
535 | }
536 |
537 | print("Score:", score, "/", puzzles.count)
538 | }
539 |
540 |
541 | func generate_test() {
542 | print("\n#####################################\n")
543 | print(" Generate Test \n")
544 |
545 | generate()
546 | print_grid(grid: grid)
547 |
548 | print("\nExported board:\n" + store())
549 | }
550 |
551 | generate_test()
552 | //solve_test()
553 |
--------------------------------------------------------------------------------
/app/Sudoku/Models/Grid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Grid.swift
3 | // Sudoku
4 | //
5 | // Created by Pedro Galhardo on 29/10/2019.
6 | // Copyright © 2019 Pedro Galhardo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | final class Grid: ObservableObject {
13 |
14 | struct CellState {
15 | let value: Int
16 | let postion: [Int]
17 | }
18 |
19 | @Published private var grid: [[Int]] = [[Int]]()
20 | @Published private var solution: [[Int]] = [[Int]]()
21 |
22 | @Published private var color: [[Color]] = [[Color]]()
23 | @Published private var inputType: [[Int]] = [[Int]]()
24 | @Published private var numberFrequency: [Int] = [Int]()
25 |
26 | private var active: [Int]?
27 | private var colored: [[Int]] = [[Int]]()
28 | private var solutions: Int = 0
29 | private var errorCount: Int = 0
30 | private var cellBefore: [Int] = [Int]()
31 | private var previousState: CellState
32 | private var difficulty: String
33 |
34 | init() {
35 | self.grid = Array(
36 | repeating:Array(repeating: UNDEFINED, count: 9),
37 | count: 9
38 | )
39 | self.color = Array(
40 | repeating: Array(repeating: Color.white, count: 9),
41 | count: 9
42 | )
43 | self.inputType = Array(
44 | repeating: Array(repeating: InputType.user, count: 9),
45 | count: 9
46 | )
47 | self.numberFrequency = Array(repeating: 0, count: 9)
48 | self.previousState = CellState(value: UNDEFINED, postion: [UNDEFINED, UNDEFINED])
49 | self.difficulty = String.init()
50 | }
51 |
52 | /*==========================================================================
53 | Core functions
54 | ==========================================================================*/
55 |
56 | func reset() -> Void {
57 | self.active = nil
58 | self.colored = [[Int]]()
59 | self.numberFrequency = Array(repeating: 0, count: 9)
60 |
61 | self.grid = Array(
62 | repeating: Array(repeating: UNDEFINED, count: 9),
63 | count: 9
64 | )
65 | let defaultGridColor: Color = Color(UIColor(named: "GridBackground")!)
66 | self.color = Array(
67 | repeating: Array(repeating: defaultGridColor, count: 9),
68 | count: 9
69 | )
70 | self.inputType = Array(
71 | repeating: Array(repeating: InputType.user, count: 9),
72 | count: 9
73 | )
74 | }
75 |
76 | func load(puzzle: String, difficulty: String) -> Void {
77 | var count: Int = 0
78 | var user: Bool = false
79 | var error: Bool = false
80 | var str: String = puzzle
81 |
82 | while !str.isEmpty {
83 | let row: Int = count / 9
84 | let col: Int = count % 9
85 | let char: Character = str.removeFirst()
86 |
87 | if "0" <= char && char <= "9" {
88 | let char2String: String = String(char)
89 | let value: Int = Int(char2String) ?? 0
90 |
91 | self.grid[row][col] = value
92 |
93 | if user == true {
94 | self.inputType[row][col] = InputType.user
95 | } else if error == true {
96 | self.inputType[row][col] = InputType.error
97 | } else {
98 | self.inputType[row][col] = InputType.system
99 | }
100 |
101 | user = false
102 | error = false
103 | count += 1
104 | if (value > 0) {
105 | numberFrequency[value - 1] += 1
106 | }
107 | }
108 |
109 | else if char == "u" {
110 | user = true
111 | } else if char == "e" {
112 | error = true
113 | }
114 | }
115 |
116 | self.difficulty = difficulty
117 | }
118 |
119 | func loadFromSeed() -> Void {
120 | let randPuzzle: Int = Int.random(in: 0 ..< Puzzles.easy.count)
121 | self.difficulty = "Easy"
122 |
123 | var str: String = Puzzles.easy[randPuzzle]
124 | var count: Int = 0
125 |
126 | while !str.isEmpty {
127 | let row: Int = count / 9
128 | let col: Int = count % 9
129 | let char: Character = str.removeFirst()
130 |
131 | if "0" <= char && char <= "9" {
132 | let char2String: String = String(char)
133 | let value: Int = Int(char2String) ?? 0
134 |
135 | self.grid[row][col] = value
136 | if value != UNDEFINED {
137 | self.inputType[row][col] = InputType.system
138 | } else {
139 | self.inputType[row][col] = InputType.user
140 | }
141 |
142 | count += 1
143 | }
144 | }
145 | }
146 |
147 | func toString() -> String {
148 | var str = String()
149 |
150 | for row: Int in (0 ..< 9) {
151 | for col: Int in (0 ..< 9) {
152 | if (self.inputType[row][col] == InputType.user) {
153 | str.append("u")
154 | }
155 |
156 | else if (self.inputType[row][col] == InputType.error) {
157 | str.append("e")
158 | }
159 | str.append(String(grid[row][col]))
160 | }
161 | }
162 | return str
163 | }
164 |
165 | func getNumberFrequency() -> [Int] {
166 | return self.numberFrequency
167 | }
168 |
169 | func full() -> Bool {
170 | for row: Int in (0 ..< 9) {
171 | for col: Int in (0 ..< 9) {
172 | if self.grid[row][col] == UNDEFINED {
173 | return false
174 | }
175 | }
176 | }
177 | return true
178 | }
179 |
180 | func completion() -> Int {
181 | var filled: Int = 0
182 |
183 | for row: Int in (0 ..< 9) {
184 | for col: Int in (0 ..< 9) {
185 | if grid[row][col] != UNDEFINED {
186 | filled += 1
187 | }
188 | }
189 | }
190 | return filled * 100 / 81
191 | }
192 |
193 | func getErrorCount() -> Int {
194 | return self.errorCount
195 | }
196 |
197 | /*==========================================================================
198 | Single cell actions
199 | ==========================================================================*/
200 |
201 | func valueAt(row: Int, col: Int) -> Int {
202 | return grid[row][col]
203 | }
204 |
205 | func colorAt(row: Int, col: Int) -> Color {
206 | return color[row][col]
207 | }
208 |
209 | @discardableResult func setValue(row: Int, col: Int, value: Int) -> Bool {
210 |
211 | if inputType[row][col] == InputType.system {
212 | return false
213 | }
214 |
215 | else if grid[row][col] == value {
216 | return true
217 | }
218 |
219 | else if value != UNDEFINED
220 | && !possible(number: value, row: row, col: col) {
221 |
222 | inputType[row][col] = InputType.error
223 | errorCount += 1
224 | }
225 |
226 | else {
227 | inputType[row][col] = InputType.user
228 |
229 | if grid[row][col] > 0 {
230 | numberFrequency[grid[row][col] - 1] -= 1
231 | }
232 |
233 | if value > 0 {
234 | numberFrequency[value - 1] += 1
235 | }
236 | }
237 |
238 | grid[row][col] = value
239 | return true
240 | }
241 |
242 | func getActive() -> [Int]? {
243 | return active
244 | }
245 |
246 | func setActive(row: Int, col: Int, areas: Bool, similar: Bool) -> Void {
247 | let previous: [Int]? = active
248 | active = [row, col]
249 |
250 | for i: Int in (0 ..< colored.count) {
251 | toggleColor(cell: colored[i])
252 | }
253 | colored.removeAll()
254 |
255 | if previous == active {
256 | active = nil
257 | } else {
258 | toggleColor(cell: active)
259 |
260 | if areas {
261 | highlightRow(cell: active)
262 | highlightCol(cell: active)
263 | }
264 | if similar && grid[row][col] != UNDEFINED {
265 | highlightSimilar(row: row, col: col)
266 | }
267 | }
268 | }
269 |
270 | func toggleColor(cell: [Int]?) -> Void {
271 | if cell == nil { return }
272 |
273 | let row: Int = cell![0]
274 | let col: Int = cell![1]
275 |
276 | let defaultGridColor: Color = Color(UIColor(named: "GridBackground")!)
277 |
278 | if color[row][col] == defaultGridColor {
279 | color[row][col] = Colors.ActiveBlue
280 | colored.append([row, col])
281 | } else {
282 | color[row][col] = defaultGridColor
283 | }
284 | }
285 |
286 | /*==========================================================================
287 | Groups of cells
288 | ==========================================================================*/
289 |
290 | func getSquare(row: Int, col: Int) -> [[Int]] {
291 | // this points to upper left corner
292 | let row: Int = (row / 3) * 3
293 | let col: Int = (col / 3) * 3
294 | var square: [[Int]] = [[Int]]()
295 |
296 | for i: Int in (row ..< row + 3) {
297 | square.append([grid[i][col],
298 | grid[i][col + 1],
299 | grid[i][col + 2]])
300 | }
301 | return square
302 | }
303 |
304 | func numberInRow(number: Int, row: Int) -> Bool {
305 | return grid[row].filter { $0 == number }.count > 0
306 | }
307 |
308 | func numberInCol(number: Int, col: Int) -> Bool {
309 | return grid.filter { $0[col] == number }.count > 0
310 | }
311 |
312 | func numberInSquare(number: Int, row: Int, col: Int) -> Bool {
313 | let square: [[Int]] = getSquare(row: row, col: col)
314 |
315 | return (square[0].contains(number)
316 | || square[1].contains(number)
317 | || square[2].contains(number))
318 | }
319 |
320 | func highlightRow(cell: [Int]?) -> Void {
321 | let row: Int = cell![0]
322 | let col: Int = cell![1]
323 |
324 | for i: Int in (0 ..< 9) {
325 | if i == col { continue }
326 | color[row][i] = Colors.LightBlue
327 | colored.append([row, i])
328 | }
329 | }
330 |
331 | func highlightCol(cell: [Int]?) -> Void {
332 | let row: Int = cell![0]
333 | let col: Int = cell![1]
334 |
335 | for i: Int in (0 ..< 9) {
336 | if i == row { continue }
337 | color[i][col] = Colors.LightBlue
338 | colored.append([i, col])
339 | }
340 | }
341 |
342 | func highlightSimilar(row: Int, col: Int) -> Void {
343 | let value: Int = grid[row][col]
344 | var found: Int = 0
345 |
346 | for i: Int in (0 ..< 9) {
347 | if i == row { continue }
348 |
349 | for j: Int in (0 ..< 9) {
350 | if j == col { continue }
351 |
352 | if grid[i][j] == value {
353 | color[i][j] = Colors.LightBlue
354 | colored.append([i, j])
355 | found += 1
356 | }
357 |
358 | if found == numberFrequency[value - 1] { return }
359 | }
360 | }
361 | }
362 |
363 | func possible(number: Int, row: Int, col: Int) -> Bool {
364 | return !numberInRow(number: number, row: row)
365 | && !numberInCol(number: number, col: col)
366 | && !numberInSquare(number: number, row: row, col: col)
367 | }
368 |
369 | func render(row: Int, col: Int, fontSize: CGFloat) -> Text {
370 |
371 | let value: Int = grid[row][col]
372 | let type: Int = inputType[row][col]
373 |
374 | if value == UNDEFINED { return Text(" ") }
375 |
376 | if type == InputType.system {
377 | return Text("\(value)")
378 | .font(.custom("CaviarDreams-Bold",
379 | size: fontSize))
380 | .foregroundColor(Color(.label))
381 | } else if type == InputType.user {
382 | return Text("\(value)")
383 | .font(.custom("CaviarDreams-Bold",
384 | size: fontSize))
385 | //.foregroundColor(Colors.DeepBlue)
386 | .foregroundColor(Colors.Golden)
387 | }
388 | return Text("\(value)")
389 | .font(.custom("CaviarDreams-Bold",
390 | size: fontSize))
391 | .foregroundColor(Color(.systemPink))
392 | }
393 |
394 | /*==========================================================================
395 | Generator
396 | ==========================================================================*/
397 |
398 | func generate() -> Void {
399 | loadFromSeed()
400 | //randomize()
401 | self.errorCount = 0
402 |
403 | computeTokenFrequency()
404 | }
405 |
406 | func computeTokenFrequency() -> Void {
407 | for row in grid {
408 | for val in row {
409 | if val != UNDEFINED {
410 | numberFrequency[val - 1] += 1
411 | }
412 | }
413 | }
414 | }
415 |
416 | /*==========================================================================
417 | Solver
418 | ==========================================================================*/
419 |
420 | func solve() -> Int {
421 | solutions = 0
422 |
423 | basicTechniques()
424 | if full() {
425 | solutions = 1
426 | }
427 | else {
428 | self.solution = Array(
429 | repeating: Array(repeating: UNDEFINED, count: 9),
430 | count: 9
431 | )
432 | backtrack(prev: -1, pos: nextEmptyPos(ref: -1))
433 | self.grid = self.solution
434 | }
435 |
436 | return solutions
437 | }
438 |
439 |
440 | func basicTechniques() -> Void {
441 | func nakedSingles(possibles: [[[Int]]]) -> Int {
442 | var solved: Int = 0
443 |
444 | for row in (0 ..< 9) {
445 | for col in (0 ..< 9) {
446 | if possibles[row][col].count == 1 {
447 | solved += 1
448 | grid[row][col] = possibles[row][col][0]
449 | }
450 | }
451 | }
452 | return solved
453 | }
454 |
455 | func hiddenSingles(possibles: [[[Int]]]) -> Int {
456 |
457 | func uniqueRow(row: Int, value: Int, possibles: [[Int]]) -> Bool {
458 | var count: Int = 0
459 | var index: Int = 0
460 |
461 | for col in (0 ..< 9) {
462 | for inner in (0 ..< possibles[col].count) {
463 | if possibles[col][inner] == value {
464 | count += 1
465 | index = col
466 | }
467 | }
468 | }
469 |
470 | if count == 1 {
471 | grid[row][index] = value
472 | }
473 | return count == 1
474 | }
475 |
476 | func uniqueCol(col: Int, value: Int, possibles: [[Int]]) -> Bool {
477 | var count: Int = 0
478 | var index: Int = 0
479 |
480 | for row in (0 ..< 9) {
481 | for inner in (0 ..< possibles[row].count) {
482 | if possibles[row][inner] == value {
483 | count += 1
484 | index = row
485 | }
486 | }
487 | }
488 |
489 | if count == 1 {
490 | grid[index][col] = value
491 | }
492 | return count == 1
493 | }
494 |
495 | func uniqueSquare(row: Int, col: Int, value: Int,
496 | possibles: [[[Int]]]) -> Bool {
497 |
498 | var square: [[Int]] = [[Int]]()
499 | for i in (row ..< row + 3) {
500 | for j in (col ..< col + 3) {
501 | square.append(possibles[i][j])
502 | }
503 | }
504 |
505 | var count: Int = 0
506 | var index: Int = 0
507 | for outter in (0 ..< 9) {
508 | for inner in (0 ..< square[outter].count) {
509 | if square[outter][inner] == value {
510 | count += 1
511 | index = outter
512 | }
513 | }
514 | }
515 |
516 | if count == 1 {
517 | let rowOffset: Int = index / 3
518 | let colOffset: Int = index % 3
519 | grid[row + rowOffset][col + colOffset] = value
520 | }
521 | return count == 1
522 | }
523 |
524 | var found: Int = 0
525 |
526 | // detect hidden singles in rows
527 | for row: Int in (0 ..< 9) {
528 | for value: Int in (1 ... 9) {
529 | if uniqueRow(row: row,
530 | value: value,
531 | possibles: possibles[row]
532 | ) {
533 | found += 1
534 | }
535 | }
536 | }
537 |
538 | // detect hidden singles in columns
539 | for col in (0 ..< 9) {
540 | for value in (1 ... 9) {
541 | if uniqueCol(col: col,
542 | value: value,
543 | possibles: possibles.map { $0[col] }
544 | ) {
545 | found += 1
546 | }
547 | }
548 | }
549 |
550 | // detect hidden singles in boxes
551 | let delim: [Int] = [0, 3, 6]
552 | for row in delim {
553 | for col in delim {
554 | for value in (1 ... 9) {
555 | if uniqueSquare(row: row,
556 | col: col,
557 | value: value,
558 | possibles: possibles
559 | ) {
560 | found += 1
561 | }
562 | }
563 | }
564 | }
565 | return found
566 | }
567 |
568 | while true {
569 | let possibles: [[[Int]]] = computeGridPossibles()
570 |
571 | if nakedSingles(possibles: possibles) > 0 { continue }
572 | else if hiddenSingles(possibles: possibles) > 0 { continue }
573 |
574 | break
575 | }
576 | }
577 |
578 |
579 | @discardableResult func backtrack(prev: Int, pos: Int) -> Bool {
580 | let row: Int = pos / 9
581 | let col: Int = pos % 9
582 |
583 | var possibles: [Int] = getPossibles(row: row, col: col)
584 |
585 | while possibles.count > 0 {
586 | let first: Int = possibles.removeFirst()
587 | grid[row][col] = first
588 |
589 | let nextpos: Int = nextEmptyPos(ref: pos)
590 | if nextpos == -1 {
591 |
592 | // Solution found. We must check if it is the only one (so far).
593 | solutions += 1
594 | if solutions > 1 {
595 | solution = Array(repeating: Array(repeating: UNDEFINED,
596 | count: 9),
597 | count: 9)
598 | return false
599 | }
600 | else {
601 | solution = grid
602 | }
603 | }
604 | else {
605 | backtrack(prev: pos, pos: nextpos)
606 | if solutions > 1 {
607 | return false
608 | }
609 | }
610 |
611 | // If we got here, everything after us failed.
612 | // We need to try another possible number.
613 | }
614 |
615 | grid[row][col] = UNDEFINED
616 | return false
617 | }
618 |
619 |
620 | func nextEmptyPos(ref: Int) -> Int {
621 | var nextpos: Int = ref + 1
622 |
623 | while true {
624 | let nextrow: Int = nextpos / 9
625 | let nextcol: Int = nextpos % 9
626 | if nextpos > 80 {
627 | // Board is filled
628 | return -1
629 | }
630 | else if grid[nextrow][nextcol] == UNDEFINED {
631 | return nextpos
632 | }
633 | nextpos += 1
634 | }
635 | }
636 |
637 |
638 | func getPossibles(row: Int, col: Int) -> [Int] {
639 | var possibles: [Int] = []
640 |
641 | for i in (1 ... 9) {
642 | if possible(token: i, row: row, col: col) {
643 | possibles.append(i)
644 | }
645 | }
646 | return possibles
647 | }
648 |
649 |
650 | func computeGridPossibles() -> [[[Int]]] {
651 | var possibles: [[[Int]]] = Array(repeating: Array(repeating: [],
652 | count: 9),
653 | count: 9)
654 |
655 | for row in (0 ..< 9) {
656 | for col in (0 ..< 9) {
657 | if grid[row][col] == UNDEFINED {
658 | possibles[row][col] = getPossibles(row: row, col: col)
659 | }
660 | }
661 | }
662 | return possibles
663 | }
664 |
665 | func possible(token: Int, row: Int, col: Int) -> Bool {
666 | return !tokenInRow(token: token, row: row)
667 | && !tokenInCol(token: token, col: col)
668 | && !tokenInSquare(token: token, row: row, col: col)
669 | }
670 |
671 | func tokenInRow(token: Int, row: Int) -> Bool {
672 | return grid[row].filter { $0 == token }.count > 0
673 | }
674 |
675 |
676 | func tokenInCol(token: Int, col: Int) -> Bool {
677 | return grid.filter { $0[col] == token }.count > 0
678 | }
679 |
680 |
681 | func tokenInSquare(token: Int, row: Int, col: Int) -> Bool {
682 | let square: [[Int]] = getSquare(row: row, col: col)
683 |
684 | return square[0].contains(token)
685 | || square[1].contains(token)
686 | || square[2].contains(token)
687 | }
688 |
689 | func getDifficulty() -> String {
690 | return self.difficulty
691 | }
692 | }
693 |
--------------------------------------------------------------------------------
/app/Sudoku.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 680CD04D2368B3F900D734A1 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680CD04C2368B3F900D734A1 /* Constant.swift */; };
11 | 681A72BB23661BB7002B9053 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681A72BA23661BB7002B9053 /* AppDelegate.swift */; };
12 | 681A72BD23661BB7002B9053 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681A72BC23661BB7002B9053 /* SceneDelegate.swift */; };
13 | 681A72BF23661BB7002B9053 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681A72BE23661BB7002B9053 /* MenuView.swift */; };
14 | 681A72C123661BB8002B9053 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 681A72C023661BB8002B9053 /* Assets.xcassets */; };
15 | 681A72C423661BB8002B9053 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 681A72C323661BB8002B9053 /* Preview Assets.xcassets */; };
16 | 681A72C723661BB8002B9053 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 681A72C523661BB8002B9053 /* LaunchScreen.storyboard */; };
17 | 681A72CF23662961002B9053 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681A72CE23662961002B9053 /* GridView.swift */; };
18 | 681E989724874F4C002C682C /* Seeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681E989624874F4C002C682C /* Seeds.swift */; };
19 | 683A3D8C2368F77A00135D55 /* Grid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683A3D8B2368F77A00135D55 /* Grid.swift */; };
20 | 683A3D8E2369012200135D55 /* GameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683A3D8D2369012200135D55 /* GameView.swift */; };
21 | 683A3D90236916CF00135D55 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683A3D8F236916CF00135D55 /* RootView.swift */; };
22 | 684849DC248043420067E21B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 684849DE248043420067E21B /* Localizable.strings */; };
23 | 684EC5F7236A4FFF009EE48C /* KeyboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684EC5F6236A4FFF009EE48C /* KeyboardView.swift */; };
24 | 684EC5F9236A5E89009EE48C /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684EC5F8236A5E89009EE48C /* TimerView.swift */; };
25 | 6868C88B248C1C700024CDFF /* GenericTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6868C88A248C1C700024CDFF /* GenericTopBarView.swift */; };
26 | 68908C9B236E6B6F00233C69 /* StatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68908C9A236E6B6F00233C69 /* StatisticsView.swift */; };
27 | 68908C9F236E735E00233C69 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68908C9E236E735E00233C69 /* Settings.swift */; };
28 | 68AED1AA236FCF3F00490E6D /* StrategiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68AED1A9236FCF3F00490E6D /* StrategiesView.swift */; };
29 | 68DA619A24DEBE3000E545A5 /* Sudoku.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 68DA619824DEBE3000E545A5 /* Sudoku.xcdatamodeld */; };
30 | 68DA619C24DEC14C00E545A5 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DA619B24DEC14C00E545A5 /* Record.swift */; };
31 | 68EA2F21236A1ABB000FBCFC /* CaviarDreams_Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 68EA2F20236A1ABB000FBCFC /* CaviarDreams_Bold.ttf */; };
32 | 68FDA2B7236E30D1003F954C /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68FDA2B6236E30D1003F954C /* SettingsView.swift */; };
33 | 68FF20542370AA5C00B433F2 /* CaviarDreams.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 68FF20532370AA5C00B433F2 /* CaviarDreams.ttf */; };
34 | /* End PBXBuildFile section */
35 |
36 | /* Begin PBXFileReference section */
37 | 680CD04C2368B3F900D734A1 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; };
38 | 681A72B723661BB7002B9053 /* Sudoku.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sudoku.app; sourceTree = BUILT_PRODUCTS_DIR; };
39 | 681A72BA23661BB7002B9053 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
40 | 681A72BC23661BB7002B9053 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
41 | 681A72BE23661BB7002B9053 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; };
42 | 681A72C023661BB8002B9053 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
43 | 681A72C323661BB8002B9053 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
44 | 681A72C623661BB8002B9053 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
45 | 681A72C823661BB8002B9053 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
46 | 681A72CE23662961002B9053 /* GridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = ""; };
47 | 681E989624874F4C002C682C /* Seeds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Seeds.swift; sourceTree = ""; };
48 | 683A3D8B2368F77A00135D55 /* Grid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Grid.swift; sourceTree = ""; };
49 | 683A3D8D2369012200135D55 /* GameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameView.swift; sourceTree = ""; };
50 | 683A3D8F236916CF00135D55 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; };
51 | 683A3D932369246D00135D55 /* Sudoku.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sudoku.entitlements; sourceTree = ""; };
52 | 684849DD248043420067E21B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
53 | 684849E1248045490067E21B /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; };
54 | 684EC5F6236A4FFF009EE48C /* KeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardView.swift; sourceTree = ""; };
55 | 684EC5F8236A5E89009EE48C /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = ""; };
56 | 6868C88A248C1C700024CDFF /* GenericTopBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericTopBarView.swift; sourceTree = ""; };
57 | 68908C9A236E6B6F00233C69 /* StatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsView.swift; sourceTree = ""; };
58 | 68908C9E236E735E00233C69 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; };
59 | 68AED1A9236FCF3F00490E6D /* StrategiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrategiesView.swift; sourceTree = ""; };
60 | 68DA619924DEBE3000E545A5 /* Record.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Record.xcdatamodel; sourceTree = ""; };
61 | 68DA619B24DEC14C00E545A5 /* Record.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; };
62 | 68EA2F20236A1ABB000FBCFC /* CaviarDreams_Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = CaviarDreams_Bold.ttf; sourceTree = ""; };
63 | 68FDA2B6236E30D1003F954C /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
64 | 68FF20532370AA5C00B433F2 /* CaviarDreams.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = CaviarDreams.ttf; sourceTree = ""; };
65 | /* End PBXFileReference section */
66 |
67 | /* Begin PBXFrameworksBuildPhase section */
68 | 681A72B423661BB7002B9053 /* Frameworks */ = {
69 | isa = PBXFrameworksBuildPhase;
70 | buildActionMask = 2147483647;
71 | files = (
72 | );
73 | runOnlyForDeploymentPostprocessing = 0;
74 | };
75 | /* End PBXFrameworksBuildPhase section */
76 |
77 | /* Begin PBXGroup section */
78 | 680CD04E2368C13500D734A1 /* fonts */ = {
79 | isa = PBXGroup;
80 | children = (
81 | 68EA2F20236A1ABB000FBCFC /* CaviarDreams_Bold.ttf */,
82 | 68FF20532370AA5C00B433F2 /* CaviarDreams.ttf */,
83 | );
84 | path = fonts;
85 | sourceTree = "";
86 | };
87 | 681A72AE23661BB7002B9053 = {
88 | isa = PBXGroup;
89 | children = (
90 | 681A72B923661BB7002B9053 /* Sudoku */,
91 | 681A72B823661BB7002B9053 /* Products */,
92 | );
93 | sourceTree = "";
94 | };
95 | 681A72B823661BB7002B9053 /* Products */ = {
96 | isa = PBXGroup;
97 | children = (
98 | 681A72B723661BB7002B9053 /* Sudoku.app */,
99 | );
100 | name = Products;
101 | sourceTree = "";
102 | };
103 | 681A72B923661BB7002B9053 /* Sudoku */ = {
104 | isa = PBXGroup;
105 | children = (
106 | 683A3D932369246D00135D55 /* Sudoku.entitlements */,
107 | 681A72BA23661BB7002B9053 /* AppDelegate.swift */,
108 | 680CD04C2368B3F900D734A1 /* Constant.swift */,
109 | 681E989624874F4C002C682C /* Seeds.swift */,
110 | 681A72BC23661BB7002B9053 /* SceneDelegate.swift */,
111 | 683A3D9123691BCF00135D55 /* Models */,
112 | 683A3D9223691BE200135D55 /* Views */,
113 | 684EC5F5236A4FE4009EE48C /* Support Views */,
114 | 681A72C023661BB8002B9053 /* Assets.xcassets */,
115 | 681A72C523661BB8002B9053 /* LaunchScreen.storyboard */,
116 | 681A72C823661BB8002B9053 /* Info.plist */,
117 | 681A72C223661BB8002B9053 /* Preview Content */,
118 | 680CD04E2368C13500D734A1 /* fonts */,
119 | 684849DE248043420067E21B /* Localizable.strings */,
120 | );
121 | path = Sudoku;
122 | sourceTree = "";
123 | };
124 | 681A72C223661BB8002B9053 /* Preview Content */ = {
125 | isa = PBXGroup;
126 | children = (
127 | 681A72C323661BB8002B9053 /* Preview Assets.xcassets */,
128 | );
129 | path = "Preview Content";
130 | sourceTree = "";
131 | };
132 | 683A3D9123691BCF00135D55 /* Models */ = {
133 | isa = PBXGroup;
134 | children = (
135 | 68DA619824DEBE3000E545A5 /* Sudoku.xcdatamodeld */,
136 | 683A3D8B2368F77A00135D55 /* Grid.swift */,
137 | 68DA619B24DEC14C00E545A5 /* Record.swift */,
138 | 68908C9E236E735E00233C69 /* Settings.swift */,
139 | );
140 | path = Models;
141 | sourceTree = "";
142 | };
143 | 683A3D9223691BE200135D55 /* Views */ = {
144 | isa = PBXGroup;
145 | children = (
146 | 681A72BE23661BB7002B9053 /* MenuView.swift */,
147 | 683A3D8D2369012200135D55 /* GameView.swift */,
148 | 683A3D8F236916CF00135D55 /* RootView.swift */,
149 | 68FDA2B6236E30D1003F954C /* SettingsView.swift */,
150 | 68908C9A236E6B6F00233C69 /* StatisticsView.swift */,
151 | 68AED1A9236FCF3F00490E6D /* StrategiesView.swift */,
152 | );
153 | path = Views;
154 | sourceTree = "";
155 | };
156 | 684EC5F5236A4FE4009EE48C /* Support Views */ = {
157 | isa = PBXGroup;
158 | children = (
159 | 6868C88A248C1C700024CDFF /* GenericTopBarView.swift */,
160 | 681A72CE23662961002B9053 /* GridView.swift */,
161 | 684EC5F6236A4FFF009EE48C /* KeyboardView.swift */,
162 | 684EC5F8236A5E89009EE48C /* TimerView.swift */,
163 | );
164 | path = "Support Views";
165 | sourceTree = "";
166 | };
167 | /* End PBXGroup section */
168 |
169 | /* Begin PBXNativeTarget section */
170 | 681A72B623661BB7002B9053 /* Sudoku */ = {
171 | isa = PBXNativeTarget;
172 | buildConfigurationList = 681A72CB23661BB8002B9053 /* Build configuration list for PBXNativeTarget "Sudoku" */;
173 | buildPhases = (
174 | 681A72B323661BB7002B9053 /* Sources */,
175 | 681A72B423661BB7002B9053 /* Frameworks */,
176 | 681A72B523661BB7002B9053 /* Resources */,
177 | );
178 | buildRules = (
179 | );
180 | dependencies = (
181 | );
182 | name = Sudoku;
183 | productName = Sudoku;
184 | productReference = 681A72B723661BB7002B9053 /* Sudoku.app */;
185 | productType = "com.apple.product-type.application";
186 | };
187 | /* End PBXNativeTarget section */
188 |
189 | /* Begin PBXProject section */
190 | 681A72AF23661BB7002B9053 /* Project object */ = {
191 | isa = PBXProject;
192 | attributes = {
193 | LastSwiftUpdateCheck = 1110;
194 | LastUpgradeCheck = 1200;
195 | ORGANIZATIONNAME = "Pedro Galhardo";
196 | TargetAttributes = {
197 | 681A72B623661BB7002B9053 = {
198 | CreatedOnToolsVersion = 11.1;
199 | };
200 | };
201 | };
202 | buildConfigurationList = 681A72B223661BB7002B9053 /* Build configuration list for PBXProject "Sudoku" */;
203 | compatibilityVersion = "Xcode 9.3";
204 | developmentRegion = en;
205 | hasScannedForEncodings = 0;
206 | knownRegions = (
207 | en,
208 | Base,
209 | "pt-PT",
210 | );
211 | mainGroup = 681A72AE23661BB7002B9053;
212 | productRefGroup = 681A72B823661BB7002B9053 /* Products */;
213 | projectDirPath = "";
214 | projectRoot = "";
215 | targets = (
216 | 681A72B623661BB7002B9053 /* Sudoku */,
217 | );
218 | };
219 | /* End PBXProject section */
220 |
221 | /* Begin PBXResourcesBuildPhase section */
222 | 681A72B523661BB7002B9053 /* Resources */ = {
223 | isa = PBXResourcesBuildPhase;
224 | buildActionMask = 2147483647;
225 | files = (
226 | 681A72C723661BB8002B9053 /* LaunchScreen.storyboard in Resources */,
227 | 684849DC248043420067E21B /* Localizable.strings in Resources */,
228 | 68FF20542370AA5C00B433F2 /* CaviarDreams.ttf in Resources */,
229 | 68EA2F21236A1ABB000FBCFC /* CaviarDreams_Bold.ttf in Resources */,
230 | 681A72C423661BB8002B9053 /* Preview Assets.xcassets in Resources */,
231 | 681A72C123661BB8002B9053 /* Assets.xcassets in Resources */,
232 | );
233 | runOnlyForDeploymentPostprocessing = 0;
234 | };
235 | /* End PBXResourcesBuildPhase section */
236 |
237 | /* Begin PBXSourcesBuildPhase section */
238 | 681A72B323661BB7002B9053 /* Sources */ = {
239 | isa = PBXSourcesBuildPhase;
240 | buildActionMask = 2147483647;
241 | files = (
242 | 681A72BB23661BB7002B9053 /* AppDelegate.swift in Sources */,
243 | 681A72CF23662961002B9053 /* GridView.swift in Sources */,
244 | 68DA619C24DEC14C00E545A5 /* Record.swift in Sources */,
245 | 6868C88B248C1C700024CDFF /* GenericTopBarView.swift in Sources */,
246 | 68DA619A24DEBE3000E545A5 /* Sudoku.xcdatamodeld in Sources */,
247 | 681A72BD23661BB7002B9053 /* SceneDelegate.swift in Sources */,
248 | 68908C9B236E6B6F00233C69 /* StatisticsView.swift in Sources */,
249 | 684EC5F9236A5E89009EE48C /* TimerView.swift in Sources */,
250 | 68AED1AA236FCF3F00490E6D /* StrategiesView.swift in Sources */,
251 | 683A3D8C2368F77A00135D55 /* Grid.swift in Sources */,
252 | 683A3D90236916CF00135D55 /* RootView.swift in Sources */,
253 | 683A3D8E2369012200135D55 /* GameView.swift in Sources */,
254 | 681A72BF23661BB7002B9053 /* MenuView.swift in Sources */,
255 | 68908C9F236E735E00233C69 /* Settings.swift in Sources */,
256 | 684EC5F7236A4FFF009EE48C /* KeyboardView.swift in Sources */,
257 | 680CD04D2368B3F900D734A1 /* Constant.swift in Sources */,
258 | 68FDA2B7236E30D1003F954C /* SettingsView.swift in Sources */,
259 | 681E989724874F4C002C682C /* Seeds.swift in Sources */,
260 | );
261 | runOnlyForDeploymentPostprocessing = 0;
262 | };
263 | /* End PBXSourcesBuildPhase section */
264 |
265 | /* Begin PBXVariantGroup section */
266 | 681A72C523661BB8002B9053 /* LaunchScreen.storyboard */ = {
267 | isa = PBXVariantGroup;
268 | children = (
269 | 681A72C623661BB8002B9053 /* Base */,
270 | );
271 | name = LaunchScreen.storyboard;
272 | sourceTree = "";
273 | };
274 | 684849DE248043420067E21B /* Localizable.strings */ = {
275 | isa = PBXVariantGroup;
276 | children = (
277 | 684849DD248043420067E21B /* en */,
278 | 684849E1248045490067E21B /* pt-PT */,
279 | );
280 | name = Localizable.strings;
281 | sourceTree = "";
282 | };
283 | /* End PBXVariantGroup section */
284 |
285 | /* Begin XCBuildConfiguration section */
286 | 681A72C923661BB8002B9053 /* Debug */ = {
287 | isa = XCBuildConfiguration;
288 | buildSettings = {
289 | ALWAYS_SEARCH_USER_PATHS = NO;
290 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
291 | CLANG_ANALYZER_NONNULL = YES;
292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
294 | CLANG_CXX_LIBRARY = "libc++";
295 | CLANG_ENABLE_MODULES = YES;
296 | CLANG_ENABLE_OBJC_ARC = YES;
297 | CLANG_ENABLE_OBJC_WEAK = YES;
298 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
299 | CLANG_WARN_BOOL_CONVERSION = YES;
300 | CLANG_WARN_COMMA = YES;
301 | CLANG_WARN_CONSTANT_CONVERSION = YES;
302 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
303 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
304 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
305 | CLANG_WARN_EMPTY_BODY = YES;
306 | CLANG_WARN_ENUM_CONVERSION = YES;
307 | CLANG_WARN_INFINITE_RECURSION = YES;
308 | CLANG_WARN_INT_CONVERSION = YES;
309 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
310 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
311 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
312 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
313 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
314 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
315 | CLANG_WARN_STRICT_PROTOTYPES = YES;
316 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
317 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
318 | CLANG_WARN_UNREACHABLE_CODE = YES;
319 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
320 | COPY_PHASE_STRIP = NO;
321 | DEBUG_INFORMATION_FORMAT = dwarf;
322 | ENABLE_STRICT_OBJC_MSGSEND = YES;
323 | ENABLE_TESTABILITY = YES;
324 | GCC_C_LANGUAGE_STANDARD = gnu11;
325 | GCC_DYNAMIC_NO_PIC = NO;
326 | GCC_NO_COMMON_BLOCKS = YES;
327 | GCC_OPTIMIZATION_LEVEL = 0;
328 | GCC_PREPROCESSOR_DEFINITIONS = (
329 | "DEBUG=1",
330 | "$(inherited)",
331 | );
332 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
333 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
334 | GCC_WARN_UNDECLARED_SELECTOR = YES;
335 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
336 | GCC_WARN_UNUSED_FUNCTION = YES;
337 | GCC_WARN_UNUSED_VARIABLE = YES;
338 | IPHONEOS_DEPLOYMENT_TARGET = 13.1;
339 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
340 | MTL_FAST_MATH = YES;
341 | ONLY_ACTIVE_ARCH = YES;
342 | OTHER_SWIFT_FLAGS = "";
343 | SDKROOT = iphoneos;
344 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
345 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
346 | };
347 | name = Debug;
348 | };
349 | 681A72CA23661BB8002B9053 /* Release */ = {
350 | isa = XCBuildConfiguration;
351 | buildSettings = {
352 | ALWAYS_SEARCH_USER_PATHS = NO;
353 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
354 | CLANG_ANALYZER_NONNULL = YES;
355 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
356 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
357 | CLANG_CXX_LIBRARY = "libc++";
358 | CLANG_ENABLE_MODULES = YES;
359 | CLANG_ENABLE_OBJC_ARC = YES;
360 | CLANG_ENABLE_OBJC_WEAK = YES;
361 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
362 | CLANG_WARN_BOOL_CONVERSION = YES;
363 | CLANG_WARN_COMMA = YES;
364 | CLANG_WARN_CONSTANT_CONVERSION = YES;
365 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
366 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
367 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
368 | CLANG_WARN_EMPTY_BODY = YES;
369 | CLANG_WARN_ENUM_CONVERSION = YES;
370 | CLANG_WARN_INFINITE_RECURSION = YES;
371 | CLANG_WARN_INT_CONVERSION = YES;
372 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
373 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
374 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
375 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
376 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
377 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
378 | CLANG_WARN_STRICT_PROTOTYPES = YES;
379 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
380 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
381 | CLANG_WARN_UNREACHABLE_CODE = YES;
382 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
383 | COPY_PHASE_STRIP = NO;
384 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
385 | ENABLE_NS_ASSERTIONS = NO;
386 | ENABLE_STRICT_OBJC_MSGSEND = YES;
387 | GCC_C_LANGUAGE_STANDARD = gnu11;
388 | GCC_NO_COMMON_BLOCKS = YES;
389 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
390 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
391 | GCC_WARN_UNDECLARED_SELECTOR = YES;
392 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
393 | GCC_WARN_UNUSED_FUNCTION = YES;
394 | GCC_WARN_UNUSED_VARIABLE = YES;
395 | IPHONEOS_DEPLOYMENT_TARGET = 13.1;
396 | MTL_ENABLE_DEBUG_INFO = NO;
397 | MTL_FAST_MATH = YES;
398 | OTHER_SWIFT_FLAGS = "";
399 | SDKROOT = iphoneos;
400 | SWIFT_COMPILATION_MODE = wholemodule;
401 | SWIFT_OPTIMIZATION_LEVEL = "-O";
402 | VALIDATE_PRODUCT = YES;
403 | };
404 | name = Release;
405 | };
406 | 681A72CC23661BB8002B9053 /* Debug */ = {
407 | isa = XCBuildConfiguration;
408 | buildSettings = {
409 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
410 | CODE_SIGN_ENTITLEMENTS = Sudoku/Sudoku.entitlements;
411 | CODE_SIGN_STYLE = Automatic;
412 | DEVELOPMENT_ASSET_PATHS = "\"Sudoku/Preview Content\"";
413 | DEVELOPMENT_TEAM = E4RT75W4WH;
414 | ENABLE_PREVIEWS = YES;
415 | INFOPLIST_FILE = Sudoku/Info.plist;
416 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
417 | LD_RUNPATH_SEARCH_PATHS = (
418 | "$(inherited)",
419 | "@executable_path/Frameworks",
420 | );
421 | OTHER_SWIFT_FLAGS = "";
422 | PRODUCT_BUNDLE_IDENTIFIER = com.pedro.Sudoku;
423 | PRODUCT_NAME = "$(TARGET_NAME)";
424 | SUPPORTS_MACCATALYST = YES;
425 | SWIFT_OPTIMIZATION_LEVEL = "-O";
426 | SWIFT_VERSION = 5.0;
427 | TARGETED_DEVICE_FAMILY = "1,2";
428 | };
429 | name = Debug;
430 | };
431 | 681A72CD23661BB8002B9053 /* Release */ = {
432 | isa = XCBuildConfiguration;
433 | buildSettings = {
434 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
435 | CODE_SIGN_ENTITLEMENTS = Sudoku/Sudoku.entitlements;
436 | CODE_SIGN_STYLE = Automatic;
437 | DEVELOPMENT_ASSET_PATHS = "\"Sudoku/Preview Content\"";
438 | DEVELOPMENT_TEAM = E4RT75W4WH;
439 | ENABLE_PREVIEWS = YES;
440 | INFOPLIST_FILE = Sudoku/Info.plist;
441 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
442 | LD_RUNPATH_SEARCH_PATHS = (
443 | "$(inherited)",
444 | "@executable_path/Frameworks",
445 | );
446 | PRODUCT_BUNDLE_IDENTIFIER = com.pedro.Sudoku;
447 | PRODUCT_NAME = "$(TARGET_NAME)";
448 | SUPPORTS_MACCATALYST = YES;
449 | SWIFT_OPTIMIZATION_LEVEL = "-O";
450 | SWIFT_VERSION = 5.0;
451 | TARGETED_DEVICE_FAMILY = "1,2";
452 | };
453 | name = Release;
454 | };
455 | /* End XCBuildConfiguration section */
456 |
457 | /* Begin XCConfigurationList section */
458 | 681A72B223661BB7002B9053 /* Build configuration list for PBXProject "Sudoku" */ = {
459 | isa = XCConfigurationList;
460 | buildConfigurations = (
461 | 681A72C923661BB8002B9053 /* Debug */,
462 | 681A72CA23661BB8002B9053 /* Release */,
463 | );
464 | defaultConfigurationIsVisible = 0;
465 | defaultConfigurationName = Release;
466 | };
467 | 681A72CB23661BB8002B9053 /* Build configuration list for PBXNativeTarget "Sudoku" */ = {
468 | isa = XCConfigurationList;
469 | buildConfigurations = (
470 | 681A72CC23661BB8002B9053 /* Debug */,
471 | 681A72CD23661BB8002B9053 /* Release */,
472 | );
473 | defaultConfigurationIsVisible = 0;
474 | defaultConfigurationName = Release;
475 | };
476 | /* End XCConfigurationList section */
477 |
478 | /* Begin XCVersionGroup section */
479 | 68DA619824DEBE3000E545A5 /* Sudoku.xcdatamodeld */ = {
480 | isa = XCVersionGroup;
481 | children = (
482 | 68DA619924DEBE3000E545A5 /* Record.xcdatamodel */,
483 | );
484 | currentVersion = 68DA619924DEBE3000E545A5 /* Record.xcdatamodel */;
485 | path = Sudoku.xcdatamodeld;
486 | sourceTree = "";
487 | versionGroupType = wrapper.xcdatamodel;
488 | };
489 | /* End XCVersionGroup section */
490 | };
491 | rootObject = 681A72AF23661BB7002B9053 /* Project object */;
492 | }
493 |
--------------------------------------------------------------------------------