diff --git a/Spellbook.xcodeproj/project.pbxproj b/Spellbook.xcodeproj/project.pbxproj index d4df155..d3f194f 100644 --- a/Spellbook.xcodeproj/project.pbxproj +++ b/Spellbook.xcodeproj/project.pbxproj @@ -1071,10 +1071,12 @@ PRODUCT_BUNDLE_IDENTIFIER = dnd.jon.Spellbook; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "SWRevealViewController/Spellbook-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1100,9 +1102,11 @@ PRODUCT_BUNDLE_IDENTIFIER = dnd.jon.Spellbook; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "SWRevealViewController/Spellbook-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/Spellbook/AppDelegate.swift b/Spellbook/AppDelegate.swift index 16f5b25..d129673 100644 --- a/Spellbook/AppDelegate.swift +++ b/Spellbook/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Disable rotation - var orientationLock = UIInterfaceOrientationMask.portrait + private let orientationLock = oniPad ? UIInterfaceOrientationMask.allButUpsideDown : UIInterfaceOrientationMask.portrait func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return self.orientationLock } diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index df4436a..fa205d2 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -400,107 +400,20 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -556,7 +469,7 @@ - + @@ -579,7 +492,7 @@ - + @@ -665,7 +578,7 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -1776,14 +1710,14 @@ - + - + @@ -1828,7 +1762,7 @@ - + @@ -2187,7 +2121,7 @@ - + @@ -2245,7 +2179,7 @@ - + @@ -2335,7 +2269,7 @@ - + @@ -2394,7 +2328,7 @@ - + @@ -2455,16 +2389,16 @@ - + - + - + - - + - + + + + + + + + + + + + + + + diff --git a/Spellbook/SideMenuController.swift b/Spellbook/SideMenuController.swift index bb84605..c15ce64 100644 --- a/Spellbook/SideMenuController.swift +++ b/Spellbook/SideMenuController.swift @@ -20,6 +20,8 @@ class SideMenuController: UIViewController, UIPopoverPresentationControllerDeleg @IBOutlet weak var updateInfoLabel: UILabel! @IBOutlet weak var whatsNewButton: UIButton! @IBOutlet weak var spellSlotsButton: UIButton! + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var contentView: UIView! var statusController: StatusFilterController? @@ -61,69 +63,12 @@ class SideMenuController: UIViewController, UIPopoverPresentationControllerDeleg viewHeight = viewRect.size.height viewWidth = viewRect.size.width - // Set the dimensions for the background image - // No padding necessary for this - backgroundView.frame = CGRect(x: 0, y: -backgroundOffset, width: viewWidth, height: viewHeight + backgroundOffset) - - //let headerHeight = CGFloat(0.1 * viewHeight) - //let statusFilterHeight = CGFloat(0.3 * viewHeight) - let headerHeight = CGFloat(57) - let statusFilterHeight = CGFloat(171) - let characterLabelHeight = CGFloat(20) - let exportSpellListLabelHeight = CGFloat(20) - let selectionButtonHeight = CGFloat(20) - let spellSlotsButtonHeight = CGFloat(20) - let belowFilterPadding = min(max(0.05 * SizeUtils.screenHeight, 25), 40) - let belowCharacterLabelPadding = CGFloat(14) - let belowExportSpellListLabelPadding = CGFloat(14) - let belowSelectionButtonPadding = CGFloat(20) - let belowSpellSlotsButtonPadding = CGFloat(23) - let notchTopPadding = CGFloat(35) - let updateInfoLabelHeight = CGFloat(20) - let belowUpdateInfoLabelPadding = CGFloat(14) - let whatsNewButtonHeight = CGFloat(20) - - // Does the device have a notch or not? - let hasNotch = UIDevice.current.hasNotch - - // Set up the view positioning - var currentY = CGFloat(topPadding) - if hasNotch { - currentY += notchTopPadding - } - sideMenuHeader.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth, height: headerHeight) - - currentY += (headerHeight + tablePadding) - statusFilterView.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: statusFilterHeight + belowFilterPadding) - - currentY += (statusFilterHeight + belowFilterPadding) - characterLabel.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: characterLabelHeight) - - currentY += characterLabelHeight + belowCharacterLabelPadding - selectionButton.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: selectionButtonHeight) - - currentY += exportSpellListLabelHeight + belowExportSpellListLabelPadding - exportSpellListButton.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: exportSpellListLabelHeight) - - currentY += spellSlotsButtonHeight + belowSelectionButtonPadding - spellSlotsButton.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: spellSlotsButtonHeight) - - currentY += selectionButtonHeight + belowSpellSlotsButtonPadding - updateInfoLabel.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: updateInfoLabelHeight) - - currentY += updateInfoLabelHeight + belowUpdateInfoLabelPadding - whatsNewButton.frame = CGRect(x: leftPadding, y: currentY, width: viewWidth - leftPadding, height: whatsNewButtonHeight) + characterLabel.textColor = defaultFontColor selectionButton.addTarget(self, action: #selector(selectionButtonPressed), for: UIControl.Event.touchUpInside) - exportSpellListButton.addTarget(self, action: #selector(exportSpellListButtonPressed), for: UIControl.Event.touchUpInside) - whatsNewButton.addTarget(self, action: #selector(updateInfoButtonPressed), for: UIControl.Event.touchUpInside) - spellSlotsButton.addTarget(self, action: #selector(spellSlotsButtonPressed), for: UIControl.Event.touchUpInside) - - characterLabel.textColor = defaultFontColor - } override func viewWillAppear(_ animated: Bool) { @@ -132,7 +77,7 @@ class SideMenuController: UIViewController, UIPopoverPresentationControllerDeleg $0.select { $0.profile?.name } } } - + override func viewDidAppear(_ animated: Bool) { // Set the character label @@ -141,7 +86,24 @@ class SideMenuController: UIViewController, UIPopoverPresentationControllerDeleg characterLabel.text = "Character: " + name! } } - + + func setScrollViewSize() { + if let content = contentView, let scroll = scrollView { + // The content size of the scroll view is equal to the content view's size + scroll.contentSize = content.frame.size + + } + } + + override func viewWillTransition(to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator) { + setScrollViewSize() + } + + override func viewDidLayoutSubviews() { + setScrollViewSize() + } + // Connecting to the child controllers override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "statusSegue" { @@ -158,16 +120,15 @@ class SideMenuController: UIViewController, UIPopoverPresentationControllerDeleg // // } } - + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return UIModalPresentationStyle.none } - + @objc func selectionButtonPressed() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let controller = storyboard.instantiateViewController(withIdentifier: "characterSelection") as! CharacterSelectionController - let popupHeight = 0.5 * SizeUtils.screenHeight let popupWidth = 0.75 * SizeUtils.screenWidth let maxPopupHeight = CGFloat(320) diff --git a/Spellbook/SpellTableViewController.swift b/Spellbook/SpellTableViewController.swift index 09be7d7..fc3e210 100644 --- a/Spellbook/SpellTableViewController.swift +++ b/Spellbook/SpellTableViewController.swift @@ -27,30 +27,50 @@ class SpellTableViewController: UITableViewController { // Vertical position in main view var mainY = CGFloat(0) - - - @IBOutlet var spellTable: UITableView! let cellReuseIdentifier = "cell" let spellWindowSegueIdentifier = "spellWindowSegue" let spellWindowIdentifier = "spellWindow" static let estimatedHeight = CGFloat(60) + // Usable height and width + var usableHeight = UIScreen.main.bounds.height + var usableWidth = UIScreen.main.bounds.width + + // Extreme padding amounts + let maxHorizPadding = CGFloat(5) + let maxTopPadding = CGFloat(5) + let maxBotPadding = CGFloat(3) + let minHorizPadding = CGFloat(1) + let minTopPadding = CGFloat(1) + let minBotPadding = CGFloat(1) + + // Padding amounts + let leftPaddingFraction = CGFloat(0.01) + let rightPaddingFraction = CGFloat(0.01) + let topPaddingFraction = CGFloat(0.01) + let bottomPaddingFraction = CGFloat(0.01) + // The button images // It's too costly to do the re-rendering every time, so we just do it once - static let buttonFraction = CGFloat(0.09) - static let imageWidth = SpellTableViewController.buttonFraction * ViewController.usableWidth - static let imageHeight = SpellTableViewController.imageWidth - static let starEmpty = UIImage(named: "star_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: SpellTableViewController.imageWidth, height: SpellTableViewController.imageHeight) - static let starFilled = UIImage(named: "star_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: SpellTableViewController.imageWidth, height: SpellTableViewController.imageHeight) - static let wandEmpty = UIImage(named: "wand_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: SpellTableViewController.imageWidth, height: SpellTableViewController.imageHeight) - static let wandFilled = UIImage(named: "wand_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: SpellTableViewController.imageWidth, height: SpellTableViewController.imageHeight) - static let bookEmpty = UIImage(named: "book_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: SpellTableViewController.imageWidth, height: SpellTableViewController.imageHeight) - static let bookFilled = UIImage(named: "book_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: SpellTableViewController.imageWidth, height: SpellTableViewController.imageHeight) - + var buttonFraction: CGFloat! + var imageWidth: CGFloat! + var imageHeight: CGFloat! + var starEmpty: UIImage! + var starFilled: UIImage! + var wandEmpty: UIImage! + var wandFilled: UIImage! + var bookEmpty: UIImage! + var bookFilled: UIImage! + override func viewDidLoad() { super.viewDidLoad() + setNeedsStatusBarAppearanceUpdate() + + tableView.delegate = self + tableView.dataSource = self + // Populate the list of spells //tableView.register(SpellDataCell.self, forCellReuseIdentifier: cellReuseIdentifier) tableView.separatorStyle = .none @@ -78,8 +98,6 @@ class SpellTableViewController: UITableViewController { // Uncomment the following line to display an Edit button in the navigation bar for this view controller. // self.navigationItem.rightBarButtonItem = self.editButtonItem - - } override func viewDidAppear(_ animated: Bool) { @@ -88,28 +106,64 @@ class SpellTableViewController: UITableViewController { // Get the main view //main = self.parent as? ViewController + if !firstAppear { return } + // If this is the view's first appearance (i.e. when the app is opening), we initialize spellArray - if firstAppear { - spellArray = store.state.currentSpellList - tableView.reloadData() - firstAppear = false - } + spellArray = store.state.currentSpellList + tableView.reloadData() // Initial filtering and sorting filter() sort() + firstAppear = false } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + + // Set the sizes of the container views (there are no other top level elements) + let screenRect = UIScreen.main.bounds + setContainerDimensions(screenWidth: screenRect.size.width, screenHeight: screenRect.size.height) + store.subscribe(self) { $0.select { - $0.currentSpellList + ($0.currentSpellList, $0.dirtySpellIDs) } } } + // This function sets the sizes of the top-level container views + func setContainerDimensions(screenWidth: CGFloat, screenHeight: CGFloat) { + + self.buttonFraction = oniPad ? CGFloat(0.04) : CGFloat(0.08) + + // Get the padding sizes + let leftPadding = max(min(leftPaddingFraction * screenWidth, maxHorizPadding), minHorizPadding) + let rightPadding = max(min(rightPaddingFraction * screenWidth, maxHorizPadding), minHorizPadding) + let topPadding = max(min(topPaddingFraction * screenHeight, maxTopPadding), minTopPadding) + let bottomPadding = max(min(bottomPaddingFraction * screenHeight, maxBotPadding), minBotPadding) + + // Account for padding + self.usableHeight = screenHeight - topPadding - bottomPadding + self.usableWidth = screenWidth - leftPadding - rightPadding + + // The button images + // It's too costly to do the re-rendering every time, so we just do it once + // self.imageWidth = max(self.buttonFraction * self.usableWidth, CGFloat(30)) + + self.imageWidth = oniPad ? CGFloat(40.5) : CGFloat(30.5) + self.imageHeight = self.imageWidth + self.starEmpty = UIImage(named: "star_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: self.imageWidth, height: self.imageHeight) + self.starFilled = UIImage(named: "star_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: self.imageWidth, height: self.imageHeight) + self.wandEmpty = UIImage(named: "wand_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: self.imageWidth, height: self.imageHeight) + self.wandFilled = UIImage(named: "wand_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: self.imageWidth, height: self.imageHeight) + self.bookEmpty = UIImage(named: "book_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: self.imageWidth, height: self.imageHeight) + self.bookFilled = UIImage(named: "book_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: self.imageWidth, height: self.imageHeight) + + tableView.reloadData() + } + func setTableDimensions(leftPadding: CGFloat, bottomPadding: CGFloat, usableHeight: CGFloat, usableWidth: CGFloat, tableTopPadding: CGFloat) { // Set the table dimensions @@ -129,9 +183,16 @@ class SpellTableViewController: UITableViewController { } // Set the footer height - override func tableView(_ tableView: UITableView, heightForFooterInSection: Int) -> CGFloat { - return 2 * SpellTableViewController.estimatedHeight - } +// func tableView(_ tableView: UITableView, heightForFooterInSection: Int) -> CGFloat { +// return 2 * SpellTableViewController.estimatedHeight +// } + + +// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { +// let preview = makeTargetedPreview(for: configuration) +// preview?.view.backgroundColor = UIColor.clear +// return preview +// } // Return the footer view @@ -150,6 +211,13 @@ class SpellTableViewController: UITableViewController { let spell = spellArray[indexPath.row] cell.spell = spell + setupCell(cell: cell, spell: spell) + + return cell + } + + func setupCell(cell: SpellDataCell, spell: Spell) { + // Cell formatting cell.layoutMargins = UIEdgeInsets(top: 2, left: 0, bottom: 2, right: 0) cell.selectionStyle = .gray @@ -166,38 +234,113 @@ class SpellTableViewController: UITableViewController { cell.sourcebookLabel.textColor = defaultFontColor // Set the label text colors - for label in [ cell.nameLabel, cell.levelSchoolLabel, cell.sourcebookLabel ] { + for label in [cell.nameLabel, cell.levelSchoolLabel, cell.sourcebookLabel] { label?.textColor = defaultFontColor } // Set the button images - cell.favoriteButton.setTrueImage(image: SpellTableViewController.starFilled!) - cell.favoriteButton.setFalseImage(image: SpellTableViewController.starEmpty!) - cell.preparedButton.setTrueImage(image: SpellTableViewController.wandFilled!) - cell.preparedButton.setFalseImage(image: SpellTableViewController.wandEmpty!) - cell.knownButton.setTrueImage(image: SpellTableViewController.bookFilled!) - cell.knownButton.setFalseImage(image: SpellTableViewController.bookEmpty!) + cell.favoriteButton.setTrueImage(image: self.starFilled!) + cell.favoriteButton.setFalseImage(image: self.starEmpty!) + cell.preparedButton.setTrueImage(image: self.wandFilled!) + cell.preparedButton.setFalseImage(image: self.wandEmpty!) + cell.knownButton.setTrueImage(image: self.bookFilled!) + cell.knownButton.setFalseImage(image: self.bookEmpty!) // Set the button statuses - if let spellFilterStatus = store.state.profile?.spellFilterStatus { - cell.favoriteButton.set(spellFilterStatus.isFavorite(spell)) - cell.preparedButton.set(spellFilterStatus.isPrepared(spell)) - cell.knownButton.set(spellFilterStatus.isKnown(spell)) - } + let sfs = store.state.profile?.spellFilterStatus ?? SpellFilterStatus() + cell.favoriteButton.set(sfs.isFavorite(spell)) + cell.preparedButton.set(sfs.isPrepared(spell)) + cell.knownButton.set(sfs.isKnown(spell)) // Set the button callbacks // Set the callbacks for the buttons cell.favoriteButton.setCallback({ - store.dispatch(TogglePropertyAction(spell: spell, property: StatusFilterField.Favorites)) + store.dispatch(TogglePropertyAction(spell: cell.spell, property: .Favorites, markDirty: false)) }) cell.preparedButton.setCallback({ - store.dispatch(TogglePropertyAction(spell: spell, property: StatusFilterField.Prepared)) + store.dispatch(TogglePropertyAction(spell: cell.spell, property: .Prepared, markDirty: false)) }) cell.knownButton.setCallback({ - store.dispatch(TogglePropertyAction(spell: spell, property: StatusFilterField.Known)) + store.dispatch(TogglePropertyAction(spell: cell.spell, property: .Known, markDirty: false)) }) - return cell + let width = tableView.frame.width + NSLayoutConstraint.activate([ + cell.nameLabel.widthAnchor.constraint(equalToConstant: width - 3 * self.imageWidth - 30) + ]) + + let buttons = [cell.favoriteButton, cell.preparedButton, cell.knownButton] + NSLayoutConstraint.activate(buttons.compactMap({ + button in + return button?.widthAnchor.constraint(equalToConstant: self.imageWidth) + })) + NSLayoutConstraint.activate(buttons.compactMap({ + button in + return button?.heightAnchor.constraint(equalToConstant: self.imageHeight) + })) + } + + private func makeTargetedPreview(for configuration: UIContextMenuConfiguration, backgroundColor: UIColor? = nil) -> UITargetedPreview? { + guard let indexPath = configuration.identifier as? IndexPath else { return nil } + guard let cell = tableView.cellForRow(at: indexPath) as? SpellDataCell else { return nil } + cell.contentView.backgroundColor = UIColor.lightGray + let preview = UITargetedPreview(view: cell.contentView) + if (backgroundColor != nil) { + preview.view.backgroundColor = backgroundColor + } + return preview + } + + func setCellBackgroundColor(indexPath: IndexPath, color: UIColor) { + guard let cell = tableView.cellForRow(at: indexPath) as? SpellDataCell else { return } + cell.contentView.backgroundColor = color + } + + override func tableView(_ tableView: UITableView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { + guard let indexPath = configuration.identifier as? IndexPath else { return } + setCellBackgroundColor(indexPath: indexPath, color: UIColor.white) + } + + override func tableView(_ tableView: UITableView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { + animator?.addCompletion { + guard let indexPath = configuration.identifier as? IndexPath else { return } + self.setCellBackgroundColor(indexPath: indexPath, color: UIColor.clear) + } + } + + override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return makeTargetedPreview(for: configuration) + } + + override func tableView( + _ tableView: UITableView, + contextMenuConfigurationForRowAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + let cell = tableView.cellForRow(at: indexPath) as! SpellDataCell + let spell = cell.spell + return UIContextMenuConfiguration( + identifier: indexPath as NSCopying, + previewProvider: nil, + actionProvider: { + suggestedActions in + let shortcutAction = + UIAction( + title: "Create Shortcut", + image: UIImage(named: "book_empty.png")?.inverseImage(cgResult: true)) { + action in addSpellShortcut(spell: spell) + } + return UIMenu(title: "Spell Options", children: [shortcutAction]) + } + ) + } + + // This is supposed to handle rotations, etc. + // so we re-call setContainerDimensions and change the size associated to SpellDataCell + // But for the moment, the SpellDataCell change doesn't work correctly, and so rotation is disabled + override func viewWillTransition(to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + setContainerDimensions(screenWidth: size.width, screenHeight: size.height) } func sort() { @@ -209,6 +352,17 @@ class SpellTableViewController: UITableViewController { store.dispatchFunction(FilterNeededAction()) } + func indexPathsForIDs(spellIDs: [Int]) -> [IndexPath] { + var indexPaths: [IndexPath] = [] + for (idx, spell) in spellArray.enumerated() { + if (spellIDs.contains(spell.id)) { + let indexPath = IndexPath(item: idx, section: 0) + indexPaths.append(indexPath) + } + } + return indexPaths + } + // If one of the side menus is open, we want to close the menu rather than select a cell override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { //return main.closeMenuIfOpen() ? nil : indexPath @@ -234,7 +388,6 @@ class SpellTableViewController: UITableViewController { self.present(spellWindowController, animated: true, completion: nil) spellWindowController.spell = spell spellWindowController.spellIndex = spellIndex - //print("") } @@ -270,6 +423,7 @@ class SpellTableViewController: UITableViewController { // Filter on pulldown @objc func handlePullDown(_ sender: Any) { filter() + tableView.reloadData() refreshControl!.endRefreshing() } @@ -349,8 +503,19 @@ class SpellTableViewController: UITableViewController { // MARK: StoreSubscriber extension SpellTableViewController: StoreSubscriber { - func newState(state spells: [Spell]) { - spellArray = spells - tableView.reloadData() + typealias StoreSubscriberStateType = (currentSpellList: [Spell], dirtySpellIDs: [Int]) + + func newState(state: StoreSubscriberStateType) { + let needReload = state.currentSpellList != spellArray + if (needReload) { + spellArray = state.currentSpellList + tableView.reloadData() + } + + if (state.dirtySpellIDs.count > 0 && !needReload) { + let indexPaths = self.indexPathsForIDs(spellIDs: state.dirtySpellIDs) + self.tableView.reloadRows(at: indexPaths, with: UITableView.RowAnimation.none) + store.dispatch(MarkAllSpellsCleanAction()) + } } } diff --git a/Spellbook/SpellWindowController.swift b/Spellbook/SpellWindowController.swift index 64a413d..3b94124 100644 --- a/Spellbook/SpellWindowController.swift +++ b/Spellbook/SpellWindowController.swift @@ -15,10 +15,11 @@ class SpellWindowController: UIViewController { // How much of the horizontal width goes to the name label // The rest is for the favoriting button - static let nameLabelFraction = CGFloat(0.87) - static let buttonFraction = 1 - SpellWindowController.nameLabelFraction - static let imageWidth = UIScreen.main.bounds.width * SpellWindowController.buttonFraction - static let imageHeight = UIScreen.main.bounds.width * SpellWindowController.buttonFraction + static let majorWidthFraction = oniPad ? CGFloat(0.94) : CGFloat(0.86) + static let minorWidthFraction = 1 - SpellWindowController.majorWidthFraction + static let halfGapWidth = CGFloat(10) + static let imageWidth = UIScreen.main.bounds.width * SpellWindowController.minorWidthFraction + static let imageHeight = UIScreen.main.bounds.width * SpellWindowController.minorWidthFraction // Font sizes static let nameSize = CGFloat(30) @@ -92,13 +93,22 @@ class SpellWindowController: UIViewController { super.init(coder: coder) } - override func viewDidLoad() { - super.viewDidLoad() - + private var swipeRightHandler: UISwipeGestureRecognizer? = nil + private func setupSwipeRightHandler() { + if let handler = self.swipeRightHandler { + self.view.removeGestureRecognizer(handler) + } // We close the window on a swipe to the right let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture)) swipeRight.direction = UISwipeGestureRecognizer.Direction.right self.view.addGestureRecognizer(swipeRight) + self.swipeRightHandler = swipeRight + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupSwipeRightHandler() // Set the button images favoriteButton.setTrueImage(image: SpellWindowController.isFavoriteImage!) @@ -130,20 +140,64 @@ class SpellWindowController: UIViewController { // Set the content view to fill the screen contentView.frame = UIScreen.main.bounds + let minorWidthViews = [ + favoriteButton, + preparedButton, + knownButton, + castButton + ] + let majorWidthViews = [ + locationLabel, + concentrationLabel, + castingTimeLabel, + rangeLabel, + royaltyLabel, + componentsLabel, + expandedClassesLabel, + spellNameLabel, + schoolLevelLabel, + durationLabel, + materialsLabel, + classesLabel, + ] + + let width = contentView.frame.width + + NSLayoutConstraint.activate(minorWidthViews.compactMap({ view in + return view?.widthAnchor.constraint(equalToConstant: SpellWindowController.minorWidthFraction * width - SpellWindowController.halfGapWidth) + })) + NSLayoutConstraint.activate(majorWidthViews.compactMap({ view in + return view?.widthAnchor.constraint(equalToConstant: SpellWindowController.majorWidthFraction * width - SpellWindowController.halfGapWidth) + })) + NSLayoutConstraint.activate([ + favoriteButton.widthAnchor.constraint(equalToConstant: SpellWindowController.minorWidthFraction * width), + preparedButton.widthAnchor.constraint(equalToConstant: SpellWindowController.minorWidthFraction * width), + knownButton.widthAnchor.constraint(equalToConstant: SpellWindowController.minorWidthFraction * width), + ]) + // Set the label fonts let labels = [ spellNameLabel, schoolLevelLabel, locationLabel, concentrationLabel, castingTimeLabel, rangeLabel, componentsLabel, materialsLabel, durationLabel, classesLabel, expandedClassesLabel, descriptionLabel, higherLevelLabel ] labels.forEach { $0!.textColor = defaultFontColor } - } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + // Set the content view to fill the screen + contentView.frame = UIScreen.main.bounds + // The content size of the scroll view is equal to the content view's size scrollView.contentSize = contentView.frame.size } + override func viewWillTransition(to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + scrollView.contentSize = contentView.frame.size + setupSwipeRightHandler() + } + @objc func respondToSwipeGesture(gesture: UIGestureRecognizer) { if let swipeGesture = gesture as? UISwipeGestureRecognizer { switch swipeGesture.direction { diff --git a/Spellbook/ViewController.swift b/Spellbook/ViewController.swift index 272edb7..0ce2276 100644 --- a/Spellbook/ViewController.swift +++ b/Spellbook/ViewController.swift @@ -10,7 +10,7 @@ import UIKit import CoreActionSheetPicker import ReSwift -class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, SWRevealViewControllerDelegate { +class ViewController: UIViewController, UISearchBarDelegate, SWRevealViewControllerDelegate { // Spellbook let spellbook = Spellbook(jsonStr: try! String(contentsOf: Bundle.main.url(forResource: "Spells", withExtension: "json")!)) @@ -65,10 +65,8 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega var characterProfile = CharacterProfile() var selectionWindow: CharacterSelectionController? - // The spell table - @IBOutlet var spellTable: UITableView! - // The UIViews that hold the child controllers + @IBOutlet weak var spellTableView: UIView! @IBOutlet weak var sortFilterTableView: UIView! // Dimensions @@ -95,8 +93,8 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega let bottomPaddingFraction = CGFloat(0.01) // Usable height and width - static var usableHeight = UIScreen.main.bounds.height - static var usableWidth = UIScreen.main.bounds.width + var usableHeight = UIScreen.main.bounds.height + var usableWidth = UIScreen.main.bounds.width // The navigation bar and its items @IBOutlet var navBar: UINavigationItem! @@ -109,15 +107,15 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // The button images // It's too costly to do the re-rendering every time, so we just do it once - static let buttonFraction = CGFloat(0.09) - static let imageWidth = max(ViewController.buttonFraction * ViewController.usableWidth, CGFloat(30)) - static let imageHeight = ViewController.imageWidth - static let starEmpty = UIImage(named: "star_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: ViewController.imageWidth, height: ViewController.imageHeight) - static let starFilled = UIImage(named: "star_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: ViewController.imageWidth, height: ViewController.imageHeight) - static let wandEmpty = UIImage(named: "wand_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: ViewController.imageWidth, height: ViewController.imageHeight) - static let wandFilled = UIImage(named: "wand_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: ViewController.imageWidth, height: ViewController.imageHeight) - static let bookEmpty = UIImage(named: "book_empty.png")?.withRenderingMode(.alwaysOriginal).resized(width: ViewController.imageWidth, height: ViewController.imageHeight) - static let bookFilled = UIImage(named: "book_filled.png")?.withRenderingMode(.alwaysOriginal).resized(width: ViewController.imageWidth, height: ViewController.imageHeight) + var buttonFraction: CGFloat! + var imageWidth: CGFloat! + var imageHeight: CGFloat! + var starEmpty: UIImage! + var starFilled: UIImage! + var wandEmpty: UIImage! + var wandFilled: UIImage! + var bookEmpty: UIImage! + var bookFilled: UIImage! // What to do when the search button is pressed @IBAction func searchButtonPressed(_ sender: Any) { @@ -129,6 +127,7 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + store.subscribe(self) { $0.select { ($0.profile, $0.profile?.sortFilterStatus, $0.profile?.spellFilterStatus, $0.currentSpellList, $0.dirtySpellIDs) @@ -146,10 +145,6 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // Set the reveal controller delegate Controllers.revealController.delegate = self - // Set the spell table delegates - spellTable.delegate = self - spellTable.dataSource = self - // Set the status bar color // let statusBarBGColor = UIColor.black // if #available(iOS 13.0, *) { @@ -194,10 +189,6 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // as this calls some subview methods as well as sets dimensions if !firstAppearance { return } - // Set the sizes of the container views (there are no other top level elements) - let screenRect = UIScreen.main.bounds - setContainerDimensions(screenWidth: screenRect.size.width, screenHeight: screenRect.size.height) - // Dismiss keyboard when not in the search field //let tapper = UITapGestureRecognizer(target: self, action: #selector(endEditing)) //tapper.cancelsTouchesInView = false @@ -216,16 +207,6 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega self.searchBar.endEditing(true) } - // For the swipe-to-filter functionality - // For iOS >= 10, which we're using, the TableView already has this property - // so we can just assign to it - spellTable.refreshControl = { - let refreshControl = UIRefreshControl() - refreshControl.addTarget(self, action: #selector(handlePullDown(_:)), for: UIControl.Event.valueChanged) - return refreshControl - }() - - // Set the navigation bar button callbacks leftMenuButton.target = self leftMenuButton.action = #selector(leftMenuButtonPressed) @@ -258,26 +239,6 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // The view has appeared, so we can set firstAppearance to false firstAppearance = false - - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - - // This function sets the sizes of the top-level container views - func setContainerDimensions(screenWidth: CGFloat, screenHeight: CGFloat) { - - // Get the padding sizes - let leftPadding = max(min(leftPaddingFraction * screenWidth, maxHorizPadding), minHorizPadding) - let rightPadding = max(min(rightPaddingFraction * screenWidth, maxHorizPadding), minHorizPadding) - let topPadding = max(min(topPaddingFraction * screenHeight, maxTopPadding), minTopPadding) - let bottomPadding = max(min(bottomPaddingFraction * screenHeight, maxBotPadding), minBotPadding) - - // Account for padding - ViewController.usableHeight = screenHeight - topPadding - bottomPadding - ViewController.usableWidth = screenWidth - leftPadding - rightPadding - } override func didReceiveMemoryWarning() { @@ -292,15 +253,6 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega } } - // This is supposed to handle rotations, etc. - // so we re-call setContainerDimensions and change the size associated to SpellDataCell - // But for the moment, the SpellDataCell change doesn't work correctly, and so rotation is disabled - override func viewWillTransition(to size: CGSize, - with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - setContainerDimensions(screenWidth: size.width, screenHeight: size.height) - } - // Until the issue with the SpellDataCell sizing is fixed, let's disable rotation override open var shouldAutorotate: Bool { return false @@ -388,7 +340,7 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // For toggling the sort/filter windows func toggleWindowVisibilities() { filterVisible = !filterVisible - spellTable.isHidden = filterVisible + spellTableView.isHidden = filterVisible sortFilterTableView.isHidden = !filterVisible navigationController?.hidesBarsOnSwipe = !filterVisible searchButton.isEnabled = !filterVisible @@ -414,7 +366,12 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega navigationItem.titleView = searchBar searchBar.alpha = 0 navigationItem.setLeftBarButton(nil, animated: true) - navigationItem.setRightBarButtonItems(nil, animated: true) + if oniPad { + let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(iPadSearchBarCancelButtonClicked)) + navigationItem.setRightBarButtonItems([cancelButton], animated: true) + } else { + navigationItem.setRightBarButtonItems(nil, animated: true) + } self.searchBar.alpha = 1 self.searchBar.becomeFirstResponder() // UIView.animate(withDuration: 0.5, animations: { @@ -444,6 +401,9 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega return toClose } + @objc func iPadSearchBarCancelButtonClicked() { + searchBarCancelButtonClicked(self.searchBar) + } // MARK: UISearchBarDelegate func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { @@ -523,245 +483,15 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega return spells.count } - // Set the footer height -// func tableView(_ tableView: UITableView, heightForFooterInSection: Int) -> CGFloat { -// return 2 * SpellTableViewController.estimatedHeight -// } - - - // Return the footer view - // We override this method so that we can make the background clear - func tableView(_ tableView: UITableView, viewForFooterInSection: Int) -> UIView? { - let view = UIView() - view.backgroundColor = UIColor.clear - return view - } - - // Function for adding SpellDataCell to table - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as! SpellDataCell - - // Get the spell - let spell = spells[indexPath.row] - cell.spell = spell - - setupCell(cell: cell, spell: spell) - - return cell - } - - func setupCell(cell: SpellDataCell, spell: Spell) { - - // Cell formatting - cell.layoutMargins = UIEdgeInsets(top: 2, left: 0, bottom: 2, right: 0) - cell.selectionStyle = .gray - cell.isUserInteractionEnabled = true - cell.backgroundColor = UIColor.clear - - // Set the text for the labels - cell.nameLabel.text = spell.name - cell.levelSchoolLabel.text = spell.levelSchoolString() - cell.sourcebookLabel.text = spell.sourcebooksString() - - for label in [cell.nameLabel, cell.levelSchoolLabel, cell.sourcebookLabel] { - label?.textColor = defaultFontColor - } - - // Set the button images - cell.favoriteButton.setTrueImage(image: ViewController.starFilled!) - cell.favoriteButton.setFalseImage(image: ViewController.starEmpty!) - cell.preparedButton.setTrueImage(image: ViewController.wandFilled!) - cell.preparedButton.setFalseImage(image: ViewController.wandEmpty!) - cell.knownButton.setTrueImage(image: ViewController.bookFilled!) - cell.knownButton.setFalseImage(image: ViewController.bookEmpty!) - - // Set the button statuses - let sfs = store.state.profile?.spellFilterStatus ?? SpellFilterStatus() - cell.favoriteButton.set(sfs.isFavorite(spell)) - cell.preparedButton.set(sfs.isPrepared(spell)) - cell.knownButton.set(sfs.isKnown(spell)) - - // Set the button callbacks - // Set the callbacks for the buttons - cell.favoriteButton.setCallback({ - store.dispatch(TogglePropertyAction(spell: cell.spell, property: .Favorites, markDirty: false)) - }) - cell.preparedButton.setCallback({ - store.dispatch(TogglePropertyAction(spell: cell.spell, property: .Prepared, markDirty: false)) - }) - cell.knownButton.setCallback({ - store.dispatch(TogglePropertyAction(spell: cell.spell, property: .Known, markDirty: false)) - }) - } - - private func makeTargetedPreview(for configuration: UIContextMenuConfiguration, backgroundColor: UIColor? = nil) -> UITargetedPreview? { - guard let indexPath = configuration.identifier as? IndexPath else { return nil } - guard let cell = spellTable.cellForRow(at: indexPath) as? SpellDataCell else { return nil } - cell.contentView.backgroundColor = UIColor.lightGray - let preview = UITargetedPreview(view: cell.contentView) - if (backgroundColor != nil) { - preview.view.backgroundColor = backgroundColor - } - return preview - } - - func setCellBackgroundColor(indexPath: IndexPath, color: UIColor) { - guard let cell = spellTable.cellForRow(at: indexPath) as? SpellDataCell else { return } - cell.contentView.backgroundColor = color - } - - func tableView(_ tableView: UITableView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { - guard let indexPath = configuration.identifier as? IndexPath else { return } - setCellBackgroundColor(indexPath: indexPath, color: UIColor.white) - } - - func tableView(_ tableView: UITableView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { - animator?.addCompletion { - guard let indexPath = configuration.identifier as? IndexPath else { return } - self.setCellBackgroundColor(indexPath: indexPath, color: UIColor.clear) - } - } - - func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - return makeTargetedPreview(for: configuration) - } - -// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { -// let preview = makeTargetedPreview(for: configuration) -// preview?.view.backgroundColor = UIColor.clear -// return preview -// } - - func tableView(_ tableView: UITableView, - contextMenuConfigurationForRowAt indexPath: IndexPath, - point: CGPoint) -> UIContextMenuConfiguration? { - let cell = tableView.cellForRow(at: indexPath) as! SpellDataCell - let spell = cell.spell - return UIContextMenuConfiguration( - identifier: indexPath as NSCopying, - previewProvider: nil, - actionProvider: { - suggestedActions in - let shortcutAction = - UIAction( - title: "Create Shortcut", - image: UIImage(named: "book_empty.png")?.inverseImage(cgResult: true)) { - action in addSpellShortcut(spell: spell) - } - return UIMenu(title: "Spell Options", children: [shortcutAction]) - } - ) - } - func sort() { store.dispatch(SortNeededAction()) } - internal func filterThroughArray(spell: Spell, values: [E], filter: (Spell,E) -> Bool) -> Bool { - for e in values { - if filter(spell, e) { - return false - } - } - return true - } - - internal func filterAgainstBounds(spell s: Spell, bounds: (Quantity,Quantity)?, quantityGetter: (Spell) -> Quantity) -> Bool { - - // If the bounds are nil, this check should be skipped - if (bounds == nil) { return false } - - // Get the quantity - // If it isn't of the spanning type, return false - let quantity = quantityGetter(s) - if quantity.isTypeSpanning() { - return ( (quantity < bounds!.0) || (quantity > bounds!.1) ) - } else { - return false - } - - } - // Function to filter the table data func filter() { store.dispatch(FilterNeededAction()) } - func indexPathsForIDs(spellIDs: [Int]) -> [IndexPath] { - var indexPaths: [IndexPath] = [] - for (idx, spell) in spells.enumerated() { - if (spellIDs.contains(spell.id)) { - let indexPath = IndexPath(item: idx, section: 0) - indexPaths.append(indexPath) - } - } - return indexPaths - } - - // If one of the side menus is open, we want to close the menu rather than select a cell - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - //return main.closeMenuIfOpen() ? nil : indexPath - return indexPath - } - - // Set what happens when a cell is selected - // For us, that's creating a segue to a view with the spell info - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - - tableView.deselectRow(at: indexPath, animated: true) - - if indexPath.row >= spells.count { return } - let spellIndex = indexPath.row - let spell = spells[spellIndex] - - let spellWindowController = storyboard?.instantiateViewController(withIdentifier: spellWindowIdentifier) as! SpellWindowController - spellWindowController.modalPresentationStyle = .fullScreen - spellWindowController.transitioningDelegate = spellWindowController - //view.window?.layer.add(Transitions.fromRightTransition, forKey: kCATransition) - //print("Presenting...") - self.present(spellWindowController, animated: true, completion: nil) - spellWindowController.spell = spell - spellWindowController.spellIndex = spellIndex - UIApplication.shared.setStatusBarTextColor(.light) - } - - - @objc func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) { - let p = gestureRecognizer.location(in: spellTable) - //let pAbs = gestureRecognizer.location(in: main?.view) - //print("Long press at \(pAbs.x), \(pAbs.y)") - let indexPath = spellTable.indexPathForRow(at: p) - if indexPath == nil { - return - } else if (gestureRecognizer.state == UIGestureRecognizer.State.began) { - if indexPath!.row >= spells.count { return } - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let controller = storyboard.instantiateViewController(withIdentifier: "statusPopup") as! StatusPopupController - - let popupHeight = CGFloat(52) - let popupWidth = CGFloat(166) - controller.width = popupWidth - controller.height = popupHeight - let cell = spellTable.cellForRow(at: indexPath!) as! SpellDataCell - let positionX = CGFloat(0) - let positionY = cell.frame.maxY - let position = CGPoint(x: positionX, y: positionY) - let absPosition = view.convert(position, from: self.spellTable) - let popupPosition = PopupViewController.PopupPosition.topLeft(absPosition) - controller.spell = spells[indexPath!.row] - let popupVC = PopupViewController(contentController: controller, position: popupPosition, popupWidth: popupWidth, popupHeight: popupHeight) - popupVC.backgroundAlpha = 0 - self.present(popupVC, animated: true, completion: nil) - } - } - - // Filter on pulldown - @objc func handlePullDown(_ sender: Any) { - filter() - spellTable.reloadData() - spellTable.refreshControl!.endRefreshing() - } - // For navigation bar behavior when scrolling func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -783,24 +513,13 @@ extension ViewController: StoreSubscriber { typealias StoreSubscriberStateType = (profile: CharacterProfile?, sortFilterStatus: SortFilterStatus?, spellFilterStatus: SpellFilterStatus?, currentSpellList: [Spell], dirtySpellIDs: [Int]) func newState(state: StoreSubscriberStateType) { - var needReload = false if let profile = state.profile { if (profile.name != characterProfile.name) { self.setCharacterProfile(cp: profile, initialLoad: false) - needReload = true } } if (state.currentSpellList != spells) { spells = state.currentSpellList - needReload = true - } - if (state.dirtySpellIDs.count > 0 && !needReload) { - let indexPaths = self.indexPathsForIDs(spellIDs: state.dirtySpellIDs) - self.spellTable.reloadRows(at: indexPaths, with: UITableView.RowAnimation.none) - store.dispatch(MarkAllSpellsCleanAction()) - } - if (needReload) { - spellTable.reloadData() } } diff --git a/Spellbook/iOSUtils.swift b/Spellbook/iOSUtils.swift index dda0c46..082de8d 100644 --- a/Spellbook/iOSUtils.swift +++ b/Spellbook/iOSUtils.swift @@ -12,11 +12,14 @@ import Foundation let iOSNSFoundationVersion = NSFoundationVersionNumber let iOSVersion = Version.fromString(UIDevice.current.systemVersion) ?? Version(major: 0, minor: 0, patch: 0) -// The iPhone version -let iPhoneVersion = UIDevice.current.model +// The device version +let deviceVersion = UIDevice.current.model // The current version number of the app let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") ?? "0" // The current build number of the app let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") ?? "0" + +// Are we on an iPad? +let oniPad = UIDevice.current.userInterfaceIdiom == .pad