Implement the core game logic for WordScramble
This commit is contained in:
parent
15902e08a7
commit
41bbf426ce
File diff suppressed because it is too large
Load Diff
|
@ -12,8 +12,12 @@
|
||||||
F3660DA1235EC5B100FAF849 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3660DA0235EC5B100FAF849 /* Assets.xcassets */; };
|
F3660DA1235EC5B100FAF849 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3660DA0235EC5B100FAF849 /* Assets.xcassets */; };
|
||||||
F3660DA4235EC5B100FAF849 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3660DA3235EC5B100FAF849 /* Preview Assets.xcassets */; };
|
F3660DA4235EC5B100FAF849 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3660DA3235EC5B100FAF849 /* Preview Assets.xcassets */; };
|
||||||
F3660DA7235EC5B100FAF849 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F3660DA5235EC5B100FAF849 /* LaunchScreen.storyboard */; };
|
F3660DA7235EC5B100FAF849 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F3660DA5235EC5B100FAF849 /* LaunchScreen.storyboard */; };
|
||||||
F3660DB2235EC62500FAF849 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3660DB1235EC62500FAF849 /* MainView.swift */; };
|
|
||||||
F3660DB5235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3660DB4235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift */; };
|
F3660DB5235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3660DB4235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift */; };
|
||||||
|
F367BF242360873600FDEB0C /* GameViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367BF232360873600FDEB0C /* GameViewModel.swift */; };
|
||||||
|
F367BF2623608A4500FDEB0C /* GameContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367BF2523608A4500FDEB0C /* GameContainerView.swift */; };
|
||||||
|
F367BF2823608B1600FDEB0C /* GameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367BF2723608B1600FDEB0C /* GameView.swift */; };
|
||||||
|
F367BF2A23609FC400FDEB0C /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367BF2923609FC400FDEB0C /* Appearance.swift */; };
|
||||||
|
F367BF2F2360CEF800FDEB0C /* game-words.txt in Resources */ = {isa = PBXBuildFile; fileRef = F367BF2E2360CEF800FDEB0C /* game-words.txt */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -24,8 +28,12 @@
|
||||||
F3660DA3235EC5B100FAF849 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
F3660DA3235EC5B100FAF849 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
F3660DA6235EC5B100FAF849 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
F3660DA6235EC5B100FAF849 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
F3660DA8235EC5B100FAF849 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
F3660DA8235EC5B100FAF849 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
F3660DB1235EC62500FAF849 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
|
||||||
F3660DB4235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+DecodeFromFileName.swift"; sourceTree = "<group>"; };
|
F3660DB4235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+DecodeFromFileName.swift"; sourceTree = "<group>"; };
|
||||||
|
F367BF232360873600FDEB0C /* GameViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
F367BF2523608A4500FDEB0C /* GameContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameContainerView.swift; sourceTree = "<group>"; };
|
||||||
|
F367BF2723608B1600FDEB0C /* GameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameView.swift; sourceTree = "<group>"; };
|
||||||
|
F367BF2923609FC400FDEB0C /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = "<group>"; };
|
||||||
|
F367BF2E2360CEF800FDEB0C /* game-words.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "game-words.txt"; path = "Resources/game-words.txt"; sourceTree = SOURCE_ROOT; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -60,11 +68,12 @@
|
||||||
children = (
|
children = (
|
||||||
F3660DA8235EC5B100FAF849 /* Info.plist */,
|
F3660DA8235EC5B100FAF849 /* Info.plist */,
|
||||||
F3660D9A235EC5B000FAF849 /* AppDelegate.swift */,
|
F3660D9A235EC5B000FAF849 /* AppDelegate.swift */,
|
||||||
|
F367BF2923609FC400FDEB0C /* Appearance.swift */,
|
||||||
F3660D9C235EC5B000FAF849 /* SceneDelegate.swift */,
|
F3660D9C235EC5B000FAF849 /* SceneDelegate.swift */,
|
||||||
F3660DA0235EC5B100FAF849 /* Assets.xcassets */,
|
|
||||||
F3660DAF235EC5FF00FAF849 /* Data */,
|
F3660DAF235EC5FF00FAF849 /* Data */,
|
||||||
F3660DA5235EC5B100FAF849 /* LaunchScreen.storyboard */,
|
F3660DA5235EC5B100FAF849 /* LaunchScreen.storyboard */,
|
||||||
F3660DA2235EC5B100FAF849 /* Preview Content */,
|
F3660DA2235EC5B100FAF849 /* Preview Content */,
|
||||||
|
F367BF2D2360CEAA00FDEB0C /* Resources */,
|
||||||
F3660DB0235EC60600FAF849 /* Reusables */,
|
F3660DB0235EC60600FAF849 /* Reusables */,
|
||||||
F3660DAE235EC5FA00FAF849 /* Scenes */,
|
F3660DAE235EC5FA00FAF849 /* Scenes */,
|
||||||
);
|
);
|
||||||
|
@ -82,7 +91,7 @@
|
||||||
F3660DAE235EC5FA00FAF849 /* Scenes */ = {
|
F3660DAE235EC5FA00FAF849 /* Scenes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F3660DB1235EC62500FAF849 /* MainView.swift */,
|
F367BF222360872900FDEB0C /* Game */,
|
||||||
);
|
);
|
||||||
path = Scenes;
|
path = Scenes;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -110,6 +119,25 @@
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
F367BF222360872900FDEB0C /* Game */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F367BF232360873600FDEB0C /* GameViewModel.swift */,
|
||||||
|
F367BF2523608A4500FDEB0C /* GameContainerView.swift */,
|
||||||
|
F367BF2723608B1600FDEB0C /* GameView.swift */,
|
||||||
|
);
|
||||||
|
path = Game;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
F367BF2D2360CEAA00FDEB0C /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F3660DA0235EC5B100FAF849 /* Assets.xcassets */,
|
||||||
|
F367BF2E2360CEF800FDEB0C /* game-words.txt */,
|
||||||
|
);
|
||||||
|
path = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
@ -170,6 +198,7 @@
|
||||||
files = (
|
files = (
|
||||||
F3660DA7235EC5B100FAF849 /* LaunchScreen.storyboard in Resources */,
|
F3660DA7235EC5B100FAF849 /* LaunchScreen.storyboard in Resources */,
|
||||||
F3660DA4235EC5B100FAF849 /* Preview Assets.xcassets in Resources */,
|
F3660DA4235EC5B100FAF849 /* Preview Assets.xcassets in Resources */,
|
||||||
|
F367BF2F2360CEF800FDEB0C /* game-words.txt in Resources */,
|
||||||
F3660DA1235EC5B100FAF849 /* Assets.xcassets in Resources */,
|
F3660DA1235EC5B100FAF849 /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -183,8 +212,11 @@
|
||||||
files = (
|
files = (
|
||||||
F3660DB5235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift in Sources */,
|
F3660DB5235EC9D000FAF849 /* Bundle+DecodeFromFileName.swift in Sources */,
|
||||||
F3660D9B235EC5B000FAF849 /* AppDelegate.swift in Sources */,
|
F3660D9B235EC5B000FAF849 /* AppDelegate.swift in Sources */,
|
||||||
F3660DB2235EC62500FAF849 /* MainView.swift in Sources */,
|
F367BF242360873600FDEB0C /* GameViewModel.swift in Sources */,
|
||||||
F3660D9D235EC5B000FAF849 /* SceneDelegate.swift in Sources */,
|
F3660D9D235EC5B000FAF849 /* SceneDelegate.swift in Sources */,
|
||||||
|
F367BF2A23609FC400FDEB0C /* Appearance.swift in Sources */,
|
||||||
|
F367BF2823608B1600FDEB0C /* GameView.swift in Sources */,
|
||||||
|
F367BF2623608A4500FDEB0C /* GameContainerView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// Appearance.swift
|
||||||
|
// WordScramble
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 10/23/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
enum Appearance {
|
||||||
|
|
||||||
|
enum Navbar {
|
||||||
|
static let `default`: UINavigationBarAppearance = {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
|
||||||
|
appearance.titleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor.systemPink,
|
||||||
|
]
|
||||||
|
|
||||||
|
appearance.largeTitleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor.systemPink,
|
||||||
|
]
|
||||||
|
|
||||||
|
appearance.configureWithTransparentBackground()
|
||||||
|
|
||||||
|
return appearance
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func set(navBarAppearance: UINavigationBarAppearance) {
|
||||||
|
UINavigationBar.appearance().standardAppearance = navBarAppearance
|
||||||
|
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,19 +13,18 @@ extension Bundle {
|
||||||
|
|
||||||
public func createString(
|
public func createString(
|
||||||
fromFileNamed fileName: String,
|
fromFileNamed fileName: String,
|
||||||
withExtension extensionName: String? = nil
|
withExtension extensionName: String? = nil,
|
||||||
) throws -> Result<String, Error> {
|
then completionHandler: ((Result<String, Error>) -> Void)
|
||||||
|
) {
|
||||||
guard let url = url(forResource: fileName, withExtension: extensionName) else {
|
guard let url = url(forResource: fileName, withExtension: extensionName) else {
|
||||||
return .success("")
|
let fileDebugName = extensionName == nil ? fileName : "\(fileName).\(extensionName)"
|
||||||
|
fatalError("No url found for file named \(fileDebugName)")
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
return .success(try String(contentsOf: url))
|
completionHandler(.success(try String(contentsOf: url)))
|
||||||
} catch {
|
} catch {
|
||||||
return .failure(error)
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||||
|
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
let contentView = MainView()
|
|
||||||
|
|
||||||
// Use a UIHostingController as window root view controller.
|
// Use a UIHostingController as window root view controller.
|
||||||
if let windowScene = scene as? UIWindowScene {
|
if let windowScene = scene as? UIWindowScene {
|
||||||
let window = UIWindow(windowScene: windowScene)
|
let window = UIWindow(windowScene: windowScene)
|
||||||
|
let contentView = GameContainerView()
|
||||||
|
|
||||||
window.rootViewController = UIHostingController(rootView: contentView)
|
window.rootViewController = UIHostingController(rootView: contentView)
|
||||||
|
Appearance.set(navBarAppearance: Appearance.Navbar.default)
|
||||||
|
|
||||||
self.window = window
|
self.window = window
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// GameContainerView.swift
|
||||||
|
// WordScramble
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 10/22/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
struct GameContainerView: View {
|
||||||
|
@ObservedObject var gameViewModel = GameViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
extension GameContainerView {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GameView(viewModel: gameViewModel)
|
||||||
|
.onAppear {
|
||||||
|
self.loadWords()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
extension GameContainerView {
|
||||||
|
|
||||||
|
private func loadWords() {
|
||||||
|
Bundle.main.createString(fromFileNamed: "game-words", withExtension: "txt") { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let string):
|
||||||
|
self.gameViewModel.allRootWords = string.components(separatedBy: "\n")
|
||||||
|
self.gameViewModel.startNewRound()
|
||||||
|
case .failure:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
struct MainView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
return GameContainerView(gameViewModel: GameViewModel(rootWords: sampleWords))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
//
|
||||||
|
// GameView.swift
|
||||||
|
// WordScramble
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 10/23/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
struct GameView: View {
|
||||||
|
@ObservedObject var viewModel: GameViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
extension GameView {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
VStack {
|
||||||
|
VStack {
|
||||||
|
Text("Choose an anagram for")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Text("\(viewModel.currentRootWord)")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.foregroundColor(.pink)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
"Enter your word",
|
||||||
|
text: $viewModel.currentGuess,
|
||||||
|
onCommit: viewModel.checkNewWord
|
||||||
|
)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
List(viewModel.usedWords, id: \.self) { word in
|
||||||
|
Text(word)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "\(word.count).circle")
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitle("Anagrams")
|
||||||
|
.alert(isPresented: $viewModel.shouldShowErrorAlert) {
|
||||||
|
Alert(
|
||||||
|
title: Text(self.viewModel.errorTitle),
|
||||||
|
message: Text(self.viewModel.errorMessage),
|
||||||
|
dismissButton: .default(Text("OK"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
struct GameView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
GameView(viewModel: GameViewModel(rootWords: sampleWords))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
//
|
||||||
|
// GameViewModel.swift
|
||||||
|
// WordScramble
|
||||||
|
//
|
||||||
|
// Created by CypherPoet on 10/23/19.
|
||||||
|
// ✌️
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
final class GameViewModel: ObservableObject {
|
||||||
|
@Published var allRootWords: [String] = []
|
||||||
|
@Published var currentRootWord: String = ""
|
||||||
|
@Published var currentGuess: String = ""
|
||||||
|
@Published var usedWords: [String] = []
|
||||||
|
|
||||||
|
|
||||||
|
@Published var shouldShowErrorAlert: Bool = false
|
||||||
|
@Published var errorTitle = ""
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
|
||||||
|
private static let textChecker = UITextChecker()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
extension GameViewModel {
|
||||||
|
convenience init(rootWords: [String]) {
|
||||||
|
self.init()
|
||||||
|
self.allRootWords = rootWords
|
||||||
|
|
||||||
|
startNewRound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Computeds
|
||||||
|
extension GameViewModel {
|
||||||
|
|
||||||
|
var currentGuessIsOriginal: Bool {
|
||||||
|
!usedWords.contains(currentGuess)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var currentGuessIsAnagram: Bool {
|
||||||
|
let guessSet = NSCountedSet(array: Array(currentGuess))
|
||||||
|
let rootWordSet = NSCountedSet(array: Array(currentRootWord))
|
||||||
|
|
||||||
|
return guessSet.allSatisfy { character in
|
||||||
|
guessSet.count(for: character) <= rootWordSet.count(for: character)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var currentGuessIsRealWord: Bool {
|
||||||
|
let range = NSRange(location: 0, length: currentRootWord.utf16.count)
|
||||||
|
|
||||||
|
let misspelledRange = Self.textChecker.rangeOfMisspelledWord(
|
||||||
|
in: currentRootWord,
|
||||||
|
range: range,
|
||||||
|
startingAt: 0,
|
||||||
|
wrap: false,
|
||||||
|
language: "en"
|
||||||
|
)
|
||||||
|
|
||||||
|
return misspelledRange.location == NSNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
extension GameViewModel {
|
||||||
|
|
||||||
|
func startNewRound() {
|
||||||
|
usedWords.removeAll(keepingCapacity: true)
|
||||||
|
currentGuess = ""
|
||||||
|
|
||||||
|
currentRootWord = allRootWords.randomElement()!
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func checkNewWord() {
|
||||||
|
let word = currentGuess.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
guard !word.isEmpty else { return } // TODO: Better error handling here
|
||||||
|
|
||||||
|
guard word != currentRootWord else {
|
||||||
|
setWordError(
|
||||||
|
title: "Mix it up!",
|
||||||
|
message: "Your answer shouldn't match the original word."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard currentGuessIsOriginal else {
|
||||||
|
setWordError(
|
||||||
|
title: "Be original!",
|
||||||
|
message: "You've already used \"\(word)\" as an anagram for \"\(currentRootWord)\"."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard currentGuessIsAnagram else {
|
||||||
|
setWordError(
|
||||||
|
title: "Try Again",
|
||||||
|
message: "\"\(word)\" is not an anagram for \"\(currentRootWord)\"."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard currentGuessIsRealWord else {
|
||||||
|
setWordError(
|
||||||
|
title: "Is that a word?",
|
||||||
|
message: "Unfortunatley, we don't recognize \"\(word)\" as a valid English word 🤷♂️."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
usedWords.insert(word, at: 0)
|
||||||
|
currentGuess = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension GameViewModel {
|
||||||
|
|
||||||
|
func setWordError(title: String, message: String) {
|
||||||
|
errorTitle = title
|
||||||
|
errorMessage = message
|
||||||
|
shouldShowErrorAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
let sampleWords = [
|
||||||
|
"coffee",
|
||||||
|
"cambridge",
|
||||||
|
"digital",
|
||||||
|
"agency",
|
||||||
|
"wordsmith",
|
||||||
|
"fahrenheit",
|
||||||
|
"network",
|
||||||
|
]
|
||||||
|
#endif
|
|
@ -1,31 +0,0 @@
|
||||||
//
|
|
||||||
// MainView.swift
|
|
||||||
// WordScramble
|
|
||||||
//
|
|
||||||
// Created by CypherPoet on 10/22/19.
|
|
||||||
// ✌️
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
|
|
||||||
struct MainView: View {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Body
|
|
||||||
extension MainView {
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Preview
|
|
||||||
struct MainView_Previews: PreviewProvider {
|
|
||||||
|
|
||||||
static var previews: some View {
|
|
||||||
MainView()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,6 +23,7 @@ From the description:
|
||||||
>
|
>
|
||||||
> For example, if the starter word is “alarming” they might spell “alarm”, “ring”, “main”, and so on.
|
> For example, if the starter word is “alarming” they might spell “alarm”, “ring”, “main”, and so on.
|
||||||
|
|
||||||
|
These "words from another word" are also known as [anagrams](https://en.wikipedia.org/wiki/Anagram).
|
||||||
|
|
||||||
## Introducing List, your best friend
|
## Introducing List, your best friend
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Day 30: _Project 5: WordScramble_ (Part One)
|
||||||
|
|
||||||
|
_Follow along at https://www.hackingwithswift.com/100/swiftui/30_.
|
||||||
|
|
||||||
|
|
||||||
|
# 📒 Field Notes
|
||||||
|
|
||||||
|
This day covers Part One of _`Project 5: WordScramble`_ in the [100 Days of SwiftUI Challenge](https://www.hackingwithswift.com/100/swiftui/30).
|
||||||
|
|
||||||
|
It focuses on several specific topics:
|
||||||
|
|
||||||
|
- Word Scramble: Introduction
|
||||||
|
- Introducing List, your best friend
|
||||||
|
- Loading resources from your app bundle
|
||||||
|
- Working with strings
|
||||||
|
|
||||||
|
|
||||||
|
## Word Scramble: Introduction
|
||||||
|
|
||||||
|
From the description:
|
||||||
|
|
||||||
|
> The game will show players a random eight-letter word, and ask them to make words out of it.
|
||||||
|
>
|
||||||
|
> For example, if the starter word is “alarming” they might spell “alarm”, “ring”, “main”, and so on.
|
||||||
|
|
||||||
|
|
||||||
|
## Introducing List, your best friend
|
||||||
|
|
||||||
|
`List`s are essentially SwiftUI's version of UIKit's TableView. But one neat difference is their ability to seamlessly integrate static and dynamic content within the same `List` element:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
List {
|
||||||
|
Section(header: Text("Section 1")) {
|
||||||
|
Text("Static row 1")
|
||||||
|
Text("Static row 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Section 2")) {
|
||||||
|
ForEach(0..<5) {
|
||||||
|
Text("Dynamic row \($0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Section 3")) {
|
||||||
|
Text("Static row 3")
|
||||||
|
Text("Static row 4")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Oh... and, that tight integration with the `Section` element is pretty sweet, too 🙂.
|
||||||
|
|
||||||
|
|
||||||
|
## Loading resources from your app bundle
|
||||||
|
|
||||||
|
Whenever we have something in our app's `Bundle` that we want to deal with in code, we first need to locate it with a URL (which is why it's called a "Uniform Resource Locator").
|
||||||
|
|
||||||
|
In many cases, we'd use this URL to create an instance of `Data`, and then decode that data into some kind of structured model based upon the structure of the file.
|
||||||
|
|
||||||
|
In this app, though, we'll be grabbing the contents of a plain-text file that lacks the structure of something like JSON.
|
||||||
|
|
||||||
|
|
||||||
|
Fortunately, because Swift `String`s are weapons-grade, we can also create them directly from the content's of a file:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
if let fileContents = try? String(contentsOf: fileURL) {
|
||||||
|
// we loaded the file into a string!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue