diff --git a/Cores/DeltaCore b/Cores/DeltaCore index e2b3f0e..2a6779e 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit e2b3f0e46b4c64670e13fd0466ebdac719f84555 +Subproject commit 2a6779e1271bc5d2e09aea2aa41fa6a0b75b62aa diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index fe093e7..a96f841 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ D524F4A1273DE9A100D500B2 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = D524F4A0273DE9A100D500B2 /* AltKit */; }; D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; }; D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; }; + D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -363,6 +364,7 @@ C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = ""; }; D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = ""; }; D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = ""; }; + D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = ""; }; DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -411,6 +413,7 @@ BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */, D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */, D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */, + D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */, ); path = Extensions; sourceTree = ""; @@ -1126,6 +1129,7 @@ BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */, BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */, BF59427E1E09BC830051894B /* Game.swift in Sources */, + D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */, BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */, BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */, BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, @@ -1453,6 +1457,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1482,6 +1487,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index f5f1dc9..b69123d 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -46,6 +46,9 @@ + + + diff --git a/Delta/Base.lproj/Settings.storyboard b/Delta/Base.lproj/Settings.storyboard index 2a2a5d6..77eb2ba 100644 --- a/Delta/Base.lproj/Settings.storyboard +++ b/Delta/Base.lproj/Settings.storyboard @@ -1048,6 +1048,7 @@ + diff --git a/Delta/Deep Linking/CopyDeepLinkActivity.swift b/Delta/Deep Linking/CopyDeepLinkActivity.swift index 3dfc956..7e54bc7 100644 --- a/Delta/Deep Linking/CopyDeepLinkActivity.swift +++ b/Delta/Deep Linking/CopyDeepLinkActivity.swift @@ -15,6 +15,8 @@ extension UIActivity.ActivityType class CopyDeepLinkActivity: UIActivity { + private var deepLink: URL? + override class var activityCategory: UIActivity.Category { return .action } @@ -28,7 +30,7 @@ class CopyDeepLinkActivity: UIActivity } override var activityImage: UIImage? { - return UIImage(named: "Link") + return UIImage(symbolNameIfAvailable: "link") ?? UIImage(named: "Link") } override func canPerform(withActivityItems activityItems: [Any]) -> Bool @@ -47,7 +49,19 @@ class CopyDeepLinkActivity: UIActivity { guard let game = activityItems.first(where: { $0 is Game }) as? Game else { return } - let deepLink = URL(action: .launchGame(identifier: game.identifier)) - UIPasteboard.general.url = deepLink + self.deepLink = URL(action: .launchGame(identifier: game.identifier)) + } + + override func perform() + { + if let deepLink = self.deepLink + { + UIPasteboard.general.url = deepLink + self.activityDidFinish(true) + } + else + { + self.activityDidFinish(false) + } } } diff --git a/Delta/Extensions/CharacterSet+Filename.swift b/Delta/Extensions/CharacterSet+Filename.swift new file mode 100644 index 0000000..a1119c3 --- /dev/null +++ b/Delta/Extensions/CharacterSet+Filename.swift @@ -0,0 +1,22 @@ +// +// CharacterSet+Filename.swift +// Delta +// +// Created by Riley Testut on 4/28/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import Foundation + +extension CharacterSet +{ + // Different than .urlPathAllowed + // Copied from https://stackoverflow.com/a/39443252 + static var urlFilenameAllowed: CharacterSet { + var illegalCharacters = CharacterSet(charactersIn: ":/") + illegalCharacters.formUnion(.newlines) + illegalCharacters.formUnion(.illegalCharacters) + illegalCharacters.formUnion(.controlCharacters) + return illegalCharacters.inverted + } +} diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index 51cfb10..026f333 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -66,6 +66,8 @@ class GameCollectionViewController: UICollectionViewController private weak var _previewTransitionViewController: PreviewGameViewController? private weak var _previewTransitionDestinationViewController: UIViewController? + private weak var _popoverSourceView: UIView? + private var _renameAction: UIAlertAction? private var _changingArtworkGame: Game? private var _importingSaveFileGame: Game? @@ -93,10 +95,6 @@ extension GameCollectionViewController self.collectionView?.prefetchDataSource = self.dataSource self.collectionView?.delegate = self - let layout = self.collectionViewLayout as! GridCollectionViewLayout - layout.itemWidth = 90 - layout.minimumInteritemSpacing = 12 - if #available(iOS 13, *) {} else { @@ -105,6 +103,8 @@ extension GameCollectionViewController let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:))) self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) } + + self.update() } override func viewWillDisappear(_ animated: Bool) @@ -131,6 +131,13 @@ extension GameCollectionViewController super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) + { + super.traitCollectionDidChange(previousTraitCollection) + + self.update() + } } //MARK: - Segues - @@ -224,6 +231,26 @@ extension GameCollectionViewController //MARK: - Private Methods - private extension GameCollectionViewController { + func update() + { + let layout = self.collectionViewLayout as! GridCollectionViewLayout + + switch self.traitCollection.horizontalSizeClass + { + case .regular: + layout.itemWidth = 150 + layout.minimumInteritemSpacing = 25 // 30 == only 3 games per line for iPad mini 6 in portrait + + case .unspecified, .compact: + layout.itemWidth = 90 + layout.minimumInteritemSpacing = 12 + + @unknown default: break + } + + self.collectionView.reloadData() + } + //MARK: - Data Source func prepareDataSource() { @@ -284,7 +311,19 @@ private extension GameCollectionViewController cell.imageView.image = #imageLiteral(resourceName: "BoxArt") - cell.maximumImageSize = CGSize(width: 90, height: 90) + if self.traitCollection.horizontalSizeClass == .regular + { + let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)! + cell.textLabel.font = UIFont(descriptor: fontDescriptor, size: 0) + } + else + { + cell.textLabel.font = UIFont.preferredFont(forTextStyle: .caption1) + } + + let layout = self.collectionViewLayout as! GridCollectionViewLayout + cell.maximumImageSize = CGSize(width: layout.itemWidth, height: layout.itemWidth) + cell.textLabel.text = game.name cell.textLabel.textColor = UIColor.gray cell.tintColor = cell.textLabel.textColor @@ -484,7 +523,9 @@ private extension GameCollectionViewController func delete(_ game: Game) { - let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game? All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""), message: nil, preferredStyle: .actionSheet) + let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game?", comment: ""), + message: NSLocalizedString("All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""), + preferredStyle: .alert) confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Game", comment: ""), style: .destructive, handler: { action in DatabaseManager.shared.performBackgroundTask { (context) in @@ -554,6 +595,7 @@ private extension GameCollectionViewController let importController = ImportController(documentTypes: [kUTTypeImage as String]) importController.delegate = self importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption] + importController.sourceView = self._popoverSourceView self.present(importController, animated: true, completion: nil) } @@ -663,26 +705,36 @@ private extension GameCollectionViewController func share(_ game: Game) { - let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - let symbolicURL = temporaryDirectory.appendingPathComponent(game.name + "." + game.fileURL.pathExtension) + let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + + let sanitizedName = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined() + let temporaryURL = temporaryDirectory.appendingPathComponent(sanitizedName + "." + game.fileURL.pathExtension, isDirectory: false) do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) - - // Create a symbolic link so we can control the file name used when sharing. + + // Make a temporary copy so we can control the filename used when sharing. // Otherwise, if we just passed in game.fileURL to UIActivityViewController, the file name would be the game's SHA1 hash. - try FileManager.default.createSymbolicLink(at: symbolicURL, withDestinationURL: game.fileURL) + try FileManager.default.copyItem(at: game.fileURL, to: temporaryURL, shouldReplace: true) } catch { - print(error) + let alertController = UIAlertController(title: NSLocalizedString("Could Not Share Game", comment: ""), error: error) + self.present(alertController, animated: true, completion: nil) + + return } let copyDeepLinkActivity = CopyDeepLinkActivity() - let activityViewController = UIActivityViewController(activityItems: [symbolicURL, game], applicationActivities: [copyDeepLinkActivity]) + let activityViewController = UIActivityViewController(activityItems: [temporaryURL, game], applicationActivities: [copyDeepLinkActivity]) + activityViewController.popoverPresentationController?.sourceView = self._popoverSourceView?.superview + activityViewController.popoverPresentationController?.sourceRect = self._popoverSourceView?.frame ?? .zero activityViewController.completionWithItemsHandler = { (activityType, finished, returnedItems, error) in + // Make sure the user either shared the game or cancelled before deleting temporaryDirectory. + guard finished || activityType == nil else { return } + do { try FileManager.default.removeItem(at: temporaryDirectory) @@ -692,6 +744,7 @@ private extension GameCollectionViewController print(error) } } + self.present(activityViewController, animated: true, completion: nil) } @@ -747,8 +800,7 @@ private extension GameCollectionViewController { do { - let illegalCharacterSet = CharacterSet(charactersIn: "\"\\/?<>:*|") - let sanitizedFilename = game.name.components(separatedBy: illegalCharacterSet).joined() + "." + game.gameSaveURL.pathExtension + let sanitizedFilename = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined() let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename) try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true) @@ -807,6 +859,9 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate previewingContext.sourceRect = layoutAttributes.frame + let cell = collectionView.cellForItem(at: indexPath) + self._popoverSourceView = cell + let game = self.dataSource.item(at: indexPath) let gameViewController = self.makePreviewGameViewController(for: game) @@ -974,6 +1029,9 @@ extension GameCollectionViewController let game = self.dataSource.item(at: indexPath) let actions = self.actions(for: game) + let cell = self.collectionView.cellForItem(at: indexPath) + self._popoverSourceView = cell + return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in guard let self = self else { return nil } diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index 7b5180b..f28e975 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -47,6 +47,7 @@ class GamesViewController: UIViewController private let fetchedResultsController: NSFetchedResultsController private var searchController: RSTSearchController? + private lazy var importController: ImportController = self.makeImportController() private var syncingToastView: RSTToastView? { didSet { @@ -58,6 +59,8 @@ class GamesViewController: UIViewController } private var syncingProgressObservation: NSKeyValueObservation? + @IBOutlet private var importButton: UIBarButtonItem! + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { fatalError("initWithNibName: not implemented") } @@ -114,10 +117,16 @@ extension GamesViewController let navigationBarAppearance = navigationController.navigationBar.standardAppearance.copy() navigationBarAppearance.backgroundEffect = UIBlurEffect(style: .dark) navigationController.navigationBar.standardAppearance = navigationBarAppearance + navigationController.navigationBar.scrollEdgeAppearance = navigationBarAppearance let toolbarAppearance = navigationController.toolbar.standardAppearance.copy() toolbarAppearance.backgroundEffect = UIBlurEffect(style: .dark) - navigationController.toolbar.standardAppearance = toolbarAppearance + navigationController.toolbar.standardAppearance = toolbarAppearance + + if #available(iOS 15, *) + { + navigationController.toolbar.scrollEdgeAppearance = toolbarAppearance + } } else { @@ -126,6 +135,22 @@ extension GamesViewController } } + if #available(iOS 14, *) + { + self.importController.presentingViewController = self + + let importActions = self.importController.makeActions().menuActions + let importMenu = UIMenu(title: NSLocalizedString("Import From…", comment: ""), image: UIImage(systemName: "square.and.arrow.down"), children: importActions) + self.importButton.menu = importMenu + + self.importButton.action = nil + self.importButton.target = nil + } + else + { + self.importController.barButtonItem = self.importButton + } + self.prepareSearchController() self.updateTheme() @@ -352,8 +377,8 @@ private extension GamesViewController /// Importing extension GamesViewController: ImportControllerDelegate { - @IBAction private func importFiles() - { + private func makeImportController() -> ImportController + { var documentTypes = Set(System.registeredSystems.map { $0.gameType.rawValue }) documentTypes.insert(kUTTypeZipArchive as String) documentTypes.insert("com.rileytestut.delta.skin") @@ -373,7 +398,13 @@ extension GamesViewController: ImportControllerDelegate let importController = ImportController(documentTypes: documentTypes) importController.delegate = self importController.importOptions = [itunesImportOption] - self.present(importController, animated: true, completion: nil) + + return importController + } + + @IBAction private func importFiles() + { + self.present(self.importController, animated: true, completion: nil) } func importController(_ importController: ImportController, didImportItemsAt urls: Set, errors: [Error]) diff --git a/Delta/Importing/Import Options/iTunesImportOption.swift b/Delta/Importing/Import Options/iTunesImportOption.swift index 6b9390f..1272156 100644 --- a/Delta/Importing/Import Options/iTunesImportOption.swift +++ b/Delta/Importing/Import Options/iTunesImportOption.swift @@ -13,7 +13,7 @@ import DeltaCore struct iTunesImportOption: ImportOption { let title = NSLocalizedString("iTunes", comment: "") - let image: UIImage? = nil + let image: UIImage? = UIImage(symbolNameIfAvailable: "music.note") private let presentingViewController: UIViewController diff --git a/Delta/Importing/ImportController.swift b/Delta/Importing/ImportController.swift index e1cc5a9..7c40864 100644 --- a/Delta/Importing/ImportController.swift +++ b/Delta/Importing/ImportController.swift @@ -37,7 +37,10 @@ class ImportController: NSObject var delegate: ImportControllerDelegate? var importOptions: [ImportOption]? - private weak var presentingViewController: UIViewController? + weak var presentingViewController: UIViewController? + + weak var barButtonItem: UIBarButtonItem? + weak var sourceView: UIView? // Store presentedViewController separately, since when we dismiss we don't know if it has already been dismissed. // Calling dismiss on presentingViewController in that case would dismiss presentingViewController, which is bad. @@ -61,26 +64,54 @@ class ImportController: NSObject super.init() } + func makeActions() -> [Action] + { + assert(self.presentingViewController != nil, "presentingViewController must be set before calling makeActions()") + + var actions = (self.importOptions ?? []).map { (option) -> Action in + let action = Action(title: option.title, style: .default, image: option.image) { _ in + option.import { importedURLs in + self.finish(with: importedURLs, errors: []) + } + } + + return action + } + + let filesAction = Action(title: NSLocalizedString("Files", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "doc")) { action in + self.presentDocumentBrowser() + } + actions.append(filesAction) + + return actions + } + fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { self.presentingViewController = presentingViewController - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alertController.addAction(UIAlertAction.cancel) + let actions = self.makeActions() - if let importOptions = self.importOptions + if actions.count > 1 { - for importOption in importOptions + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction.cancel) + + let alertActions = actions.map { UIAlertAction($0) } + for action in alertActions { - alertController.add(importOption) { [unowned self] (urls) in - self.finish(with: urls, errors: []) - } + alertController.addAction(action) } - let filesAction = UIAlertAction(title: NSLocalizedString("Files", comment: ""), style: .default) { (action) in - self.presentDocumentBrowser() + if let sourceView = self.sourceView + { + alertController.popoverPresentationController?.sourceView = sourceView.superview + alertController.popoverPresentationController?.sourceRect = sourceView.frame + } + else + { + alertController.popoverPresentationController?.barButtonItem = self.barButtonItem } - alertController.addAction(filesAction) self.presentedViewController = alertController self.presentingViewController?.present(alertController, animated: true, completion: nil) @@ -198,7 +229,7 @@ private var ImportControllerKey: UInt8 = 0 extension UIViewController { - fileprivate(set) var importController: ImportController? + fileprivate var importController: ImportController? { set { diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index de77072..f146007 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -93,14 +93,6 @@ extension SaveStatesViewController self.collectionView?.dataSource = self.dataSource self.collectionView?.prefetchDataSource = self.dataSource - let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout - let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2 - let portraitScreenWidth = UIScreen.main.coordinateSpace.convert(UIScreen.main.bounds, to: UIScreen.main.fixedCoordinateSpace).width - - // Use dimensions that allow two cells to fill the screen horizontally with padding in portrait mode - // We'll keep the same size for landscape orientation, which will allow more to fit - collectionViewLayout.itemWidth = floor((portraitScreenWidth - (averageHorizontalInset * 3)) / 2) - switch self.mode { case .saving: @@ -113,8 +105,7 @@ extension SaveStatesViewController self.navigationItem.rightBarButtonItems?.removeFirst() } - // Manually update prototype cell properties - self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth) + self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: 0) self.prototypeCellWidthConstraint.isActive = true self.prepareEmulatorCoreSaveState() @@ -238,6 +229,26 @@ private extension SaveStatesViewController } self.sortButton.transform = CGAffineTransform.identity.rotated(by: Settings.sortSaveStatesByOldestFirst ? 0 : .pi) + + let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout + + if self.traitCollection.horizontalSizeClass == .regular + { + collectionViewLayout.itemWidth = 180 + collectionViewLayout.minimumInteritemSpacing = 30 + } + else + { + let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2 + let portraitScreenWidth = UIScreen.main.coordinateSpace.convert(UIScreen.main.bounds, to: UIScreen.main.fixedCoordinateSpace).width + + // Use dimensions that allow two cells to fill the screen horizontally with padding in portrait mode + // We'll keep the same size for landscape orientation, which will allow more to fit + collectionViewLayout.itemWidth = floor((portraitScreenWidth - (averageHorizontalInset * 3)) / 2) + } + + // Manually update prototype cell properties + self.prototypeCellWidthConstraint.constant = collectionViewLayout.itemWidth } //MARK: - Configure Views - diff --git a/Delta/Settings/Controllers/ControllerInputsViewController.swift b/Delta/Settings/Controllers/ControllerInputsViewController.swift index 97e8be3..2051891 100644 --- a/Delta/Settings/Controllers/ControllerInputsViewController.swift +++ b/Delta/Settings/Controllers/ControllerInputsViewController.swift @@ -40,6 +40,8 @@ class ControllerInputsViewController: UIViewController private var activeCalloutView: InputCalloutView? + private var _didLayoutSubviews = false + @IBOutlet private var actionsMenuViewControllerHeightConstraint: NSLayoutConstraint! @IBOutlet private var cancelTapGestureRecognizer: UITapGestureRecognizer! @@ -65,7 +67,15 @@ class ControllerInputsViewController: UIViewController self.gameViewController.controllerView.addReceiver(self) - self.navigationController?.navigationBar.barStyle = .black + if let navigationController = self.navigationController, #available(iOS 13, *) + { + navigationController.overrideUserInterfaceStyle = .dark + navigationController.navigationBar.scrollEdgeAppearance = navigationController.navigationBar.standardAppearance // Fixes invisible navigation bar on iPad. + } + else + { + self.navigationController?.navigationBar.barStyle = .black + } NSLayoutConstraint.activate([self.gameViewController.gameView.centerYAnchor.constraint(equalTo: self.actionsMenuViewController.view.centerYAnchor)]) @@ -81,6 +91,23 @@ class ControllerInputsViewController: UIViewController { self.actionsMenuViewControllerHeightConstraint.constant = self.actionsMenuViewController.preferredContentSize.height } + + if let window = self.view.window, !_didLayoutSubviews + { + var traits = DeltaCore.ControllerSkin.Traits.defaults(for: window) + traits.orientation = .portrait + + if traits.device == .ipad + { + // Use standard iPhone skins instead of iPad skins. + traits.device = .iphone + traits.displayType = .standard + } + + self.gameViewController.controllerView.overrideControllerSkinTraits = traits + + _didLayoutSubviews = true + } } override func viewDidAppear(_ animated: Bool) @@ -403,6 +430,7 @@ private extension ControllerInputsViewController } let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.popoverPresentationController?.barButtonItem = sender alertController.addAction(.cancel) alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset Controls to Defaults", comment: ""), style: .destructive, handler: { (action) in reset() @@ -562,3 +590,15 @@ extension ControllerInputsViewController: SMCalloutViewDelegate self.toggle(calloutView) } } + +extension ControllerInputsViewController: UIAdaptivePresentationControllerDelegate +{ + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle + { + switch (traitCollection.horizontalSizeClass, traitCollection.verticalSizeClass) + { + case (.regular, .regular): return .formSheet // Regular width and height, so display as form sheet + default: return .fullScreen // Compact width and/or height, so display full screen + } + } +} diff --git a/Delta/Settings/Controllers/ControllersSettingsViewController.swift b/Delta/Settings/Controllers/ControllersSettingsViewController.swift index 97771d4..2c9169d 100644 --- a/Delta/Settings/Controllers/ControllersSettingsViewController.swift +++ b/Delta/Settings/Controllers/ControllersSettingsViewController.swift @@ -108,9 +108,18 @@ extension ControllersSettingsViewController switch identifier { case "controllerInputsSegue": - let controllerInputsViewController = (segue.destination as! UINavigationController).topViewController as! ControllerInputsViewController + let navigationController = segue.destination as! UINavigationController + + let controllerInputsViewController = navigationController.topViewController as! ControllerInputsViewController controllerInputsViewController.gameController = self.gameController + if self.view.traitCollection.userInterfaceIdiom == .pad + { + // For now, only iPads can display ControllerInputsViewController as a form sheet. + navigationController.modalPresentationStyle = .formSheet + navigationController.presentationController?.delegate = controllerInputsViewController + } + default: break } @@ -296,6 +305,8 @@ extension ControllersSettingsViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let previousGameController = self.gameController + switch Section(rawValue: indexPath.section)! { case .localDevice: self.gameController = self.localDeviceController @@ -310,7 +321,7 @@ extension ControllersSettingsViewController let previousIndexPath: IndexPath? - if let gameController = self.gameController + if let gameController = previousGameController { if gameController == self.localDeviceController { diff --git a/Delta/Supporting Files/Info.plist b/Delta/Supporting Files/Info.plist index cc6ca54..b60ad4e 100644 --- a/Delta/Supporting Files/Info.plist +++ b/Delta/Supporting Files/Info.plist @@ -215,6 +215,8 @@ armv7 + UIRequiresFullScreen + UIStatusBarStyle UIStatusBarStyleLightContent UISupportedInterfaceOrientations @@ -223,6 +225,13 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UISupportsDocumentBrowser UTExportedTypeDeclarations