From 59fe9b0480e199071e0ad62b5ee12967f1f5f883 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 1 Nov 2015 03:19:10 -0600 Subject: [PATCH] Added basic UICollectionView-based UI for selecting games --- .../GameCollectionViewCell.swift | 137 +++++++++++++ .../GameCollectionViewDataSource.swift | 122 ++++++++++++ .../GameCollectionViewLayout.swift | 113 +++++++++++ .../GameCollectionViewLayoutAttributes.swift | 32 +++ Common/Components/BoxArtImageView.swift | 55 ++++++ Delta.xcodeproj/project.pbxproj | 89 +++++++-- Delta/Base.lproj/Main.storyboard | 183 +++++++++++++++++- Delta/Emulation/EmulationViewController.swift | 21 +- Delta/Emulation/EmulationViewController.xib | 68 ------- .../DirectoryContentsDataSource.swift | 118 ----------- .../GamesCollectionViewController.swift | 86 ++++++++ .../Game Selection/GamesViewController.swift | 116 +++++------ DeltaTV/Base.lproj/Main.storyboard | 112 +++++++++-- DeltaTV/EmulationViewController.swift | 59 ++++++ DeltaTV/GameSelectionViewController.swift | 103 ++++++++++ DeltaTV/ViewController.swift | 25 --- Pods/Pods.xcodeproj/project.pbxproj | 12 +- .../BoxArt.imageset/BoxArt-1.png | Bin 0 -> 10411 bytes .../BoxArt.imageset/BoxArt.png | Bin 0 -> 3052 bytes .../BoxArt.imageset/BoxArt@2x.png | Bin 0 -> 6415 bytes .../BoxArt.imageset/BoxArt@3x.png | Bin 0 -> 10411 bytes .../BoxArt.imageset/Contents.json | 28 +++ 22 files changed, 1156 insertions(+), 323 deletions(-) create mode 100644 Common/Collection View/GameCollectionViewCell.swift create mode 100644 Common/Collection View/GameCollectionViewDataSource.swift create mode 100644 Common/Collection View/GameCollectionViewLayout.swift create mode 100644 Common/Collection View/GameCollectionViewLayoutAttributes.swift create mode 100644 Common/Components/BoxArtImageView.swift delete mode 100644 Delta/Emulation/EmulationViewController.xib delete mode 100644 Delta/Game Selection/DirectoryContentsDataSource.swift create mode 100644 Delta/Game Selection/GamesCollectionViewController.swift create mode 100644 DeltaTV/EmulationViewController.swift create mode 100644 DeltaTV/GameSelectionViewController.swift delete mode 100644 DeltaTV/ViewController.swift create mode 100644 Resources/Assets.xcassets/BoxArt.imageset/BoxArt-1.png create mode 100644 Resources/Assets.xcassets/BoxArt.imageset/BoxArt.png create mode 100644 Resources/Assets.xcassets/BoxArt.imageset/BoxArt@2x.png create mode 100644 Resources/Assets.xcassets/BoxArt.imageset/BoxArt@3x.png create mode 100644 Resources/Assets.xcassets/BoxArt.imageset/Contents.json diff --git a/Common/Collection View/GameCollectionViewCell.swift b/Common/Collection View/GameCollectionViewCell.swift new file mode 100644 index 0000000..3e8707a --- /dev/null +++ b/Common/Collection View/GameCollectionViewCell.swift @@ -0,0 +1,137 @@ +// +// GameCollectionViewCell.swift +// Delta +// +// Created by Riley Testut on 10/21/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class GameCollectionViewCell: UICollectionViewCell +{ + let imageView = BoxArtImageView() + let nameLabel = UILabel() + + private var maximumBoxArtSize: CGSize = CGSize(width: 100, height: 100) { + didSet + { + self.imageViewWidthConstraint.constant = self.maximumBoxArtSize.width + self.imageViewHeightConstraint.constant = self.maximumBoxArtSize.height + + self.nameLabelVerticalSpacingConstraint.constant = self.maximumBoxArtSize.height / 20.0 + self.nameLabelFocusedVerticalSpacingConstraint?.constant = self.maximumBoxArtSize.height / 20.0 + + } + } + + private var imageViewWidthConstraint: NSLayoutConstraint! + private var imageViewHeightConstraint: NSLayoutConstraint! + + private var nameLabelBottomAnchorConstraint: NSLayoutConstraint! + + private var nameLabelVerticalSpacingConstraint: NSLayoutConstraint! + private var nameLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint? + + override init(frame: CGRect) + { + super.init(frame: frame) + + self.configureSubviews() + } + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + self.configureSubviews() + } + + private func configureSubviews() + { + // Fix super annoying Unsatisfiable Constraints message in debugger + self.contentView.translatesAutoresizingMaskIntoConstraints = false + + self.imageView.translatesAutoresizingMaskIntoConstraints = false + self.contentView.addSubview(self.imageView) + + self.nameLabel.translatesAutoresizingMaskIntoConstraints = false + self.nameLabel.font = UIFont.boldSystemFontOfSize(12) + self.nameLabel.textAlignment = .Center + self.nameLabel.numberOfLines = 0 + self.contentView.addSubview(self.nameLabel) + + + // Auto Layout + + self.imageView.leadingAnchor.constraintEqualToAnchor(self.contentView.leadingAnchor).active = true + self.imageView.trailingAnchor.constraintEqualToAnchor(self.contentView.trailingAnchor).active = true + self.imageView.topAnchor.constraintEqualToAnchor(self.contentView.topAnchor).active = true + + let verticalSpacingConstant = self.maximumBoxArtSize.height / 20.0 + + self.nameLabelVerticalSpacingConstraint = self.nameLabel.topAnchor.constraintEqualToAnchor(self.imageView.bottomAnchor, constant: verticalSpacingConstant) + + #if os(tvOS) + + self.nameLabelVerticalSpacingConstraint.active = false + + self.nameLabelFocusedVerticalSpacingConstraint = self.nameLabel.topAnchor.constraintEqualToAnchor(self.imageView.focusedFrameGuide.bottomAnchor, constant: verticalSpacingConstant) + self.nameLabelFocusedVerticalSpacingConstraint?.active = true + #else + self.nameLabelVerticalSpacingConstraint.active = true + #endif + + self.nameLabel.leadingAnchor.constraintEqualToAnchor(self.contentView.leadingAnchor).active = true + self.nameLabel.trailingAnchor.constraintEqualToAnchor(self.contentView.trailingAnchor).active = true + + self.nameLabelBottomAnchorConstraint = self.nameLabel.bottomAnchor.constraintEqualToAnchor(self.contentView.bottomAnchor) + self.nameLabelBottomAnchorConstraint.active = true + + self.imageViewWidthConstraint = self.imageView.widthAnchor.constraintEqualToConstant(self.maximumBoxArtSize.width) + self.imageViewWidthConstraint.active = true + + self.imageViewHeightConstraint = self.imageView.heightAnchor.constraintEqualToConstant(self.maximumBoxArtSize.height) + self.imageViewHeightConstraint.active = true + } + + override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) + { + guard let attributes = layoutAttributes as? GameCollectionViewLayoutAttributes else { return } + + self.maximumBoxArtSize = attributes.maximumBoxArtSize + } + + override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) + { + super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator) + + coordinator.addCoordinatedAnimations({ + + if context.nextFocusedView == self + { + self.nameLabelBottomAnchorConstraint?.active = false + self.nameLabelVerticalSpacingConstraint.active = false + + self.nameLabelFocusedVerticalSpacingConstraint?.active = true + + self.nameLabel.textColor = UIColor.whiteColor() + + } + else + { + self.nameLabelFocusedVerticalSpacingConstraint?.active = false + + self.nameLabelBottomAnchorConstraint?.active = true + self.nameLabelVerticalSpacingConstraint.active = true + + self.nameLabel.textColor = UIColor.blackColor() + } + + self.layoutIfNeeded() + + }, completion: nil) + } + + +} diff --git a/Common/Collection View/GameCollectionViewDataSource.swift b/Common/Collection View/GameCollectionViewDataSource.swift new file mode 100644 index 0000000..0e20705 --- /dev/null +++ b/Common/Collection View/GameCollectionViewDataSource.swift @@ -0,0 +1,122 @@ +// +// GameCollectionViewDataSource.swift +// Delta +// +// Created by Riley Testut on 10/30/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit +import CoreData + +class GameCollectionViewDataSource: NSObject +{ + var gameTypeIdentifiers: [String] = [] { + didSet + { + self.updateFetchedResultsController() + } + } + + var sectionTitles: [String] = [] + + var cellConfigurationHandler: ((GameCollectionViewCell, Game) -> Void)? + + private(set) var fetchedResultsController: NSFetchedResultsController = NSFetchedResultsController() + private let prototypeCell = GameCollectionViewCell(frame: CGRectZero) + + // MARK: - Update - + + func update() + { + do + { + try self.fetchedResultsController.performFetch() + } + catch let error as NSError + { + print(error) + } + } + + private func updateFetchedResultsController() + { + let previousDelegate = self.fetchedResultsController.delegate + + let fetchRequest = Game.fetchRequest() + + var predicates: [NSPredicate] = [] + + for typeIdentifier in self.gameTypeIdentifiers + { + let predicate = NSPredicate(format: "%K == %@", GameAttributes.typeIdentifier.rawValue, typeIdentifier) + predicates.append(predicate) + } + + if predicates.count > 0 + { + fetchRequest.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates) + } + + fetchRequest.sortDescriptors = [NSSortDescriptor(key: GameAttributes.name.rawValue, ascending: true)] + + self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: GameAttributes.typeIdentifier.rawValue, cacheName: nil) + self.fetchedResultsController.delegate = previousDelegate + + self.update() + } + + // MARK: - Collection View - + + private func configureCell(cell: GameCollectionViewCell, forIndexPath indexPath: NSIndexPath) + { + let game = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Game + + if let handler = self.cellConfigurationHandler + { + handler(cell, game) + } + } +} + +extension GameCollectionViewDataSource: UICollectionViewDataSource +{ + func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int + { + return self.fetchedResultsController.sections?.count ?? 0 + } + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int + { + let count = self.fetchedResultsController.sections?[section].numberOfObjects ?? 0 + return count + } + + func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell + { + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("GameCell", forIndexPath: indexPath) as! GameCollectionViewCell + + self.configureCell(cell, forIndexPath: indexPath) + + return cell + } +} + +extension GameCollectionViewDataSource: UICollectionViewDelegate +{ + func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize + { + if let layoutAttributes = collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath) + { + self.prototypeCell.applyLayoutAttributes(layoutAttributes) + } + + self.configureCell(self.prototypeCell, forIndexPath: indexPath) + self.prototypeCell.layoutIfNeeded() + + let size = self.prototypeCell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) + return size + } + + +} diff --git a/Common/Collection View/GameCollectionViewLayout.swift b/Common/Collection View/GameCollectionViewLayout.swift new file mode 100644 index 0000000..41720a7 --- /dev/null +++ b/Common/Collection View/GameCollectionViewLayout.swift @@ -0,0 +1,113 @@ +// +// GameCollectionViewLayout.swift +// Delta +// +// Created by Riley Testut on 10/24/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class GameCollectionViewLayout: UICollectionViewFlowLayout +{ + var maximumBoxArtSize = CGSize(width: 100, height: 100) { + didSet + { + self.invalidateLayout() + } + } + + override class func layoutAttributesClass() -> AnyClass + { + return GameCollectionViewLayoutAttributes.self + } + + override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? + { + // Need to implement this method as well in case the view controller calls it (which it does) + + let layoutAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as! GameCollectionViewLayoutAttributes + self.configureLayoutAttributes(layoutAttributes) + + return layoutAttributes + } + + override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? + { + guard let collectionView = self.collectionView else { return nil } + + let maximumItemsPerRow = floor((collectionView.bounds.width - self.minimumInteritemSpacing) / (self.maximumBoxArtSize.width + self.minimumInteritemSpacing)) + let interitemSpacing = (collectionView.bounds.width - maximumItemsPerRow * self.maximumBoxArtSize.width) / (maximumItemsPerRow + 1) + + self.sectionInset = UIEdgeInsets(top: interitemSpacing, left: interitemSpacing, bottom: interitemSpacing, right: interitemSpacing) + + let layoutAttributes = super.layoutAttributesForElementsInRect(rect)?.map({ $0.copy() }) as! [GameCollectionViewLayoutAttributes] + + var minimumY: CGFloat? = nil + var maximumY: CGFloat? = nil + var tempLayoutAttributes: [GameCollectionViewLayoutAttributes] = [] + + 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 + } + } + + self.configureLayoutAttributes(attributes) + + 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 + { + for tempAttributes in tempLayoutAttributes + { + tempAttributes.frame.origin.y = minY + } + + // Reset tempLayoutAttributes + tempLayoutAttributes.removeAll() + minimumY = nil + maximumY = nil + } + } + + if minimumY == nil || attributes.frame.minY < minimumY! + { + minimumY = attributes.frame.minY + } + + if maximumY == nil || attributes.frame.maxY > maximumY! + { + maximumY = attributes.frame.maxY + } + + tempLayoutAttributes.append(attributes) + } + + // Handle the remaining tempLayoutAttributes + if let minimumY = minimumY + { + for tempAttributes in tempLayoutAttributes + { + tempAttributes.frame.origin.y = minimumY + } + } + + return layoutAttributes + } + + // You'd think you could just do this in layoutAttributesForItemAtIndexPath, but alas layoutAttributesForElementsInRect does not call that method :( + private func configureLayoutAttributes(layoutAttributes: GameCollectionViewLayoutAttributes) + { + layoutAttributes.maximumBoxArtSize = self.maximumBoxArtSize + } +} diff --git a/Common/Collection View/GameCollectionViewLayoutAttributes.swift b/Common/Collection View/GameCollectionViewLayoutAttributes.swift new file mode 100644 index 0000000..c4b1d0f --- /dev/null +++ b/Common/Collection View/GameCollectionViewLayoutAttributes.swift @@ -0,0 +1,32 @@ +// +// GameCollectionViewLayoutAttributes.swift +// Delta +// +// Created by Riley Testut on 10/28/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class GameCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes +{ + var maximumBoxArtSize = CGSize(width: 100, height: 100) + + override func copyWithZone(zone: NSZone) -> AnyObject + { + let copy = super.copyWithZone(zone) as! GameCollectionViewLayoutAttributes + copy.maximumBoxArtSize = self.maximumBoxArtSize + + return copy + } + + override func isEqual(object: AnyObject?) -> Bool + { + guard super.isEqual(object) else { return false } + guard let attributes = object as? GameCollectionViewLayoutAttributes else { return false } + + guard CGSizeEqualToSize(self.maximumBoxArtSize, attributes.maximumBoxArtSize) else { return false } + + return true + } +} diff --git a/Common/Components/BoxArtImageView.swift b/Common/Components/BoxArtImageView.swift new file mode 100644 index 0000000..47c8cdd --- /dev/null +++ b/Common/Components/BoxArtImageView.swift @@ -0,0 +1,55 @@ +// +// BoxArtImageView.swift +// Delta +// +// Created by Riley Testut on 10/27/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +class BoxArtImageView: UIImageView +{ + override var image: UIImage? { + didSet + { + if image == nil + { + image = UIImage(named: "BoxArt") + } + + } + } + + init() + { + super.init(image: nil) + + self.initialize() + } + + override init(frame: CGRect) + { + super.init(frame: frame) + + self.image = nil + + self.initialize() + } + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + self.initialize() + } + + private func initialize() + { + #if os(tvOS) + self.adjustsImageWhenAncestorFocused = true + #endif + + self.contentMode = .ScaleAspectFit + } +} diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 3f5c00c..3187e31 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -14,17 +14,22 @@ BF27CC8D1BC9FE5300A20D89 /* Pods.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; }; BF27CC8F1BCA010200A20D89 /* GamePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB28441BC9DA7B001D0C83 /* GamePickerController.swift */; }; + BF27CC911BCB156200A20D89 /* EmulationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC901BCB156200A20D89 /* EmulationViewController.swift */; }; + BF27CC951BCB7B7A00A20D89 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF27CC941BCB7B7A00A20D89 /* GameController.framework */; }; + BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */; }; + BF27CC991BCC8AE600A20D89 /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC981BCC8AE600A20D89 /* GamesViewController.swift */; }; BF2A53FB1BB74FC10052BD0C /* SNESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; }; BF2A53FC1BB74FC10052BD0C /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF2A53FD1BB74FC60052BD0C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; }; BF2A53FE1BB74FC60052BD0C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF4566E81BC090B6007BFA1A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF4566E61BC090B6007BFA1A /* Model.xcdatamodeld */; }; BF4566E91BC090B6007BFA1A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF4566E61BC090B6007BFA1A /* Model.xcdatamodeld */; }; - BF46894F1AAC46EF00A2586D /* DirectoryContentsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF46894E1AAC46EF00A2586D /* DirectoryContentsDataSource.swift */; }; + BF50DC421BD851740024C720 /* GameCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF50DC411BD851740024C720 /* GameCollectionViewCell.swift */; }; + BF50DC431BD851740024C720 /* GameCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF50DC411BD851740024C720 /* GameCollectionViewCell.swift */; }; BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */; }; BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF5E7F451B9A652600AE44F8 /* Settings.storyboard */; }; BF6BB23F1BB73FE800CCF94A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BB23E1BB73FE800CCF94A /* AppDelegate.swift */; }; - BF6BB2411BB73FE800CCF94A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BB2401BB73FE800CCF94A /* ViewController.swift */; }; + BF6BB2411BB73FE800CCF94A /* GameSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BB2401BB73FE800CCF94A /* GameSelectionViewController.swift */; }; BF6BB2441BB73FE800CCF94A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2421BB73FE800CCF94A /* Main.storyboard */; }; BF6BB2461BB73FE800CCF94A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; }; BF70798C1B6B464B0019077C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; }; @@ -39,8 +44,12 @@ BF8624AA1BB7464B00C12EEE /* DeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF9F4FCF1AAD7B87004C9500 /* DeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; }; BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BFA5342A1BDC6B520088F1BE /* GameCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA534291BDC6B520088F1BE /* GameCollectionViewLayout.swift */; }; + BFA5342B1BDC6B520088F1BE /* GameCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA534291BDC6B520088F1BE /* GameCollectionViewLayout.swift */; }; BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; }; BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FF31B8AD7F900495943 /* ControllersSettingsViewController.swift */; }; + BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB141171BE46934004FBF46 /* GameCollectionViewDataSource.swift */; }; + BFB141191BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB141171BE46934004FBF46 /* GameCollectionViewDataSource.swift */; }; BFC134E11AAD82460087AD7B /* SNESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; }; BFC134E21AAD82470087AD7B /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BFDB28451BC9DA7B001D0C83 /* GamePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB28441BC9DA7B001D0C83 /* GamePickerController.swift */; }; @@ -50,12 +59,14 @@ BFDE393D1BC0CEDF003F72E8 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDE39391BC0CEDF003F72E8 /* Game.swift */; }; BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; }; BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BFF1E5641BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF1E5631BE04CAF000E9EF6 /* BoxArtImageView.swift */; }; + BFF1E5651BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF1E5631BE04CAF000E9EF6 /* BoxArtImageView.swift */; }; + BFF4EA011BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF4EA001BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift */; }; + BFF4EA021BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF4EA001BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift */; }; BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; }; - BFFA71DF1AAC406100EE9DD1 /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DE1AAC406100EE9DD1 /* GamesViewController.swift */; }; BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; }; BFFA71E71AAC406100EE9DD1 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E51AAC406100EE9DD1 /* LaunchScreen.xib */; }; BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFB709E1AF99B1700DE56FE /* EmulationViewController.swift */; }; - BFFB70A11AF99DFB00DE56FE /* EmulationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFFB70A01AF99DFB00DE56FE /* EmulationViewController.xib */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -99,13 +110,17 @@ BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+Vibration.m"; sourceTree = ""; }; BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = ""; }; BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = ""; }; + BF27CC901BCB156200A20D89 /* EmulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulationViewController.swift; sourceTree = ""; }; + BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; }; + BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesCollectionViewController.swift; sourceTree = ""; }; + BF27CC981BCC8AE600A20D89 /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = ""; }; BF4566E71BC090B6007BFA1A /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; - BF46894E1AAC46EF00A2586D /* DirectoryContentsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryContentsDataSource.swift; sourceTree = ""; }; + BF50DC411BD851740024C720 /* GameCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameCollectionViewCell.swift; path = "Collection View/GameCollectionViewCell.swift"; sourceTree = ""; }; BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; BF6BB23C1BB73FE800CCF94A /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; }; BF6BB23E1BB73FE800CCF94A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - BF6BB2401BB73FE800CCF94A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + BF6BB2401BB73FE800CCF94A /* GameSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameSelectionViewController.swift; sourceTree = ""; }; BF6BB2431BB73FE800CCF94A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BF6BB2471BB73FE800CCF94A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -113,21 +128,23 @@ 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 = ""; }; 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 = ""; }; BFAA1FF31B8AD7F900495943 /* ControllersSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllersSettingsViewController.swift; sourceTree = ""; }; + BFB141171BE46934004FBF46 /* GameCollectionViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameCollectionViewDataSource.swift; path = "Collection View/GameCollectionViewDataSource.swift"; sourceTree = ""; }; BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SNESDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFDB28441BC9DA7B001D0C83 /* GamePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamePickerController.swift; sourceTree = ""; }; BFDE39381BC0CEDF003F72E8 /* Game+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Game+CoreDataProperties.swift"; sourceTree = ""; }; BFDE39391BC0CEDF003F72E8 /* Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = ""; }; BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BFF1E5631BE04CAF000E9EF6 /* BoxArtImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BoxArtImageView.swift; path = Components/BoxArtImageView.swift; sourceTree = ""; }; + BFF4EA001BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameCollectionViewLayoutAttributes.swift; path = "Collection View/GameCollectionViewLayoutAttributes.swift"; sourceTree = ""; }; BFFA71D71AAC406100EE9DD1 /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - BFFA71DE1AAC406100EE9DD1 /* GamesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = ""; }; BFFA71E11AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BFFA71E61AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; BFFB709E1AF99B1700DE56FE /* EmulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulationViewController.swift; sourceTree = ""; }; - BFFB70A01AF99DFB00DE56FE /* EmulationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EmulationViewController.xib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -137,6 +154,7 @@ files = ( BF2A53FB1BB74FC10052BD0C /* SNESDeltaCore.framework in Frameworks */, BF8624A91BB7464B00C12EEE /* DeltaCore.framework in Frameworks */, + BF27CC951BCB7B7A00A20D89 /* GameController.framework in Frameworks */, BF2A53FD1BB74FC60052BD0C /* ZipZap.framework in Frameworks */, BF8624881BB743FE00C12EEE /* Roxas.framework in Frameworks */, AF0535CD7331785FA15E0864 /* Pods.framework in Frameworks */, @@ -180,6 +198,8 @@ BF4566E31BC09026007BFA1A /* Common */ = { isa = PBXGroup; children = ( + BFF1E55F1BE04BF6000E9EF6 /* Components */, + BF9257571BD8244800B109DA /* Collection View */, BF4566E41BC0902E007BFA1A /* Database */, BFDB28431BC9D9D1001D0C83 /* Importing */, BF762EA91BC1B044002C8866 /* Extensions */, @@ -209,8 +229,8 @@ BF46894D1AAC469800A2586D /* Game Selection */ = { isa = PBXGroup; children = ( - BF46894E1AAC46EF00A2586D /* DirectoryContentsDataSource.swift */, - BFFA71DE1AAC406100EE9DD1 /* GamesViewController.swift */, + BF27CC981BCC8AE600A20D89 /* GamesViewController.swift */, + BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */, ); path = "Game Selection"; sourceTree = ""; @@ -220,7 +240,8 @@ children = ( BF6BB23E1BB73FE800CCF94A /* AppDelegate.swift */, BF6BB2421BB73FE800CCF94A /* Main.storyboard */, - BF6BB2401BB73FE800CCF94A /* ViewController.swift */, + BF6BB2401BB73FE800CCF94A /* GameSelectionViewController.swift */, + BF27CC901BCB156200A20D89 /* EmulationViewController.swift */, BF8624621BB7400E00C12EEE /* Supporting Files */, ); path = DeltaTV; @@ -242,9 +263,21 @@ path = "Supporting Files"; sourceTree = ""; }; + BF9257571BD8244800B109DA /* Collection View */ = { + isa = PBXGroup; + children = ( + BF50DC411BD851740024C720 /* GameCollectionViewCell.swift */, + BFA534291BDC6B520088F1BE /* GameCollectionViewLayout.swift */, + BFF4EA001BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift */, + BFB141171BE46934004FBF46 /* GameCollectionViewDataSource.swift */, + ); + name = "Collection View"; + sourceTree = ""; + }; BF9F4FCD1AAD7B25004C9500 /* Frameworks */ = { isa = PBXGroup; children = ( + BF27CC941BCB7B7A00A20D89 /* GameController.framework */, BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */, BF70798B1B6B464B0019077C /* ZipZap.framework */, BFEC732C1AAECC4A00650035 /* Roxas.framework */, @@ -282,6 +315,14 @@ path = Resources; sourceTree = ""; }; + BFF1E55F1BE04BF6000E9EF6 /* Components */ = { + isa = PBXGroup; + children = ( + BFF1E5631BE04CAF000E9EF6 /* BoxArtImageView.swift */, + ); + name = Components; + sourceTree = ""; + }; BFFA71CE1AAC406100EE9DD1 = { isa = PBXGroup; children = ( @@ -333,7 +374,6 @@ isa = PBXGroup; children = ( BFFB709E1AF99B1700DE56FE /* EmulationViewController.swift */, - BFFB70A01AF99DFB00DE56FE /* EmulationViewController.xib */, ); path = Emulation; sourceTree = ""; @@ -396,6 +436,11 @@ BF6BB23B1BB73FE800CCF94A = { CreatedOnToolsVersion = 7.1; DevelopmentTeam = 6XVY5G3U44; + SystemCapabilities = { + com.apple.GameControllers.appletvos = { + enabled = 1; + }; + }; }; BFFA71D61AAC406100EE9DD1 = { CreatedOnToolsVersion = 6.3; @@ -442,7 +487,6 @@ buildActionMask = 2147483647; files = ( BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */, - BFFB70A11AF99DFB00DE56FE /* EmulationViewController.xib in Resources */, BFFA71E71AAC406100EE9DD1 /* LaunchScreen.xib in Resources */, BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */, BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */, @@ -549,11 +593,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BF6BB2411BB73FE800CCF94A /* ViewController.swift in Sources */, + BFF4EA021BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift in Sources */, + BF6BB2411BB73FE800CCF94A /* GameSelectionViewController.swift in Sources */, BF27CC8F1BCA010200A20D89 /* GamePickerController.swift in Sources */, BFDE393D1BC0CEDF003F72E8 /* Game.swift in Sources */, BF762E9F1BC19D31002C8866 /* DatabaseManager.swift in Sources */, BFDE393B1BC0CEDF003F72E8 /* Game+CoreDataProperties.swift in Sources */, + BF27CC911BCB156200A20D89 /* EmulationViewController.swift in Sources */, + BFB141191BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, + BFF1E5651BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */, + BF50DC431BD851740024C720 /* GameCollectionViewCell.swift in Sources */, + BFA5342B1BDC6B520088F1BE /* GameCollectionViewLayout.swift in Sources */, BF6BB23F1BB73FE800CCF94A /* AppDelegate.swift in Sources */, BF4566E91BC090B6007BFA1A /* Model.xcdatamodeld in Sources */, BF762EAC1BC1B076002C8866 /* NSManagedObject+Conveniences.swift in Sources */, @@ -564,15 +614,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BF27CC991BCC8AE600A20D89 /* GamesViewController.swift in Sources */, + BFF4EA011BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift in Sources */, BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, - BFFA71DF1AAC406100EE9DD1 /* GamesViewController.swift in Sources */, + BF50DC421BD851740024C720 /* GameCollectionViewCell.swift in Sources */, + BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */, BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */, + BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */, BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */, BF4566E81BC090B6007BFA1A /* Model.xcdatamodeld in Sources */, BFDE393C1BC0CEDF003F72E8 /* Game.swift in Sources */, - BF46894F1AAC46EF00A2586D /* DirectoryContentsDataSource.swift in Sources */, + BFF1E5641BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */, BF762EAB1BC1B076002C8866 /* NSManagedObject+Conveniences.swift in Sources */, + BFA5342A1BDC6B520088F1BE /* GameCollectionViewLayout.swift in Sources */, BF762E9E1BC19D31002C8866 /* DatabaseManager.swift in Sources */, BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, @@ -638,7 +693,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution"; INFOPLIST_FILE = "DeltaTV/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PROVISIONING_PROFILE = ""; diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index 0cb735c..43b9065 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -1,7 +1,7 @@ - + - + @@ -66,6 +66,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -74,6 +234,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Emulation/EmulationViewController.swift b/Delta/Emulation/EmulationViewController.swift index e51f0b5..bd8e303 100644 --- a/Delta/Emulation/EmulationViewController.swift +++ b/Delta/Emulation/EmulationViewController.swift @@ -9,13 +9,19 @@ import UIKit import DeltaCore +import SNESDeltaCore class EmulationViewController: UIViewController { //MARK: - Properties - /** Properties **/ - let game: Game - let emulatorCore: EmulatorCore + var game: Game! { + didSet + { + self.emulatorCore = SNESEmulatorCore(game: game) + } + } + private(set) var emulatorCore: EmulatorCore! //MARK: - Private Properties @IBOutlet private var controllerView: ControllerView! @@ -25,21 +31,14 @@ class EmulationViewController: UIViewController //MARK: - Initializers - /** Initializers **/ - required init(game: Game) + required init?(coder aDecoder: NSCoder) { - self.game = game - self.emulatorCore = EmulatorCore(game: game) - - super.init(nibName: "EmulationViewController", bundle: nil) + super.init(coder: aDecoder) NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("updateControllers"), name: ExternalControllerDidConnectNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("updateControllers"), name: ExternalControllerDidDisconnectNotification, object: nil) } - required init(coder aDecoder: NSCoder) { - fatalError("initWithCoder: not implemented.") - } - //MARK: - Overrides /** Overrides **/ diff --git a/Delta/Emulation/EmulationViewController.xib b/Delta/Emulation/EmulationViewController.xib deleted file mode 100644 index b191768..0000000 --- a/Delta/Emulation/EmulationViewController.xib +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Delta/Game Selection/DirectoryContentsDataSource.swift b/Delta/Game Selection/DirectoryContentsDataSource.swift deleted file mode 100644 index ad46f72..0000000 --- a/Delta/Game Selection/DirectoryContentsDataSource.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// DirectoryContentsDataSource.swift -// Delta -// -// Created by Riley Testut on 3/8/15. -// Copyright (c) 2015 Riley Testut. All rights reserved. -// - -import UIKit -import Foundation - -public extension DirectoryContentsDataSource -{ - func URLAtIndexPath(indexPath: NSIndexPath) -> NSURL - { - let URL = self.directoryContents[indexPath.row] - return URL - } -} - -public class DirectoryContentsDataSource: NSObject -{ - public let directoryURL: NSURL - public var tableViewCellIdentifier: String = "Cell" - - public var contentsUpdateHandler: (Void -> Void)? - public var cellConfigurationBlock: ((UITableViewCell, NSIndexPath, NSURL) -> Void)? - - private let fileDescriptor: Int32 - private let directoryMonitorDispatchQueue: dispatch_queue_t - private let directoryMonitorDispatchSource: dispatch_source_t! - - private var directoryContents: [NSURL] - - required public init?(directoryURL: NSURL) - { - self.directoryURL = directoryURL - self.fileDescriptor = open(self.directoryURL.fileSystemRepresentation, O_EVTONLY) - - self.directoryMonitorDispatchQueue = dispatch_queue_create("com.rileytestut.DirectoryContentsDataSource", DISPATCH_QUEUE_SERIAL) - self.directoryMonitorDispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, UInt(self.fileDescriptor), DISPATCH_VNODE_WRITE, self.directoryMonitorDispatchQueue) - - self.directoryContents = [NSURL]() - - super.init() - - if self.fileDescriptor < 0 - { - return nil - } - - if self.directoryMonitorDispatchSource == nil - { - close(self.fileDescriptor); - - return nil - } - - dispatch_source_set_event_handler(self.directoryMonitorDispatchSource, { - self.didUpdateDirectoryContents() - }); - - dispatch_source_set_cancel_handler(self.directoryMonitorDispatchSource, { - close(self.fileDescriptor); - }); - - dispatch_resume(self.directoryMonitorDispatchSource); - - self.didUpdateDirectoryContents() - } - - deinit - { - if self.fileDescriptor >= 0 - { - close(self.fileDescriptor); - } - - if self.directoryMonitorDispatchSource != nil - { - dispatch_source_cancel(self.directoryMonitorDispatchSource); - } - } -} - -private extension DirectoryContentsDataSource -{ - func didUpdateDirectoryContents() - { - do - { - self.directoryContents = try NSFileManager.defaultManager().contentsOfDirectoryAtURL(self.directoryURL, includingPropertiesForKeys: nil, options:[NSDirectoryEnumerationOptions.SkipsSubdirectoryDescendants, NSDirectoryEnumerationOptions.SkipsHiddenFiles]) - } - catch let error as NSError - { - print("\(error) \(error.userInfo)") - } - - self.contentsUpdateHandler?() - } -} - -extension DirectoryContentsDataSource: UITableViewDataSource -{ - public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int - { - return self.directoryContents.count - } - - public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell - { - let tableViewCell = tableView.dequeueReusableCellWithIdentifier(self.tableViewCellIdentifier, forIndexPath: indexPath) as UITableViewCell - - self.cellConfigurationBlock?(tableViewCell, indexPath, self.URLAtIndexPath(indexPath)) - - return tableViewCell - } -} \ No newline at end of file diff --git a/Delta/Game Selection/GamesCollectionViewController.swift b/Delta/Game Selection/GamesCollectionViewController.swift new file mode 100644 index 0000000..ca620a6 --- /dev/null +++ b/Delta/Game Selection/GamesCollectionViewController.swift @@ -0,0 +1,86 @@ +// +// GamesCollectionViewController.swift +// Delta +// +// Created by Riley Testut on 10/12/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit +import CoreData + +import DeltaCore + +class GamesCollectionViewController: UICollectionViewController +{ + var gameTypeIdentifier: String! { + didSet + { + self.dataSource.gameTypeIdentifiers = [self.gameTypeIdentifier] + } + } + + private let dataSource = GameCollectionViewDataSource() + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + self.dataSource.fetchedResultsController.delegate = self + self.dataSource.cellConfigurationHandler = self.configureCell + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.collectionView?.dataSource = self.dataSource + self.collectionView?.delegate = self.dataSource + + if let layout = self.collectionViewLayout as? GameCollectionViewLayout + { + layout.maximumBoxArtSize = CGSize(width: 100, height: 100) + } + } + + override func viewWillAppear(animated: Bool) + { + self.dataSource.update() + + super.viewWillAppear(animated) + } + + override func didReceiveMemoryWarning() + { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + // 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?) + { + guard let viewController = segue.destinationViewController as? EmulationViewController else { return } + + let indexPath = self.collectionView?.indexPathsForSelectedItems()?.first + let game = self.dataSource.fetchedResultsController.objectAtIndexPath(indexPath!) as! Game + + viewController.game = game + } + + // MARK: - Collection View - + + private func configureCell(cell: GameCollectionViewCell, game: Game) + { + cell.nameLabel.text = game.name + } +} + +extension GamesCollectionViewController: NSFetchedResultsControllerDelegate +{ + func controllerDidChangeContent(controller: NSFetchedResultsController) + { + self.collectionView?.reloadData() + } +} diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index 7fbcd70..bf89e8c 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -2,88 +2,49 @@ // GamesViewController.swift // Delta // -// Created by Riley Testut on 3/8/15. -// Copyright (c) 2015 Riley Testut. All rights reserved. +// Created by Riley Testut on 10/12/15. +// Copyright © 2015 Riley Testut. All rights reserved. // import UIKit -import DeltaCore -class GamesViewController: UITableViewController +import SNESDeltaCore + +class GamesViewController: UIViewController { - let directoryContentsDataSource: DirectoryContentsDataSource? + var pageViewController: UIPageViewController! = nil - override init(style: UITableViewStyle) - { - if let documentsDirectoryURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first - { - self.directoryContentsDataSource = DirectoryContentsDataSource(directoryURL: documentsDirectoryURL) - } - else - { - self.directoryContentsDataSource = nil - } - - super.init(style: style) - } + let supportedGameTypeIdentifiers = [kUTTypeSNESGame as String] - required init?(coder aDecoder: NSCoder) - { - if let documentsDirectoryURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first - { - self.directoryContentsDataSource = DirectoryContentsDataSource(directoryURL: documentsDirectoryURL) - } - else - { - self.directoryContentsDataSource = nil - } - - super.init(coder: aDecoder) - } - override func viewDidLoad() { super.viewDidLoad() - self.tableView.dataSource = self.directoryContentsDataSource + self.automaticallyAdjustsScrollViewInsets = false - self.directoryContentsDataSource!.contentsUpdateHandler = { - dispatch_async(dispatch_get_main_queue(), { - self.tableView.reloadData() - }) - } + self.pageViewController = self.childViewControllers.first as? UIPageViewController + self.pageViewController.dataSource = self + self.pageViewController.delegate = self - self.directoryContentsDataSource?.cellConfigurationBlock = { (cell, indexPath, URL) in - cell.textLabel?.text = URL.lastPathComponent - } + let viewController = self.viewControllerForIndex(0) + self.pageViewController.setViewControllers([viewController], direction: .Forward, animated: false, completion: nil) + + // Do any additional setup after loading the view. } - override func viewDidAppear(animated: Bool) + override func viewDidLayoutSubviews() { - super.viewDidAppear(animated) + super.viewDidLayoutSubviews() - if self.directoryContentsDataSource == nil - { - let alertController = UIAlertController(title: NSLocalizedString("Invalid Games Directory", comment: ""), message: NSLocalizedString("Please ensure the current games directory exists, then restart Delta.", comment: ""), preferredStyle: .Alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: UIAlertActionStyle.Cancel, handler: nil)) - - self.presentViewController(alertController, animated: true, completion: nil) - } + let viewController = self.pageViewController.viewControllers?.first as! GamesCollectionViewController + viewController.collectionView?.contentInset.top = self.topLayoutGuide.length } - override func didReceiveMemoryWarning() - { + override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } - //MARK: - Settings - - - @IBAction func dismissSettingsViewController(segue: UIStoryboardSegue) - { - - } - // MARK: - Importing - @IBAction func importFiles() @@ -92,13 +53,23 @@ class GamesViewController: UITableViewController gamePickerController.delegate = self self.presentGamePickerController(gamePickerController, animated: true, completion: nil) } - - - //MARK: UITableViewDelegate - - override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) +} + +private extension GamesViewController +{ + func viewControllerForIndex(index: Int) -> GamesCollectionViewController { + var safeIndex = index % self.supportedGameTypeIdentifiers.count + if safeIndex < 0 + { + safeIndex = self.supportedGameTypeIdentifiers.count + safeIndex + } + + let viewController = self.storyboard?.instantiateViewControllerWithIdentifier("gamesCollectionViewController") as! GamesCollectionViewController + viewController.gameTypeIdentifier = self.supportedGameTypeIdentifiers[safeIndex] as String + viewController.collectionView?.contentInset.top = self.topLayoutGuide.length + return viewController } } @@ -106,7 +77,24 @@ extension GamesViewController: GamePickerControllerDelegate { func gamePickerController(gamePickerController: GamePickerController, didImportGames games: [Game]) { + DatabaseManager.sharedManager.save() print(games) } } +extension GamesViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource +{ + func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? + { + let index = self.supportedGameTypeIdentifiers.indexOf((viewController as! GamesCollectionViewController).gameTypeIdentifier) + let viewController = self.viewControllerForIndex(index! - 1) + return viewController + } + + func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? + { + let index = self.supportedGameTypeIdentifiers.indexOf((viewController as! GamesCollectionViewController).gameTypeIdentifier) + let viewController = self.viewControllerForIndex(index! + 1) + return viewController + } +} diff --git a/DeltaTV/Base.lproj/Main.storyboard b/DeltaTV/Base.lproj/Main.storyboard index 6d2b8e4..f734464 100644 --- a/DeltaTV/Base.lproj/Main.storyboard +++ b/DeltaTV/Base.lproj/Main.storyboard @@ -1,25 +1,109 @@ - + - + - - + + - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DeltaTV/EmulationViewController.swift b/DeltaTV/EmulationViewController.swift new file mode 100644 index 0000000..f709124 --- /dev/null +++ b/DeltaTV/EmulationViewController.swift @@ -0,0 +1,59 @@ +// +// EmulationViewController.swift +// Delta +// +// Created by Riley Testut on 10/11/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore +import SNESDeltaCore + +class EmulationViewController: UIViewController +{ + //MARK: - Properties - + /** Properties **/ + var game: Game! { + didSet + { + self.emulatorCore = SNESEmulatorCore(game: game) + } + } + private(set) var emulatorCore: EmulatorCore! + + //MARK: - Private Properties + @IBOutlet private var gameView: GameView! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.emulatorCore.addGameView(self.gameView) + } + + override func viewDidAppear(animated: Bool) + { + super.viewDidAppear(animated) + + self.emulatorCore.startEmulation() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // 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?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/DeltaTV/GameSelectionViewController.swift b/DeltaTV/GameSelectionViewController.swift new file mode 100644 index 0000000..9d0fb0b --- /dev/null +++ b/DeltaTV/GameSelectionViewController.swift @@ -0,0 +1,103 @@ +// +// ViewController.swift +// DeltaTV +// +// Created by Riley Testut on 9/26/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit +import CoreData + +import SNESDeltaCore + +class GameSelectionViewController: UICollectionViewController +{ + private let dataSource = GameCollectionViewDataSource() + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + self.title = NSLocalizedString("Games", comment: "") + + self.dataSource.gameTypeIdentifiers = [kUTTypeSNESGame as String] + self.dataSource.fetchedResultsController.delegate = self + self.dataSource.cellConfigurationHandler = self.configureCell + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.collectionView?.dataSource = self.dataSource + self.collectionView?.delegate = self.dataSource + + if let layout = self.collectionViewLayout as? GameCollectionViewLayout + { + layout.maximumBoxArtSize = CGSize(width: 200, height: 200) + } + } + + override func viewWillAppear(animated: Bool) + { + self.dataSource.update() + + super.viewWillAppear(animated) + } + + override func didReceiveMemoryWarning() + { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + // MARK: - Importing - + + @IBAction func importFiles() + { + let gamePickerController = GamePickerController() + gamePickerController.delegate = self + self.presentGamePickerController(gamePickerController, animated: true, completion: nil) + } + + + // MARK: - Navigation - + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) + { + guard let viewController = segue.destinationViewController as? EmulationViewController else { return } + + let indexPath = self.collectionView?.indexPathsForSelectedItems()?.first + let game = self.dataSource.fetchedResultsController.objectAtIndexPath(indexPath!) as! Game + + viewController.game = game + } + + // MARK: - Collection View - + + private func configureCell(cell: GameCollectionViewCell, game: Game) + { + cell.nameLabel.font = UIFont.boldSystemFontOfSize(30) + cell.nameLabel.text = game.name + } +} + +// MARK: - - +extension GameSelectionViewController: GamePickerControllerDelegate +{ + func gamePickerController(gamePickerController: GamePickerController, didImportGames games: [Game]) + { + DatabaseManager.sharedManager.save() + print(games) + } +} + +extension GameSelectionViewController: NSFetchedResultsControllerDelegate +{ + func controllerDidChangeContent(controller: NSFetchedResultsController) + { + self.collectionView?.reloadData() + } +} + diff --git a/DeltaTV/ViewController.swift b/DeltaTV/ViewController.swift deleted file mode 100644 index 5bfb008..0000000 --- a/DeltaTV/ViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ViewController.swift -// DeltaTV -// -// Created by Riley Testut on 9/26/15. -// Copyright © 2015 Riley Testut. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - -} - diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj index 75deaf7..633224a 100644 --- a/Pods/Pods.xcodeproj/project.pbxproj +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -285,6 +285,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = DA312349A49333542E6F4B36B329960E /* Pods.release.xcconfig */; buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -300,7 +301,7 @@ OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_NAME = Pods; - SDKROOT = appletvos; + SDKROOT = iphoneos; SKIP_INSTALL = YES; TVOS_DEPLOYMENT_TARGET = 9.0; VERSIONING_SYSTEM = "apple-generic"; @@ -312,6 +313,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 087F6379BD69587FB559DDED4732D982 /* FileMD5Hash.xcconfig */; buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -324,7 +326,7 @@ MODULEMAP_FILE = "Target Support Files/FileMD5Hash/FileMD5Hash.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = FileMD5Hash; - SDKROOT = appletvos; + SDKROOT = iphoneos; SKIP_INSTALL = YES; TVOS_DEPLOYMENT_TARGET = 9.0; VERSIONING_SYSTEM = "apple-generic"; @@ -336,6 +338,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 087F6379BD69587FB559DDED4732D982 /* FileMD5Hash.xcconfig */; buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -348,7 +351,7 @@ MODULEMAP_FILE = "Target Support Files/FileMD5Hash/FileMD5Hash.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_NAME = FileMD5Hash; - SDKROOT = appletvos; + SDKROOT = iphoneos; SKIP_INSTALL = YES; TVOS_DEPLOYMENT_TARGET = 9.0; VERSIONING_SYSTEM = "apple-generic"; @@ -399,6 +402,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 977577C045EDA9D9D1F46E2598D19FC7 /* Pods.debug.xcconfig */; buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -414,7 +418,7 @@ OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_NAME = Pods; - SDKROOT = appletvos; + SDKROOT = iphoneos; SKIP_INSTALL = YES; TVOS_DEPLOYMENT_TARGET = 9.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/Resources/Assets.xcassets/BoxArt.imageset/BoxArt-1.png b/Resources/Assets.xcassets/BoxArt.imageset/BoxArt-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5060eedde404cbfa3db3611a071389ae976f39a3 GIT binary patch literal 10411 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 KjB4U}=YIj3Ygz&T literal 0 HcmV?d00001 diff --git a/Resources/Assets.xcassets/BoxArt.imageset/BoxArt.png b/Resources/Assets.xcassets/BoxArt.imageset/BoxArt.png new file mode 100644 index 0000000000000000000000000000000000000000..2f8d3b7870de155cff805a924f66ee9ff93189df GIT binary patch literal 3052 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 literal 0 HcmV?d00001 diff --git a/Resources/Assets.xcassets/BoxArt.imageset/BoxArt@3x.png b/Resources/Assets.xcassets/BoxArt.imageset/BoxArt@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5060eedde404cbfa3db3611a071389ae976f39a3 GIT binary patch literal 10411 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 KjB4U}=YIj3Ygz&T literal 0 HcmV?d00001 diff --git a/Resources/Assets.xcassets/BoxArt.imageset/Contents.json b/Resources/Assets.xcassets/BoxArt.imageset/Contents.json new file mode 100644 index 0000000..ec0a894 --- /dev/null +++ b/Resources/Assets.xcassets/BoxArt.imageset/Contents.json @@ -0,0 +1,28 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "BoxArt.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BoxArt@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "BoxArt@3x.png", + "scale" : "3x" + }, + { + "idiom" : "tv", + "filename" : "BoxArt-1.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file