Last active
July 3, 2019 06:57
-
-
Save epaga/720e8c8c31e53808dbf8b2243a9c8fea to your computer and use it in GitHub Desktop.
A simple SwiftUI Tic Tac Toe game I made together with my son to learn SwiftUI together
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// TicTacToe | |
// | |
// Created by John Goering on 08.06.19. | |
// Copyright © 2019 John Goering. All rights reserved. | |
// | |
import SwiftUI | |
import Combine | |
class GameData : BindableObject { | |
var turnIsX = true { | |
didSet { | |
didChange.send(Void()) | |
} | |
} | |
var game:[String] { | |
didSet { | |
didChange.send(Void()) | |
} | |
} | |
init(game:[String] = [ | |
" "," "," ", | |
" "," "," ", | |
" "," "," " | |
]) { | |
self.game = game | |
} | |
var didChange = PassthroughSubject<Void, Never>() | |
func reset() { | |
game = [ | |
" "," "," ", | |
" "," "," ", | |
" "," "," " | |
] | |
turnIsX = true | |
} | |
var winningIndexes: [Int]? { | |
get { | |
let waysToWin:[[Int]] = [ | |
[0,1,2], | |
[3,4,5], | |
[6,7,8], | |
[0,3,6], | |
[1,4,7], | |
[2,5,8], | |
[0,4,8], | |
[2,4,6] | |
] | |
return waysToWin.first{ | |
wayToWin in | |
return game[wayToWin[0]] == game[wayToWin[1]] && | |
game[wayToWin[1]] == game[wayToWin[2]] && | |
game[wayToWin[0]] != " " | |
} | |
} | |
} | |
var gameIsOver: Bool { | |
get { | |
return winningIndexes != nil || | |
game.first {$0 == " "} == nil | |
} | |
} | |
} | |
struct ContentView : View { | |
@ObjectBinding var game:GameData | |
var body: some View { | |
let turnMessage = game.gameIsOver ? "Game Over!" : | |
"It's \(game.turnIsX ? "X" : "O")'s turn!" | |
return ZStack { | |
VStack(spacing: 0) { | |
Text(turnMessage) | |
.font(.largeTitle) | |
.padding() | |
Spacer() | |
Row(rowIndex: 0, game:game) | |
Row(rowIndex: 1, game:game) | |
Row(rowIndex: 2, game:game) | |
Spacer() | |
}.background(Color(white: 0.8)) | |
if game.gameIsOver { | |
ResetButton(game: game) | |
} | |
} | |
} | |
} | |
struct ResetButton : View { | |
@ObjectBinding var game:GameData | |
var body: some View { | |
VStack { | |
Spacer() | |
Button(action: { | |
self.game.reset() | |
}) { | |
Text("Reset") | |
.font(.largeTitle) | |
} | |
.padding() | |
.background(Color.black) | |
.cornerRadius(10) | |
.offset(x: 0, y: -20) | |
} | |
} | |
} | |
struct Row : View { | |
var rowIndex:Int | |
@ObjectBinding var game:GameData | |
var body: some View { | |
HStack(spacing: 0) { | |
Field(rowIndex: rowIndex, colIndex: 0, game:game) | |
Field(rowIndex: rowIndex, colIndex: 1, game:game) | |
Field(rowIndex: rowIndex, colIndex: 2, game:game) | |
} | |
} | |
} | |
struct Field : View { | |
var rowIndex:Int | |
var colIndex:Int | |
@ObjectBinding var game:GameData | |
var body: some View { | |
let gameIndex = rowIndex * 3 + colIndex | |
let isWinningIndex = (game.winningIndexes ?? []).contains(gameIndex) | |
return ZStack { | |
if isWinningIndex { | |
Color.gray | |
.border(Color.black) | |
.animation(.basic()) | |
} else { | |
Color.white | |
.border(Color.black) | |
.animation(.basic()) | |
} | |
Text(game.game[gameIndex]) | |
.font(.system(size: 100)) | |
.color(isWinningIndex ? Color.red : Color.black ) | |
} | |
.tapAction { | |
if self.game.game[gameIndex] == " " { | |
if self.game.turnIsX { | |
self.game.game[gameIndex] = "X" | |
} else { | |
self.game.game[gameIndex] = "O" | |
} | |
self.game.turnIsX.toggle() | |
} | |
} | |
} | |
} | |
#if DEBUG | |
struct ContentView_Previews : PreviewProvider { | |
static var previews: some View { | |
ContentView(game:GameData(game: [ | |
"X"," "," ", | |
" ","O"," ", | |
" "," ","X" | |
])) | |
} | |
} | |
#endif |
After closer inspection I have made some changes to the two platform initialisers and to the debug code initialisation:
- HostingController.swift for watchOS
class HostingController : WKHostingController<ContentView> {
override var body: ContentView {
return ContentView(game: GameData()) // EJG: initialise added for watchOS hosting
}
}
or, 2. SceneDelegate.swift for iOS (this would have been in AppDelegate.swift in the past, but this split was split out for SwiftUI
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView(game:GameData())) // EJG: initialise added for iOS hosting
self.window = window
window.makeKeyAndVisible()
}
And I noticed that debug game with its two Xs and one O was incorrectly initialised (given that the default for turnIsX = true); so I could have initialised the board to all " " (and at one stage I did), or rescued it as above from two Xs and an O to simple one X and one O; however I decided to set ip up follows, as it enables a discussion around forking as a strategy to win:
- for both iOS and watchOS the debug section now reads as follows:
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(game:GameData(game: [
"X"," "," ",
" ","O"," ",
"O"," ","X"
]))
}
}
#endif
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I communicated with John that I had taken his could work to clone and port to watchOS as an exercise in how a quite complex UI could be transitioned. I have left my workings in places as // comments where I tried something before moving on to finally ending up with a UI design the works in the 40mm Watch simulator. I said to John I would "branch" his .gist however seems I can not branch nor attach my watchOS SwiftUI code as a file; however I can inline it below ... I hope this is useful for others ... I am sure John will check/ test my workings and comment appropriately. Thanks John for giving me a great learning opportunity.