// // EditCheatViewController.swift // Delta // // Created by Riley Testut on 5/21/16. // Copyright © 2016 Riley Testut. All rights reserved. // import UIKit import CoreData import DeltaCore import Roxas protocol EditCheatViewControllerDelegate: class { func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) func editCheatViewController(_ editCheatViewController: EditCheatViewController, deactivateCheat cheat: Cheat) } private extension EditCheatViewController { enum Section: Int { case name case type case code } } class EditCheatViewController: UITableViewController { var game: Game! { didSet { let deltaCore = Delta.core(for: self.game.type)! self.supportedCheatFormats = deltaCore.supportedCheatFormats.sorted() { $0.name < $1.name } } } var cheat: Cheat? weak var delegate: EditCheatViewControllerDelegate? var isPreviewing = false fileprivate var supportedCheatFormats: [CheatFormat]! fileprivate var selectedCheatFormat: CheatFormat { let cheatFormat = self.supportedCheatFormats[self.typeSegmentedControl.selectedSegmentIndex] return cheatFormat } fileprivate var mutableCheat: Cheat! fileprivate var managedObjectContext = DatabaseManager.shared.newBackgroundContext() @IBOutlet fileprivate var nameTextField: UITextField! @IBOutlet fileprivate var typeSegmentedControl: UISegmentedControl! @IBOutlet fileprivate var codeTextView: CheatTextView! override var previewActionItems: [UIPreviewActionItem] { guard let cheat = self.cheat else { return [] } let copyCodeAction = UIPreviewAction(title: NSLocalizedString("Copy Code", comment: ""), style: .default) { (action, viewController) in UIPasteboard.general.string = cheat.code } let presentingViewController = self.presentingViewController! let editCheatAction = UIPreviewAction(title: NSLocalizedString("Edit", comment: ""), style: .default) { (action, viewController) in // Delaying until next run loop prevents self from being dismissed immediately DispatchQueue.main.async { let editCheatViewController = viewController as! EditCheatViewController editCheatViewController.isPreviewing = false editCheatViewController.presentWithPresentingViewController(presentingViewController) } } let deleteAction = UIPreviewAction(title: NSLocalizedString("Delete", comment: ""), style: .destructive) { [unowned self] (action, viewController) in self.delegate?.editCheatViewController(self, deactivateCheat: cheat) DatabaseManager.shared.performBackgroundTask { (context) in let temporaryCheat = context.object(with: cheat.objectID) context.delete(temporaryCheat) context.saveWithErrorLogging() } } let cancelDeleteAction = UIPreviewAction(title: NSLocalizedString("Cancel", comment: ""), style: .default) { (action, viewController) in } let deleteActionGroup = UIPreviewActionGroup(title: NSLocalizedString("Delete", comment: ""), style: .destructive, actions: [deleteAction, cancelDeleteAction]) return [copyCodeAction, editCheatAction, deleteActionGroup] } } extension EditCheatViewController { override func viewDidLoad() { super.viewDidLoad() var name: String! var type: CheatType! var code: String! self.managedObjectContext.performAndWait { // Main Thread context is read-only, so we either create a new cheat, or get a reference to the current cheat in a new background context if let cheat = self.cheat { self.mutableCheat = self.managedObjectContext.object(with: cheat.objectID) as? Cheat } else { self.mutableCheat = Cheat.insertIntoManagedObjectContext(self.managedObjectContext) self.mutableCheat.game = self.managedObjectContext.object(with: self.game.objectID) as? Game self.mutableCheat.type = self.supportedCheatFormats.first!.type self.mutableCheat.code = "" self.mutableCheat.name = "" } self.mutableCheat.isEnabled = true // After we save a cheat, it should be enabled name = self.mutableCheat.name type = self.mutableCheat.type code = self.mutableCheat.code.sanitized(with: self.selectedCheatFormat.allowedCodeCharacters) } // Update UI if name.characters.count == 0 { self.title = NSLocalizedString("Cheat", comment: "") } else { self.title = name } self.nameTextField.text = name self.codeTextView.text = code self.typeSegmentedControl.removeAllSegments() for (index, format) in self.supportedCheatFormats.enumerated() { self.typeSegmentedControl.insertSegment(withTitle: format.name, at: index, animated: false) } if let index = self.supportedCheatFormats.index(where: { $0.type == type }) { self.typeSegmentedControl.selectedSegmentIndex = index } else { self.typeSegmentedControl.selectedSegmentIndex = 0 } self.updateCheatType(self.typeSegmentedControl) self.updateSaveButtonState() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // This matters when going from peek -> pop // Otherwise, has no effect because viewDidLayoutSubviews has already been called if self.isAppearing && !self.isPreviewing { self.nameTextField.becomeFirstResponder() } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if let superview = self.codeTextView.superview { let layoutMargins = superview.layoutMargins self.codeTextView.textContainerInset = layoutMargins self.codeTextView.textContainer.lineFragmentPadding = 0 } if self.isAppearing && !self.isPreviewing { self.nameTextField.becomeFirstResponder() } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { self.nameTextField.resignFirstResponder() self.codeTextView.resignFirstResponder() } } internal extension EditCheatViewController { func presentWithPresentingViewController(_ presentingViewController: UIViewController) { let navigationController = RSTNavigationController(rootViewController: self) navigationController.modalPresentationStyle = .overFullScreen // Keeps PausePresentationController active to ensure layout is not messed up navigationController.modalPresentationCapturesStatusBarAppearance = true presentingViewController.present(navigationController, animated: true, completion: nil) } } private extension EditCheatViewController { @IBAction func updateCheatName(_ sender: UITextField) { var title = sender.text ?? "" if title.characters.count == 0 { title = NSLocalizedString("Cheat", comment: "") } self.title = title self.updateSaveButtonState() } @IBAction func updateCheatType(_ sender: UISegmentedControl) { self.codeTextView.cheatFormat = self.selectedCheatFormat UIView.performWithoutAnimation { self.tableView.reloadSections(IndexSet(integer: Section.type.rawValue), with: .none) // Hacky-ish workaround so we can update the footer text for the code section without causing text view to resign first responder status self.tableView.beginUpdates() if let footerView = self.tableView.footerView(forSection: Section.code.rawValue) { footerView.textLabel!.text = self.tableView(self.tableView, titleForFooterInSection: Section.code.rawValue) footerView.sizeToFit() } self.tableView.endUpdates() } } func updateSaveButtonState() { let isValidName = !(self.nameTextField.text ?? "").isEmpty let isValidCode = !self.codeTextView.text.isEmpty self.navigationItem.rightBarButtonItem?.isEnabled = isValidName && isValidCode } @IBAction func saveCheat(_ sender: UIBarButtonItem) { self.mutableCheat.managedObjectContext?.performAndWait { self.mutableCheat.name = self.nameTextField.text ?? "" self.mutableCheat.type = self.selectedCheatFormat.type self.mutableCheat.code = self.codeTextView.text.formatted(with: self.selectedCheatFormat) do { try self.validateCheat(self.mutableCheat) self.delegate?.editCheatViewController(self, activateCheat: self.mutableCheat, previousCheat: self.cheat) self.mutableCheat.managedObjectContext?.saveWithErrorLogging() self.performSegue(withIdentifier: "unwindEditCheatSegue", sender: sender) } catch CheatValidator.Error.invalidCode { self.presentErrorAlert(title: NSLocalizedString("Invalid Code", comment: ""), message: NSLocalizedString("Please make sure you typed the cheat code in correctly and try again.", comment: "")) { self.codeTextView.becomeFirstResponder() } } catch CheatValidator.Error.invalidName { self.presentErrorAlert(title: NSLocalizedString("Invalid Name", comment: ""), message: NSLocalizedString("Please rename this cheat and try again.", comment: "")) { self.codeTextView.becomeFirstResponder() } } catch CheatValidator.Error.duplicateCode { self.presentErrorAlert(title: NSLocalizedString("Duplicate Code", comment: ""), message: NSLocalizedString("A cheat already exists with this code. Please type in a different code and try again.", comment: "")) { self.codeTextView.becomeFirstResponder() } } catch CheatValidator.Error.duplicateName { self.presentErrorAlert(title: NSLocalizedString("Duplicate Name", comment: ""), message: NSLocalizedString("A cheat already exists with this name. Please rename this cheat and try again.", comment: "")) { self.nameTextField.becomeFirstResponder() } } catch { print(error) self.presentErrorAlert(title: NSLocalizedString("Unknown Error", comment: ""), message: NSLocalizedString("An error occured. Please make sure you typed the cheat code in correctly and try again.", comment: "")) { self.codeTextView.becomeFirstResponder() } } } } func validateCheat(_ cheat: Cheat) throws { let validator = CheatValidator(format: self.selectedCheatFormat, managedObjectContext: self.managedObjectContext) try validator.validate(cheat) } @IBAction func textFieldDidEndEditing(_ sender: UITextField) { sender.resignFirstResponder() } func presentErrorAlert(title: String, message: String, handler: (() -> Void)?) { DispatchQueue.main.async { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { action in handler?() })) self.present(alertController, animated: true, completion: nil) } } } extension EditCheatViewController { override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { switch Section(rawValue: section)! { case .name: return super.tableView(tableView, titleForFooterInSection: section) case .type: let title = String.localizedStringWithFormat("Code format is %@.", self.selectedCheatFormat.format) return title case .code: let containsSpaces = self.selectedCheatFormat.format.contains(" ") let containsDashes = self.selectedCheatFormat.format.contains("-") switch (containsSpaces, containsDashes) { case (true, false): return NSLocalizedString("Spaces will be inserted automatically as you type.", comment: "") case (false, true): return NSLocalizedString("Dashes will be inserted automatically as you type.", comment: "") case (true, true): return NSLocalizedString("Spaces and dashes will be inserted automatically as you type.", comment: "") case (false, false): return NSLocalizedString("Code will be formatted automatically as you type.", comment: "") } } } } extension EditCheatViewController: UITextViewDelegate { func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { defer { self.updateSaveButtonState() } guard text != "\n" else { textView.resignFirstResponder() return false } let sanitizedText = text.sanitized(with: self.selectedCheatFormat.allowedCodeCharacters) guard sanitizedText != text else { return true } // We need to manually add back the attributes when manually modifying the underlying text storage // Otherwise, pasting text into an empty text view will result in the wrong font being used let attributedString = NSAttributedString(string: sanitizedText, attributes: textView.typingAttributes) textView.textStorage.replaceCharacters(in: range, with: attributedString) // We must add attributedString.length, not range.length, in case the attributed string's length differs textView.selectedRange = NSRange(location: range.location + attributedString.length, length: 0) return false } }