diff --git a/WordleWithFriends.xcodeproj/project.pbxproj b/WordleWithFriends.xcodeproj/project.pbxproj index d000400..b6f75e0 100644 --- a/WordleWithFriends.xcodeproj/project.pbxproj +++ b/WordleWithFriends.xcodeproj/project.pbxproj @@ -65,6 +65,8 @@ 6C94887C27D0AB15005252F1 /* LetterStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94887B27D0AB15005252F1 /* LetterStateTests.swift */; }; 6C94887E27D0B4E4005252F1 /* KeyboardRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94887D27D0B4E4005252F1 /* KeyboardRow.swift */; }; 6C94F50227D5C23D00C19D43 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F50127D5C23D00C19D43 /* Array+Extension.swift */; }; + 6C94F4F727D4494C00C19D43 /* GameInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F4F627D4494C00C19D43 /* GameInstructionsViewController.swift */; }; + 6C94F4FA27D44A8100C19D43 /* HorizontalSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F4F927D44A8100C19D43 /* HorizontalSeparatorView.swift */; }; 6CBBCECF279BDC7D00875C30 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBBCECE279BDC7D00875C30 /* Double+Extension.swift */; }; 6CBCC3CD2797565D005EB254 /* DismissableAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBCC3CC2797565D005EB254 /* DismissableAlertController.swift */; }; 6CBCC3D1279764FF005EB254 /* CGPoint+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBCC3D0279764FF005EB254 /* CGPoint+Extension.swift */; }; @@ -154,6 +156,8 @@ 6C94887B27D0AB15005252F1 /* LetterStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterStateTests.swift; sourceTree = ""; }; 6C94887D27D0B4E4005252F1 /* KeyboardRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardRow.swift; sourceTree = ""; }; 6C94F50127D5C23D00C19D43 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; + 6C94F4F627D4494C00C19D43 /* GameInstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameInstructionsViewController.swift; sourceTree = ""; }; + 6C94F4F927D44A8100C19D43 /* HorizontalSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalSeparatorView.swift; sourceTree = ""; }; 6CBBCECE279BDC7D00875C30 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; 6CBCC3CC2797565D005EB254 /* DismissableAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableAlertController.swift; sourceTree = ""; }; 6CBCC3D0279764FF005EB254 /* CGPoint+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Extension.swift"; sourceTree = ""; }; @@ -294,6 +298,7 @@ 6C19F59C27921EFA00062083 /* AppDelegate.swift */, 6C19F59E27921EFA00062083 /* SceneDelegate.swift */, 6C19F5A027921EFA00062083 /* GameSetupViewController.swift */, + 6C94F4F527D4493A00C19D43 /* How To Play */, 60D948072793BB0D00086C51 /* Settings */, 6C19F5D52792356500062083 /* ClueGuessViewController.swift */, 6C19F5A527921EFB00062083 /* Assets.xcassets */, @@ -396,6 +401,23 @@ path = CustomKeyboard; sourceTree = ""; }; + 6C94F4F527D4493A00C19D43 /* How To Play */ = { + isa = PBXGroup; + children = ( + 6C94F4F827D44A7200C19D43 /* Views */, + 6C94F4F627D4494C00C19D43 /* GameInstructionsViewController.swift */, + ); + path = "How To Play"; + sourceTree = ""; + }; + 6C94F4F827D44A7200C19D43 /* Views */ = { + isa = PBXGroup; + children = ( + 6C94F4F927D44A8100C19D43 /* HorizontalSeparatorView.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -560,7 +582,9 @@ 6C19F5E527923B1400062083 /* WordGuessRow.swift in Sources */, 6C94887127D07FD4005252F1 /* WordleKeyboardInputView.swift in Sources */, 6CBBCECF279BDC7D00875C30 /* Double+Extension.swift in Sources */, + 6C94F4FA27D44A8100C19D43 /* HorizontalSeparatorView.swift in Sources */, 6C19F60227923F8400062083 /* LetterTileView.swift in Sources */, + 6C94F4F727D4494C00C19D43 /* GameInstructionsViewController.swift in Sources */, 6CBCC3CD2797565D005EB254 /* DismissableAlertController.swift in Sources */, 60D948062793AE1400086C51 /* GameSettingsViewController.swift in Sources */, 6C94887827D0AAD9005252F1 /* LetterState.swift in Sources */, diff --git a/WordleWithFriends/Extensions/NSLayoutConstraint+Extension.swift b/WordleWithFriends/Extensions/NSLayoutConstraint+Extension.swift index afa70e8..e973e21 100644 --- a/WordleWithFriends/Extensions/NSLayoutConstraint+Extension.swift +++ b/WordleWithFriends/Extensions/NSLayoutConstraint+Extension.swift @@ -8,5 +8,13 @@ import UIKit extension NSLayoutConstraint { + func with(priority: UILayoutPriority) -> NSLayoutConstraint { + self.priority = priority + + return self + } + func with(priority: Int) -> NSLayoutConstraint { + return with(priority: UILayoutPriority(priority.asFloat)) + } } diff --git a/WordleWithFriends/Extensions/String+Extension.swift b/WordleWithFriends/Extensions/String+Extension.swift index e527280..24bc0e2 100644 --- a/WordleWithFriends/Extensions/String+Extension.swift +++ b/WordleWithFriends/Extensions/String+Extension.swift @@ -43,6 +43,32 @@ extension String { } } +extension String { + var asAttributedString: NSAttributedString { .init(string: self) } + + var bolded: NSMutableAttributedString { + .init(string: self, attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.labelFontSize)]) + } + + func appending(_ attributedString: NSAttributedString) -> NSMutableAttributedString { + let mutableString = NSMutableAttributedString(string: self) + mutableString.append(attributedString) + + return mutableString + } +} + +extension NSMutableAttributedString { + func appending(_ attributedString: NSAttributedString) -> NSMutableAttributedString { + append(attributedString) + return self + } + + func appending(_ string: String) -> NSMutableAttributedString { + appending(string.asAttributedString) + } +} + extension StringProtocol { subscript(offset: Int) -> Character { self[index(startIndex, offsetBy: offset)] diff --git a/WordleWithFriends/GameSetupViewController.swift b/WordleWithFriends/GameSetupViewController.swift index a8487fc..ac5f7d6 100644 --- a/WordleWithFriends/GameSetupViewController.swift +++ b/WordleWithFriends/GameSetupViewController.swift @@ -96,13 +96,30 @@ final class GameSetupViewController: UIViewController { return button }() + private lazy var instructionsButton: UIBarButtonItem = { + let button = UIBarButtonItem(title: "❓", style: .plain, target: self, action: #selector(showInstructions)) + button.accessibilityLabel = "How to play" + + return button + }() + + private lazy var instructionsTextLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center + + return label + }() + private lazy var startGameButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("Start", for: .normal) button.addTarget(self, action: #selector(checkAndInitiateGame), for: .touchUpInside) - button.titleLabel?.font = .boldSystemFont(ofSize: 16.0) + button.titleLabel?.font = .boldSystemFont(ofSize: UIFont.buttonFontSize) button.setTitleColor(.systemGray, for: .disabled) + button.setTitleColor(.systemGreen, for: .normal) button.isEnabled = false button.isHidden = true @@ -138,6 +155,7 @@ final class GameSetupViewController: UIViewController { view.backgroundColor = .systemBackground navigationItem.leftBarButtonItem = settingsButton + navigationItem.rightBarButtonItem = instructionsButton let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false @@ -192,6 +210,12 @@ final class GameSetupViewController: UIViewController { navigationController?.present(UINavigationController(rootViewController: vc), animated: true) } + @objc private func showInstructions() { + let vc = GameInstructionsViewController() + + navigationController?.present(UINavigationController(rootViewController: vc), animated: true) + } + func updateScreen() { let clueLength = GameSettings.clueLength.readIntValue().spelledOut ?? " " humanInstructionsTextLabel.text = "Enter your \(GameSettings.clueLength.readIntValue().spelledOut ?? " ")-letter clue below:" diff --git a/WordleWithFriends/GameSetupViewController.swift.orig b/WordleWithFriends/GameSetupViewController.swift.orig new file mode 100644 index 0000000..5ed3a2e --- /dev/null +++ b/WordleWithFriends/GameSetupViewController.swift.orig @@ -0,0 +1,347 @@ +// +// GameSetupViewController.swift +// WordleWithFriends +// +// Created by Geoffrey Liu on 1/14/22. +// + +import UIKit +import AudioToolbox + +final class GameSetupViewController: UIViewController { + + private var selectedGamemode: GameMode? { + didSet { + switch selectedGamemode { + case .some(.human): + // WHY THIS SO LAGGY??? + humanInstructionsTextLabel.isHidden = false + clueTextField.isHidden = false + clueTextField.becomeFirstResponder() + startGameButton.isHidden = false + versusHumanButton.isHidden = true + versusComputerButton.isHidden = true + infiniteModeButton.isHidden = true + switchGamemodeButton.isHidden = false + case .none, + .some(.computer), + .some(.infinite): + humanInstructionsTextLabel.isHidden = true + startGameButton.isHidden = true + clueTextField.isHidden = true + clueTextField.resignFirstResponder() + versusHumanButton.isHidden = false + versusComputerButton.isHidden = false + infiniteModeButton.isHidden = false + switchGamemodeButton.isHidden = true + } + } + } + + private lazy var settingsButton: UIBarButtonItem = { + let button = UIBarButtonItem(title: "⚙", style: .plain, target: self, action: #selector(openSettings)) + button.accessibilityLabel = "Game settings" + + return button + }() + + private lazy var welcomeTextLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center + label.text = "Welcome to Wordle With Friends!" + label.font = UIFont.boldSystemFont(ofSize: 24.0) // TODO: Dynamic font sizes + + return label + }() + + private lazy var humanInstructionsTextLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center + label.isHidden = true + + return label + }() + + private lazy var versusHumanButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false +<<<<<<< HEAD + button.setTitle("Play vs. human", for: .normal) + button.addTarget(self, action: #selector(promptForClue), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: 16.0) +======= + button.setTitle("Start", for: .normal) + button.setTitleColor(.systemGreen, for: .normal) + button.addTarget(self, action: #selector(checkAndInitiateGame), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: UIFont.buttonFontSize) + button.setTitleColor(.systemGray, for: .disabled) + button.isEnabled = false +>>>>>>> b3f7d16 (Teaser to settings) + + return button + }() + + private lazy var versusComputerButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false +<<<<<<< HEAD + button.setTitle("Play vs. computer", for: .normal) + button.addTarget(self, action: #selector(initiateGameVersusComputer), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: 16.0) +======= + button.setTitle("Random word", for: .normal) + button.setTitleColor(.systemOrange, for: .normal) + button.addTarget(self, action: #selector(initiateGameWithRandomWord), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: UIFont.buttonFontSize) +>>>>>>> b3f7d16 (Teaser to settings) + + return button + }() + + private lazy var infiniteModeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Infinite mode", for: .normal) + button.addTarget(self, action: #selector(initiateGameOnInfiniteMode), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: UIFont.buttonFontSize) + + return button + }() + + private lazy var instructionsButton: UIBarButtonItem = { + let button = UIBarButtonItem(title: "❓", style: .plain, target: self, action: #selector(showInstructions)) + button.accessibilityLabel = "How to play" + + return button + }() + + private lazy var instructionsTextLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center + + return label + }() + + private lazy var startGameButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Start", for: .normal) + button.addTarget(self, action: #selector(checkAndInitiateGame), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: 16.0) + button.setTitleColor(.systemGray, for: .disabled) + button.isEnabled = false + button.isHidden = true + + return button + }() + + private lazy var switchGamemodeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Switch gamemode", for: .normal) + button.addTarget(self, action: #selector(resetGamemode), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: 16.0) + button.setTitleColor(.systemGray, for: .disabled) + button.isHidden = true + + return button + }() + + private lazy var clueTextField: UITextField = { + let textField = WordInputTextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.delegate = self + textField.accessibilityIdentifier = "GameSetupViewController.clueTextField" + textField.isHidden = true + + return textField + }() + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + title = "Wordle with Friends" + view.backgroundColor = .systemBackground + + navigationItem.leftBarButtonItem = settingsButton + navigationItem.rightBarButtonItem = instructionsButton + + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 8.0 + + updateScreen() + stackView.addArrangedSubview(welcomeTextLabel) + stackView.addArrangedSubview(humanInstructionsTextLabel) + stackView.addArrangedSubview(clueTextField) + stackView.addArrangedSubview(startGameButton) + stackView.addArrangedSubview(switchGamemodeButton) + stackView.addArrangedSubview(versusHumanButton) + stackView.addArrangedSubview(versusComputerButton) + stackView.addArrangedSubview(infiniteModeButton) + view.addSubview(stackView) + + stackView.setCustomSpacing(32.0, after: welcomeTextLabel) + stackView.setCustomSpacing(16.0, after: humanInstructionsTextLabel) + + let maxWidth = LayoutUtility.size(screenWidthPercentage: 85.0, maxWidth: 300) + + NSLayoutConstraint.activate([ + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + clueTextField.widthAnchor.constraint(equalToConstant: CGFloat(maxWidth)), + ]) + + updateScreen() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + view.endEditing(true) + startGameButton.isEnabled = false + + NotificationCenter.default.removeObserver(self, name: UITextField.textDidChangeNotification, object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + startGameButton.isEnabled = false + + NotificationCenter.default.addObserver(self, selector: #selector(textFieldDidUpdate), name: UITextField.textDidChangeNotification, object: nil) + } + + @objc private func openSettings() { + let vc = GameSettingsViewController() + vc.delegate = self + navigationController?.present(UINavigationController(rootViewController: vc), animated: true) + } + + @objc private func showInstructions() { + let vc = GameInstructionsViewController() + + navigationController?.present(UINavigationController(rootViewController: vc), animated: true) + } + + func updateScreen() { + let clueLength = GameSettings.clueLength.readIntValue().spelledOut ?? " " + humanInstructionsTextLabel.text = "Enter your \(GameSettings.clueLength.readIntValue().spelledOut ?? " ")-letter clue below:" + + clueTextField.accessibilityLabel = "Enter a \(clueLength)-letter word here." + } + + @objc private func promptForClue() { + // TODO: Ensure this stays uptodate + selectedGamemode = .human + } + + @objc private func textFieldDidUpdate(_ notification: Notification) { + guard let textField = notification.object as? UITextField else { + return + } + + if textField.text?.count == GameSettings.clueLength.readIntValue() { + startGameButton.isEnabled = true + } else { + startGameButton.isEnabled = false + } + } + + private func isWordValid() -> WordValidity { + guard let inputText = clueTextField.text else { + return .missingWord + } + + if inputText.count < GameSettings.clueLength.readIntValue() { + return .insufficientLength + } else if inputText.count > GameSettings.clueLength.readIntValue() { + return .excessLength + } + + if !inputText.isARealWord() { + return .notDefined + } + + return .valid + } + + @objc private func checkAndInitiateGame() -> Bool { + let wordValidity = isWordValid() + let isValid = wordValidity == .valid + if isValid { + initiateGame() + } else { + let ctrl = UIAlertController(title: "Error", message: wordValidity.rawValue, preferredStyle: .alert) + ctrl.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil)) + AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate)) + self.present(ctrl, animated: true, completion: nil) + } + return isValid + } + + @objc private func resetGamemode() { + selectedGamemode = nil + } + + @objc private func initiateGameVersusComputer() { + selectedGamemode = .computer + clueTextField.text = GameUtility.pickWord() + + initiateGame() + } + + @objc private func initiateGameOnInfiniteMode() { + selectedGamemode = .infinite + clueTextField.text = GameUtility.pickWord() + + initiateGame() + } + + private func initiateGame() { + guard let gamemode = selectedGamemode else { return } + // start game + let clueGuessVC = ClueGuessViewController(clue: clueTextField.text?.uppercased() ?? "", gamemode: gamemode) + clueTextField.text = "" + startGameButton.isEnabled = false + + navigationController?.pushViewController(clueGuessVC, animated: true) + } +} + +extension GameSetupViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return checkAndInitiateGame() + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard string.isLettersOnly(), + (textField.text?.count ?? 0) + string.count <= GameSettings.clueLength.readIntValue() else { + AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate)) + return false + } + + return true + } +} + +extension GameSetupViewController: GameSettingsDelegate { + func didDismissSettings() { + updateScreen() + } +} + +enum WordValidity: String { + case insufficientLength = "Your word is not long enough." + case excessLength = "Your word is too long." + case notDefined = "That's not a word in our dictionary." + case missingWord = "Please input a word." + case valid +} diff --git a/WordleWithFriends/How To Play/GameInstructionsViewController.swift b/WordleWithFriends/How To Play/GameInstructionsViewController.swift new file mode 100644 index 0000000..2cabfa5 --- /dev/null +++ b/WordleWithFriends/How To Play/GameInstructionsViewController.swift @@ -0,0 +1,153 @@ +// +// GameInstructionsViewController.swift +// WordleWithFriends +// +// Created by Geoffrey Liu on 3/5/22. +// + +import UIKit + +final class GameInstructionsViewController: UIViewController { + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .top + stackView.spacing = 16.0 + + return stackView + }() + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + + return scrollView + }() + + private lazy var instructionsHeader: UILabel = { + // TODO: Pluralization? + let numberOfAttemptsText = "\(GameSettings.maxGuesses.readIntValue().spelledOut ?? " ") tries".bolded + let clueLengthText = "\(GameSettings.clueLength.readIntValue().spelledOut ?? " ")-letter".bolded + + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.attributedText = "Guess the clue in " + .appending(numberOfAttemptsText) + .appending(" or less. The clue will be a ") + .appending(clueLengthText) + .appending(" word.") + + label.numberOfLines = 0 + + return label + }() + + private lazy var headerLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Examples" + label.font = UIFont.boldSystemFont(ofSize: UIFont.labelFontSize) + + return label + }() + + private lazy var correctGuessExampleRow: WordGuessRowView = { + let row = WordGuessRowView() + var guess = WordGuess(guess: "GEOFF") + guess.mark(2, as: .correct) + row.configure(with: guess) + + return row + }() + + private lazy var correctGuessExplanation: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.attributedText = "The letter ".appending("O".bolded).appending(" is in the correct place.".asAttributedString) + + return label + }() + + private lazy var misplacedGuessExampleRow: WordGuessRowView = { + let row = WordGuessRowView() + var guess = WordGuess(guess: "APPLE") + guess.mark(3, as: .misplaced) + row.configure(with: guess) + + return row + }() + + private lazy var misplacedGuessExplanation: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.attributedText = "The letter ".appending("L".bolded).appending(" is in the clue but in the wrong place.".asAttributedString) + + return label + }() + + private lazy var incorrectGuessExampleRow: WordGuessRowView = { + let row = WordGuessRowView() + var guess = WordGuess(guess: "PARKS") + guess.mark(0, as: .incorrect) + row.configure(with: guess) + + return row + }() + + private lazy var incorrectGuessExplanation: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.attributedText = "The letter ".appending("P".bolded).appending(" is not in the clue.".asAttributedString) + + return label + }() + + private lazy var settingsTeaser: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.attributedText = "✨ Pro tip! ".bolded.appending("You can change stuff like the number of guesses in the Settings.\n\n") // TODO: deeplink to settings + + return label + }() + + override func viewDidLoad() { + super.viewDidLoad() + + // TODO: Factor some of this out as it's shared with GameSettingsVC + title = "How to play Wordle" + navigationItem.title = "How to play Wordle" + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(didTapClose)) + + view.backgroundColor = .systemBackground + + stackView.addArrangedSubview(instructionsHeader) + stackView.addArrangedSubview(HorizontalSeparatorView()) + stackView.addArrangedSubview(headerLabel) + stackView.addArrangedSubview(correctGuessExampleRow) + stackView.addArrangedSubview(correctGuessExplanation) + stackView.addArrangedSubview(misplacedGuessExampleRow) + stackView.addArrangedSubview(misplacedGuessExplanation) + stackView.addArrangedSubview(incorrectGuessExampleRow) + stackView.addArrangedSubview(incorrectGuessExplanation) + stackView.addArrangedSubview(HorizontalSeparatorView()) + stackView.addArrangedSubview(settingsTeaser) + + + scrollView.addSubview(stackView) + stackView.pin(to: scrollView, margins: .init(top: 0, left: 16, bottom: 0, right: 16)) + stackView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor).isActive = true + + view.addSubview(scrollView) + scrollView.pin(to: view.safeAreaLayoutGuide) + + } + + @objc private func didTapClose() { + dismiss(animated: true) + } +} diff --git a/WordleWithFriends/How To Play/Views/HorizontalSeparatorView.swift b/WordleWithFriends/How To Play/Views/HorizontalSeparatorView.swift new file mode 100644 index 0000000..9fb7677 --- /dev/null +++ b/WordleWithFriends/How To Play/Views/HorizontalSeparatorView.swift @@ -0,0 +1,42 @@ +// +// HorizontalSeparatorView.swift +// WordleWithFriends +// +// Created by Geoffrey Liu on 3/5/22. +// + +import UIKit + +final class HorizontalSeparatorView: UIView { + private weak var widthConstraint: NSLayoutConstraint? + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + backgroundColor = .systemGray + translatesAutoresizingMaskIntoConstraints = false + + heightAnchor.constraint(equalToConstant: 1.0).with(priority: .required).isActive = true + setContentCompressionResistancePriority(.required, for: .vertical) + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if let superview = superview { + if let widthConstraint = widthConstraint { + widthConstraint.isActive = false + } + widthConstraint = widthAnchor.constraint(equalTo: superview.widthAnchor) + widthConstraint?.isActive = true + } + } +} diff --git a/WordleWithFriends/Models/WordGuess.swift b/WordleWithFriends/Models/WordGuess.swift index 401343e..6ae9218 100644 --- a/WordleWithFriends/Models/WordGuess.swift +++ b/WordleWithFriends/Models/WordGuess.swift @@ -42,6 +42,7 @@ struct WordGuess { /// Checks a player's guess /// - Parameter clue: the word to check against /// - Returns: true if the user guessed correctly; false otherwise + @discardableResult mutating func checkGuess(against clue: String) -> Bool { var clue = clue var didGuessCorrectly = true @@ -50,9 +51,9 @@ struct WordGuess { guess.enumerated().forEach { index, letterGuess in if letterGuess.letter == clue[index] { clue.replaceAt(index, with: "#") - guess[index].state = .correct + mark(index, as: .correct) } else if !clue.contains(letterGuess.letter) { - guess[index].state = .incorrect + mark(index, as: .incorrect) didGuessCorrectly = false } } @@ -60,13 +61,12 @@ struct WordGuess { // pass 2: the rest are misplaced or incorrect (0..= 0, index < guess.count else { return } + guess[index].state = letterState + } + func asString() -> String { guess.reduce("") { $0 + String($1.state.rawValue) } } diff --git a/WordleWithFriends/Settings/GameSettingsViewController.swift b/WordleWithFriends/Settings/GameSettingsViewController.swift index 72c8bd9..051da69 100644 --- a/WordleWithFriends/Settings/GameSettingsViewController.swift +++ b/WordleWithFriends/Settings/GameSettingsViewController.swift @@ -34,7 +34,7 @@ final class GameSettingsViewController: UIViewController { title = "Settings" navigationItem.title = "Settings" - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(didTapCloseSettings)) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(didTapClose)) view.backgroundColor = .systemBackground let table = UITableView(frame: .zero, style: .insetGrouped) @@ -49,7 +49,7 @@ final class GameSettingsViewController: UIViewController { table.pin(to: view.safeAreaLayoutGuide) } - @objc private func didTapCloseSettings() { + @objc private func didTapClose() { dismiss(animated: true) { [weak self] in self?.delegate?.didDismissSettings() } } } diff --git a/WordleWithFriends/Views/LetterTileView.swift b/WordleWithFriends/Views/LetterTileView.swift index 0348345..74e6e3d 100644 --- a/WordleWithFriends/Views/LetterTileView.swift +++ b/WordleWithFriends/Views/LetterTileView.swift @@ -29,7 +29,6 @@ final class LetterTileView: UIView { super.init(coder: coder) configure() } - private func configure(_ letterGuess: LetterGuess? = nil) { let letterGuess = letterGuess ?? .default diff --git a/WordleWithFriends/Views/WordGuessRow.swift b/WordleWithFriends/Views/WordGuessRow.swift index 122e68c..660799f 100644 --- a/WordleWithFriends/Views/WordGuessRow.swift +++ b/WordleWithFriends/Views/WordGuessRow.swift @@ -24,15 +24,8 @@ final class WordGuessRow: UITableViewCell { return CGFloat(padding) }() - private lazy var letterStack: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal - stackView.distribution = .fillEqually - stackView.alignment = .fill - stackView.spacing = CGFloat(LayoutUtility.gridPadding(numberOfColumns: GameSettings.clueLength.readIntValue())) - - return stackView + private lazy var guessRowView: WordGuessRowView = { + .init() }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -48,26 +41,50 @@ final class WordGuessRow: UITableViewCell { } private func setupCell() { - (0...(GameSettings.clueLength.readIntValue()-1)).forEach { _ in - let tile = LetterTileView() - letterStack.addArrangedSubview(tile) - } - - contentView.addSubview(letterStack) + contentView.addSubview(guessRowView) NSLayoutConstraint.activate([ - letterStack.topAnchor.constraint(equalTo: contentView.topAnchor), - letterStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -calculatedPadding), - letterStack.heightAnchor.constraint(equalToConstant: calculatedHeight), - letterStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + guessRowView.topAnchor.constraint(equalTo: contentView.topAnchor), + guessRowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -calculatedPadding), + guessRowView.heightAnchor.constraint(equalToConstant: calculatedHeight), + guessRowView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), ]) } func configure(with wordGuess: WordGuess = .init()) { - letterStack.removeAllArrangedSubviews() + guessRowView.configure(with: wordGuess) + } +} + +final class WordGuessRowView: UIStackView { + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + axis = .horizontal + distribution = .fillEqually + alignment = .fill + spacing = CGFloat(LayoutUtility.gridPadding(numberOfColumns: GameSettings.clueLength.readIntValue())) + + (0...(GameSettings.clueLength.readIntValue()-1)).forEach { _ in + let tile = LetterTileView() + addArrangedSubview(tile) + } + } + + func configure(with wordGuess: WordGuess = .init()) { + removeAllArrangedSubviews() (0...(GameSettings.clueLength.readIntValue()-1)).forEach { index in let tile = LetterTileView(letterGuess: wordGuess.guess(at: index)) - letterStack.addArrangedSubview(tile) + addArrangedSubview(tile) } } }