Adds SyncResultViewController to view errors that occured during sync
This commit is contained in:
parent
eaa8429bd8
commit
bace668739
@ -36,6 +36,10 @@
|
||||
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF15AF831F54B43B009B6AAB /* ActionInput.swift */; };
|
||||
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; };
|
||||
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */; };
|
||||
BF1F45A421AF274D00EF9895 /* SyncResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */; };
|
||||
BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */; };
|
||||
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */; };
|
||||
BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45BE21AF676F00EF9895 /* Box.swift */; };
|
||||
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; };
|
||||
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; };
|
||||
BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF31878A1D489AAA00BD020D /* CheatValidator.swift */; };
|
||||
@ -180,6 +184,10 @@
|
||||
BF15AF831F54B43B009B6AAB /* ActionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionInput.swift; sourceTree = "<group>"; };
|
||||
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = "<group>"; };
|
||||
BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemControllerSkinsViewController.swift; sourceTree = "<group>"; };
|
||||
BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncResultViewController.swift; sourceTree = "<group>"; };
|
||||
BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SyncResultsViewController.storyboard; sourceTree = "<group>"; };
|
||||
BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HarmonyMetadataKey+Keys.swift"; sourceTree = "<group>"; };
|
||||
BF1F45BE21AF676F00EF9895 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||
BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = "<group>"; };
|
||||
BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = "<group>"; };
|
||||
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; };
|
||||
@ -327,6 +335,7 @@
|
||||
BFC6F7B71F435BC500221B96 /* Input+Display.swift */,
|
||||
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */,
|
||||
BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */,
|
||||
BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -404,6 +413,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF4828871F90290F00028B97 /* Action.swift */,
|
||||
BF1F45BE21AF676F00EF9895 /* Box.swift */,
|
||||
BFE0229C1F5B56840052D888 /* Popover Menu */,
|
||||
BF5942671E09BBB70051894B /* Collection View */,
|
||||
BF71CF881FE90471001F1613 /* Table View */,
|
||||
@ -623,6 +633,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFAB9F7C219A43380080EC7D /* SyncManager.swift */,
|
||||
BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */,
|
||||
BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */,
|
||||
);
|
||||
path = Syncing;
|
||||
sourceTree = "<group>";
|
||||
@ -851,6 +863,7 @@
|
||||
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */,
|
||||
BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */,
|
||||
BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */,
|
||||
BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */,
|
||||
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */,
|
||||
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
@ -974,6 +987,7 @@
|
||||
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */,
|
||||
BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */,
|
||||
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
|
||||
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */,
|
||||
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */,
|
||||
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */,
|
||||
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */,
|
||||
@ -983,6 +997,7 @@
|
||||
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */,
|
||||
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */,
|
||||
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
|
||||
BF1F45A421AF274D00EF9895 /* SyncResultViewController.swift in Sources */,
|
||||
BF5942881E09BC8B0051894B /* _Game.swift in Sources */,
|
||||
BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */,
|
||||
BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */,
|
||||
@ -991,6 +1006,7 @@
|
||||
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */,
|
||||
BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */,
|
||||
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
|
||||
BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */,
|
||||
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */,
|
||||
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
|
||||
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */,
|
||||
|
||||
19
Delta/Components/Box.swift
Normal file
19
Delta/Components/Box.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Box.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 11/28/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Box<T>
|
||||
{
|
||||
let value: T
|
||||
|
||||
init(_ value: T)
|
||||
{
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
@ -43,5 +43,14 @@ extension Cheat: Syncable
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\Cheat.game as AnyKeyPath]
|
||||
}
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
return [.gameID: game.identifier, .gameName: game.name]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,4 +109,8 @@ extension ControllerSkin: Syncable
|
||||
public var isSyncingEnabled: Bool {
|
||||
return !self.isStandard
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,4 +133,8 @@ extension Game: Syncable
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\Game.gameCollection]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,4 +39,8 @@ extension GameCollection: Syncable
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\GameCollection.index as AnyKeyPath]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,4 +96,8 @@ extension GameControllerInputMapping: Syncable
|
||||
\GameControllerInputMapping.gameType,
|
||||
\GameControllerInputMapping.playerIndex]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,4 +125,13 @@ extension SaveState: Syncable
|
||||
public var isSyncingEnabled: Bool {
|
||||
return self.type != .auto && self.type != .quick
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
return [.gameID: game.identifier, .gameName: game.name]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.localizedName
|
||||
}
|
||||
}
|
||||
|
||||
15
Delta/Extensions/HarmonyMetadataKey+Keys.swift
Normal file
15
Delta/Extensions/HarmonyMetadataKey+Keys.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// HarmonyMetadataKey+Keys.swift
|
||||
// Harmony
|
||||
//
|
||||
// Created by Riley Testut on 11/5/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Harmony
|
||||
|
||||
extension HarmonyMetadataKey
|
||||
{
|
||||
static let gameID = HarmonyMetadataKey("gameID")
|
||||
static let gameName = HarmonyMetadataKey("gameName")
|
||||
}
|
||||
@ -404,19 +404,30 @@ private extension GamesViewController
|
||||
@objc func syncingDidFinish(_ notification: Notification)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? Result<[Result<Void>]> else { return }
|
||||
guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return }
|
||||
|
||||
let toastView: RSTToastView
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success: toastView = RSTToastView(text: NSLocalizedString("Sync Complete", comment: ""), detailText: nil)
|
||||
case .failure(let error): toastView = RSTToastView(error: error)
|
||||
case .failure(let error as HarmonyError): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.failureReason)
|
||||
case .failure(let error): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.localizedDescription)
|
||||
}
|
||||
|
||||
toastView.addTarget(self, action: #selector(GamesViewController.presentSyncResultsViewController), for: .touchUpInside)
|
||||
|
||||
toastView.show(in: self.view, duration: 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func presentSyncResultsViewController()
|
||||
{
|
||||
guard let result = SyncManager.shared.previousSyncResult else { return }
|
||||
|
||||
let navigationController = SyncResultViewController.make(result: result)
|
||||
self.present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UIPageViewController -
|
||||
|
||||
@ -9,6 +9,39 @@
|
||||
import Harmony
|
||||
import Harmony_Drive
|
||||
|
||||
extension SyncManager
|
||||
{
|
||||
enum RecordType: String, Hashable
|
||||
{
|
||||
case game = "Game"
|
||||
case gameCollection = "GameCollection"
|
||||
case cheat = "Cheat"
|
||||
case saveState = "SaveState"
|
||||
case controllerSkin = "ControllerSkin"
|
||||
case gameControllerInputMapping = "GameControllerInputMapping"
|
||||
|
||||
var localizedName: String {
|
||||
switch self
|
||||
{
|
||||
case .game: return NSLocalizedString("Game", comment: "")
|
||||
case .gameCollection: return NSLocalizedString("Game Collection", comment: "")
|
||||
case .cheat: return NSLocalizedString("Cheat", comment: "")
|
||||
case .saveState: return NSLocalizedString("Save State", comment: "")
|
||||
case .controllerSkin: return NSLocalizedString("Controller Skin", comment: "")
|
||||
case .gameControllerInputMapping: return NSLocalizedString("Game Controller Input Mapping", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Syncable where Self: NSManagedObject
|
||||
{
|
||||
var recordType: SyncManager.RecordType {
|
||||
let recordType = SyncManager.RecordType(rawValue: self.syncableType)!
|
||||
return recordType
|
||||
}
|
||||
}
|
||||
|
||||
final class SyncManager
|
||||
{
|
||||
static let shared = SyncManager()
|
||||
@ -21,6 +54,8 @@ final class SyncManager
|
||||
return self.syncCoordinator.recordController
|
||||
}
|
||||
|
||||
private(set) var previousSyncResult: SyncResult?
|
||||
|
||||
private(set) var isAuthenticated = false
|
||||
|
||||
let syncCoordinator = SyncCoordinator(service: DriveService.shared, persistentContainer: DatabaseManager.shared)
|
||||
@ -28,6 +63,8 @@ final class SyncManager
|
||||
private init()
|
||||
{
|
||||
DriveService.shared.clientID = "457607414709-5puj6lcv779gpu3ql43e6k3smjj40dmu.apps.googleusercontent.com"
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +84,7 @@ extension SyncManager
|
||||
|
||||
self.isAuthenticated = true
|
||||
}
|
||||
catch let error as AuthenticationError where error.code == .noSavedCredentials
|
||||
catch let error as _AuthenticationError where error.code == .noSavedCredentials
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
@ -89,3 +126,12 @@ extension SyncManager
|
||||
self.syncCoordinator.sync()
|
||||
}
|
||||
}
|
||||
|
||||
private extension SyncManager
|
||||
{
|
||||
@objc func syncingDidFinish(_ notification: Notification)
|
||||
{
|
||||
guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return }
|
||||
self.previousSyncResult = result
|
||||
}
|
||||
}
|
||||
|
||||
329
Delta/Syncing/SyncResultViewController.swift
Normal file
329
Delta/Syncing/SyncResultViewController.swift
Normal file
@ -0,0 +1,329 @@
|
||||
//
|
||||
// SyncResultViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 11/28/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import Roxas
|
||||
import Harmony
|
||||
|
||||
@objc(SyncResultTableViewCell)
|
||||
private class SyncResultTableViewCell: UITableViewCell
|
||||
{
|
||||
@IBOutlet var nameLabel: UILabel!
|
||||
@IBOutlet var errorLabel: UILabel!
|
||||
}
|
||||
|
||||
extension SyncResultViewController
|
||||
{
|
||||
private enum Group: Hashable
|
||||
{
|
||||
case game(RecordID)
|
||||
case saveState(gameID: RecordID)
|
||||
case cheat(gameID: RecordID)
|
||||
case controllerSkin
|
||||
case gameControllerInputMapping
|
||||
case gameCollection
|
||||
case other
|
||||
|
||||
var sortIndex: Int {
|
||||
switch self
|
||||
{
|
||||
case .game: return 0
|
||||
case .saveState: return 1
|
||||
case .cheat: return 2
|
||||
case .controllerSkin: return 3
|
||||
case .gameControllerInputMapping: return 4
|
||||
case .gameCollection: return 5
|
||||
case .other: return 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResultViewController: UITableViewController
|
||||
{
|
||||
private(set) var result: Result<[Record<NSManagedObject>: Result<Void>]>!
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
private lazy var sortedErrors = self.processResults()
|
||||
private lazy var gameNamesByRecordID = self.fetchGameNames()
|
||||
|
||||
private init()
|
||||
{
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncResultViewController
|
||||
{
|
||||
class func make(result: Result<[Record<NSManagedObject>: Result<Void>]>) -> UINavigationController
|
||||
{
|
||||
let storyboard = UIStoryboard(name: "SyncResultsViewController", bundle: nil)
|
||||
|
||||
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
|
||||
|
||||
let syncResultViewController = navigationController.viewControllers[0] as! SyncResultViewController
|
||||
syncResultViewController.result = result
|
||||
|
||||
return navigationController
|
||||
}
|
||||
}
|
||||
|
||||
private extension SyncResultViewController
|
||||
{
|
||||
func makeDataSource() -> RSTCompositeTableViewDataSource<Box<Error>>
|
||||
{
|
||||
let dataSources = self.sortedErrors.map { (_, errors) -> RSTArrayTableViewDataSource<Box<Error>> in
|
||||
let dataSource = RSTArrayTableViewDataSource<Box<Error>>(items: errors.map(Box.init))
|
||||
dataSource.cellConfigurationHandler = { (cell, error, indexPath) in
|
||||
let cell = cell as! SyncResultTableViewCell
|
||||
|
||||
let title: String?
|
||||
let errorMessage: String?
|
||||
|
||||
switch error.value
|
||||
{
|
||||
case let error as RecordError:
|
||||
guard let recordType = SyncManager.RecordType(rawValue: error.record.recordID.type) else { return }
|
||||
|
||||
switch recordType
|
||||
{
|
||||
case .game: title = NSLocalizedString("Game", comment: "")
|
||||
case .saveState, .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping: title = error.record.localizedName ?? recordType.localizedName
|
||||
}
|
||||
|
||||
switch error
|
||||
{
|
||||
case .filesFailed(_, let errors):
|
||||
var messages = [String]()
|
||||
|
||||
for error in errors
|
||||
{
|
||||
messages.append(error.localizedDescription)
|
||||
}
|
||||
|
||||
errorMessage = messages.joined(separator: "\n")
|
||||
|
||||
default: errorMessage = error.failureReason
|
||||
}
|
||||
|
||||
case let error as HarmonyError:
|
||||
title = error.failureDescription
|
||||
errorMessage = error.failureReason
|
||||
|
||||
case let error:
|
||||
assertionFailure("Only HarmonyErrors should be thrown by syncing logic.")
|
||||
|
||||
title = nil
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
cell.nameLabel.text = title
|
||||
cell.errorLabel.text = errorMessage
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.text = NSLocalizedString("Sync Successful", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("There were no errors during last sync.", comment: "")
|
||||
|
||||
let compositeDataSource = RSTCompositeTableViewDataSource(dataSources: dataSources)
|
||||
compositeDataSource.proxy = self
|
||||
compositeDataSource.placeholderView = placeholderView
|
||||
return compositeDataSource
|
||||
}
|
||||
|
||||
private func processResults() -> [(group: Group, errors: [Error])]
|
||||
{
|
||||
var errors = [Error]()
|
||||
|
||||
do
|
||||
{
|
||||
try self.result.verify()
|
||||
}
|
||||
catch SyncError.partial(let recordResults)
|
||||
{
|
||||
for (_, result) in recordResults
|
||||
{
|
||||
guard case .failure(let error) = result else { continue }
|
||||
errors.append(error)
|
||||
}
|
||||
}
|
||||
catch SyncError.cancelled
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
catch let error as SyncError
|
||||
{
|
||||
let error = error.underlyingError ?? error
|
||||
errors.append(error)
|
||||
}
|
||||
catch
|
||||
{
|
||||
assertionFailure("Non-SyncError thrown by sync result.")
|
||||
errors.append(error)
|
||||
}
|
||||
|
||||
var errorsByGroup = [Group: [Error]]()
|
||||
|
||||
for error in errors
|
||||
{
|
||||
let group: Group
|
||||
|
||||
switch error
|
||||
{
|
||||
case let error as RecordError:
|
||||
guard let recordType = SyncManager.RecordType(rawValue: error.record.recordID.type) else { continue }
|
||||
|
||||
switch recordType
|
||||
{
|
||||
case .game: group = .game(error.record.recordID)
|
||||
case .gameCollection: group = .gameCollection
|
||||
case .controllerSkin: group = .controllerSkin
|
||||
case .gameControllerInputMapping: group = .gameControllerInputMapping
|
||||
|
||||
case .saveState:
|
||||
guard let gameID = error.record.metadata?[.gameID] else { continue }
|
||||
|
||||
let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
|
||||
group = .saveState(gameID: recordID)
|
||||
|
||||
case .cheat:
|
||||
guard let gameID = error.record.metadata?[.gameID] else { continue }
|
||||
|
||||
let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
|
||||
group = .cheat(gameID: recordID)
|
||||
}
|
||||
|
||||
default: group = .other
|
||||
}
|
||||
|
||||
errorsByGroup[group, default: []].append(error)
|
||||
}
|
||||
|
||||
let sortedErrors = errorsByGroup.sorted { (a, b) in
|
||||
let groupA = a.key
|
||||
let groupB = b.key
|
||||
|
||||
// Sort initially by game, then sort by type.
|
||||
// This way games and their associated records (such as save states) are visually grouped together.
|
||||
switch (groupA, groupB)
|
||||
{
|
||||
// Game-related records, but different game identifiers, so sort by game identifiers (implicitly grouping related game records together).
|
||||
// Using `fallthrough` for these cases seg faults the compiler (as of Swift 4.2.1), so we just duplicate the return expression.
|
||||
case (.game(let a), .game(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.game(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.game(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.saveState(let a), .game(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.saveState(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.saveState(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.cheat(let a), .game(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.cheat(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier
|
||||
case (.cheat(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier
|
||||
|
||||
// Otherwise, just return their relative ordering.
|
||||
case (.game, _): fallthrough
|
||||
case (.saveState, _): fallthrough
|
||||
case (.cheat, _): fallthrough
|
||||
case (.controllerSkin, _): fallthrough
|
||||
case (.gameControllerInputMapping, _): fallthrough
|
||||
case (.gameCollection, _): fallthrough
|
||||
case (.other, _): return groupA.sortIndex < groupB.sortIndex
|
||||
}
|
||||
}
|
||||
|
||||
return sortedErrors.map { (group: $0.key, errors: $0.value) }
|
||||
}
|
||||
|
||||
func fetchGameNames() -> [RecordID: String]
|
||||
{
|
||||
let fetchRequest = Game.fetchRequest() as NSFetchRequest<Game>
|
||||
fetchRequest.propertiesToFetch = [#keyPath(Game.name), #keyPath(Game.identifier)]
|
||||
|
||||
do
|
||||
{
|
||||
let games = try DatabaseManager.shared.viewContext.fetch(fetchRequest)
|
||||
|
||||
let gameNames = Dictionary(uniqueKeysWithValues: games.map { (RecordID(type: SyncManager.RecordType.game.rawValue, identifier: $0.identifier), $0.name) })
|
||||
return gameNames
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch game names.", error)
|
||||
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SyncResultViewController
|
||||
{
|
||||
@IBAction func dismiss()
|
||||
{
|
||||
self.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncResultViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
let section = self.sortedErrors[section]
|
||||
|
||||
switch section.group
|
||||
{
|
||||
case .controllerSkin: return NSLocalizedString("Controller Skins", comment: "")
|
||||
case .gameCollection: return NSLocalizedString("Game Collections", comment: "")
|
||||
case .gameControllerInputMapping: return NSLocalizedString("Input Mappings", comment: "")
|
||||
case .other: return NSLocalizedString("Misc.", comment: "")
|
||||
|
||||
case .game:
|
||||
guard let error = section.errors.first as? RecordError else { return nil }
|
||||
return error.record.localizedName
|
||||
|
||||
case .saveState(let gameID):
|
||||
guard let error = section.errors.first as? RecordError else { return nil }
|
||||
|
||||
if let gameName = self.gameNamesByRecordID[gameID] ?? error.record.metadata?[.gameName]
|
||||
{
|
||||
return gameName + " - " + NSLocalizedString("Save States", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("Save States", comment: "")
|
||||
}
|
||||
|
||||
case .cheat(let gameID):
|
||||
guard let error = section.errors.first as? RecordError else { return nil }
|
||||
|
||||
if let gameName = self.gameNamesByRecordID[gameID] ?? error.record.metadata?[.gameName]
|
||||
{
|
||||
return gameName + " - " + NSLocalizedString("Cheats", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("Cheats", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Delta/Syncing/SyncResultsViewController.storyboard
Normal file
93
Delta/Syncing/SyncResultsViewController.storyboard
Normal file
@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="PiK-Yl-5r8">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Results-->
|
||||
<scene sceneID="bU4-NU-gHn">
|
||||
<objects>
|
||||
<tableViewController id="Vv7-67-y3h" customClass="SyncResultViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Mge-4Z-RnG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" rowHeight="81" id="LBH-gN-Qjn" customClass="SyncResultTableViewCell">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="81"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LBH-gN-Qjn" id="qeU-Pt-UrC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4.5" translatesAutoresizingMaskIntoConstraints="NO" id="sqc-Zb-cJa">
|
||||
<rect key="frame" x="16" y="11" width="343" height="59"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title Screen" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="E5a-nn-ak9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="The record could not be uploaded because an error occured." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gzf-2D-v9F">
|
||||
<rect key="frame" x="0.0" y="25" width="343" height="34"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="sqc-Zb-cJa" secondAttribute="trailing" id="uaR-cD-kNU"/>
|
||||
<constraint firstItem="sqc-Zb-cJa" firstAttribute="leading" secondItem="qeU-Pt-UrC" secondAttribute="leadingMargin" id="xp4-eH-qd4"/>
|
||||
<constraint firstItem="sqc-Zb-cJa" firstAttribute="top" secondItem="qeU-Pt-UrC" secondAttribute="topMargin" id="z2y-9Y-d1U"/>
|
||||
<constraint firstItem="sqc-Zb-cJa" firstAttribute="bottom" secondItem="qeU-Pt-UrC" secondAttribute="bottomMargin" id="zT9-af-Xcc"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="errorLabel" destination="gzf-2D-v9F" id="QL8-uA-FvI"/>
|
||||
<outlet property="nameLabel" destination="E5a-nn-ak9" id="iBv-cv-b6G"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Vv7-67-y3h" id="9Yf-Hl-bIM"/>
|
||||
<outlet property="delegate" destination="Vv7-67-y3h" id="fDj-g9-Whw"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Results" id="Qzj-eH-8qF">
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="s6x-aF-rco">
|
||||
<connections>
|
||||
<action selector="dismiss" destination="Vv7-67-y3h" id="gW2-WJ-4ii"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x5W-HZ-Oy6" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="365.60000000000002" y="-179.46026986506749"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="d6h-g9-dij">
|
||||
<objects>
|
||||
<navigationController id="PiK-Yl-5r8" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EvV-dw-xfY">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="Vv7-67-y3h" kind="relationship" relationship="rootViewController" id="pdH-jQ-yc1"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ems-7x-mVT" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-457" y="-178"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
2
External/Harmony
vendored
2
External/Harmony
vendored
@ -1 +1 @@
|
||||
Subproject commit c562f4213f8e7f351873435a7add8d755d45f350
|
||||
Subproject commit 7f28cf57b0f68a26e342d392c94de18b29db9acd
|
||||
Loading…
Reference in New Issue
Block a user