diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 086a9b2..44906e8 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -38,6 +38,14 @@ BF762E9F1BC19D31002C8866 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF762E9D1BC19D31002C8866 /* DatabaseManager.swift */; }; BF762EAB1BC1B076002C8866 /* NSManagedObject+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */; }; BF762EAC1BC1B076002C8866 /* NSManagedObject+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */; }; + BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; }; + BF7AE7FE1C2E857A00B1B5BC /* GridCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE7FB1C2E857A00B1B5BC /* GridCollectionViewCell.swift */; }; + BF7AE7FF1C2E857A00B1B5BC /* GridCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF7AE7FC1C2E857A00B1B5BC /* GridCollectionViewCell.xib */; }; + BF7AE8001C2E857A00B1B5BC /* GridCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE7FD1C2E857A00B1B5BC /* GridCollectionViewLayout.swift */; }; + BF7AE8051C2E858400B1B5BC /* PausePresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8011C2E858400B1B5BC /* PausePresentationController.swift */; }; + BF7AE8061C2E858400B1B5BC /* PausePresentationControllerContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF7AE8021C2E858400B1B5BC /* PausePresentationControllerContentView.xib */; }; + BF7AE8071C2E858400B1B5BC /* PauseStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8031C2E858400B1B5BC /* PauseStoryboardSegue.swift */; }; + BF7AE8081C2E858400B1B5BC /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* PauseViewController.swift */; }; BF8624881BB743FE00C12EEE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; }; BF8624891BB743FE00C12EEE /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF8624A91BB7464B00C12EEE /* DeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; }; @@ -131,6 +139,14 @@ BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF762E9D1BC19D31002C8866 /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Conveniences.swift"; sourceTree = ""; }; + BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = ""; }; + BF7AE7FB1C2E857A00B1B5BC /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewCell.swift; path = "Delta/Pause Menu/Collection View/GridCollectionViewCell.swift"; sourceTree = SOURCE_ROOT; }; + BF7AE7FC1C2E857A00B1B5BC /* GridCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = GridCollectionViewCell.xib; path = "Delta/Pause Menu/Collection View/GridCollectionViewCell.xib"; sourceTree = SOURCE_ROOT; }; + BF7AE7FD1C2E857A00B1B5BC /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewLayout.swift; path = "Delta/Pause Menu/Collection View/GridCollectionViewLayout.swift"; sourceTree = SOURCE_ROOT; }; + BF7AE8011C2E858400B1B5BC /* PausePresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PausePresentationController.swift; path = "Pause Menu/PausePresentationController.swift"; sourceTree = ""; }; + BF7AE8021C2E858400B1B5BC /* PausePresentationControllerContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PausePresentationControllerContentView.xib; path = "Pause Menu/PausePresentationControllerContentView.xib"; sourceTree = ""; }; + BF7AE8031C2E858400B1B5BC /* PauseStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseStoryboardSegue.swift; path = "Pause Menu/PauseStoryboardSegue.swift"; sourceTree = ""; }; + BF7AE8041C2E858400B1B5BC /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = ""; }; BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFA534291BDC6B520088F1BE /* GameCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameCollectionViewLayout.swift; path = "Collection View/GameCollectionViewLayout.swift"; sourceTree = ""; }; BFAA1FEC1B8AA4FA00495943 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; @@ -197,6 +213,7 @@ children = ( BF090CF21B490D8300DCAB45 /* UIDevice+Vibration.h */, BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */, + BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */, ); path = Extensions; sourceTree = ""; @@ -263,6 +280,18 @@ path = Extensions; sourceTree = ""; }; + BF7AE7FA1C2E851F00B1B5BC /* Pause Menu */ = { + isa = PBXGroup; + children = ( + BF7AE8041C2E858400B1B5BC /* PauseViewController.swift */, + BF7AE8031C2E858400B1B5BC /* PauseStoryboardSegue.swift */, + BF7AE8011C2E858400B1B5BC /* PausePresentationController.swift */, + BF7AE8021C2E858400B1B5BC /* PausePresentationControllerContentView.xib */, + BF9CB2261C2A025700E7D6C8 /* Collection View */, + ); + name = "Pause Menu"; + sourceTree = ""; + }; BF8624621BB7400E00C12EEE /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -282,6 +311,17 @@ name = "Collection View"; sourceTree = ""; }; + BF9CB2261C2A025700E7D6C8 /* Collection View */ = { + isa = PBXGroup; + children = ( + BF7AE7FB1C2E857A00B1B5BC /* GridCollectionViewCell.swift */, + BF7AE7FC1C2E857A00B1B5BC /* GridCollectionViewCell.xib */, + BF7AE7FD1C2E857A00B1B5BC /* GridCollectionViewLayout.swift */, + ); + name = "Collection View"; + path = Emulation; + sourceTree = ""; + }; BF9F4FCD1AAD7B25004C9500 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -360,6 +400,7 @@ BFFA71E01AAC406100EE9DD1 /* Main.storyboard */, BF46894D1AAC469800A2586D /* Game Selection */, BFFB709D1AF99ACA00DE56FE /* Emulation */, + BF7AE7FA1C2E851F00B1B5BC /* Pause Menu */, BFAA1FEB1B8AA4E800495943 /* Settings */, BF090CEE1B490C1A00DCAB45 /* Extensions */, BFFA71DA1AAC406100EE9DD1 /* Supporting Files */, @@ -495,8 +536,10 @@ buildActionMask = 2147483647; files = ( BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */, + BF7AE8061C2E858400B1B5BC /* PausePresentationControllerContentView.xib in Resources */, BFFA71E71AAC406100EE9DD1 /* LaunchScreen.xib in Resources */, BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */, + BF7AE7FF1C2E857A00B1B5BC /* GridCollectionViewCell.xib in Resources */, BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -628,8 +671,10 @@ BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, BF50DC421BD851740024C720 /* GameCollectionViewCell.swift in Sources */, BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, + BF7AE8001C2E857A00B1B5BC /* GridCollectionViewLayout.swift in Sources */, BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */, BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */, + BF7AE8071C2E858400B1B5BC /* PauseStoryboardSegue.swift in Sources */, BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */, BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */, BF4566E81BC090B6007BFA1A /* Model.xcdatamodeld in Sources */, @@ -641,10 +686,14 @@ BFA5342A1BDC6B520088F1BE /* GameCollectionViewLayout.swift in Sources */, BF762E9E1BC19D31002C8866 /* DatabaseManager.swift in Sources */, BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */, + BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, BFDB28451BC9DA7B001D0C83 /* GamePickerController.swift in Sources */, + BF7AE8051C2E858400B1B5BC /* PausePresentationController.swift in Sources */, + BF7AE7FE1C2E857A00B1B5BC /* GridCollectionViewCell.swift in Sources */, BFDE393A1BC0CEDF003F72E8 /* Game+CoreDataProperties.swift in Sources */, + BF7AE8081C2E858400B1B5BC /* PauseViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index afcc684..c5c0349 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -1,7 +1,8 @@ - + + @@ -151,12 +152,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -196,6 +302,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/Delta/Emulation/EmulationViewController.swift b/Delta/Emulation/EmulationViewController.swift index d233295..a284a76 100644 --- a/Delta/Emulation/EmulationViewController.swift +++ b/Delta/Emulation/EmulationViewController.swift @@ -121,8 +121,47 @@ class EmulationViewController: UIViewController } } - //MARK: - Controllers - - /// Controllers + // MARK: - Navigation - + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) + { + self.emulatorCore.pauseEmulation() + + if segue.identifier == "pauseSegue" + { + if let destinationViewController = segue.destinationViewController as? UINavigationController, pauseViewController = destinationViewController.topViewController as? PauseViewController + { + pauseViewController.pauseText = self.game.name + } + } + } + + @IBAction func unwindFromPauseViewController(segue: UIStoryboardSegue) + { + self.emulatorCore.resumeEmulation() + } + + //MARK: - 3D Touch - + /// 3D Touch + override func previewActionItems() -> [UIPreviewActionItem] + { + let presentingViewController = self.presentingViewController + + let launchGameAction = UIPreviewAction(title: NSLocalizedString("Launch \(self.game.name)", comment: ""), style: .Default) { (action, viewController) in + // Delaying until next run loop prevents self from being dismissed immediately + dispatch_async(dispatch_get_main_queue()) { + presentingViewController?.presentViewController(viewController, animated: true, completion: nil) + } + } + return [launchGameAction] + } +} + +//MARK: - Controllers - +/// Controllers +private extension EmulationViewController +{ func updateControllers() { self.emulatorCore.removeAllGameControllers() @@ -142,21 +181,6 @@ class EmulationViewController: UIViewController self.view.setNeedsLayout() } - - //MARK: - 3D Touch - - /// 3D Touch - override func previewActionItems() -> [UIPreviewActionItem] - { - let presentingViewController = self.presentingViewController - - let launchGameAction = UIPreviewAction(title: NSLocalizedString("Launch \(self.game.name)", comment: ""), style: .Default) { (action, viewController) in - // Delaying until next run loop prevents self from being dismissed immediately - dispatch_async(dispatch_get_main_queue()) { - presentingViewController?.presentViewController(viewController, animated: true, completion: nil) - } - } - return [launchGameAction] - } } //MARK: - - @@ -173,6 +197,11 @@ extension EmulationViewController: GameControllerReceiverType guard let input = input as? ControllerInput else { return } print("Activated \(input)") + + switch input + { + case ControllerInput.Menu: self.performSegueWithIdentifier("pauseSegue", sender: gameController) + } } func gameController(gameController: GameControllerType, didDeactivateInput input: InputType) diff --git a/Delta/Extensions/UILabel+FontSize.swift b/Delta/Extensions/UILabel+FontSize.swift new file mode 100644 index 0000000..c906e04 --- /dev/null +++ b/Delta/Extensions/UILabel+FontSize.swift @@ -0,0 +1,28 @@ +// +// UILabel+FontSize.swift +// Delta +// +// Created by Riley Testut on 12/25/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +internal extension UILabel +{ + var currentScaleFactor: CGFloat + { + guard let text = self.text else { return 1.0 } + + let context = NSStringDrawingContext() + context.minimumScaleFactor = self.minimumScaleFactor + + // Using self.attributedString returns incorrect calculations, so we create our own attributed string + let attributedString = NSAttributedString(string: text, attributes: [NSFontAttributeName: self.font]) + attributedString.boundingRectWithSize(self.bounds.size, options: [.UsesLineFragmentOrigin, .UsesFontLeading], context: context) + + let scaleFactor = context.actualScaleFactor + return scaleFactor + } +} + diff --git a/Delta/Pause Menu/Collection View/GridCollectionViewCell.swift b/Delta/Pause Menu/Collection View/GridCollectionViewCell.swift new file mode 100644 index 0000000..b6d1879 --- /dev/null +++ b/Delta/Pause Menu/Collection View/GridCollectionViewCell.swift @@ -0,0 +1,105 @@ +// +// GridCollectionViewCell.swift +// Delta +// +// Created by Riley Testut on 10/21/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class GridCollectionViewCell: UICollectionViewCell +{ + @IBOutlet private(set) var imageView: UIImageView! + @IBOutlet private(set) var textLabel: UILabel! + + var maximumImageSize: CGSize = CGSize(width: 100, height: 100) { + didSet { + self.updateMaximumImageSize() + } + } + + @IBOutlet private var imageViewWidthConstraint: NSLayoutConstraint! + @IBOutlet private var imageViewHeightConstraint: NSLayoutConstraint! + + @IBOutlet private var textLabelBottomAnchorConstraint: NSLayoutConstraint! + + @IBOutlet private var textLabelVerticalSpacingConstraint: NSLayoutConstraint! + private var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint? + + override func awakeFromNib() + { + super.awakeFromNib() + + // Fix super annoying Unsatisfiable Constraints message in debugger by setting autoresizingMask + self.contentView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] + + self.imageView.translatesAutoresizingMaskIntoConstraints = false + #if os(tvOS) + self.imageView.adjustsImageWhenAncestorFocused = true + #endif + self.contentView.addSubview(self.imageView) + + self.textLabel.translatesAutoresizingMaskIntoConstraints = false + self.textLabel.font = UIFont.boldSystemFontOfSize(12) + self.textLabel.textAlignment = .Center + self.textLabel.numberOfLines = 0 + self.contentView.addSubview(self.textLabel) + + // Auto Layout + + #if os(tvOS) + self.textLabelVerticalSpacingConstraint.active = false + + self.textLabelFocusedVerticalSpacingConstraint = self.textLabel.topAnchor.constraintEqualToAnchor(self.imageView.focusedFrameGuide.bottomAnchor, constant: verticalSpacingConstant) + self.textLabelFocusedVerticalSpacingConstraint?.active = true + #else + self.textLabelVerticalSpacingConstraint.active = true + #endif + + self.updateMaximumImageSize() + } + + override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) + { + super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator) + + coordinator.addCoordinatedAnimations({ + + if context.nextFocusedView == self + { + self.textLabelBottomAnchorConstraint?.active = false + self.textLabelVerticalSpacingConstraint.active = false + + self.textLabelFocusedVerticalSpacingConstraint?.active = true + + self.textLabel.textColor = UIColor.whiteColor() + + } + else + { + self.textLabelFocusedVerticalSpacingConstraint?.active = false + + self.textLabelBottomAnchorConstraint?.active = true + self.textLabelVerticalSpacingConstraint.active = true + + self.textLabel.textColor = UIColor.blackColor() + } + + self.layoutIfNeeded() + + }, completion: nil) + } +} + +private extension GridCollectionViewCell +{ + func updateMaximumImageSize() + { + self.imageViewWidthConstraint.constant = self.maximumImageSize.width + self.imageViewHeightConstraint.constant = self.maximumImageSize.height + + self.textLabelVerticalSpacingConstraint.constant = self.maximumImageSize.height / 10.0 + self.textLabelFocusedVerticalSpacingConstraint?.constant = self.maximumImageSize.height / 10.0 + } +} \ No newline at end of file diff --git a/Delta/Pause Menu/Collection View/GridCollectionViewCell.xib b/Delta/Pause Menu/Collection View/GridCollectionViewCell.xib new file mode 100644 index 0000000..b610cf8 --- /dev/null +++ b/Delta/Pause Menu/Collection View/GridCollectionViewCell.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Pause Menu/Collection View/GridCollectionViewLayout.swift b/Delta/Pause Menu/Collection View/GridCollectionViewLayout.swift new file mode 100644 index 0000000..0467743 --- /dev/null +++ b/Delta/Pause Menu/Collection View/GridCollectionViewLayout.swift @@ -0,0 +1,121 @@ +// +// GridCollectionViewLayout.swift +// Delta +// +// Created by Riley Testut on 10/24/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class GridCollectionViewLayout: UICollectionViewFlowLayout +{ + var itemWidth: CGFloat = 150 { + didSet { + self.invalidateLayout() + } + } + + // If only one row, distribute the items equally horizontally + var usesEqualHorizontalSpacingDistributionForSingleRow = false + + override var estimatedItemSize: CGSize { + didSet { + fatalError("GridCollectionViewLayout does not support self-sizing cells.") + } + } + + override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? + { + guard let collectionView = self.collectionView else { return nil } + + let maximumItemsPerRow = floor((collectionView.bounds.width - self.minimumInteritemSpacing) / (self.itemWidth + self.minimumInteritemSpacing)) + let interitemSpacing = (collectionView.bounds.width - maximumItemsPerRow * self.itemWidth) / (maximumItemsPerRow + 1) + + self.sectionInset.left = interitemSpacing + self.sectionInset.right = interitemSpacing + + let layoutAttributes = super.layoutAttributesForElementsInRect(rect)?.map({ $0.copy() }) as! [UICollectionViewLayoutAttributes] + + var minimumY: CGFloat? = nil + var maximumY: CGFloat? = nil + var tempLayoutAttributes: [UICollectionViewLayoutAttributes] = [] + + var isSingleRow = true + + for (index, attributes) in layoutAttributes.enumerate() + { + // Ensure equal spacing between items (that also match the section insets) + if index > 0 + { + let previousLayoutAttributes = layoutAttributes[index - 1] + + if abs(attributes.frame.minX - self.sectionInset.left) > 1 + { + attributes.frame.origin.x = previousLayoutAttributes.frame.maxX + interitemSpacing + } + } + + if let maxY = maximumY, minY = minimumY + { + // If attributes.frame.minY is greater than maximumY, then it is a new row + // In this case, we need to align all the previous tempLayoutAttributes to the same Y-value + if attributes.frame.minY > maxY + { + isSingleRow = false + + self.alignLayoutAttributes(tempLayoutAttributes, toMinimumY: minY) + + // Reset tempLayoutAttributes + tempLayoutAttributes.removeAll() + minimumY = nil + maximumY = nil + } + } + + // Update minimumY value if needed + if minimumY == nil || attributes.frame.minY < minimumY! + { + minimumY = attributes.frame.minY + } + + // Update maximumY value if needed + if maximumY == nil || attributes.frame.maxY > maximumY! + { + maximumY = attributes.frame.maxY + } + + tempLayoutAttributes.append(attributes) + } + + // Handle the remaining tempLayoutAttributes + if let minimumY = minimumY + { + self.alignLayoutAttributes(tempLayoutAttributes, toMinimumY: minimumY) + + if isSingleRow && self.usesEqualHorizontalSpacingDistributionForSingleRow + { + let spacing = (collectionView.bounds.width - (self.itemWidth * CGFloat(tempLayoutAttributes.count))) / (CGFloat(tempLayoutAttributes.count) + 1.0) + + for (index, layoutAttributes) in tempLayoutAttributes.enumerate() + { + layoutAttributes.frame.origin.x = spacing + (spacing + self.itemWidth) * CGFloat(index) + } + } + } + + return layoutAttributes + } + +} + +private extension GridCollectionViewLayout +{ + func alignLayoutAttributes(layoutAttributes: [UICollectionViewLayoutAttributes], toMinimumY minimumY: CGFloat) + { + for attributes in layoutAttributes + { + attributes.frame.origin.y = minimumY + } + } +} diff --git a/Delta/Pause Menu/PausePresentationController.swift b/Delta/Pause Menu/PausePresentationController.swift new file mode 100644 index 0000000..cc618f7 --- /dev/null +++ b/Delta/Pause Menu/PausePresentationController.swift @@ -0,0 +1,178 @@ +// +// PausePresentationController.swift +// Delta +// +// Created by Riley Testut on 12/21/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +import Roxas + +protocol PauseInfoProvidable +{ + var pauseText: String? { get } +} + +class PausePresentationController: UIPresentationController +{ + private let blurringView: UIVisualEffectView + private let vibrancyView: UIVisualEffectView + + private var contentView: UIView! + @IBOutlet private weak var pauseLabel: UILabel! + @IBOutlet private weak var pauseIconImageView: UIImageView! + @IBOutlet private weak var stackView: UIStackView! + + override init(presentedViewController: UIViewController, presentingViewController: UIViewController) + { + self.blurringView = UIVisualEffectView(effect: nil) + self.vibrancyView = UIVisualEffectView(effect: nil) + + super.init(presentedViewController: presentedViewController, presentingViewController: presentingViewController) + + self.contentView = NSBundle.mainBundle().loadNibNamed("PausePresentationControllerContentView", owner: self, options: nil).first as! UIView + } + + override func frameOfPresentedViewInContainerView() -> CGRect + { + guard let containerView = self.containerView else { return super.frameOfPresentedViewInContainerView() } + + let contentHeight: CGFloat + + if let navigationController = self.presentedViewController as? UINavigationController, topViewController = navigationController.topViewController + { + contentHeight = topViewController.preferredContentSize.height + navigationController.navigationBar.bounds.height + } + else + { + contentHeight = self.presentedViewController.preferredContentSize.height + } + + let frame = CGRect(x: 0, y: containerView.bounds.height - contentHeight, width: containerView.bounds.width, height: containerView.bounds.height) + return frame + } + + override func presentationTransitionWillBegin() + { + if let provider = self.presentedViewController as? PauseInfoProvidable + { + self.pauseLabel.text = provider.pauseText + } + else if let navigationController = self.presentedViewController as? UINavigationController, provider = navigationController.topViewController as? PauseInfoProvidable + { + self.pauseLabel.text = provider.pauseText + } + else + { + self.pauseLabel.text = nil + } + + self.blurringView.frame = self.containerView!.frame + self.blurringView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] + self.containerView?.addSubview(self.blurringView) + + self.vibrancyView.frame = self.containerView!.frame + self.vibrancyView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] + self.containerView?.addSubview(self.vibrancyView) + + self.contentView.alpha = 0.0 + self.vibrancyView.contentView.addSubview(self.contentView) + + self.presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in + + let blurEffect = UIBlurEffect(style: .Dark) + + self.blurringView.effect = blurEffect + self.vibrancyView.effect = UIVibrancyEffect(forBlurEffect: blurEffect) + + self.contentView.alpha = 1.0 + + }, completion: nil) + } + + override func dismissalTransitionWillBegin() + { + self.presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in + self.blurringView.effect = nil + self.vibrancyView.effect = nil + self.contentView.alpha = 0.0 + }, completion: nil) + } + + override func dismissalTransitionDidEnd(completed: Bool) + { + self.blurringView.removeFromSuperview() + self.vibrancyView.removeFromSuperview() + } + + override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) + { + super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator) + + // Super super hacky, but the system for some reason tries to layout the view in a (slightly) smaller space, which sometimes breaks constraints + // To fix this, we ensure there is enough room for the constraints to be valid temporarily, and then the frame will be fixed in containerViewDidLayoutSubviews() + let currentY = self.contentView.frame.minY + self.contentView.frame = self.containerView!.bounds + self.contentView.frame.origin.y = currentY + } + + override func containerViewDidLayoutSubviews() + { + super.containerViewDidLayoutSubviews() + + // Magical calculations. If you edit ANY of them, you have to make sure everything still lays out correctly on *all* devices + // So, I'd recommend that you not touch this :) + + + /* Hacky Layout Bug Workaround */ + + + // For some reason, attempting to calculate the layout while contentView is in the view hierarchy doesn't properly follow constraint priorities exactly + // Specifically, on 5s with long pause label text, it will sometimes resize the text before the image, or it will not resize the image enough for the size class + self.contentView.removeFromSuperview() + + // Temporarily match the bounds of self.containerView (accounting for the status bar) + let statusBarHeight = UIApplication.sharedApplication().statusBarFrame.height + self.contentView.frame = CGRect(x: 0, y: statusBarHeight, width: self.containerView!.bounds.width, height: self.containerView!.bounds.height - statusBarHeight) + + // Layout content view + self.contentView.setNeedsLayout() + self.contentView.layoutIfNeeded() + + // Add back to the view hierarchy + self.vibrancyView.contentView.addSubview(self.contentView) + + + /* Resume Normal Calculations */ + + + // Ensure width is correct + self.presentedView()?.bounds.size.width = self.containerView!.bounds.width + self.presentedView()?.layoutIfNeeded() + + self.presentedView()?.frame = self.frameOfPresentedViewInContainerView() + + // Unhide pauseIconImageView so its height is involved with layout calculations + self.pauseIconImageView.hidden = false + + self.contentView.frame = CGRect(x: 0, y: statusBarHeight, width: self.containerView!.bounds.width, height: self.frameOfPresentedViewInContainerView().minY - statusBarHeight) + + self.contentView.setNeedsLayout() // Ensures that layout will actually occur (sometimes the system thinks a layout is not needed, which messes up calculations) + self.contentView.layoutIfNeeded() + + let currentScaleFactor = self.pauseLabel.currentScaleFactor + if currentScaleFactor < self.pauseLabel.minimumScaleFactor || CGFloatEqualToFloat(currentScaleFactor, self.pauseLabel.minimumScaleFactor) + { + self.pauseIconImageView.hidden = true + } + else + { + self.pauseIconImageView.hidden = false + } + + self.contentView.setNeedsLayout() // Ensures that layout will actually occur (sometimes the system thinks a layout is not needed, which messes up calculations) + self.contentView.layoutIfNeeded() + } +} diff --git a/Delta/Pause Menu/PausePresentationControllerContentView.xib b/Delta/Pause Menu/PausePresentationControllerContentView.xib new file mode 100644 index 0000000..fc98d62 --- /dev/null +++ b/Delta/Pause Menu/PausePresentationControllerContentView.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Pause Menu/PauseStoryboardSegue.swift b/Delta/Pause Menu/PauseStoryboardSegue.swift new file mode 100644 index 0000000..72bd396 --- /dev/null +++ b/Delta/Pause Menu/PauseStoryboardSegue.swift @@ -0,0 +1,79 @@ +// +// PauseStoryboardSegue.swift +// Delta +// +// Created by Riley Testut on 12/21/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class PauseStoryboardSegue: UIStoryboardSegue +{ + private let presentationController: PausePresentationController + + override init(identifier: String?, source: UIViewController, destination: UIViewController) + { + self.presentationController = PausePresentationController(presentedViewController: destination, presentingViewController: source) + + super.init(identifier: identifier, source: source, destination: destination) + } + + override func perform() + { + self.destinationViewController.transitioningDelegate = self + self.destinationViewController.modalPresentationStyle = .Custom + self.destinationViewController.modalPresentationCapturesStatusBarAppearance = true + + super.perform() + } +} + +extension PauseStoryboardSegue: UIViewControllerTransitioningDelegate +{ + func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + return self + } + + func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + return nil + } + + func presentationControllerForPresentedViewController(presentedViewController: UIViewController, presentingViewController: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? + { + return self.presentationController + } +} + +extension PauseStoryboardSegue: UIViewControllerAnimatedTransitioning +{ + func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval + { + return 0.65 + } + + func animateTransition(transitionContext: UIViewControllerContextTransitioning) + { + let destinationViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! UINavigationController + + destinationViewController.view.frame = transitionContext.finalFrameForViewController(destinationViewController) + destinationViewController.view.frame.origin.y = transitionContext.containerView()!.bounds.height + transitionContext.containerView()!.addSubview(destinationViewController.view) + + UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: { + + // Calling layoutIfNeeded before the animation block for some reason prevents the blur from fading in + // Additionally, if it's animated, it looks weird + // So we need to wrap it in a no-animation block, inside an animation block. Blech. + UIView.performWithoutAnimation({ + destinationViewController.view.layoutIfNeeded() + }) + + destinationViewController.view.frame = self.presentationController.frameOfPresentedViewInContainerView() + }, completion: { finished in + transitionContext.completeTransition(finished) + }) + } +} diff --git a/Delta/Pause Menu/PauseViewController.swift b/Delta/Pause Menu/PauseViewController.swift new file mode 100644 index 0000000..4bc9114 --- /dev/null +++ b/Delta/Pause Menu/PauseViewController.swift @@ -0,0 +1,111 @@ +// +// PauseViewController.swift +// Delta +// +// Created by Riley Testut on 12/21/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +struct PauseItem +{ + let image: UIImage + let text: String + let action: (PauseItem -> Void) +} + +class PauseViewController: UIViewController, PauseInfoProvidable +{ + var items = [PauseItem]() { + didSet { + + if self.items.count > 8 + { + fatalError("PauseViewController only supports up to 8 items (for my sanity when laying out on a landscape iPhone 4s") + } + + self.collectionView?.reloadData() + } + } + + var pauseText: String? = nil + + override var preferredContentSize: CGSize { + set { } + get { return self.collectionView.contentSize } + } + + @IBOutlet private(set) var collectionView: UICollectionView! + private var collectionViewLayout: GridCollectionViewLayout { + return self.collectionView.collectionViewLayout as! GridCollectionViewLayout + } + + private var prototypeCell: GridCollectionViewCell = { + let nib = UINib(nibName: "GridCollectionViewCell", bundle: nil) + return nib.instantiateWithOwner(nil, options: nil).first as! GridCollectionViewCell + }() + + override func preferredStatusBarStyle() -> UIStatusBarStyle + { + return .LightContent + } + + override func viewDidLoad() + { + super.viewDidLoad() + + let nib = UINib(nibName: "GridCollectionViewCell", bundle: nil) + self.collectionView.registerNib(nib, forCellWithReuseIdentifier: "Cell") + + self.collectionViewLayout.itemWidth = 90 + self.collectionViewLayout.usesEqualHorizontalSpacingDistributionForSingleRow = true + + // Manually update prototype cell properties + self.prototypeCell.contentView.widthAnchor.constraintEqualToConstant(self.collectionViewLayout.itemWidth).active = true + + let pauseItem = PauseItem(image: UIImage(named: "Pause")!, text: "Resume", action: { _ in }) + self.items = [pauseItem, pauseItem, pauseItem, pauseItem, pauseItem, pauseItem] + } +} + +private extension PauseViewController +{ + func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath) + { + let array = ["Save State", "Load State", "Cheat Codes", "Fast Forward", "Sustain Button", "Event Distribution"] + + cell.maximumImageSize = CGSize(width: 60, height: 60) + + cell.imageView.layer.borderWidth = 2 + cell.imageView.layer.borderColor = UIColor.whiteColor().CGColor + cell.imageView.layer.cornerRadius = 10 + + cell.textLabel.text = array[indexPath.item] + cell.textLabel.textColor = UIColor.whiteColor() + } +} + +extension PauseViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout +{ + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int + { + return self.items.count + } + + func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell + { + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! GridCollectionViewCell + self.configureCollectionViewCell(cell, forIndexPath: indexPath) + return cell + } + + func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize + { + self.configureCollectionViewCell(self.prototypeCell, forIndexPath: indexPath) + + let size = self.prototypeCell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) + return size + } +} + diff --git a/External/Roxas b/External/Roxas index 12973b6..325d2c7 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit 12973b60eeddd6e5b3b398d5ec5b2dadd461810d +Subproject commit 325d2c786bcb9cc61389257023399282e1d8071d diff --git a/Resources/Assets.xcassets/Pause.imageset/Contents.json b/Resources/Assets.xcassets/Pause.imageset/Contents.json new file mode 100644 index 0000000..5e3afa3 --- /dev/null +++ b/Resources/Assets.xcassets/Pause.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Pause.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Pause@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Pause@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Resources/Assets.xcassets/Pause.imageset/Pause.png b/Resources/Assets.xcassets/Pause.imageset/Pause.png new file mode 100644 index 0000000..714ddbf Binary files /dev/null and b/Resources/Assets.xcassets/Pause.imageset/Pause.png differ diff --git a/Resources/Assets.xcassets/Pause.imageset/Pause@2x.png b/Resources/Assets.xcassets/Pause.imageset/Pause@2x.png new file mode 100644 index 0000000..c581c70 Binary files /dev/null and b/Resources/Assets.xcassets/Pause.imageset/Pause@2x.png differ diff --git a/Resources/Assets.xcassets/Pause.imageset/Pause@3x.png b/Resources/Assets.xcassets/Pause.imageset/Pause@3x.png new file mode 100644 index 0000000..e5109ea Binary files /dev/null and b/Resources/Assets.xcassets/Pause.imageset/Pause@3x.png differ