diff --git a/Project E/Project E.xcodeproj/xcshareddata/xcschemes/Project E.xcscheme b/Project E/Project E.xcodeproj/xcshareddata/xcschemes/Project E.xcscheme new file mode 100644 index 0000000..bcd645d --- /dev/null +++ b/Project E/Project E.xcodeproj/xcshareddata/xcschemes/Project E.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer.xcodeproj/xcshareddata/xcschemes/WKRArticlesPreviewer.xcscheme b/WKRArticlesPreviewer/WKRArticlesPreviewer.xcodeproj/xcshareddata/xcschemes/WKRArticlesPreviewer.xcscheme new file mode 100644 index 0000000..1848d5a --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer.xcodeproj/xcshareddata/xcschemes/WKRArticlesPreviewer.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme b/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme index 24013e8..818a193 100644 --- a/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme +++ b/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitOfflineTests.xcscheme b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitOfflineTests.xcscheme index bbe14e2..da5d682 100644 --- a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitOfflineTests.xcscheme +++ b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitOfflineTests.xcscheme @@ -1,6 +1,6 @@ /The_Magic_Carpets_of_Aladdin /Pixar_Pal-A-Round /Slinky_Dog_Zigzag_Spin + /La_La_Land + /Apple_Silicon + /Johny_Srouji diff --git a/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist b/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist index a27d9fc..678d8cb 100644 --- a/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist +++ b/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist @@ -41,5 +41,11 @@ 8 PageTitleMaxRandomLength 30 + ManageGameCenterLink + https://support.apple.com/en-us/HT210401 + RaceCodeRecordMinReuseTimeSinceLastUpdate + 600 + RaceResultsSpectatorUpdateInterval + 4 diff --git a/WKRKit/WKRKit/Constants/WKRKitConstants.plist b/WKRKit/WKRKit/Constants/WKRKitConstants.plist index c78af72..6448165 100644 --- a/WKRKit/WKRKit/Constants/WKRKitConstants.plist +++ b/WKRKit/WKRKit/Constants/WKRKitConstants.plist @@ -28,7 +28,7 @@ RandomURLString https://en.m.wikipedia.org/wiki/Special:Random Version - 26 + 32 WhatLinksHereURLString https://en.m.wikipedia.org/w/index.php?title=Special:WhatLinksHere MaxFoundPagePlayers @@ -40,8 +40,14 @@ BonusPointsInterval 120 MaxGlobalRacePlayers - 4 + 8 MaxLocalRacePlayers 8 + ManageGameCenterLink + https://support.apple.com/en-us/HT210401 + RaceCodeRecordMinReuseTimeSinceLastUpdate + 600 + RaceResultsSpectatorUpdateInterval + 4 diff --git a/WKRKit/WKRKit/Constants/WKRKitConstants.swift b/WKRKit/WKRKit/Constants/WKRKitConstants.swift index 1b5263b..f663838 100644 --- a/WKRKit/WKRKit/Constants/WKRKitConstants.swift +++ b/WKRKit/WKRKit/Constants/WKRKitConstants.swift @@ -7,7 +7,7 @@ // import CloudKit -import Foundation +import os.log public struct WKRKitConstants { @@ -38,9 +38,15 @@ public struct WKRKitConstants { public let maxGlobalRacePlayers: Int public let maxLocalRacePlayers: Int + public let manageGameCenterLink: URL + public let raceCodeRecordMinReuseTimeSinceLastUpdate: Int + public let raceResultsSpectatorUpdateInterval: Int + // MARK: - Initalization init() { + os_log("%{public}s", log: .constants, type: .info, #function) + guard let documentsConstantsURL = FileManager.default.documentsDirectory?.appendingPathComponent("WKRKitConstants.plist"), let documentsConstants = NSDictionary(contentsOf: documentsConstantsURL) as? [String: Any] else { fatalError("Failed to load constants") @@ -95,6 +101,17 @@ public struct WKRKitConstants { fatalError("WKRKitConstants: No MaxLocalRacePlayers value") } + guard let manageGameCenterLinkString = documentsConstants["ManageGameCenterLink"] as? String, + let manageGameCenterLink = URL(string: manageGameCenterLinkString) else { + fatalError("WKRKitConstants: No ManageGameCenterLink value") + } + guard let raceCodeRecordMinReuseTimeSinceLastUpdate = documentsConstants["RaceCodeRecordMinReuseTimeSinceLastUpdate"] as? Int else { + fatalError("WKRKitConstants: No RaceCodeRecordMinReuseTimeSinceLastUpdate value") + } + guard let raceResultsSpectatorUpdateInterval = documentsConstants["RaceResultsSpectatorUpdateInterval"] as? Int else { + fatalError("WKRKitConstants: No RaceResultsSpectatorUpdateInterval value") + } + self.version = version self.isQuickRaceMode = quickRace self.connectionTestTimeout = connectionTestTimeout @@ -116,17 +133,22 @@ public struct WKRKitConstants { self.bannedURLFragments = bannedURLFragments self.maxGlobalRacePlayers = maxGlobalRacePlayers self.maxLocalRacePlayers = maxLocalRacePlayers + + self.manageGameCenterLink = manageGameCenterLink + self.raceCodeRecordMinReuseTimeSinceLastUpdate = raceCodeRecordMinReuseTimeSinceLastUpdate + self.raceResultsSpectatorUpdateInterval = raceResultsSpectatorUpdateInterval } // MARK: - Helpers @available(*, deprecated, message: "Only for testing") static public func removeConstants() { + os_log("%{public}s", log: .constants, type: .info, #function) let fileManager = FileManager.default guard let folderPath = fileManager.documentsDirectory?.path, let filePaths = try? fileManager.contentsOfDirectory(atPath: folderPath) else { - fatalError() + return } for filePath in filePaths { do { @@ -139,10 +161,12 @@ public struct WKRKitConstants { @available(*, deprecated, message: "Only for testing") static public func updateConstantsForTestingCharacterClipping() { + os_log("%{public}s", log: .constants, type: .info, #function) copyBundledResourcesToDocuments(constantsFileName: "WKRKitConstants-TESTING_ONLY") } static public func updateConstants() { + os_log("%{public}s", log: .constants, type: .info, #function) copyBundledResourcesToDocuments() guard ProcessInfo.processInfo.environment["Cloud_Disabled"] != "true" else { @@ -154,12 +178,14 @@ public struct WKRKitConstants { publicDB.fetch(withRecordID: recordID) { record, _ in guard let record = record else { + os_log("%{public}s: no record", log: .constants, type: .error, #function) return } guard let recordConstantsAssetURL = (record["ConstantsFile"] as? CKAsset)?.fileURL, let recordArticlesAssetURL = (record["ArticlesFile"] as? CKAsset)?.fileURL, let recordGetLinksScriptAssetURL = (record["GetLinksScriptFile"] as? CKAsset)?.fileURL else { + os_log("%{public}s: invalid record assets", log: .constants, type: .error, #function) return } @@ -178,12 +204,14 @@ public struct WKRKitConstants { guard FileManager.default.fileExists(atPath: newConstantsFileURL.path), FileManager.default.fileExists(atPath: newArticlesFileURL.path), FileManager.default.fileExists(atPath: newGetLinksScriptFileURL.path) else { + os_log("%{public}s: files don't exist", log: .constants, type: .error, #function) return } guard let newConstants = NSDictionary(contentsOf: newConstantsFileURL), let newConstantsVersion = newConstants["Version"] as? Int, let documentsDirectory = FileManager.default.documentsDirectory else { + os_log("%{public}s: version doesn't exist", log: .constants, type: .error, #function) return } @@ -204,6 +232,9 @@ public struct WKRKitConstants { if newConstantsVersion <= documentsConstantsVersions { shouldReplaceExisitingConstants = false + os_log("%{public}s: don't replace: new: %{public}ld, existing: %{public}ld", log: .constants, type: .info, #function, newConstantsVersion, documentsConstantsVersions) + } else { + os_log("%{public}s: replace: new: %{public}ld, existing: %{public}ld", log: .constants, type: .info, #function, newConstantsVersion, documentsConstantsVersions) } } @@ -227,6 +258,8 @@ public struct WKRKitConstants { } static private func copyBundledResourcesToDocuments(constantsFileName: String = "WKRKitConstants") { + os_log("%{public}s", log: .constants, type: .error, #function) + guard Thread.isMainThread, let bundle = Bundle(identifier: "com.andrewfinke.WKRKit"), let bundledPlistURL = bundle.url(forResource: constantsFileName, withExtension: "plist"), @@ -240,7 +273,7 @@ public struct WKRKitConstants { newGetLinksScriptFileURL: bundledGetLinksScriptURL) } - lazy private(set) var finalArticles: [String] = { + lazy public private(set) var finalArticles: [String] = { guard let documentsArticlesURL = FileManager.default.documentsDirectory?.appendingPathComponent("WKRArticlesData.plist"), let arrayFromURL = NSArray(contentsOf: documentsArticlesURL), let array = arrayFromURL as? [String] else { diff --git a/WKRKit/WKRKit/Game/WKRGame.swift b/WKRKit/WKRKit/Game/WKRGame.swift index 4d967a0..75fba3b 100644 --- a/WKRKit/WKRKit/Game/WKRGame.swift +++ b/WKRKit/WKRKit/Game/WKRGame.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit final public class WKRGame { @@ -99,7 +99,7 @@ final public class WKRGame { // MARK: - Player Voting internal func player(_ profile: WKRPlayerProfile, votedFor page: WKRPage) { - preRaceConfig?.voteInfo.player(profile, votedFor: page) + preRaceConfig?.votingState.player(profile, votedFor: page) } internal func playerDisconnected(_ profile: WKRPlayerProfile) { diff --git a/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift b/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift index fe84332..d06502d 100644 --- a/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift +++ b/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit /// Used to transmit voting data and starting page public struct WKRPreRaceConfig: Codable, Equatable { @@ -14,7 +14,7 @@ public struct WKRPreRaceConfig: Codable, Equatable { // MARK: - Properties /// The voting info - internal var voteInfo: WKRVoteInfo + internal var votingState: WKRVotingState /// The starting page internal let startingPage: WKRPage @@ -24,9 +24,9 @@ public struct WKRPreRaceConfig: Codable, Equatable { /// /// - Parameters: /// - startingPage: The starting page - /// - voteInfo: The voting info - private init(startingPage: WKRPage, voteInfo: WKRVoteInfo) { - self.voteInfo = voteInfo + /// - votingState: The voting info + private init(startingPage: WKRPage, votingState: WKRVotingState) { + self.votingState = votingState self.startingPage = startingPage } @@ -36,7 +36,7 @@ public struct WKRPreRaceConfig: Codable, Equatable { /// /// - Returns: The new race config internal func raceConfig(with weights: [WKRPlayerProfile: Int]) -> (WKRRaceConfig?, WKRLogEvent?) { - let (finalPage, logEvent) = voteInfo.selectFinalPage(with: weights) + let (finalPage, logEvent) = votingState.selectFinalPage(with: weights) guard let page = finalPage else { return (nil, logEvent) } @@ -71,7 +71,7 @@ public struct WKRPreRaceConfig: Codable, Equatable { let events = logEvents.compactMap { $0 } if !finalPages.isEmpty, let page = startingPage { - let config = WKRPreRaceConfig(startingPage: page, voteInfo: WKRVoteInfo(pages: finalPages)) + let config = WKRPreRaceConfig(startingPage: page, votingState: WKRVotingState(pages: finalPages)) completionHandler(config, events) } else { completionHandler(nil, events) diff --git a/WKRKit/WKRKit/Game/WKRRace.swift b/WKRKit/WKRKit/Game/WKRRace.swift index 812c0c4..d66780f 100644 --- a/WKRKit/WKRKit/Game/WKRRace.swift +++ b/WKRKit/WKRKit/Game/WKRRace.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit internal struct WKRRace { diff --git a/WKRKit/WKRKit/Game/WKRRaceConfig.swift b/WKRKit/WKRKit/Game/WKRRaceConfig.swift index 93cc99d..56be4dc 100644 --- a/WKRKit/WKRKit/Game/WKRRaceConfig.swift +++ b/WKRKit/WKRKit/Game/WKRRaceConfig.swift @@ -9,11 +9,11 @@ import Foundation /// A race config. Lightweight object for sending out information about the race. -internal struct WKRRaceConfig: Codable { +public struct WKRRaceConfig: Codable { /// The starting page for the race - let startingPage: WKRPage + public let startingPage: WKRPage /// The final page for the race - let endingPage: WKRPage + public let endingPage: WKRPage /// Creates a new config object /// diff --git a/WKRKit/WKRKit/Game/WKRVoteInfo.swift b/WKRKit/WKRKit/Game/WKRVotingState.swift similarity index 65% rename from WKRKit/WKRKit/Game/WKRVoteInfo.swift rename to WKRKit/WKRKit/Game/WKRVotingState.swift index 38297c0..fddd55e 100644 --- a/WKRKit/WKRKit/Game/WKRVoteInfo.swift +++ b/WKRKit/WKRKit/Game/WKRVotingState.swift @@ -1,56 +1,46 @@ // -// WKRVoteInfo.swift +// WKRvotingState.swift // WKRKit // // Created by Andrew Finke on 8/5/17. // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit -public struct WKRVoteInfo: Codable, Equatable { +public struct WKRVotingState: Codable, Equatable { // MARK: - Properties - internal let pages: [WKRPage] - private var playerVotes = [WKRPlayerProfile: WKRPage]() - - public var pageCount: Int { - return pages.count + internal var pages: [WKRPage] { + return Array(playerVotes.keys) } + private var playerVotes = [WKRPage: [WKRPlayerProfile]]() // MARK: - Initialization internal init(pages: [WKRPage]) { - let sortedPages = pages.sorted { (pageOne, pageTwo) -> Bool in - return pageOne.title?.lowercased() ?? "" < pageTwo.title?.lowercased() ?? "" - } - self.pages = sortedPages + pages.forEach { playerVotes[$0] = [] } } // MARK: - Helpers - internal mutating func player(_ profile: WKRPlayerProfile, votedFor page: WKRPage) { - playerVotes[profile] = page + public mutating func player(_ profile: WKRPlayerProfile, votedFor page: WKRPage) { + playerVotes.keys.forEach { page in + guard let index = playerVotes[page]?.firstIndex(of: profile) else { return } + playerVotes[page]?.remove(at: index) + } + playerVotes[page]?.append(profile) } internal func selectFinalPage(with weights: [WKRPlayerProfile: Int]) -> (WKRPage?, WKRLogEvent?) { - var votes = [WKRPage: Int]() - pages.forEach { votes[$0] = 0 } - - for page in playerVotes.values { - let pageVotes = votes[page] ?? 0 - votes[page] = pageVotes + 1 - } - var pagesWithMostVotes = [WKRPage]() var mostVotes = 0 - - for (page, votes) in votes { - if votes > mostVotes { + for (page, voters) in playerVotes { + if voters.count > mostVotes { pagesWithMostVotes = [page] - mostVotes = votes - } else if votes == mostVotes { + mostVotes = voters.count + } else if voters.count == mostVotes { pagesWithMostVotes.append(page) } } @@ -68,8 +58,7 @@ public struct WKRVoteInfo: Codable, Equatable { totalPoints > 4, lowestScoringPlayers.count > 1, let player = lowestScoringPlayers.first, - let page = playerVotes[player.key], - pagesWithMostVotes.contains(page) { + let page = pagesWithMostVotes.first(where: { playerVotes[$0]?.contains(player.key) ?? false }) { let lowestScore = player.value let secondLowestScore = lowestScoringPlayers[1].value @@ -104,17 +93,14 @@ public struct WKRVoteInfo: Codable, Equatable { // MARK: - Public Accessors - public func page(for index: Int) -> (page: WKRPage, votes: Int)? { - guard index < pages.count else { return nil } - - let page = pages[index] - let votes = Array(playerVotes.values).filter({ $0 == page }).count - - return (page, votes) - } + public var current: [(page: WKRPage, voters: [WKRPlayerProfile])] { + let sortedPages = playerVotes.keys.sorted { (pageOne, pageTwo) -> Bool in + return pageOne.title?.lowercased() ?? "" < pageTwo.title?.lowercased() ?? "" + } - public func index(of page: WKRPage) -> Int? { - return pages.firstIndex(of: page) + return sortedPages.map { page in + return (page, playerVotes[page] ?? []) + } } } diff --git a/WKRKit/WKRKit/Info.plist b/WKRKit/WKRKit/Info.plist index 3ec7774..a79a077 100644 --- a/WKRKit/WKRKit/Info.plist +++ b/WKRKit/WKRKit/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 10453 + 13074 NSPrincipalClass diff --git a/WKRKit/WKRKit/Manager/WKRManager+Codable.swift b/WKRKit/WKRKit/Manager/WKRManager+Codable.swift index a41ac53..bd75a53 100644 --- a/WKRKit/WKRKit/Manager/WKRManager+Codable.swift +++ b/WKRKit/WKRKit/Manager/WKRManager+Codable.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit extension WKRGameManager { @@ -15,16 +15,16 @@ extension WKRGameManager { internal func receivedRaw(_ object: WKRCodable, from player: WKRPlayerProfile) { if let preRaceConfig = object.typeOf(WKRPreRaceConfig.self) { game.preRaceConfig = preRaceConfig - votingUpdate(.voteInfo(preRaceConfig.voteInfo)) + votingUpdate(.votingState(preRaceConfig.votingState)) if webView?.url != preRaceConfig.startingPage.url { webView?.load(URLRequest(url: preRaceConfig.startingPage.url)) } - WKRSeenFinalArticlesStore.addLocalPlayerSeenFinalPages(preRaceConfig.voteInfo.pages) + WKRSeenFinalArticlesStore.addLocalPlayerSeenFinalPages(preRaceConfig.votingState.pages) } else if let raceConfig = object.typeOf(WKRRaceConfig.self) { game.startRace(with: raceConfig) - votingUpdate(.finalPage(raceConfig.endingPage)) + votingUpdate(.raceConfig(raceConfig)) } else if let playerObject = object.typeOf(WKRPlayer.self) { if !game.players.contains(playerObject) && playerObject != localPlayer { peerNetwork.send(object: WKRCodable(localPlayer)) @@ -44,12 +44,13 @@ extension WKRGameManager { var samePageMessage: String? if samePagePlayers.count == 1 { - samePageMessage = "\(samePagePlayers[0].name) is on same page" + samePageMessage = "is on same page" } else if samePagePlayers.count > 1 { samePageMessage = "\(samePagePlayers.count) players are on same page" } if let message = samePageMessage { enqueue(message: message, + for: samePagePlayers.count == 1 ? samePagePlayers[0] : nil, duration: 2.0, isRaceSpecific: true, playHaptic: false) @@ -102,7 +103,8 @@ extension WKRGameManager { break } - enqueue(message: message.text(for: player), + enqueue(message: message.text, + for: player, duration: 3.0, isRaceSpecific: isRaceSpecific, playHaptic: playHaptic) @@ -126,6 +128,7 @@ extension WKRGameManager { let string = int.value == 1 ? "Point" : "Points" let message = "Race Bonus Now \(int.value) " + string enqueue(message: message, + for: nil, duration: 2.0, isRaceSpecific: true, playHaptic: false) @@ -155,12 +158,13 @@ extension WKRGameManager { let points = resultsInfo.raceRewardPoints(for: localPlayer) var place: Int? - for playerIndex in 0.. UIViewController? { - return peerNetwork.hostNetworkInterface() - } - - public func enqueue(message: String, duration: Double, isRaceSpecific: Bool, playHaptic: Bool) { + public func enqueue(message: String, for player: WKRPlayerProfile?, duration: Double, isRaceSpecific: Bool, playHaptic: Bool) { alertView.enqueue(text: message, + for: player, duration: duration, isRaceSpecific: isRaceSpecific, playHaptic: playHaptic) diff --git a/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift b/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift index f171bef..016acfa 100644 --- a/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift @@ -8,6 +8,7 @@ import Foundation import GameKit +import WKRUIKit final internal class WKRGameKitNetwork: NSObject, GKMatchDelegate, WKRPeerNetwork { @@ -43,10 +44,6 @@ final internal class WKRGameKitNetwork: NSObject, GKMatchDelegate, WKRPeerNetwor } } - internal func hostNetworkInterface() -> UIViewController? { - return nil - } - // MARK: - MCSessionDelegate func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) { @@ -80,12 +77,6 @@ final internal class WKRGameKitNetwork: NSObject, GKMatchDelegate, WKRPeerNetwor extension GKPlayer { func wkrProfile() -> WKRPlayerProfile { // alias is unique, but teamPlayerID is different depending on local or remote player - return WKRPlayerProfile(name: alias, playerID: alias) - } -} - -extension WKRPlayer { - static var isLocalPlayerCreator: Bool { - return GKLocalPlayer.local.isAuthenticated && GKLocalPlayer.local.alias == "J3D1 WARR10R" + return WKRPlayerProfile(name: displayName, playerID: alias) } } diff --git a/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift b/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift index 1089330..1338f07 100644 --- a/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit final internal class WKRSplitViewNetwork: WKRPeerNetwork { @@ -85,8 +85,4 @@ final internal class WKRSplitViewNetwork: WKRPeerNetwork { print("Would Disconnect") } - func hostNetworkInterface() -> UIViewController? { - return nil - } - } diff --git a/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift b/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift deleted file mode 100644 index 66537c9..0000000 --- a/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// WKRMultipeerNetwork.swift -// WKRKit -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import Foundation -import MultipeerConnectivity - -final internal class WKRMultipeerNetwork: NSObject, MCSessionDelegate, MCBrowserViewControllerDelegate, WKRPeerNetwork { - - // MARK: - Closures - - var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? - - // MARK: - Properties - - private weak var session: MCSession? - private let serviceType: String - - // MARK: - Initialization - - init(serviceType: String, session: MCSession) { - self.serviceType = serviceType - self.session = session - super.init() - session.delegate = self - } - - // MARK: - WKRNetwork - - func disconnect() { - session?.disconnect() - } - - func send(object: WKRCodable) { - guard let session = session, let data = try? WKRCodable.encoder.encode(object) else { return } - do { - try session.send(data, toPeers: session.connectedPeers, with: .reliable) - networkUpdate?(.object(object, profile: session.myPeerID.wkrProfile())) - } catch { - print(error) - } - } - - internal func hostNetworkInterface() -> UIViewController? { - guard let session = session else { fatalError("Session is nil") } - let browserViewController = MCBrowserViewController(serviceType: serviceType, session: session) - browserViewController.maximumNumberOfPeers = 8 - browserViewController.delegate = self - return browserViewController - } - - // MARK: - MCSessionDelegate - - public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - do { - let object = try WKRCodable.decoder.decode(WKRCodable.self, from: data) - networkUpdate?(.object(object, profile: peerID.wkrProfile())) - } catch { - print(data.description) - } - } - - public func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { - DispatchQueue.main.async { - switch state { - case .connected: self.networkUpdate?(.playerConnected(profile: peerID.wkrProfile())) - case .notConnected: self.networkUpdate?(.playerDisconnected(profile: peerID.wkrProfile())) - default: break - } - - // no players left - if session.connectedPeers.isEmpty { - self.networkUpdate?(.playerDisconnected(profile: session.myPeerID.wkrProfile())) - } - } - } - - // MARK: - MCBrowserViewControllerDelegate - - public func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) { - browserViewController.presentingViewController?.dismiss(animated: true, completion: nil) - } - - public func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) { - browserViewController.presentingViewController?.dismiss(animated: true, completion: nil) - } - - // Not needed - - public func session(_ session: MCSession, - didStartReceivingResourceWithName resourceName: String, - fromPeer peerID: MCPeerID, - with progress: Progress) { - } - - public func session(_ session: MCSession, - didFinishReceivingResourceWithName resourceName: String, - fromPeer peerID: MCPeerID, - at localURL: URL?, - withError error: Error?) { - } - - public func session(_ session: MCSession, - didReceive stream: InputStream, - withName streamName: String, - fromPeer peerID: MCPeerID) { - } - -} - -// MARK: - WKRKit Extensions - -extension MCPeerID { - func wkrProfile() -> WKRPlayerProfile { - return WKRPlayerProfile(name: displayName, playerID: hashValue.description) - } -} diff --git a/WKRKit/WKRKit/Network/WKRPeerNetwork.swift b/WKRKit/WKRKit/Network/WKRPeerNetwork.swift index 922815a..41489e3 100644 --- a/WKRKit/WKRKit/Network/WKRPeerNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRPeerNetwork.swift @@ -6,14 +6,13 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit internal protocol WKRPeerNetwork: class { var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? { get set } func disconnect() func send(object: WKRCodable) - func hostNetworkInterface() -> UIViewController? } enum WKRPeerNetworkUpdate { diff --git a/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift b/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift index 86eb796..e19d7e0 100644 --- a/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift +++ b/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift @@ -9,23 +9,20 @@ import Foundation import MultipeerConnectivity import GameKit +import WKRUIKit public enum WKRPeerNetworkConfig { case solo(name: String) - case gameKit(match: GKMatch, isHost: Bool) + case gameKitPublic(match: GKMatch, isHost: Bool) + case gameKitPrivate(match: GKMatch, isHost: Bool) case multiwindow(windowName: String, isHost: Bool) - case mpc(serviceType: String, session: MCSession, isHost: Bool) public var isHost: Bool { switch self { case .solo: return true - case .gameKit(_, let isHost): - return isHost - case .mpc(_, _, let isHost): - return isHost - case .multiwindow(_, let isHost): + case .gameKitPublic(_, let isHost), .gameKitPrivate(_, let isHost), .multiwindow(_, let isHost): return isHost } } @@ -36,12 +33,9 @@ public enum WKRPeerNetworkConfig { let profile = WKRPlayerProfile(name: name, playerID: name) let player = WKRPlayer(profile: profile, isHost: true) return (player, WKRSoloNetwork(profile: profile)) - case .gameKit(let match, let isHost): + case .gameKitPublic(let match, let isHost), .gameKitPrivate(let match, let isHost): let player = WKRPlayer(profile: GKLocalPlayer.local.wkrProfile(), isHost: isHost) return (player, WKRGameKitNetwork(match: match)) - case .mpc(let serviceType, let session, let isHost): - let player = WKRPlayer(profile: session.myPeerID.wkrProfile(), isHost: isHost) - return (player, WKRMultipeerNetwork(serviceType: serviceType, session: session)) case .multiwindow(let windowName, let isHost): let profile = WKRPlayerProfile(name: windowName, playerID: windowName) let player = WKRPlayer(profile: profile, isHost: isHost) diff --git a/WKRKit/WKRKit/Network/WKRSoloNetwork.swift b/WKRKit/WKRKit/Network/WKRSoloNetwork.swift index 274ad8b..85eeeae 100644 --- a/WKRKit/WKRKit/Network/WKRSoloNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRSoloNetwork.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit final internal class WKRSoloNetwork: WKRPeerNetwork { @@ -33,8 +33,4 @@ final internal class WKRSoloNetwork: WKRPeerNetwork { func disconnect() { } - func hostNetworkInterface() -> UIViewController? { - return nil - } - } diff --git a/WKRKit/WKRKit/Other/OSLog+WKRKit.swift b/WKRKit/WKRKit/Other/OSLog+WKRKit.swift new file mode 100644 index 0000000..aed0144 --- /dev/null +++ b/WKRKit/WKRKit/Other/OSLog+WKRKit.swift @@ -0,0 +1,28 @@ +// +// OSLog+WKRKit.swift +// WKRKit +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation +import os.log + +extension OSLog { + + // MARK: - Types - + + private enum CustomCategory: String { + case constants, seenArticlesStore + } + + private static let subsystem: String = { + guard let identifier = Bundle.main.bundleIdentifier else { fatalError() } + return identifier + }() + + static let constants = OSLog(subsystem: subsystem, category: CustomCategory.constants.rawValue) + static let seenArticlesStore = OSLog(subsystem: subsystem, category: CustomCategory.seenArticlesStore.rawValue) + +} diff --git a/WKRKit/WKRKit/Other/WKRFatalError.swift b/WKRKit/WKRKit/Other/WKRFatalError.swift index 9bdb802..fc98b82 100644 --- a/WKRKit/WKRKit/Other/WKRFatalError.swift +++ b/WKRKit/WKRKit/Other/WKRFatalError.swift @@ -26,7 +26,7 @@ public enum WKRFatalError: Int { public var message: String { switch self { case .disconnected: return "You are no longer connected to the host of the race." - case .noPeers: return "There are no other players left in the game." + case .noPeers: return "There are no other racers left in the game." case .internetSpeed: return "A fast internet connection is required to play WikiRaces." case .configCreationFailed: return "The host's internet connection was too slow to start the race." } diff --git a/WKRKit/WKRKit/Pages/WKRPageFetcher.swift b/WKRKit/WKRKit/Pages/WKRPageFetcher.swift index 5265b5a..dda02a1 100644 --- a/WKRKit/WKRKit/Pages/WKRPageFetcher.swift +++ b/WKRKit/WKRKit/Pages/WKRPageFetcher.swift @@ -30,10 +30,6 @@ public struct WKRPageFetcher { return URLSession(configuration: config) }() - static private let queue = DispatchQueue( - label: "com.andrewfinke.wikiraces.pagefetcher", - qos: .utility) - // MARK: - Helpers /// Returns the title from the raw HTML @@ -100,22 +96,19 @@ public struct WKRPageFetcher { session = WKRPageFetcher.noCacheSession } - queue.async { - var observation: NSKeyValueObservation? - let task = session.dataTask(with: url) { (data, _, error) in - observation?.invalidate() - if let data = data, let string = String(data: data, encoding: .utf8) { - completionHandler(string, nil) - } else { - completionHandler(nil, error) - } - } - observation = task.progress.observe(\.fractionCompleted) { progress, _ in - print("asdasd \(progress)") - progressHandler(Float(progress.fractionCompleted)) + var observation: NSKeyValueObservation? + let task = session.dataTask(with: url) { (data, _, error) in + observation?.invalidate() + if let data = data, let string = String(data: data, encoding: .utf8) { + completionHandler(string, nil) + } else { + completionHandler(nil, error) } - task.resume() } + observation = task.progress.observe(\.fractionCompleted) { progress, _ in + progressHandler(Float(progress.fractionCompleted)) + } + task.resume() } } diff --git a/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift b/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift index 271c7fd..3f9c32d 100644 --- a/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift +++ b/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift @@ -7,6 +7,7 @@ // import Foundation +import os.log public struct WKRSeenFinalArticlesStore { @@ -74,19 +75,25 @@ public struct WKRSeenFinalArticlesStore { } private static func resetLocalPlayerSeenFinalArticles() { + os_log("%{public}s", log: .seenArticlesStore, type: .info, #function) localPlayersSeenFinalArticles = [] } // MARK: - Remote Players public static func addRemoteTransferData(_ data: Data) { + os_log("%{public}s", log: .seenArticlesStore, type: .info, #function) guard let tranfer = try? JSONDecoder().decode(RemoteTransfer.self, from: data) else { return } + os_log("%{public}s: got %{public}ld", log: .seenArticlesStore, type: .info, #function, tranfer.articles.count) + // 1. Add new paths // 2. Remove copies from remote array that are already in local array uniqueRemotePlayersSeenFinalArticles = uniqueRemotePlayersSeenFinalArticles .union(tranfer.articles) .subtracting(localPlayersSeenFinalArticles) + + os_log("%{public}s: total remote seen %{public}ld", log: .seenArticlesStore, type: .info, #function, uniqueRemotePlayersSeenFinalArticles.count) } public static func isRemoteTransferData(_ data: Data) -> Bool { @@ -96,6 +103,7 @@ public struct WKRSeenFinalArticlesStore { public static func resetRemotePlayersSeenFinalArticles() { uniqueRemotePlayersSeenFinalArticles = [] + os_log("%{public}s", log: .seenArticlesStore, type: .info, #function) } public static func hostLogEvents() -> [WKRLogEvent] { diff --git a/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift b/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift index d529df3..44d60b0 100644 --- a/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift +++ b/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit public struct WKRResultsInfo: Codable { @@ -21,10 +21,6 @@ public struct WKRResultsInfo: Codable { // MARK: - Properties - public var playerCount: Int { - return playersSortedByState.count - } - private var playersSortedByState: [WKRPlayer]! private var playersSortedByPoints: [WKRPlayer] @@ -114,6 +110,10 @@ public struct WKRResultsInfo: Codable { // MARK: - Helpers + public mutating func minimize() { + playersSortedByState = [] + } + internal func raceRewardPoints(for player: WKRPlayer) -> Int { return racePoints[player.profile] ?? 0 } @@ -131,33 +131,47 @@ public struct WKRResultsInfo: Codable { return playersSortedByState[updatedPlayerIndex] } - public func raceRankingsPlayer(at index: Int) -> WKRPlayer { - return playersSortedByState[index] + public func player(for playerID: String) -> WKRPlayer? { + guard let updatedPlayerIndex = playersSortedByState.firstIndex(where: { $0.profile.playerID == playerID }) else { return nil } + return playersSortedByState[updatedPlayerIndex] + } + + public func raceRankings() -> [WKRPlayer] { + return playersSortedByState } - public func sessionResults(at index: Int) -> WKRProfileSessionResults { - let player = playersSortedByPoints[index] - let points = sessionPoints[player.profile] ?? 0 - - var isTied = false - var ranking = 1 - for otherPlayer in playersSortedByPoints.filter({ $0 != player }) { - let otherPlayerPoints = sessionPoints[otherPlayer.profile] ?? 0 - if otherPlayerPoints > points { - ranking += 1 - } else if otherPlayerPoints == points { - isTied = true + public func sessionResults() -> [WKRProfileSessionResults] { + var results = [WKRProfileSessionResults]() + for player in playersSortedByPoints { + let points = sessionPoints[player.profile] ?? 0 + + var isTied = false + var ranking = 1 + for otherPlayer in playersSortedByPoints.filter({ $0 != player }) { + let otherPlayerPoints = sessionPoints[otherPlayer.profile] ?? 0 + if otherPlayerPoints > points { + ranking += 1 + } else if otherPlayerPoints == points { + isTied = true + } } - } - return WKRProfileSessionResults(profile: player.profile, - points: sessionPoints[player.profile] ?? 0, - ranking: ranking, - isTied: isTied) + let item = WKRProfileSessionResults(profile: player.profile, + points: sessionPoints[player.profile] ?? 0, + ranking: ranking, + isTied: isTied) + results.append(item) + } + return results } +} - public func raceResultsPlayerProfileOrder() -> [WKRPlayerProfile] { - return playersSortedByState.map { $0.profile } - } +#if os(macOS) +extension WKRResultsInfo { + public var _playersForLiveViewer: [WKRPlayer] { + return playersSortedByPoints + } } + +#endif diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayer.swift b/WKRKit/WKRKit/Players/Single/WKRPlayer.swift index 7380501..0865564 100644 --- a/WKRKit/WKRKit/Players/Single/WKRPlayer.swift +++ b/WKRKit/WKRKit/Players/Single/WKRPlayer.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation +import WKRUIKit final public class WKRPlayer: Codable, Hashable { @@ -19,7 +19,7 @@ final public class WKRPlayer: Codable, Hashable { public internal(set) var raceHistory: WKRHistory? public internal(set) var state: WKRPlayerState = .connecting - internal let profile: WKRPlayerProfile + public let profile: WKRPlayerProfile public var name: String { return profile.name } @@ -27,7 +27,6 @@ final public class WKRPlayer: Codable, Hashable { // MARK: - Stat Properties public private(set) var stats = WKRPlayerRaceStats() - public var isCreator: Bool = false // MARK: - Initialization diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift b/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift index dced773..4c7b529 100644 --- a/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift +++ b/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift @@ -18,15 +18,15 @@ public enum WKRPlayerMessage: Int { case quit case onUSA - func text(for player: WKRPlayerProfile) -> String { + var text: String { switch self { - case .linkOnPage: return player.name + " is close" - case .foundPage: return player.name + " found the page" - case .neededHelp: return player.name + " needed help" - case .forfeited: return player.name + " forfeited" - case .quit: return player.name + " quit" - case .missedLink: return player.name + " missed the link" - case .onUSA: return player.name + " is on USA" + case .linkOnPage: return "is close" + case .foundPage: return "found the page" + case .neededHelp: return "needed help" + case .forfeited: return "forfeited" + case .quit: return "quit" + case .missedLink: return "missed the link" + case .onUSA: return "is on USA" } } diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayerProfile.swift b/WKRKit/WKRKit/Players/Single/WKRPlayerProfile.swift deleted file mode 100644 index 80ec278..0000000 --- a/WKRKit/WKRKit/Players/Single/WKRPlayerProfile.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// WKRPlayerProfile.swift -// WKRKit -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import Foundation - -public struct WKRPlayerProfile: Codable, Hashable, Equatable { - - // MARK: - Properties - - - public let name: String - public let playerID: String - - // MARK: - Initalization - - - internal init(name: String, playerID: String) { - self.name = name - self.playerID = playerID - } -} diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift b/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift index 3266b9b..475f187 100644 --- a/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift +++ b/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift @@ -36,23 +36,23 @@ public class WKRPlayerRaceStats: Codable, Equatable { func reset() { helpNeeded = 0 update(history: nil, state: .connecting, pixels: 0) - statsDictionary["Help needed"] = "0 Times" + statsDictionary["Help Needed"] = "0 Times" } func update(history: WKRHistory?, state: WKRPlayerState, pixels: Int) { - statsDictionary["Links missed"] = linksMissed(history: history, state: state) - statsDictionary["Average time per page"] = avergeTimeSpent(history: history) + statsDictionary["Links Missed"] = linksMissed(history: history, state: state) + statsDictionary["Average Time Per Page"] = avergeTimeSpent(history: history) if let formatted = WKRPlayerRaceStats.pixelFormatter.string(from: NSNumber(value: pixels)) { - statsDictionary["Distance scrolled"] = formatted + " Pixels" + statsDictionary["Distance Scrolled"] = formatted + " Pixels" } else { - statsDictionary["Distance scrolled"] = "0 Pixels" + statsDictionary["Distance Scrolled"] = "0 Pixels" } } func neededHelp() { helpNeeded += 1 - statsDictionary["Help needed"] = "\(helpNeeded) Time" + (helpNeeded == 1 ? "" : "s") + statsDictionary["Help Needed"] = "\(helpNeeded) Time" + (helpNeeded == 1 ? "" : "s") } // MARK: - Helpers - diff --git a/WKRKit/WKRKit/WKRKit.h b/WKRKit/WKRKit/WKRKit.h deleted file mode 100644 index c27969c..0000000 --- a/WKRKit/WKRKit/WKRKit.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// WKRKit.h -// WKRKit -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -#import - -//! Project version number for WKRKit. -FOUNDATION_EXPORT double WKRKitVersionNumber; - -//! Project version string for WKRKit. -FOUNDATION_EXPORT const unsigned char WKRKitVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift b/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift index 5ecb5ab..fbd5768 100644 --- a/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift +++ b/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift @@ -51,7 +51,11 @@ final internal class WKRLinkedPagesFetcher: NSObject, WKScriptMessageHandler { super.init() let config = WKWebViewConfiguration() + #if os(macOS) + let linksScript = WKUserScript(source: WKRKitConstants.current.getLinksScript(), injectionTime: .atDocumentEnd, forMainFrameOnly: false) + #else let linksScript = WKUserScript(source: WKRKitConstants.current.getLinksScript(), injectionTime: .atDocumentEnd) + #endif let messageDelegate = ScriptMessageDelegate(delegate: self) diff --git a/WKRKit/WKRKitCore/Info.plist b/WKRKit/WKRKitCore/Info.plist new file mode 100644 index 0000000..9271261 --- /dev/null +++ b/WKRKit/WKRKitCore/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Andrew Finke. All rights reserved. + + diff --git a/WKRKit/WKRKitCore/WKRKitCore.h b/WKRKit/WKRKitCore/WKRKitCore.h new file mode 100644 index 0000000..08be533 --- /dev/null +++ b/WKRKit/WKRKitCore/WKRKitCore.h @@ -0,0 +1,19 @@ +// +// WKRKitCore.h +// WKRKitCore +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +#import + +//! Project version number for WKRKitCore. +FOUNDATION_EXPORT double WKRKitCoreVersionNumber; + +//! Project version string for WKRKitCore. +FOUNDATION_EXPORT const unsigned char WKRKitCoreVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/WKRKit/WKRKitTests/WKRKitRaceTests.swift b/WKRKit/WKRKitTests/WKRKitRaceTests.swift index fc923ae..e7ea42f 100644 --- a/WKRKit/WKRKitTests/WKRKitRaceTests.swift +++ b/WKRKit/WKRKitTests/WKRKitRaceTests.swift @@ -15,7 +15,7 @@ class WKRKitRaceTests: WKRKitTestCase { let testExpectation = expectation(description: "finalPage") WKRPreRaceConfig.new(settings: WKRGameSettings()) { preRaceConfig, _ in XCTAssertNotNil(preRaceConfig) - XCTAssert(preRaceConfig?.voteInfo.pageCount == WKRKitConstants.current.votingArticlesCount) + XCTAssert(preRaceConfig?.votingState.pages.count == WKRKitConstants.current.votingArticlesCount) if let config = preRaceConfig { self.testEncoding(for: config) diff --git a/WKRKit/WKRKitTests/WKRKitTestCase.swift b/WKRKit/WKRKitTests/WKRKitTestCase.swift index 3c837f4..4d6fafd 100644 --- a/WKRKit/WKRKitTests/WKRKitTestCase.swift +++ b/WKRKit/WKRKitTests/WKRKitTestCase.swift @@ -8,13 +8,15 @@ import XCTest @testable import WKRKit +import WKRUIKit class WKRKitTestCase: XCTestCase { override func setUp() { super.setUp() + WKRKitConstants.removeConstants() WKRKitConstants.updateConstants() - let expectedVersion = 26 + let expectedVersion = 32 XCTAssertEqual(WKRKitConstants.current.version, expectedVersion, "Installed WKRKitConstants not version \(expectedVersion)") diff --git a/WKRKit/WKRKitTests/WKRKitTests.swift b/WKRKit/WKRKitTests/WKRKitTests.swift index 1c7d824..e7b0561 100644 --- a/WKRKit/WKRKitTests/WKRKitTests.swift +++ b/WKRKit/WKRKitTests/WKRKitTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import WKRKit +import WKRUIKit class WKRKitTests: WKRKitTestCase { @@ -220,7 +221,7 @@ class WKRKitTests: WKRKitTestCase { WKRKitConstants.updateConstants() let version = WKRKitConstants.current.version - XCTAssertEqual(WKRKitConstants.current.version, 26) + XCTAssertEqual(WKRKitConstants.current.version, 32) WKRKitConstants.removeConstants() WKRKitConstants.updateConstants() @@ -295,7 +296,7 @@ class WKRKitTests: WKRKitTestCase { XCTAssertNotEqual(entryWithDifPage, entryNoDuration) } - // MARK: - WKRVoteInfo + // MARK: - WKRVotingState func testVotingObject() { let page1 = WKRPage.mockApple() @@ -303,32 +304,10 @@ class WKRKitTests: WKRKitTestCase { let player = WKRPlayerProfile.mock() - var votingObject = WKRVoteInfo(pages: [page1, page2]) - - var firstPageVotes = votingObject.page(for: 0) - - XCTAssertEqual(firstPageVotes?.page, page1) - XCTAssertEqual(firstPageVotes?.votes, 0) + var votingObject = WKRVotingState(pages: [page1, page2]) votingObject.player(player, votedFor: page1) - - firstPageVotes = votingObject.page(for: 0) - - XCTAssertEqual(firstPageVotes?.page, page1) - XCTAssertEqual(firstPageVotes?.votes, 1) - votingObject.player(player, votedFor: page2) - - firstPageVotes = votingObject.page(for: 0) - - XCTAssertEqual(firstPageVotes?.page, page1) - XCTAssertEqual(firstPageVotes?.votes, 0) - - let secondPageVotes = votingObject.page(for: 1) - - XCTAssertEqual(secondPageVotes?.page, page2) - XCTAssertEqual(secondPageVotes?.votes, 1) - testEncoding(for: votingObject) } @@ -443,7 +422,7 @@ class WKRKitTests: WKRKitTestCase { func testTiebreakVoting() { let pageOne = WKRPage.mockApple(withSuffix: "One") let pageTwo = WKRPage.mockApple(withSuffix: "Two") - var vote = WKRVoteInfo(pages: [pageOne, pageTwo]) + var vote = WKRVotingState(pages: [pageOne, pageTwo]) let playerOne = WKRPlayerProfile.mock(named: "Andrew") let playerTwo = WKRPlayerProfile.mock(named: "Carol") diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer.xcodeproj/project.pbxproj b/WKRRaceLiveViewer/WKRRaceLiveViewer.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2d9de22 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer.xcodeproj/project.pbxproj @@ -0,0 +1,375 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 146E057B24AE40F0001E1917 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146E057A24AE40F0001E1917 /* AppDelegate.swift */; }; + 146E057F24AE40F1001E1917 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 146E057E24AE40F1001E1917 /* Assets.xcassets */; }; + 146E058224AE40F1001E1917 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 146E058124AE40F1001E1917 /* Preview Assets.xcassets */; }; + 146E058524AE40F1001E1917 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 146E058324AE40F1001E1917 /* Main.storyboard */; }; + 146E059124AE4108001E1917 /* WKRRaceActiveRecordWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146E058D24AE4108001E1917 /* WKRRaceActiveRecordWrapper.swift */; }; + 146E059224AE4108001E1917 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146E058F24AE4108001E1917 /* ContentView.swift */; }; + 146E059324AE4108001E1917 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146E059024AE4108001E1917 /* Model.swift */; }; + 146E059624AE427A001E1917 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 146E059524AE427A001E1917 /* CloudKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 146E057724AE40F0001E1917 /* WKRRaceLiveViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WKRRaceLiveViewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 146E057A24AE40F0001E1917 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 146E057E24AE40F1001E1917 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 146E058124AE40F1001E1917 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 146E058424AE40F1001E1917 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 146E058624AE40F1001E1917 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 146E058D24AE4108001E1917 /* WKRRaceActiveRecordWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKRRaceActiveRecordWrapper.swift; sourceTree = ""; }; + 146E058E24AE4108001E1917 /* WKRRaceLiveViewer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = WKRRaceLiveViewer.entitlements; sourceTree = ""; }; + 146E058F24AE4108001E1917 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 146E059024AE4108001E1917 /* Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; + 146E059524AE427A001E1917 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 146E057424AE40F0001E1917 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 146E059624AE427A001E1917 /* CloudKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 146E056E24AE40F0001E1917 = { + isa = PBXGroup; + children = ( + 146E057924AE40F0001E1917 /* WKRRaceLiveViewer */, + 146E057824AE40F0001E1917 /* Products */, + 146E059424AE427A001E1917 /* Frameworks */, + ); + sourceTree = ""; + }; + 146E057824AE40F0001E1917 /* Products */ = { + isa = PBXGroup; + children = ( + 146E057724AE40F0001E1917 /* WKRRaceLiveViewer.app */, + ); + name = Products; + sourceTree = ""; + }; + 146E057924AE40F0001E1917 /* WKRRaceLiveViewer */ = { + isa = PBXGroup; + children = ( + 146E057A24AE40F0001E1917 /* AppDelegate.swift */, + 146E057E24AE40F1001E1917 /* Assets.xcassets */, + 146E058324AE40F1001E1917 /* Main.storyboard */, + 146E058624AE40F1001E1917 /* Info.plist */, + 146E059024AE4108001E1917 /* Model.swift */, + 146E058F24AE4108001E1917 /* ContentView.swift */, + 146E058D24AE4108001E1917 /* WKRRaceActiveRecordWrapper.swift */, + 146E058E24AE4108001E1917 /* WKRRaceLiveViewer.entitlements */, + 146E058024AE40F1001E1917 /* Preview Content */, + ); + path = WKRRaceLiveViewer; + sourceTree = ""; + }; + 146E058024AE40F1001E1917 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 146E058124AE40F1001E1917 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 146E059424AE427A001E1917 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 146E059524AE427A001E1917 /* CloudKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 146E057624AE40F0001E1917 /* WKRRaceLiveViewer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 146E058A24AE40F1001E1917 /* Build configuration list for PBXNativeTarget "WKRRaceLiveViewer" */; + buildPhases = ( + 146E057324AE40F0001E1917 /* Sources */, + 146E057424AE40F0001E1917 /* Frameworks */, + 146E057524AE40F0001E1917 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WKRRaceLiveViewer; + productName = WKRRaceLiveViewer; + productReference = 146E057724AE40F0001E1917 /* WKRRaceLiveViewer.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 146E056F24AE40F0001E1917 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1150; + TargetAttributes = { + 146E057624AE40F0001E1917 = { + CreatedOnToolsVersion = 12.0; + }; + }; + }; + buildConfigurationList = 146E057224AE40F0001E1917 /* Build configuration list for PBXProject "WKRRaceLiveViewer" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 146E056E24AE40F0001E1917; + productRefGroup = 146E057824AE40F0001E1917 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 146E057624AE40F0001E1917 /* WKRRaceLiveViewer */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 146E057524AE40F0001E1917 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 146E058524AE40F1001E1917 /* Main.storyboard in Resources */, + 146E058224AE40F1001E1917 /* Preview Assets.xcassets in Resources */, + 146E057F24AE40F1001E1917 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 146E057324AE40F0001E1917 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 146E059324AE4108001E1917 /* Model.swift in Sources */, + 146E059124AE4108001E1917 /* WKRRaceActiveRecordWrapper.swift in Sources */, + 146E059224AE4108001E1917 /* ContentView.swift in Sources */, + 146E057B24AE40F0001E1917 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 146E058324AE40F1001E1917 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 146E058424AE40F1001E1917 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 146E058824AE40F1001E1917 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 146E058924AE40F1001E1917 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 146E058B24AE40F1001E1917 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WKRRaceLiveViewer/WKRRaceLiveViewer.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_ASSET_PATHS = "\"WKRRaceLiveViewer/Preview Content\""; + DEVELOPMENT_TEAM = 72S993BNAV; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = WKRRaceLiveViewer/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRRaceLiveViewer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 146E058C24AE40F1001E1917 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WKRRaceLiveViewer/WKRRaceLiveViewer.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_ASSET_PATHS = "\"WKRRaceLiveViewer/Preview Content\""; + DEVELOPMENT_TEAM = 72S993BNAV; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = WKRRaceLiveViewer/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRRaceLiveViewer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 146E057224AE40F0001E1917 /* Build configuration list for PBXProject "WKRRaceLiveViewer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 146E058824AE40F1001E1917 /* Debug */, + 146E058924AE40F1001E1917 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 146E058A24AE40F1001E1917 /* Build configuration list for PBXNativeTarget "WKRRaceLiveViewer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 146E058B24AE40F1001E1917 /* Debug */, + 146E058C24AE40F1001E1917 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 146E056F24AE40F0001E1917 /* Project object */; +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/AppDelegate.swift b/WKRRaceLiveViewer/WKRRaceLiveViewer/AppDelegate.swift new file mode 100644 index 0000000..e69b3f7 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// WKRRaceLiveViewer +// +// Created by Andrew Finke on 7/2/20. +// + +import Cocoa +import SwiftUI + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + var window: NSWindow! + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Create the SwiftUI view that provides the window contents. + let contentView = ContentView() + + // Create the window and set the content view. + window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + window.center() + window.setFrameAutosaveName("Main Window") + window.contentView = NSHostingView(rootView: contentView) + window.makeKeyAndOrderFront(nil) + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/AccentColor.colorset/Contents.json b/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/AppIcon.appiconset/Contents.json b/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/Contents.json b/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Base.lproj/Main.storyboard b/WKRRaceLiveViewer/WKRRaceLiveViewer/Base.lproj/Main.storyboard new file mode 100644 index 0000000..ad2fe7b --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Base.lproj/Main.storyboard @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/ContentView.swift b/WKRRaceLiveViewer/WKRRaceLiveViewer/ContentView.swift new file mode 100644 index 0000000..72300da --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/ContentView.swift @@ -0,0 +1,27 @@ +// +// ContentView.swift +// WKRRaceLiveViewer +// +// Created by Andrew Finke on 7/2/20. +// + +import SwiftUI + +struct ContentView: View { + + @ObservedObject var model = Model(raceCode: "buzzard") + + var body: some View { + VStack { + Text("\(model.host ?? "")").padding() + Text("\(model.state?.rawValue.description ?? "-")").padding() + Text("\(model.resultsInfo?._playersForLiveViewer.map({ $0.raceHistory?.entries.last?.page.title }).description ?? "-")").padding() + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Info.plist b/WKRRaceLiveViewer/WKRRaceLiveViewer/Info.plist new file mode 100644 index 0000000..cfbbdb7 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Model.swift b/WKRRaceLiveViewer/WKRRaceLiveViewer/Model.swift new file mode 100644 index 0000000..7414845 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Model.swift @@ -0,0 +1,52 @@ +// +// Model.swift +// WKRRaceLiveViewer +// +// Created by Andrew Finke on 7/2/20. +// + +import CloudKit +import WKRKitCore + +class Model: ObservableObject { + + // MARK: - Properties - + + private let raceCode: String + + @Published var host: String? + @Published var state: WKRGameState? + @Published var resultsInfo: WKRResultsInfo? + + // MARK: - Initalization - + + init(raceCode: String) { + self.raceCode = raceCode + update() + Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in + self.update() + } + } + + // MARK: - Helpers - + + func update() { + let predicate = NSPredicate(format: "Code == %@", raceCode) + let sort = NSSortDescriptor(key: "modificationDate", ascending: false) + let query = CKQuery(recordType: "RaceActive", predicate: predicate) + query.sortDescriptors = [sort] + + let operation = CKQueryOperation(query: query) + operation.resultsLimit = 1 + operation.recordFetchedBlock = { record in + let wrapper = WKRRaceActiveRecordWrapper(record: record) + DispatchQueue.main.async { + self.host = wrapper.host() + self.state = wrapper.state() + self.resultsInfo = wrapper.resultsInfo() + } + } + CKContainer(identifier: "iCloud.com.andrewfinke.wikiraces").publicCloudDatabase.add(operation) + } + +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/Preview Content/Preview Assets.xcassets/Contents.json b/WKRRaceLiveViewer/WKRRaceLiveViewer/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/WKRRaceActiveRecordWrapper.swift b/WKRRaceLiveViewer/WKRRaceLiveViewer/WKRRaceActiveRecordWrapper.swift new file mode 100644 index 0000000..c38ba14 --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/WKRRaceActiveRecordWrapper.swift @@ -0,0 +1,53 @@ +// +// WKRRaceLiveRecord.swift +// WKRRaceLiveViewer +// +// Created by Andrew Finke on 7/2/20. +// + +import CloudKit +import WKRKitCore + +struct WKRRaceActiveRecordWrapper { + + // MARK: - Types - + + private enum Key: String { + case version, state, host, code, resultsInfo + } + + // MARK: - Properties - + + private let record: CKRecord + + // MARK: - Initalization - + + init(record: CKRecord) { + self.record = record + } + + // MARK: - Helpers - + + func state() -> WKRGameState? { + guard let value = record[Key.state.rawValue.capitalized] as? Int else { return nil } + return WKRGameState(rawValue: value) + } + + func host() -> String? { + guard let value = record[Key.host.rawValue.capitalized] as? String else { return nil } + return value + } + + func code() -> String? { + guard let value = record[Key.code.rawValue.capitalized] as? String else { return nil } + return value + } + + func resultsInfo() -> WKRResultsInfo? { + guard let value = record["ResultsInfo"] as? CKAsset, + let url = value.fileURL, + let data = try? Data(contentsOf: url), + let object = try? JSONDecoder().decode(WKRResultsInfo.self, from: data) else { return nil } + return object + } +} diff --git a/WKRRaceLiveViewer/WKRRaceLiveViewer/WKRRaceLiveViewer.entitlements b/WKRRaceLiveViewer/WKRRaceLiveViewer/WKRRaceLiveViewer.entitlements new file mode 100644 index 0000000..fad366a --- /dev/null +++ b/WKRRaceLiveViewer/WKRRaceLiveViewer/WKRRaceLiveViewer.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.developer.aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.andrewfinke.wikiraces + + com.apple.developer.icloud-services + + CloudKit + + + diff --git a/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist b/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist index 795d8a8..30f5620 100644 --- a/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist +++ b/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 379 + 536 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift b/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift index cef10c8..a29e4de 100644 --- a/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift +++ b/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift @@ -32,11 +32,17 @@ class ViewController: UIViewController { ] NSLayoutConstraint.activate(constraints) - Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { (_) in - DispatchQueue.main.async { - self.title = self.webView.pixelsScrolled.description - } - } + } + + override func viewDidAppear(_ animated: Bool) { + let alertView = WKRUIAlertView() + + Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { (_) in + DispatchQueue.main.async { + alertView.enqueue(text: "IS CLOSE" + Int.random(in: 0...100000000000).description, for: nil, isRaceSpecific: true, playHaptic: false) + } + } + } } diff --git a/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj b/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj index e3f2ae1..fafd711 100644 --- a/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj +++ b/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj @@ -36,6 +36,16 @@ 14A3D4AC1F3634110038388F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 14A3D4AA1F3634110038388F /* LaunchScreen.storyboard */; }; 14A3D4B11F36342A0038388F /* WKRUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 149FF7F31F362B0D000A5D96 /* WKRUIKit.framework */; }; 14A3D4B21F36342A0038388F /* WKRUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 149FF7F31F362B0D000A5D96 /* WKRUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 14E0CA5924AE58C50091868E /* WKRUIPlayerImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA5824AE58C50091868E /* WKRUIPlayerImageManager.swift */; }; + 14E0CA5A24AE58C50091868E /* WKRUIPlayerImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA5824AE58C50091868E /* WKRUIPlayerImageManager.swift */; }; + 14E0CA5C24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA5B24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift */; }; + 14E0CA5D24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA5B24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift */; }; + 14E0CA5F24AE59450091868E /* OSLog+WKRUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA5E24AE59450091868E /* OSLog+WKRUIKit.swift */; }; + 14E0CA6024AE59450091868E /* OSLog+WKRUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA5E24AE59450091868E /* OSLog+WKRUIKit.swift */; }; + 14E0CA6224AE59930091868E /* WKRUIPlayerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA6124AE59930091868E /* WKRUIPlayerImageView.swift */; }; + 14E0CA6324AE59930091868E /* WKRUIPlayerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA6124AE59930091868E /* WKRUIPlayerImageView.swift */; }; + 14E0CA6524AE59A30091868E /* WKRPlayerProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA6424AE59A30091868E /* WKRPlayerProfile.swift */; }; + 14E0CA6624AE59A30091868E /* WKRPlayerProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0CA6424AE59A30091868E /* WKRPlayerProfile.swift */; }; 14EB9F6E1F36838B00FF1A9E /* WKRUIKit+Scripts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EB9F6D1F36838B00FF1A9E /* WKRUIKit+Scripts.swift */; }; 14FC66871F362FD4001868C4 /* WKRUIKit+Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14FC66861F362FD4001868C4 /* WKRUIKit+Fonts.swift */; }; /* End PBXBuildFile section */ @@ -95,6 +105,11 @@ 14A3D4A81F3634110038388F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14A3D4AB1F3634110038388F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 14A3D4AD1F3634110038388F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 14E0CA5824AE58C50091868E /* WKRUIPlayerImageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRUIPlayerImageManager.swift; sourceTree = ""; }; + 14E0CA5B24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRUIPlayerPlaceholderImageRenderer.swift; sourceTree = ""; }; + 14E0CA5E24AE59450091868E /* OSLog+WKRUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+WKRUIKit.swift"; sourceTree = ""; }; + 14E0CA6124AE59930091868E /* WKRUIPlayerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRUIPlayerImageView.swift; sourceTree = ""; }; + 14E0CA6424AE59A30091868E /* WKRPlayerProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRPlayerProfile.swift; sourceTree = ""; }; 14EB9F6D1F36838B00FF1A9E /* WKRUIKit+Scripts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKRUIKit+Scripts.swift"; sourceTree = ""; }; 14FC66861F362FD4001868C4 /* WKRUIKit+Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKRUIKit+Fonts.swift"; sourceTree = ""; }; 14FC668B1F3631F5001868C4 /* WKRUILabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRUILabel.swift; sourceTree = ""; }; @@ -165,6 +180,11 @@ 14A3D4B61F3634960038388F /* Elements */, 14A3D4B71F3634A20038388F /* Extensions */, 14A3D4B81F3634DF0038388F /* Views */, + 14E0CA5824AE58C50091868E /* WKRUIPlayerImageManager.swift */, + 14E0CA5B24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift */, + 14E0CA6124AE59930091868E /* WKRUIPlayerImageView.swift */, + 14E0CA6424AE59A30091868E /* WKRPlayerProfile.swift */, + 14E0CA5E24AE59450091868E /* OSLog+WKRUIKit.swift */, 149FF7F71F362B0D000A5D96 /* Info.plist */, ); path = WKRUIKit; @@ -397,20 +417,25 @@ 1441A06922FFBEBA005D63B9 /* WKRUINavigationController.swift in Sources */, 1441A06522FFBE5E005D63B9 /* WKRUIStyle.swift in Sources */, 14EB9F6E1F36838B00FF1A9E /* WKRUIKit+Scripts.swift in Sources */, + 14E0CA6524AE59A30091868E /* WKRPlayerProfile.swift in Sources */, 1413AB251F364FE400873ACA /* WKRUIButtonStyle.swift in Sources */, 1413AB201F364FBA00873ACA /* WKRUIThinLineView.swift in Sources */, 14FC66871F362FD4001868C4 /* WKRUIKit+Fonts.swift in Sources */, 1413AB211F364FBD00873ACA /* WKRUIBottomOverlayView.swift in Sources */, 1413AB2C1F36500400873ACA /* WKRUIWebView.swift in Sources */, + 14E0CA6224AE59930091868E /* WKRUIPlayerImageView.swift in Sources */, 1413AB231F364FD800873ACA /* WKRUIKitConstants.swift in Sources */, 1413AB261F364FE400873ACA /* WKRUICenteredTableView.swift in Sources */, + 14E0CA5924AE58C50091868E /* WKRUIPlayerImageManager.swift in Sources */, 1413AB241F364FE400873ACA /* WKRUIButton.swift in Sources */, 149FF8AC1F362CF6000A5D96 /* WKRUIKit+Colors.swift in Sources */, 1413AB221F364FC100873ACA /* WKRUIAlertView.swift in Sources */, 1413AB2A1F364FF100873ACA /* WKRUIProgressView.swift in Sources */, 140ADB7724628E320080F825 /* WKRUIBarButtonItem.swift in Sources */, + 14E0CA5F24AE59450091868E /* OSLog+WKRUIKit.swift in Sources */, 144C221E241BEEBD00176EB5 /* WKRUIWindow.swift in Sources */, 144C221D241BEEBD00176EB5 /* WKRUIKit+Common.swift in Sources */, + 14E0CA5C24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift in Sources */, 1413AB271F364FE400873ACA /* WKRUILabel.swift in Sources */, 1441A06722FFBE8D005D63B9 /* WKRUIKit+Blurs.swift in Sources */, ); @@ -420,6 +445,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 14E0CA5A24AE58C50091868E /* WKRUIPlayerImageManager.swift in Sources */, + 14E0CA6024AE59450091868E /* OSLog+WKRUIKit.swift in Sources */, + 14E0CA6324AE59930091868E /* WKRUIPlayerImageView.swift in Sources */, + 14E0CA5D24AE58F70091868E /* WKRUIPlayerPlaceholderImageRenderer.swift in Sources */, + 14E0CA6624AE59A30091868E /* WKRPlayerProfile.swift in Sources */, 14A3D4A41F3634110038388F /* ViewController.swift in Sources */, 14A3D4A21F3634110038388F /* AppDelegate.swift in Sources */, ); @@ -506,7 +536,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -562,7 +592,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -587,9 +617,9 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = WKRUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2020.05.1; + MARKETING_VERSION = 2020.07; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -615,9 +645,9 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = WKRUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2020.05.1; + MARKETING_VERSION = 2020.07; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -633,8 +663,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = 72S993BNAV; + ENABLE_BITCODE = NO; INFOPLIST_FILE = "WKRUIKit (UI Catalog)/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WKRUIKit--UI-Catalog-"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -649,8 +680,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = 72S993BNAV; + ENABLE_BITCODE = NO; INFOPLIST_FILE = "WKRUIKit (UI Catalog)/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WKRUIKit--UI-Catalog-"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme b/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme index 98ddd51..04a292c 100644 --- a/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme +++ b/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme @@ -1,6 +1,6 @@ Version - 17 + 22 diff --git a/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift b/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift index edb86b1..a0fc0f7 100644 --- a/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift +++ b/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift @@ -19,7 +19,9 @@ public struct WKRUIKitConstants { static let progessViewAnimateOutDelay = 0.85 static let progessViewAnimateOutDuration = 0.4 - static let alertLabelHeight: CGFloat = 30.0 + static let alertViewHeight: CGFloat = 50.0 + static let alertViewImageHeight: CGFloat = 22 + static let alertViewImagePadding: CGFloat = 5 static let alertAnimateInDuration = 0.2 static let alertAnimateOutDuration = 0.15 public static let alertDefaultDuration = 3.0 @@ -58,10 +60,10 @@ public struct WKRUIKitConstants { } guard let recordConstantsAssetURL = (record["ConstantsFile"] as? CKAsset)?.fileURL, - let recordStyleScriptAssetURL = (record["StyleScriptFile"] as? CKAsset)?.fileURL, - let recordCleanScriptAssetURL = (record["CleanScriptFile"] as? CKAsset)?.fileURL, - let recordContentBlockerAssetURL = (record["ContentBlockerFile"] as? CKAsset)?.fileURL else { - return + let recordStyleScriptAssetURL = (record["StyleScriptFile"] as? CKAsset)?.fileURL, + let recordCleanScriptAssetURL = (record["CleanScriptFile"] as? CKAsset)?.fileURL, + let recordContentBlockerAssetURL = (record["ContentBlockerFile"] as? CKAsset)?.fileURL else { + return } DispatchQueue.main.async { @@ -79,15 +81,15 @@ public struct WKRUIKitConstants { newContentBlockerFileURL: URL) { guard FileManager.default.fileExists(atPath: newConstantsFileURL.path), - FileManager.default.fileExists(atPath: newStyleScriptFileURL.path), - FileManager.default.fileExists(atPath: newCleanScriptFileURL.path), - FileManager.default.fileExists(atPath: newContentBlockerFileURL.path) else { - return + FileManager.default.fileExists(atPath: newStyleScriptFileURL.path), + FileManager.default.fileExists(atPath: newCleanScriptFileURL.path), + FileManager.default.fileExists(atPath: newContentBlockerFileURL.path) else { + return } guard let newConstants = NSDictionary(contentsOf: newConstantsFileURL), - let newConstantsVersion = newConstants["Version"] as? Int else { - return + let newConstantsVersion = newConstants["Version"] as? Int else { + return } let documentsConstantsURL = documentsPath(for: "WKRUIConstants.plist") @@ -97,8 +99,8 @@ public struct WKRUIKitConstants { var shouldReplaceExisitingConstants = true if FileManager.default.fileExists(atPath: documentsConstantsURL.path), - let documentsConstants = NSDictionary(contentsOf: documentsConstantsURL), - let documentsConstantsVersions = documentsConstants["Version"] as? Int { + let documentsConstants = NSDictionary(contentsOf: documentsConstantsURL), + let documentsConstantsVersions = documentsConstants["Version"] as? Int { if newConstantsVersion <= documentsConstantsVersions { shouldReplaceExisitingConstants = false @@ -133,12 +135,12 @@ public struct WKRUIKitConstants { static private func copyBundledResourcesToDocuments() { guard Thread.isMainThread, - let bundle = Bundle(identifier: "com.andrewfinke.WKRUIKit"), - let bundledPlistURL = bundle.url(forResource: "WKRUIConstants", withExtension: "plist"), - let bundledStyleScriptURL = bundle.url(forResource: "WKRStyleScript", withExtension: "js"), - let bundledCleanScriptURL = bundle.url(forResource: "WKRCleanScript", withExtension: "js"), - let bundledContentBlockerURL = bundle.url(forResource: "WKRContentBlocker", withExtension: "json") else { - fatalError("Failed to load bundled constants") + let bundle = Bundle(identifier: "com.andrewfinke.WKRUIKit"), + let bundledPlistURL = bundle.url(forResource: "WKRUIConstants", withExtension: "plist"), + let bundledStyleScriptURL = bundle.url(forResource: "WKRStyleScript", withExtension: "js"), + let bundledCleanScriptURL = bundle.url(forResource: "WKRCleanScript", withExtension: "js"), + let bundledContentBlockerURL = bundle.url(forResource: "WKRContentBlocker", withExtension: "json") else { + fatalError("Failed to load bundled constants") } copyIfNewer(newConstantsFileURL: bundledPlistURL, diff --git a/WKRUIKit/WKRUIKit/Elements/WKRUIStyle.swift b/WKRUIKit/WKRUIKit/Elements/WKRUIStyle.swift index 5552167..2a94822 100644 --- a/WKRUIKit/WKRUIKit/Elements/WKRUIStyle.swift +++ b/WKRUIKit/WKRUIKit/Elements/WKRUIStyle.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI final public class WKRUIStyle { public static func isDark(_ traitCollection: UITraitCollection) -> Bool { @@ -16,4 +17,12 @@ final public class WKRUIStyle { return false } } + + public static func isDark(_ colorScheme: ColorScheme) -> Bool { + if colorScheme == .dark { + return true + } else { + return false + } + } } diff --git a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Blurs.swift b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Blurs.swift index 415c0ce..aeea67d 100644 --- a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Blurs.swift +++ b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Blurs.swift @@ -9,6 +9,5 @@ import UIKit extension UIBlurEffect { - public static let wkrBlurEffect = UIBlurEffect(style: .systemThickMaterial) - public static let wkrLightBlurEffect = UIBlurEffect(style: .systemThickMaterial) + public static let wkrBlurEffect = UIBlurEffect(style: .systemThinMaterial) } diff --git a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift index bfcaeee..25d78ab 100644 --- a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift +++ b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift @@ -7,6 +7,18 @@ // import UIKit +import SwiftUI + +extension Color { + + public static func wkrTextColor(for colorScheme: ColorScheme) -> Color { + return Color(WKRUIStyle.isDark(colorScheme) ? .white : #colorLiteral(red: 54.0/255.0, green: 54.0/255.0, blue: 54.0/255.0, alpha: 1.0)) + } + + public static func wkrSubtitleTextColor(for colorScheme: ColorScheme) -> Color { + return Color(WKRUIStyle.isDark(colorScheme) ? #colorLiteral(red: 210.0/255.0, green: 210.0/255.0, blue: 210.0/255.0, alpha: 1.0) : #colorLiteral(red: 136.0/255.0, green: 136.0/255.0, blue: 136.0/255.0, alpha: 1.0)) + } +} extension UIColor { diff --git a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Common.swift b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Common.swift index 9138de0..1b30670 100644 --- a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Common.swift +++ b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Common.swift @@ -74,19 +74,6 @@ extension UIView { } } -extension SKStoreReviewController { - private static let shouldPromptForRatingKey = "ShouldPromptForRating" - - static public var shouldPromptForRating: Bool { - get { - return UserDefaults.standard.bool(forKey: shouldPromptForRatingKey) - } - set { - UserDefaults.standard.set(newValue, forKey: shouldPromptForRatingKey) - } - } -} - extension UIApplication { final public func openSettings() { guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { diff --git a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Fonts.swift b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Fonts.swift index 15f9a7c..1fa16a2 100644 --- a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Fonts.swift +++ b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Fonts.swift @@ -25,7 +25,7 @@ extension UIFont { self.init(descriptor: fontDescriptor, size: monospaceSize) } - static public func systemRoundedFont(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont { + static public func systemRoundedFont(ofSize size: CGFloat, weight: UIFont.Weight = .regular) -> UIFont { let font = UIFont.systemFont(ofSize: size, weight: weight) guard let descriptor = font.fontDescriptor.withDesign(.rounded) else { fatalError() diff --git a/WKRUIKit/WKRUIKit/Info.plist b/WKRUIKit/WKRUIKit/Info.plist index 05ae7c7..de89ff3 100644 --- a/WKRUIKit/WKRUIKit/Info.plist +++ b/WKRUIKit/WKRUIKit/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 11111 + 13977 NSPrincipalClass diff --git a/WikiRaces/Shared/Menu View Controllers/PlusViewController/OSLog+Magic.swift b/WKRUIKit/WKRUIKit/OSLog+WKRUIKit.swift similarity index 52% rename from WikiRaces/Shared/Menu View Controllers/PlusViewController/OSLog+Magic.swift rename to WKRUIKit/WKRUIKit/OSLog+WKRUIKit.swift index b71e8ad..8560782 100644 --- a/WikiRaces/Shared/Menu View Controllers/PlusViewController/OSLog+Magic.swift +++ b/WKRUIKit/WKRUIKit/OSLog+WKRUIKit.swift @@ -1,9 +1,9 @@ // -// OSLog+Magic.swift -// Magic +// OSLog+WKRUIKit.swift +// WKRUIKit // -// Created by Andrew Finke on 9/26/19. -// Copyright © 2019 Andrew Finke. All rights reserved. +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. // import Foundation @@ -14,15 +14,14 @@ extension OSLog { // MARK: - Types - private enum CustomCategory: String { - case store + case imageManager } - // MARK: - Properties - - private static let subsystem: String = { guard let identifier = Bundle.main.bundleIdentifier else { fatalError() } return identifier }() - static let store = OSLog(subsystem: subsystem, category: CustomCategory.store.rawValue) + static let imageManager = OSLog(subsystem: subsystem, category: CustomCategory.imageManager.rawValue) + } diff --git a/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift b/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift index 169ce4d..da1fbf1 100644 --- a/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift +++ b/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift @@ -7,6 +7,7 @@ // import UIKit +import GameKit final public class WKRUIAlertView: WKRUIBottomOverlayView { @@ -14,6 +15,7 @@ final public class WKRUIAlertView: WKRUIBottomOverlayView { private struct WKRAlertMessage: Equatable { let text: String + let player: WKRPlayerProfile? let duration: Double let isRaceSpecific: Bool let playHaptic: Bool @@ -28,6 +30,7 @@ final public class WKRUIAlertView: WKRUIBottomOverlayView { private var topConstraint: NSLayoutConstraint! private var isPresenting = false + private let imageView = UIImageView() // MARK: - Initalization - @@ -43,25 +46,22 @@ final public class WKRUIAlertView: WKRUIBottomOverlayView { label.textAlignment = .center label.numberOfLines = 0 - label.font = UIFont.systemFont(ofSize: 20, weight: .medium) - label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 16, weight: .medium) contentView.addSubview(label) + imageView.contentMode = .scaleAspectFit + imageView.isHidden = true + imageView.layer.cornerRadius = WKRUIKitConstants.alertViewImageHeight / 2 + imageView.clipsToBounds = true + contentView.addSubview(imageView) + topConstraint = topAnchor.constraint(equalTo: alertWindow.bottomAnchor) - let inset: CGFloat = 10 let constraints: [NSLayoutConstraint] = [ topConstraint, leftAnchor.constraint(equalTo: alertWindow.leftAnchor), rightAnchor.constraint(equalTo: alertWindow.rightAnchor), - - label.topAnchor.constraint(equalTo: topAnchor, constant: inset), - label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset - alertWindow.safeAreaInsets.bottom / 2), - - label.leftAnchor.constraint(equalTo: leftAnchor, constant: inset), - label.rightAnchor.constraint(equalTo: rightAnchor, constant: -inset), - - label.heightAnchor.constraint(greaterThanOrEqualToConstant: WKRUIKitConstants.alertLabelHeight) + heightAnchor.constraint(equalToConstant: WKRUIKitConstants.alertViewHeight + alertWindow.safeAreaInsets.bottom / 2) ] NSLayoutConstraint.activate(constraints) } @@ -80,14 +80,17 @@ final public class WKRUIAlertView: WKRUIBottomOverlayView { // MARK: - Enqueuing Messages - public func enqueue(text: String, + for player: WKRPlayerProfile?, duration: Double = WKRUIKitConstants.alertDefaultDuration, isRaceSpecific: Bool, playHaptic: Bool) { - let message = WKRAlertMessage(text: text, - duration: duration, - isRaceSpecific: isRaceSpecific, - playHaptic: playHaptic) + let message = WKRAlertMessage( + text: text, + player: player, + duration: duration, + isRaceSpecific: isRaceSpecific, + playHaptic: playHaptic) // Make sure message doesn't equal most recent in queue. // If queue empty, make sure message isn't the same as the one being displayed. @@ -121,6 +124,34 @@ final public class WKRUIAlertView: WKRUIBottomOverlayView { let message = queue.removeFirst() label.text = message.text.uppercased() + + let rect = label.attributedText?.boundingRect( + with: CGSize(width: frame.width, height: .infinity), + options: .usesLineFragmentOrigin, + context: nil) ?? bounds + + label.frame = rect + + let viewCenterY = frame.height / 2 - alertWindow.safeAreaInsets.bottom / 4 + let imageViewPadding = WKRUIKitConstants.alertViewImagePadding + let imageViewWidth = WKRUIKitConstants.alertViewImageHeight + + if let player = message.player { + imageView.isHidden = false + label.center = CGPoint(x: center.x + (imageViewWidth + imageViewPadding) / 2, y: viewCenterY) + imageView.image = player.rawImage + + imageView.frame = CGRect( + x: label.frame.minX - imageViewWidth - imageViewPadding, + y: viewCenterY - imageViewWidth / 2, + width: imageViewWidth, + height: imageViewWidth) + + } else { + imageView.isHidden = true + label.center = CGPoint(x: center.x, y: viewCenterY) + } + setNeedsLayout() layoutIfNeeded() diff --git a/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js b/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js index deb575d..f34bd42 100644 --- a/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js +++ b/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js @@ -1,65 +1,79 @@ +var pixelsScrolled = 0; +var lastPixelOffset = 0; + window.onscroll = function () { - webkit.messageHandlers.scrollY.postMessage(window.scrollY); + let scrollY = window.scrollY + pixelsScrolled += Math.abs(scrollY - lastPixelOffset); + lastPixelOffset = scrollY; +}; + +document.body.onclick = function(e){ + webkit.messageHandlers.scrollY.postMessage(pixelsScrolled); + pixelsScrolled = 0; + return true }; function cleanPage() { - console.log("WKRUIKit: cleanPage"); + console.log("WKRUIKit: cleanPage"); - // Close the sections - var headers = document.getElementsByClassName("section-heading"); - for (i = 0; i < headers.length; i++) { - if (headers[i].className.indexOf("open-block") > 0) { - headers[i].click(); + // Close the sections + if (window.location.href.indexOf("#") == -1) { + var headers = document.getElementsByClassName("section-heading"); + for (i = 0; i < headers.length; i++) { + if (headers[i].className.indexOf("open-block") > 0) { + headers[i].click(); + } + } + console.log("WKRUIKit: Closed Sections"); } - } - console.log("WKRUIKit: Closed Sections"); - // Hide dynamic tags - var tagsToRemove = ["footer"]; - for (i = 0; i < tagsToRemove.length; i++) { - try { - var elements = document.getElementsByTagName(tagsToRemove[i]); - for (var i = 0; i < elements.length; i++) { - elements[i].parentElement.removeChild(elements[i]); - } - } catch (error) { - console.log("WKRUIKit: Tag Error"); + // Hide dynamic tags + let tagsToRemove = ["footer"]; + for (i = 0; i < tagsToRemove.length; i++) { + try { + var elements = document.getElementsByTagName(tagsToRemove[i]); + for (var i = 0; i < elements.length; i++) { + elements[i].parentElement.removeChild(elements[i]); + } + } catch (error) { + console.log("WKRUIKit: Tag Error"); + } } - } - console.log("WKRUIKit: Removed Tags"); - - // Remove sections - var sectionsToRemove = ["Notes_and_references", "Sources", "Footnotes", "Bibliography", "Notes", "References", "Further_reading", "External_links", "Links"]; - for (var i = 0; i < sectionsToRemove.length; i++) { - var sectionHeaderContent = document.getElementById(sectionsToRemove[i]); + console.log("WKRUIKit: Removed Tags"); - try { - if (typeof sectionHeaderContent !== 'undefined' && sectionHeaderContent != null) { - var sectionHeader = sectionHeaderContent.parentElement; - - var sectionContentID = sectionHeader.getAttribute("aria-controls"); - var sectionContent = document.getElementById(sectionContentID); - if (typeof sectionContent !== 'undefined' && sectionContent != null) { - var sectionHeaderParent = sectionHeader.parentElement; - var sectionContentParent = sectionContent.parentElement; - if (typeof sectionHeaderParent !== 'undefined' && sectionHeaderParent != null && typeof sectionContentParent !== 'undefined' && sectionContentParent != null) { - sectionHeaderParent.removeChild(sectionHeader); - sectionContentParent.removeChild(sectionContent); - } + // Remove sections + let sectionsToRemove = [ + "Notes_and_references", + "Sources", + "Footnotes", + "Bibliography", + "Notes", + "References", + "Further_reading", + "External_links", + "Links", + ]; + for (var i = 0; i < sectionsToRemove.length; i++) { + try { + let sectionHeadlineSpanElement = document.getElementById(sectionsToRemove[i]); + let sectionID = sectionHeadlineSpanElement.getAttribute("aria-controls"); + if (sectionID != null) { + let sectionHeadingElement = sectionHeadlineSpanElement.parentElement; + let sectionSectionElement = document.getElementById(sectionID); + sectionHeadingElement.parentElement.removeChild(sectionHeadingElement); + sectionSectionElement.parentElement.removeChild(sectionSectionElement); + console.log("WKRUIKit: Removed: " + sectionsToRemove[i]); + } + } catch (error) { } - } - } catch (error) { - console.log("WKRUIKit: Section Error"); } - - } - console.log("WKRUIKit: Removed Sections"); + console.log("WKRUIKit: Removed Sections"); } -var userInteracted = false +var userInteracted = false; function setUserInteracted() { - userInteracted = true - console.log("WKRUIKit: User Interacted"); + userInteracted = true; + console.log("WKRUIKit: User Interacted"); } document.ontouchstart = setUserInteracted; document.onmousedown = setUserInteracted; @@ -67,38 +81,44 @@ window.onload = cleanPage(); var firstLength = 0; var checks = 0; -var interval = setInterval(function() { - checks += 1; - console.log("WKRUIKit: Check (" + checks.toString() + ")"); - if (firstLength == 0) { - firstLength = document.documentElement.innerHTML.length; - console.log("WKRUIKit: Got first page length (" + firstLength.toString() + ")"); - } else if (firstLength + 100 < document.documentElement.innerHTML.length) { - setTimeout(function() { cleanPage() }, 200); - clearInterval(interval); - console.log("WKRUIKit: Got longer page length (" + document.documentElement.innerHTML.length.toString() + ")"); - return; - } else if (checks > 500) { - cleanPage(); - clearInterval(interval); - console.log("WKRUIKit: Stopped checking for load due to check count"); - return; - } else if (userInteracted) { - clearInterval(interval); - console.log("WKRUIKit: Stopped checking for load due to interaction"); - return; - } +var interval = setInterval(function () { + checks += 1; + console.log("WKRUIKit: Check (" + checks.toString() + ")"); + if (firstLength == 0) { + firstLength = document.documentElement.innerHTML.length; + console.log("WKRUIKit: Got first page length (" + firstLength.toString() + ")"); + } else if (firstLength + 100 < document.documentElement.innerHTML.length) { + setTimeout(function () { + cleanPage(); + }, 200); + clearInterval(interval); + console.log("WKRUIKit: Got longer page length (" + document.documentElement.innerHTML.length.toString() + ")"); + return; + } else if (checks > 500) { + cleanPage(); + clearInterval(interval); + console.log("WKRUIKit: Stopped checking for load due to check count"); + return; + } else if (userInteracted) { + clearInterval(interval); + console.log("WKRUIKit: Stopped checking for load due to interaction"); + return; + } }, 10); - -(function(){ - var oldLog = console.log; - console.log = function (message) { - if (message.includes("Wikipedia is powered by MediaWiki. MediaWiki is open source software and we're always keen to hear from fellow developers")) { +(function () { + var oldLog = console.log; + console.log = function (message) { + if ( + message.includes( + "Wikipedia is powered by MediaWiki. MediaWiki is open source software and we're always keen to hear from fellow developers" + ) + ) { console.log("WKRUIKit: Got MediaWiki console message"); - setTimeout(function() { cleanPage() }, 100); + setTimeout(function () { + cleanPage(); + }, 100); } oldLog.apply(console, arguments); - }; - })(); - + }; +})(); diff --git a/WKRUIKit/WKRUIKit/Views/Web View/WKRStyleScript.js b/WKRUIKit/WKRUIKit/Views/Web View/WKRStyleScript.js index e1c9da3..1b2874a 100644 --- a/WKRUIKit/WKRUIKit/Views/Web View/WKRStyleScript.js +++ b/WKRUIKit/WKRUIKit/Views/Web View/WKRStyleScript.js @@ -1,4 +1,4 @@ -var styleElement = document.createElement('style'); +var styleElement = document.createElement("style"); document.documentElement.appendChild(styleElement); styleElement.textContent = ` @@ -6,7 +6,7 @@ sup { display: none !important; } a[href^="/wiki/"] { - background-color: #f5f5f5; font-weight: 500; + background-color: #f5f5f5; font-weight: 500; padding: 1px; padding-right: 4px; padding-left: 4px; border-radius: 5px; } a[href*=":"] { background-color: clear; font-weight: 400; @@ -37,7 +37,6 @@ a[href*=":"] { a[href^="/wiki/"] { background-color: rgba(0,0,0,0); color: #986400; - font-weight: 500; } body { filter: invert(1); @@ -46,5 +45,5 @@ a[href*=":"] { } `; -document.documentElement.style.webkitTouchCallout='none'; -document.documentElement.style.webkitUserSelect='none'; +document.documentElement.style.webkitTouchCallout = "none"; +document.documentElement.style.webkitUserSelect = "none"; diff --git a/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift b/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift index 214ac72..4619aae 100644 --- a/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift +++ b/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift @@ -47,7 +47,6 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { private let linkCountLabel = UILabel() private let loadingView = UIView() - private let slowConnectionLabel = UILabel() public var progressView: WKRUIProgressView? { didSet { @@ -56,7 +55,6 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { } public private(set) var pixelsScrolled = 0 - private var lastPixelOffset = 0 // network progress (fetch raw html) vs render progress (load html + images) private static let networkProgressWeight: Float = 0.7 @@ -93,7 +91,9 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { .typeIdentifier: kMonospacedNumbersSelector ] ] - let fontDescriptor = UIFont.systemFont(ofSize: 100, weight: .semibold).fontDescriptor.addingAttributes( + let fontDescriptor = UIFont.systemFont(ofSize: 100, weight: .medium) + .fontDescriptor + .addingAttributes( [UIFontDescriptor.AttributeName.featureSettings: features] ) @@ -112,18 +112,6 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { linkCountLabel.translatesAutoresizingMaskIntoConstraints = false loadingView.addSubview(linkCountLabel) - slowConnectionLabel.text = "IF YOU SEE THIS FOR > 10 SECONDS, PLEASE LMK." - slowConnectionLabel.textColor = .white - slowConnectionLabel.textAlignment = .center - slowConnectionLabel.numberOfLines = 0 - - slowConnectionLabel.adjustsFontSizeToFitWidth = true - slowConnectionLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) - slowConnectionLabel.translatesAutoresizingMaskIntoConstraints = false - loadingView.addSubview(slowConnectionLabel) - - slowConnectionLabel.isHidden = true // only show during development - scrollView.decelerationRate = UIScrollView.DecelerationRate.normal let constraints = [ @@ -135,12 +123,7 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { linkCountLabel.topAnchor.constraint(equalTo: loadingView.topAnchor), linkCountLabel.bottomAnchor.constraint(equalTo: loadingView.bottomAnchor), linkCountLabel.leftAnchor.constraint(equalTo: loadingView.leftAnchor), - linkCountLabel.rightAnchor.constraint(equalTo: loadingView.rightAnchor), - - slowConnectionLabel.bottomAnchor.constraint(equalTo: loadingView.safeAreaLayoutGuide.bottomAnchor, - constant: -20), - slowConnectionLabel.leftAnchor.constraint(equalTo: loadingView.leftAnchor), - slowConnectionLabel.rightAnchor.constraint(equalTo: loadingView.rightAnchor) + linkCountLabel.rightAnchor.constraint(equalTo: loadingView.rightAnchor) ] NSLayoutConstraint.activate(constraints) @@ -180,7 +163,6 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { public func startedPageLoad() { progressView?.show() - lastPixelOffset = 0 isUserInteractionEnabled = false let duration = WKRUIKitConstants.webViewAnimateOutDuration @@ -246,8 +228,7 @@ final public class WKRUIWebView: WKWebView, WKScriptMessageHandler { guard let messageBody = message.body as? Int else { return } switch message.name { case "scrollY": - pixelsScrolled += abs(messageBody - lastPixelOffset) - lastPixelOffset = messageBody + pixelsScrolled += messageBody default: return } } diff --git a/WKRUIKit/WKRUIKit/WKRPlayerProfile.swift b/WKRUIKit/WKRUIKit/WKRPlayerProfile.swift new file mode 100644 index 0000000..b069b54 --- /dev/null +++ b/WKRUIKit/WKRUIKit/WKRPlayerProfile.swift @@ -0,0 +1,41 @@ +// +// WKRPlayerProfile.swift +// WKRUIKit +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import SwiftUI +import UIKit + +public struct WKRPlayerProfile: Identifiable, Equatable, Hashable, Codable { + + // MARK: - Properties - + + public var id: String { + return playerID + } + public let name: String + public let playerID: String + + public var image: Image { Image(uiImage: rawImage) } + public var rawImage: UIImage { WKRUIPlayerImageManager.shared.image(for: id) } + + // MARK: - Initalization - + + public init(name: String, playerID: String) { + self.name = name + self.playerID = playerID + } + + public init(player: GKPlayer) { + self.name = player.displayName + self.playerID = player.alias + } + + public static func ==(lhs: WKRPlayerProfile, rhs: WKRPlayerProfile) -> Bool { + return lhs.playerID == rhs.playerID + } +} diff --git a/WKRUIKit/WKRUIKit/WKRUIPlayerImageManager.swift b/WKRUIKit/WKRUIKit/WKRUIPlayerImageManager.swift new file mode 100644 index 0000000..fe9696d --- /dev/null +++ b/WKRUIKit/WKRUIKit/WKRUIPlayerImageManager.swift @@ -0,0 +1,100 @@ +// +// WKRUIPlayerImageManager.swift +// WKRUIKit +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation +import GameKit +import SwiftUI +import os.log + +public class WKRUIPlayerImageManager { + + // MARK: - Types - + + public struct Container: Codable { + let items: [String: Data] + + init(connectedPlayerImages: [String: UIImage], localPlayerImage: UIImage) { + var mapped = [String: Data]() + func add(image: UIImage, for playerID: String) { + mapped[playerID] = image.jpegData(compressionQuality: 0.7) + } + connectedPlayerImages.forEach { add(image: $0.value, for: $0.key) } + add(image: localPlayerImage, for: GKLocalPlayer.local.alias) + self.items = mapped + } + } + + // MARK: - Properties - + + public static var shared = WKRUIPlayerImageManager() + + private var connectedPlayerImages = [String: UIImage]() + private var localPlayerImage: UIImage? + + public private(set) var isLocalPlayerImageFromGameCenter = false + + // MARK: - Initalization - + + private init() {} + + // MARK: - Helpers - + + public func connected(to player: GKPlayer, completion: (() -> Void)?) { + DispatchQueue.main.async { + let placeholder = WKRUIPlayerPlaceholderImageRenderer.render(name: player.displayName) + os_log("%{public}s: generated placeholder for %{public}s", log: .imageManager, type: .info, #function, player.alias) + + self.update(image: placeholder, for: player.alias) + + player.loadPhoto(for: .small) { photo, _ in + guard let photo = photo else { + os_log("%{public}s: load photo failed for %{public}s", log: .imageManager, type: .error, #function, player.alias) + completion?() + return + } + os_log("%{public}s: load photo success for %{public}s", log: .imageManager, type: .info, #function, player.alias) + + if player == GKLocalPlayer.local { + self.isLocalPlayerImageFromGameCenter = true + } + + self.update(image: photo, for: player.alias) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + completion?() + } + } + } + } + + private func update(image: UIImage, for playerID: String) { + if playerID == GKLocalPlayer.local.alias { + localPlayerImage = image + } else { + connectedPlayerImages[playerID] = image + } + } + + public func image(for playerID: String) -> UIImage { + if playerID == GKLocalPlayer.local.alias, let image = localPlayerImage { + return image + } else if let image = connectedPlayerImages[playerID] { + return image + } else { + fatalError() + } + } + + public func clearConnectedPlayers() { + connectedPlayerImages.removeAll() + } + + public func container() -> Container { + guard let image = localPlayerImage else { fatalError() } + return Container(connectedPlayerImages: connectedPlayerImages, localPlayerImage: image) + } +} diff --git a/WKRUIKit/WKRUIKit/WKRUIPlayerImageView.swift b/WKRUIKit/WKRUIKit/WKRUIPlayerImageView.swift new file mode 100644 index 0000000..0374b1d --- /dev/null +++ b/WKRUIKit/WKRUIKit/WKRUIPlayerImageView.swift @@ -0,0 +1,35 @@ +// +// WKRUIPlayerImageView.swift +// WKRUIKit +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI + +public struct WKRUIPlayerImageView: View { + + // MARK: - Properties - + + let player: WKRPlayerProfile + let size: CGFloat + let effectSize: CGFloat + + // MARK: - Body - + + public var body: some View { + Image(uiImage: WKRUIPlayerImageManager.shared.image(for: player.playerID)) + .renderingMode(.original) + .resizable() + .frame(width: size, height: size) + .clipShape(Circle()) + .shadow(radius: effectSize) + } + + public init(player: WKRPlayerProfile, size: CGFloat, effectSize: CGFloat) { + self.player = player + self.size = size + self.effectSize = effectSize + } +} diff --git a/WKRUIKit/WKRUIKit/WKRUIPlayerPlaceholderImageRenderer.swift b/WKRUIKit/WKRUIKit/WKRUIPlayerPlaceholderImageRenderer.swift new file mode 100644 index 0000000..4221d80 --- /dev/null +++ b/WKRUIKit/WKRUIKit/WKRUIPlayerPlaceholderImageRenderer.swift @@ -0,0 +1,61 @@ +// +// WKRUIPlayerPlaceholderImageRenderer.swift +// WKRUIKit +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import UIKit + +struct WKRUIPlayerPlaceholderImageRenderer { + + static private let colors: [UIColor] = [ + .systemRed, + .systemGreen, + .systemBlue, + .systemOrange, + .systemPink, + .systemPurple, + .systemTeal, + .systemIndigo + ] + + static func render(name: String) -> UIImage { + let imageView = UIView(frame: CGRect(x: -100, y: -100, width: 100, height: 100)) + imageView.clipsToBounds = true + + let gradient = CAGradientLayer() + gradient.frame = imageView.bounds + gradient.startPoint = CGPoint(x: 0, y: 0) + gradient.endPoint = CGPoint(x: 1, y: 1) + + let shuffledColors = colors.shuffled() + gradient.colors = [ + shuffledColors[0].cgColor, + shuffledColors[1].cgColor + ] + imageView.layer.insertSublayer(gradient, at: 0) + imageView.layer.cornerRadius = imageView.bounds.width / 2 + + let label = UILabel(frame: imageView.bounds) + label.text = name.first?.uppercased() ?? "-" + label.textColor = .white + label.textAlignment = .center + label.font = UIFont.systemRoundedFont(ofSize: 56, weight: .semibold) + imageView.addSubview(label) + + let format = UIGraphicsImageRendererFormat() + format.opaque = false + format.preferredRange = .automatic + + guard let view = UIApplication.shared.windows.first else { return UIImage() } + view.addSubview(imageView) + let renderer = UIGraphicsImageRenderer(size: imageView.bounds.size, format: format) + let image = renderer.image { ctx in + imageView.layer.render(in: ctx.cgContext) + } + imageView.removeFromSuperview() + return image + } +} diff --git a/WikiRaces.xcworkspace/contents.xcworkspacedata b/WikiRaces.xcworkspace/contents.xcworkspacedata index 4c1db11..693f29d 100644 --- a/WikiRaces.xcworkspace/contents.xcworkspacedata +++ b/WikiRaces.xcworkspace/contents.xcworkspacedata @@ -16,6 +16,9 @@ + + diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics deleted file mode 100755 index 214fb90..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/ANSCompatibility.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/ANSCompatibility.h deleted file mode 100644 index 6ec011d..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/ANSCompatibility.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// ANSCompatibility.h -// AnswersKit -// -// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. -// - -#pragma once - -#if !__has_feature(nullability) -#define nonnull -#define nullable -#define _Nullable -#define _Nonnull -#endif - -#ifndef NS_ASSUME_NONNULL_BEGIN -#define NS_ASSUME_NONNULL_BEGIN -#endif - -#ifndef NS_ASSUME_NONNULL_END -#define NS_ASSUME_NONNULL_END -#endif - -#if __has_feature(objc_generics) -#define ANS_GENERIC_NSARRAY(type) NSArray -#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary -#else -#define ANS_GENERIC_NSARRAY(type) NSArray -#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary -#endif diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/Answers.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/Answers.h deleted file mode 100644 index 8deacbe..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/Answers.h +++ /dev/null @@ -1,210 +0,0 @@ -// -// Answers.h -// Crashlytics -// -// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. -// - -#import -#import "ANSCompatibility.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * This class exposes the Answers Events API, allowing you to track key - * user user actions and metrics in your app. - */ -@interface Answers : NSObject - -/** - * Log a Sign Up event to see users signing up for your app in real-time, understand how - * many users are signing up with different methods and their success rate signing up. - * - * @param signUpMethodOrNil The method by which a user logged in, e.g. Twitter or Digits. - * @param signUpSucceededOrNil The ultimate success or failure of the login - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logSignUpWithMethod:(nullable NSString *)signUpMethodOrNil - success:(nullable NSNumber *)signUpSucceededOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log an Log In event to see users logging into your app in real-time, understand how many - * users are logging in with different methods and their success rate logging into your app. - * - * @param loginMethodOrNil The method by which a user logged in, e.g. email, Twitter or Digits. - * @param loginSucceededOrNil The ultimate success or failure of the login - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logLoginWithMethod:(nullable NSString *)loginMethodOrNil - success:(nullable NSNumber *)loginSucceededOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Share event to see users sharing from your app in real-time, letting you - * understand what content they're sharing from the type or genre down to the specific id. - * - * @param shareMethodOrNil The method by which a user shared, e.g. email, Twitter, SMS. - * @param contentNameOrNil The human readable name for this piece of content. - * @param contentTypeOrNil The type of content shared. - * @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logShareWithMethod:(nullable NSString *)shareMethodOrNil - contentName:(nullable NSString *)contentNameOrNil - contentType:(nullable NSString *)contentTypeOrNil - contentId:(nullable NSString *)contentIdOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log an Invite Event to track how users are inviting other users into - * your application. - * - * @param inviteMethodOrNil The method of invitation, e.g. GameCenter, Twitter, email. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logInviteWithMethod:(nullable NSString *)inviteMethodOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Purchase event to see your revenue in real-time, understand how many users are making purchases, see which - * items are most popular, and track plenty of other important purchase-related metrics. - * - * @param itemPriceOrNil The purchased item's price. - * @param currencyOrNil The ISO4217 currency code. Example: USD - * @param purchaseSucceededOrNil Was the purchase successful or unsuccessful - * @param itemNameOrNil The human-readable form of the item's name. Example: - * @param itemTypeOrNil The type, or genre of the item. Example: Song - * @param itemIdOrNil The machine-readable, unique item identifier Example: SKU - * @param customAttributesOrNil A dictionary of custom attributes to associate with this purchase. - */ -+ (void)logPurchaseWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil - currency:(nullable NSString *)currencyOrNil - success:(nullable NSNumber *)purchaseSucceededOrNil - itemName:(nullable NSString *)itemNameOrNil - itemType:(nullable NSString *)itemTypeOrNil - itemId:(nullable NSString *)itemIdOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Level Start Event to track where users are in your game. - * - * @param levelNameOrNil The level name - * @param customAttributesOrNil A dictionary of custom attributes to associate with this level start event. - */ -+ (void)logLevelStart:(nullable NSString *)levelNameOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Level End event to track how users are completing levels in your game. - * - * @param levelNameOrNil The name of the level completed, E.G. "1" or "Training" - * @param scoreOrNil The score the user completed the level with. - * @param levelCompletedSuccesfullyOrNil A boolean representing whether or not the level was completed successfully. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logLevelEnd:(nullable NSString *)levelNameOrNil - score:(nullable NSNumber *)scoreOrNil - success:(nullable NSNumber *)levelCompletedSuccesfullyOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log an Add to Cart event to see users adding items to a shopping cart in real-time, understand how - * many users start the purchase flow, see which items are most popular, and track plenty of other important - * purchase-related metrics. - * - * @param itemPriceOrNil The purchased item's price. - * @param currencyOrNil The ISO4217 currency code. Example: USD - * @param itemNameOrNil The human-readable form of the item's name. Example: - * @param itemTypeOrNil The type, or genre of the item. Example: Song - * @param itemIdOrNil The machine-readable, unique item identifier Example: SKU - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logAddToCartWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil - currency:(nullable NSString *)currencyOrNil - itemName:(nullable NSString *)itemNameOrNil - itemType:(nullable NSString *)itemTypeOrNil - itemId:(nullable NSString *)itemIdOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Start Checkout event to see users moving through the purchase funnel in real-time, understand how many - * users are doing this and how much they're spending per checkout, and see how it related to other important - * purchase-related metrics. - * - * @param totalPriceOrNil The total price of the cart. - * @param currencyOrNil The ISO4217 currency code. Example: USD - * @param itemCountOrNil The number of items in the cart. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logStartCheckoutWithPrice:(nullable NSDecimalNumber *)totalPriceOrNil - currency:(nullable NSString *)currencyOrNil - itemCount:(nullable NSNumber *)itemCountOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Rating event to see users rating content within your app in real-time and understand what - * content is most engaging, from the type or genre down to the specific id. - * - * @param ratingOrNil The integer rating given by the user. - * @param contentNameOrNil The human readable name for this piece of content. - * @param contentTypeOrNil The type of content shared. - * @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logRating:(nullable NSNumber *)ratingOrNil - contentName:(nullable NSString *)contentNameOrNil - contentType:(nullable NSString *)contentTypeOrNil - contentId:(nullable NSString *)contentIdOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Content View event to see users viewing content within your app in real-time and - * understand what content is most engaging, from the type or genre down to the specific id. - * - * @param contentNameOrNil The human readable name for this piece of content. - * @param contentTypeOrNil The type of content shared. - * @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logContentViewWithName:(nullable NSString *)contentNameOrNil - contentType:(nullable NSString *)contentTypeOrNil - contentId:(nullable NSString *)contentIdOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Search event allows you to see users searching within your app in real-time and understand - * exactly what they're searching for. - * - * @param queryOrNil The user's query. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. - */ -+ (void)logSearchWithQuery:(nullable NSString *)queryOrNil - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -/** - * Log a Custom Event to see user actions that are uniquely important for your app in real-time, to see how often - * they're performing these actions with breakdowns by different categories you add. Use a human-readable name for - * the name of the event, since this is how the event will appear in Answers. - * - * @param eventName The human-readable name for the event. - * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. Attribute keys - * must be NSString and values must be NSNumber or NSString. - * @discussion How we treat NSNumbers: - * We will provide information about the distribution of values over time. - * - * How we treat NSStrings: - * NSStrings are used as categorical data, allowing comparison across different category values. - * Strings are limited to a maximum length of 100 characters, attributes over this length will be - * truncated. - * - * When tracking the Tweet views to better understand user engagement, sending the tweet's length - * and the type of media present in the tweet allows you to track how tweet length and the type of media influence - * engagement. - */ -+ (void)logCustomEventWithName:(NSString *)eventName - customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSAttributes.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSAttributes.h deleted file mode 100644 index 1526b0d..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSAttributes.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// CLSAttributes.h -// Crashlytics -// -// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. -// - -#pragma once - -#define CLS_DEPRECATED(x) __attribute__ ((deprecated(x))) - -#if !__has_feature(nullability) - #define nonnull - #define nullable - #define _Nullable - #define _Nonnull -#endif - -#ifndef NS_ASSUME_NONNULL_BEGIN - #define NS_ASSUME_NONNULL_BEGIN -#endif - -#ifndef NS_ASSUME_NONNULL_END - #define NS_ASSUME_NONNULL_END -#endif - -#if __has_feature(objc_generics) - #define CLS_GENERIC_NSARRAY(type) NSArray - #define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary -#else - #define CLS_GENERIC_NSARRAY(type) NSArray - #define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary -#endif diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSLogging.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSLogging.h deleted file mode 100644 index 59590d5..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSLogging.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// CLSLogging.h -// Crashlytics -// -// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. -// -#ifdef __OBJC__ -#import "CLSAttributes.h" -#import - -NS_ASSUME_NONNULL_BEGIN -#endif - - - -/** - * - * The CLS_LOG macro provides as easy way to gather more information in your log messages that are - * sent with your crash data. CLS_LOG prepends your custom log message with the function name and - * line number where the macro was used. If your app was built with the DEBUG preprocessor macro - * defined CLS_LOG uses the CLSNSLog function which forwards your log message to NSLog and CLSLog. - * If the DEBUG preprocessor macro is not defined CLS_LOG uses CLSLog only. - * - * Example output: - * -[AppDelegate login:] line 134 $ login start - * - * If you would like to change this macro, create a new header file, unset our define and then define - * your own version. Make sure this new header file is imported after the Crashlytics header file. - * - * #undef CLS_LOG - * #define CLS_LOG(__FORMAT__, ...) CLSNSLog... - * - **/ -#ifdef __OBJC__ -#ifdef DEBUG -#define CLS_LOG(__FORMAT__, ...) CLSNSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) -#else -#define CLS_LOG(__FORMAT__, ...) CLSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) -#endif -#endif - -/** - * - * Add logging that will be sent with your crash data. This logging will not show up in the system.log - * and will only be visible in your Crashlytics dashboard. - * - **/ - -#ifdef __OBJC__ -OBJC_EXTERN void CLSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2); -OBJC_EXTERN void CLSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0); - -/** - * - * Add logging that will be sent with your crash data. This logging will show up in the system.log - * and your Crashlytics dashboard. It is not recommended for Release builds. - * - **/ -OBJC_EXTERN void CLSNSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2); -OBJC_EXTERN void CLSNSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0); - - -NS_ASSUME_NONNULL_END -#endif diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSReport.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSReport.h deleted file mode 100644 index a8ff3b0..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSReport.h +++ /dev/null @@ -1,103 +0,0 @@ -// -// CLSReport.h -// Crashlytics -// -// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. -// - -#import -#import "CLSAttributes.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * The CLSCrashReport protocol is deprecated. See the CLSReport class and the CrashyticsDelegate changes for details. - **/ -@protocol CLSCrashReport - -@property (nonatomic, copy, readonly) NSString *identifier; -@property (nonatomic, copy, readonly) NSDictionary *customKeys; -@property (nonatomic, copy, readonly) NSString *bundleVersion; -@property (nonatomic, copy, readonly) NSString *bundleShortVersionString; -@property (nonatomic, readonly, nullable) NSDate *crashedOnDate; -@property (nonatomic, copy, readonly) NSString *OSVersion; -@property (nonatomic, copy, readonly) NSString *OSBuildVersion; - -@end - -/** - * The CLSReport exposes an interface to the phsyical report that Crashlytics has created. You can - * use this class to get information about the event, and can also set some values after the - * event has occurred. - **/ -@interface CLSReport : NSObject - -- (instancetype)init NS_UNAVAILABLE; -+ (instancetype)new NS_UNAVAILABLE; - -/** - * Returns the session identifier for the report. - **/ -@property (nonatomic, copy, readonly) NSString *identifier; - -/** - * Returns the custom key value data for the report. - **/ -@property (nonatomic, copy, readonly) NSDictionary *customKeys; - -/** - * Returns the CFBundleVersion of the application that generated the report. - **/ -@property (nonatomic, copy, readonly) NSString *bundleVersion; - -/** - * Returns the CFBundleShortVersionString of the application that generated the report. - **/ -@property (nonatomic, copy, readonly) NSString *bundleShortVersionString; - -/** - * Returns the date that the report was created. - **/ -@property (nonatomic, copy, readonly) NSDate *dateCreated; - -/** - * Returns the os version that the application crashed on. - **/ -@property (nonatomic, copy, readonly) NSString *OSVersion; - -/** - * Returns the os build version that the application crashed on. - **/ -@property (nonatomic, copy, readonly) NSString *OSBuildVersion; - -/** - * Returns YES if the report contains any crash information, otherwise returns NO. - **/ -@property (nonatomic, assign, readonly) BOOL isCrash; - -/** - * You can use this method to set, after the event, additional custom keys. The rules - * and semantics for this method are the same as those documented in Crashlytics.h. Be aware - * that the maximum size and count of custom keys is still enforced, and you can overwrite keys - * and/or cause excess keys to be deleted by using this method. - **/ -- (void)setObjectValue:(nullable id)value forKey:(NSString *)key; - -/** - * Record an application-specific user identifier. See Crashlytics.h for details. - **/ -@property (nonatomic, copy, nullable) NSString * userIdentifier; - -/** - * Record a user name. See Crashlytics.h for details. - **/ -@property (nonatomic, copy, nullable) NSString * userName; - -/** - * Record a user email. See Crashlytics.h for details. - **/ -@property (nonatomic, copy, nullable) NSString * userEmail; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSStackFrame.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSStackFrame.h deleted file mode 100644 index cdb5596..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/CLSStackFrame.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// CLSStackFrame.h -// Crashlytics -// -// Copyright 2015 Crashlytics, Inc. All rights reserved. -// - -#import -#import "CLSAttributes.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * - * This class is used in conjunction with -[Crashlytics recordCustomExceptionName:reason:frameArray:] to - * record information about non-ObjC/C++ exceptions. All information included here will be displayed - * in the Crashlytics UI, and can influence crash grouping. Be particularly careful with the use of the - * address property. If set, Crashlytics will attempt symbolication and could overwrite other properities - * in the process. - * - **/ -@interface CLSStackFrame : NSObject - -+ (instancetype)stackFrame; -+ (instancetype)stackFrameWithAddress:(NSUInteger)address; -+ (instancetype)stackFrameWithSymbol:(NSString *)symbol; - -@property (nonatomic, copy, nullable) NSString *symbol; -@property (nonatomic, copy, nullable) NSString *rawSymbol; -@property (nonatomic, copy, nullable) NSString *library; -@property (nonatomic, copy, nullable) NSString *fileName; -@property (nonatomic, assign) uint32_t lineNumber; -@property (nonatomic, assign) uint64_t offset; -@property (nonatomic, assign) uint64_t address; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/Crashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/Crashlytics.h deleted file mode 100644 index 7104ca8..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Headers/Crashlytics.h +++ /dev/null @@ -1,288 +0,0 @@ -// -// Crashlytics.h -// Crashlytics -// -// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. -// - -#import - -#import "CLSAttributes.h" -#import "CLSLogging.h" -#import "CLSReport.h" -#import "CLSStackFrame.h" -#import "Answers.h" - -NS_ASSUME_NONNULL_BEGIN - -@protocol CrashlyticsDelegate; - -/** - * Crashlytics. Handles configuration and initialization of Crashlytics. - * - * Note: The Crashlytics class cannot be subclassed. If this is causing you pain for - * testing, we suggest using either a wrapper class or a protocol extension. - */ -@interface Crashlytics : NSObject - -@property (nonatomic, readonly, copy) NSString *APIKey; -@property (nonatomic, readonly, copy) NSString *version; -@property (nonatomic, assign) BOOL debugMode; - -/** - * - * The delegate can be used to influence decisions on reporting and behavior, as well as reacting - * to previous crashes. - * - * Make certain that the delegate is setup before starting Crashlytics with startWithAPIKey:... or - * via +[Fabric with:...]. Failure to do will result in missing any delegate callbacks that occur - * synchronously during start. - * - **/ -@property (nonatomic, assign, nullable) id delegate; - -/** - * The recommended way to install Crashlytics into your application is to place a call to +startWithAPIKey: - * in your -application:didFinishLaunchingWithOptions: or -applicationDidFinishLaunching: - * method. - * - * Note: Starting with 3.0, the submission process has been significantly improved. The delay parameter - * is no longer required to throttle submissions on launch, performance will be great without it. - * - * @param apiKey The Crashlytics API Key for this app - * - * @return The singleton Crashlytics instance - */ -+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey; -+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey afterDelay:(NSTimeInterval)delay CLS_DEPRECATED("Crashlytics no longer needs or uses the delay parameter. Please use +startWithAPIKey: instead."); - -/** - * If you need the functionality provided by the CrashlyticsDelegate protocol, you can use - * these convenience methods to activate the framework and set the delegate in one call. - * - * @param apiKey The Crashlytics API Key for this app - * @param delegate A delegate object which conforms to CrashlyticsDelegate. - * - * @return The singleton Crashlytics instance - */ -+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(nullable id)delegate; -+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(nullable id)delegate afterDelay:(NSTimeInterval)delay CLS_DEPRECATED("Crashlytics no longer needs or uses the delay parameter. Please use +startWithAPIKey:delegate: instead."); - -/** - * Access the singleton Crashlytics instance. - * - * @return The singleton Crashlytics instance - */ -+ (Crashlytics *)sharedInstance; - -/** - * The easiest way to cause a crash - great for testing! - */ -- (void)crash; - -/** - * The easiest way to cause a crash with an exception - great for testing. - */ -- (void)throwException; - -/** - * Specify a user identifier which will be visible in the Crashlytics UI. - * - * Many of our customers have requested the ability to tie crashes to specific end-users of their - * application in order to facilitate responses to support requests or permit the ability to reach - * out for more information. We allow you to specify up to three separate values for display within - * the Crashlytics UI - but please be mindful of your end-user's privacy. - * - * We recommend specifying a user identifier - an arbitrary string that ties an end-user to a record - * in your system. This could be a database id, hash, or other value that is meaningless to a - * third-party observer but can be indexed and queried by you. - * - * Optionally, you may also specify the end-user's name or username, as well as email address if you - * do not have a system that works well with obscured identifiers. - * - * Pursuant to our EULA, this data is transferred securely throughout our system and we will not - * disseminate end-user data unless required to by law. That said, if you choose to provide end-user - * contact information, we strongly recommend that you disclose this in your application's privacy - * policy. Data privacy is of our utmost concern. - * - * @param identifier An arbitrary user identifier string which ties an end-user to a record in your system. - */ -- (void)setUserIdentifier:(nullable NSString *)identifier; - -/** - * Specify a user name which will be visible in the Crashlytics UI. - * Please be mindful of your end-user's privacy and see if setUserIdentifier: can fulfil your needs. - * @see setUserIdentifier: - * - * @param name An end user's name. - */ -- (void)setUserName:(nullable NSString *)name; - -/** - * Specify a user email which will be visible in the Crashlytics UI. - * Please be mindful of your end-user's privacy and see if setUserIdentifier: can fulfil your needs. - * - * @see setUserIdentifier: - * - * @param email An end user's email address. - */ -- (void)setUserEmail:(nullable NSString *)email; - -+ (void)setUserIdentifier:(nullable NSString *)identifier CLS_DEPRECATED("Please access this method via +sharedInstance"); -+ (void)setUserName:(nullable NSString *)name CLS_DEPRECATED("Please access this method via +sharedInstance"); -+ (void)setUserEmail:(nullable NSString *)email CLS_DEPRECATED("Please access this method via +sharedInstance"); - -/** - * Set a value for a for a key to be associated with your crash data which will be visible in the Crashlytics UI. - * When setting an object value, the object is converted to a string. This is typically done by calling - * -[NSObject description]. - * - * @param value The object to be associated with the key - * @param key The key with which to associate the value - */ -- (void)setObjectValue:(nullable id)value forKey:(NSString *)key; - -/** - * Set an int value for a key to be associated with your crash data which will be visible in the Crashlytics UI. - * - * @param value The integer value to be set - * @param key The key with which to associate the value - */ -- (void)setIntValue:(int)value forKey:(NSString *)key; - -/** - * Set an BOOL value for a key to be associated with your crash data which will be visible in the Crashlytics UI. - * - * @param value The BOOL value to be set - * @param key The key with which to associate the value - */ -- (void)setBoolValue:(BOOL)value forKey:(NSString *)key; - -/** - * Set an float value for a key to be associated with your crash data which will be visible in the Crashlytics UI. - * - * @param value The float value to be set - * @param key The key with which to associate the value - */ -- (void)setFloatValue:(float)value forKey:(NSString *)key; - -+ (void)setObjectValue:(nullable id)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance"); -+ (void)setIntValue:(int)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance"); -+ (void)setBoolValue:(BOOL)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance"); -+ (void)setFloatValue:(float)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance"); - -/** - * This method can be used to record a single exception structure in a report. This is particularly useful - * when your code interacts with non-native languages like Lua, C#, or Javascript. This call can be - * expensive and should only be used shortly before process termination. This API is not intended be to used - * to log NSException objects. All safely-reportable NSExceptions are automatically captured by - * Crashlytics. - * - * @param name The name of the custom exception - * @param reason The reason this exception occurred - * @param frameArray An array of CLSStackFrame objects - */ -- (void)recordCustomExceptionName:(NSString *)name reason:(nullable NSString *)reason frameArray:(CLS_GENERIC_NSARRAY(CLSStackFrame *) *)frameArray; - -/** - * - * This allows you to record a non-fatal event, described by an NSError object. These events will be grouped and - * displayed similarly to crashes. Keep in mind that this method can be expensive. Also, the total number of - * NSErrors that can be recorded during your app's life-cycle is limited by a fixed-size circular buffer. If the - * buffer is overrun, the oldest data is dropped. Errors are relayed to Crashlytics on a subsequent launch - * of your application. - * - * You can also use the -recordError:withAdditionalUserInfo: to include additional context not represented - * by the NSError instance itself. - * - **/ -- (void)recordError:(NSError *)error; -- (void)recordError:(NSError *)error withAdditionalUserInfo:(nullable CLS_GENERIC_NSDICTIONARY(NSString *, id) *)userInfo; - -- (void)logEvent:(NSString *)eventName CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:"); -- (void)logEvent:(NSString *)eventName attributes:(nullable NSDictionary *) attributes CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:"); -+ (void)logEvent:(NSString *)eventName CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:"); -+ (void)logEvent:(NSString *)eventName attributes:(nullable NSDictionary *) attributes CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:"); - -@end - -/** - * - * The CrashlyticsDelegate protocol provides a mechanism for your application to take - * action on events that occur in the Crashlytics crash reporting system. You can make - * use of these calls by assigning an object to the Crashlytics' delegate property directly, - * or through the convenience +startWithAPIKey:delegate: method. - * - */ -@protocol CrashlyticsDelegate -@optional - - -- (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics CLS_DEPRECATED("Please refer to -crashlyticsDidDetectReportForLastExecution:"); -- (void)crashlytics:(Crashlytics *)crashlytics didDetectCrashDuringPreviousExecution:(id )crash CLS_DEPRECATED("Please refer to -crashlyticsDidDetectReportForLastExecution:"); - -/** - * - * Called when a Crashlytics instance has determined that the last execution of the - * application resulted in a saved report. This is called synchronously on Crashlytics - * initialization. Your delegate must invoke the completionHandler, but does not need to do so - * synchronously, or even on the main thread. Invoking completionHandler with NO will cause the - * detected report to be deleted and not submitted to Crashlytics. This is useful for - * implementing permission prompts, or other more-complex forms of logic around submitting crashes. - * - * Instead of using this method, you should try to make use of -crashlyticsDidDetectReportForLastExecution: - * if you can. - * - * @warning Failure to invoke the completionHandler will prevent submissions from being reported. Watch out. - * - * @warning Just implementing this delegate method will disable all forms of synchronous report submission. This can - * impact the reliability of reporting crashes very early in application launch. - * - * @param report The CLSReport object representing the last detected report - * @param completionHandler The completion handler to call when your logic has completed. - * - */ -- (void)crashlyticsDidDetectReportForLastExecution:(CLSReport *)report completionHandler:(void (^)(BOOL submit))completionHandler; - -/** - * - * Called when a Crashlytics instance has determined that the last execution of the - * application resulted in a saved report. This method differs from - * -crashlyticsDidDetectReportForLastExecution:completionHandler: in three important ways: - * - * - it is not called synchronously during initialization - * - it does not give you the ability to prevent the report from being submitted - * - the report object itself is immutable - * - * Thanks to these limitations, making use of this method does not impact reporting - * reliabilty in any way. - * - * @param report The read-only CLSReport object representing the last detected report - * - */ - -- (void)crashlyticsDidDetectReportForLastExecution:(CLSReport *)report; - -/** - * If your app is running on an OS that supports it (OS X 10.9+, iOS 7.0+), Crashlytics will submit - * most reports using out-of-process background networking operations. This results in a significant - * improvement in reliability of reporting, as well as power and performance wins for your users. - * If you don't want this functionality, you can disable by returning NO from this method. - * - * @warning Background submission is not supported for extensions on iOS or OS X. - * - * @param crashlytics The Crashlytics singleton instance - * - * @return Return NO if you don't want out-of-process background network operations. - * - */ -- (BOOL)crashlyticsCanUseBackgroundSessions:(Crashlytics *)crashlytics; - -@end - -/** - * `CrashlyticsKit` can be used as a parameter to `[Fabric with:@[CrashlyticsKit]];` in Objective-C. In Swift, use Crashlytics.sharedInstance() - */ -#define CrashlyticsKit [Crashlytics sharedInstance] - -NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist deleted file mode 100644 index d86059b..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Modules/module.modulemap deleted file mode 100644 index da0845e..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Modules/module.modulemap +++ /dev/null @@ -1,14 +0,0 @@ -framework module Crashlytics { - header "Crashlytics.h" - header "Answers.h" - header "ANSCompatibility.h" - header "CLSLogging.h" - header "CLSReport.h" - header "CLSStackFrame.h" - header "CLSAttributes.h" - - export * - - link "z" - link "c++" -} diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/run b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/run deleted file mode 100755 index 736cd2f..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/run +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh - -# run -# -# Copyright (c) 2015 Crashlytics. All rights reserved. -# -# -# This script is meant to be run as a Run Script in the "Build Phases" section -# of your Xcode project. It sends debug symbols to symbolicate stacktraces, -# sends build events to track versions, and onboard apps for Crashlytics. -# -# This script calls upload-symbols twice: -# -# 1) First it calls upload-symbols synchronously in "validation" mode. If the -# script finds issues with the build environment, it will report errors to Xcode. -# In validation mode it exits before doing any time consuming work. -# -# 2) Then it calls upload-symbols in the background to actually send the build -# event and upload symbols. It does this in the background so that it doesn't -# slow down your builds. If an error happens here, you won't see it in Xcode. -# -# You can find the output for the background execution in Console.app, by -# searching for "upload-symbols". -# -# If you want verbose output, you can pass the --debug flag to this script -# - -# Figure out where we're being called from -DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - -# If the first argument is specified without a dash, treat it as the Fabric API -# Key and add it as an argument -if [ -z "$1" ] || [[ $1 == -* ]]; then - API_KEY_ARG="" -else - API_KEY_ARG="-a $1"; shift -fi - -# If a second argument is specified without a dash, treat it as the Build Secret -# and add it as an argument -if [ -z "$1" ] || [[ $1 == -* ]]; then - BUILD_SECRET_ARG="" -else - BUILD_SECRET_ARG="-bs $1"; shift -fi - -# Build up the arguments list, passing through any flags added after the -# API Key and Build Secret -ARGUMENTS="$API_KEY_ARG $BUILD_SECRET_ARG $@" -VALIDATE_ARGUMENTS="$ARGUMENTS --build-phase --validate" -UPLOAD_ARGUMENTS="$ARGUMENTS --build-phase" - -# Quote the path to handle folders with special characters -COMMAND_PATH="\"$DIR/upload-symbols\" " - -# Ensure params are as expected, run in sync mode to validate, -# and cause a build error if validation fails -eval $COMMAND_PATH$VALIDATE_ARGUMENTS -return_code=$? - -if [[ $return_code != 0 ]]; then - exit $return_code -fi - -# Verification passed, convert and upload cSYMs in the background to prevent -# build delays -# -# Note: Validation is performed again at this step before upload -# -# Note: Output can still be found in Console.app, by searching for -# "upload-symbols" -# -eval $COMMAND_PATH$UPLOAD_ARGUMENTS > /dev/null 2>&1 & diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit deleted file mode 100755 index 3fda5cf..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/upload-symbols b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/upload-symbols deleted file mode 100755 index 5af65de..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/upload-symbols and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric deleted file mode 100755 index aa394a3..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Headers/FABAttributes.h b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Headers/FABAttributes.h deleted file mode 100644 index 3a9355a..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Headers/FABAttributes.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// FABAttributes.h -// Fabric -// -// Copyright (C) 2015 Twitter, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#pragma once - -#define FAB_UNAVAILABLE(x) __attribute__((unavailable(x))) - -#if !__has_feature(nullability) - #define nonnull - #define nullable - #define _Nullable - #define _Nonnull -#endif - -#ifndef NS_ASSUME_NONNULL_BEGIN - #define NS_ASSUME_NONNULL_BEGIN -#endif - -#ifndef NS_ASSUME_NONNULL_END - #define NS_ASSUME_NONNULL_END -#endif - - -/** - * The following macros are defined here to provide - * backwards compatability. If you are still using - * them you should migrate to the native nullability - * macros. - */ -#define fab_nullable nullable -#define fab_nonnull nonnull -#define FAB_NONNULL __fab_nonnull -#define FAB_NULLABLE __fab_nullable -#define FAB_START_NONNULL NS_ASSUME_NONNULL_BEGIN -#define FAB_END_NONNULL NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Headers/Fabric.h b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Headers/Fabric.h deleted file mode 100644 index ecbdb53..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Headers/Fabric.h +++ /dev/null @@ -1,82 +0,0 @@ -// -// Fabric.h -// Fabric -// -// Copyright (C) 2015 Twitter, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import "FABAttributes.h" - -NS_ASSUME_NONNULL_BEGIN - -#if TARGET_OS_IPHONE -#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 - #error "Fabric's minimum iOS version is 6.0" -#endif -#else -#if __MAC_OS_X_VERSION_MIN_REQUIRED < 1070 - #error "Fabric's minimum OS X version is 10.7" -#endif -#endif - -/** - * Fabric Base. Coordinates configuration and starts all provided kits. - */ -@interface Fabric : NSObject - -/** - * Initialize Fabric and all provided kits. Call this method within your App Delegate's `application:didFinishLaunchingWithOptions:` and provide the kits you wish to use. - * - * For example, in Objective-C: - * - * `[Fabric with:@[[Crashlytics class], [Twitter class], [Digits class], [MoPub class]]];` - * - * Swift: - * - * `Fabric.with([Crashlytics.self(), Twitter.self(), Digits.self(), MoPub.self()])` - * - * Only the first call to this method is honored. Subsequent calls are no-ops. - * - * @param kitClasses An array of kit Class objects - * - * @return Returns the shared Fabric instance. In most cases this can be ignored. - */ -+ (instancetype)with:(NSArray *)kitClasses; - -/** - * Returns the Fabric singleton object. - */ -+ (instancetype)sharedSDK; - -/** - * This BOOL enables or disables debug logging, such as kit version information. The default value is NO. - */ -@property (nonatomic, assign) BOOL debug; - -/** - * Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance. - */ -- (id)init FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance."); - -/** - * Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance. - */ -+ (instancetype)new FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance."); - -@end - -NS_ASSUME_NONNULL_END - diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist deleted file mode 100644 index a617b03..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Modules/module.modulemap deleted file mode 100644 index 2a31223..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module Fabric { - umbrella header "Fabric.h" - - export * - module * { export * } -} \ No newline at end of file diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/run b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/run deleted file mode 100755 index 736cd2f..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/run +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh - -# run -# -# Copyright (c) 2015 Crashlytics. All rights reserved. -# -# -# This script is meant to be run as a Run Script in the "Build Phases" section -# of your Xcode project. It sends debug symbols to symbolicate stacktraces, -# sends build events to track versions, and onboard apps for Crashlytics. -# -# This script calls upload-symbols twice: -# -# 1) First it calls upload-symbols synchronously in "validation" mode. If the -# script finds issues with the build environment, it will report errors to Xcode. -# In validation mode it exits before doing any time consuming work. -# -# 2) Then it calls upload-symbols in the background to actually send the build -# event and upload symbols. It does this in the background so that it doesn't -# slow down your builds. If an error happens here, you won't see it in Xcode. -# -# You can find the output for the background execution in Console.app, by -# searching for "upload-symbols". -# -# If you want verbose output, you can pass the --debug flag to this script -# - -# Figure out where we're being called from -DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - -# If the first argument is specified without a dash, treat it as the Fabric API -# Key and add it as an argument -if [ -z "$1" ] || [[ $1 == -* ]]; then - API_KEY_ARG="" -else - API_KEY_ARG="-a $1"; shift -fi - -# If a second argument is specified without a dash, treat it as the Build Secret -# and add it as an argument -if [ -z "$1" ] || [[ $1 == -* ]]; then - BUILD_SECRET_ARG="" -else - BUILD_SECRET_ARG="-bs $1"; shift -fi - -# Build up the arguments list, passing through any flags added after the -# API Key and Build Secret -ARGUMENTS="$API_KEY_ARG $BUILD_SECRET_ARG $@" -VALIDATE_ARGUMENTS="$ARGUMENTS --build-phase --validate" -UPLOAD_ARGUMENTS="$ARGUMENTS --build-phase" - -# Quote the path to handle folders with special characters -COMMAND_PATH="\"$DIR/upload-symbols\" " - -# Ensure params are as expected, run in sync mode to validate, -# and cause a build error if validation fails -eval $COMMAND_PATH$VALIDATE_ARGUMENTS -return_code=$? - -if [[ $return_code != 0 ]]; then - exit $return_code -fi - -# Verification passed, convert and upload cSYMs in the background to prevent -# build delays -# -# Note: Validation is performed again at this step before upload -# -# Note: Output can still be found in Console.app, by searching for -# "upload-symbols" -# -eval $COMMAND_PATH$UPLOAD_ARGUMENTS > /dev/null 2>&1 & diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/upload-symbols b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/upload-symbols deleted file mode 100755 index a18d0b6..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/upload-symbols and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/Info.plist b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/Info.plist new file mode 100644 index 0000000..e755a5b --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/Info.plist @@ -0,0 +1,55 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-armv7_arm64 + LibraryPath + FirebaseCrashlytics.framework + SupportedArchitectures + + armv7 + arm64 + + SupportedPlatform + ios + + + LibraryIdentifier + ios-i386_x86_64-simulator + LibraryPath + FirebaseCrashlytics.framework + SupportedArchitectures + + i386 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + LibraryIdentifier + ios-x86_64-maccatalyst + LibraryPath + FirebaseCrashlytics.framework + SupportedArchitectures + + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + maccatalyst + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/FirebaseCrashlytics b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/FirebaseCrashlytics new file mode 100644 index 0000000..a4a9a68 Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/FirebaseCrashlytics differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h new file mode 100644 index 0000000..9f65153 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h @@ -0,0 +1,192 @@ +// Copyright 2019 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "FIRExceptionModel.h" + +#if __has_include() +#warning "FirebaseCrashlytics and Crashlytics are not compatible \ +in the same app because including multiple crash reporters can \ +cause problems when registering exception handlers." +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics API provides methods to annotate and manage fatal and + * non-fatal reports captured and reported to Firebase Crashlytics. + * + * By default, Firebase Crashlytics is initialized with `[FIRApp configure]`. + * + * Note: The Crashlytics class cannot be subclassed. If this makes testing difficult, + * we suggest using a wrapper class or a protocol extension. + */ +NS_SWIFT_NAME(Crashlytics) +@interface FIRCrashlytics : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Accesses the singleton Crashlytics instance. + * + * @return The singleton Crashlytics instance. + */ ++ (instancetype)crashlytics NS_SWIFT_NAME(crashlytics()); + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param msg Message to log + */ +- (void)log:(NSString *)msg; + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param format Format of string + * @param ... A comma-separated list of arguments to substitute into format + */ +- (void)logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param format Format of string + * @param args Arguments to substitute into format + */ +- (void)logWithFormat:(NSString *)format + arguments:(va_list)args NS_SWIFT_NAME(log(format:arguments:)); + +/** + * Sets a custom key and value to be associated with subsequent fatal and non-fatal reports. + * When setting an object value, the object is converted to a string. This is + * typically done by calling "-[NSObject description]". + * + * @param value The value to be associated with the key + * @param key A unique key + */ +- (void)setCustomValue:(id)value forKey:(NSString *)key; + +/** + * Records a user ID (identifier) that's associated with subsequent fatal and non-fatal reports. + * + * If you want to associate a crash with a specific user, we recommend specifying an arbitrary + * string (e.g., a database, ID, hash, or other value that you can index and query, but is + * meaningless to a third-party observer). This allows you to facilitate responses for support + * requests and reach out to users for more information. + * + * @param userID An arbitrary user identifier string that associates a user to a record in your + * system. + */ +- (void)setUserID:(NSString *)userID; + +/** + * Records a non-fatal event described by an NSError object. The events are + * grouped and displayed similarly to crashes. Keep in mind that this method can be expensive. + * The total number of NSErrors that can be recorded during your app's life-cycle is limited by a + * fixed-size circular buffer. If the buffer is overrun, the oldest data is dropped. Errors are + * relayed to Crashlytics on a subsequent launch of your application. + * + * @param error Non-fatal error to be recorded + */ +- (void)recordError:(NSError *)error NS_SWIFT_NAME(record(error:)); + +/** + * Records an Exception Model described by an FIRExceptionModel object. The events are + * grouped and displayed similarly to crashes. Keep in mind that this method can be expensive. + * The total number of FIRExceptionModels that can be recorded during your app's life-cycle is + * limited by a fixed-size circular buffer. If the buffer is overrun, the oldest data is dropped. + * Exception Models are relayed to Crashlytics on a subsequent launch of your application. + * + * @param exceptionModel Instance of the FIRExceptionModel to be recorded + */ +- (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel + NS_SWIFT_NAME(record(exceptionModel:)); + +/** + * Returns whether the app crashed during the previous execution. + */ +- (BOOL)didCrashDuringPreviousExecution; + +/** + * Enables/disables automatic data collection. + * + * Calling this method overrides both the FirebaseCrashlyticsCollectionEnabled flag in your + * App's Info.plist and FIRApp's isDataCollectionDefaultEnabled flag. + * + * When you set a value for this method, it persists across runs of the app. + * + * The value does not apply until the next run of the app. If you want to disable data + * collection without rebooting, add the FirebaseCrashlyticsCollectionEnabled flag to your app's + * Info.plist. + * * + * @param enabled Determines whether automatic data collection is enabled + */ +- (void)setCrashlyticsCollectionEnabled:(BOOL)enabled; + +/** + * Indicates whether or not automatic data collection is enabled + * + * This method uses three ways to decide whether automatic data collection is enabled, + * in order of priority: + * - If setCrashlyticsCollectionEnabled iscalled with a value, use it + * - If the FirebaseCrashlyticsCollectionEnabled key is in your app's Info.plist, use it + * - Otherwise, use the default isDataCollectionDefaultEnabled in FIRApp + */ +- (BOOL)isCrashlyticsCollectionEnabled; + +/** + * Determines whether there are any unsent crash reports cached on the device, then calls the given + * callback. + * + * The callback only executes if automatic data collection is disabled. You can use + * the callback to get one-time consent from a user upon a crash, and then call + * sendUnsentReports or deleteUnsentReports, depending on whether or not the user gives consent. + * + * Disable automatic collection by: + * - Adding the FirebaseCrashlyticsCollectionEnabled: NO key to your App's Info.plist + * - Calling [[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:NO] in your app + * - Setting FIRApp's isDataCollectionDefaultEnabled to NO + * + * @param completion The callback that's executed once Crashlytics finishes checking for unsent + * reports. The callback is set to YES if there are unsent reports on disk. + */ +- (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL))completion + NS_SWIFT_NAME(checkForUnsentReports(completion:)); + +/** + * Enqueues any unsent reports on the device to upload to Crashlytics. + * + * This method only applies if automatic data collection is disabled. + * + * When automatic data collection is enabled, Crashlytics automatically uploads and deletes reports + * at startup, so this method is ignored. + */ +- (void)sendUnsentReports; + +/** + * Deletes any unsent reports on the device. + * + * This method only applies if automatic data collection is disabled. + */ +- (void)deleteUnsentReports; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h new file mode 100644 index 0000000..a0ee157 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h @@ -0,0 +1,57 @@ +// Copyright 2020 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "FIRStackFrame.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics Exception Model provides a way to report custom exceptions + * to Crashlytics that came from a runtime environment outside of the native + * platform Crashlytics is running in. + */ +NS_SWIFT_NAME(ExceptionModel) +@interface FIRExceptionModel : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes an Exception Model model with the given required fields. + * + * @param name - typically the type of the Exception class + * @param reason - the human-readable reason the issue occurred + */ +- (instancetype)initWithName:(NSString *)name reason:(NSString *)reason; + +/** + * Creates an Exception Model model with the given required fields. + * + * @param name - typically the type of the Exception class + * @param reason - the human-readable reason the issue occurred + */ ++ (instancetype)exceptionModelWithName:(NSString *)name + reason:(NSString *)reason NS_SWIFT_UNAVAILABLE(""); + +/** + * A list of Stack Frames that make up the stack trace. The order of the stack trace is top-first, + * so typically the "main" function is the last element in this list. + */ +@property(nonatomic, copy) NSArray *stackTrace; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h new file mode 100644 index 0000000..ef9746f --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h @@ -0,0 +1,53 @@ +// Copyright 2020 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics Stack Frame provides a way to construct the lines of + * a stack trace for reporting along with a recorded Exception Model. + */ +NS_SWIFT_NAME(StackFrame) +@interface FIRStackFrame : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes a symbolicated Stack Frame with the given required fields. Symbolicated + * Stack Frames will appear in the Crashlytics dashboard as reported in these fields. + * + * @param symbol - The function or method name + * @param file - the file where the exception occurred + * @param line - the line number + */ +- (instancetype)initWithSymbol:(NSString *)symbol file:(NSString *)file line:(NSInteger)line; + +/** + * Creates a symbolicated Stack Frame with the given required fields. Symbolicated + * Stack Frames will appear in the Crashlytics dashboard as reported in these fields. * + * + * @param symbol - The function or method name + * @param file - the file where the exception occurred + * @param line - the line number + */ ++ (instancetype)stackFrameWithSymbol:(NSString *)symbol + file:(NSString *)file + line:(NSInteger)line NS_SWIFT_UNAVAILABLE(""); + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h new file mode 100644 index 0000000..9022811 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRCrashlytics.h" +#import "FIRExceptionModel.h" +#import "FIRStackFrame.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Modules/module.modulemap new file mode 100644 index 0000000..fdceddb --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-armv7_arm64/FirebaseCrashlytics.framework/Modules/module.modulemap @@ -0,0 +1,12 @@ +framework module FirebaseCrashlytics { +umbrella header "FirebaseCrashlytics.h" +export * +module * { export * } + link framework "CoreTelephony" + link framework "Foundation" + link framework "Security" + link framework "SystemConfiguration" + link framework "UIKit" + link "c++" + link "z" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/FirebaseCrashlytics b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/FirebaseCrashlytics new file mode 100644 index 0000000..63335e2 Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/FirebaseCrashlytics differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h new file mode 100644 index 0000000..9f65153 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h @@ -0,0 +1,192 @@ +// Copyright 2019 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "FIRExceptionModel.h" + +#if __has_include() +#warning "FirebaseCrashlytics and Crashlytics are not compatible \ +in the same app because including multiple crash reporters can \ +cause problems when registering exception handlers." +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics API provides methods to annotate and manage fatal and + * non-fatal reports captured and reported to Firebase Crashlytics. + * + * By default, Firebase Crashlytics is initialized with `[FIRApp configure]`. + * + * Note: The Crashlytics class cannot be subclassed. If this makes testing difficult, + * we suggest using a wrapper class or a protocol extension. + */ +NS_SWIFT_NAME(Crashlytics) +@interface FIRCrashlytics : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Accesses the singleton Crashlytics instance. + * + * @return The singleton Crashlytics instance. + */ ++ (instancetype)crashlytics NS_SWIFT_NAME(crashlytics()); + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param msg Message to log + */ +- (void)log:(NSString *)msg; + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param format Format of string + * @param ... A comma-separated list of arguments to substitute into format + */ +- (void)logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param format Format of string + * @param args Arguments to substitute into format + */ +- (void)logWithFormat:(NSString *)format + arguments:(va_list)args NS_SWIFT_NAME(log(format:arguments:)); + +/** + * Sets a custom key and value to be associated with subsequent fatal and non-fatal reports. + * When setting an object value, the object is converted to a string. This is + * typically done by calling "-[NSObject description]". + * + * @param value The value to be associated with the key + * @param key A unique key + */ +- (void)setCustomValue:(id)value forKey:(NSString *)key; + +/** + * Records a user ID (identifier) that's associated with subsequent fatal and non-fatal reports. + * + * If you want to associate a crash with a specific user, we recommend specifying an arbitrary + * string (e.g., a database, ID, hash, or other value that you can index and query, but is + * meaningless to a third-party observer). This allows you to facilitate responses for support + * requests and reach out to users for more information. + * + * @param userID An arbitrary user identifier string that associates a user to a record in your + * system. + */ +- (void)setUserID:(NSString *)userID; + +/** + * Records a non-fatal event described by an NSError object. The events are + * grouped and displayed similarly to crashes. Keep in mind that this method can be expensive. + * The total number of NSErrors that can be recorded during your app's life-cycle is limited by a + * fixed-size circular buffer. If the buffer is overrun, the oldest data is dropped. Errors are + * relayed to Crashlytics on a subsequent launch of your application. + * + * @param error Non-fatal error to be recorded + */ +- (void)recordError:(NSError *)error NS_SWIFT_NAME(record(error:)); + +/** + * Records an Exception Model described by an FIRExceptionModel object. The events are + * grouped and displayed similarly to crashes. Keep in mind that this method can be expensive. + * The total number of FIRExceptionModels that can be recorded during your app's life-cycle is + * limited by a fixed-size circular buffer. If the buffer is overrun, the oldest data is dropped. + * Exception Models are relayed to Crashlytics on a subsequent launch of your application. + * + * @param exceptionModel Instance of the FIRExceptionModel to be recorded + */ +- (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel + NS_SWIFT_NAME(record(exceptionModel:)); + +/** + * Returns whether the app crashed during the previous execution. + */ +- (BOOL)didCrashDuringPreviousExecution; + +/** + * Enables/disables automatic data collection. + * + * Calling this method overrides both the FirebaseCrashlyticsCollectionEnabled flag in your + * App's Info.plist and FIRApp's isDataCollectionDefaultEnabled flag. + * + * When you set a value for this method, it persists across runs of the app. + * + * The value does not apply until the next run of the app. If you want to disable data + * collection without rebooting, add the FirebaseCrashlyticsCollectionEnabled flag to your app's + * Info.plist. + * * + * @param enabled Determines whether automatic data collection is enabled + */ +- (void)setCrashlyticsCollectionEnabled:(BOOL)enabled; + +/** + * Indicates whether or not automatic data collection is enabled + * + * This method uses three ways to decide whether automatic data collection is enabled, + * in order of priority: + * - If setCrashlyticsCollectionEnabled iscalled with a value, use it + * - If the FirebaseCrashlyticsCollectionEnabled key is in your app's Info.plist, use it + * - Otherwise, use the default isDataCollectionDefaultEnabled in FIRApp + */ +- (BOOL)isCrashlyticsCollectionEnabled; + +/** + * Determines whether there are any unsent crash reports cached on the device, then calls the given + * callback. + * + * The callback only executes if automatic data collection is disabled. You can use + * the callback to get one-time consent from a user upon a crash, and then call + * sendUnsentReports or deleteUnsentReports, depending on whether or not the user gives consent. + * + * Disable automatic collection by: + * - Adding the FirebaseCrashlyticsCollectionEnabled: NO key to your App's Info.plist + * - Calling [[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:NO] in your app + * - Setting FIRApp's isDataCollectionDefaultEnabled to NO + * + * @param completion The callback that's executed once Crashlytics finishes checking for unsent + * reports. The callback is set to YES if there are unsent reports on disk. + */ +- (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL))completion + NS_SWIFT_NAME(checkForUnsentReports(completion:)); + +/** + * Enqueues any unsent reports on the device to upload to Crashlytics. + * + * This method only applies if automatic data collection is disabled. + * + * When automatic data collection is enabled, Crashlytics automatically uploads and deletes reports + * at startup, so this method is ignored. + */ +- (void)sendUnsentReports; + +/** + * Deletes any unsent reports on the device. + * + * This method only applies if automatic data collection is disabled. + */ +- (void)deleteUnsentReports; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h new file mode 100644 index 0000000..a0ee157 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h @@ -0,0 +1,57 @@ +// Copyright 2020 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "FIRStackFrame.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics Exception Model provides a way to report custom exceptions + * to Crashlytics that came from a runtime environment outside of the native + * platform Crashlytics is running in. + */ +NS_SWIFT_NAME(ExceptionModel) +@interface FIRExceptionModel : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes an Exception Model model with the given required fields. + * + * @param name - typically the type of the Exception class + * @param reason - the human-readable reason the issue occurred + */ +- (instancetype)initWithName:(NSString *)name reason:(NSString *)reason; + +/** + * Creates an Exception Model model with the given required fields. + * + * @param name - typically the type of the Exception class + * @param reason - the human-readable reason the issue occurred + */ ++ (instancetype)exceptionModelWithName:(NSString *)name + reason:(NSString *)reason NS_SWIFT_UNAVAILABLE(""); + +/** + * A list of Stack Frames that make up the stack trace. The order of the stack trace is top-first, + * so typically the "main" function is the last element in this list. + */ +@property(nonatomic, copy) NSArray *stackTrace; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h new file mode 100644 index 0000000..ef9746f --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h @@ -0,0 +1,53 @@ +// Copyright 2020 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics Stack Frame provides a way to construct the lines of + * a stack trace for reporting along with a recorded Exception Model. + */ +NS_SWIFT_NAME(StackFrame) +@interface FIRStackFrame : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes a symbolicated Stack Frame with the given required fields. Symbolicated + * Stack Frames will appear in the Crashlytics dashboard as reported in these fields. + * + * @param symbol - The function or method name + * @param file - the file where the exception occurred + * @param line - the line number + */ +- (instancetype)initWithSymbol:(NSString *)symbol file:(NSString *)file line:(NSInteger)line; + +/** + * Creates a symbolicated Stack Frame with the given required fields. Symbolicated + * Stack Frames will appear in the Crashlytics dashboard as reported in these fields. * + * + * @param symbol - The function or method name + * @param file - the file where the exception occurred + * @param line - the line number + */ ++ (instancetype)stackFrameWithSymbol:(NSString *)symbol + file:(NSString *)file + line:(NSInteger)line NS_SWIFT_UNAVAILABLE(""); + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h new file mode 100644 index 0000000..9022811 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRCrashlytics.h" +#import "FIRExceptionModel.h" +#import "FIRStackFrame.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Modules/module.modulemap new file mode 100644 index 0000000..fdceddb --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-i386_x86_64-simulator/FirebaseCrashlytics.framework/Modules/module.modulemap @@ -0,0 +1,12 @@ +framework module FirebaseCrashlytics { +umbrella header "FirebaseCrashlytics.h" +export * +module * { export * } + link framework "CoreTelephony" + link framework "Foundation" + link framework "Security" + link framework "SystemConfiguration" + link framework "UIKit" + link "c++" + link "z" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/FirebaseCrashlytics b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/FirebaseCrashlytics new file mode 100644 index 0000000..7e71511 Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/FirebaseCrashlytics differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h new file mode 100644 index 0000000..9f65153 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRCrashlytics.h @@ -0,0 +1,192 @@ +// Copyright 2019 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "FIRExceptionModel.h" + +#if __has_include() +#warning "FirebaseCrashlytics and Crashlytics are not compatible \ +in the same app because including multiple crash reporters can \ +cause problems when registering exception handlers." +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics API provides methods to annotate and manage fatal and + * non-fatal reports captured and reported to Firebase Crashlytics. + * + * By default, Firebase Crashlytics is initialized with `[FIRApp configure]`. + * + * Note: The Crashlytics class cannot be subclassed. If this makes testing difficult, + * we suggest using a wrapper class or a protocol extension. + */ +NS_SWIFT_NAME(Crashlytics) +@interface FIRCrashlytics : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Accesses the singleton Crashlytics instance. + * + * @return The singleton Crashlytics instance. + */ ++ (instancetype)crashlytics NS_SWIFT_NAME(crashlytics()); + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param msg Message to log + */ +- (void)log:(NSString *)msg; + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param format Format of string + * @param ... A comma-separated list of arguments to substitute into format + */ +- (void)logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +/** + * Adds logging that is sent with your crash data. The logging does not appear in the + * system.log and is only visible in the Crashlytics dashboard. + * + * @param format Format of string + * @param args Arguments to substitute into format + */ +- (void)logWithFormat:(NSString *)format + arguments:(va_list)args NS_SWIFT_NAME(log(format:arguments:)); + +/** + * Sets a custom key and value to be associated with subsequent fatal and non-fatal reports. + * When setting an object value, the object is converted to a string. This is + * typically done by calling "-[NSObject description]". + * + * @param value The value to be associated with the key + * @param key A unique key + */ +- (void)setCustomValue:(id)value forKey:(NSString *)key; + +/** + * Records a user ID (identifier) that's associated with subsequent fatal and non-fatal reports. + * + * If you want to associate a crash with a specific user, we recommend specifying an arbitrary + * string (e.g., a database, ID, hash, or other value that you can index and query, but is + * meaningless to a third-party observer). This allows you to facilitate responses for support + * requests and reach out to users for more information. + * + * @param userID An arbitrary user identifier string that associates a user to a record in your + * system. + */ +- (void)setUserID:(NSString *)userID; + +/** + * Records a non-fatal event described by an NSError object. The events are + * grouped and displayed similarly to crashes. Keep in mind that this method can be expensive. + * The total number of NSErrors that can be recorded during your app's life-cycle is limited by a + * fixed-size circular buffer. If the buffer is overrun, the oldest data is dropped. Errors are + * relayed to Crashlytics on a subsequent launch of your application. + * + * @param error Non-fatal error to be recorded + */ +- (void)recordError:(NSError *)error NS_SWIFT_NAME(record(error:)); + +/** + * Records an Exception Model described by an FIRExceptionModel object. The events are + * grouped and displayed similarly to crashes. Keep in mind that this method can be expensive. + * The total number of FIRExceptionModels that can be recorded during your app's life-cycle is + * limited by a fixed-size circular buffer. If the buffer is overrun, the oldest data is dropped. + * Exception Models are relayed to Crashlytics on a subsequent launch of your application. + * + * @param exceptionModel Instance of the FIRExceptionModel to be recorded + */ +- (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel + NS_SWIFT_NAME(record(exceptionModel:)); + +/** + * Returns whether the app crashed during the previous execution. + */ +- (BOOL)didCrashDuringPreviousExecution; + +/** + * Enables/disables automatic data collection. + * + * Calling this method overrides both the FirebaseCrashlyticsCollectionEnabled flag in your + * App's Info.plist and FIRApp's isDataCollectionDefaultEnabled flag. + * + * When you set a value for this method, it persists across runs of the app. + * + * The value does not apply until the next run of the app. If you want to disable data + * collection without rebooting, add the FirebaseCrashlyticsCollectionEnabled flag to your app's + * Info.plist. + * * + * @param enabled Determines whether automatic data collection is enabled + */ +- (void)setCrashlyticsCollectionEnabled:(BOOL)enabled; + +/** + * Indicates whether or not automatic data collection is enabled + * + * This method uses three ways to decide whether automatic data collection is enabled, + * in order of priority: + * - If setCrashlyticsCollectionEnabled iscalled with a value, use it + * - If the FirebaseCrashlyticsCollectionEnabled key is in your app's Info.plist, use it + * - Otherwise, use the default isDataCollectionDefaultEnabled in FIRApp + */ +- (BOOL)isCrashlyticsCollectionEnabled; + +/** + * Determines whether there are any unsent crash reports cached on the device, then calls the given + * callback. + * + * The callback only executes if automatic data collection is disabled. You can use + * the callback to get one-time consent from a user upon a crash, and then call + * sendUnsentReports or deleteUnsentReports, depending on whether or not the user gives consent. + * + * Disable automatic collection by: + * - Adding the FirebaseCrashlyticsCollectionEnabled: NO key to your App's Info.plist + * - Calling [[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:NO] in your app + * - Setting FIRApp's isDataCollectionDefaultEnabled to NO + * + * @param completion The callback that's executed once Crashlytics finishes checking for unsent + * reports. The callback is set to YES if there are unsent reports on disk. + */ +- (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL))completion + NS_SWIFT_NAME(checkForUnsentReports(completion:)); + +/** + * Enqueues any unsent reports on the device to upload to Crashlytics. + * + * This method only applies if automatic data collection is disabled. + * + * When automatic data collection is enabled, Crashlytics automatically uploads and deletes reports + * at startup, so this method is ignored. + */ +- (void)sendUnsentReports; + +/** + * Deletes any unsent reports on the device. + * + * This method only applies if automatic data collection is disabled. + */ +- (void)deleteUnsentReports; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h new file mode 100644 index 0000000..a0ee157 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRExceptionModel.h @@ -0,0 +1,57 @@ +// Copyright 2020 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "FIRStackFrame.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics Exception Model provides a way to report custom exceptions + * to Crashlytics that came from a runtime environment outside of the native + * platform Crashlytics is running in. + */ +NS_SWIFT_NAME(ExceptionModel) +@interface FIRExceptionModel : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes an Exception Model model with the given required fields. + * + * @param name - typically the type of the Exception class + * @param reason - the human-readable reason the issue occurred + */ +- (instancetype)initWithName:(NSString *)name reason:(NSString *)reason; + +/** + * Creates an Exception Model model with the given required fields. + * + * @param name - typically the type of the Exception class + * @param reason - the human-readable reason the issue occurred + */ ++ (instancetype)exceptionModelWithName:(NSString *)name + reason:(NSString *)reason NS_SWIFT_UNAVAILABLE(""); + +/** + * A list of Stack Frames that make up the stack trace. The order of the stack trace is top-first, + * so typically the "main" function is the last element in this list. + */ +@property(nonatomic, copy) NSArray *stackTrace; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h new file mode 100644 index 0000000..ef9746f --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FIRStackFrame.h @@ -0,0 +1,53 @@ +// Copyright 2020 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Firebase Crashlytics Stack Frame provides a way to construct the lines of + * a stack trace for reporting along with a recorded Exception Model. + */ +NS_SWIFT_NAME(StackFrame) +@interface FIRStackFrame : NSObject + +/** :nodoc: */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes a symbolicated Stack Frame with the given required fields. Symbolicated + * Stack Frames will appear in the Crashlytics dashboard as reported in these fields. + * + * @param symbol - The function or method name + * @param file - the file where the exception occurred + * @param line - the line number + */ +- (instancetype)initWithSymbol:(NSString *)symbol file:(NSString *)file line:(NSInteger)line; + +/** + * Creates a symbolicated Stack Frame with the given required fields. Symbolicated + * Stack Frames will appear in the Crashlytics dashboard as reported in these fields. * + * + * @param symbol - The function or method name + * @param file - the file where the exception occurred + * @param line - the line number + */ ++ (instancetype)stackFrameWithSymbol:(NSString *)symbol + file:(NSString *)file + line:(NSInteger)line NS_SWIFT_UNAVAILABLE(""); + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h new file mode 100644 index 0000000..9022811 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Headers/FirebaseCrashlytics.h @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRCrashlytics.h" +#import "FIRExceptionModel.h" +#import "FIRStackFrame.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Modules/module.modulemap new file mode 100644 index 0000000..fdceddb --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics.xcframework/ios-x86_64-maccatalyst/FirebaseCrashlytics.framework/Modules/module.modulemap @@ -0,0 +1,12 @@ +framework module FirebaseCrashlytics { +umbrella header "FirebaseCrashlytics.h" +export * +module * { export * } + link framework "CoreTelephony" + link framework "Foundation" + link framework "Security" + link framework "SystemConfiguration" + link framework "UIKit" + link "c++" + link "z" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics/run b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics/run new file mode 100755 index 0000000..9316eea --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics/run @@ -0,0 +1,76 @@ +#!/bin/sh + +# Copyright 2019 Google +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# run +# +# This script is meant to be run as a Run Script in the "Build Phases" section +# of your Xcode project. It sends debug symbols to symbolicate stacktraces, +# sends build events to track versions, and onboards apps for Crashlytics. +# +# This script calls upload-symbols twice: +# +# 1) First it calls upload-symbols synchronously in "validation" mode. If the +# script finds issues with the build environment, it will report errors to Xcode. +# In validation mode it exits before doing any time consuming work. +# +# 2) Then it calls upload-symbols in the background to actually send the build +# event and upload symbols. It does this in the background so that it doesn't +# slow down your builds. If an error happens here, you won't see it in Xcode. +# +# You can find the output for the background execution in Console.app, by +# searching for "upload-symbols". +# +# If you want verbose output, you can pass the --debug flag to this script +# + +# Figure out where we're being called from +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +# If the first argument is specified without a dash, treat it as the Fabric API +# Key and add it as an argument. +if [ -z "$1" ] || [[ $1 == -* ]]; then + API_KEY_ARG="" +else + API_KEY_ARG="-a $1"; shift +fi + +# Build up the arguments list, passing through any flags added after the +# API Key +ARGUMENTS="$API_KEY_ARG $@" +VALIDATE_ARGUMENTS="$ARGUMENTS --build-phase --validate" +UPLOAD_ARGUMENTS="$ARGUMENTS --build-phase" + +# Quote the path to handle folders with special characters +COMMAND_PATH="\"$DIR/upload-symbols\" " + +# Ensure params are as expected, run in sync mode to validate, +# and cause a build error if validation fails +eval $COMMAND_PATH$VALIDATE_ARGUMENTS +return_code=$? + +if [[ $return_code != 0 ]]; then + exit $return_code +fi + +# Verification passed, convert and upload cSYMs in the background to prevent +# build delays +# +# Note: Validation is performed again at this step before upload +# +# Note: Output can still be found in Console.app, by searching for +# "upload-symbols" +# +eval $COMMAND_PATH$UPLOAD_ARGUMENTS > /dev/null 2>&1 & diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics/upload-symbols b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics/upload-symbols new file mode 100755 index 0000000..76738d0 Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrashlytics/upload-symbols differ diff --git a/WikiRaces/Shared/Logging/OSLog+WikiRaces.swift b/WikiRaces/Shared/Logging/OSLog+WikiRaces.swift new file mode 100644 index 0000000..59a5b68 --- /dev/null +++ b/WikiRaces/Shared/Logging/OSLog+WikiRaces.swift @@ -0,0 +1,31 @@ +// +// OSLog+WikiRaces.swift +// WikiRaces +// +// Created by Andrew Finke on 6/30/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation +import os.log + +extension OSLog { + + // MARK: - Types - + + private enum CustomCategory: String { + case store, gameKit, nearby, matchSupport, raceLiveDatabase + } + + private static let subsystem: String = { + guard let identifier = Bundle.main.bundleIdentifier else { fatalError() } + return identifier + }() + + static let store = OSLog(subsystem: subsystem, category: CustomCategory.store.rawValue) + static let gameKit = OSLog(subsystem: subsystem, category: CustomCategory.gameKit.rawValue) + static let nearby = OSLog(subsystem: subsystem, category: CustomCategory.nearby.rawValue) + static let matchSupport = OSLog(subsystem: subsystem, category: CustomCategory.matchSupport.rawValue) + static let raceLiveDatabase = OSLog(subsystem: subsystem, category: CustomCategory.raceLiveDatabase.rawValue) + +} diff --git a/WikiRaces/Shared/Logging/PlayerCloudKitLiveRaceManager.swift b/WikiRaces/Shared/Logging/PlayerCloudKitLiveRaceManager.swift new file mode 100644 index 0000000..d0aa2f1 --- /dev/null +++ b/WikiRaces/Shared/Logging/PlayerCloudKitLiveRaceManager.swift @@ -0,0 +1,250 @@ +// +// PlayerCloudKitLiveRaceManager.swift +// WikiRaces +// +// Created by Andrew Finke on 7/1/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import CloudKit +import WKRKit +import WKRUIKit + +import os.log + +class PlayerCloudKitLiveRaceManager { + + // MARK: - Types - + + enum RaceCodeResult { + case valid, invalid, noiCloudAccount + } + + // MARK: - Properties - + + static let shared = PlayerCloudKitLiveRaceManager() + + private let writeQueue = DispatchQueue(label: "com.andrewfinke.WikiRaces", qos: .utility) + private let encoder = JSONEncoder() + + private var activeRecord: CKRecord? + private var lastUpdateDate: Date? + + private var queuedResultsInfo: WKRResultsInfo? + + // MARK: - Initalization - + + private init() {} + + // MARK: - Update Data - + + func reset() { + activeRecord = nil + queuedResultsInfo = nil + lastUpdateDate = nil + } + + func savePlayerImages() { + writeQueue.async { + guard let record = self.activeRecord, + let data = try? JSONEncoder().encode(WKRUIPlayerImageManager.shared.container()), + let url = self.write(data: data, suffix: "ImageContainer") else { + os_log("%{public}s: exit early", log: .raceLiveDatabase, type: .error, #function) + return + } + + record["ImageContainer"] = CKAsset(fileURL: url) + self.save(record: record, enforceRecentUpdateCheck: true) + } + } + + func updated(resultsInfo: WKRResultsInfo) { + writeQueue.async { + var adjusted = resultsInfo + adjusted.minimize() + self.queuedResultsInfo = adjusted + os_log("%{public}s: queued results", log: .raceLiveDatabase, type: .info, #function) + } + writeQueue.asyncAfter(deadline: .now() + .seconds(WKRKitConstants.current.raceResultsSpectatorUpdateInterval)) { + os_log("%{public}s: async check started", log: .raceLiveDatabase, type: .info, #function) + guard let resultsInfo = self.queuedResultsInfo, + let data = try? self.encoder.encode(resultsInfo), + let url = self.write(data: data, suffix: "WKRResultsInfo"), + let record = self.activeRecord else { + os_log("%{public}s: async check exit early", log: .raceLiveDatabase, type: .info, #function) + return + } + self.queuedResultsInfo = nil + record["ResultsInfo"] = CKAsset(fileURL: url) + + os_log("%{public}s: async check completed", log: .raceLiveDatabase, type: .info, #function) + self.save(record: record, enforceRecentUpdateCheck: true) + } + } + + func updated(config: WKRRaceConfig) { + writeQueue.async { + guard let record = self.activeRecord, + let data = try? JSONEncoder().encode(config), + let url = self.write(data: data, suffix: "Config") else { + os_log("%{public}s: exit early", log: .raceLiveDatabase, type: .error, #function) + return + } + record["Config"] = CKAsset(fileURL: url) + self.save(record: record, enforceRecentUpdateCheck: true) + } + } + + func updated(state: WKRGameState) { + guard let record = activeRecord else { return } + if state == .voting { + record["ResultsInfo"] = nil + record["Config"] = nil + } + record["State"] = state.rawValue + save(record: record, enforceRecentUpdateCheck: true) + } + + func save(record: CKRecord, enforceRecentUpdateCheck: Bool) { + if enforceRecentUpdateCheck { + if let date = lastUpdateDate { + let timeIntervalSinceNow = -Int(date.timeIntervalSinceNow) + if timeIntervalSinceNow < WKRKitConstants.current.raceCodeRecordMinReuseTimeSinceLastUpdate { + os_log("%{public}s: passed enforce recent check: %{public}ld, max is %{public}ld", log: .raceLiveDatabase, type: .info, #function, timeIntervalSinceNow, WKRKitConstants.current.raceCodeRecordMinReuseTimeSinceLastUpdate) + } else { + os_log("%{public}s: failed enforce recent check: %{public}ld, max is %{public}ld", log: .raceLiveDatabase, type: .info, #function, timeIntervalSinceNow, WKRKitConstants.current.raceCodeRecordMinReuseTimeSinceLastUpdate) + return + } + } else { + os_log("%{public}s: no date", log: .raceLiveDatabase, type: .error, #function) + return + } + } + CKContainer.default().publicCloudDatabase.save(record) { _, error in + if let error = error { + os_log("%{public}s: save error: %{public}s", log: .raceLiveDatabase, type: .error, #function, error.localizedDescription) + } else { + self.lastUpdateDate = Date() + os_log("%{public}s: save successful", log: .raceLiveDatabase, type: .info, #function) + } + } + } + + private func write(data: Data, suffix: String) -> URL? { + os_log("%{public}s", log: .raceLiveDatabase, type: .info, #function) + + guard let filePath = FileManager + .default + .urls(for: .documentDirectory, in: .userDomainMask) + .last? + .path + .appendingFormat("/\(suffix).data") else { + return nil + } + + os_log("%{public}s: writing to %{public}s (size: %{public}ld)", log: .raceLiveDatabase, type: .info, #function, filePath, data.count) + let filePathURL = URL(fileURLWithPath: filePath) + + do { + if FileManager.default.fileExists(atPath: filePath) { + try FileManager.default.removeItem(atPath: filePath) + } + try data.write(to: filePathURL) + } catch { + os_log("%{public}s: write error: %{public}s", log: .raceLiveDatabase, type: .error, #function, error.localizedDescription) + return nil + } + return filePathURL + } + + // MARK: - Setup - + + func isCloudEnabled(completion: @escaping ((Bool) -> Void)) { + CKContainer.default().accountStatus { status, _ in + os_log("%{public}s: status: %{public}ld", log: .raceLiveDatabase, type: .info, #function, status.rawValue) + completion(status == .available) + } + } + + func isRaceCodeValid(raceCode: String, host: String, completion: @escaping ((RaceCodeResult) -> Void)) { + os_log("%{public}s: %{public}s", log: .raceLiveDatabase, type: .info, #function, raceCode) + + isCloudEnabled { isEnabled in + if isEnabled { + self.fetchValidRecord(for: raceCode) { record, isRaceCodeValid in + if isRaceCodeValid { + if record != nil { + PlayerFirebaseAnalytics.log(event: .raceCodeRecordCreated) + } + let raceRecord = record ?? CKRecord(recordType: "RaceActive") + self.claim(record: raceRecord, raceCode: raceCode, host: host) + completion(.valid) + } else { + completion(.invalid) + } + } + } else { + completion(.noiCloudAccount) + } + } + } + + private func fetchValidRecord(for raceCode: String, completion: @escaping ((CKRecord?, Bool) -> Void)) { + let predicate = NSPredicate(format: "Code == %@", raceCode) + let sort = NSSortDescriptor(key: "modificationDate", ascending: false) + let query = CKQuery(recordType: "RaceActive", predicate: predicate) + query.sortDescriptors = [sort] + + var isRaceCodeValid = true + var existingRecord: CKRecord? + + let operation = CKQueryOperation(query: query) + operation.desiredKeys = [] + operation.resultsLimit = 1 + operation.recordFetchedBlock = { record in + if let date = record.modificationDate { + existingRecord = record + + let timeIntervalSinceNow = -Int(date.timeIntervalSinceNow) + if timeIntervalSinceNow < WKRKitConstants.current.raceCodeRecordMinReuseTimeSinceLastUpdate { + isRaceCodeValid = false + PlayerFirebaseAnalytics.log(event: .raceCodeRecordTooRecent) + os_log("%{public}s: existing record too new: %{public}ld, min is %{public}ld", log: .raceLiveDatabase, type: .info, #function, timeIntervalSinceNow, WKRKitConstants.current.raceCodeRecordMinReuseTimeSinceLastUpdate) + } else { + PlayerFirebaseAnalytics.log(event: .raceCodeRecordReused) + os_log("%{public}s: existing record is reusable: %{public}ld, min is %{public}ld", log: .raceLiveDatabase, type: .info, #function, timeIntervalSinceNow, WKRKitConstants.current.raceCodeRecordMinReuseTimeSinceLastUpdate) + } + } else { + isRaceCodeValid = false + PlayerFirebaseAnalytics.log(event: .raceCodeRecordTooRecent) + os_log("%{public}s: existing record has no date", log: .raceLiveDatabase, type: .error, #function) + } + } + + operation.completionBlock = { + if isRaceCodeValid { + os_log("%{public}s: valid race code", log: .raceLiveDatabase, type: .info, #function) + completion(existingRecord, true) + } else { + os_log("%{public}s: invalid race code", log: .raceLiveDatabase, type: .info, #function) + completion(nil, false) + } + } + + CKContainer.default().publicCloudDatabase.add(operation) + } + + private func claim(record: CKRecord, raceCode: String, host: String) { + os_log("%{public}s", log: .raceLiveDatabase, type: .info, #function) + record["Code"] = raceCode + record["Version"] = 1 + record["Host"] = host + record["State"] = WKRGameState.preMatch.rawValue + record["ImageContainer"] = nil + record["ResultsInfo"] = nil + record["Config"] = nil + activeRecord = record + save(record: record, enforceRecentUpdateCheck: false) + } + +} diff --git a/WikiRaces/Shared/Logging/PlayerDatabaseMetrics.swift b/WikiRaces/Shared/Logging/PlayerCloudKitStatsManager.swift similarity index 90% rename from WikiRaces/Shared/Logging/PlayerDatabaseMetrics.swift rename to WikiRaces/Shared/Logging/PlayerCloudKitStatsManager.swift index b0054e8..a62eb18 100644 --- a/WikiRaces/Shared/Logging/PlayerDatabaseMetrics.swift +++ b/WikiRaces/Shared/Logging/PlayerCloudKitStatsManager.swift @@ -10,7 +10,7 @@ import CloudKit import UIKit import WKRKit -final class PlayerDatabaseMetrics: NSObject { +final class PlayerCloudKitStatsManager: NSObject { // MARK: - Types - @@ -21,11 +21,11 @@ final class PlayerDatabaseMetrics: NSObject { let links: Int } - static let banHammerNotification = Notification.Name("banHammerNotification") - // MARK: - Properties - - static var shared = PlayerDatabaseMetrics() + static let shared = PlayerCloudKitStatsManager() + + static let banHammerNotification = Notification.Name("banHammerNotification") private let container = CKContainer.default() private let publicDB = CKContainer.default().publicCloudDatabase @@ -39,6 +39,12 @@ final class PlayerDatabaseMetrics: NSObject { private var queuedKeyValues = [String: CKRecordValueProtocol]() + // MARK: - Initalization - + + private override init() { + super.init() + } + // MARK: - Connecting - func connect() { @@ -62,7 +68,7 @@ final class PlayerDatabaseMetrics: NSObject { // negative races indicates ban if let raceCount = userRecord["Races"] as? NSNumber, raceCount.intValue == -1 { DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { - let name = PlayerDatabaseMetrics.banHammerNotification + let name = PlayerCloudKitStatsManager.banHammerNotification NotificationCenter.default.post(name: name, object: nil) }) return @@ -70,10 +76,10 @@ final class PlayerDatabaseMetrics: NSObject { // Get user stats record, or create new one. guard let statsRecordName = userRecord.object(forKey: "UserStatsNamev3") as? NSString, - statsRecordName.length > 5 else { - self.createUserStatsRecord() - self.isConnecting = false - return + statsRecordName.length > 5 else { + self.createUserStatsRecord() + self.isConnecting = false + return } let userStatsRecordID = CKRecord.ID(recordName: statsRecordName as String) self.publicDB.fetch(withRecordID: userStatsRecordID, completionHandler: { userStatsRecord, error in @@ -145,10 +151,10 @@ final class PlayerDatabaseMetrics: NSObject { private func saveKeyValues() { guard !queuedKeyValues.isEmpty, - !isConnecting, - !isCreatingStatsRecord, - !isSyncing, - let record = userStatsRecord else { return } + !isConnecting, + !isCreatingStatsRecord, + !isSyncing, + let record = userStatsRecord else { return } isSyncing = true let keyValues = queuedKeyValues @@ -250,27 +256,24 @@ final class PlayerDatabaseMetrics: NSObject { var totalPlayerTime = 0 var csvString = "Name,State,Duration,Pages\n" - for index in 0.. cloudValue { @@ -313,7 +319,7 @@ final internal class PlayerStatsManager { } else if cloudValue > deviceValue { defaults.set(cloudValue, forKey: key) } - } else if PlayerDatabaseStat.numericLowStats.contains(stat) { + } else if PlayerUserDefaultsStat.numericLowStats.contains(stat) { let deviceValue = defaults.double(forKey: stat.key) let cloudValue = keyValueStore.double(forKey: stat.key) if cloudValue < deviceValue && cloudValue != 0.0 { @@ -322,14 +328,14 @@ final internal class PlayerStatsManager { keyValueStore.set(deviceValue, forKey: stat.key) } } else if stat == .mpcTotalPlayers { - syncPlayerNamesStat(raceType: .mpc) + syncPlayerNamesStat(raceType: .private) } else if stat == .gkTotalPlayers { - syncPlayerNamesStat(raceType: .gameKit) + syncPlayerNamesStat(raceType: .public) } } private func ubiquitousStoreSync() { - for stat in PlayerDatabaseStat.numericHighStats { + for stat in PlayerUserDefaultsStat.numericHighStats { let deviceValue = defaults.double(forKey: stat.key) let cloudValue = keyValueStore.double(forKey: stat.key) if deviceValue > cloudValue { @@ -338,7 +344,7 @@ final internal class PlayerStatsManager { defaults.set(cloudValue, forKey: stat.key) } } - for stat in PlayerDatabaseStat.numericLowStats { + for stat in PlayerUserDefaultsStat.numericLowStats { let deviceValue = defaults.double(forKey: stat.key) let cloudValue = keyValueStore.double(forKey: stat.key) if cloudValue < deviceValue && cloudValue != 0.0 { @@ -348,15 +354,15 @@ final internal class PlayerStatsManager { } } - syncPlayerNamesStat(raceType: .mpc) - syncPlayerNamesStat(raceType: .gameKit) + syncPlayerNamesStat(raceType: .private) + syncPlayerNamesStat(raceType: .public) } private func syncPlayerNamesStat(raceType: RaceType) { var stat = "" - if raceType == .mpc { + if raceType == .private { stat = "PlayersArray" - } else if raceType == .gameKit { + } else if raceType == .public { stat = "GKPlayersArray" } else { return @@ -370,20 +376,20 @@ final internal class PlayerStatsManager { } } - private func logStatToMetric(_ stat: PlayerDatabaseStat) { - let metrics = PlayerDatabaseMetrics.shared + private func logStatToMetric(_ stat: PlayerUserDefaultsStat) { + let metrics = PlayerCloudKitStatsManager.shared metrics.log(value: stat.value(), for: stat.rawValue) } private func logAllStatsToMetric() { - Set(PlayerDatabaseStat.allCases).forEach { logStatToMetric($0) } + Set(PlayerUserDefaultsStat.allCases).forEach { logStatToMetric($0) } } private func playerDatabaseSync() { logAllStatsToMetric() menuStatsUpdated?(multiplayerPoints, multiplayerRaces, - PlayerDatabaseStat.multiplayerAverage.value()) + PlayerUserDefaultsStat.multiplayerAverage.value()) } private func leaderboardSync() { @@ -393,7 +399,7 @@ final internal class PlayerStatsManager { let points = multiplayerPoints let races = multiplayerRaces - let average = PlayerDatabaseStat.multiplayerAverage.value() + let average = PlayerUserDefaultsStat.multiplayerAverage.value() let totalTime = multiplayerTotalTime let fastestTime = multiplayerFastestTime diff --git a/WikiRaces/Shared/Logging/PlayerDatabaseStat.swift b/WikiRaces/Shared/Logging/PlayerUserDefaultsStat.swift similarity index 94% rename from WikiRaces/Shared/Logging/PlayerDatabaseStat.swift rename to WikiRaces/Shared/Logging/PlayerUserDefaultsStat.swift index a5f9eee..d22efdc 100644 --- a/WikiRaces/Shared/Logging/PlayerDatabaseStat.swift +++ b/WikiRaces/Shared/Logging/PlayerUserDefaultsStat.swift @@ -1,5 +1,5 @@ // -// PlayerStat.swift +// PlayerUserDefaultsStat.swift // WikiRaces // // Created by Andrew Finke on 3/6/19. @@ -8,7 +8,7 @@ import Foundation -enum PlayerDatabaseStat: String, CaseIterable { +enum PlayerUserDefaultsStat: String, CaseIterable { case multiplayerAverage case mpcVotes @@ -60,7 +60,7 @@ enum PlayerDatabaseStat: String, CaseIterable { case triggeredEasterEgg - static var numericHighStats: [PlayerDatabaseStat] = [ + static var numericHighStats: [PlayerUserDefaultsStat] = [ .mpcVotes, .mpcHelp, .mpcPoints, @@ -104,7 +104,7 @@ enum PlayerDatabaseStat: String, CaseIterable { .triggeredEasterEgg ] - static var numericLowStats: [PlayerDatabaseStat] = [ + static var numericLowStats: [PlayerUserDefaultsStat] = [ .mpcFastestTime, .gkFastestTime, .soloFastestTime diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift index d806b52..1ebdf34 100644 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift @@ -14,7 +14,7 @@ import WKRUIKit import FirebasePerformance #endif -class ConnectViewController: UIViewController { +class GKConnectViewController: VisualEffectViewController { // MARK: - Types - @@ -23,6 +23,14 @@ class ConnectViewController: UIViewController { let gameSettings: WKRGameSettings } + struct MiniMessage: Codable { + enum Info: String, Codable { + case connected, cancelled + } + let info: Info + let uuid: UUID + } + // MARK: - Interface Elements - /// General status label @@ -41,7 +49,8 @@ class ConnectViewController: UIViewController { final func runConnectionTest(completion: @escaping (Bool) -> Void) { #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - let trace = Performance.startTrace(name: "Connection Test Trace") + // TODO: fix +// let trace = Performance.startTrace(name: "Connection Test Trace") #endif let startDate = Date() @@ -49,7 +58,7 @@ class ConnectViewController: UIViewController { DispatchQueue.main.async { if success { #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - trace?.stop() +// trace?.stop() #endif } PlayerAnonymousMetrics.log(event: .connectionTestResult, @@ -94,7 +103,7 @@ class ConnectViewController: UIViewController { cancelButton.setAttributedTitle(NSAttributedString(string: "CANCEL", spacing: 1.5), for: .normal) cancelButton.translatesAutoresizingMaskIntoConstraints = false - cancelButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium) + cancelButton.titleLabel?.font = UIFont.systemRoundedFont(ofSize: 17, weight: .medium) cancelButton.alpha = 0.0 cancelButton.addTarget(self, action: #selector(pressedCancelButton), for: .touchUpInside) view.addSubview(cancelButton) @@ -102,7 +111,7 @@ class ConnectViewController: UIViewController { descriptionLabel.translatesAutoresizingMaskIntoConstraints = false descriptionLabel.alpha = 0.0 descriptionLabel.textAlignment = .center - descriptionLabel.font = UIFont.systemFont(ofSize: 17, weight: .medium) + descriptionLabel.font = UIFont.systemRoundedFont(ofSize: 17, weight: .medium) view.addSubview(descriptionLabel) updateDescriptionLabel(to: "CHECKING CONNECTION") @@ -140,7 +149,7 @@ class ConnectViewController: UIViewController { final func updateDescriptionLabel(to text: String) { descriptionLabel.attributedText = NSAttributedString(string: text.uppercased(), spacing: 2.0, - font: UIFont.systemFont(ofSize: 20.0, weight: .semibold)) + font: UIFont.systemRoundedFont(ofSize: 20.0, weight: .semibold)) } final func showConnectionSpeedError() { @@ -216,4 +225,5 @@ class ConnectViewController: UIViewController { }) } } + } diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceController.swift similarity index 100% rename from WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceController.swift diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceNotificationsController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceNotificationsController.swift similarity index 98% rename from WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceNotificationsController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceNotificationsController.swift index 31a1c3f..df2e7a4 100644 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceNotificationsController.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceNotificationsController.swift @@ -79,7 +79,7 @@ final class CustomRaceNotificationsController: CustomRaceController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { - fatalError() + fatalError() } cell.toggle.tag = indexPath.row cell.toggle.addTarget(self, action: #selector(switchChanged(updatedSwitch:)), for: .valueChanged) diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceNumericalViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceNumericalViewController.swift similarity index 100% rename from WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceNumericalViewController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceNumericalViewController.swift diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceOtherController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceOtherController.swift similarity index 97% rename from WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceOtherController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceOtherController.swift index 4c6a9a4..205de32 100644 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceOtherController.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceOtherController.swift @@ -78,7 +78,7 @@ final class CustomRaceOtherController: CustomRaceController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { - fatalError() + fatalError() } cell.toggle.tag = indexPath.row cell.toggle.addTarget(self, action: #selector(switchChanged(updatedSwitch:)), for: .valueChanged) diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRacePageViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRacePageViewController.swift similarity index 97% rename from WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRacePageViewController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRacePageViewController.swift index 9cfec0b..b6e2033 100644 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRacePageViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRacePageViewController.swift @@ -42,9 +42,9 @@ final class CustomRacePageViewController: CustomRaceController { if customPages.isEmpty { guard let appleURL = URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc."), - let usaURL = URL(string: "https://en.m.wikipedia.org/wiki/United_States"), - let disURL = URL(string: "https://en.m.wikipedia.org/wiki/Magic_Kingdom") else { - fatalError() + let usaURL = URL(string: "https://en.m.wikipedia.org/wiki/United_States"), + let disURL = URL(string: "https://en.m.wikipedia.org/wiki/Magic_Kingdom") else { + fatalError() } self.customPages.append(contentsOf: [ WKRPage(title: "Apple Inc", url: appleURL), diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceViewController.swift similarity index 91% rename from WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceViewController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceViewController.swift index b09d14d..bda358e 100644 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/CustomRaceViewController/CustomRaceViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/CustomRaceViewController/CustomRaceViewController.swift @@ -8,6 +8,7 @@ import UIKit import WKRKit +import WKRUIKit final class CustomRaceViewController: CustomRaceController { @@ -24,21 +25,38 @@ final class CustomRaceViewController: CustomRaceController { // MARK: - Properties - private let settingOptions = Setting.allCases - var allCustomPages = [WKRPage]() let settings: WKRGameSettings + var allCustomPages: [WKRPage] + var finalPagesCallback: ([WKRPage]) -> Void + // MARK: - Initalization - - init(settings: WKRGameSettings) { + init(settings: WKRGameSettings, pages: [WKRPage], finalPages: @escaping (([WKRPage]) -> Void)) { self.settings = settings + self.allCustomPages = pages + self.finalPagesCallback = finalPages super.init(style: .grouped) title = "Customize Race".uppercased() + + navigationItem.leftBarButtonItem = nil + navigationItem.rightBarButtonItem = WKRUIBarButtonItem( + systemName: "xmark", + target: self, + action: #selector(doneButtonPressed)) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + // MARK: - View Life Cycle - + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + finalPagesCallback(allCustomPages) + } + // MARK: - UITableViewDataSource - override func numberOfSections(in tableView: UITableView) -> Int { @@ -87,7 +105,7 @@ final class CustomRaceViewController: CustomRaceController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if !PlusStore.shared.isPlus { - PlayerAnonymousMetrics.log(event: .forcedIntoStoreFromCustomize) + PlayerFirebaseAnalytics.log(event: .forcedIntoStoreFromCustomize) let controller = PlusViewController() controller.modalPresentationStyle = .overCurrentContext present(controller, animated: false, completion: nil) @@ -250,4 +268,8 @@ final class CustomRaceViewController: CustomRaceController { return "" } } + + @objc func doneButtonPressed() { + dismiss(animated: true, completion: nil) + } } diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKConnectViewController.swift new file mode 100644 index 0000000..8b8a895 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKConnectViewController.swift @@ -0,0 +1,121 @@ +// +// ConnectViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 1/26/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import GameKit +import UIKit +import WKRKit +import WKRUIKit +import os.log + +class GKConnectViewController: VisualEffectViewController { + + // MARK: - Types - + + struct StartMessage: Codable { + let hostName: String + let gameSettings: WKRGameSettings + } + + struct MiniMessage: Codable { + enum Info: String, Codable { + case connected, cancelled + } + let info: Info + let uuid: UUID + } + + // MARK: - Interface Elements - + + final var match: GKMatch? + final var isPlayerHost: Bool + final var isShowingError = false + + // MARK: - Initalization - + + init(isPlayerHost: Bool) { + self.isPlayerHost = isPlayerHost + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Life Cycle - + + override func viewDidLoad() { + super.viewDidLoad() + WKRSeenFinalArticlesStore.resetRemotePlayersSeenFinalArticles() + } + + // MARK: - Helpers - + + final func disconnectFromMatch() { + os_log("%{public}s", log: .gameKit, type: .info, #function) + + match?.delegate = nil + if isPlayerHost { + sendMiniMessage(info: .cancelled) + } + match?.disconnect() + GKMatchmaker.shared().cancel() + } + + final func showError(title: String, message: String) { + os_log("%{public}s: %{public}s", log: .gameKit, type: .info, #function, title) + + guard !isShowingError else { return } + isShowingError = true + + disconnectFromMatch() + + GKNotificationBanner.show(withTitle: title, message: message, completionHandler: nil) + } + + final func sendMiniMessage(info: GKConnectViewController.MiniMessage.Info) { + os_log("%{public}s: %{public}s", log: .gameKit, type: .info, #function, "\(info)") + let message = GKConnectViewController.MiniMessage(info: info, uuid: UUID()) + guard let data = try? JSONEncoder().encode(message) else { + fatalError() + } + try? match?.sendData(toAllPlayers: data, with: .reliable) + } + + // MARK: - Match States - + + /// Cancels the join/create a race action and sends player back to main menu + @objc + final func cancelMatch() { + os_log("%{public}s", log: .gameKit, type: .info, #function) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) + + disconnectFromMatch() + + UIView.animate(withDuration: 0.25, animations: { + self.view.alpha = 0.0 + }, completion: { _ in + self.navigationController?.popToRootViewController(animated: false) + }) + } + + final func transitionToGame(for networkConfig: WKRPeerNetworkConfig, settings: WKRGameSettings) { + os_log("%{public}s", log: .gameKit, type: .info, #function) + + guard !isShowingError else { return } + isShowingError = true + + let controller = GameViewController(network: networkConfig, settings: settings) + let nav = WKRUINavigationController(rootViewController: controller) + nav.modalPresentationStyle = .fullScreen + nav.modalTransitionStyle = .crossDissolve + nav.isModalInPresentation = true + present(nav, animated: true, completion: { [weak self] in + self?.view.alpha = 0 + }) + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/GKHostViewController+Match.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/GKHostViewController+Match.swift new file mode 100644 index 0000000..6dc8079 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/GKHostViewController+Match.swift @@ -0,0 +1,137 @@ +// +// HostViewController+Match.swift +// WikiRaces +// +// Created by Andrew Finke on 6/24/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import WKRKit +import WKRUIKit +import os.log + +extension GKHostViewController: GKMatchDelegate { + + func startMatchmaking(attempt: Int = 1) { + os_log("%{public}s: %{public}ld", log: .gameKit, type: .info, #function, attempt) + + guard isMatchmakingEnabled else { return } + + if attempt > 3 { + os_log("%{public}s: too many attempts", log: .gameKit, type: .error, #function) + DispatchQueue.main.async { + self.isMatchmakingEnabled = false + self.showError(title: "Failed to Create Race", message: "Please try again later.") + self.model.state = .soloRace + } + return + } + + func matchmake(raceCode: String) { + os_log("%{public}s-%{public}s", log: .gameKit, type: .info, #function, raceCode) + + var didFail = false + let startDate = Date() + let request = GKMatchRequest.hostRequest(raceCode: raceCode, isInital: true) + GKMatchmaker.shared().findMatch(for: request) { [weak self] match, error in + guard let self = self, self.isMatchmakingEnabled else { return } + + if let error = error { + os_log("%{public}s-%{public}s: error: %{public}s (%{public}f)", log: .gameKit, type: .error, #function, raceCode, error.localizedDescription, -startDate.timeIntervalSinceNow) + + didFail = true + if -startDate.timeIntervalSinceNow < 1 { + os_log("%{public}s-%{public}s: error: (quick)", log: .gameKit, type: .error, #function, raceCode) + + PlayerFirebaseAnalytics.log(event: .matchmakingQuickFail) + DispatchQueue.global().asyncAfter(deadline: .now() + 4) { + self.startMatchmaking(attempt: attempt + 1) + } + } else { + os_log("%{public}s-%{public}s: error (long)", log: .gameKit, type: .error, #function, raceCode) + DispatchQueue.main.async { + self.isMatchmakingEnabled = false + self.showError(title: "Failed to Create Race", message: "Please try again later.") + self.model.state = .soloRace + } + } + } else if let match = match { + os_log("%{public}s-%{public}s: found match", log: .gameKit, type: .info, #function, raceCode) + self.match = match + self.match?.delegate = self + self.addPlayers() + } else { + fatalError() + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { + os_log("%{public}s-%{public}s: asyncAfter: %{public}ld (%{public}ld)", log: .gameKit, type: .info, #function, raceCode, didFail ? 1 : 0, self.isMatchmakingEnabled ? 1 : 0) + if !didFail && self.isMatchmakingEnabled { + self.model.raceCode = raceCode + self.model.state = .showingRacers + self.startNearbyAdvertising() + } + }) + } + + raceCodeGenerator.new { [weak self] code in + guard let self = self, self.isMatchmakingEnabled else { return } + os_log("%{public}s: raceCodeGenerator callback", log: .gameKit, type: .info, #function) + matchmake(raceCode: code) + } + } + + private func addPlayers() { + guard let match = self.match, let code = model.raceCode, isMatchmakingEnabled else { return } + GKMatchmaker.shared().addPlayers(to: match, matchRequest: GKMatchRequest.hostRequest(raceCode: code, isInital: false)) { [weak self] error in + if let error = error { + os_log("%{public}s: error: %{public}s", log: .gameKit, type: .info, #function, error.localizedDescription) + } else { + os_log("%{public}s: success", log: .gameKit, type: .info, #function) + self?.addPlayers() + } + } + } + + func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) { + os_log("%{public}s: player: %{public}s, state: %{public}ld", log: .gameKit, type: .info, #function, player.alias, state.rawValue) + if state == .connected { + WKRUIPlayerImageManager.shared.connected(to: player, completion: { [weak self] in + DispatchQueue.main.async { + guard let self = self else { return } + let player = WKRPlayerProfile(player: player) + if !self.model.connectedPlayers.contains(player) { + self.model.connectedPlayers.append(player) + } + } + self?.sendMiniMessage(info: .connected) + }) + } else if state == .disconnected { + DispatchQueue.main.async { + let player = WKRPlayerProfile(player: player) + if let index = self.model.connectedPlayers.firstIndex(of: player) { + self.model.connectedPlayers.remove(at: index) + } + } + } + } + + func match(_ match: GKMatch, didFailWithError error: Error?) { + os_log("%{public}s", log: .gameKit, type: .error, #function, error?.localizedDescription ?? "-") + cancelMatch() + } + + func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) { + os_log("%{public}s", log: .gameKit, type: .info, #function) + guard WKRSeenFinalArticlesStore.isRemoteTransferData(data) else { + os_log("%{public}s: failed", log: .gameKit, type: .error, #function) + return + } + + os_log("%{public}s: success", log: .gameKit, type: .info, #function) + WKRSeenFinalArticlesStore.addRemoteTransferData(data) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/GKHostViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/GKHostViewController.swift new file mode 100644 index 0000000..68b4451 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/GKHostViewController.swift @@ -0,0 +1,202 @@ +// +// RevampHostViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import UIKit +import os.log + +import WKRKit +import WKRUIKit +import SwiftUI + +final internal class GKHostViewController: GKConnectViewController { + + // MARK: - Properties - + + let raceCodeGenerator = RaceCodeGenerator() + private let advertiser = NearbyRaceAdvertiser() + private let sourceView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 100))) + + var isMatchmakingEnabled = true { + didSet { + if !isMatchmakingEnabled { + advertiser.stop() + raceCodeGenerator.cancel() + } + } + } + + var model = HostContentViewModel() + lazy var contentViewHosting = UIHostingController( + rootView: HostContentView( + model: model, + cancelAction: { [weak self] in + PlayerFirebaseAnalytics.log(event: .hostCancelledPreMatch) + self?.isMatchmakingEnabled = false + self?.cancelMatch() + }, + startMatch: { [weak self] in + self?.isMatchmakingEnabled = false + self?.startMatch() + }, + presentModal: { [weak self] modal in + self?.presentModal(modal: modal) + })) + + // MARK: - Initalization - + + init() { + super.init(isPlayerHost: true) + startMatchmaking() + WKRSeenFinalArticlesStore.resetRemotePlayersSeenFinalArticles() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Life Cycle - + + override func viewDidLoad() { + super.viewDidLoad() + sourceView.alpha = 0 + contentView.addSubview(sourceView) + contentViewHosting.view.alpha = 0 + configure(hostingView: contentViewHosting.view) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIView.animate(withDuration: 0.5) { [weak self] in + self?.contentViewHosting.view.alpha = 1 + } + + guard !Defaults.promptedAutoInvite else { + return + } + Defaults.promptedAutoInvite = true + + let controller = UIAlertController( + title: "Invite Nearby Racers?", + message: "Would you like to automatically invite nearby racers? This preference can be changed later in the settings app.", + preferredStyle: .alert) + + let action = UIAlertAction(title: "Yes", style: .default) { [weak self] _ in + Defaults.isAutoInviteOn = true + self?.startNearbyAdvertising() + os_log("%{public}s: enabled auto invite", log: .gameKit, type: .info, #function) + } + controller.addAction(action) + + let cancelAction = UIAlertAction(title: "Not Now", style: .cancel) { _ in + os_log("%{public}s: disabled auto invite", log: .gameKit, type: .info, #function) + } + controller.addAction(cancelAction) + present(controller, animated: true, completion: nil) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + sourceView.center = CGPoint(x: contentView.center.x, y: contentView.center.y - 150) + } + + // MARK: - Actions - + + func startMatch() { + os_log("%{public}s", log: .gameKit, type: .info, #function) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) + + model.state = .raceStarting + raceCodeGenerator.cancel() + + advertiser.stop() + contentViewHosting.view.isUserInteractionEnabled = false + + func sendStartMessage() { + guard let match = match else { fatalError("match is nil") } + GKMatchmaker.shared().finishMatchmaking(for: match) + os_log("%{public}s: sending start message", log: .gameKit, type: .info, #function) + + let message = GKConnectViewController.StartMessage( + hostName: GKLocalPlayer.local.alias, + gameSettings: model.settings) + do { + let data = try JSONEncoder().encode(message) + try match.sendData(toAllPlayers: data, with: .reliable) + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + self.transitionToGame( + for: .gameKitPrivate(match: match, isHost: true), + settings: self.model.settings) + } + } catch { + self.cancelMatch() + } + } + + func startSolo() { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.transitionToGame(for: + .solo(name: GKLocalPlayer.local.alias), + settings: self.model.settings) + } + } + + if match == nil || match?.players.count == 0 { + if Defaults.promptedSoloRacesStats { + startSolo() + } else { + let controller = UIAlertController(title: "Solo Race", message: "Solo races will not impact your leaderboard stats.", preferredStyle: .alert) + + let startAction = UIAlertAction(title: "Ok", style: .default) { _ in + startSolo() + } + controller.addAction(startAction) + + present(controller, animated: true, completion: nil) + Defaults.promptedSoloRacesStats = true + } + } else { + sendStartMessage() + } + + PlayerCloudKitLiveRaceManager.shared.savePlayerImages() + } + + // MARK: - Other - + + private func presentModal(modal: HostContentView.Modal) { + os_log("%{public}s: %{public}s", log: .gameKit, type: .info, #function, "\(modal)") + + let controller: UIViewController + switch modal { + case .activity: + guard let code = model.raceCode, let url = URL(string: "WikiRaces://Invite?Code=\(code)") else { + fatalError() + } + controller = UIActivityViewController(activityItems: [url], applicationActivities: nil) + PlayerFirebaseAnalytics.log(event: .raceCodeShared) + case .settings: + controller = CustomRaceViewController(settings: model.settings, pages: model.customPages) { pages in + self.model.customPages = pages + } + } + let nav = WKRUINavigationController(rootViewController: controller) + nav.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .phone ? .fullScreen : .formSheet + nav.popoverPresentationController?.sourceView = sourceView + present(nav, animated: true, completion: nil) + } + + func startNearbyAdvertising() { + guard Defaults.isAutoInviteOn, Defaults.promptedAutoInvite, let code = model.raceCode else { + return + } + os_log("%{public}s", log: .gameKit, type: .info, #function) + advertiser.start(hostName: GKLocalPlayer.local.alias, raceCode: code) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/HostViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/HostViewController.swift new file mode 100644 index 0000000..70f8027 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/HostViewController.swift @@ -0,0 +1,164 @@ +// +// RevampHostViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import UIKit + +import WKRKit +import WKRUIKit +import SwiftUI + +final internal class GKHostViewController: VisualEffectViewController { + + // MARK: - Types - + + enum ListenerUpdate { + case start(match: GKMatch, settings: WKRGameSettings) + case startSolo(settings: WKRGameSettings) + case cancel + } + + // MARK: - Properties - + + private let raceCodeGenerator = RaceCodeGenerator() + private let advertiser = NearbyRaceAdvertiser() + + var match: GKMatch? + var isQuitting = false + + var gameSettings = WKRGameSettings() + + lazy var model = PrivateRaceContentViewModel(settings: gameSettings) + lazy var contentViewHosting = UIHostingController( + rootView: PrivateRaceContentView(model: model)) + + private let activityView = UIActivityIndicatorView(style: .medium) + let listenerUpdate: (ListenerUpdate) -> Void + + // MARK: - Initalization - + + init(listenerUpdate: @escaping ((ListenerUpdate) -> Void)) { + self.listenerUpdate = listenerUpdate + super.init(nibName: nil, bundle: nil) + + let date = Date() + raceCodeGenerator.new { [weak self] code in + print(date.timeIntervalSinceNow) + + guard let self = self else { return } + + DispatchQueue.main.async { + self.model.raceCode = code + self.startNearbyAdvertising() + self.startMatchmaking() + } + } + + title = "PRIVATE RACE" + navigationItem.leftBarButtonItem = WKRUIBarButtonItem(systemName: "xmark", + target: self, + action: #selector(cancelMatch)) + + let startButton = WKRUIBarButtonItem(systemName: "play.fill", + target: self, + action: #selector(startMatch)) + navigationItem.rightBarButtonItem = startButton + } + + override func viewDidLoad() { + super.viewDidLoad() + configure(hostingView: contentViewHosting.view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Actions - + + func sendMiniMessage(info: ConnectViewController.MiniMessage.Info) { + let message = ConnectViewController.MiniMessage(info: info, uuid: UUID()) + guard let data = try? JSONEncoder().encode(message) else { + fatalError() + } + try? match?.sendData(toAllPlayers: data, with: .reliable) + } + + @objc + func cancelMatch() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .hostCancelledPreMatch) + + isQuitting = true + GKMatchmaker.shared().cancel() + + advertiser.stop() + match?.delegate = nil + sendMiniMessage(info: .cancelled) + match?.disconnect() + + listenerUpdate(.cancel) + } + + @objc + func startMatch() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + + advertiser.stop() +// tableView.isUserInteractionEnabled = false + + activityView.sizeToFit() + activityView.startAnimating() + + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityView) + navigationItem.leftBarButtonItem?.isEnabled = false + + func sendStartMessage() { + guard let match = match else { fatalError("match is nil") } + GKMatchmaker.shared().finishMatchmaking(for: match) + + let message = ConnectViewController.StartMessage(hostName: GKLocalPlayer.local.alias, gameSettings: gameSettings) + do { + let data = try JSONEncoder().encode(message) + try match.sendData(toAllPlayers: data, with: .reliable) + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + self.listenerUpdate(.start(match: match, settings: self.gameSettings)) + } + } catch { + self.cancelMatch() + } + } + + if match == nil || match?.players.count == 0 { + if Defaults.promptedSoloRacesStats { + listenerUpdate(.startSolo(settings: gameSettings)) + } else { + let controller = UIAlertController(title: "Solo Race", message: "Solo races will not count towards your stats.", preferredStyle: .alert) + let startAction = UIAlertAction(title: "Ok", style: .default) { [weak self] _ in + guard let self = self else { return } + self.listenerUpdate(.startSolo(settings: self.gameSettings)) + } + controller.addAction(startAction) + controller.addCancelAction(title: "Back") + present(controller, animated: true, completion: nil) + Defaults.promptedSoloRacesStats = true + } + } else { + sendStartMessage() + } + } + + private func startNearbyAdvertising() { + guard Defaults.isAutoInviteOn, let code = model.raceCode else { + advertiser.stop() + return + } + advertiser.start(hostName: GKLocalPlayer.local.alias, raceCode: code) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/ActivityIndicatorView.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/ActivityIndicatorView.swift new file mode 100644 index 0000000..2643899 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/ActivityIndicatorView.swift @@ -0,0 +1,20 @@ +// +// ActivityIndicatorView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI + +struct ActivityIndicatorView: UIViewRepresentable { + + func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { + let view = UIActivityIndicatorView(style: .medium) + view.startAnimating() + return view + } + + func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {} +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostContentView.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostContentView.swift new file mode 100644 index 0000000..fce3e69 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostContentView.swift @@ -0,0 +1,121 @@ +// +// HostContentView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI +import WKRKit +import WKRUIKit +import GameKit + +struct HostContentView: View { + + // MARK: - Types - + + enum Modal { + case activity, settings + } + + // MARK: - Properties - + + @ObservedObject var model: HostContentViewModel + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let cancelAction: () -> Void + let startMatch: () -> Void + let presentModal: (Modal) -> Void + + // MARK: - Body - + + var body: some View { + VStack { + HStack { + Button(action: cancelAction, label: { + Image(systemName: "chevron.left") + .font(.system(size: 22)) + }) + .opacity(model.state == .raceStarting ? 0.2 : 1) + Spacer() + if model.state == .raceStarting { + ActivityIndicatorView() + } else { + Button(action: startMatch, label: { + Image(systemName: "play.fill") + .font(.system(size: 22)) + }) + } + } + .foregroundColor(.wkrTextColor(for: colorScheme)) + .padding() + .padding(.horizontal) + .frame(height: 60) + .allowsHitTesting(model.state != .raceStarting) + + Spacer() + WKRUIPlayerImageView( + player: WKRPlayerProfile(player: GKLocalPlayer.local), + size: 100, + effectSize: 5) + .padding(.bottom, 20) + + if !WKRUIPlayerImageManager.shared.isLocalPlayerImageFromGameCenter { + HStack { + Spacer() + Text("Set a custom racer photo\nin the Game Center settings") + .font(.system(size: 12, weight: .regular)) + .multilineTextAlignment(.center) + .offset(y: -5) + Spacer() + } + } + + VStack { + HostSectionView( + header: "RACE CODE", + title: model.raceCode?.uppercased() ?? "-", + imageName: "square.and.arrow.up", + disabled: model.raceCode == nil) { + self.presentModal(.activity) + } + .padding(.vertical, 16) + + HostSectionView( + header: "TYPE", + title: model.settings.isCustom ? "CUSTOM" : "STANDARD", + imageName: "gear", + disabled: false) { + self.presentModal(.settings) + } + }.frame(width: 220) + + Color.clear.frame(height: 50) + Spacer() + + HStack { + Color.clear.frame(width: 1) + ForEach(model.connectedPlayers) { player in + WKRUIPlayerImageView(player: player, size: 44, effectSize: 3) + .padding(.all, 2) + } + .transition(.opacity) + .scaleEffect(model.connectedPlayers.count < 6 ? 1 : 1.5 - 0.1 * CGFloat(model.connectedPlayers.count), anchor: .center) + Color.clear.frame(width: 1) + } + .frame(maxHeight: 60) + VStack { + Text(model.status) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.secondary) + .transition(.opacity) + .id(model.status) + ActivityIndicatorView().offset(x: 0, y: -5) + .opacity((model.state == .raceStarting || model.state == .soloRace) ? 0 : 1) + } + .padding(.bottom, 20) + } + .animation(.spring()) + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostContentViewModel.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostContentViewModel.swift new file mode 100644 index 0000000..dbe53b0 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostContentViewModel.swift @@ -0,0 +1,49 @@ +// +// HostContentViewModel.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI +import WKRKit +import WKRUIKit + +class HostContentViewModel: ObservableObject { + + enum State { + case generatingRaceCode + case soloRace + case showingRacers + case raceStarting + + } + + @Published var state: State = .generatingRaceCode + + @Published var raceCode: String? + @Published var connectedPlayers = [WKRPlayerProfile]() + + @Published var settings = WKRGameSettings() + @Published var customPages = [WKRPage]() + + var status: String { + switch state { + + case .generatingRaceCode: + return "GENERATING RACE CODE" + case .soloRace: + return "SOLO RACE" + case .showingRacers: + if connectedPlayers.isEmpty { + return "NO CONNECTED RACERS" + } else { + return "\(connectedPlayers.count) CONNECTED RACER" + (connectedPlayers.count == 1 ? "" : "S") + } + case .raceStarting: + return "RACE STARTING" + } + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostSectionView.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostSectionView.swift new file mode 100644 index 0000000..fced57c --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/HostSectionView.swift @@ -0,0 +1,55 @@ +// +// HostSectionView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI + +struct HostSectionView: View { + + // MARK: - Properties - + + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let header: String + let title: String + + let imageName: String + + let disabled: Bool + let action: () -> Void + + // MARK: - Body - + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(header) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.wkrSubtitleTextColor(for: colorScheme)) + .multilineTextAlignment(.leading) + Text(title) + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(.wkrTextColor(for: colorScheme)) + .multilineTextAlignment(.leading) + .transition(.opacity) + .id(title) + } + Spacer() + Button(action: action, label: { + Image(systemName: imageName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24) + .font(.system(size: 30, weight: .regular)) + .foregroundColor(.wkrTextColor(for: colorScheme)) + }) + .opacity(disabled ? 0.2 : 1) + .disabled(disabled) + .animation(.spring()) + } + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/PlayerImageView.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/PlayerImageView.swift new file mode 100644 index 0000000..be9994e --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKHostViewController/SwiftUI/PlayerImageView.swift @@ -0,0 +1,29 @@ +// +// PlayerImageView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI + +struct PlayerImageView: View { + + // MARK: - Properties - + + let player: SwiftUIPlayer + let size: CGFloat + let effectSize: CGFloat + + // MARK: - Body - + + var body: some View { + PlayerImageDatabase.shared.image(for: player.id) + .renderingMode(.original) + .resizable() + .frame(width: size, height: size) + .clipShape(Circle()) + .shadow(radius: effectSize) + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController+Match.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController+Match.swift new file mode 100644 index 0000000..4fbb6a0 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController+Match.swift @@ -0,0 +1,66 @@ +// +// GKJoinViewController+Match.swift +// WikiRaces +// +// Created by Andrew Finke on 1/26/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import GameKit +import WKRKit +import WKRUIKit + +import os.log + +#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) +import FirebasePerformance +#endif + +extension GKJoinViewController: GKMatchDelegate { + + func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) { + os_log("%{public}s", log: .gameKit, type: .info, #function) + + if isPublicRace { + publicRaceProcess(data: data, from: player) + } else { + if let object = try? JSONDecoder().decode(StartMessage.self, from: data) { + self.transitionToGame(for: .gameKitPrivate(match: match, isHost: false), settings: object.gameSettings) + } else if let message = try? JSONDecoder().decode(MiniMessage.self, from: data) { + DispatchQueue.main.async { + switch message.info { + case .connected: + os_log("%{public}s: connected", log: .gameKit, type: .info, #function) + self.model.title = "Waiting for host" + case .cancelled: + os_log("%{public}s: cancelled", log: .gameKit, type: .info, #function) + self.showError(title: "Host cancelled race", message: "") + self.model.title = "Race Cancelled" + self.model.activityOpacity = 0 + } + } + } + } + } + + func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) { + os_log("%{public}s: player: %{public}s, state: %{public}ld", log: .gameKit, type: .info, #function, player.alias, state.rawValue) + + guard state == .connected else { return } + WKRUIPlayerImageManager.shared.connected(to: player, completion: nil) + + guard let data = WKRSeenFinalArticlesStore.encodedLocalPlayerSeenFinalArticles() else { return } + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + try? match.send(data, to: [player], dataMode: .reliable) + } + } + + func match(_ match: GKMatch, didFailWithError error: Error?) { + os_log("%{public}s", log: .gameKit, type: .error, #function, error?.localizedDescription ?? "-") + + showError(title: "Unable To Connect", message: "Please try again later.") + self.model.title = "Race Error" + self.model.activityOpacity = 0 + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController+PublicRace.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController+PublicRace.swift new file mode 100644 index 0000000..4517fe3 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController+PublicRace.swift @@ -0,0 +1,101 @@ +// +// a.swift +// WikiRaces +// +// Created by Andrew Finke on 6/24/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import WKRKit +import os.log + +extension GKJoinViewController { + + // MARK: - Public Races - + + func publicRaceProcess(data: Data, from player: GKPlayer) { + guard let match = match else { fatalError() } + + if isPlayerHost, WKRSeenFinalArticlesStore.isRemoteTransferData(data) { + os_log("%{public}s: remote articles", log: .gameKit, type: .info, #function) + WKRSeenFinalArticlesStore.addRemoteTransferData(data) + } else if let object = try? JSONDecoder().decode(StartMessage.self, from: data) { + os_log("%{public}s: start message", log: .gameKit, type: .info, #function) + + guard let hostAlias = self.publicRaceHostAlias, object.hostName == hostAlias else { + PlayerFirebaseAnalytics.log(event: .globalFailedToFindHost) + let message = "Please try again later." + showError(title: "Unable To Find Best Host", message: message) + model.title = "Race Error" + os_log("%{public}s: wrong host started match: %{public}s, expected: %{public}s", log: .gameKit, type: .info, #function, object.hostName, self.publicRaceHostAlias ?? "-") + return + } + if let data = WKRSeenFinalArticlesStore.encodedLocalPlayerSeenFinalArticles() { + os_log("%{public}s: encoded seen articles", log: .gameKit, type: .info, #function) + try? match.send(data, to: [player], dataMode: .reliable) + } + transitionToGame(for: .gameKitPublic(match: match, isHost: isPlayerHost), settings: object.gameSettings) + } + } + + func publicRaceSendStartMessage() { + os_log("%{public}s", log: .gameKit, type: .info, #function) + + func fail() { + showError(title: "Unable To Start Race", message: "Please try again later.") + model.title = "Race Error" + } + guard let match = match else { + fail() + let info = "findMatch: No valid match" + PlayerFirebaseAnalytics.log(event: .error(info)) + return + } + + let settings = WKRGameSettings() + let message = StartMessage(hostName: GKLocalPlayer.local.alias, gameSettings: settings) + do { + let data = try JSONEncoder().encode(message) + try match.sendData(toAllPlayers: data, with: .reliable) + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + self.transitionToGame(for: .gameKitPublic(match: match, isHost: true), settings: settings) + } + } catch { + os_log("%{public}s: failed to send start message", log: .gameKit, type: .error, #function) + fail() + let info = "sendStartMessageToPlayers: " + error.localizedDescription + PlayerFirebaseAnalytics.log(event: .error(info)) + } + } + + func publicRaceDetermineHost(match: GKMatch) { + os_log("%{public}s", log: .gameKit, type: .info, #function) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + self.model.title = "Finding best host" + var players = match.players + players.append(GKLocalPlayer.local) + if let hostPlayer = players.sorted(by: { $0.alias > $1.alias }).first { + self.publicRaceHostAlias = hostPlayer.alias + os_log("%{public}s: determined host: %{public}s", log: .gameKit, type: .info, #function, hostPlayer.alias) + + if hostPlayer.alias == GKLocalPlayer.local.alias { + self.isPlayerHost = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { + self.publicRaceSendStartMessage() + }) + } + } else { + os_log("%{public}s: no host", log: .gameKit, type: .error, #function) + + let info = "matchmaker...didFind: No host player" + PlayerFirebaseAnalytics.log(event: .error(info)) + PlayerFirebaseAnalytics.log(event: .globalFailedToFindHost) + self.showError(title: "Unable To Find Best Host", message: "Please try again later.") + self.model.title = "Race Error" + } + } + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController.swift new file mode 100644 index 0000000..bc9ba63 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GKJoinViewController/GKJoinViewController.swift @@ -0,0 +1,113 @@ +// +// GKJoinViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 1/25/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import GameKit +import SwiftUI +import os.log + +import WKRKit +import WKRUIKit + +#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) +import FirebasePerformance +#endif + +final class GKJoinViewController: GKConnectViewController { + + // MARK: - Properties - + + let raceCode: String? + + let isPublicRace: Bool + var publicRaceHostAlias: String? + + var model = LoadingContentViewModel() + lazy var contentViewHosting = UIHostingController( + rootView: LoadingContentView(model: model, cancel: { [weak self] in + self?.isShowingError = true + self?.cancelMatch() + }, disclaimerButton: nil)) + + // MARK: - Initalization - + + init(raceCode: String?) { + self.raceCode = raceCode + self.isPublicRace = raceCode == nil + super.init(isPlayerHost: false) + + os_log("%{public}s", log: .gameKit, type: .info, #function) + + joinMatch() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Life Cycle - + + override func viewDidLoad() { + super.viewDidLoad() + configure(hostingView: contentViewHosting.view) + } + + // MARK: - Helpers - + + func joinMatch() { + os_log("%{public}s: race code: %{public}s", log: .gameKit, type: .info, #function, raceCode ?? "-") + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + let type = raceCode == nil ? "Public" : "Private" + let findTrace = Performance.startTrace(name: "Global Race Find Trace - " + type) + #endif + + DispatchQueue.main.async { + if self.raceCode == nil { + self.model.title = "searching for race" + } else { + self.model.title = "joining race" + } + } + + GKMatchmaker.shared().findMatch(for: GKMatchRequest.joinRequest(raceCode: raceCode)) { [weak self] match, error in + DispatchQueue.main.async { + guard let self = self else { return } + if let error = error { + os_log("%{public}s: result: error: %{public}s", log: .gameKit, type: .error, #function, error.localizedDescription) + + let bannerTitle: String + let interfaceTitle: String + if self.isPublicRace { + bannerTitle = "Unable To Find Race" + interfaceTitle = "NO OPEN RACES" + } else { + bannerTitle = "Unable To Join Race" + interfaceTitle = "MATCHMAKING ISSUE" + } + self.showError(title: bannerTitle, message: "Please try again later.") + self.model.title = interfaceTitle + self.model.activityOpacity = 0 + } else if let match = match { + os_log("%{public}s: found match", log: .gameKit, type: .info, #function) + + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + findTrace?.stop() + #endif + self.match = match + match.delegate = self + + if self.isPublicRace { + self.publicRaceDetermineHost(match: match) + } + } else { + fatalError() + } + } + } + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController+Match.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController+Match.swift deleted file mode 100644 index 66b8d34..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController+Match.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// GameKitConnectViewController+Match.swift -// WikiRaces -// -// Created by Andrew Finke on 1/26/19. -// Copyright © 2019 Andrew Finke. All rights reserved. -// - -import GameKit -import WKRKit - -#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) -import FirebasePerformance -#endif - -extension GameKitConnectViewController: GKMatchDelegate, GKMatchmakerViewControllerDelegate { - - // MARK: - Helpers - - - func findMatch() { - let request = GKMatchRequest() - request.minPlayers = 2 - request.defaultNumberOfPlayers = 2 - let maxPlayerCount = min(WKRKitConstants.current.maxGlobalRacePlayers, - GKMatchRequest.maxPlayersAllowedForMatch(of: .peerToPeer)) - request.maxPlayers = maxPlayerCount - if let invite = GlobalRaceHelper.shared.lastInvite, - let controller = GKMatchmakerViewController(invite: invite) { - - PlayerAnonymousMetrics.log(event: .userAction("issue#119: invite")) - - controller.matchmakerDelegate = self - present(controller, animated: true, completion: nil) - self.controller = controller - GlobalRaceHelper.shared.lastInvite = nil - } else if let controller = GKMatchmakerViewController(matchRequest: request) { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: matchRequest")) - - controller.matchmakerDelegate = self - present(controller, animated: true, completion: nil) - self.controller = controller - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - findTrace = Performance.startTrace(name: "Global Race Find Trace") - #endif - } else { - showError(title: "Unable To Find Match", message: "Please try again later.") - let info = "findMatch: No valid controller" - PlayerAnonymousMetrics.log(event: .error(info)) - } - } - - func sendStartMessageToPlayers() { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: sendStartMessageToPlayers")) - - func fail() { - showError(title: "Unable To Start Match", - message: "Please try again later.") - } - guard let match = match else { - fail() - let info = "findMatch: No valid match" - PlayerAnonymousMetrics.log(event: .error(info)) - return - } - - let settings = WKRGameSettings() - let message = StartMessage(hostName: GKLocalPlayer.local.alias, gameSettings: settings) - do { - let data = try JSONEncoder().encode(message) - try match.sendData(toAllPlayers: data, with: .reliable) - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - self.showMatch(for: .gameKit(match: match, - isHost: true), - settings: settings, - andHide: []) - } - } catch { - fail() - let info = "sendStartMessageToPlayers: " + error.localizedDescription - PlayerAnonymousMetrics.log(event: .error(info)) - } - } - - // MARK: - GKMatchDelegate - - - func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: didReceive")) - - if isPlayerHost, WKRSeenFinalArticlesStore.isRemoteTransferData(data) { - WKRSeenFinalArticlesStore.addRemoteTransferData(data) - } else if let object = try? JSONDecoder().decode(StartMessage.self, from: data) { - guard let hostAlias = self.hostPlayerAlias, object.hostName == hostAlias else { - PlayerAnonymousMetrics.log(event: .globalFailedToFindHost) - let message = "Please try again later." - showError(title: "Unable To Find Best Host", message: message) - return - } - if let data = WKRSeenFinalArticlesStore.encodedLocalPlayerSeenFinalArticles() { - try? match.send(data, to: [player], dataMode: .reliable) - } - showMatch(for: .gameKit(match: match, - isHost: isPlayerHost), - settings: object.gameSettings, - andHide: []) - } - } - - // MARK: - GKMatchmakerViewControllerDelegate - - - func matchmakerViewControllerWasCancelled(_ viewController: GKMatchmakerViewController) { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: wasCancelled")) - - dismiss(animated: true) { - self.pressedCancelButton() - } - } - - func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFailWithError error: Error) { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: didFailWithError")) - - let info = "matchmaker...didFailWithError: " + error.localizedDescription - PlayerAnonymousMetrics.log(event: .error(info)) - - dismiss(animated: true) { - self.showError(title: "Unable To Find Match", - message: "Please try again later.") - } - } - - func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFind match: GKMatch) { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: didFind")) - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - DispatchQueue.global().async { - self.findTrace?.stop() - } - #endif - updateDescriptionLabel(to: "Finding best host") - - dismiss(animated: true) { - self.toggleCoreInterface(isHidden: false, duration: 0.25) - } - - match.delegate = self - self.match = match - - PlayerAnonymousMetrics.log(event: .userAction("issue#119: didFind match set")) - - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - var players = match.players - players.append(GKLocalPlayer.local) - if let hostPlayer = players.sorted(by: { $0.alias > $1.alias }).first { - self.hostPlayerAlias = hostPlayer.alias - if hostPlayer.alias == GKLocalPlayer.local.alias { - self.isPlayerHost = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { - self.sendStartMessageToPlayers() - }) - } - } else { - let info = "matchmaker...didFind: No host player" - PlayerAnonymousMetrics.log(event: .error(info)) - PlayerAnonymousMetrics.log(event: .globalFailedToFindHost) - self.showError(title: "Unable To Find Best Host", - message: "Please try again later.") - } - } - - GlobalRaceHelper.shared.lastInvite = nil - } -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController.swift deleted file mode 100644 index 45e9ba1..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// GameKitMatchmakingViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 1/25/19. -// Copyright © 2019 Andrew Finke. All rights reserved. -// - -import UIKit -import GameKit - -import WKRKit - -#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) -import FirebasePerformance -import FirebaseAnalytics -import Crashlytics -#endif - -final class GameKitConnectViewController: ConnectViewController { - - // MARK: - Properties - - - var isPlayerHost = false - var hostPlayerAlias: String? - var match: GKMatch? - weak var controller: GKMatchmakerViewController? - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - var findTrace: Trace? - #endif - - // MARK: - View Life Cycle - - - override func viewDidLoad() { - super.viewDidLoad() - setupCoreInterface() - - onQuit = { [weak self] in - guard let self = self else { return } - self.teardown() - } - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - let playerName = GKLocalPlayer.local.alias - Crashlytics.sharedInstance().setUserName(playerName) - Analytics.setUserProperty(playerName, forName: "playerName") - #endif - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - PlayerAnonymousMetrics.log(event: .userAction("issue#119: viewDidAppear")) - - guard isFirstAppear else { - return - } - isFirstAppear = false - - runConnectionTest { [weak self] success in - guard let self = self else { return } - if success { - self.toggleCoreInterface(isHidden: true, duration: 0.25) - self.findMatch() - } else if !success { - self.showConnectionSpeedError() - } - } - - toggleCoreInterface(isHidden: false, duration: 0.5) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - PlayerAnonymousMetrics.log(event: .userAction("issue#119: viewDidDisappear")) - } - - private func teardown() { - PlayerAnonymousMetrics.log(event: .userAction("issue#119: teardown game connect")) - controller?.matchmakerDelegate = nil - match?.delegate = nil - match?.disconnect() - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/LoadingContentView.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/LoadingContentView.swift new file mode 100644 index 0000000..4798825 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/LoadingContentView.swift @@ -0,0 +1,51 @@ +// +// LoadingContentView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/27/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI + +struct LoadingContentView: View { + + @ObservedObject var model = LoadingContentViewModel() + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let cancel: () -> Void + var disclaimerButton: (() -> Void)? + + var body: some View { + GeometryReader { geometry in + VStack { + Spacer() + Text(self.model.title.uppercased()) + .font(.system(size: 20, weight: .medium)) + .padding() + ActivityIndicatorView() + .opacity(self.model.activityOpacity) + .animation(.easeInOut(duration: 0.5), value: self.model.activityOpacity) + Color.clear.frame(height: geometry.size.height * 0.5) + + Button(action: { + self.disclaimerButton?() + }, label: { + Text(self.model.disclaimerButtonTitle) + .font(.system(size: 16, weight: .regular)) + .foregroundColor(Color(.systemBlue)) + .opacity(self.model.disclaimerButtonOpacity) + .animation(.easeInOut(duration: 0.5), value: self.model.disclaimerButtonOpacity) + }) + Button(action: self.cancel, label: { + Image(systemName: "chevron.left") + .font(.system(size: 22)) + .padding() + .padding() + }) + .foregroundColor(.wkrTextColor(for: self.colorScheme)) + Spacer() + } + } + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/LoadingContentViewModel.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/LoadingContentViewModel.swift new file mode 100644 index 0000000..6b9f04b --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/LoadingContentViewModel.swift @@ -0,0 +1,17 @@ +// +// LoadingContentViewModel.swift +// WikiRaces +// +// Created by Andrew Finke on 6/27/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation + +class LoadingContentViewModel: ObservableObject { + @Published var title: String = "" + @Published var activityOpacity: Double = 1 + + @Published var disclaimerButtonTitle: String = " " + @Published var disclaimerButtonOpacity: Double = 0 +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+Invite.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+Invite.swift deleted file mode 100644 index 73fd6ed..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+Invite.swift +++ /dev/null @@ -1,200 +0,0 @@ -// -// MPCConnectViewController+Invite.swift -// WikiRaces -// -// Created by Andrew Finke on 9/6/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import MultipeerConnectivity -import UIKit - -import WKRKit - -#if !MULTIWINDOWDEBUG -import FirebasePerformance -#endif - -extension MPCConnectViewController: MCNearbyServiceAdvertiserDelegate, MCSessionDelegate { - - // MARK: - MCSessionDelegate - - - func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - guard let object = try? JSONDecoder().decode(StartMessage.self, from: data) else { - return - } - - guard let hostName = hostPeerID?.displayName, object.hostName == hostName else { - let info = "session...didReceive: \(String(describing: hostPeerID?.displayName)), \(object.hostName)" - PlayerAnonymousMetrics.log(event: .error(info)) - - DispatchQueue.main.async { - self.showError(title: "Connection Issue", - message: "The connection to the host was lost.") - } - return - } - - session.delegate = nil - showMatch(for: .mpc(serviceType: serviceType, - session: session, - isHost: isPlayerHost), - settings: object.gameSettings, - andHide: [inviteView]) - } - - func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { - DispatchQueue.main.async { - if state == .connected && peerID == self.hostPeerID { - self.updateDescriptionLabel(to: "WAITING FOR HOST") - - DispatchQueue.global().asyncAfter(deadline: .now() + 0.25, execute: { - if let data = WKRSeenFinalArticlesStore.encodedLocalPlayerSeenFinalArticles() { - try? session.send(data, toPeers: [peerID], with: .reliable) - } - }) - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - self.connectingTrace?.stop() - self.connectingTrace = nil - #endif - } else if state == .notConnected && peerID == self.hostPeerID { - let info = "session...didChange: Host not connected" - PlayerAnonymousMetrics.log(event: .error(info)) - self.showError(title: "Connection Issue", message: "The connection to the host was lost.") - } - } - } - - func session(_ session: MCSession, - didReceive stream: InputStream, - withName streamName: String, - fromPeer peerID: MCPeerID) {} - func session(_ session: MCSession, - didStartReceivingResourceWithName resourceName: String, - fromPeer peerID: MCPeerID, - with progress: Progress) {} - func session(_ session: MCSession, - didFinishReceivingResourceWithName resourceName: String, - fromPeer peerID: MCPeerID, - at localURL: URL?, - withError error: Error?) {} - - func startAdvertising() { - session.delegate = self - advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType) - advertiser?.delegate = self - advertiser?.startAdvertisingPeer() - updateDescriptionLabel(to: "WAITING FOR INVITE") - } - - // MARK: - MCAdvertiserAssistantDelegate - - - func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) { - let info = "didNotStartAdvertisingPeer: " + error.localizedDescription - PlayerAnonymousMetrics.log(event: .error(info)) - } - - func advertiser(_ advertiser: MCNearbyServiceAdvertiser, - didReceiveInvitationFromPeer peerID: MCPeerID, - withContext context: Data?, - invitationHandler: @escaping (Bool, MCSession?) -> Void) { - - guard let data = context, let object = try? JSONDecoder().decode(MPCHostContext.self, from: data) else { - return - } - invites.append((invitationHandler, peerID, object)) - showNextInvite() - } - - /// Shows the next invite - func showNextInvite() { - guard !invites.isEmpty && !isShowingInvite else { - return - } - - UINotificationFeedbackGenerator().notificationOccurred(.warning) - - isShowingInvite = true - - let invite = invites.removeFirst() - activeInvite = invite.handler - hostContext = invite.context - hostPeerID = invite.host - - hostNameLabel.text = "FROM " + invite.host.displayName.uppercased() - UIView.animate(withDuration: 0.25, animations: { - self.activityIndicatorView.alpha = 0.0 - self.inviteView.alpha = 1.0 - }) - updateDescriptionLabel(to: "INVITE RECEIVED") - - activeInviteTimeoutTimer?.invalidate() - let timeout: TimeInterval = invite.context.inviteTimeout - activeInviteTimeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, - repeats: false, - block: { [weak self] _ in - self?.declineInvite() - }) - - if invite.context.minPeerAppBuild > Bundle.main.appInfo.build { - let info = "showNextInvite: \(invite.context.minPeerAppBuild) > \(Bundle.main.appInfo.build)" - PlayerAnonymousMetrics.log(event: .error(info)) - - let message = "You received an invite to a race that requires the latest version of WikiRaces. Please download the lastest update on the App Store." - showError(title: "Update Required", message: message) - } else if invite.context.minPeerAppBuild < MPCHostContext.minBuildToJoinRemoteHost { - let info = "showNextInvite: \(invite.context.minPeerAppBuild) < \(MPCHostContext.minBuildToJoinRemoteHost)" - PlayerAnonymousMetrics.log(event: .error(info)) - - let message = "You received an invite to a race from a host with an old version of WikiRaces. Please have the host download the lastest update on the App Store." - showError(title: "Host Update Required", message: message) - } - } - - // MARK: - User Actions - - - /// Accepts the displayed invite - @objc - func acceptInvite() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - activeInviteTimeoutTimer?.invalidate() - - activeInvite?(true, session) - updateDescriptionLabel(to: "CONNECTING TO HOST") - - isShowingInvite = false - - UIView.animate(withDuration: 0.5) { - self.activityIndicatorView.alpha = 1.0 - self.inviteView.alpha = 0.0 - } - - stopAdvertising() - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - connectingTrace = Performance.startTrace(name: "Player Connecting Trace") - #endif - } - - /// Declines the displayed invite - @objc - func declineInvite() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - - activeInvite?(false, session) - - updateDescriptionLabel(to: "WAITING FOR INVITE") - - UIView.animate(withDuration: 0.25, animations: { - self.descriptionLabel.alpha = 1.0 - self.activityIndicatorView.alpha = 1.0 - self.cancelButton.alpha = 1.0 - self.inviteView.alpha = 0.0 - }, completion: { _ in - self.isShowingInvite = false - self.showNextInvite() - }) - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+KB.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+KB.swift deleted file mode 100644 index 65cffcb..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+KB.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// MPCConnectViewController+KB.swift -// WikiRaces -// -// Created by Andrew Finke on 9/15/18. -// Copyright © 2018 Andrew Finke. All rights reserved. -// - -import UIKit - -extension MPCConnectViewController { - - // MARK: - Keyboard Support - - - override var keyCommands: [UIKeyCommand]? { - var commands = [ - UIKeyCommand(title: "Return to Menu", - action: #selector(keyboardQuit), - input: UIKeyCommand.inputEscape, - modifierFlags: []) - ] - - if isShowingInvite { - let inviteCommands = [ - UIKeyCommand(title: "Accept Invite", - action: #selector(keyboardAttemptAcceptInvite), - input: "a", - modifierFlags: .command), - UIKeyCommand(title: "Decline Invite", - action: #selector(keyboardAttemptDeclineInvite), - input: "d", - modifierFlags: .command) - ] - commands.append(contentsOf: inviteCommands) - } - return commands - } - - @objc - private func keyboardQuit() { - pressedCancelButton() - } - - @objc - private func keyboardAttemptAcceptInvite() { - guard isShowingInvite else { return } - acceptInvite() - } - - @objc - private func keyboardAttemptDeclineInvite() { - guard isShowingInvite else { return } - declineInvite() - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+UI.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+UI.swift deleted file mode 100644 index 7703466..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+UI.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// MPCConnectViewController+UI.swift -// WikiRaces -// -// Created by Andrew Finke on 9/15/18. -// Copyright © 2018 Andrew Finke. All rights reserved. -// - -import UIKit - -extension MPCConnectViewController { - - // MARK: - Interface - - - func setupInviteInterface() { - inviteView.alpha = 0.0 - inviteView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(inviteView) - - hostNameLabel.text = "" - hostNameLabel.numberOfLines = 0 - hostNameLabel.textAlignment = .center - hostNameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium) - hostNameLabel.translatesAutoresizingMaskIntoConstraints = false - inviteView.addSubview(hostNameLabel) - - acceptButton.setTitle("Accept", for: .normal) - acceptButton.setTitleColor(UIColor(red: 0, green: 122.0/255.0, blue: 1.0, alpha: 1.0), for: .normal) - acceptButton.titleLabel?.font = UIFont.systemFont(ofSize: 19, weight: .medium) - acceptButton.translatesAutoresizingMaskIntoConstraints = false - acceptButton.addTarget(self, action: #selector(acceptInvite), for: .touchUpInside) - inviteView.addSubview(acceptButton) - - declineButton.setTitle("Decline", for: .normal) - declineButton.setTitleColor(UIColor(red: 1, green: 0, blue: 0, alpha: 1.0), for: .normal) - declineButton.titleLabel?.font = UIFont.systemFont(ofSize: 19, weight: .medium) - declineButton.translatesAutoresizingMaskIntoConstraints = false - declineButton.addTarget(self, action: #selector(declineInvite), for: .touchUpInside) - inviteView.addSubview(declineButton) - - if #available(iOS 13.4, *) { - acceptButton.isPointerInteractionEnabled = true - declineButton.isPointerInteractionEnabled = true - } - - setupConstraints() - } - - private func setupConstraints() { - let offset: CGFloat = 20 - let constraints = [ - inviteView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 10), - inviteView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10), - inviteView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 10), - - hostNameLabel.leftAnchor.constraint(equalTo: inviteView.leftAnchor), - hostNameLabel.rightAnchor.constraint(equalTo: inviteView.rightAnchor), - hostNameLabel.topAnchor.constraint(equalTo: inviteView.topAnchor), - - declineButton.rightAnchor.constraint(equalTo: inviteView.centerXAnchor, constant: -offset), - declineButton.topAnchor.constraint(equalTo: hostNameLabel.bottomAnchor, constant: 50), - declineButton.bottomAnchor.constraint(lessThanOrEqualTo: inviteView.bottomAnchor), - - acceptButton.leftAnchor.constraint(equalTo: inviteView.centerXAnchor, constant: offset), - acceptButton.topAnchor.constraint(equalTo: declineButton.topAnchor), - acceptButton.bottomAnchor.constraint(equalTo: declineButton.bottomAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController.swift deleted file mode 100644 index 210ddaa..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// MPCConnectViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 9/6/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import GameKit -import MultipeerConnectivity -import UIKit - -import WKRKit -import WKRUIKit - -#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) -import FirebasePerformance -import FirebaseAnalytics -import Crashlytics -#endif - -final internal class MPCConnectViewController: ConnectViewController { - - // MARK: - Interface Elements - - - let inviteView = UIView() - let hostNameLabel = UILabel() - let acceptButton = UIButton() - let declineButton = UIButton() - - // MARK: - Properties - - - var playerName = UIDevice.current.name - var isValidPlayerName = false - var isSolo = false - var isPlayerHost = false - var isShowingInvite = false - - // MARK: - MPC Properties - - - var advertiser: MCNearbyServiceAdvertiser? - var activeInvite: ((Bool, MCSession) -> Void)? - var activeInviteTimeoutTimer: Timer? - - var invites = [(handler: ((Bool, MCSession) -> Void)?, host: MCPeerID, context: MPCHostContext)]() - - var peerID: MCPeerID! - var hostPeerID: MCPeerID? - var hostContext: MPCHostContext? - - let serviceType = "WKRPeer30" - lazy var session: MCSession = { - return MCSession(peer: self.peerID) - }() - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - var connectingTrace: Trace? - #endif - - // MARK: - View Life Cycle - - - override func viewDidLoad() { - super.viewDidLoad() - - // Gets either the player name specified in settings.app, then GK alias, the device name - if let name = UserDefaults.standard.object(forKey: "name_preference") as? String { - playerName = name - PlayerAnonymousMetrics.log(event: .nameType, attributes: ["Type": "CustomName"]) - } else if GKLocalPlayer.local.isAuthenticated { - playerName = GKLocalPlayer.local.alias - PlayerAnonymousMetrics.log(event: .nameType, attributes: ["Type": "GCAlias"]) - } else { - PlayerAnonymousMetrics.log(event: .nameType, attributes: ["Type": "DeviceName"]) - } - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - Crashlytics.sharedInstance().setUserName(playerName) - Analytics.setUserProperty(playerName, forName: "playerName") - #endif - - PlayerAnonymousMetrics.log(event: .userAction("Using player name \(playerName)")) - isValidPlayerName = playerName.utf8.count > 0 && playerName.utf8.count < 40 - guard isValidPlayerName else { return } - - // Uses existing peer ID object if already created (recommended per Apple docs) - if let pastPeerIDData = UserDefaults.standard.data(forKey: "PeerID"), - let lastPeerID = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, - from: pastPeerIDData), - lastPeerID.displayName == playerName { - peerID = lastPeerID - } else { - // Attempting to prevent https://github.com/atfinke/WikiRaces/issues/43 - // Also, see rdar://47570877 - UserDefaults.standard.set(true, forKey: "AttemptingMCPeerIDCreation") - peerID = MCPeerID(displayName: playerName) - UserDefaults.standard.set(false, forKey: "AttemptingMCPeerIDCreation") - if let peerID = peerID, - let data = try? NSKeyedArchiver.archivedData(withRootObject: peerID, - requiringSecureCoding: true) { - - UserDefaults.standard.set(data, forKey: "PeerID") - } - } - - setupCoreInterface() - setupInviteInterface() - - onQuit = { [weak self] in - self?.session.delegate = nil - self?.session.disconnect() - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - stopAdvertising() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - guard isFirstAppear else { - return - } - isFirstAppear = false - - // Test the connection to Wikipedia - runConnectionTest { [weak self] success in - guard let self = self else { return } - if success && self.isValidPlayerName { - if self.isPlayerHost { - self.toggleCoreInterface(isHidden: true, - duration: 0.25, - and: [self.inviteView], - completion: { [weak self] in - self?.presentHostInterface() - }) - } else { - self.startAdvertising() - } - } else if !success { - self.showConnectionSpeedError() - } - } - - toggleCoreInterface(isHidden: false, duration: 0.5) - - if !isValidPlayerName { - let info = "viewDidAppear...isValidPlayerName: " + playerName - PlayerAnonymousMetrics.log(event: .error(info)) - - let length = playerName.count == 0 ? "Short" : "Long" - let message = "Your player name is too \(length.lowercased()). " - showError(title: "Player Name Too \(length)", - message: message + "Would you like to open settings to adjust it?", - showSettingsButton: true) - } - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - hostNameLabel.textColor = .wkrSubtitleTextColor(for: traitCollection) - } - - // MARK: - State Changes - - - func stopAdvertising() { - advertiser?.stopAdvertisingPeer() - - // Reject all the pending invites - for invite in invites { - invite.handler?(false, session) - } - } - - func presentHostInterface() { - let controller = MPCHostViewController(style: .grouped) - controller.peerID = peerID - controller.session = session - controller.serviceType = serviceType - controller.listenerUpdate = { [weak self] update in - guard let self = self else { return } - switch update { - case .startMatch(let isSolo, let settings): - self.isSolo = isSolo - self.dismiss(animated: true, completion: { - var networkConfig: WKRPeerNetworkConfig = .solo(name: self.playerName) - if !isSolo { - networkConfig = .mpc(serviceType: self.serviceType, - session: self.session, - isHost: self.isPlayerHost) - } - self.showMatch(for: networkConfig, settings: settings, andHide: []) - }) - case .cancel: - self.dismiss(animated: true, completion: { - self.navigationController?.popToRootViewController(animated: false) - }) - } - } - - let nav = WKRUINavigationController(rootViewController: controller) - nav.modalPresentationStyle = .fullScreen - nav.isModalInPresentation = true - present(nav, animated: true, completion: nil) - } - - override func showError(title: String, message: String, showSettingsButton: Bool = false) { - activeInvite?(false, session) - invites.forEach { $0.handler?(false, session) } - super.showError(title: title, message: message, showSettingsButton: showSettingsButton) - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift deleted file mode 100644 index 5715983..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MPCHostContext.swift -// WikiRaces -// -// Created by Andrew Finke on 1/30/19. -// Copyright © 2019 Andrew Finke. All rights reserved. -// - -import Foundation - -struct MPCHostContext: Codable { - - static let minBuildToJoinLocalHost: Int = 7000 - static let minBuildToJoinRemoteHost: Int = 7000 - - let appBuild: Int - let appVersion: String - let name: String - - let inviteTimeout: TimeInterval - let minPeerAppBuild: Int -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostAutoInviteCell.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostAutoInviteCell.swift deleted file mode 100644 index 37ff00e..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostAutoInviteCell.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// MPCHostAutoInviteCell.swift -// WikiRaces -// -// Created by Andrew Finke on 11/6/19. -// Copyright © 2019 Andrew Finke. All rights reserved. -// - -import UIKit -import WKRUIKit - -final internal class MPCHostAutoInviteCell: UITableViewCell { - - // MARK: - Properties - - var onToggle: ((Bool) -> Void)? - var isEnabled: Bool = false { - didSet { - toggle.isOn = isEnabled - onToggle?(isEnabled) - } - } - - private let detailLabel = UILabel() - private let toggle = UISwitch() - - static let reuseIdentifier = "hostAutoInviteCell" - - // MARK: - Initialization - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - - detailLabel.text = "Auto-Invite" - detailLabel.textAlignment = .left - detailLabel.font = UIFont.systemFont(ofSize: 17, weight: .regular) - detailLabel.numberOfLines = 0 - detailLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(detailLabel) - - toggle.addTarget(self, action: #selector(toggled), for: .valueChanged) - toggle.translatesAutoresizingMaskIntoConstraints = false - addSubview(toggle) - - let leftMarginConstraint = NSLayoutConstraint(item: detailLabel, - attribute: .left, - relatedBy: .equal, - toItem: self, - attribute: .leftMargin, - multiplier: 1.0, - constant: 0.0) - - let rightMarginConstraint = NSLayoutConstraint(item: toggle, - attribute: .right, - relatedBy: .equal, - toItem: self, - attribute: .rightMargin, - multiplier: 1.0, - constant: 0.0) - - let constraints = [ - leftMarginConstraint, - detailLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), - detailLabel.rightAnchor.constraint(lessThanOrEqualTo: toggle.leftAnchor, - constant: -10), - detailLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), - - rightMarginConstraint, - toggle.centerYAnchor.constraint(equalTo: centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View Life Cycle - - - public override func layoutSubviews() { - super.layoutSubviews() - let textColor = UIColor.wkrTextColor(for: traitCollection) - toggle.onTintColor = textColor - detailLabel.textColor = textColor - } - - // MARK: - Helpers - - - @objc - func toggled() { - isEnabled = toggle.isOn - UISelectionFeedbackGenerator().selectionChanged() - } -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostPeerStateCell.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostPeerStateCell.swift deleted file mode 100644 index 58d5255..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostPeerStateCell.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// MPCHostPeerStateCell.swift -// WikiRaces -// -// Created by Andrew Finke on 9/15/18 -// Copyright © 2018 Andrew Finke. All rights reserved. -// - -import UIKit - -final internal class MPCHostPeerStateCell: UITableViewCell { - - // MARK: - Properties - - let peerLabel = UILabel() - let detailLabel = UILabel() - - static let reuseIdentifier = "reuseIdentifier" - - // MARK: - Initialization - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - peerLabel.textAlignment = .left - peerLabel.font = UIFont.systemFont(ofSize: 17, weight: .regular) - peerLabel.numberOfLines = 0 - peerLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(peerLabel) - - detailLabel.textAlignment = .right - detailLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - detailLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(detailLabel) - - let leftMarginConstraint = NSLayoutConstraint(item: peerLabel, - attribute: .left, - relatedBy: .equal, - toItem: self, - attribute: .leftMargin, - multiplier: 1.0, - constant: 0.0) - - let rightMarginConstraint = NSLayoutConstraint(item: detailLabel, - attribute: .right, - relatedBy: .equal, - toItem: self, - attribute: .rightMargin, - multiplier: 1.0, - constant: 0.0) - - let constraints = [ - leftMarginConstraint, - peerLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), - peerLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 25), - peerLabel.rightAnchor.constraint(lessThanOrEqualTo: detailLabel.leftAnchor, - constant: -10), - peerLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), - - rightMarginConstraint, - detailLabel.centerYAnchor.constraint(equalTo: centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View Life Cycle - - - public override func layoutSubviews() { - super.layoutSubviews() - let textColor = UIColor.wkrTextColor(for: traitCollection) - peerLabel.textColor = textColor - detailLabel.textColor = textColor - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSearchingCell.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSearchingCell.swift deleted file mode 100644 index f32134f..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSearchingCell.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// MPCHostSearchingCell.swift -// WikiRaces -// -// Created by Andrew Finke on 12/26/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit - -final internal class MPCHostSearchingCell: UITableViewCell { - - // MARK: - Properties - - - private var dots: Int = 3 - private var timer: Timer? - - static let reuseIdentifier = "searchingCell" - - // MARK: - Initialization - - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - isUserInteractionEnabled = false - textLabel?.textColor = UIColor(red: 184.0 / 255.0, - green: 184.0 / 255.0, - blue: 184.0 / 255.0, - alpha: 1.0) - - updateText() - timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in - self?.updateText() - }) - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - // MARK: - Helpers - - - func updateText() { - dots += 1 - if dots >= 4 { - dots = 0 - } - textLabel?.text = "Searching" + (0.. Int { - return 4 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - if peers.isEmpty { - return 1 - } else { - return peers.count - } - } else { - return 1 - } - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - if section == 0 { - return "Choose 1 to 7 players" - } else if section == 1 { - return nil - } else if section == 2 { - return nil - } else { - return nil - } - } - - override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { - if section == 0 { - return "Make sure all players are on the same Wi-Fi network and have Bluetooth enabled for the best results." - } else if section == 1 { - return "Automatically invite nearby players to the race." - } else if section == 2 { - return nil - } else { - return "Practice your skills in solo races. Solo races will not count towards your stats." - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 1 { - guard let cell = tableView.dequeueReusableCell(withIdentifier: MPCHostAutoInviteCell.reuseIdentifier, - for: indexPath) as? MPCHostAutoInviteCell else { - fatalError() - } - cell.isEnabled = isAutoInviteOn - cell.onToggle = { [weak self] toggle in - self?.isAutoInviteOn = toggle - PlayerAnonymousMetrics.log(event: .autoInviteToggled) - } - return cell - } else if indexPath.section == 2 { - let cell = UITableViewCell(style: .value1, reuseIdentifier: nil) - cell.textLabel?.text = "Customize Race" - cell.detailTextLabel?.text = gameSettings.isCustom ? "Custom" : "Standard" - cell.accessoryType = .disclosureIndicator - return cell - } else if indexPath.section == 3 { - return tableView.dequeueReusableCell(withIdentifier: MPCHostSoloCell.reuseIdentifier, - for: indexPath) - } else if peers.isEmpty { - return tableView.dequeueReusableCell(withIdentifier: MPCHostSearchingCell.reuseIdentifier, - for: indexPath) - } - - guard let cell = tableView.dequeueReusableCell(withIdentifier: MPCHostPeerStateCell.reuseIdentifier, for: indexPath) as? MPCHostPeerStateCell else { - fatalError() - } - - let peerID = sortedPeers[indexPath.row] - guard let state = peers[peerID] else { - fatalError("No state for peerID: \(peerID)") - } - - cell.peerLabel.text = peerID.displayName - if state == .found { - cell.detailLabel.text = nil - cell.isUserInteractionEnabled = true - } else { - cell.detailLabel.text = peers[peerID]?.rawValue.capitalized - cell.isUserInteractionEnabled = state == .found - } - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.section == 1 { return } - - PlayerAnonymousMetrics.log(event: .userAction(#function)) - - if indexPath.section == 2 { - PlayerAnonymousMetrics.log(event: .customRaceOpened) - - let controller = CustomRaceViewController(settings: gameSettings) - controller.allCustomPages = allCustomPages - navigationController?.pushViewController(controller, animated: true) - self.gameSettingsController = controller - return - } else if indexPath.section == 3 { - PlayerAnonymousMetrics.log(event: .hostStartedSoloMatch) - - session?.disconnect() - listenerUpdate?(.startMatch(isSolo: true, settings: gameSettings)) - tableView.isUserInteractionEnabled = false - return - } - - // Hits this case when the "Searching..." placeholder cell is selected - guard !peers.isEmpty else { return } - - let peerID = sortedPeers[indexPath.row] - invite(peerID: peerID) - - tableView.deselectRow(at: indexPath, animated: true) - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if (indexPath.section == 0 && peers.isEmpty) || indexPath.section == 1 { - return 44.0 - } - return super.tableView(tableView, heightForRowAt: indexPath) - } - - func invite(peerID: MCPeerID) { - guard let session = session else { - fatalError("Session is nil") - } - - let maxPlayerCount = min(WKRKitConstants.current.maxLocalRacePlayers, - kMCSessionMaximumNumberOfPeers) - let peerCount = session.connectedPeers.count - guard maxPlayerCount > peerCount + 1 else { return } - - if session.connectedPeers.map({ $0.displayName }).contains(peerID.displayName) { - let alertController = UIAlertController(title: "Duplicate Name", - message: "Player has the same name as another player in the match.", - preferredStyle: .alert) - alertController.addCancelAction(title: "Ok") - present(alertController, animated: true, completion: nil) - } - - update(peerID: peerID, to: .invited) - - let appInfo = Bundle.main.appInfo - let context = MPCHostContext(appBuild: appInfo.build, - appVersion: appInfo.version, - name: session.myPeerID.displayName, - inviteTimeout: 45.0, - minPeerAppBuild: MPCHostContext.minBuildToJoinLocalHost) - guard let data = try? JSONEncoder().encode(context) else { - fatalError("Couldn't encode context") - } - - browser?.invitePeer(peerID, - to: session, - withContext: data, - timeout: context.inviteTimeout) - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController.swift deleted file mode 100644 index 3d53743..0000000 --- a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController.swift +++ /dev/null @@ -1,322 +0,0 @@ -// -// MPCHostViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 9/9/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import MultipeerConnectivity -import UIKit - -import WKRKit -import WKRUIKit - -#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) -import FirebasePerformance -#endif - -final internal class MPCHostViewController: UITableViewController, MCSessionDelegate, MCNearbyServiceBrowserDelegate { - - // MARK: - Types - - - enum PeerState: String { - case found - case invited - case joining - case joined - case declined - } - - enum ListenerUpdate { - case startMatch(isSolo: Bool, settings: WKRGameSettings) - case cancel - } - // MARK: - Properties - - - var gameSettings = WKRGameSettings() - var allCustomPages = [WKRPage]() - weak var gameSettingsController: CustomRaceViewController? - - var peers = [MCPeerID: PeerState]() - var sortedPeers: [MCPeerID] { - return peers.keys.sorted(by: { (lhs, rhs) -> Bool in - lhs.displayName < rhs.displayName - }) - } - - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - var peersConnectTraces = [MCPeerID: Trace]() - #endif - - var peerID: MCPeerID? - var session: MCSession? - var serviceType: String? - var browser: MCNearbyServiceBrowser? - - private static let isAutoInviteOnKey = "isAutoInviteOnKey" - var isAutoInviteOn = UserDefaults.standard.bool(forKey: MPCHostViewController.isAutoInviteOnKey) { - didSet { - UserDefaults.standard.set(isAutoInviteOn, forKey: MPCHostViewController.isAutoInviteOnKey) - if isAutoInviteOn { - peers.forEach { peerID, state in - if state == PeerState.found { - self.invite(peerID: peerID) - } - } - } - } - } - - var listenerUpdate: ((ListenerUpdate) -> Void)? - private let activityView = UIActivityIndicatorView(style: .medium) - - // MARK: - View Life Cycle - - - override func viewDidLoad() { - super.viewDidLoad() - title = "CREATE LOCAL RACE" - - guard let peerID = peerID, let serviceType = serviceType else { - fatalError("Required properties peerID or serviceType not set") - } - browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType) - browser?.delegate = self - session?.delegate = self - - navigationItem.leftBarButtonItem = WKRUIBarButtonItem(systemName: "xmark", - target: self, - action: #selector(cancelMatch(_:))) - - let startButton = WKRUIBarButtonItem(systemName: "play.fill", - target: self, - action: #selector(startMatch(_:))) - startButton.isEnabled = false - navigationItem.rightBarButtonItem = startButton - - tableView.estimatedRowHeight = 150 - tableView.rowHeight = UITableView.automaticDimension - tableView.register(MPCHostPeerStateCell.self, - forCellReuseIdentifier: MPCHostPeerStateCell.reuseIdentifier) - tableView.register(MPCHostSearchingCell.self, - forCellReuseIdentifier: MPCHostSearchingCell.reuseIdentifier) - tableView.register(MPCHostAutoInviteCell.self, - forCellReuseIdentifier: MPCHostAutoInviteCell.reuseIdentifier) - tableView.register(MPCHostSoloCell.self, - forCellReuseIdentifier: MPCHostSoloCell.reuseIdentifier) - - PlayerAnonymousMetrics.log(event: .autoInviteState, - attributes: ["On": isAutoInviteOn ? 1 : 0]) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - browser?.startBrowsingForPeers() - - if let controller = gameSettingsController { - allCustomPages = controller.allCustomPages - tableView.reloadRows(at: [IndexPath(item: 0, section: 2)], with: .none) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - browser?.stopBrowsingForPeers() - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - activityView.color = .wkrActivityIndicatorColor(for: traitCollection) - } - - // MARK: - Actions - - - @objc - func cancelMatch(_ sender: Any) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - PlayerAnonymousMetrics.log(event: .hostCancelledPreMatch) - - browser?.stopBrowsingForPeers() - browser?.delegate = nil - - session?.disconnect() - session?.delegate = nil - listenerUpdate?(.cancel) - } - - @objc - func startMatch(_ sender: Any) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - - browser?.stopBrowsingForPeers() - browser?.delegate = nil - - tableView.isUserInteractionEnabled = false - - activityView.sizeToFit() - activityView.startAnimating() - - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityView) - navigationItem.leftBarButtonItem?.isEnabled = false - - guard let session = session else { fatalError("Session is nil") } - do { - let message = ConnectViewController.StartMessage( - hostName: session.myPeerID.displayName, - gameSettings: gameSettings) - let data = try JSONEncoder().encode(message) - try session.send(data, toPeers: session.connectedPeers, with: .reliable) - - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - self.listenerUpdate?(.startMatch(isSolo: false, settings: self.gameSettings)) - } - } catch { - let info = "startMatch: " + error.localizedDescription - PlayerAnonymousMetrics.log(event: .error(info)) - - session.disconnect() - listenerUpdate?(.cancel) - } - } - - /// Updates the peerID to a new state and updates the table view - /// - /// - Parameters: - /// - peerID: Peer ID updated - /// - newState: The new state - func update(peerID: MCPeerID, to newState: PeerState?) { - let newStateString = String(describing: newState?.rawValue) - PlayerAnonymousMetrics.log(event: .gameState("Peer Update: \(peerID.displayName) \(newStateString)")) - - guard let newState = newState else { - if let index = sortedPeers.firstIndex(of: peerID) { - peers[peerID] = nil - if peers.isEmpty { - tableView.reloadRows(at: [IndexPath(row: index)], with: .fade) - } else { - tableView.deleteRows(at: [IndexPath(row: index)], with: .fade) - } - } - return - } - - if let state = peers[peerID], state != newState { - peers[peerID] = newState - if let index = sortedPeers.firstIndex(of: peerID) { - tableView.reloadRows(at: [IndexPath(row: index)], with: .fade) - } else { - tableView.reloadData() - } - } else if peers[peerID] == nil { - peers[peerID] = newState - if let index = sortedPeers.firstIndex(of: peerID) { - if peers.count == 1 { - tableView.reloadRows(at: [IndexPath(row: index)], with: .fade) - } else { - tableView.insertRows(at: [IndexPath(row: index)], with: .left) - } - } else { - tableView.reloadData() - } - } - let joinedPlayers: [MCPeerID: PeerState] = peers.filter({ $0.value == .joined }) - navigationItem.rightBarButtonItem?.isEnabled = !joinedPlayers.isEmpty - performaceTrace(peerID: peerID, newState: newState) - } - - func performaceTrace(peerID: MCPeerID, newState: PeerState?) { - #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) - - let hostInviteResponseTraceName = "Host Invite Response Trace" - let hostInviteJoingTraceName = "Host Invite Joining Trace" - - if newState == .invited { - peersConnectTraces[peerID] = Performance.startTrace(name: hostInviteResponseTraceName) - } else if newState == .declined, - let trace = peersConnectTraces[peerID], - trace.name == hostInviteResponseTraceName { - trace.stop() - } else if newState == .joining, - let trace = peersConnectTraces[peerID], - trace.name == hostInviteResponseTraceName { - trace.stop() - peersConnectTraces[peerID] = Performance.startTrace(name: hostInviteJoingTraceName) - } else if newState == .joined, - let trace = peersConnectTraces[peerID], - trace.name == hostInviteJoingTraceName { - trace.stop() - } - - #endif - } - - // MARK: - MCNearbyServiceBrowserDelegate - - - func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { - DispatchQueue.main.async { - let state = self.peers[peerID] ?? .found - if state != .invited && state != .joining && state != .joined { - self.update(peerID: peerID, to: nil) - } - } - } - - func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) { - let info = "didNotStartBrowsingForPeers: " + error.localizedDescription - PlayerAnonymousMetrics.log(event: .error(info)) - listenerUpdate?(.cancel) - } - - func browser(_ browser: MCNearbyServiceBrowser, - foundPeer peerID: MCPeerID, - withDiscoveryInfo info: [String: String]?) { - DispatchQueue.main.async { - self.update(peerID: peerID, to: .found) - if self.isAutoInviteOn { - self.invite(peerID: peerID) - } - } - } - - // MARK: - MCSessionDelegate - - - func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { - DispatchQueue.main.async { - switch state { - case .notConnected: - self.update(peerID: peerID, to: .declined) - UINotificationFeedbackGenerator().notificationOccurred(.error) - case .connecting: - self.update(peerID: peerID, to: .joining) - case .connected: - self.update(peerID: peerID, to: .joined) - UINotificationFeedbackGenerator().notificationOccurred(.success) - @unknown default: - return - } - } - } - - func session(_ session: MCSession, - didReceive data: Data, - fromPeer peerID: MCPeerID) { - WKRSeenFinalArticlesStore.addRemoteTransferData(data) - } - - // MARK: - Unused MCSessionDelegate - - - func session(_ session: MCSession, - didReceive stream: InputStream, - withName streamName: String, - fromPeer peerID: MCPeerID) {} - func session(_ session: MCSession, - didStartReceivingResourceWithName resourceName: String, - fromPeer peerID: MCPeerID, - with progress: Progress) {} - func session(_ session: MCSession, - didFinishReceivingResourceWithName resourceName: String, - fromPeer peerID: MCPeerID, - at localURL: URL?, - withError error: Error?) {} - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/Nearby.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/Nearby.swift new file mode 100644 index 0000000..be53a85 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/Nearby.swift @@ -0,0 +1,18 @@ +// +// Nearby.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import MultipeerConnectivity + +struct Nearby { + static let serviceType = "WKR-2020-07" + static let peerID = MCPeerID(displayName: UUID().uuidString) + struct Invite: Codable { + let hostName: String + let raceCode: String + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceAdvertiser.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceAdvertiser.swift new file mode 100644 index 0000000..1317204 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceAdvertiser.swift @@ -0,0 +1,59 @@ +// +// NearbyAdvertiser.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import MultipeerConnectivity +import os.log + +class NearbyRaceAdvertiser: NSObject, MCNearbyServiceBrowserDelegate { + + // MARK: - Properties - + + private var session: MCSession? + private var browser: MCNearbyServiceBrowser? + + private var raceCode: String = "" + private var invitedPeers = [MCPeerID]() + + // MARK: - Helpers - + + func start(hostName: String, raceCode: String) { + os_log("Advertiser: %{public}s: host name: %{public}s, race code: %{public}s", log: .nearby, type: .info, #function, hostName, raceCode) + + let session = MCSession(peer: Nearby.peerID) + browser = MCNearbyServiceBrowser(peer: Nearby.peerID, serviceType: Nearby.serviceType) + browser?.delegate = self + + self.raceCode = raceCode + self.session = session + + browser?.startBrowsingForPeers() + } + + func stop() { + os_log("Advertiser: %{public}s", log: .nearby, type: .info, #function) + browser?.stopBrowsingForPeers() + } + + func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) { + guard !invitedPeers.contains(peerID), + let session = session, + let data = try? JSONEncoder().encode(Nearby.Invite(hostName: GKLocalPlayer.local.alias, raceCode: raceCode)) else { + return + } + os_log("Advertiser: %{public}s: peer: %{public}s", log: .nearby, type: .info, #function, peerID.displayName) + if peerID.displayName == Nearby.peerID.displayName { + os_log("Advertiser: same name, skipping", log: .nearby, type: .info, #function) + return + } + + browser.invitePeer(peerID, to: session, withContext: data, timeout: 600) + invitedPeers.append(peerID) + } + func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {} +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceBrowser.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceBrowser.swift new file mode 100644 index 0000000..270479d --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceBrowser.swift @@ -0,0 +1,41 @@ +// +// NearbyBrowser.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import MultipeerConnectivity + +class NearbyRaceListener: NSObject, MCNearbyServiceAdvertiserDelegate { + + // MARK: - Properties - + + private lazy var peerID = MCPeerID(displayName: UUID().uuidString) + private var advertiser: MCNearbyServiceAdvertiser? + private var handler: ((_ hostName: String, _ raceCode: String) -> Void)? + + // MARK: - Helpers - + + func start(nearbyRaces: @escaping ((_ hostName: String, _ raceCode: String) -> Void)) { + self.handler = nearbyRaces + + advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: Nearby.serviceType) + advertiser?.delegate = self + advertiser?.startAdvertisingPeer() + } + + func stop() { + advertiser?.stopAdvertisingPeer() + } + + // MARK: - MCNearbyServiceAdvertiserDelegate - + + func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { + if let data = context, let invite = try? JSONDecoder().decode(Nearby.Invite.self, from: data) { + handler?(invite.hostName, invite.raceCode) + } + invitationHandler(false, nil) + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceListener.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceListener.swift new file mode 100644 index 0000000..8d22fd8 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Nearby/NearbyRaceListener.swift @@ -0,0 +1,44 @@ +// +// NearbyRaceListener.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import MultipeerConnectivity +import os.log + +class NearbyRaceListener: NSObject, MCNearbyServiceAdvertiserDelegate { + + // MARK: - Properties - + + private var advertiser: MCNearbyServiceAdvertiser? + private var handler: ((_ hostName: String, _ raceCode: String) -> Void)? + + // MARK: - Helpers - + + func start(nearbyRaces: @escaping ((_ hostName: String, _ raceCode: String) -> Void)) { + os_log("Listener: %{public}s", log: .nearby, type: .info, #function) + self.handler = nearbyRaces + + advertiser = MCNearbyServiceAdvertiser(peer: Nearby.peerID, discoveryInfo: nil, serviceType: Nearby.serviceType) + advertiser?.delegate = self + advertiser?.startAdvertisingPeer() + } + + func stop() { + os_log("Listener: %{public}s", log: .nearby, type: .info, #function) + advertiser?.stopAdvertisingPeer() + } + + // MARK: - MCNearbyServiceAdvertiserDelegate - + + func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { + if let data = context, let invite = try? JSONDecoder().decode(Nearby.Invite.self, from: data) { + os_log("Listener: %{public}s: host name: %{public}s, race code: %{public}s", log: .nearby, type: .info, #function, invite.hostName, invite.raceCode) + handler?(invite.hostName, invite.raceCode) + } + invitationHandler(false, nil) + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/RaceChecksViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/RaceChecksViewController.swift new file mode 100644 index 0000000..e713b6c --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/RaceChecksViewController.swift @@ -0,0 +1,167 @@ +// +// RaceChecksViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 6/27/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import SwiftUI +import WKRKit +import os.log + +#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) +import FirebasePerformance +#endif + +final class RaceChecksViewController: VisualEffectViewController { + + // MARK: - Types - + + enum Destination { + case joinPrivate(raceCode: String), joinPublic, hostPrivate + } + + // MARK: - Properties - + + let startDate = Date() + let destination: Destination + final var model = LoadingContentViewModel() + final lazy var contentViewHosting = UIHostingController( + rootView: LoadingContentView(model: model, cancel: { [weak self] in + self?.cancel() + }, disclaimerButton: { + UIApplication.shared.open(WKRKitConstants.current.manageGameCenterLink) + })) + + // MARK: - Initalization - + + init(destination: Destination) { + self.destination = destination + super.init(nibName: nil, bundle: nil) + GKMatchmaker.shared().cancel() + + model.title = "Checking Connection" + model.disclaimerButtonTitle = "Manage Game Center" + view.alpha = 0 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Life Cycle - + + override func viewDidLoad() { + super.viewDidLoad() + configure(hostingView: contentViewHosting.view) + + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + let trace = Performance.startTrace(name: "Connection Test Trace") + #endif + + let startDate = Date() + WKRConnectionTester.start { [weak self] success in + guard let self = self else { return } + DispatchQueue.main.async { + if success { + os_log("%{public}s: connection success: %{public}f", log: .matchSupport, type: .info, #function, -startDate.timeIntervalSinceNow) + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + trace?.stop() + #endif + self.connectionSuccess() + } else { + os_log("%{public}s: connection error", log: .matchSupport, type: .error, #function) + GKNotificationBanner.show( + withTitle: "Slow Connection", + message: "A faster internet connection is required", + completionHandler: nil) + self.model.title = "connection issue" + self.model.activityOpacity = 0 + } + PlayerFirebaseAnalytics.log( + event: .connectionTestResult, + attributes: [ + "Result": NSNumber(value: success).intValue, + "Duration": -startDate.timeIntervalSinceNow + ]) + } + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIView.animate(withDuration: 0.5) { [weak self] in + self?.view.alpha = 1 + } + } + + // MARK: - Helpers - + + final func cancel() { + UIView.animate(withDuration: 0.5, animations: { [weak self] in + self?.view.alpha = 0 + }, completion: { [weak self] _ in + self?.navigationController?.popToRootViewController(animated: false) + }) + } + + final func connectionSuccess() { + let startDate = Date() + let isAuthenticated = GKLocalPlayer.local.isAuthenticated + if !isAuthenticated { + self.model.title = "WAITING FOR GAME CENTER" + } + + DispatchQueue.global(qos: .userInitiated).async { + while !GKLocalPlayer.local.isAuthenticated { + if -startDate.timeIntervalSinceNow > 8 && self.model.disclaimerButtonOpacity == 0 { + DispatchQueue.main.async { + self.model.disclaimerButtonOpacity = 1 + } + } + sleep(2) + } + + // Give Game Center more time to get its act together + if !isAuthenticated { + sleep(3) + } + + DispatchQueue.main.async { + self.model.disclaimerButtonOpacity = 0 + } + + DispatchQueue.main.async { [weak self] in + self?.readyForNextMatchmakingStep() + } + } + } + + func readyForNextMatchmakingStep() { + let shouldFadeAlpha: Bool + let controller: UIViewController + switch self.destination { + case .joinPrivate(let raceCode): + shouldFadeAlpha = false + controller = GKJoinViewController(raceCode: raceCode) + case .joinPublic: + shouldFadeAlpha = false + controller = GKJoinViewController(raceCode: nil) + case .hostPrivate: + shouldFadeAlpha = true + controller = GKHostViewController() + } + + let delay = max(0, 2 - (-self.startDate.timeIntervalSinceNow)) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(delay * 1000))) { + UIView.animate(withDuration: 0.5, animations: { [weak self] in + self?.contentViewHosting.view.alpha = shouldFadeAlpha ? 0 : 1 + }, completion: { [weak self] _ in + self?.navigationController?.pushViewController(controller, animated: false) + }) + } + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/HostViewController+Table.swift b/WikiRaces/Shared/Menu View Controllers/HostViewController+Table.swift new file mode 100644 index 0000000..919e5af --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/HostViewController+Table.swift @@ -0,0 +1,11 @@ +// +// HostViewController+Table.swift +// WKRUIKit +// +// Created by Andrew Finke on 6/24/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation + +extension CFHost diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift index c8b28ab..73f184f 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift @@ -17,12 +17,6 @@ final class MedalScene: SKScene { private let bronzeNode: SKNode private let dnfNode: SKNode - public var isActive = false { - didSet { - isPaused = !isActive - } - } - // MARK: - Initalization - override init(size: CGSize) { @@ -76,7 +70,7 @@ final class MedalScene: SKScene { node.removeFromParent() } DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.isActive = !self.children.isEmpty + self.isPaused = self.children.isEmpty } } @@ -100,7 +94,7 @@ final class MedalScene: SKScene { let padding: CGFloat = 40 let maxX = size.width - padding node.position = CGPoint(x: CGFloat.random(in: padding.. 0 else { return } medalScene.showMedals(gold: Int(firstMedals), @@ -60,7 +60,7 @@ final class MedalView: SKView { bronze: Int(thirdMedals), dnf: Int(dnfCount)) - medalScene.isActive = true + medalScene.isPaused = false UIImpactFeedbackGenerator().impactOccurred() } } diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift index f4322c1..160ae7b 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift @@ -15,96 +15,96 @@ extension MenuView { /// Join button pressed @objc - func showLocalRaceOptions() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - PlayerAnonymousMetrics.log(event: .pressedLocalOptions) + func showJoinOptions() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) UISelectionFeedbackGenerator().selectionChanged() - animateOptionsOutAndTransition(to: .localOptions) + animateOptionsOutAndTransition(to: .joinOptions) } @objc - func joinGlobalRace() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - PlayerAnonymousMetrics.log(event: .pressedGlobalJoin) - PlayerDatabaseStat.gkPressedJoin.increment() + func createRace() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .revampPressedHost) UISelectionFeedbackGenerator().selectionChanged() - guard GKLocalPlayer.local.isAuthenticated || UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") else { - self.listenerUpdate?(.presentGlobalAuth) - return - } + PlayerCloudKitLiveRaceManager.shared.isCloudEnabled { isEnabled in + DispatchQueue.main.async { + if isEnabled { + self.animateMenuOut { + self.listenerUpdate?(.presentCreateRace) + } + } else { + let message = "You must be logged into iCloud to create a private race." + let alertController = UIAlertController(title: "iCloud Issue", message: message, preferredStyle: .alert) + alertController.addCancelAction(title: "Ok") + + #if targetEnvironment(simulator) + let action = UIAlertAction(title: "SIM BYPASS", style: .default) { _ in + self.animateMenuOut { + self.listenerUpdate?(.presentCreateRace) + } + } + alertController.addAction(action) + #endif + + self.listenerUpdate?(.presentAlert(alertController)) + PlayerFirebaseAnalytics.log(event: .revampPressedHostiCloudIssue) + } - guard !promptGlobalRacesPopularity() else { - return - } - - animateMenuOut { - self.listenerUpdate?(.presentGlobalConnect) + } } } - /// Join button pressed @objc - func joinLocalRace() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - PlayerAnonymousMetrics.log(event: .pressedJoin) - PlayerDatabaseStat.mpcPressedJoin.increment() + func joinPublicRace() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .revampPressedJoinPublic) + PlayerUserDefaultsStat.gkPressedJoin.increment() UISelectionFeedbackGenerator().selectionChanged() - guard !promptForCustomName(isHost: false) else { + guard !promptGlobalRacesPopularity() else { return } animateMenuOut { - self.listenerUpdate?(.presentMPCConnect(isHost: false)) + self.listenerUpdate?(.presentJoinPublicRace) } } - /// Create button pressed @objc - func createLocalRace() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - PlayerAnonymousMetrics.log(event: .pressedHost) - PlayerDatabaseStat.mpcPressedHost.increment() + func joinPrivateRace() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .revampPressedJoinPrivate) + PlayerUserDefaultsStat.mpcPressedJoin.increment() UISelectionFeedbackGenerator().selectionChanged() - guard !promptForCustomName(isHost: true) else { - return - } - animateMenuOut { - self.listenerUpdate?(.presentMPCConnect(isHost: true)) + self.listenerUpdate?(.presentJoinPrivateRace) } } @objc func backButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - + PlayerFirebaseAnalytics.log(event: .userAction(#function)) UISelectionFeedbackGenerator().selectionChanged() - - animateOptionsOutAndTransition(to: .raceTypeOptions) + animateOptionsOutAndTransition(to: .joinOrCreate) } @objc func plusButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - + PlayerFirebaseAnalytics.log(event: .userAction(#function)) UISelectionFeedbackGenerator().selectionChanged() - animateOptionsOutAndTransition(to: .plusOptions) } @objc func statsButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - + PlayerFirebaseAnalytics.log(event: .userAction(#function)) UISelectionFeedbackGenerator().selectionChanged() - if PlusStore.shared.isPlus { animateMenuOut { self.listenerUpdate?(.presentStats) @@ -118,13 +118,8 @@ extension MenuView { /// /// - Parameter sender: The pressed tile @objc - func menuTilePressed(sender: MenuTile) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - - guard GKLocalPlayer.local.isAuthenticated else { - self.listenerUpdate?(.presentGlobalAuth) - return - } + func menuTilePressed() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) animateMenuOut { self.listenerUpdate?(.presentLeaderboard) @@ -132,7 +127,7 @@ extension MenuView { } func triggeredEasterEgg() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) medalView.showMedals() } @@ -144,22 +139,28 @@ extension MenuView { movingPuzzleView.start() - state = .raceTypeOptions + state = .joinOrCreate setNeedsLayout() - UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle, - animations: { - self.layoutIfNeeded() - }, completion: { _ in - self.isUserInteractionEnabled = true - completion?() - }) + UIView.animate( + withDuration: WKRAnimationDurationConstants.menuToggle, + animations: { + self.layoutIfNeeded() + }, completion: { _ in + self.isUserInteractionEnabled = true + completion?() + }) } /// Animates the views off screen /// /// - Parameter completion: The completion handler func animateMenuOut(completion: (() -> Void)?) { + if state == .noInterface { + completion?() + return + } + isUserInteractionEnabled = false state = .noInterface @@ -177,19 +178,21 @@ extension MenuView { self.state = .noOptions setNeedsLayout() - UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle / 2, - animations: { + UIView.animate( + withDuration: WKRAnimationDurationConstants.menuToggle / 2, + animations: { + self.layoutIfNeeded() + }, completion: { _ in + self.state = state + self.setNeedsLayout() + + UIView.animate( + withDuration: WKRAnimationDurationConstants.menuToggle / 2, + delay: WKRAnimationDurationConstants.menuToggle / 4, + animations: { self.layoutIfNeeded() - }, completion: { _ in - self.state = state - self.setNeedsLayout() - - UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle / 2, - delay: WKRAnimationDurationConstants.menuToggle / 4, - animations: { - self.layoutIfNeeded() + }) }) - }) } } diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift index 9ac269d..14970ed 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift @@ -22,23 +22,23 @@ extension MenuView { titleLabelConstraint = titleLabel.topAnchor.constraint(equalTo: topView.safeAreaLayoutGuide.topAnchor) - localRaceTypeButtonWidthConstraint = localRaceTypeButton.widthAnchor.constraint(equalToConstant: 0) - localRaceTypeButtonHeightConstraint = localRaceTypeButton.heightAnchor.constraint(equalToConstant: 0) - localRaceTypeButtonLeftConstraint = localRaceTypeButton.leftAnchor.constraint(equalTo: topView.leftAnchor, - constant: 0) - globalRaceTypeButtonWidthConstraint = globalRaceTypeButton.widthAnchor.constraint(equalToConstant: 0) + joinButtonWidthConstraint = joinButton.widthAnchor.constraint(equalToConstant: 0) + joinButtonHeightConstraint = joinButton.heightAnchor.constraint(equalToConstant: 0) + joinButtonLeftConstraint = joinButton.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 0) + createButtonWidthConstraint = createButton.widthAnchor.constraint(equalToConstant: 0) - joinLocalRaceButtonWidthConstraint = joinLocalRaceButton.widthAnchor.constraint(equalToConstant: 0) - joinLocalRaceButtonLeftConstraint = joinLocalRaceButton.leftAnchor.constraint(equalTo: topView.leftAnchor, - constant: 0) - createLocalRaceButtonWidthConstraint = createLocalRaceButton.widthAnchor.constraint(equalToConstant: 0) - localOptionsBackButtonWidth = localOptionsBackButton.widthAnchor.constraint(equalToConstant: 30) - - statsButtonLeftConstraint = statsButton.leftAnchor.constraint(equalTo: topView.leftAnchor, - constant: 0) + publicButtonWidthConstraint = publicButton.widthAnchor.constraint(equalToConstant: 0) + privateButtonLeftConstraint = publicButton.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 0) + privateButtonWidthConstraint = privateButton.widthAnchor.constraint(equalToConstant: 0) + backButtonWidth = backButton.widthAnchor.constraint(equalToConstant: 30) + statsButtonLeftConstraint = statsButton.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 0) statsButtonWidthConstraint = statsButton.widthAnchor.constraint(equalToConstant: 0) + backButtonLeftConstraintForStats = backButton.leftAnchor.constraint(equalTo: statsButton.leftAnchor) + backButtonLeftConstraintForJoinOptions = backButton.leftAnchor.constraint(equalTo: publicButton.leftAnchor) + backButtonLeftConstraintForJoinOptions.isActive = true + let constraints = [ titleLabelConstraint!, @@ -54,99 +54,81 @@ extension MenuView { subtitleLabel.widthAnchor.constraint(equalTo: topView.widthAnchor), subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), - localRaceTypeButtonWidthConstraint!, - localRaceTypeButtonHeightConstraint!, - localRaceTypeButtonLeftConstraint!, - globalRaceTypeButtonWidthConstraint!, + joinButtonWidthConstraint!, + joinButtonHeightConstraint!, + joinButtonLeftConstraint!, + createButtonWidthConstraint!, - joinLocalRaceButtonWidthConstraint!, - joinLocalRaceButtonLeftConstraint!, - createLocalRaceButtonWidthConstraint!, - localOptionsBackButtonWidth!, + publicButtonWidthConstraint!, + privateButtonLeftConstraint!, + privateButtonWidthConstraint!, + backButtonWidth!, statsButtonLeftConstraint!, statsButtonWidthConstraint!, - localRaceTypeButton.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, - constant: 40.0), - joinLocalRaceButton.topAnchor.constraint(equalTo: localRaceTypeButton.topAnchor), - statsButton.topAnchor.constraint(equalTo: localRaceTypeButton.topAnchor), - - globalRaceTypeButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), - joinLocalRaceButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), - createLocalRaceButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), - statsButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), - - globalRaceTypeButton.leftAnchor.constraint(equalTo: localRaceTypeButton.leftAnchor), - createLocalRaceButton.leftAnchor.constraint(equalTo: joinLocalRaceButton.leftAnchor), - - globalRaceTypeButton.topAnchor.constraint(equalTo: localRaceTypeButton.bottomAnchor, - constant: 20.0), - createLocalRaceButton.topAnchor.constraint(equalTo: globalRaceTypeButton.topAnchor), - - localOptionsBackButton.leftAnchor.constraint(equalTo: joinLocalRaceButton.leftAnchor), - localOptionsBackButton.topAnchor.constraint(equalTo: createLocalRaceButton.bottomAnchor, - constant: 20.0), - - localOptionsBackButton.heightAnchor.constraint(equalTo: localOptionsBackButton.widthAnchor, - multiplier: 1), - - plusOptionsBackButton.leftAnchor.constraint(equalTo: statsButton.leftAnchor), - plusOptionsBackButton.topAnchor.constraint(equalTo: statsButton.bottomAnchor, - constant: 20.0), - plusOptionsBackButton.widthAnchor.constraint(equalTo: localOptionsBackButton.widthAnchor), - plusOptionsBackButton.heightAnchor.constraint(equalTo: localOptionsBackButton.widthAnchor), - - plusButton.leftAnchor.constraint(equalTo: localRaceTypeButton.leftAnchor), - plusButton.topAnchor.constraint(equalTo: globalRaceTypeButton.bottomAnchor, - constant: 20.0), - plusButton.widthAnchor.constraint(equalTo: localOptionsBackButton.widthAnchor), - plusButton.heightAnchor.constraint(equalTo: localOptionsBackButton.widthAnchor) + joinButton.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, + constant: 40.0), + publicButton.topAnchor.constraint(equalTo: joinButton.topAnchor), + statsButton.topAnchor.constraint(equalTo: joinButton.topAnchor), + + createButton.heightAnchor.constraint(equalTo: joinButton.heightAnchor), + publicButton.heightAnchor.constraint(equalTo: joinButton.heightAnchor), + privateButton.heightAnchor.constraint(equalTo: joinButton.heightAnchor), + statsButton.heightAnchor.constraint(equalTo: joinButton.heightAnchor), + + createButton.leftAnchor.constraint(equalTo: joinButton.leftAnchor), + privateButton.leftAnchor.constraint(equalTo: publicButton.leftAnchor), + createButton.topAnchor.constraint(equalTo: joinButton.bottomAnchor, constant: 20.0), + privateButton.topAnchor.constraint(equalTo: createButton.topAnchor), + + plusButton.leftAnchor.constraint(equalTo: joinButton.leftAnchor), + plusButton.topAnchor.constraint(equalTo: createButton.bottomAnchor, constant: 20.0), + plusButton.widthAnchor.constraint(equalTo: backButton.widthAnchor), + plusButton.heightAnchor.constraint(equalTo: backButton.widthAnchor), + + backButton.topAnchor.constraint(equalTo: privateButton.bottomAnchor, constant: 20.0), + backButton.heightAnchor.constraint(equalTo: backButton.widthAnchor, multiplier: 1) ] NSLayoutConstraint.activate(constraints) } /// Sets up the buttons private func setupButtons() { - localRaceTypeButton.title = "local race" - localRaceTypeButton.translatesAutoresizingMaskIntoConstraints = false - localRaceTypeButton.addTarget(self, action: #selector(showLocalRaceOptions), for: .touchUpInside) - topView.addSubview(localRaceTypeButton) - - globalRaceTypeButton.title = "global race" - globalRaceTypeButton.translatesAutoresizingMaskIntoConstraints = false - globalRaceTypeButton.addTarget(self, action: #selector(joinGlobalRace), for: .touchUpInside) - topView.addSubview(globalRaceTypeButton) - - joinLocalRaceButton.title = "join race" - joinLocalRaceButton.translatesAutoresizingMaskIntoConstraints = false - joinLocalRaceButton.addTarget(self, action: #selector(joinLocalRace), for: .touchUpInside) - topView.addSubview(joinLocalRaceButton) - - createLocalRaceButton.title = "create race" - createLocalRaceButton.translatesAutoresizingMaskIntoConstraints = false - createLocalRaceButton.addTarget(self, action: #selector(createLocalRace), for: .touchUpInside) - topView.addSubview(createLocalRaceButton) - - statsButton.title = "race stats" + joinButton.title = "join" + joinButton.translatesAutoresizingMaskIntoConstraints = false + joinButton.addTarget(self, action: #selector(showJoinOptions), for: .touchUpInside) + topView.addSubview(joinButton) + + createButton.title = "create" + createButton.translatesAutoresizingMaskIntoConstraints = false + createButton.addTarget(self, action: #selector(createRace), for: .touchUpInside) + topView.addSubview(createButton) + + publicButton.title = "public" + publicButton.translatesAutoresizingMaskIntoConstraints = false + publicButton.addTarget(self, action: #selector(joinPublicRace), for: .touchUpInside) + topView.addSubview(publicButton) + + privateButton.title = "private" + privateButton.translatesAutoresizingMaskIntoConstraints = false + privateButton.addTarget(self, action: #selector(joinPrivateRace), for: .touchUpInside) + topView.addSubview(privateButton) + + statsButton.title = "stats" statsButton.translatesAutoresizingMaskIntoConstraints = false statsButton.addTarget(self, action: #selector(statsButtonPressed), for: .touchUpInside) topView.addSubview(statsButton) if #available(iOS 13.4, *) { - localOptionsBackButton.isPointerInteractionEnabled = true + backButton.isPointerInteractionEnabled = true plusButton.isPointerInteractionEnabled = true } - localOptionsBackButton.setImage(UIImage(named: "Back")!, for: .normal) - localOptionsBackButton.translatesAutoresizingMaskIntoConstraints = false - localOptionsBackButton.addTarget(self, action: #selector(backButtonPressed), for: .touchUpInside) - topView.addSubview(localOptionsBackButton) - - plusOptionsBackButton.setImage(UIImage(named: "Back")!, for: .normal) - plusOptionsBackButton.translatesAutoresizingMaskIntoConstraints = false - plusOptionsBackButton.addTarget(self, action: #selector(backButtonPressed), for: .touchUpInside) - topView.addSubview(plusOptionsBackButton) + backButton.setImage(UIImage(named: "Back")!, for: .normal) + backButton.translatesAutoresizingMaskIntoConstraints = false + backButton.addTarget(self, action: #selector(backButtonPressed), for: .touchUpInside) + topView.addSubview(backButton) let config = UIImage.SymbolConfiguration(weight: .semibold) plusButton.setImage(UIImage(systemName: "plus", withConfiguration: config)!, @@ -154,7 +136,7 @@ extension MenuView { plusButton.translatesAutoresizingMaskIntoConstraints = false plusButton.addTarget(self, action: #selector(plusButtonPressed), for: .touchUpInside) topView.addSubview(plusButton) - } + } /// Sets up the labels private func setupLabels() { @@ -203,7 +185,7 @@ extension MenuView { let middleMenuTile = MenuTile(title: "AVG PER RACE") middleMenuTile.isAverage = true - middleMenuTile.value = PlayerDatabaseStat.multiplayerAverage.value() + middleMenuTile.value = PlayerUserDefaultsStat.multiplayerAverage.value() statsStackView.addArrangedSubview(middleMenuTile) let leftThinLine = WKRUIThinLineView() @@ -228,9 +210,9 @@ extension MenuView { ] NSLayoutConstraint.activate(constraints) - leftMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) - middleMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) - rightMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) + leftMenuTile.addTarget(self, action: #selector(menuTilePressed), for: .touchUpInside) + middleMenuTile.addTarget(self, action: #selector(menuTilePressed), for: .touchUpInside) + rightMenuTile.addTarget(self, action: #selector(menuTilePressed), for: .touchUpInside) self.leftMenuTile = leftMenuTile self.middleMenuTile = middleMenuTile @@ -244,7 +226,7 @@ extension MenuView { } } - if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + if Defaults.isFastlaneSnapshotInstance { self.leftMenuTile?.value = 140 self.middleMenuTile?.value = 140/72 self.rightMenuTile?.value = 72 diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift index b0a8157..bd22fa8 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift @@ -15,12 +15,13 @@ final class MenuView: UIView { // MARK: Types enum InterfaceState { - case raceTypeOptions, noOptions, localOptions, plusOptions, noInterface + case joinOrCreate, noOptions, joinOptions, plusOptions, noInterface } enum ListenerUpdate { - case presentDebug, presentGlobalConnect, presentLeaderboard, presentGlobalAuth - case presentMPCConnect(isHost: Bool) + case presentDebug, presentLeaderboard + case presentJoinPublicRace, presentJoinPrivateRace + case presentCreateRace case presentAlert(UIAlertController) case presentStats, presentSubscription } @@ -46,16 +47,16 @@ final class MenuView: UIView { /// The "Conquer..." label let subtitleLabel = UILabel() - let localRaceTypeButton = WKRUIButton() - let globalRaceTypeButton = WKRUIButton() - let joinLocalRaceButton = WKRUIButton() - let createLocalRaceButton = WKRUIButton() - let localOptionsBackButton = UIButton() + let joinButton = WKRUIButton() + let createButton = WKRUIButton() + let publicButton = WKRUIButton() + let privateButton = WKRUIButton() + + let backButton = UIButton() let plusButton = UIButton() let statsButton = WKRUIButton() - let plusOptionsBackButton = UIButton() /// The Wiki Points tile var leftMenuTile: MenuTile? @@ -86,15 +87,18 @@ final class MenuView: UIView { /// Used for adjusting button widths and heights based on screen width - var localRaceTypeButtonLeftConstraint: NSLayoutConstraint! - var localRaceTypeButtonWidthConstraint: NSLayoutConstraint! - var localRaceTypeButtonHeightConstraint: NSLayoutConstraint! - var globalRaceTypeButtonWidthConstraint: NSLayoutConstraint! + var joinButtonLeftConstraint: NSLayoutConstraint! + var joinButtonWidthConstraint: NSLayoutConstraint! + var joinButtonHeightConstraint: NSLayoutConstraint! + var createButtonWidthConstraint: NSLayoutConstraint! + + var publicButtonWidthConstraint: NSLayoutConstraint! + var privateButtonLeftConstraint: NSLayoutConstraint! + var privateButtonWidthConstraint: NSLayoutConstraint! - var joinLocalRaceButtonLeftConstraint: NSLayoutConstraint! - var joinLocalRaceButtonWidthConstraint: NSLayoutConstraint! - var createLocalRaceButtonWidthConstraint: NSLayoutConstraint! - var localOptionsBackButtonWidth: NSLayoutConstraint! + var backButtonLeftConstraintForJoinOptions: NSLayoutConstraint! + var backButtonLeftConstraintForStats: NSLayoutConstraint! + var backButtonWidth: NSLayoutConstraint! var statsButtonLeftConstraint: NSLayoutConstraint! var statsButtonWidthConstraint: NSLayoutConstraint! @@ -161,17 +165,13 @@ final class MenuView: UIView { let textColor: UIColor = .wkrTextColor(for: traitCollection) titleLabel.textColor = textColor subtitleLabel.textColor = textColor - localOptionsBackButton.tintColor = textColor - localOptionsBackButton.layer.borderColor = textColor.cgColor - localOptionsBackButton.layer.borderWidth = 1.7 - - plusOptionsBackButton.tintColor = localOptionsBackButton.tintColor - plusOptionsBackButton.layer.borderColor = localOptionsBackButton.layer.borderColor - plusOptionsBackButton.layer.borderWidth = localOptionsBackButton.layer.borderWidth + backButton.tintColor = textColor + backButton.layer.borderColor = textColor.cgColor + backButton.layer.borderWidth = 1.7 - plusButton.tintColor = localOptionsBackButton.tintColor - plusButton.layer.borderColor = localOptionsBackButton.layer.borderColor - plusButton.layer.borderWidth = localOptionsBackButton.layer.borderWidth + plusButton.tintColor = backButton.tintColor + plusButton.layer.borderColor = backButton.layer.borderColor + plusButton.layer.borderWidth = backButton.layer.borderWidth // Button Styles let buttonStyle: WKRUIButtonStyle @@ -179,11 +179,11 @@ final class MenuView: UIView { let buttonHeight: CGFloat if frame.size.width > 420 { buttonStyle = .large - buttonWidth = 210 + buttonWidth = 150 buttonHeight = 50 } else { buttonStyle = .normal - buttonWidth = 175 + buttonWidth = 100 buttonHeight = 40 } @@ -197,10 +197,10 @@ final class MenuView: UIView { rightMenuTile?.title = "RACES PLAYED" } - localRaceTypeButton.style = buttonStyle - globalRaceTypeButton.style = buttonStyle - joinLocalRaceButton.style = buttonStyle - createLocalRaceButton.style = buttonStyle + joinButton.style = buttonStyle + createButton.style = buttonStyle + publicButton.style = buttonStyle + privateButton.style = buttonStyle statsButton.style = buttonStyle // Label Fonts @@ -215,9 +215,10 @@ final class MenuView: UIView { } switch state { - case .raceTypeOptions: - localRaceTypeButtonLeftConstraint.constant = 30 - joinLocalRaceButtonLeftConstraint.constant = -createLocalRaceButton.frame.width + case .joinOrCreate: + joinButtonLeftConstraint.constant = 30 + privateButtonLeftConstraint.constant = -privateButton.frame.width * 2 + statsButtonLeftConstraint.constant = -statsButton.frame.width topViewLeftConstraint.constant = 0 @@ -227,81 +228,55 @@ final class MenuView: UIView { bottomViewAnchorConstraint.constant = 75 } case .noOptions: - localRaceTypeButtonLeftConstraint.constant = -globalRaceTypeButton.frame.width - joinLocalRaceButtonLeftConstraint.constant = -createLocalRaceButton.frame.width + joinButtonLeftConstraint.constant = -createButton.frame.width + privateButtonLeftConstraint.constant = -privateButton.frame.width statsButtonLeftConstraint.constant = -statsButton.frame.width - case .localOptions: - localRaceTypeButtonLeftConstraint.constant = -globalRaceTypeButton.frame.width - joinLocalRaceButtonLeftConstraint.constant = 30 + case .joinOptions: + joinButtonLeftConstraint.constant = -createButton.frame.width + privateButtonLeftConstraint.constant = 30 + + backButtonLeftConstraintForStats.isActive = false + backButtonLeftConstraintForJoinOptions.isActive = true case .noInterface: topViewLeftConstraint.constant = -topView.frame.width bottomViewAnchorConstraint.constant = bottomView.frame.height - localRaceTypeButtonLeftConstraint.constant = 30 - joinLocalRaceButtonLeftConstraint.constant = 30 + joinButtonLeftConstraint.constant = 30 + privateButtonLeftConstraint.constant = 30 case .plusOptions: - localRaceTypeButtonLeftConstraint.constant = -globalRaceTypeButton.frame.width + joinButtonLeftConstraint.constant = -createButton.frame.width statsButtonLeftConstraint.constant = 30 - } - localRaceTypeButtonHeightConstraint.constant = buttonHeight - localRaceTypeButtonWidthConstraint.constant = buttonWidth + 18 - globalRaceTypeButtonWidthConstraint.constant = buttonWidth + 32 + backButtonLeftConstraintForJoinOptions.isActive = false + backButtonLeftConstraintForStats.isActive = true + } - joinLocalRaceButtonWidthConstraint.constant = buttonWidth - createLocalRaceButtonWidthConstraint.constant = buttonWidth + 30 - localOptionsBackButtonWidth.constant = buttonHeight - 10 + joinButtonHeightConstraint.constant = buttonHeight + joinButtonWidthConstraint.constant = buttonWidth + 10 + createButtonWidthConstraint.constant = buttonWidth + 40 - statsButtonWidthConstraint.constant = buttonWidth + 13 + publicButtonWidthConstraint.constant = buttonWidth + 34 + privateButtonWidthConstraint.constant = buttonWidth + 44 + statsButtonWidthConstraint.constant = buttonWidth + 20 - localOptionsBackButton.layer.cornerRadius = localOptionsBackButtonWidth.constant / 2 - plusOptionsBackButton.layer.cornerRadius = localOptionsBackButton.layer.cornerRadius - plusButton.layer.cornerRadius = localOptionsBackButton.layer.cornerRadius - } + backButtonWidth.constant = buttonHeight - 10 - func promptForCustomName(isHost: Bool) -> Bool { - guard !UserDefaults.standard.bool(forKey: "PromptedCustomName") else { - return false - } - UserDefaults.standard.set(true, forKey: "PromptedCustomName") - - let message = "Would you like to set a custom player name for local races?" - let alertController = UIAlertController(title: "Set Name?", message: message, preferredStyle: .alert) - - let laterAction = UIAlertAction(title: "Maybe Later", style: .cancel, handler: { _ in - PlayerAnonymousMetrics.log(event: .userAction("promptForCustomNamePrompt:rejected")) - PlayerAnonymousMetrics.log(event: .namePromptResult, attributes: ["Result": "Cancelled"]) - if isHost { - self.createLocalRace() - } else { - self.joinLocalRace() - } - }) - alertController.addAction(laterAction) - - let settingsAction = UIAlertAction(title: "Open Settings", style: .default, handler: { _ in - PlayerAnonymousMetrics.log(event: .userAction("promptForCustomNamePrompt:accepted")) - PlayerAnonymousMetrics.log(event: .namePromptResult, attributes: ["Result": "Accepted"]) - UIApplication.shared.openSettings() - }) - alertController.addAction(settingsAction) - - listenerUpdate?(.presentAlert(alertController)) - return true + backButton.layer.cornerRadius = backButtonWidth.constant / 2 + plusButton.layer.cornerRadius = backButton.layer.cornerRadius } func promptGlobalRacesPopularity() -> Bool { - guard !UserDefaults.standard.bool(forKey: "PromptedGlobalRacesPopularity") else { + guard !Defaults.promptedGlobalRacesPopularity else { return false } - UserDefaults.standard.set(true, forKey: "PromptedGlobalRacesPopularity") + Defaults.promptedGlobalRacesPopularity = true - let message = "Most global races are started with invited friends. Invite a friend for the best chance at joining a race." - let alertController = UIAlertController(title: "Global Races", message: message, preferredStyle: .alert) + let message = "Most racers use private races to play with friends. Create a private race and invite a friend for the best chance at joining a race. You can also start a solo race at any time." + let alertController = UIAlertController(title: "Public Races", message: message, preferredStyle: .alert) - let action = UIAlertAction(title: "Ok", style: .default, handler: { _ in - PlayerAnonymousMetrics.log(event: .userAction("promptGlobalRacesPopularity:ok")) + let action = UIAlertAction(title: "Find Public Race", style: .default, handler: { _ in + PlayerFirebaseAnalytics.log(event: .userAction("promptGlobalRacesPopularity:ok")) self.animateMenuOut { - self.listenerUpdate?(.presentGlobalConnect) + self.listenerUpdate?(.presentJoinPublicRace) } }) alertController.addAction(action) diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift index 06a32c0..1c3e10d 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift @@ -10,10 +10,14 @@ import UIKit final class MovingPuzzleView: UIView, UIScrollViewDelegate { + // MARK: - Properties - + private var puzzleTimer: Timer? /// The puzzle piece view private let innerPuzzleView = UIScrollView() + // MARK: - Initalization - + init() { super.init(frame: .zero) @@ -51,10 +55,10 @@ final class MovingPuzzleView: UIView, UIScrollViewDelegate { // MARK: - View Life Cycle - - public override func layoutSubviews() { - super.layoutSubviews() - backgroundColor = .wkrMenuPuzzleViewColor(for: traitCollection) - } + public override func layoutSubviews() { + super.layoutSubviews() + backgroundColor = .wkrMenuPuzzleViewColor(for: traitCollection) + } // MARK: - UIScrollViewDelegate - @@ -63,7 +67,7 @@ final class MovingPuzzleView: UIView, UIScrollViewDelegate { if contentOffset > innerPuzzleView.contentSize.width * 0.8 { animateContentOffsetReset() } - PlayerAnonymousMetrics.log(event: .puzzleViewScrolled) + PlayerFirebaseAnalytics.log(event: .puzzleViewScrolled) } // MARK: - Helpers - @@ -72,14 +76,14 @@ final class MovingPuzzleView: UIView, UIScrollViewDelegate { UIView.animate(withDuration: 0.25, animations: { self.innerPuzzleView.alpha = 0.0 - }, completion: { _ in - self.stop() - self.start() - UIView.animate(withDuration: 0.25, - animations: { - self.innerPuzzleView.alpha = 1.0 - }) - }) + }, completion: { _ in + self.stop() + self.start() + UIView.animate(withDuration: 0.25, + animations: { + self.innerPuzzleView.alpha = 1.0 + }) + }) } @objc @@ -105,7 +109,7 @@ final class MovingPuzzleView: UIView, UIScrollViewDelegate { options: options, animations: { self.innerPuzzleView.contentOffset = CGPoint(x: xOffset, y: 0) - }, completion: nil) + }, completion: nil) } puzzleTimer?.invalidate() diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift index 23a8a3d..aad2d53 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift @@ -15,7 +15,7 @@ extension MenuViewController { @objc func presentDebugController() { - PlayerAnonymousMetrics.log(event: .versionInfo) + PlayerFirebaseAnalytics.log(event: .versionInfo) let message = "If your name isn't Andrew, you probably shouldn’t be here." let alertController = UIAlertController(title: "Debug Panel", @@ -46,12 +46,12 @@ extension MenuViewController { let interfaceBundleInfo = Bundle(for: WKRUIWebView.self).infoDictionary guard let appBundleVersion = appBundleInfo?[versionKey] as? String, - let appBundleShortVersion = appBundleInfo?[shortVersionKey] as? String, - let kitBundleVersion = kitBundleInfo?[versionKey] as? String, - let kitBundleShortVersion = kitBundleInfo?[shortVersionKey] as? String, - let interfaceBundleVersion = interfaceBundleInfo?[versionKey] as? String, - let interfaceBundleShortVersion = interfaceBundleInfo?[shortVersionKey] as? String else { - fatalError("No bundle info dictionary") + let appBundleShortVersion = appBundleInfo?[shortVersionKey] as? String, + let kitBundleVersion = kitBundleInfo?[versionKey] as? String, + let kitBundleShortVersion = kitBundleInfo?[shortVersionKey] as? String, + let interfaceBundleVersion = interfaceBundleInfo?[versionKey] as? String, + let interfaceBundleShortVersion = interfaceBundleInfo?[shortVersionKey] as? String else { + fatalError("No bundle info dictionary") } let debugInfoController = DebugInfoTableViewController() @@ -77,7 +77,7 @@ extension MenuViewController { .dictionaryRepresentation() .sorted { (lhs, rhs) -> Bool in return lhs.key.lowercased() < rhs.key.lowercased() - } + } let navController = WKRUINavigationController(rootViewController: debugInfoController) present(navController, animated: true, completion: nil) diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift index a2b95f1..d46c5cc 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift @@ -12,55 +12,38 @@ extension MenuViewController: GKGameCenterControllerDelegate { // MARK: - Game Center - - /// Attempts Game Center login - func attemptGlobalAuthentication() { - // seperated due to long type-checking time as closure - func auth(_ controller: UIViewController?, _ error: Error?, _ forceShowError: Bool) { - if let controller = controller, self.menuView.state != .noInterface { - if self.presentedViewController == nil { - self.present(controller, animated: true, completion: nil) + func setupGKAuthHandler() { + func auth(result: GKHelper.AuthResult) { + switch result { + case .error(let error): + let info = "attemptGlobalAuthentication: " + error.localizedDescription + PlayerFirebaseAnalytics.log(event: .error(info)) + case .controller(let controller): + if presentedViewController == nil, self.menuView.state != .noInterface { + present(controller, animated: true, completion: nil) } - } else if GKLocalPlayer.local.isAuthenticated { - let metrics = PlayerDatabaseMetrics.shared + case .isAuthenticated: + let metrics = PlayerCloudKitStatsManager.shared metrics.log(value: GKLocalPlayer.local.alias, for: "GCAliases") - } else if !GKLocalPlayer.local.isAuthenticated { - if error != nil || forceShowError { - self.presentGameKitAuthAlert() - } - } - if let error = error { - let info = "attemptGlobalAuthentication: " + error.localizedDescription - PlayerAnonymousMetrics.log(event: .error(info)) } } - GlobalRaceHelper.shared.authenticate(completion: auth) + + GKHelper.shared.authHandler = auth + } + + func setupInviteHandler() { + GKHelper.shared.inviteHandler = { code in + self.joinRace(raceCode: code) + } } // MARK: - GKGameCenterControllerDelegate - func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) dismiss(animated: true) { self.menuView.animateMenuIn() } } - // MARK: - Other - - - func presentGameKitAuthAlert() { - let title = "Global Races Unavailable" - let message = """ - Please try logging into Game Center in the Settings app to join a Global Race. - """ - - let controller = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - controller.addCancelAction(title: "Ok") - - if presentedViewController == nil { - present(controller, animated: true, completion: nil) - } - } - } diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift index b96df95..1e1e922 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift @@ -10,40 +10,20 @@ import UIKit extension MenuViewController { - // MARK: - Keyboard Support - - - override var keyCommands: [UIKeyCommand]? { - return [ - UIKeyCommand(title: "Create Local Race", - action: #selector(keyboardCreateLocalRace), - input: "n", - modifierFlags: .command), - UIKeyCommand(title: "Join Local Race", - action: #selector(keyboardJoinLocalRace), - input: "j", - modifierFlags: .command), - UIKeyCommand(title: "Join Global Race", - action: #selector(keyboardJoinGlobalRace), - input: "g", - modifierFlags: .command) - ] - } - - @objc private func keyboardJoinLocalRace() { - PlayerAnonymousMetrics.log(event: .pressedJoin) - PlayerDatabaseStat.mpcPressedJoin.increment() - presentMPCConnect(isHost: false) - } - - @objc private func keyboardCreateLocalRace() { - PlayerAnonymousMetrics.log(event: .pressedHost) - PlayerDatabaseStat.mpcPressedHost.increment() - presentMPCConnect(isHost: true) - } - - @objc private func keyboardJoinGlobalRace() { - PlayerAnonymousMetrics.log(event: .pressedGlobalJoin) - PlayerDatabaseStat.gkPressedJoin.increment() - presentGlobalConnect() - } + // // MARK: - Keyboard Support - + // + // override var keyCommands: [UIKeyCommand]? { + // return [ + // UIKeyCommand(title: "Create", + // action: #selector(keyboardCreateRace), + // input: "n", + // modifierFlags: .command) + // ] + // } + // + // @objc private func keyboardCreateRace() { + // PlayerAnonymousMetrics.log(event: .pressedHost) + // createRace() + // } + // } diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift index 820391a..d1e8da6 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift @@ -30,69 +30,90 @@ final internal class MenuViewController: UIViewController { private var isFirstAppearence = true override var canBecomeFirstResponder: Bool { return true } + private let nearbyRaceListener = NearbyRaceListener() + // MARK: - View Life Cycle - override func viewDidLoad() { super.viewDidLoad() - NotificationCenter.default.addObserver(forName: NSNotification.Name.localPlayerQuit, - object: nil, - queue: nil) { _ in - DispatchQueue.main.async { - self.dismiss(animated: true, completion: { - self.navigationController?.popToRootViewController(animated: false) - }) - } + NotificationCenter.default.addObserver( + forName: NSNotification.Name.localPlayerQuit, + object: nil, + queue: nil) { _ in + UIView.animate(withDuration: 0.5, animations: { + self.presentedViewController?.view.alpha = 0 + self.presentedViewController?.presentedViewController?.view.alpha = 0 + }, completion: { [weak self] _ in + self?.dismiss(animated: false) { + self?.navigationController?.popToRootViewController(animated: false) + } + }) } menuView.listenerUpdate = { [weak self] update in guard let self = self else { return } switch update { - case .presentDebug: self.presentDebugController() - case .presentGlobalConnect: self.presentGlobalConnect() - case .presentGlobalAuth: self.attemptGlobalAuthentication() - case .presentMPCConnect(let isHost): self.presentMPCConnect(isHost: isHost) - case .presentAlert(let alertController): - self.present(alertController, animated: true, completion: nil) + case .presentDebug: + self.presentDebugController() case .presentLeaderboard: let controller = GKGameCenterViewController() controller.gameCenterDelegate = self controller.viewState = .leaderboards controller.leaderboardTimeScope = .allTime self.present(controller, animated: true, completion: nil) + case .presentJoinPublicRace: + self.joinRace(raceCode: nil) + case .presentJoinPrivateRace: + let controller = UIAlertController( + title: "Join Private Race", + message: "Enter the race code", + preferredStyle: .alert) + controller.addTextField { textField in + textField.placeholder = "Race Code" + } + let action = UIAlertAction(title: "Join", style: .default) { [weak controller, weak self] _ in + guard let controller = controller, let code = controller.textFields?.first?.text else { return } + self?.joinRace(raceCode: code) + } + controller.addAction(action) + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in + self?.menuView.animateMenuIn() + } + controller.addAction(cancelAction) + self.present(controller, animated: true, completion: nil) + case .presentCreateRace: + self.createRace() + case .presentAlert(let alert): + self.present(alert, animated: true, completion: nil) case .presentStats: let nav = WKRUINavigationController(rootViewController: StatsViewController()) - nav.modalPresentationStyle = .fullScreen + nav.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .phone ? .fullScreen : .formSheet self.present(nav, animated: true, completion: nil) - case.presentSubscription: - PlayerAnonymousMetrics.log(event: .forcedIntoStoreFromStats) + case .presentSubscription: + PlayerFirebaseAnalytics.log(event: .forcedIntoStoreFromStats) let controller = PlusViewController() controller.modalPresentationStyle = .overCurrentContext self.present(controller, animated: false, completion: nil) } - } - GlobalRaceHelper.shared.didReceiveInvite = { - DispatchQueue.main.async { - guard self.presentedViewController == nil else { return } - self.menuView.joinGlobalRace() - } - } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - PlayerAnonymousMetrics.log(event: .userAction("issue#119: menu viewDidAppear")) UIApplication.shared.isIdleTimerDisabled = false + PlayerCloudKitLiveRaceManager.shared.reset() + WKRUIPlayerImageManager.shared.clearConnectedPlayers() // adjusts views before animation if rotation occured menuView.setNeedsLayout() menuView.layoutIfNeeded() menuView.animateMenuIn(completion: { - if SKStoreReviewController.shouldPromptForRating { + if Defaults.shouldPromptForRating { #if !DEBUG SKStoreReviewController.requestReview() #endif @@ -109,25 +130,40 @@ final internal class MenuViewController: UIViewController { #else if isFirstAppearence { isFirstAppearence = false - attemptGlobalAuthentication() + setupGKAuthHandler() + setupInviteHandler() } #endif - let metrics = PlayerDatabaseMetrics.shared + let metrics = PlayerCloudKitStatsManager.shared metrics.log(value: UIDevice.current.name, for: "DeviceNames") metrics.log(value: PlusStore.shared.isPlus ? 1 : 0, for: "isPlus") - if let name = UserDefaults.standard.object(forKey: "name_preference") as? String { - metrics.log(value: name, for: "CustomNames") - } - promptForInvalidName() + nearbyRaceListener.start { host, raceCode in + let controller = UIAlertController( + title: "Nearby Race", + message: "\(host) is starting a race nearby. Would you like to join?", + preferredStyle: .alert) + let action = UIAlertAction(title: "Join", style: .default) { [weak self] _ in + DispatchQueue.main.async { + self?.joinRace(raceCode: raceCode) + } + } + controller.addAction(action) + controller.addCancelAction(title: "No") + DispatchQueue.main.async { + if self.presentedViewController == nil { + self.present(controller, animated: true, completion: nil) + } + } + } becomeFirstResponder() } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() let mode = WKRUIStyle.isDark(traitCollection) ? 1 : 0 - PlayerAnonymousMetrics.log(event: .interfaceMode, attributes: ["Dark": mode]) + PlayerFirebaseAnalytics.log(event: .interfaceMode, attributes: ["Dark": mode]) } override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { @@ -136,53 +172,52 @@ final internal class MenuViewController: UIViewController { } } - // MARK: - Name Checking - - - func promptForInvalidName() { - guard UserDefaults.standard.bool(forKey: "AttemptingMCPeerIDCreation") else { - return - } - UserDefaults.standard.set(false, forKey: "AttemptingMCPeerIDCreation") - - let message = "There was an unexpected issue starting a race with your player name. This can often occur when your name has too many emojis or too many letters. Please set a new custom player name before racing." - let alertController = UIAlertController(title: "Player Name Issue", message: message, preferredStyle: .alert) - - let laterAction = UIAlertAction(title: "Maybe Later", style: .cancel, handler: { _ in - PlayerAnonymousMetrics.log(event: .userAction("promptForInvalidName:rejected")) - }) - alertController.addAction(laterAction) - - let settingsAction = UIAlertAction(title: "Change Name", style: .default, handler: { _ in - PlayerAnonymousMetrics.log(event: .userAction("promptForInvalidName:accepted")) - UIApplication.shared.openSettings() - }) - alertController.addAction(settingsAction) - - present(alertController, animated: true, completion: nil) - } - // MARK: - Other - - func presentMPCConnect(isHost: Bool) { + func prepareForRace(completion: @escaping () -> Void) { UIApplication.shared.isIdleTimerDisabled = true + nearbyRaceListener.stop() + resignFirstResponder() - let controller = MPCConnectViewController() - controller.isPlayerHost = isHost - navigationController?.pushViewController(controller, animated: false) + if presentedViewController == nil { + completion() + } else { + dismiss(animated: false) { + self.navigationController?.popToRootViewController(animated: false) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.nearbyRaceListener.stop() + completion() + } + } + } + } + + func joinRace(raceCode: String?) { + prepareForRace(completion: { + self.menuView.animateMenuOut { + let destination: RaceChecksViewController.Destination + if let code = raceCode { + destination = .joinPrivate(raceCode: code) + } else { + destination = .joinPublic + } + let controller = RaceChecksViewController(destination: destination) + self.navigationController?.pushViewController(controller, animated: false) + } + }) } - func presentGlobalConnect() { - if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + func createRace() { + if Defaults.isFastlaneSnapshotInstance { let controller = GameViewController(network: .solo(name: "_"), settings: WKRGameSettings()) let nav = WKRUINavigationController(rootViewController: controller) nav.modalPresentationStyle = .overCurrentContext present(nav, animated: true, completion: nil) - } else if GKLocalPlayer.local.isAuthenticated { - UIApplication.shared.isIdleTimerDisabled = true - let controller = GameKitConnectViewController() - navigationController?.pushViewController(controller, animated: false) } else { - presentGameKitAuthAlert() + prepareForRace(completion: { + let controller = RaceChecksViewController(destination: .hostPrivate) + self.navigationController?.pushViewController(controller, animated: false) + }) } } diff --git a/WikiRaces/Shared/Menu View Controllers/PlusViewController/MagicSubscription.swift b/WikiRaces/Shared/Menu View Controllers/PlusViewController/MagicSubscription.swift index 8f44bc7..ff53b3d 100644 --- a/WikiRaces/Shared/Menu View Controllers/PlusViewController/MagicSubscription.swift +++ b/WikiRaces/Shared/Menu View Controllers/PlusViewController/MagicSubscription.swift @@ -40,7 +40,7 @@ struct MagicSubscription { MagicSubscription.priceFormatter.locale = product.priceLocale guard let price = MagicSubscription.priceFormatter.string(from: product.price), - let subscriptionPeriod = product.subscriptionPeriod else { return nil } + let subscriptionPeriod = product.subscriptionPeriod else { return nil } self.price = price switch subscriptionPeriod.unit { diff --git a/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusStore.swift b/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusStore.swift index ba744da..57481d8 100644 --- a/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusStore.swift +++ b/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusStore.swift @@ -17,8 +17,6 @@ class PlusStore: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserv enum MagicError: Error { case unableToMakePayments case noProduct - case networkError(Error) - case serverError } enum PlusType { @@ -56,7 +54,11 @@ class PlusStore: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserv UserDefaults.standard.set(newValue, forKey: "isPlus") } get { - UserDefaults.standard.bool(forKey: "isPlus") + #if targetEnvironment(simulator) + return true + #else + return UserDefaults.standard.bool(forKey: "isPlus") + #endif } } @@ -181,10 +183,10 @@ class PlusStore: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserv os_log("%{public}s: called", log: .store, type: .info, #function) verifyReceiptTimer?.invalidate() verifyReceiptTimer = Timer.scheduledTimer(timeInterval: 0.5, - target: self, - selector: #selector(processPaymentQueueTransactions), - userInfo: nil, - repeats: false) + target: self, + selector: #selector(processPaymentQueueTransactions), + userInfo: nil, + repeats: false) } @objc diff --git a/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusView.swift b/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusView.swift index 675e36f..17e5fa5 100644 --- a/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusView.swift +++ b/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusView.swift @@ -142,15 +142,15 @@ class PlusView: UIView { forName: PlusStore.productsUpdatedNotificationName, object: nil, queue: nil) { [weak self] _ in - DispatchQueue.main.async { - guard let products = PlusStore.shared.products else { - self?.toggleProductButtons(on: false) - return - } - self?.standardOptionButton.label.text = products.standard.displayString - self?.ultimateOptionButton.label.text = products.ultimate.displayString - self?.toggleProductButtons(on: true) + DispatchQueue.main.async { + guard let products = PlusStore.shared.products else { + self?.toggleProductButtons(on: false) + return } + self?.standardOptionButton.label.text = products.standard.displayString + self?.ultimateOptionButton.label.text = products.ultimate.displayString + self?.toggleProductButtons(on: true) + } } toggleProductButtons(on: PlusStore.shared.products != nil) @@ -334,13 +334,13 @@ class PlusView: UIView { func openPrivacy() { let urlString = "https://www.andrewfinke.com/privacy" guard let url = URL(string: urlString) else { fatalError() } - UIApplication.shared.open(url, options: [:], completionHandler: nil) + UIApplication.shared.open(url) } @objc func openTerms() { let urlString = "https://www.andrewfinke.com/terms" guard let url = URL(string: urlString) else { fatalError() } - UIApplication.shared.open(url, options: [:], completionHandler: nil) + UIApplication.shared.open(url) } } diff --git a/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusViewController.swift b/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusViewController.swift index 339919d..4654271 100644 --- a/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/PlusViewController/PlusViewController.swift @@ -68,7 +68,7 @@ class PlusViewController: UIViewController { self.plusView.center = CGPoint(x: self.view.center.x, y: self.view.center.y) } self.alphaView.alpha = 0.5 - }, completion: nil) + }, completion: nil) } // MARK: - Helpers - @@ -84,11 +84,11 @@ class PlusViewController: UIViewController { self.plusView.center = CGPoint(x: self.view.center.x, y: self.view.frame.height + self.plusView.bounds.height / 2) self.alphaView.alpha = 0 - }, completion: { _ in - self.dismiss(animated: false, completion: { - self.onCompletion?() - }) + }, completion: { _ in + self.dismiss(animated: false, completion: { + self.onCompletion?() + }) - }) + }) } } diff --git a/WikiRaces/Shared/Menu View Controllers/StatsViewController/StatsViewController.swift b/WikiRaces/Shared/Menu View Controllers/StatsViewController/StatsViewController.swift index 78c62aa..ce066bf 100644 --- a/WikiRaces/Shared/Menu View Controllers/StatsViewController/StatsViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/StatsViewController/StatsViewController.swift @@ -46,14 +46,14 @@ class StatsViewController: UITableViewController { detail: formatted(for: [.mpcPoints, .gkPoints], suffix: "Point")), Item( name: "Points Per Race", - detail: String(format: "%.2f PPR", PlayerDatabaseStat.multiplayerAverage.value())), + detail: String(format: "%.2f PPR", PlayerUserDefaultsStat.multiplayerAverage.value())), Item( name: "Total Time", detail: formatted(for: [.soloTotalTime, .mpcTotalTime, .gkTotalTime], suffix: "S", checkPlural: false)), Item( name: "Pages Viewed", detail: formatted(for: [.soloPages, .mpcPages, .gkPages], suffix: "Page")) - ]), + ]), Section( name: "Solo", items: [ @@ -63,9 +63,9 @@ class StatsViewController: UITableViewController { Item( name: "Total Time", detail: formatted(for: .soloTotalTime, suffix: "S", checkPlural: false)) - ]), + ]), Section( - name: "Local Races", + name: "Private Races", items: [ Item( name: "Races", @@ -82,9 +82,9 @@ class StatsViewController: UITableViewController { Item( name: "Players Raced", detail: nil) - ]), + ]), Section( - name: "Global Races", + name: "Public Races", items: [ Item( name: "Races", @@ -101,7 +101,7 @@ class StatsViewController: UITableViewController { Item( name: "Players Raced", detail: nil) - ]), + ]), Section( name: "Other", items: [ @@ -114,7 +114,7 @@ class StatsViewController: UITableViewController { Item( name: "Needed Help", detail: formatted(for: [.soloHelp, .mpcHelp, .gkHelp], suffix: "Time")) - ]) + ]) ] // MARK: - Initalization - @@ -191,11 +191,11 @@ class StatsViewController: UITableViewController { } - static func formatted(for item: PlayerDatabaseStat, suffix: String?, checkPlural: Bool = true) -> String { + static func formatted(for item: PlayerUserDefaultsStat, suffix: String?, checkPlural: Bool = true) -> String { return formatted(for: item.value(), suffix: suffix, checkPlural: checkPlural) } - static func formatted(for items: [PlayerDatabaseStat], suffix: String?, checkPlural: Bool = true) -> String { + static func formatted(for items: [PlayerUserDefaultsStat], suffix: String?, checkPlural: Bool = true) -> String { var value = Double() items.forEach { value += $0.value() } return formatted(for: value, suffix: suffix, checkPlural: checkPlural) diff --git a/WikiRaces/Shared/Other/Defaults.swift b/WikiRaces/Shared/Other/Defaults.swift new file mode 100644 index 0000000..d6fe649 --- /dev/null +++ b/WikiRaces/Shared/Other/Defaults.swift @@ -0,0 +1,82 @@ +// +// Defaults.swift +// WikiRaces +// +// Created by Andrew Finke on 6/24/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation + +struct Defaults { + + private static let defaults = UserDefaults.standard + + private static let promptedGlobalRacesPopularityKey = "PromptedGlobalRacesPopularity" + static var promptedGlobalRacesPopularity: Bool { + get { + return defaults.bool(forKey: promptedGlobalRacesPopularityKey) + } + set { + defaults.set(newValue, forKey: promptedGlobalRacesPopularityKey) + } + } + + private static let fastlaneKey = "FASTLANE_SNAPSHOT" + static var isFastlaneSnapshotInstance: Bool { + get { + return defaults.bool(forKey: fastlaneKey) + } + } + + private static let isAutoInviteOnKey = "isAutoInviteOnKey" + static var isAutoInviteOn: Bool { + get { + return defaults.bool(forKey: isAutoInviteOnKey) + } + set { + defaults.set(newValue, forKey: isAutoInviteOnKey) + } + } + + private static let promptedAutoInviteKey = "PromptedAutoInviteKey" + static var promptedAutoInvite: Bool { + get { + return defaults.bool(forKey: promptedAutoInviteKey) + } + set { + defaults.set(newValue, forKey: promptedAutoInviteKey) + } + } + + private static let shouldPromptForRatingKey = "ShouldPromptForRating" + static var shouldPromptForRating: Bool { + get { + return defaults.bool(forKey: shouldPromptForRatingKey) + } + set { + defaults.setValue(newValue, forKey: shouldPromptForRatingKey) + } + } + + private static let shouldAutoSaveResultImageKey = "force_save_result_image" + static var shouldAutoSaveResultImage: Bool { + get { + return defaults.bool(forKey: shouldAutoSaveResultImageKey) + } + set { + defaults.setValue(newValue, forKey: shouldAutoSaveResultImageKey) + } + } + + private static let promptedSoloRacesStatsKey = "PromptedSoloRacesStatsKey" + static var promptedSoloRacesStats: Bool { + get { + return defaults.bool(forKey: promptedSoloRacesStatsKey) + } + set { + defaults.set(newValue, forKey: promptedSoloRacesStatsKey) + } + } + +} diff --git a/WikiRaces/Shared/Other/GameKit Support/GKHelper.swift b/WikiRaces/Shared/Other/GameKit Support/GKHelper.swift new file mode 100644 index 0000000..2b877fb --- /dev/null +++ b/WikiRaces/Shared/Other/GameKit Support/GKHelper.swift @@ -0,0 +1,103 @@ +// +// GKHelper.swift +// WikiRaces +// +// Created by Andrew Finke on 6/24/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import Foundation +import GameKit +import WKRUIKit + +#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) +import FirebaseAnalytics +import FirebaseCrashlytics +#endif + +class GKHelper { + + enum AuthResult { + case controller(UIViewController) + case error(Error) + case isAuthenticated + } + + static let shared = GKHelper() + + private var pendingInvite: String? + var inviteHandler: ((String) -> Void)? { + didSet { + pushInviteToHandler() + } + } + + private var pendingResult: AuthResult? + var authHandler: ((AuthResult) -> Void)? { + didSet { + pushResultToHandler() + } + } + + // MARK: - Initalization - + + private init() {} + + // MARK: - Handlers - + + private func pushResultToHandler() { + guard let result = pendingResult, let handler = authHandler else { return } + DispatchQueue.main.async { + handler(result) + } + pendingResult = nil + } + + private func pushInviteToHandler() { + guard let invite = pendingInvite, let handler = inviteHandler else { return } + DispatchQueue.main.async { + handler(invite) + } + pendingInvite = nil + } + + // MARK: - Helpers - + + func start() { + guard !Defaults.isFastlaneSnapshotInstance else { + return + } + + DispatchQueue.global().async { + GKLocalPlayer.local.authenticateHandler = { controller, error in + DispatchQueue.main.async { + if let error = error { + self.pendingResult = .error(error) + } else if let controller = controller { + self.pendingResult = .controller(controller) + } else if GKLocalPlayer.local.isAuthenticated { + self.pendingResult = .isAuthenticated + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + WKRUIPlayerImageManager.shared.connected(to: GKLocalPlayer.local, completion: nil) + } + + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + let playerName = GKLocalPlayer.local.alias + Crashlytics.crashlytics().setUserID(playerName) + Analytics.setUserProperty(playerName, forName: "playerName") + #endif + } else { + fatalError() + } + self.pushResultToHandler() + } + } + } + } + + func acceptedInvite(code: String) { + pendingInvite = code + pushInviteToHandler() + } + +} diff --git a/WikiRaces/Shared/Other/GameKit Support/GKMatchRequest+WKR.swift b/WikiRaces/Shared/Other/GameKit Support/GKMatchRequest+WKR.swift new file mode 100644 index 0000000..36dee42 --- /dev/null +++ b/WikiRaces/Shared/Other/GameKit Support/GKMatchRequest+WKR.swift @@ -0,0 +1,66 @@ +// +// GKMatchRequest+WKR.swift +// WikiRaces +// +// Created by Andrew Finke on 6/26/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import WKRKit +import os.log + +extension GKMatchRequest { + + static func hostRequest(raceCode: String, isInital: Bool) -> GKMatchRequest { + let request = GKMatchRequest() + request.minPlayers = 2 + request.maxPlayers = isInital ? 2 : min(GKMatchRequest.maxPlayersAllowedForMatch(of: .peerToPeer), WKRKitConstants.current.maxGlobalRacePlayers) + request.playerGroup = RaceCodeGenerator.playerGroup(for: raceCode) + request.playerAttributes = 0xFFFF0000 + + os_log("%{public}s: %{public}s, %{public}ld, %{public}ld-%{public}ld, %{public}ld, %{public}ld", + log: .matchSupport, + type: .info, + #function, + raceCode, + isInital ? 1 : 0, + request.minPlayers, + request.maxPlayers, + request.playerGroup, + request.playerAttributes + ) + + return request + } + + static func joinRequest(raceCode: String?) -> GKMatchRequest { + let request = GKMatchRequest() + request.minPlayers = 2 + if let code = raceCode { + request.maxPlayers = min(GKMatchRequest.maxPlayersAllowedForMatch(of: .peerToPeer), WKRKitConstants.current.maxGlobalRacePlayers) + request.playerGroup = RaceCodeGenerator.playerGroup(for: code) + request.playerAttributes = 0x0000FFFF + } else { + request.maxPlayers = 2 + request.playerGroup = publicRacePlayerGroup() + } + + os_log("%{public}s: %{public}s, %{public}ld-%{public}ld, %{public}ld, %{public}ld", + log: .matchSupport, + type: .info, + #function, + raceCode ?? "-", + request.minPlayers, + request.maxPlayers, + request.playerGroup, + request.playerAttributes + ) + + return request + } + + private static func publicRacePlayerGroup() -> Int { + return 10 + } +} diff --git a/WikiRaces/Shared/Other/GameKit Support/RaceCodeGenerator.swift b/WikiRaces/Shared/Other/GameKit Support/RaceCodeGenerator.swift new file mode 100644 index 0000000..158eb42 --- /dev/null +++ b/WikiRaces/Shared/Other/GameKit Support/RaceCodeGenerator.swift @@ -0,0 +1,115 @@ +// +// RaceCodeGenerator.swift +// WikiRaces +// +// Created by Andrew Finke on 6/23/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import GameKit +import WKRKit +import os.log + +#if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) +import FirebasePerformance +#endif + +class RaceCodeGenerator { + + private static let validCharacters = "abcdefghijklmnopqrstuvwxyz" + private static let validCharactersSet = CharacterSet(charactersIn: validCharacters) + private static let validCharactersArray = validCharacters.map { $0 } + + private(set) static var codes: [String] = { + let invalidCharacters = CharacterSet.alphanumerics.inverted + return WKRKitConstants.current.finalArticles + .filter { $0.count < 10 } + .map { String($0.dropFirst()).lowercased() } + .filter { validCharactersSet.isSuperset(of: CharacterSet(charactersIn: $0)) } + }() + + private var callback: ((String) -> Void)? + private var isCancelled = false + private var attempts = 0 + private var newStartDate = Date() + + func new(code: @escaping ((String) -> Void)) { + os_log("RaceCodeGenerator: %{public}s", log: .matchSupport, type: .info, #function) + callback = code + attempts = 0 + newStartDate = Date() + generate() + } + + func cancel() { + os_log("RaceCodeGenerator: %{public}s", log: .matchSupport, type: .info, #function) + callback = nil + isCancelled = true + } + + // TODO: Switch to CloudKit + private func generate() { + guard !isCancelled else { return } + attempts += 1 + + guard let code = RaceCodeGenerator.codes.randomElement else { fatalError() } + os_log("RaceCodeGenerator: %{public}s: %{public}s", log: .matchSupport, type: .info, #function, code) + + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + let traceGK = Performance.startTrace(name: "Race Code Trace: queryPlayerGroupActivity") + let traceTotal = Performance.startTrace(name: "Race Code Trace: Total Success Time") + #endif + + let queryCheckStartDate = Date() + GKMatchmaker.shared().queryPlayerGroupActivity(RaceCodeGenerator.playerGroup(for: code)) { [weak self] count, error in + if count == 0 && error == nil { + os_log("RaceCodeGenerator: %{public}s: queryPlayerGroupActivity success in %{public}f", log: .matchSupport, type: .info, #function, -queryCheckStartDate.timeIntervalSinceNow) + PlayerFirebaseAnalytics.log(event: .raceCodeGKSuccess) + + guard let self = self, !self.isCancelled else { return } + PlayerCloudKitLiveRaceManager.shared.isRaceCodeValid(raceCode: code, host: GKLocalPlayer.local.alias) { result in + switch result { + case .valid: + self.callback?(code) + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + traceTotal?.stop() + #endif + PlayerFirebaseAnalytics.log(event: .raceCodeGenerationFinished, attributes: [ + "attempts": self.attempts, + "duration": -self.newStartDate.timeIntervalSinceNow + ]) + case .invalid: + self.generate() + case .noiCloudAccount: + #if targetEnvironment(simulator) + self.callback?(code) + #endif + break + } + } + } else { + os_log("RaceCodeGenerator: %{public}s: queryPlayerGroupActivity failed, count: %{public}ld, error: %{public}s", log: .matchSupport, type: .info, #function, count, error?.localizedDescription ?? "-") + self?.generate() + PlayerFirebaseAnalytics.log(event: .raceCodeGKFailed) + } + #if !MULTIWINDOWDEBUG && !targetEnvironment(macCatalyst) + traceGK?.stop() + #endif + } + } + + static func playerGroup(for raceCode: String) -> Int { + guard raceCode.count < 10 else { return -1 } + + let formattedCode = raceCode.lowercased() + var playerGroup = formattedCode.count + for (index, char) in formattedCode.enumerated() { + let charValue: Int = (validCharactersArray.firstIndex(of: char) ?? 50) + 1 + let offset = Int(pow(Double(10), Double(index * 2) + 1)) + playerGroup += offset * charValue + } + + os_log("%{public}s: %{public}s -> %{public}ld", log: .matchSupport, type: .info, #function, raceCode, playerGroup) + return playerGroup + } +} diff --git a/WikiRaces/Shared/Other/GlobalRacesHelper.swift b/WikiRaces/Shared/Other/GlobalRacesHelper.swift deleted file mode 100644 index c1bd8ee..0000000 --- a/WikiRaces/Shared/Other/GlobalRacesHelper.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// GlobalRacesHelper.swift -// WikiRaces -// -// Created by Andrew Finke on 2/23/19. -// Copyright © 2019 Andrew Finke. All rights reserved. -// - -import GameKit - -final class GlobalRaceHelper: NSObject, GKLocalPlayerListener { - - // MARK: - Properties - - - static let shared = GlobalRaceHelper() - var lastInvite: GKInvite? - var isHandlerSetup = false - var didReceiveInvite: (() -> Void)? - - // MARK: - Helpers - - - func authenticate(completion: ((UIViewController?, Error?, _ forceShowErrorMessage: Bool) -> Void)?) { - guard !UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") else { - return - } - - guard !isHandlerSetup else { - completion?(nil, nil, true) - return - } - isHandlerSetup = true - - GKLocalPlayer.local.authenticateHandler = { controller, error in - DispatchQueue.main.async { - completion?(controller, error, false) - if GKLocalPlayer.local.isAuthenticated { - GKLocalPlayer.local.register(self) - } - } - } - } - - // MARK: - GKLocalPlayerListener - - - func player(_ player: GKPlayer, didAccept invite: GKInvite) { - PlayerDatabaseStat.gkInvitedToMatch.increment() - lastInvite = invite - didReceiveInvite?() - } -} diff --git a/WikiRaces/Shared/Other/PointerInteractionTableViewCell.swift b/WikiRaces/Shared/Other/PointerInteractionTableViewCell.swift deleted file mode 100644 index d1d6aa2..0000000 --- a/WikiRaces/Shared/Other/PointerInteractionTableViewCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// PointerInteractionTableViewCell.swift -// WikiRaces -// -// Created by Andrew Finke on 3/24/20. -// Copyright © 2020 Andrew Finke. All rights reserved. -// - -import UIKit - -class PointerInteractionTableViewCell: UITableViewCell { - - // MARK: - Initalization - - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - if #available(iOS 13.4, *) { - configurePointer() - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -@available(iOS 13.4, *) -extension PointerInteractionTableViewCell: UIPointerInteractionDelegate { - - func configurePointer() { - let interaction = UIPointerInteraction(delegate: self) - addInteraction(interaction) - } - - // MARK: - UIPointerInteractionDelegate - - - public func pointerInteraction(_ interaction: UIPointerInteraction, - styleFor region: UIPointerRegion) -> UIPointerStyle? { - var pointerStyle: UIPointerStyle? - if let interactionView = interaction.view { - let targetedPreview = UITargetedPreview(view: interactionView) - pointerStyle = UIPointerStyle(effect: UIPointerEffect.highlight(targetedPreview)) - } - return pointerStyle - } -} diff --git a/WikiRaces/Shared/Other/WKRAnimationDurationConstants.swift b/WikiRaces/Shared/Other/WKRAnimationDurationConstants.swift index cb0d472..28ba548 100644 --- a/WikiRaces/Shared/Other/WKRAnimationDurationConstants.swift +++ b/WikiRaces/Shared/Other/WKRAnimationDurationConstants.swift @@ -9,20 +9,10 @@ import Foundation internal struct WKRAnimationDurationConstants { + static let menuToggle: Double = 0.5 - static let menuToggle = 0.75 - - static let gameFadeIn = 1.0 - static let gameFadeInDelay = 0.25 - static let gameFadeOut = 4.0 - static let gameFadeOutDelay = 2.0 - - static let votingLabelsFlash = 0.75 - static let votingTableAppear = 0.5 - static let votingEndedStateTransition = 0.5 - static let votingFinalPageStateTransition = 1.5 - - static let resultsOverlayButtonToggle = 0.5 - static let resultsTableFlash = 2.0 - static let resultsCellLabelsFade = 0.25 + static let gameFadeIn: Double = 2.5 + static let gameFadeInDelay: Double = 3 + static let gameFadeOut: Double = 2.5 + static let gameFadeOutDelay: Double = 1 } diff --git a/WikiRaces/Shared/Other/WKRAppDelegate.swift b/WikiRaces/Shared/Other/WKRAppDelegate.swift index 5aa83f9..994a2bd 100644 --- a/WikiRaces/Shared/Other/WKRAppDelegate.swift +++ b/WikiRaces/Shared/Other/WKRAppDelegate.swift @@ -21,7 +21,7 @@ internal class WKRAppDelegate: UIResponder, UIApplicationDelegate { WKRUIKitConstants.updateConstants() // Don't be that app that prompts people when they first open it - SKStoreReviewController.shouldPromptForRating = false + Defaults.shouldPromptForRating = false } final func cleanTempDirectory() { @@ -44,4 +44,15 @@ internal class WKRAppDelegate: UIResponder, UIApplicationDelegate { } } + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + guard let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems, + let code = items.first(where: { $0.name == "Code" })?.value else { + + return false + } + GKHelper.shared.acceptedInvite(code: code) + PlayerFirebaseAnalytics.log(event: .raceCodeLinkOpened) + return true + } + } diff --git a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift index 6a447ce..dda7e75 100644 --- a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift +++ b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift @@ -36,9 +36,9 @@ extension GameViewController { let headerCommands = (1..<10).map { index in return UIKeyCommand(title: "Toggle Section \(index)", - action: #selector(keyboardAttemptToggleSection(_:)), - input: index.description, - modifierFlags: .command) + action: #selector(keyboardAttemptToggleSection(_:)), + input: index.description, + modifierFlags: .command) } commands.append(contentsOf: headerCommands) @@ -73,10 +73,10 @@ extension GameViewController { @objc private func keyboardAttemptToggleSection(_ keyCommand: UIKeyCommand) { guard let webView = webView, - let input = keyCommand.input, - let index = Int(input), - gameState == .race else { - return + let input = keyCommand.input, + let index = Int(input), + gameState == .race else { + return } let script = "document.getElementsByClassName('section-heading')[\(index - 1)].click()" diff --git a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift index 29fde77..8f8fe3f 100644 --- a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift +++ b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift @@ -15,10 +15,11 @@ extension GameViewController { // MARK: - WKRGameManager - func setupGameManager() { - gameManager = WKRGameManager(networkConfig: networkConfig, - settings: gameSettings, - gameUpdate: { [weak self] gameUpdate in - self?.gameUpdate(gameUpdate) + gameManager = WKRGameManager( + networkConfig: networkConfig, + settings: gameSettings, + gameUpdate: { [weak self] gameUpdate in + self?.gameUpdate(gameUpdate) }, votingUpdate: { [weak self] votingUpdate in self?.votingUpdate(votingUpdate) }, resultsUpdate: { [weak self] resultsUpdate in @@ -29,7 +30,7 @@ extension GameViewController { private func gameUpdate(_ gameUpdate: WKRGameManager.GameUpdate) { switch gameUpdate { case .state(let state): - PlayerAnonymousMetrics.log(event: .gameState("Transition: \(state).")) + PlayerFirebaseAnalytics.log(event: .gameState("Transition: \(state).")) func startTransition(to state: WKRGameState) { transitionState = .inProgress @@ -53,6 +54,10 @@ extension GameViewController { startTransition(to: state) } } + + if networkConfig.isHost { + PlayerCloudKitLiveRaceManager.shared.updated(state: state) + } case .error(let error): DispatchQueue.main.async { self.errorOccurred(error) @@ -68,30 +73,32 @@ extension GameViewController { private func processRaceStats(points: Int, place: Int?, webViewPixelsScrolled: Int, pages: [WKRPage]) { guard let raceType = statRaceType else { return } - PlayerStatsManager.shared.completedRace(type: raceType, - points: points, - place: place, - timeRaced: timeRaced, - pixelsScrolled: webViewPixelsScrolled, - pages: pages, - isEligibleForPoints: gameSettings.points.isStandard, - isEligibleForSpeed: gameSettings.startPage.isStandard) - - let event: PlayerAnonymousMetrics.Event + PlayerStatsManager.shared.completedRace( + type: raceType, + points: points, + place: place, + timeRaced: timeRaced, + pixelsScrolled: webViewPixelsScrolled, + pages: pages, + isEligibleForPoints: gameSettings.points.isStandard, + isEligibleForSpeed: gameSettings.startPage.isStandard) + + let event: PlayerFirebaseAnalytics.Event switch raceType { - case .mpc: + case .private: event = .mpcRaceCompleted - case .gameKit: + case .public: event = .gkRaceCompleted case .solo: event = .soloRaceCompleted } - PlayerAnonymousMetrics.log(event: event, - attributes: [ - "Time": timeRaced, - "Points": points, - "WebViewScrolled": webViewPixelsScrolled - ]) + PlayerFirebaseAnalytics.log( + event: event, + attributes: [ + "Time": timeRaced, + "Points": points, + "WebViewScrolled": webViewPixelsScrolled + ]) } private func votingUpdate(_ votingUpdate: WKRGameManager.VotingUpdate) { @@ -101,17 +108,26 @@ extension GameViewController { if time == 0 { logFinalVotes() } - case .voteInfo(let voteInfo): - votingViewController?.voteInfo = voteInfo - case .finalPage(let page): - finalPage = page - votingViewController?.finalPageSelected(page) - - UIView.animate(withDuration: WKRAnimationDurationConstants.gameFadeIn, - delay: WKRAnimationDurationConstants.gameFadeInDelay, - animations: { - self.webView?.alpha = 1.0 - }, completion: nil) + case .votingState(let votingState): + votingViewController?.votingState = votingState + case .raceConfig(let config): + finalPage = config.endingPage + votingViewController?.finalPageSelected(config.endingPage) + + votingViewController?.backingAlpha = 1 + view.alpha = 1 + UIView.animate( + withDuration: WKRAnimationDurationConstants.gameFadeIn, + delay: WKRAnimationDurationConstants.gameFadeInDelay, + animations: { [weak self] in + self?.votingViewController?.backingAlpha = 0 + }, completion: nil) + + // Because web view is a pain, force relayout + navigationController?.setNavigationBarHidden(true, animated: false) + navigationController?.setNavigationBarHidden(false, animated: false) + + PlayerCloudKitLiveRaceManager.shared.updated(config: config) } } @@ -125,8 +141,14 @@ extension GameViewController { if resultsViewController?.state != .hostResults { resultsViewController?.resultsInfo = resultsInfo } + if networkConfig.isHost { + PlayerCloudKitLiveRaceManager.shared.updated(resultsInfo: resultsInfo) + } case .hostResultsInfo(let resultsInfo): resultsViewController?.resultsInfo = resultsInfo + if networkConfig.isHost { + PlayerCloudKitLiveRaceManager.shared.updated(resultsInfo: resultsInfo) + } case .readyStates(let readyStates): resultsViewController?.readyStates = readyStates } @@ -140,9 +162,10 @@ extension GameViewController { navigationItem.leftBarButtonItem?.isEnabled = false navigationItem.rightBarButtonItem?.isEnabled = false - let alertController = UIAlertController(title: error.title, - message: error.message, - preferredStyle: .alert) + let alertController = UIAlertController( + title: error.title, + message: error.message, + preferredStyle: .alert) let quitAction = UIAlertAction(title: "Menu", style: .default) { [weak self] _ in self?.attemptQuit() } @@ -155,20 +178,20 @@ extension GameViewController { }) let info = "errorOccurred: " + error.message - PlayerAnonymousMetrics.log(event: .error(info)) + PlayerFirebaseAnalytics.log(event: .error(info)) - PlayerAnonymousMetrics.log(event: .fatalError, - attributes: ["Error": error.title as Any]) + PlayerFirebaseAnalytics.log(event: .fatalError, + attributes: ["Error": error.title as Any]) } func logEvent(_ logEvent: WKRLogEvent) { #if !MULTIWINDOWDEBUG - let metric = PlayerAnonymousMetrics.Event(event: logEvent) + let metric = PlayerFirebaseAnalytics.Event(event: logEvent) if metric == .pageView, let raceType = PlayerStatsManager.RaceType(networkConfig) { PlayerStatsManager.shared.viewedPage(raceType: raceType) } - PlayerAnonymousMetrics.log(event: metric, attributes: logEvent.attributes) + PlayerFirebaseAnalytics.log(event: metric, attributes: logEvent.attributes) #endif } @@ -236,7 +259,7 @@ extension GameViewController { private func showVotingController(completion: @escaping () -> Void) { let controller = VotingViewController() - controller.voteInfo = gameManager.voteInfo + controller.votingState = gameManager.votingState controller.quitAlertController = quitAlertController(raceStarted: false) controller.listenerUpdate = { [weak self] update in guard let self = self else { return } @@ -244,14 +267,14 @@ extension GameViewController { case .voted(let page): self.gameManager.player(.voted(page)) // capitalized to keep consistent with past analytics - PlayerAnonymousMetrics.log(event: .voted, + PlayerFirebaseAnalytics.log(event: .voted, attributes: ["Page": page.title?.capitalized as Any]) if let raceType = self.statRaceType { - var stat = PlayerDatabaseStat.mpcVotes + var stat = PlayerUserDefaultsStat.mpcVotes switch raceType { - case .mpc: stat = .mpcVotes - case .gameKit: stat = .gkVotes + case .private: stat = .mpcVotes + case .public: stat = .gkVotes case .solo: stat = .soloVotes } stat.increment() @@ -280,15 +303,6 @@ extension GameViewController { if activeViewController != resultsViewController || resultsViewController == nil { dismissActiveController(completion: { [weak self] in self?.showResultsController(completion: completion) - UIView.animate(withDuration: WKRAnimationDurationConstants.gameFadeOut, - delay: WKRAnimationDurationConstants.gameFadeOutDelay, - options: .beginFromCurrentState, - animations: { - self?.webView?.alpha = 0.0 - }, completion: { [weak self] _ in - self?.title = nil - self?.navigationController?.setNavigationBarHidden(true, animated: false) - }) }) } else { resultsViewController?.state = gameState @@ -298,14 +312,14 @@ extension GameViewController { navigationItem.rightBarButtonItem = nil if gameState == .hostResults && networkConfig.isHost { - PlayerAnonymousMetrics.log(event: .hostEndedRace) + PlayerFirebaseAnalytics.log(event: .hostEndedRace) } } private func showResultsController(completion: @escaping () -> Void) { let controller = ResultsViewController() + controller.backingAlpha = 0 controller.localPlayer = gameManager.localPlayer - controller.addPlayersViewController = gameManager.hostNetworkInterface() controller.state = gameManager.gameState controller.resultsInfo = gameManager.hostResultsInfo controller.isPlayerHost = networkConfig.isHost @@ -332,7 +346,19 @@ extension GameViewController { self?.connectingLabel.alpha = 0.0 self?.activityIndicatorView.alpha = 0.0 completion() + + UIView.animate( + withDuration: WKRAnimationDurationConstants.gameFadeOut, + delay: WKRAnimationDurationConstants.gameFadeOutDelay, + options: .beginFromCurrentState, + animations: { + controller.backingAlpha = 1 + }, completion: { [weak self] _ in + self?.title = nil + self?.view.alpha = 0 + }) } + } private func transitionToRace(completion: @escaping () -> Void) { @@ -342,8 +368,6 @@ extension GameViewController { self?.timeRaced += 1 }) - navigationController?.setNavigationBarHidden(false, animated: false) - navigationItem.leftBarButtonItem = helpBarButtonItem navigationItem.rightBarButtonItem = quitBarButtonItem @@ -353,7 +377,7 @@ extension GameViewController { dismissActiveController(completion: completion) if networkConfig.isHost { - PlayerAnonymousMetrics.log( + PlayerFirebaseAnalytics.log( event: .hostStartedRace, attributes: [ "Page": finalPage?.title as Any, @@ -365,14 +389,12 @@ extension GameViewController { // MARK: - Log Final Votes - private func logFinalVotes() { - guard networkConfig.isHost, let votingInfo = gameManager.voteInfo else { return } - for index in 0.. String in return player.alias } - PlayerStatsManager.shared.connected(to: playerNames, raceType: .gameKit) - case .mpc(_, let session, _): - // Due to low usage, not accounting for players joining mid session - let playerNames = session.connectedPeers.map { peerID -> String in - return peerID.displayName + PlayerStatsManager.shared.connected(to: playerNames, raceType: .private) + case .gameKitPublic(let match, _): + let playerNames = match.players.map { player -> String in + return player.alias } - PlayerStatsManager.shared.connected(to: playerNames, raceType: .mpc) + PlayerStatsManager.shared.connected(to: playerNames, raceType: .public) default: break } @@ -188,7 +183,7 @@ final internal class GameViewController: UIViewController { @objc func helpButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) if gameSettings.other.isHelpEnabled { showHelp() @@ -204,7 +199,7 @@ final internal class GameViewController: UIViewController { @objc func quitButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) let alertController = quitAlertController(raceStarted: true) present(alertController, animated: true, completion: nil) @@ -218,6 +213,7 @@ final internal class GameViewController: UIViewController { controller.url = gameManager.finalPageURL controller.linkTapped = { [weak self] in self?.gameManager.enqueue(message: "Links disabled in help", + for: nil, duration: 2.0, isRaceSpecific: true, playHaptic: true) @@ -225,17 +221,17 @@ final internal class GameViewController: UIViewController { self.activeViewController = controller let navController = WKRUINavigationController(rootViewController: controller) - navController.modalPresentationStyle = .formSheet + navController.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .phone ? .fullScreen : .formSheet present(navController, animated: true, completion: nil) - PlayerAnonymousMetrics.log(event: .userAction("flagButtonPressed:help")) - PlayerAnonymousMetrics.log(event: .usedHelp, + PlayerFirebaseAnalytics.log(event: .userAction("flagButtonPressed:help")) + PlayerFirebaseAnalytics.log(event: .usedHelp, attributes: ["Page": self.finalPage?.title as Any]) if let raceType = statRaceType { - let stat: PlayerDatabaseStat + let stat: PlayerUserDefaultsStat switch raceType { - case .mpc: stat = .mpcHelp - case .gameKit: stat = .gkHelp + case .private: stat = .mpcHelp + case .public: stat = .gkHelp case .solo: stat = .soloHelp } stat.increment() @@ -243,7 +239,7 @@ final internal class GameViewController: UIViewController { } func reloadPage() { - PlayerAnonymousMetrics.log(event: .userAction("flagButtonPressed:reload")) + PlayerFirebaseAnalytics.log(event: .userAction("flagButtonPressed:reload")) self.webView?.reload() } diff --git a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift index 7f260ea..685b17c 100644 --- a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift @@ -151,8 +151,6 @@ final internal class HistoryViewController: UITableViewController, SFSafariViewC } } - print("stats i: \(shouldUpdateStatsInfo) c: \(shouldUpdateStatsCount)") - tableView.performBatchUpdates({ tableView.reloadRows(at: rowsToReload, with: .none) tableView.insertRows(at: rowsToInsert, with: .fade) @@ -173,7 +171,7 @@ final internal class HistoryViewController: UITableViewController, SFSafariViewC // MARK: - Actions - @IBAction func doneButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) presentingViewController?.dismiss(animated: true, completion: nil) } @@ -212,7 +210,7 @@ final internal class HistoryViewController: UITableViewController, SFSafariViewC if indexPath.section == 1 { guard let cell = tableView.dequeueReusableCell(withIdentifier: HistoryTableViewStatsCell.reuseIdentifier, for: indexPath) as? HistoryTableViewStatsCell else { - fatalError("Unable to create cell") + fatalError("Unable to create cell") } cell.stat = stats?.raw[indexPath.row] @@ -220,7 +218,7 @@ final internal class HistoryViewController: UITableViewController, SFSafariViewC } guard let cell = tableView.dequeueReusableCell(withIdentifier: HistoryTableViewCell.reuseIdentifier, for: indexPath) as? HistoryTableViewCell else { - fatalError("Unable to create cell") + fatalError("Unable to create cell") } let playerState = player?.state ?? .connecting @@ -259,7 +257,7 @@ final internal class HistoryViewController: UITableViewController, SFSafariViewC present(controller, animated: true, completion: nil) safariController = controller - PlayerAnonymousMetrics.log(event: .openedHistorySF) + PlayerFirebaseAnalytics.log(event: .openedHistorySF) } // MARK: - SFSafariViewControllerDelegate - diff --git a/WikiRaces/Shared/Race View Controllers/Other/BackingVisualEffectViewController.swift b/WikiRaces/Shared/Race View Controllers/Other/BackingVisualEffectViewController.swift new file mode 100644 index 0000000..6f7748c --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/Other/BackingVisualEffectViewController.swift @@ -0,0 +1,63 @@ +// +// BackingVisualEffectViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 7/2/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRUIKit + +internal class BackingVisualEffectViewController: UIViewController { + + // MARK: - Properties + + private let visualEffectView = UIVisualEffectView(effect: UIBlurEffect.wkrBlurEffect) + final var contentView: UIView { + return visualEffectView.contentView + } + + private var hostingView: UIView? + private let backingAlphaView = UIView() + var backingAlpha: CGFloat { + set { + backingAlphaView.alpha = newValue + } + get { + return backingAlphaView.alpha + } + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + guard let nav = navigationController else { return } + nav.navigationBar.setBackgroundImage(UIImage(), for: .default) + nav.navigationBar.shadowImage = UIImage() + nav.navigationBar.isTranslucent = true + nav.view.backgroundColor = .clear + + backingAlphaView.backgroundColor = UIColor.systemBackground + + view.addSubview(backingAlphaView) + view.addSubview(visualEffectView) + } + + // MARK: - Interface + + func configure(hostingView: UIView) { + hostingView.frame = view.frame + hostingView.backgroundColor = .clear + contentView.addSubview(hostingView) + self.hostingView = hostingView + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + backingAlphaView.frame = view.frame + visualEffectView.frame = view.frame + hostingView?.frame = view.frame + } +} diff --git a/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift b/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift deleted file mode 100644 index 6c98149..0000000 --- a/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// CenteredTableViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit -import WKRUIKit - -internal class CenteredTableViewController: UIViewController { - - // MARK: - Properties - - final var overlayButtonTitle: String? { - set { - overlayButton.title = newValue ?? "" - } - get { - return overlayButton.title - } - } - - final var isOverlayButtonHidden: Bool { - set { - guard isInterfaceLoaded else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { - self.isOverlayButtonHidden = newValue - }) - return - } - overlayBottomConstraint.constant = newValue ? overlayHeightConstraint.constant : 0 - descriptionLabelBottomConstraint.constant = newValue ? -view.safeAreaInsets.bottom: 0 - } - get { - return overlayBottomConstraint.constant == overlayHeightConstraint.constant - } - } - - final let guideLabel = UILabel() - final let descriptionLabel = UILabel() - - final let reuseIdentifier = "cell" - final let tableView = WKRUICenteredTableView() - final let overlayButton = WKRUIButton() - - final var contentView: UIView! - final private var overlayBottomConstraint: NSLayoutConstraint! - final private var overlayHeightConstraint: NSLayoutConstraint! - final private var descriptionLabelBottomConstraint: NSLayoutConstraint! - - final private var isInterfaceLoaded = false - - // MARK: - View Life Cycle - - override func viewDidLoad() { - super.viewDidLoad() - setupInterface() - isInterfaceLoaded = true - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - guideLabel.textColor = .wkrSubtitleTextColor(for: traitCollection) - descriptionLabel.textColor = .wkrTextColor(for: traitCollection) - } - - // MARK: - Interface - - private func setupInterface() { - let visualEffectView = UIVisualEffectView(effect: UIBlurEffect.wkrLightBlurEffect) - - tableView.clipsToBounds = false - tableView.estimatedRowHeight = 0 - tableView.isUserInteractionEnabled = false - tableView.translatesAutoresizingMaskIntoConstraints = false - - visualEffectView.contentView.addSubview(tableView) - tableView.allowsSelection = true - - guideLabel.textAlignment = .center - guideLabel.font = UIFont.systemFont(ofSize: 18.0, weight: .medium) - guideLabel.adjustsFontSizeToFitWidth = true - guideLabel.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.contentView.addSubview(guideLabel) - - descriptionLabel.textAlignment = .center - descriptionLabel.font = UIFont(monospaceSize: 20, weight: .medium) - descriptionLabel.adjustsFontSizeToFitWidth = true - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.contentView.addSubview(descriptionLabel) - - self.view = visualEffectView - self.contentView = visualEffectView.contentView - - let overlayView = setupBottomOverlayView() - overlayBottomConstraint = overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 70) - overlayHeightConstraint = overlayView.heightAnchor.constraint(equalToConstant: 70) - - let fakeWidth = tableView.widthAnchor.constraint(equalToConstant: 500) - fakeWidth.priority = UILayoutPriority.defaultLow - - navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) - navigationController?.navigationBar.shadowImage = UIImage() - navigationController?.navigationBar.isTranslucent = true - navigationController?.view.backgroundColor = .clear - - descriptionLabelBottomConstraint = descriptionLabel.bottomAnchor.constraint(equalTo: overlayView.topAnchor) - - let constraints: [NSLayoutConstraint] = [ - tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - tableView.leftAnchor.constraint(greaterThanOrEqualTo: visualEffectView.leftAnchor, constant: 25), - tableView.rightAnchor.constraint(lessThanOrEqualTo: visualEffectView.rightAnchor), - tableView.centerXAnchor.constraint(equalTo: visualEffectView.centerXAnchor), - tableView.widthAnchor.constraint(lessThanOrEqualToConstant: 400), - fakeWidth, - - overlayView.leftAnchor.constraint(equalTo: view.leftAnchor), - overlayView.rightAnchor.constraint(equalTo: view.rightAnchor), - overlayHeightConstraint, - overlayBottomConstraint, - - descriptionLabel.leftAnchor.constraint(equalTo: visualEffectView.leftAnchor), - descriptionLabel.rightAnchor.constraint(equalTo: visualEffectView.rightAnchor), - descriptionLabelBottomConstraint, - descriptionLabel.heightAnchor.constraint(equalToConstant: 50), - - guideLabel.leftAnchor.constraint(equalTo: visualEffectView.leftAnchor), - guideLabel.rightAnchor.constraint(equalTo: visualEffectView.rightAnchor), - guideLabel.bottomAnchor.constraint(equalTo: descriptionLabel.topAnchor) - ] - - NSLayoutConstraint.activate(constraints) - } - - private func setupBottomOverlayView() -> WKRUIBottomOverlayView { - guard let visualEffectView = view as? UIVisualEffectView else { - fatalError("View not a UIVisualEffectView") - } - - let bottomOverlayView = WKRUIBottomOverlayView() - bottomOverlayView.translatesAutoresizingMaskIntoConstraints = false - bottomOverlayView.clipsToBounds = true - visualEffectView.contentView.addSubview(bottomOverlayView) - - overlayButton.title = "Ready up" - overlayButton.translatesAutoresizingMaskIntoConstraints = false - overlayButton.addTarget(self, action: #selector(overlayButtonPressed), for: .touchUpInside) - bottomOverlayView.contentView.addSubview(overlayButton) - - let constraints = [ - overlayButton.centerXAnchor.constraint(equalTo: bottomOverlayView.centerXAnchor), - overlayButton.topAnchor.constraint(equalTo: bottomOverlayView.topAnchor, constant: 15), - overlayButton.widthAnchor.constraint(equalToConstant: 250), - overlayButton.heightAnchor.constraint(equalToConstant: 40) - ] - NSLayoutConstraint.activate(constraints) - - return bottomOverlayView - } - - @objc - func overlayButtonPressed() { - fatalError("overlayButtonPressed not implemented") - } - - final func registerTableView(for controller: T) { - tableView.delegate = controller - tableView.dataSource = controller - } - - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - descriptionLabelBottomConstraint.constant = -view.safeAreaInsets.bottom - overlayHeightConstraint.constant = 70 + view.safeAreaInsets.bottom - isOverlayButtonHidden = true - } - -} diff --git a/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift b/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift index 0b10e62..e0e6924 100644 --- a/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift @@ -70,7 +70,7 @@ final internal class HelpViewController: UIViewController, WKNavigationDelegate // MARK: - Actions - @IBAction func doneButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) presentingViewController?.dismiss(animated: true, completion: nil) } diff --git a/WikiRaces/Shared/Race View Controllers/Other/VisualEffectViewController.swift b/WikiRaces/Shared/Race View Controllers/Other/VisualEffectViewController.swift new file mode 100644 index 0000000..df95008 --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/Other/VisualEffectViewController.swift @@ -0,0 +1,50 @@ +// +// VisualEffectViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 8/5/17. +// Copyright © 2017 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRUIKit + +internal class VisualEffectViewController: UIViewController { + + // MARK: - Properties + + private let visualEffectView = UIVisualEffectView(effect: UIBlurEffect.wkrBlurEffect) + final var contentView: UIView { + return visualEffectView.contentView + } + + // MARK: - View Life Cycle + + override func loadView() { + self.view = visualEffectView + } + + override func viewDidLoad() { + super.viewDidLoad() + guard let nav = navigationController else { return } + nav.navigationBar.setBackgroundImage(UIImage(), for: .default) + nav.navigationBar.shadowImage = UIImage() + nav.navigationBar.isTranslucent = true + nav.view.backgroundColor = .clear + } + + // MARK: - Interface + + func configure(hostingView: UIView) { + hostingView.backgroundColor = .clear + hostingView.translatesAutoresizingMaskIntoConstraints = false + let contraints = [ + hostingView.leftAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leftAnchor), + hostingView.rightAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.rightAnchor), + hostingView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor) + ] + contentView.addSubview(hostingView) + NSLayoutConstraint.activate(contraints) + } +} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift index 7690f12..99557c2 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift @@ -115,9 +115,7 @@ extension ResultRenderer { ] var lastDetailLabel: UILabel? - for index in 0.. 340 { - constraints.append(subtitleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 15)) - } else { - constraints.append(subtitleLabel.heightAnchor.constraint(equalToConstant: 0)) - } - - NSLayoutConstraint.activate(constraints) - - } - - // MARK: - Updating - - - private func update(playerName: NSAttributedString, - detail: String, - subtitle: NSAttributedString, - animated: Bool) { - if animated { - UIView.transition(with: playerLabel, - duration: WKRAnimationDurationConstants.resultsCellLabelsFade, - options: .transitionCrossDissolve, - animations: { [weak self] in - self?.playerLabel.attributedText = playerName - }, completion: nil) - UIView.transition(with: detailLabel, - duration: WKRAnimationDurationConstants.resultsCellLabelsFade, - options: .transitionCrossDissolve, - animations: { [weak self] in - self?.detailLabel.text = detail - }, completion: nil) - UIView.transition(with: subtitleLabel, - duration: WKRAnimationDurationConstants.resultsCellLabelsFade, - options: .transitionCrossDissolve, - animations: { [weak self] in - self?.subtitleLabel.attributedText = subtitle - }, completion: nil) - } else { - playerLabel.attributedText = playerName - detailLabel.text = detail - subtitleLabel.attributedText = subtitle - } - } - - func updateResults(for player: WKRPlayer, animated: Bool) { - guard let history = player.raceHistory, let entry = history.entries.last else { - playerLabel.text = player.name - subtitleLabel.text = "-" - detailLabel.text = "-" - if player.state == .forcedEnd { - detailLabel.text = "DNF" - } else if player.state == .quit { - detailLabel.text = "Quit" - } - return - } - - let pageTitle = entry.page.title ?? "-" - var pageTitleAttributedString = NSMutableAttributedString(string: pageTitle, attributes: nil) - if entry.linkHere { - let detail = " Link Here" - pageTitleAttributedString = NSMutableAttributedString(string: pageTitle + detail, attributes: nil) - - let range = NSRange(location: pageTitle.count, length: detail.count) - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: UIColor.lightGray, - .font: UIFont.systemFont(ofSize: 13, weight: .semibold) - ] - pageTitleAttributedString.addAttributes(attributes, range: range) - } - - var detailString = player.state.text - if player.state == .foundPage, let duration = WKRDurationFormatter.string(for: history.duration) { - detailString = duration - } else if player.state == .racing { - detailString = "" - } else if player.state == .forcedEnd || player.state == .forfeited { - detailString = "DNF" - } - isShowingActivityIndicatorView = player.state == .racing - - update(playerName: playerNameAttributedString(for: player), - detail: detailString, - subtitle: pageTitleAttributedString, - animated: animated) - } - - func updateStandings(for sessionResults: WKRResultsInfo.WKRProfileSessionResults) { - isShowingActivityIndicatorView = false - isShowingCheckmark = false - - let detailString: String - if sessionResults.points == 1 { - detailString = sessionResults.points.description + " PT" - } else { - detailString = sessionResults.points.description + " PTS" - } - - var subtitleString: String - if sessionResults.ranking == 1 { - subtitleString = "1st Place" - } else if sessionResults.ranking == 2 { - subtitleString = "2nd Place" - } else if sessionResults.ranking == 3 { - subtitleString = "3rd Place" - } else { - subtitleString = "\(sessionResults.ranking)th Place" - } - subtitleString += sessionResults.isTied ? " (Tied)" : "" - - update(playerName: NSAttributedString(string: sessionResults.profile.name), - detail: detailString, - subtitle: NSAttributedString(string: subtitleString), - animated: false) - } - - // MARK: - Other - - - func playerNameAttributedString(for player: WKRPlayer) -> NSAttributedString { - if player.isCreator { - self.isPlayerCreator = true - let name = player.name - let nameAttributedString = NSMutableAttributedString(string: name, attributes: nil) - let range = NSRange(location: 0, length: name.count) - - let font = UIFont.systemRoundedFont(ofSize: 20, weight: .semibold) - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: UIColor(displayP3Red: 69.0/255.0, - green: 145.0/255.0, - blue: 208.0/255.0, - alpha: 1.0), - .font: font - ] - nameAttributedString.addAttributes(attributes, range: range) - return nameAttributedString - } - return NSAttributedString(string: player.name, attributes: nil) - } - -} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift index 946afe4..a5b8c62 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift @@ -7,15 +7,16 @@ // import UIKit +import WKRUIKit extension ResultsViewController { // MARK: - Actions - @objc func doneButtonPressed(_ sender: Any) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + @objc func doneButtonPressed() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) guard let alertController = quitAlertController else { - PlayerAnonymousMetrics.log(event: .backupQuit, + PlayerFirebaseAnalytics.log(event: .backupQuit, attributes: ["RawGameState": state.rawValue]) listenerUpdate?(.quit) return @@ -23,21 +24,13 @@ extension ResultsViewController { present(alertController, animated: true, completion: nil) } - @objc func addPlayersBarButtonItemPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - guard let controller = addPlayersViewController else { return } - present(controller, animated: true, completion: nil) - PlayerAnonymousMetrics.log(event: .hostStartMidMatchInviting) - } - @objc func shareResultsBarButtonItemPressed(_ sender: UIBarButtonItem) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerFirebaseAnalytics.log(event: .userAction(#function)) guard let image = resultImage else { return } let hackTitle = "Hack" let controller = UIActivityViewController(activityItems: [ image, - "#WikiRaces3" ], applicationActivities: nil) controller.completionWithItemsHandler = { [weak self] activityType, completed, _, _ in if !(completed && activityType == UIActivity.ActivityType.saveToCameraRoll), @@ -56,6 +49,25 @@ extension ResultsViewController { present(hack, animated: false, completion: { hack.present(controller, animated: true, completion: nil) }) - PlayerAnonymousMetrics.log(event: .openedShare) + PlayerFirebaseAnalytics.log(event: .openedShare) + } + + func tapped(playerID: String) { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) + + guard let resultsInfo = resultsInfo, let player = resultsInfo.player(for: playerID), state != .points else { + return + } + + let controller = HistoryViewController(style: .grouped) + historyViewController = controller + controller.player = player + + let navController = WKRUINavigationController(rootViewController: controller) + navController.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .phone ? .fullScreen : .formSheet + present(navController, animated: true, completion: nil) + + PlayerFirebaseAnalytics.log(event: .openedHistory, + attributes: ["GameState": state.rawValue.description as Any]) } } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift index 60363aa..7e643b8 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift @@ -14,7 +14,7 @@ extension ResultsViewController { override var keyCommands: [UIKeyCommand]? { var commands = [UIKeyCommand]() - if !isOverlayButtonHidden { + if model.buttonEnabled { let command = UIKeyCommand(title: "Ready Up", action: #selector(keyboardAttemptReadyUp), input: " ", @@ -33,14 +33,14 @@ extension ResultsViewController { @objc private func keyboardAttemptReadyUp() { - guard !isOverlayButtonHidden else { return } - overlayButtonPressed() + guard model.buttonEnabled else { return } + readyUpButtonPressed() } @objc private func keyboardAttemptQuit(_ keyCommand: UIKeyCommand) { guard presentedViewController == nil, navigationItem.rightBarButtonItem?.isEnabled ?? false else { return } - doneButtonPressed(keyCommand) + doneButtonPressed() } } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift deleted file mode 100644 index d065581..0000000 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// ResultsViewController+TableView.swift -// WikiRaces -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit -import WKRKit -import WKRUIKit - -extension ResultsViewController: UITableViewDataSource, UITableViewDelegate { - - // MARK: - UITableViewDataSource - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return resultsInfo?.playerCount ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? ResultsTableViewCell, - let resultsInfo = resultsInfo else { - fatalError("Unable to create cell") - } - configure(cell: cell, with: resultsInfo, at: indexPath.row) - return cell - } - - private func configure(cell: ResultsTableViewCell, with resultsInfo: WKRResultsInfo, at index: Int) { - switch state { - case .results, .hostResults: - let player = resultsInfo.raceRankingsPlayer(at: index) - cell.isShowingCheckmark = readyStates?.isPlayerReady(player) ?? false - cell.updateResults(for: player, animated: true) - case .points: - let sessionResults = resultsInfo.sessionResults(at: index) - cell.updateStandings(for: sessionResults) - default: - // Unexpected state - cell.isShowingActivityIndicatorView = false - cell.isShowingCheckmark = false - } - } - - // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - - guard let resultsInfo = resultsInfo else { - return - } - - let controller = HistoryViewController(style: .grouped) - historyViewController = controller - controller.player = resultsInfo.raceRankingsPlayer(at: indexPath.row) - - let navController = WKRUINavigationController(rootViewController: controller) - navController.modalPresentationStyle = .formSheet - present(navController, animated: true, completion: nil) - - PlayerAnonymousMetrics.log(event: .openedHistory, - attributes: ["GameState": state.rawValue.description as Any]) - } - -} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift index 03d690f..d65ca83 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift @@ -10,7 +10,9 @@ import UIKit import WKRKit import WKRUIKit -final internal class ResultsViewController: CenteredTableViewController { +import SwiftUI + +final internal class ResultsViewController: BackingVisualEffectViewController { // MARK: - Types - @@ -21,18 +23,21 @@ final internal class ResultsViewController: CenteredTableViewController { // MARK: - Properties - + let model = ResultsContentViewModel() + private lazy var contentViewHosting = UIHostingController( + rootView: ResultsContentView( + model: model, + readyUpButtonPressed: { [weak self] in + self?.readyUpButtonPressed() + }, tappedPlayerID: { [weak self] playerID in + self?.tapped(playerID: playerID) + })) + var listenerUpdate: ((ListenerUpdate) -> Void)? var historyViewController: HistoryViewController? var quitAlertController: UIAlertController? - var addPlayersViewController: UIViewController? - - var addPlayersBarButtonItem: UIBarButtonItem? - var shareResultsBarButtonItem: UIBarButtonItem? - - var isAnimatingToPointsStandings = false var isPulsingReadyButton = false - var hasAnimatedToPointsStandings = false let resultRenderer = ResultRenderer() var resultImage: UIImage? { @@ -44,36 +49,27 @@ final internal class ResultsViewController: CenteredTableViewController { // MARK: - Game States - var localPlayer: WKRPlayer? - - var isPlayerHost = false { - didSet { - if isPlayerHost && addPlayersViewController != nil { - addPlayersBarButtonItem?.isEnabled = false - } else if let button = shareResultsBarButtonItem { - navigationItem.leftBarButtonItems = [button] - } else { - navigationItem.leftBarButtonItems = nil - } - } - } + var isPlayerHost = false var state: WKRGameState = .results { didSet { updatedState(oldState: oldValue) + model.update(to: resultsInfo, readyStates: readyStates, for: state) } } var readyStates: WKRReadyStates? { didSet { if state == .hostResults { - updateTableViewForNewReadyStates() + model.update(to: resultsInfo, readyStates: readyStates, for: state) + checkIfOtherPlayersReady() } } } var resultsInfo: WKRResultsInfo? { didSet { - updateTableView(oldValue) + model.update(to: resultsInfo, readyStates: readyStates, for: state) updateHistoryController() } } @@ -89,121 +85,63 @@ final internal class ResultsViewController: CenteredTableViewController { override func viewDidLoad() { super.viewDidLoad() - becomeFirstResponder() - - registerTableView(for: self) - overlayButtonTitle = "Ready up" + title = "RESULTS" - guideLabel.text = "TAP PLAYER TO VIEW HISTORY" - descriptionLabel.text = "WAITING FOR PLAYERS TO FINISH" + addChild(contentViewHosting) + configure(hostingView: contentViewHosting.view) + contentViewHosting.didMove(toParent: self) - tableView.isUserInteractionEnabled = true - tableView.register(ResultsTableViewCell.self, forCellReuseIdentifier: reuseIdentifier) - tableView.estimatedRowHeight = 150 - tableView.rowHeight = UITableView.automaticDimension + model.footerTopText = "TAP PLAYER TO VIEW HISTORY" + model.footerBottomText = "WAITING FOR PLAYERS TO FINISH" let shareResultsBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareResultsBarButtonItemPressed(_:))) shareResultsBarButtonItem.isEnabled = false - let addPlayersBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, - target: self, - action: #selector(addPlayersBarButtonItemPressed)) - addPlayersBarButtonItem.isEnabled = false - - var items = [shareResultsBarButtonItem] - if isPlayerHost && addPlayersViewController != nil { - items.append(addPlayersBarButtonItem) - } - navigationItem.leftBarButtonItems = items - - self.shareResultsBarButtonItem = shareResultsBarButtonItem - self.addPlayersBarButtonItem = addPlayersBarButtonItem + navigationItem.leftBarButtonItem = shareResultsBarButtonItem navigationItem.rightBarButtonItem = WKRUIBarButtonItem( systemName: "xmark", target: self, action: #selector(doneButtonPressed)) + + becomeFirstResponder() } // MARK: - Game Updates - private func updatedState(oldState: WKRGameState) { - if state == .results || state == .hostResults { - title = "RESULTS" - tableView.isUserInteractionEnabled = true - updateTableView() - } else { - tableView.isUserInteractionEnabled = false - - if let hack = presentedViewController, hack.title == "Hack" { - hack.dismiss(animated: true) { [weak self] in - self?.dismiss(animated: false, completion: nil) - } - } else if presentedViewController != nil { - dismiss(animated: true, completion: nil) - } + guard state == .points && oldState != .points else { + return + } - if oldState != .points { - let fadeAnimation = CATransition() - fadeAnimation.duration = WKRAnimationDurationConstants.resultsTableFlash / 4 - fadeAnimation.type = .fade - - let navLayer = navigationController?.navigationBar.layer - navLayer?.add(fadeAnimation, forKey: "fadeOut") - navigationItem.title = "" - - isAnimatingToPointsStandings = true - UIView.animateFlash( - withDuration: WKRAnimationDurationConstants.resultsTableFlash, - items: [tableView], - whenHidden: { - self.tableView.reloadData() - navLayer?.add(fadeAnimation, forKey: "fadeIn") - self.navigationItem.title = "STANDINGS" - }, completion: { - self.isAnimatingToPointsStandings = false - self.hasAnimatedToPointsStandings = true - }) - - if isPlayerHost, let results = resultsInfo { - DispatchQueue.global().async { - PlayerDatabaseMetrics.shared.record(results: results) - } - } - } + model.footerOpacity = 0 - if !isAnimatingToPointsStandings && hasAnimatedToPointsStandings { - tableView.reloadSections(IndexSet(integer: 0), with: .fade) + if isPlayerHost, let results = resultsInfo { + DispatchQueue.global().async { + PlayerCloudKitStatsManager.shared.record(results: results) } + } - UIView.animate(withDuration: 0.5, animations: { - self.guideLabel.alpha = 0.0 - self.descriptionLabel.alpha = 0.0 - }) + if let hack = presentedViewController, hack.title == "Hack" { + hack.dismiss(animated: true) { [weak self] in + self?.dismiss(animated: false, completion: nil) + } + } else if presentedViewController != nil { + dismiss(animated: true, completion: nil) } } private func updatedTime(oldTime: Int) { - tableView.isUserInteractionEnabled = true if oldTime == 100 { - UIView.animateFlash( - withDuration: 0.75, - items: [guideLabel, descriptionLabel], - whenHidden: { - self.guideLabel.text = "TAP PLAYER TO VIEW HISTORY" - self.descriptionLabel.text = "NEXT ROUND STARTS IN " + self.timeRemaining.description + " S" - }, completion: nil) + model.footerOpacity = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.model.footerTopText = "TAP PLAYER TO VIEW HISTORY" + self.model.footerBottomText = "NEXT ROUND STARTS IN " + self.timeRemaining.description + " S" + } guard let localPlayer = localPlayer else { return } - var resultsPlayer: WKRPlayer? - - for index in 0..<(resultsInfo?.playerCount ?? 0) { - let player = resultsInfo?.raceRankingsPlayer(at: index) - if localPlayer == player { - resultsPlayer = player - } - } + let resultsPlayer = resultsInfo?.raceRankings().first(where: { $0 == localPlayer }) guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }), let results = self.resultsInfo, @@ -212,27 +150,28 @@ final internal class ResultsViewController: CenteredTableViewController { resultRenderer.render(with: results, for: player, on: window) { [weak self] image in self?.resultImage = image - self?.shareResultsBarButtonItem?.isEnabled = true + self?.navigationItem.leftBarButtonItem?.isEnabled = true } } else { - descriptionLabel.text = "NEXT ROUND STARTS IN " + timeRemaining.description + " S" + model.footerBottomText = "NEXT ROUND STARTS IN " + timeRemaining.description + " S" + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + guard self.state != .points else { return } + self.model.footerOpacity = 1 + } } } // MARK: - Helpers - private func updatedResultsImage() { - guard let image = resultImage, UserDefaults.standard.bool(forKey: "force_save_result_image") else { return } + guard let image = resultImage, Defaults.shouldAutoSaveResultImage else { return } UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) - PlayerAnonymousMetrics.log(event: .automaticResultsImageSave) + PlayerFirebaseAnalytics.log(event: .automaticResultsImageSave) } - private func updateTableViewForNewReadyStates() { - guard !(state == .points && !hasAnimatedToPointsStandings) else { - return - } - - guard !isAnimatingToPointsStandings, + private func checkIfOtherPlayersReady() { + guard state == .hostResults, + !isPulsingReadyButton, let resultsInfo = resultsInfo, let readyStates = readyStates else { return @@ -240,93 +179,21 @@ final internal class ResultsViewController: CenteredTableViewController { var isAnotherPlayerReady = false var isLocalPlayerReady = false - for index in 0.. Bool in - return lhs.value > rhs.value - } - - tableView.performBatchUpdates({ - for (oldIndex, newIndex) in keys { - guard let cell = tableView.cellForRow(at: IndexPath(row: oldIndex)) as? ResultsTableViewCell else { - tableView.reloadSections(IndexSet(integer: 0), with: .fade) - return - } - - cell.updateResults(for: newInfo.raceRankingsPlayer(at: newIndex), animated: true) - if newIndex < oldIndex { - tableView.moveRow(at: IndexPath(row: oldIndex), - to: IndexPath(row: newIndex)) - } - } - }, completion: nil) - } - func updateHistoryController() { guard let player = historyViewController?.player, let updatedPlayer = resultsInfo?.updatedPlayer(for: player) else { @@ -336,33 +203,22 @@ final internal class ResultsViewController: CenteredTableViewController { } func showReadyUpButton(_ showReady: Bool) { - addPlayersBarButtonItem?.isEnabled = showReady if !showReady { - shareResultsBarButtonItem?.isEnabled = showReady - } - - isOverlayButtonHidden = !showReady - UIView.animate(withDuration: WKRAnimationDurationConstants.resultsOverlayButtonToggle) { - self.view.layoutIfNeeded() + navigationItem.leftBarButtonItem?.isEnabled = showReady } + model.buttonEnabled = showReady } - override func overlayButtonPressed() { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + func readyUpButtonPressed() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) UISelectionFeedbackGenerator().selectionChanged() - addPlayersBarButtonItem?.isEnabled = false - shareResultsBarButtonItem?.isEnabled = false - + navigationItem.leftBarButtonItem?.isEnabled = false listenerUpdate?(.readyButtonPressed) - isOverlayButtonHidden = true - - UIView.animate(withDuration: WKRAnimationDurationConstants.resultsOverlayButtonToggle) { - self.view.layoutIfNeeded() - } + model.buttonEnabled = false - PlayerAnonymousMetrics.log(event: .pressedReadyButton, attributes: ["Time": timeRemaining as Any]) + PlayerFirebaseAnalytics.log(event: .pressedReadyButton, attributes: ["Time": timeRemaining as Any]) } } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ListFooterView.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ListFooterView.swift new file mode 100644 index 0000000..d5d4772 --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ListFooterView.swift @@ -0,0 +1,42 @@ +// +// ListFooterView.swift +// WKRSwiftUI +// +// Created by Andrew Finke on 6/24/20. +// + +import SwiftUI +import WKRUIKit + +struct ListFooterView: View { + + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let topText: String + let bottomText: String + let textOpacity: Double + + var body: some View { + VStack { + Divider().frame(height: 2) + .padding(.bottom, 10) + Text(topText) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.wkrSubtitleTextColor(for: colorScheme)) + .opacity(textOpacity) + .animation(nil, value: topText) + .animation(.easeInOut(duration: 0.4), value: textOpacity) + Spacer() + Text(bottomText) + .font(Font(UIFont(monospaceSize: 16, weight: .medium))) + .foregroundColor(.wkrTextColor(for: colorScheme)) + .opacity(textOpacity) + .animation(nil, value: bottomText) + .animation(.easeInOut(duration: 0.4), value: textOpacity) + + } + .frame(minHeight: 55, maxHeight: 55) + .padding(.bottom, 20) + } + +} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsContentView.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsContentView.swift new file mode 100644 index 0000000..326d0af --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsContentView.swift @@ -0,0 +1,57 @@ +// +// ContentView.swift +// WKRSwiftUI +// +// Created by Andrew Finke on 6/24/20. +// + +import SwiftUI +import UIKit +import WKRUIKit + +struct ResultsContentView: View { + + @ObservedObject var model: ResultsContentViewModel + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let readyUpButtonPressed: () -> Void + let tappedPlayerID: (_ playerID: String) -> Void + + var body: some View { + VStack { + Spacer() + VStack { + ForEach(model.items) { item in + ResultsItemContentView(item: item) { + self.tappedPlayerID(item.player.id) + } + } + } + .padding(.all, 20) + .animation(.spring()) + .frame(maxWidth: 500) + Spacer() + + ZStack { + if model.buttonEnabled { + Button(action: readyUpButtonPressed) { + Text("READY UP") + .font(.system(size: 16, weight: .semibold)) + .kerning(4.4) + .foregroundColor(Color.wkrTextColor(for: colorScheme)) + .frame(width: 160, height: 36) + .overlay( + RoundedRectangle(cornerRadius: 5) + .stroke(Color.wkrTextColor(for: colorScheme), lineWidth: 1.7) + ) + } + .opacity(model.buttonFlashOpacity) + .animation(.easeInOut(duration: 1), value: model.buttonFlashOpacity) + } + } + .frame(height: 50) + .animation(.easeInOut) + ListFooterView(topText: model.footerTopText, bottomText: model.footerBottomText, textOpacity: model.footerOpacity) + } + } +} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsContentViewModel.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsContentViewModel.swift new file mode 100644 index 0000000..6b269f8 --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsContentViewModel.swift @@ -0,0 +1,128 @@ +// +// ResultsContentViewModel.swift +// WikiRaces +// +// Created by Andrew Finke on 6/24/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI +import WKRKit +import WKRUIKit + +class ResultsContentViewModel: ObservableObject { + + // MARK: - Types - + + struct Item: Identifiable, Equatable { + var id: String { return player.id } + let player: WKRPlayerProfile + + let subtitle: String + let title: String + let detail: String + let isRacing: Bool + let isReady: Bool + } + + // MARK: - Properties - + + @Published var items = [Item]() + @Published var footerTopText: String = "" + @Published var footerBottomText: String = "" + @Published var footerOpacity: Double = 1.0 + + @Published var buttonFlashOpacity: Double = 1 + @Published var buttonEnabled: Bool = false + + // MARK: - Helpers - + + func startPulsingButton() { + Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let self = self, self.buttonEnabled else { return } + DispatchQueue.main.async { + self.buttonFlashOpacity = self.buttonFlashOpacity == 1 ? 0.5 : 1 + } + } + } + + func update(to results: WKRResultsInfo?, readyStates: WKRReadyStates?, for state: WKRGameState) { + guard let results = results else { + items = [] + return + } + + var newItems = [Item]() + + if state == .points { + for playerResult in results.sessionResults() { + var subtitleString = positionString(for: playerResult.ranking) + subtitleString += playerResult.isTied ? " (Tied)" : "" + + let pointsSuffix = playerResult.points == 1 ? "" : "s" + let item = Item( + player: playerResult.profile, + subtitle: subtitleString, + title: playerResult.points.description + " Point" + pointsSuffix, + detail: "", + isRacing: false, + isReady: false) + newItems.append(item) + } + } else { + for (index, player) in results.raceRankings().enumerated() { + if state == .results || state == .hostResults { + var subtitle: String = "" + var title: String = "" + var detail: String = "-" + + if let history = player.raceHistory, let entry = history.entries.last { + title = entry.page.title ?? "-" + subtitle = player.state.text + if player.state == .foundPage, let duration = WKRDurationFormatter.string(for: history.duration) { + subtitle = positionString(for: index + 1) + detail = duration + } else if player.state == .racing { + subtitle = "Racing" + } else if player.state == .forcedEnd || player.state == .forfeited { + subtitle = "Did Not Finish" + } + + } else { + subtitle = "-" + title = "-" + if player.state == .forcedEnd { + subtitle = "Did not finish" + } else if player.state == .quit { + subtitle = "Quit" + } + } + + let item = Item( + player: player.profile, + subtitle: subtitle, + title: title, + detail: detail, + isRacing: player.state == .racing, + isReady: readyStates?.isPlayerReady(player) ?? false) + newItems.append(item) + } + } + } + items = newItems + } + + private func positionString(for position: Int) -> String { + if position == 1 { + return "1st Place" + } else if position == 2 { + return "2nd Place" + } else if position == 3 { + return "3rd Place" + } else if position < 21 { + return "\(position)th Place" + } else { + fatalError() + } + } +} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsItemContentView.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsItemContentView.swift new file mode 100644 index 0000000..2ff556a --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/SwiftUI/ResultsItemContentView.swift @@ -0,0 +1,56 @@ +// +// ResultsItemContentView.swift +// WKRSwiftUI +// +// Created by Andrew Finke on 6/24/20. +// + +import SwiftUI +import WKRUIKit + +struct ResultsItemContentView: View { + + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let item: ResultsContentViewModel.Item + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack { + WKRUIPlayerImageView(player: item.player, size: 44, effectSize: 3) + .padding(.trailing, 6) + VStack(alignment: .leading) { + Text(item.subtitle) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.wkrSubtitleTextColor(for: colorScheme)) + .transition(.opacity) + .id("Text(item.subtitle)" + item.subtitle) + Text(item.title) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.wkrTextColor(for: colorScheme)) + .fixedSize(horizontal: false, vertical: true) + .transition(.opacity) + .id("Text(item.title)" + item.title) + } + + Spacer() + Color.clear.frame(width: 1, height: 1) + if item.isReady { + Image(systemName: "checkmark") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.wkrTextColor(for: colorScheme)) + } else if item.isRacing { + ActivityIndicatorView() + .scaleEffect(0.8) + } else { + Text(item.detail) + .font(.system(size: 15, weight: .medium)) + .foregroundColor(.wkrTextColor(for: colorScheme)) + } + } + .frame(minHeight: 50) + + } + } +} diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingContentView.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingContentView.swift new file mode 100644 index 0000000..a4c83dc --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingContentView.swift @@ -0,0 +1,39 @@ +// +// VotingContentView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI + +struct VotingContentView: View { + + // MARK: - Properties - + + @ObservedObject var model: VotingContentViewModel + let tappedVotingItem: (VotingContentViewModel.Item) -> Void + + // MARK: - Body - + + var body: some View { + VStack { + Spacer() + VStack(alignment: .leading, spacing: 0) { + ForEach(self.model.items) { item in + VotingItemContentView(item: item, isFinalArticleSelected: self.model.isFinalArticleSelected) { + self.tappedVotingItem(item) + } + } + } + .padding(.all, 20) + .animation(.spring()) + .frame(maxWidth: 500) + Spacer() + ListFooterView(topText: model.footerTopText, bottomText: model.footerBottomText, textOpacity: model.footerOpacity) + } + .allowsHitTesting(model.isVotingEnabled) + } + +} diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingContentViewModel.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingContentViewModel.swift new file mode 100644 index 0000000..9f4777c --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingContentViewModel.swift @@ -0,0 +1,55 @@ +// +// VotingContentViewModel.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI +import WKRKit +import WKRUIKit + +class VotingContentViewModel: ObservableObject { + + // MARK: - Types - + + struct Item: Identifiable, Equatable { + var id: String { return page.url.absoluteString } + var page: WKRPage + var players: [WKRPlayerProfile] + var isFinal: Bool = false + } + + // MARK: - Properties - + + @Published var items = [Item]() + @Published var isFinalArticleSelected: Bool = false + @Published var isVotingEnabled: Bool = true + + @Published var footerTopText: String = " " + @Published var footerBottomText: String = " " + @Published var footerOpacity: Double = 1.0 + + // MARK: - Helpers - + + func update(votingState: WKRVotingState?) { + guard let state = votingState else { + items = [] + return + } + items = state.current.map { page, players in + return Item(page: page, players: players) + } + } + + func selected(finalPage: WKRPage) { + for (index, item) in items.enumerated() { + if item.page == finalPage { + items[index].isFinal = true + } + } + isFinalArticleSelected = true + isVotingEnabled = false + } +} diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingItemContentView.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingItemContentView.swift new file mode 100644 index 0000000..43c91c9 --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/VotingViewController/SwiftUI/VotingItemContentView.swift @@ -0,0 +1,46 @@ +// +// VotingItemContentView.swift +// WikiRaces +// +// Created by Andrew Finke on 6/25/20. +// Copyright © 2020 Andrew Finke. All rights reserved. +// + +import SwiftUI +import WKRUIKit + +struct VotingItemContentView: View { + + // MARK: - Properties - + + @Environment(\.colorScheme) var colorScheme: ColorScheme + + let item: VotingContentViewModel.Item + let isFinalArticleSelected: Bool + let action: () -> Void + + // MARK: - Body - + + var body: some View { + let isFinal = isFinalArticleSelected && item.isFinal + let opacity = isFinal || !isFinalArticleSelected ? 1 : 0.2 + + return Button(action: action) { + HStack { + Text(item.page.title ?? "-") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.wkrTextColor(for: colorScheme)) + .fixedSize(horizontal: false, vertical: true) + .padding(.vertical, 10) + Spacer() + Color.clear.frame(width: 1, height: 26) + ForEach(item.players) { player in + WKRUIPlayerImageView(player: player, size: 24, effectSize: 1) + } + } + } + .opacity(opacity) + .animation(Animation.easeInOut(duration: 2), value: isFinalArticleSelected) + .padding(.horizontal) + } +} diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingTableViewCell.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingTableViewCell.swift deleted file mode 100644 index 8157126..0000000 --- a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingTableViewCell.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// VotingTableViewCell.swift -// WikiRaces -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import Foundation -import UIKit -import WKRKit - -final internal class VotingTableViewCell: PointerInteractionTableViewCell { - - // MARK: - Properties - - - private let titleLabel = UILabel() - private let countLabel = UILabel() - - // MARK: - Property Observers - - - override var isSelected: Bool { - didSet { - setNeedsLayout() - } - } - - var vote: (page: WKRPage, votes: Int)? { - didSet { - if let vote = vote { - titleLabel.text = vote.page.title - countLabel.text = vote.votes.description - } else { - titleLabel.text = "UNKNOWN ERROR" - countLabel.text = "0" - } - } - } - - // MARK: - Initialization - - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = UIColor.clear - - titleLabel.numberOfLines = 0 - titleLabel.text = "" - titleLabel.textAlignment = .left - titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium) - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) - - countLabel.text = "0" - countLabel.textAlignment = .right - countLabel.font = UIFont.systemFont(ofSize: 19, weight: .medium) - countLabel.translatesAutoresizingMaskIntoConstraints = false - countLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - - contentView.addSubview(titleLabel) - contentView.addSubview(countLabel) - - let leftMarginConstraint = NSLayoutConstraint(item: titleLabel, - attribute: .left, - relatedBy: .equal, - toItem: self, - attribute: .leftMargin, - multiplier: 1.0, - constant: 0.0) - - let rightMarginConstraint = NSLayoutConstraint(item: countLabel, - attribute: .right, - relatedBy: .equal, - toItem: self, - attribute: .rightMargin, - multiplier: 1.0, - constant: 0.0) - let constraints = [ - leftMarginConstraint, - rightMarginConstraint, - - titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), - titleLabel.rightAnchor.constraint(equalTo: countLabel.rightAnchor, constant: -20), - - countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - // MARK: - View Life Cycle - - - override func layoutSubviews() { - super.layoutSubviews() - - titleLabel.textColor = .wkrTextColor(for: traitCollection) - if isSelected { - countLabel.textColor = .wkrVoteCountSelectedTextColor(for: traitCollection) - } else { - countLabel.textColor = .wkrVoteCountTextColor(for: traitCollection) - } - } - -} diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController+KB.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController+KB.swift index 33ca8b1..35a7860 100644 --- a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController+KB.swift +++ b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController+KB.swift @@ -21,9 +21,9 @@ extension VotingViewController { modifierFlags: .command) commands.append(command) } - if let info = voteInfo, tableView.isUserInteractionEnabled { - let voteCommands = (0.. Int { - return voteInfo?.pageCount ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? VotingTableViewCell else { - fatalError("Failed to create cell") - } - cell.vote = voteInfo?.page(for: indexPath.row) - return cell - } - - // MARK: - UITableViewDelegate - - - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - guard let lastIndexPath = tableView.indexPathForSelectedRow else { - UISelectionFeedbackGenerator().selectionChanged() - return indexPath - } - if lastIndexPath == indexPath { - return nil - } - - UISelectionFeedbackGenerator().selectionChanged() - - return indexPath - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) - - guard let vote = voteInfo?.page(for: indexPath.row) else { - return - } - - listenerUpdate?(.voted(vote.page)) - } - -} diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift index fee33b9..3d76fe0 100644 --- a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift @@ -9,8 +9,9 @@ import UIKit import WKRKit import WKRUIKit +import SwiftUI -final internal class VotingViewController: CenteredTableViewController { +final internal class VotingViewController: BackingVisualEffectViewController { // MARK: - Types - @@ -19,56 +20,61 @@ final internal class VotingViewController: CenteredTableViewController { case quit } + private enum ViewState { + case pre, voting, post + } + // MARK: - Properties - - private var isShowingGuide = false - private var isShowingVoteCountdown = true + private(set) var model = VotingContentViewModel() + private lazy var contentViewHosting = UIHostingController( + rootView: VotingContentView( + model: model, + tappedVotingItem: { [weak self] item in + self?.listenerUpdate?(.voted(item.page)) + UISelectionFeedbackGenerator().selectionChanged() + })) var listenerUpdate: ((ListenerUpdate) -> Void)? var quitAlertController: UIAlertController? - var voteInfo: WKRVoteInfo? { + private var state: ViewState = .pre + private var isFinalTextVisible = false + var votingState: WKRVotingState? { didSet { - let selectedPath = tableView.indexPathForSelectedRow - tableView.reloadData() - tableView.selectRow(at: selectedPath, animated: false, scrollPosition: .none) - if isViewLoaded && self.tableView.alpha != 1.0 { - UIView.animate(withDuration: WKRAnimationDurationConstants.votingTableAppear, animations: { - self.tableView.alpha = 1.0 - }) - } - if voteInfo?.pageCount == 1 { - guideLabel.text = "HOST SELECTED PAGE" - } + model.update(votingState: votingState) } } var voteTimeRemaing = 100 { didSet { - if isShowingVoteCountdown { - let timeString = "VOTING ENDS IN " + voteTimeRemaing.description + " S" - if !isShowingGuide { - UIView.animateFlash( - withDuration: WKRAnimationDurationConstants.votingLabelsFlash, - items: [guideLabel, descriptionLabel], - whenHidden: { - self.descriptionLabel.text = timeString - self.isShowingGuide = true - }, completion: nil) - tableView.isUserInteractionEnabled = true - } else if voteTimeRemaing == 0 { - descriptionLabel.text = timeString - votingEnded() + let timeString = "VOTING ENDS IN " + voteTimeRemaing.description + " S" + switch state { + case .pre: + model.footerOpacity = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.model.footerTopText = "TAP ARTICLE TO VOTE" + self.model.footerBottomText = timeString + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + self.model.footerOpacity = 1 + } + model.isVotingEnabled = true + state = .voting + case .voting: + if voteTimeRemaing == 0 { + model.footerOpacity = 0 } else { - descriptionLabel.text = timeString - tableView.isUserInteractionEnabled = true + model.footerBottomText = timeString } - } else { - descriptionLabel.text = "RACE STARTS IN " + voteTimeRemaing.description + " S" - if descriptionLabel.alpha != 1.0 { - UIView.animate(withDuration: 0.5, delay: 0.5, animations: { - self.descriptionLabel.alpha = 1.0 - }) + case .post: + model.footerTopText = "GET READY" + model.footerBottomText = "RACE STARTS IN " + voteTimeRemaing.description + " S" + if !isFinalTextVisible { + isFinalTextVisible = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.model.footerOpacity = 1 + } } } } @@ -79,24 +85,14 @@ final internal class VotingViewController: CenteredTableViewController { override func viewDidLoad() { super.viewDidLoad() - tableView.alpha = 0.0 - registerTableView(for: self) - title = "VOTING" - guideLabel.alpha = 0.0 - guideLabel.text = "TAP ARTICLE TO VOTE" - if voteInfo?.pageCount == 1 { - guideLabel.text = "HOST SELECTED PAGE" - } - - descriptionLabel.text = "VOTING STARTS SOON" - - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 100 + addChild(contentViewHosting) + configure(hostingView: contentViewHosting.view) + contentViewHosting.didMove(toParent: self) - tableView.register(VotingTableViewCell.self, - forCellReuseIdentifier: reuseIdentifier) + model.footerTopText = "PREPARING" + model.footerBottomText = "VOTING STARTS SOON" navigationItem.rightBarButtonItem = WKRUIBarButtonItem( systemName: "xmark", @@ -104,22 +100,13 @@ final internal class VotingViewController: CenteredTableViewController { action: #selector(doneButtonPressed)) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if voteInfo != nil { - UIView.animate(withDuration: WKRAnimationDurationConstants.votingTableAppear, animations: { - self.tableView.alpha = 1.0 - }) - } - } - // MARK: - Actions - - @objc func doneButtonPressed(_ sender: Any) { - PlayerAnonymousMetrics.log(event: .userAction(#function)) + @objc func doneButtonPressed() { + PlayerFirebaseAnalytics.log(event: .userAction(#function)) guard let alertController = quitAlertController else { - PlayerAnonymousMetrics.log(event: .backupQuit, - attributes: ["RawGameState": WKRGameState.voting.rawValue]) + PlayerFirebaseAnalytics.log(event: .backupQuit, + attributes: ["RawGameState": WKRGameState.voting.rawValue]) self.listenerUpdate?(.quit) return } @@ -128,26 +115,9 @@ final internal class VotingViewController: CenteredTableViewController { // MARK: - Helpers - - func votingEnded() { - isShowingVoteCountdown = false - tableView.isUserInteractionEnabled = false - UIView.animate(withDuration: WKRAnimationDurationConstants.votingEndedStateTransition) { - self.guideLabel.alpha = 0.0 - self.descriptionLabel.alpha = 0.0 - } - } - func finalPageSelected(_ page: WKRPage) { - guard let votingObject = voteInfo, let finalIndex = votingObject.index(of: page) else { - fatalError("Failed to select final page with \(String(describing: voteInfo))") - } - - UIView.animate(withDuration: WKRAnimationDurationConstants.votingFinalPageStateTransition) { - for index in 0...votingObject.pageCount where index != finalIndex { - let indexPath = IndexPath(row: index) - self.tableView.cellForRow(at: indexPath)?.alpha = 0.2 - } - } + model.selected(finalPage: page) + state = .post } } diff --git a/WikiRaces/Shared/Resources/Settings.bundle/Root.plist b/WikiRaces/Shared/Resources/Settings.bundle/Root.plist index bf8c572..ec61b31 100644 --- a/WikiRaces/Shared/Resources/Settings.bundle/Root.plist +++ b/WikiRaces/Shared/Resources/Settings.bundle/Root.plist @@ -10,35 +10,21 @@ Type PSToggleSwitchSpecifier Title - Always save result image + Invite Nearby Racers Key - force_save_result_image + isAutoInviteOnKey DefaultValue - - - - Type - PSGroupSpecifier - Title - LOCAL RACES NAME + Type - PSTextFieldSpecifier + PSToggleSwitchSpecifier Title - Name + Always Save Result Image Key - name_preference + force_save_result_image DefaultValue - - IsSecure - KeyboardType - Alphabet - AutocapitalizationType - Words - AutocorrectionType - No diff --git a/WikiRaces/Shared/Resources/Settings.bundle/en.lproj/Root.strings b/WikiRaces/Shared/Resources/Settings.bundle/en.lproj/Root.strings deleted file mode 100644 index 31bcb37..0000000 Binary files a/WikiRaces/Shared/Resources/Settings.bundle/en.lproj/Root.strings and /dev/null differ diff --git a/WikiRaces/Shared/Resources/SharedAssets.xcassets/Contents.json b/WikiRaces/Shared/Resources/SharedAssets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/WikiRaces/Shared/Resources/SharedAssets.xcassets/Contents.json +++ b/WikiRaces/Shared/Resources/SharedAssets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/WikiRaces/WikiRaces (Multi-Window)/Info.plist b/WikiRaces/WikiRaces (Multi-Window)/Info.plist index bf0913f..eedc30c 100644 --- a/WikiRaces/WikiRaces (Multi-Window)/Info.plist +++ b/WikiRaces/WikiRaces (Multi-Window)/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 872 + 884 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift b/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift index f8e099d..f9a2061 100644 --- a/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift +++ b/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift @@ -22,32 +22,46 @@ internal class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + // voting() + results() + } -// let controller = VotingViewController() -// let nav = UINavigationController(rootViewController: controller) -// -// (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = nav -// -// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { -// guard let appleURL = URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc."), -// let usaURL = URL(string: "https://en.m.wikipedia.org/wiki/United_States."), -// let waltURL = URL(string: "https://en.m.wikipedia.org/wiki/Walt_Disney") else { -// fatalError() -// } -// -// controller.voteInfo = WKRVoteInfo(pages: [ -// WKRPage(title: "Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc", url: appleURL), -// WKRPage(title: "United States", url: usaURL), -// WKRPage(title: "Walt Disney", url: waltURL) -// ]) -// } + func voting() { + + let controller = VotingViewController() + let nav = UINavigationController(rootViewController: controller) + + (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = nav + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let appleURL = URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc."), + let usaURL = URL(string: "https://en.m.wikipedia.org/wiki/United_States."), + let waltURL = URL(string: "https://en.m.wikipedia.org/wiki/Walt_Disney") else { + fatalError() + } + var state = WKRVotingState(pages: [ + WKRPage(title: "Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc Apple Inc", url: appleURL), + WKRPage(title: "United States", url: usaURL), + WKRPage(title: "Walt Disney", url: waltURL) + ]) + + controller.votingState = state + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + state.player(WKRPlayerProfile(name: "", playerID: ""), votedFor: WKRPage(title: "Walt Disney", url: waltURL)) + controller.votingState = state + controller.voteTimeRemaing = 5 + } + } + } + + func results() { let controller = ResultsViewController() let nav = UINavigationController(rootViewController: controller) (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = nav - //let names = ["Andrew", "Carol", "Tom", "Lisa", "Midnight", "Uncle D", "Pops", "Sam"] + // let names = ["Andrew", "Carol", "Tom", "Lisa", "Midnight", "Uncle D", "Pops", "Sam"] let names = ["Andrew", "Carol", "Tom", "Lisa"] for var index in 0.. 4 { player.state = .foundPage -// } else if arc4random() % 25 == 0 { -// player.state = .forcedEnd -// } else if arc4random() % 30 == 0 { -// player.state = .quit -// } else if arc4random() % 30 == 0 { -// player.state = .forfeited + // } else if arc4random() % 25 == 0 { + // player.state = .forcedEnd + // } else if arc4random() % 30 == 0 { + // player.state = .quit + // } else if arc4random() % 30 == 0 { + // player.state = .forfeited } else { player.finishedViewingLastPage(pixelsScrolled: 5) player.nowViewing(page: page, linkHere: arc4random() % 5 == 0) } } - + guard !stop else { return } // controller.player = self.players[0] controller.resultsInfo = WKRResultsInfo(racePlayers: self.players, racePoints: [:], sessionPoints: [:]) - controller.showReadyUpButton(true) - } } @@ -104,23 +122,22 @@ internal class ViewController: UIViewController { random() } - if self.players.filter({$0.state == .racing }).isEmpty && !self.rendered { - self.rendered = true - for player in self.players { - ResultRenderer().render(with: controller.resultsInfo!, - for: player, - on: controller.contentView, - completion: { _ in - }) - } - } + // if self.players.filter({$0.state == .racing }).isEmpty && !self.rendered { + // self.rendered = true + // for player in self.players { + // ResultRenderer().render(with: controller.resultsInfo!, + // for: player, + // on: controller.contentView, + // completion: { _ in + // }) + // } + // } } } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { random() } - } } diff --git a/WikiRaces/WikiRaces.xcodeproj/project.pbxproj b/WikiRaces/WikiRaces.xcodeproj/project.pbxproj index 8a4dcd6..a6c26aa 100644 --- a/WikiRaces/WikiRaces.xcodeproj/project.pbxproj +++ b/WikiRaces/WikiRaces.xcodeproj/project.pbxproj @@ -9,80 +9,22 @@ /* Begin PBXBuildFile section */ 1410DB3B1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */; }; 1410DB3D1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */; }; - 1414280321FC18F600C48788 /* GameKitConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */; }; - 1414280721FC394600C48788 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* ConnectViewController.swift */; }; - 1414280B21FC437000C48788 /* GameKitConnectViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */; }; - 141854DF2373666A008C988A /* MPCHostAutoInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141854DE2373666A008C988A /* MPCHostAutoInviteCell.swift */; }; - 141892F41F60EABC006748F0 /* MPCConnectViewController+Invite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */; }; + 1414280321FC18F600C48788 /* GKJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GKJoinViewController.swift */; }; + 1414280721FC394600C48788 /* GKConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* GKConnectViewController.swift */; }; + 1414280B21FC437000C48788 /* GKJoinViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */; }; + 141B3ED524A3C08000BD18C7 /* ListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */; }; + 141B3ED624A3C08000BD18C7 /* ListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */; }; + 141B3ED724A3C08000BD18C7 /* ListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */; }; + 141B3ED824A3C08000BD18C7 /* ResultsItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */; }; + 141B3ED924A3C08000BD18C7 /* ResultsItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */; }; + 141B3EDA24A3C08000BD18C7 /* ResultsItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */; }; + 141B3EDB24A3C08000BD18C7 /* ResultsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */; }; + 141B3EDC24A3C08000BD18C7 /* ResultsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */; }; + 141B3EDD24A3C08000BD18C7 /* ResultsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */; }; 141E4CE72200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; 141E4CE82200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; 141E4CE92200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; - 141E4CF122012B2F000A0A15 /* PlayerDatabaseMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */; }; - 1429327C242AE29000D64834 /* PointerInteractionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1429327B242AE29000D64834 /* PointerInteractionTableViewCell.swift */; }; - 1429327D242AE29000D64834 /* PointerInteractionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1429327B242AE29000D64834 /* PointerInteractionTableViewCell.swift */; }; - 1429327E242AE29000D64834 /* PointerInteractionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1429327B242AE29000D64834 /* PointerInteractionTableViewCell.swift */; }; - 14293292242B0E5C00D64834 /* Protobuf.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429327F242B0E5B00D64834 /* Protobuf.xcframework */; }; - 14293293242B0E5C00D64834 /* Protobuf.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429327F242B0E5B00D64834 /* Protobuf.xcframework */; }; - 14293294242B0E5C00D64834 /* Protobuf.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429327F242B0E5B00D64834 /* Protobuf.xcframework */; }; - 14293295242B0E5C00D64834 /* FirebaseCoreDiagnostics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293280242B0E5B00D64834 /* FirebaseCoreDiagnostics.xcframework */; }; - 14293296242B0E5C00D64834 /* FirebaseCoreDiagnostics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293280242B0E5B00D64834 /* FirebaseCoreDiagnostics.xcframework */; }; - 14293297242B0E5C00D64834 /* FirebaseCoreDiagnostics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293280242B0E5B00D64834 /* FirebaseCoreDiagnostics.xcframework */; }; - 14293298242B0E5C00D64834 /* FirebaseABTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293281242B0E5B00D64834 /* FirebaseABTesting.xcframework */; }; - 14293299242B0E5C00D64834 /* FirebaseABTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293281242B0E5B00D64834 /* FirebaseABTesting.xcframework */; }; - 1429329A242B0E5C00D64834 /* FirebaseABTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293281242B0E5B00D64834 /* FirebaseABTesting.xcframework */; }; - 1429329B242B0E5C00D64834 /* FirebaseRemoteConfig.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293282242B0E5B00D64834 /* FirebaseRemoteConfig.xcframework */; }; - 1429329C242B0E5C00D64834 /* FirebaseRemoteConfig.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293282242B0E5B00D64834 /* FirebaseRemoteConfig.xcframework */; }; - 1429329D242B0E5C00D64834 /* FirebaseRemoteConfig.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293282242B0E5B00D64834 /* FirebaseRemoteConfig.xcframework */; }; - 1429329E242B0E5C00D64834 /* FirebaseInstanceID.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293283242B0E5B00D64834 /* FirebaseInstanceID.xcframework */; }; - 1429329F242B0E5C00D64834 /* FirebaseInstanceID.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293283242B0E5B00D64834 /* FirebaseInstanceID.xcframework */; }; - 142932A0242B0E5C00D64834 /* FirebaseInstanceID.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293283242B0E5B00D64834 /* FirebaseInstanceID.xcframework */; }; - 142932A1242B0E5C00D64834 /* FIRAnalyticsConnector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293284242B0E5B00D64834 /* FIRAnalyticsConnector.framework */; platformFilter = ios; }; - 142932A2242B0E5C00D64834 /* FIRAnalyticsConnector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293284242B0E5B00D64834 /* FIRAnalyticsConnector.framework */; }; - 142932A3242B0E5C00D64834 /* FIRAnalyticsConnector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293284242B0E5B00D64834 /* FIRAnalyticsConnector.framework */; }; - 142932A4242B0E5C00D64834 /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293285242B0E5B00D64834 /* GoogleAppMeasurement.framework */; platformFilter = ios; }; - 142932A5242B0E5C00D64834 /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293285242B0E5B00D64834 /* GoogleAppMeasurement.framework */; }; - 142932A6242B0E5C00D64834 /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293285242B0E5B00D64834 /* GoogleAppMeasurement.framework */; }; - 142932A7242B0E5C00D64834 /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293286242B0E5B00D64834 /* FirebaseAnalytics.framework */; platformFilter = ios; }; - 142932A8242B0E5C00D64834 /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293286242B0E5B00D64834 /* FirebaseAnalytics.framework */; }; - 142932A9242B0E5C00D64834 /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293286242B0E5B00D64834 /* FirebaseAnalytics.framework */; }; - 142932AA242B0E5C00D64834 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293287242B0E5B00D64834 /* GoogleDataTransportCCTSupport.xcframework */; }; - 142932AB242B0E5C00D64834 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293287242B0E5B00D64834 /* GoogleDataTransportCCTSupport.xcframework */; }; - 142932AC242B0E5C00D64834 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293287242B0E5B00D64834 /* GoogleDataTransportCCTSupport.xcframework */; }; - 142932AD242B0E5C00D64834 /* FirebaseInstallations.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293288242B0E5B00D64834 /* FirebaseInstallations.xcframework */; }; - 142932AE242B0E5C00D64834 /* FirebaseInstallations.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293288242B0E5B00D64834 /* FirebaseInstallations.xcframework */; }; - 142932AF242B0E5C00D64834 /* FirebaseInstallations.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293288242B0E5B00D64834 /* FirebaseInstallations.xcframework */; }; - 142932B0242B0E5C00D64834 /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293289242B0E5C00D64834 /* FirebasePerformance.framework */; platformFilter = ios; }; - 142932B1242B0E5C00D64834 /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293289242B0E5C00D64834 /* FirebasePerformance.framework */; }; - 142932B2242B0E5C00D64834 /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293289242B0E5C00D64834 /* FirebasePerformance.framework */; }; - 142932B3242B0E5C00D64834 /* GTMSessionFetcher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328A242B0E5C00D64834 /* GTMSessionFetcher.xcframework */; }; - 142932B4242B0E5C00D64834 /* GTMSessionFetcher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328A242B0E5C00D64834 /* GTMSessionFetcher.xcframework */; }; - 142932B5242B0E5C00D64834 /* GTMSessionFetcher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328A242B0E5C00D64834 /* GTMSessionFetcher.xcframework */; }; - 142932B6242B0E5C00D64834 /* FirebaseCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328B242B0E5C00D64834 /* FirebaseCore.xcframework */; }; - 142932B7242B0E5C00D64834 /* FirebaseCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328B242B0E5C00D64834 /* FirebaseCore.xcframework */; }; - 142932B8242B0E5C00D64834 /* FirebaseCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328B242B0E5C00D64834 /* FirebaseCore.xcframework */; }; - 142932BC242B0E5C00D64834 /* nanopb.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328D242B0E5C00D64834 /* nanopb.xcframework */; }; - 142932BD242B0E5C00D64834 /* nanopb.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328D242B0E5C00D64834 /* nanopb.xcframework */; }; - 142932BE242B0E5C00D64834 /* nanopb.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328D242B0E5C00D64834 /* nanopb.xcframework */; }; - 142932BF242B0E5C00D64834 /* GoogleDataTransport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328E242B0E5C00D64834 /* GoogleDataTransport.xcframework */; }; - 142932C0242B0E5C00D64834 /* GoogleDataTransport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328E242B0E5C00D64834 /* GoogleDataTransport.xcframework */; }; - 142932C1242B0E5C00D64834 /* GoogleDataTransport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328E242B0E5C00D64834 /* GoogleDataTransport.xcframework */; }; - 142932C2242B0E5C00D64834 /* GoogleUtilities.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328F242B0E5C00D64834 /* GoogleUtilities.xcframework */; }; - 142932C3242B0E5C00D64834 /* GoogleUtilities.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328F242B0E5C00D64834 /* GoogleUtilities.xcframework */; }; - 142932C4242B0E5C00D64834 /* GoogleUtilities.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1429328F242B0E5C00D64834 /* GoogleUtilities.xcframework */; }; - 142932C5242B0E5C00D64834 /* GoogleToolboxForMac.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293290242B0E5C00D64834 /* GoogleToolboxForMac.xcframework */; }; - 142932C6242B0E5C00D64834 /* GoogleToolboxForMac.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293290242B0E5C00D64834 /* GoogleToolboxForMac.xcframework */; }; - 142932C7242B0E5C00D64834 /* GoogleToolboxForMac.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293290242B0E5C00D64834 /* GoogleToolboxForMac.xcframework */; }; - 142932C8242B0E5C00D64834 /* PromisesObjC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293291242B0E5C00D64834 /* PromisesObjC.xcframework */; }; - 142932C9242B0E5C00D64834 /* PromisesObjC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293291242B0E5C00D64834 /* PromisesObjC.xcframework */; }; - 142932CA242B0E5C00D64834 /* PromisesObjC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14293291242B0E5C00D64834 /* PromisesObjC.xcframework */; }; - 142932CC242B0F2F00D64834 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142932CB242B0F2F00D64834 /* Crashlytics.framework */; platformFilter = ios; }; - 142932CD242B0F2F00D64834 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142932CB242B0F2F00D64834 /* Crashlytics.framework */; }; - 142932CE242B0F2F00D64834 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142932CB242B0F2F00D64834 /* Crashlytics.framework */; }; - 142932D0242B0F6800D64834 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142932CF242B0F6800D64834 /* Fabric.framework */; platformFilter = ios; }; - 142932D1242B0F6800D64834 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142932CF242B0F6800D64834 /* Fabric.framework */; }; - 142932D2242B0F6800D64834 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142932CF242B0F6800D64834 /* Fabric.framework */; }; - 142A09BE210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */; }; - 142A09C0210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */; }; + 141E4CF122012B2F000A0A15 /* PlayerCloudKitStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */; }; 142ABDFA24600559008E7F77 /* PlusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDF924600559008E7F77 /* PlusViewController.swift */; }; 142ABDFB2460055B008E7F77 /* PlusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDF924600559008E7F77 /* PlusViewController.swift */; }; 142ABDFC2460055C008E7F77 /* PlusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDF924600559008E7F77 /* PlusViewController.swift */; }; @@ -98,36 +40,46 @@ 142ABE0E24600ABE008E7F77 /* MagicSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */; }; 142ABE0F24600ABE008E7F77 /* MagicSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */; }; 142ABE1024600ABE008E7F77 /* MagicSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */; }; - 142ABE1224600AF0008E7F77 /* OSLog+Magic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE1124600AF0008E7F77 /* OSLog+Magic.swift */; }; - 142ABE1324600AF0008E7F77 /* OSLog+Magic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE1124600AF0008E7F77 /* OSLog+Magic.swift */; }; - 142ABE1424600AF0008E7F77 /* OSLog+Magic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE1124600AF0008E7F77 /* OSLog+Magic.swift */; }; 142ABE16246013BC008E7F77 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 142ABE15246013BC008E7F77 /* StoreKit.framework */; }; 142F714F210C33FC00C66558 /* MenuViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */; }; - 142F7153210C34A300C66558 /* MPCHostViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7152210C34A300C66558 /* MPCHostViewController+KB.swift */; }; - 142F7155210C35BC00C66558 /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; 142F7157210C375F00C66558 /* GameViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7156210C375F00C66558 /* GameViewController+KB.swift */; }; 1437C51F22285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; 1437C52022285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; - 1437C52122285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; 143948BD2144CC0F00992850 /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; 143948C12144CC8C00992850 /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; 143A8BBC1F58746800580AA2 /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; 143BB7151F60AEC900D00541 /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; 143BB7181F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; 143BB7191F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; - 143BB7351F60DF4A00D00541 /* MPCConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */; }; - 143FC31C2401BB0900AB313A /* MPCHostAutoInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141854DE2373666A008C988A /* MPCHostAutoInviteCell.swift */; }; + 143CE07E24A355DC000F6833 /* GKJoinViewController+PublicRace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */; }; + 143CE07F24A355DC000F6833 /* GKJoinViewController+PublicRace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */; }; + 143CE08024A355DC000F6833 /* GKJoinViewController+PublicRace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */; }; + 1446C1D924A334C2000A0ED3 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1D824A334C2000A0ED3 /* Defaults.swift */; }; + 1446C1DA24A334C2000A0ED3 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1D824A334C2000A0ED3 /* Defaults.swift */; }; + 1446C1DB24A334C2000A0ED3 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1D824A334C2000A0ED3 /* Defaults.swift */; }; + 1446C1DC24A334C2000A0ED3 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1D824A334C2000A0ED3 /* Defaults.swift */; }; + 1446C1DD24A334C2000A0ED3 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1D824A334C2000A0ED3 /* Defaults.swift */; }; + 1446C1E024A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */; }; + 1446C1E124A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */; }; + 1446C1E224A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */; }; + 1446C1E324A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */; }; + 1446C1E424A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */; }; 14495D8821FF9A0500CAA129 /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; 14495D8921FF9A0500CAA129 /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; 14495D8A21FF9A0500CAA129 /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; - 144A1029202FC79B003DB51A /* CenteredTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */; }; + 144A1029202FC79B003DB51A /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */; }; 144A102A202FC79B003DB51A /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; - 144A102B202FC7A1003DB51A /* CenteredTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */; }; + 144A102B202FC7A1003DB51A /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */; }; 144A102C202FC7A1003DB51A /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; - 144A102D202FC7A2003DB51A /* CenteredTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */; }; + 144A102D202FC7A2003DB51A /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */; }; 144A102E202FC7A2003DB51A /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; 144A5AEC1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */; }; 144A5AED1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */; }; + 144EC2A524ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */; }; + 144EC2A624ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */; }; + 144EC2A724ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */; }; + 144EC2A824ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */; }; + 144EC2A924ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */; }; 14584BC2220B6BE700D63428 /* WKRKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; }; 14584BC3220B6BE700D63428 /* WKRKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14584BC7220B6BEE00D63428 /* WKRUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; }; @@ -136,15 +88,148 @@ 145925DF210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; 145925E0210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; 1473E2A0210DAD7C00726377 /* HelpViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */; }; - 1473F38D24650B6800F939B0 /* MPCHostAutoInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141854DE2373666A008C988A /* MPCHostAutoInviteCell.swift */; }; 147593D11F609911005DFC90 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147593D01F609911005DFC90 /* SnapshotHelper.swift */; }; + 1475C12824A6551F00882F1F /* CustomRaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6DA762462900300EC9817 /* CustomRaceController.swift */; }; + 1475C12924A6551F00882F1F /* CustomRaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CC245FAE5500F3653F /* CustomRaceViewController.swift */; }; + 1475C12A24A6551F00882F1F /* CustomRaceNotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CD245FAE5600F3653F /* CustomRaceNotificationsController.swift */; }; + 1475C12B24A6551F00882F1F /* CustomRaceNumericalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CE245FAE5600F3653F /* CustomRaceNumericalViewController.swift */; }; + 1475C12C24A6551F00882F1F /* CustomRaceOtherController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CA245FAE5500F3653F /* CustomRaceOtherController.swift */; }; + 1475C12D24A6551F00882F1F /* CustomRacePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CB245FAE5500F3653F /* CustomRacePageViewController.swift */; }; + 1475C13024A6553F00882F1F /* ResultsContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */; }; + 1475C13124A6553F00882F1F /* ResultsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */; }; + 1475C13224A6553F00882F1F /* ResultsItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */; }; + 1475C13324A6553F00882F1F /* ListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */; }; + 1475C13424A6554000882F1F /* ResultsContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */; }; + 1475C13524A6554000882F1F /* ResultsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */; }; + 1475C13624A6554000882F1F /* ResultsItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */; }; + 1475C13724A6554000882F1F /* ListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */; }; + 1475C13824A6555A00882F1F /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */; }; + 1475C13924A6555B00882F1F /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */; }; + 1475C13A24A6556200882F1F /* GKConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* GKConnectViewController.swift */; }; + 1475C13B24A6556300882F1F /* GKConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* GKConnectViewController.swift */; }; + 1475C13C24A6556900882F1F /* PlayerFirebaseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */; }; + 1475C13D24A6556A00882F1F /* PlayerFirebaseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */; }; + 1475C13E24A6557100882F1F /* GKMatchRequest+WKR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */; }; + 1475C13F24A6557200882F1F /* GKMatchRequest+WKR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */; }; + 1475C14024A6557800882F1F /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */; }; + 1475C14124A6557800882F1F /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */; }; + 1475C14424A6559500882F1F /* PlusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDF924600559008E7F77 /* PlusViewController.swift */; }; + 1475C14524A6559500882F1F /* PlusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDF924600559008E7F77 /* PlusViewController.swift */; }; + 1475C14624A655B000882F1F /* GameViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7156210C375F00C66558 /* GameViewController+KB.swift */; }; + 1475C14724A655B100882F1F /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; + 1475C14824A655B100882F1F /* GameViewController+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */; }; + 1475C14924A655B100882F1F /* GameViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7156210C375F00C66558 /* GameViewController+KB.swift */; }; + 1475C14A24A655B100882F1F /* GameViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7156210C375F00C66558 /* GameViewController+KB.swift */; }; + 1475C14B24A655B200882F1F /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; + 1475C14C24A655B200882F1F /* GameViewController+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */; }; + 1475C14D24A655B200882F1F /* GameViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7156210C375F00C66558 /* GameViewController+KB.swift */; }; + 1475C14E24A655C100882F1F /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; + 1475C14F24A655C100882F1F /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; + 1475C15024A655C800882F1F /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; + 1475C15124A655C900882F1F /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; + 1475C15224A655CF00882F1F /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; + 1475C15324A655D000882F1F /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; + 1475C15424A655D000882F1F /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; + 1475C15524A655D700882F1F /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; + 1475C15624A655D700882F1F /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; + 1475C15724A655D700882F1F /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; + 1475C15824A655D700882F1F /* MenuView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */; }; + 1475C15924A655D700882F1F /* MedalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B67C223072AB00D6800B /* MedalView.swift */; }; + 1475C15A24A655D700882F1F /* MedalScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B6762230724A00D6800B /* MedalScene.swift */; }; + 1475C15B24A655D700882F1F /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; + 1475C15C24A655D800882F1F /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; + 1475C15D24A655D800882F1F /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; + 1475C15E24A655D800882F1F /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; + 1475C15F24A655D800882F1F /* MenuView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */; }; + 1475C16024A655D800882F1F /* MedalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B67C223072AB00D6800B /* MedalView.swift */; }; + 1475C16124A655D800882F1F /* MedalScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B6762230724A00D6800B /* MedalScene.swift */; }; + 1475C16224A655D800882F1F /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; + 1475C16324A655DB00882F1F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; + 1475C16424A655DB00882F1F /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; + 1475C16524A655DB00882F1F /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; + 1475C16624A655DB00882F1F /* MenuViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */; }; + 1475C16824A655DC00882F1F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; + 1475C16924A655DC00882F1F /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; + 1475C16A24A655DC00882F1F /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; + 1475C16B24A655DC00882F1F /* MenuViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */; }; + 1475C16C24A655DC00882F1F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; + 1475C16D24A655DC00882F1F /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; + 1475C16E24A655DC00882F1F /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; + 1475C16F24A655DC00882F1F /* MenuViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */; }; + 1475C17024A655DC00882F1F /* MenuViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */; }; + 1475C17124A655E800882F1F /* GKJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GKJoinViewController.swift */; }; + 1475C17224A655E800882F1F /* GKJoinViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */; }; + 1475C17324A655E800882F1F /* GKJoinViewController+PublicRace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */; }; + 1475C17424A655E900882F1F /* GKJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GKJoinViewController.swift */; }; + 1475C17524A655E900882F1F /* GKJoinViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */; }; + 1475C17624A655E900882F1F /* GKJoinViewController+PublicRace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */; }; + 1475C17724A655EC00882F1F /* CustomRaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6DA762462900300EC9817 /* CustomRaceController.swift */; }; + 1475C17824A655EC00882F1F /* CustomRaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CC245FAE5500F3653F /* CustomRaceViewController.swift */; }; + 1475C17924A655EC00882F1F /* CustomRaceNotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CD245FAE5600F3653F /* CustomRaceNotificationsController.swift */; }; + 1475C17A24A655EC00882F1F /* CustomRaceNumericalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CE245FAE5600F3653F /* CustomRaceNumericalViewController.swift */; }; + 1475C17B24A655EC00882F1F /* CustomRaceOtherController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CA245FAE5500F3653F /* CustomRaceOtherController.swift */; }; + 1475C17C24A655EC00882F1F /* CustomRacePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CB245FAE5500F3653F /* CustomRacePageViewController.swift */; }; + 1475C17D24A655F000882F1F /* StatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */; }; + 1475C17E24A655F000882F1F /* StatsPlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */; }; + 1475C17F24A655F100882F1F /* StatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */; }; + 1475C18024A655F100882F1F /* StatsPlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */; }; + 1475C18124A655F100882F1F /* StatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */; }; + 1475C18224A655F100882F1F /* StatsPlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */; }; + 1475C18324A655F500882F1F /* PlusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDFD24600576008E7F77 /* PlusView.swift */; }; + 1475C18424A655F500882F1F /* PlusStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0524600A74008E7F77 /* PlusStore.swift */; }; + 1475C18524A655F500882F1F /* ActivityButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE012460057C008E7F77 /* ActivityButton.swift */; }; + 1475C18624A655F500882F1F /* MagicSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */; }; + 1475C18824A655F600882F1F /* PlusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABDFD24600576008E7F77 /* PlusView.swift */; }; + 1475C18924A655F600882F1F /* PlusStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0524600A74008E7F77 /* PlusStore.swift */; }; + 1475C18A24A655F600882F1F /* ActivityButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE012460057C008E7F77 /* ActivityButton.swift */; }; + 1475C18B24A655F600882F1F /* MagicSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */; }; + 1475C18D24A655FB00882F1F /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; + 1475C18E24A655FB00882F1F /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; + 1475C18F24A655FC00882F1F /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; + 1475C19024A655FC00882F1F /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; + 1475C19124A6560000882F1F /* HelpViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */; }; + 1475C19224A6560000882F1F /* HelpViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */; }; + 1475C19324A6560000882F1F /* HelpViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */; }; + 1475C19424A6560100882F1F /* HelpViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */; }; + 1475C19524A6560700882F1F /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; + 1475C19624A6560700882F1F /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; + 1475C19824A6560700882F1F /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; + 1475C19924A6560700882F1F /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; + 1475C19A24A6560700882F1F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; + 1475C19B24A6560800882F1F /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; + 1475C19C24A6560800882F1F /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; + 1475C19E24A6560800882F1F /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; + 1475C19F24A6560800882F1F /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; + 1475C1A024A6560800882F1F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; + 1475C1A124A6560D00882F1F /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB581F368A050086B77F /* HistoryViewController.swift */; }; + 1475C1A224A6560D00882F1F /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; + 1475C1A324A6560D00882F1F /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */; }; + 1475C1A424A6560D00882F1F /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; + 1475C1A524A6560D00882F1F /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB581F368A050086B77F /* HistoryViewController.swift */; }; + 1475C1A624A6560D00882F1F /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; + 1475C1A724A6560D00882F1F /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */; }; + 1475C1A824A6560D00882F1F /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; + 1475C1A924A6560E00882F1F /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB581F368A050086B77F /* HistoryViewController.swift */; }; + 1475C1AA24A6560E00882F1F /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; + 1475C1AB24A6560E00882F1F /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */; }; + 1475C1AC24A6560E00882F1F /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; + 1475C1AD24A6561200882F1F /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; + 1475C1AE24A6561200882F1F /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; + 1475C1AF24A6561300882F1F /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; + 1475C1B024A6561400882F1F /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; + 1475C1B124A6561400882F1F /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; + 1475C1B224A6561400882F1F /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; + 1475C1B324A6561400882F1F /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; + 1475C1B424A6561A00882F1F /* PlayerUserDefaultsStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */; }; + 1475C1B524A6561A00882F1F /* PlayerCloudKitStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */; }; + 1475C1B624A6561B00882F1F /* PlayerUserDefaultsStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */; }; + 1475C1B724A6561B00882F1F /* PlayerCloudKitStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */; }; + 1475C1B824A6571200882F1F /* GKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148CCBCC24A3410700538F18 /* GKHelper.swift */; }; + 1475C1B924A6571300882F1F /* GKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148CCBCC24A3410700538F18 /* GKHelper.swift */; }; 1478B1B122095360009F2F3F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; 1478B1B222095360009F2F3F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; 1478B1B322095360009F2F3F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; 14794CA9236C884B00835DC9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 14794CA8236C884A00835DC9 /* GoogleService-Info.plist */; }; - 147EF6F92202436600583D73 /* MPCHostContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EF6F82202436600583D73 /* MPCHostContext.swift */; }; - 147EF6FA2202436600583D73 /* MPCHostContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EF6F82202436600583D73 /* MPCHostContext.swift */; }; - 147EF7122202D76000583D73 /* MPCHostContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EF6F82202436600583D73 /* MPCHostContext.swift */; }; 1480F6C321FB77D300081F58 /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; 1480F6C621FB77DE00081F58 /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; 1485B6772230724A00D6800B /* MedalScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B6762230724A00D6800B /* MedalScene.swift */; }; @@ -155,14 +240,36 @@ 1485B67F223072AB00D6800B /* MedalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B67C223072AB00D6800B /* MedalView.swift */; }; 14891AA6214F6BDB001BDEB8 /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; 14891AA9214F6BDE001BDEB8 /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; + 148CCBCD24A3410700538F18 /* GKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148CCBCC24A3410700538F18 /* GKHelper.swift */; }; + 148CCBCE24A3410700538F18 /* GKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148CCBCC24A3410700538F18 /* GKHelper.swift */; }; + 148CCBCF24A3410700538F18 /* GKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148CCBCC24A3410700538F18 /* GKHelper.swift */; }; + 148E5C4224A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */; }; + 148E5C4324A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */; }; + 148E5C4424A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */; }; + 148E5C4524A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */; }; + 148E5C4624A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */; }; + 148E5C4824A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */; }; + 148E5C4924A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */; }; + 148E5C4A24A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */; }; + 148E5C4B24A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */; }; + 148E5C4C24A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */; }; + 148E5C4E24A2943100DD43E4 /* Nearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4D24A2943100DD43E4 /* Nearby.swift */; }; + 148E5C4F24A2943100DD43E4 /* Nearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4D24A2943100DD43E4 /* Nearby.swift */; }; + 148E5C5024A2943100DD43E4 /* Nearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4D24A2943100DD43E4 /* Nearby.swift */; }; + 148E5C5124A2943100DD43E4 /* Nearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4D24A2943100DD43E4 /* Nearby.swift */; }; + 148E5C5224A2943100DD43E4 /* Nearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C4D24A2943100DD43E4 /* Nearby.swift */; }; + 148E5C5424A2967600DD43E4 /* GKHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */; }; + 148E5C5524A2967600DD43E4 /* GKHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */; }; + 148E5C5624A2967600DD43E4 /* GKHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */; }; + 148E5C5724A2967600DD43E4 /* GKHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */; }; + 148E5C5824A2967600DD43E4 /* GKHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */; }; + 148E5C6024A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */; }; + 148E5C6124A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */; }; + 148E5C6224A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */; }; + 148E5C6324A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */; }; + 148E5C6424A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */; }; 149357D9210E801A00F6453A /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; 149357DA210E801A00F6453A /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; - 149357DB210E801A00F6453A /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; - 149357DF210E934300F6453A /* MPCConnectViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357DE210E934300F6453A /* MPCConnectViewController+UI.swift */; }; - 149357E1210E934300F6453A /* MPCConnectViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357DE210E934300F6453A /* MPCConnectViewController+UI.swift */; }; - 149357E3210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */; }; - 149357E4210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */; }; - 149357E5210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */; }; 149FF84D1F362B83000A5D96 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149FF84C1F362B83000A5D96 /* AppDelegate.swift */; }; 149FF8541F362B83000A5D96 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8531F362B83000A5D96 /* Assets.xcassets */; }; 149FF8571F362B83000A5D96 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8551F362B83000A5D96 /* LaunchScreen.storyboard */; }; @@ -173,6 +280,11 @@ 149FF8911F362BF1000A5D96 /* WikiRacesScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149FF8901F362BF1000A5D96 /* WikiRacesScreenshots.swift */; }; 14A7C9B31F65A9EB00980E4D /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 143BB7321F60DE9300D00541 /* Settings.bundle */; }; 14A89F681F7ABB2400C85387 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E1B1A01F798A520082F4FA /* CloudKit.framework */; }; + 14B05D2524AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */; }; + 14B05D2624AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */; }; + 14B05D2724AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */; }; + 14B05D2824AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */; }; + 14B05D2924AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */; }; 14B2DD3B22212298009B8AB3 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; 14B2DD3C22212298009B8AB3 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; 14B2DD3D22212298009B8AB3 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; @@ -182,62 +294,43 @@ 14B2DD4522212B96009B8AB3 /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; 14B2DD4622212B96009B8AB3 /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; 14B2DD4722212B96009B8AB3 /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; - 14B2DD4C22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */; }; - 14B2DD4D22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */; }; - 14B2DD4E22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */; }; 14B4DB612224809F007D4B54 /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; 14B4DB622224809F007D4B54 /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; 14B4DB632224809F007D4B54 /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; - 14B4DB662224F1B9007D4B54 /* GameKitConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */; }; - 14B4DB672224F1BA007D4B54 /* GameKitConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */; }; - 14B4DB682224F1BC007D4B54 /* GameKitConnectViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */; }; - 14B4DB692224F1BD007D4B54 /* GameKitConnectViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */; }; - 14B4DB6B2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */; }; - 14B4DB6C2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */; }; - 14B4DB6D2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */; }; + 14B4DB662224F1B9007D4B54 /* GKJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GKJoinViewController.swift */; }; + 14B4DB672224F1BA007D4B54 /* GKJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GKJoinViewController.swift */; }; + 14B4DB682224F1BC007D4B54 /* GKJoinViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */; }; + 14B4DB692224F1BD007D4B54 /* GKJoinViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */; }; 14B55C991F3A49D20090E092 /* DebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B55C981F3A49D20090E092 /* DebugWindow.swift */; }; 14B8F80D222C456B006C7A06 /* GameKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B8F80C222C456B006C7A06 /* GameKit.framework */; }; - 14B8F811222C47FA006C7A06 /* GKMessageImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 14B8F810222C47FA006C7A06 /* GKMessageImage.png */; }; - 14BA538921FE3B1400A8CB01 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* ConnectViewController.swift */; }; + 14BA538921FE3B1400A8CB01 /* GKConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* GKConnectViewController.swift */; }; 14BA538D21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; 14BA538E21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; - 14BA538F21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; 14C25FE01F6F025A00CD7373 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C25FDF1F6F025A00CD7373 /* AppDelegate.swift */; }; 14C25FE21F6F025A00CD7373 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C25FE11F6F025A00CD7373 /* ViewController.swift */; }; 14C25FE51F6F025A00CD7373 /* Main-Catalog.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 14C25FE31F6F025A00CD7373 /* Main-Catalog.storyboard */; }; 14C25FE71F6F025A00CD7373 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14C25FE61F6F025A00CD7373 /* Assets.xcassets */; }; 14C25FEA1F6F025A00CD7373 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 14C25FE81F6F025A00CD7373 /* LaunchScreen.storyboard */; }; 14C25FF11F6F028100CD7373 /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; - 14C25FF21F6F028100CD7373 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; - 14C25FF51F6F028100CD7373 /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; 14C25FFB1F6F02A500CD7373 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */; }; 14C25FFC1F6F02A500CD7373 /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; 14C25FFE1F6F02A500CD7373 /* GameViewController+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */; }; 14C25FFF1F6F02A500CD7373 /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; - 14C260001F6F02A500CD7373 /* ResultsViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */; }; - 14C260011F6F02A500CD7373 /* ResultsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */; }; - 14C260021F6F02A500CD7373 /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */; }; - 14C260031F6F02A500CD7373 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB581F368A050086B77F /* HistoryViewController.swift */; }; 14C260041F6F02A500CD7373 /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; - 14C260051F6F02A500CD7373 /* VotingViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */; }; - 14C260061F6F02A500CD7373 /* VotingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */; }; 14C260071F6F02A500CD7373 /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; 14C2600A1F6F02A500CD7373 /* WKRAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */; }; 14C2600D1F6F04A200CD7373 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */; }; + 14C2AEFF24A3CE9000D2378C /* ResultsContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */; }; + 14C2AF0024A3CE9000D2378C /* ResultsContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */; }; + 14C2AF0124A3CE9000D2378C /* ResultsContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */; }; 14C4AB2D1F3689900086B77F /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; 14C4AB2F1F3689900086B77F /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; - 14C4AB311F36899B0086B77F /* ResultsViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */; }; - 14C4AB331F36899B0086B77F /* ResultsViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */; }; - 14C4AB351F3689A40086B77F /* ResultsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */; }; - 14C4AB371F3689A40086B77F /* ResultsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */; }; 14C4AB391F3689A90086B77F /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; 14C4AB3B1F3689A90086B77F /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; 14C4AB3D1F3689AE0086B77F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; 14C4AB3F1F3689AE0086B77F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; 14C4AB491F3689C60086B77F /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; 14C4AB4B1F3689C60086B77F /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; - 14C4AB511F3689E20086B77F /* VotingViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */; }; - 14C4AB531F3689E20086B77F /* VotingViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */; }; 14C4AB551F3689FE0086B77F /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */; }; 14C4AB571F3689FE0086B77F /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */; }; 14C4AB591F368A050086B77F /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB581F368A050086B77F /* HistoryViewController.swift */; }; @@ -248,55 +341,75 @@ 14C4AB631F368A170086B77F /* GameViewController+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */; }; 14C4AB651F368A210086B77F /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; 14C4AB671F368A210086B77F /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; - 14C4AB851F368AED0086B77F /* VotingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */; }; - 14C4AB871F368AED0086B77F /* VotingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */; }; - 14C6B1F41FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */; }; - 14C6B1F51FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */; }; - 14C6B1F61FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */; }; - 14C6B1F71FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */; }; 14C6DA772462900300EC9817 /* CustomRaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6DA762462900300EC9817 /* CustomRaceController.swift */; }; 14C6DA782462900300EC9817 /* CustomRaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6DA762462900300EC9817 /* CustomRaceController.swift */; }; 14C6DA792462900300EC9817 /* CustomRaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6DA762462900300EC9817 /* CustomRaceController.swift */; }; 14D18CDC2460CF00002E4F5D /* StatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */; }; - 14D18CDD2460CF00002E4F5D /* StatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */; }; 14D18CDE2460CF00002E4F5D /* StatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */; }; 14D18CE02460DF9C002E4F5D /* StatsPlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */; }; - 14D18CE12460DF9C002E4F5D /* StatsPlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */; }; 14D18CE22460DF9C002E4F5D /* StatsPlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */; }; - 14D8AD471F81828D00914E5A /* PlayerAnonymousMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */; }; + 14D8AD471F81828D00914E5A /* PlayerFirebaseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */; }; 14D8AD481F81835700914E5A /* DebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B55C981F3A49D20090E092 /* DebugWindow.swift */; }; + 14DB8C9924AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */; }; + 14DB8C9A24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */; }; + 14DB8C9B24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */; }; + 14DB8C9C24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */; }; + 14DB8C9D24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */; }; 14DC66D31F9072180026C6ED /* WKRKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; }; 14DC66D41F9072180026C6ED /* WKRKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14DC66D71F9072200026C6ED /* WKRUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; }; 14DC66D81F9072200026C6ED /* WKRUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 14DD97252202293900AAB389 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* ConnectViewController.swift */; }; - 14DD97282202294D00AAB389 /* PlayerDatabaseMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */; }; - 14DD97292202295100AAB389 /* PlayerDatabaseMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */; }; + 14DD97252202293900AAB389 /* GKConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* GKConnectViewController.swift */; }; + 14DD97282202294D00AAB389 /* PlayerCloudKitStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */; }; + 14DD97292202295100AAB389 /* PlayerCloudKitStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */; }; 14DF31A8211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; 14DF31A9211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; - 14DF31AA211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; - 14DF31B321169232005BA432 /* MPCConnectViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */; }; - 14DF31B421169236005BA432 /* MPCConnectViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357DE210E934300F6453A /* MPCConnectViewController+UI.swift */; }; - 14DF31B521169239005BA432 /* MPCHostViewController+Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */; }; - 14DF31B62116923C005BA432 /* MPCHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */; }; - 14DF31B721169290005BA432 /* MPCConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */; }; - 14DF31B821169291005BA432 /* MPCConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */; }; - 14DF31B921169360005BA432 /* MPCHostSearchingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */; }; - 14DF31BA21169371005BA432 /* MPCConnectViewController+Invite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */; }; - 14DF31BB21169372005BA432 /* MPCConnectViewController+Invite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */; }; - 14DFBD1E210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */; }; - 14DFBD20210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */; }; - 14E0F19F22303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */; }; - 14E0F1A022303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */; }; - 14E0F1A122303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */; }; + 14E0F19F22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */; }; + 14E0F1A022303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */; }; + 14E0F1A122303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */; }; 14E0F1AB22303FD100BFF1E9 /* WikiRacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F1AA22303FD100BFF1E9 /* WikiRacesTests.swift */; }; - 14E1B19A1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */; }; - 14E1B19B1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */; }; + 14E1B19A1F7981C70082F4FA /* PlayerFirebaseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */; }; + 14E1B19B1F7981C70082F4FA /* PlayerFirebaseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */; }; 14E1B1A11F798A520082F4FA /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E1B1A01F798A520082F4FA /* CloudKit.framework */; }; 14E6D55D1F86A1CA005EB3B9 /* WKRKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; }; 14E6D55E1F86A1CA005EB3B9 /* WKRKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14E6D5611F86A1CF005EB3B9 /* WKRUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; }; 14E6D5621F86A1CF005EB3B9 /* WKRUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 14EBF4FD24A49BD10040A7C0 /* VotingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */; }; + 14EBF4FE24A49BD10040A7C0 /* VotingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */; }; + 14EBF4FF24A49BD10040A7C0 /* VotingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */; }; + 14EBF50024A49BD10040A7C0 /* VotingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */; }; + 14EBF50124A49BD10040A7C0 /* VotingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */; }; + 14EBF50524A49BE20040A7C0 /* VotingItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */; }; + 14EBF50624A49BE20040A7C0 /* VotingItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */; }; + 14EBF50724A49BE20040A7C0 /* VotingItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */; }; + 14EBF50824A49BE20040A7C0 /* VotingItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */; }; + 14EBF50924A49BE20040A7C0 /* VotingItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */; }; + 14EBF50B24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */; }; + 14EBF50C24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */; }; + 14EBF50D24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */; }; + 14EBF50E24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */; }; + 14EBF50F24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */; }; + 14EBF58724A4D8260040A7C0 /* HostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF58624A4D8260040A7C0 /* HostContentView.swift */; }; + 14EBF58824A4D8260040A7C0 /* HostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF58624A4D8260040A7C0 /* HostContentView.swift */; }; + 14EBF58924A4D8260040A7C0 /* HostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF58624A4D8260040A7C0 /* HostContentView.swift */; }; + 14EBF58A24A4D8260040A7C0 /* HostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF58624A4D8260040A7C0 /* HostContentView.swift */; }; + 14EBF58B24A4D8260040A7C0 /* HostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF58624A4D8260040A7C0 /* HostContentView.swift */; }; + 14EBF59924A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */; }; + 14EBF59A24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */; }; + 14EBF59B24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */; }; + 14EBF59C24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */; }; + 14EBF59D24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */; }; + 14EBF59F24A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */; }; + 14EBF5A024A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */; }; + 14EBF5A124A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */; }; + 14EBF5A224A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */; }; + 14EBF5A324A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */; }; + 14EBF5AC24A4F4780040A7C0 /* HostSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */; }; + 14EBF5AD24A4F4780040A7C0 /* HostSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */; }; + 14EBF5AE24A4F4780040A7C0 /* HostSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */; }; + 14EBF5AF24A4F4780040A7C0 /* HostSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */; }; + 14EBF5B024A4F4780040A7C0 /* HostSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */; }; 14EF51D2245FAE5600F3653F /* CustomRaceOtherController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CA245FAE5500F3653F /* CustomRaceOtherController.swift */; }; 14EF51D3245FAE5600F3653F /* CustomRaceOtherController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CA245FAE5500F3653F /* CustomRaceOtherController.swift */; }; 14EF51D4245FAE5600F3653F /* CustomRaceOtherController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CA245FAE5500F3653F /* CustomRaceOtherController.swift */; }; @@ -312,6 +425,81 @@ 14EF51DE245FAE5600F3653F /* CustomRaceNumericalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CE245FAE5600F3653F /* CustomRaceNumericalViewController.swift */; }; 14EF51DF245FAE5600F3653F /* CustomRaceNumericalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CE245FAE5600F3653F /* CustomRaceNumericalViewController.swift */; }; 14EF51E0245FAE5600F3653F /* CustomRaceNumericalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EF51CE245FAE5600F3653F /* CustomRaceNumericalViewController.swift */; }; + 14F0348124A8397A006A908E /* RaceChecksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F0348024A8397A006A908E /* RaceChecksViewController.swift */; }; + 14F0348224A8397A006A908E /* RaceChecksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F0348024A8397A006A908E /* RaceChecksViewController.swift */; }; + 14F0348324A8397A006A908E /* RaceChecksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F0348024A8397A006A908E /* RaceChecksViewController.swift */; }; + 14F0348424A8397A006A908E /* RaceChecksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F0348024A8397A006A908E /* RaceChecksViewController.swift */; }; + 14F0348524A8397A006A908E /* RaceChecksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F0348024A8397A006A908E /* RaceChecksViewController.swift */; }; + 14F034A724A847FF006A908E /* LoadingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */; }; + 14F034A824A847FF006A908E /* LoadingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */; }; + 14F034A924A847FF006A908E /* LoadingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */; }; + 14F034AA24A847FF006A908E /* LoadingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */; }; + 14F034AB24A847FF006A908E /* LoadingContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */; }; + 14F034AD24A8480D006A908E /* LoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034AC24A8480D006A908E /* LoadingContentView.swift */; }; + 14F034AE24A8480D006A908E /* LoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034AC24A8480D006A908E /* LoadingContentView.swift */; }; + 14F034AF24A8480D006A908E /* LoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034AC24A8480D006A908E /* LoadingContentView.swift */; }; + 14F034B024A8480D006A908E /* LoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034AC24A8480D006A908E /* LoadingContentView.swift */; }; + 14F034B124A8480D006A908E /* LoadingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F034AC24A8480D006A908E /* LoadingContentView.swift */; }; + 14F23FF324A78DFA00820638 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDB24A78DF900820638 /* GoogleDataTransportCCTSupport.xcframework */; platformFilter = ios; }; + 14F23FF424A78DFA00820638 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDB24A78DF900820638 /* GoogleDataTransportCCTSupport.xcframework */; }; + 14F23FF524A78DFA00820638 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDB24A78DF900820638 /* GoogleDataTransportCCTSupport.xcframework */; }; + 14F23FF624A78DFA00820638 /* FirebaseABTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDD24A78DF900820638 /* FirebaseABTesting.xcframework */; platformFilter = ios; }; + 14F23FF724A78DFA00820638 /* FirebaseABTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDD24A78DF900820638 /* FirebaseABTesting.xcframework */; }; + 14F23FF824A78DFA00820638 /* FirebaseABTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDD24A78DF900820638 /* FirebaseABTesting.xcframework */; }; + 14F23FF924A78DFA00820638 /* Protobuf.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDE24A78DF900820638 /* Protobuf.xcframework */; platformFilter = ios; }; + 14F23FFA24A78DFA00820638 /* Protobuf.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDE24A78DF900820638 /* Protobuf.xcframework */; }; + 14F23FFB24A78DFA00820638 /* Protobuf.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDE24A78DF900820638 /* Protobuf.xcframework */; }; + 14F23FFC24A78DFA00820638 /* FirebaseInstanceID.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDF24A78DF900820638 /* FirebaseInstanceID.xcframework */; platformFilter = ios; }; + 14F23FFD24A78DFA00820638 /* FirebaseInstanceID.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDF24A78DF900820638 /* FirebaseInstanceID.xcframework */; }; + 14F23FFE24A78DFA00820638 /* FirebaseInstanceID.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FDF24A78DF900820638 /* FirebaseInstanceID.xcframework */; }; + 14F23FFF24A78DFA00820638 /* FirebaseRemoteConfig.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE024A78DF900820638 /* FirebaseRemoteConfig.xcframework */; platformFilter = ios; }; + 14F2400024A78DFA00820638 /* FirebaseRemoteConfig.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE024A78DF900820638 /* FirebaseRemoteConfig.xcframework */; }; + 14F2400124A78DFA00820638 /* FirebaseRemoteConfig.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE024A78DF900820638 /* FirebaseRemoteConfig.xcframework */; }; + 14F2400224A78DFA00820638 /* GTMSessionFetcher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE124A78DF900820638 /* GTMSessionFetcher.xcframework */; platformFilter = ios; }; + 14F2400324A78DFA00820638 /* GTMSessionFetcher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE124A78DF900820638 /* GTMSessionFetcher.xcframework */; }; + 14F2400424A78DFA00820638 /* GTMSessionFetcher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE124A78DF900820638 /* GTMSessionFetcher.xcframework */; }; + 14F2400524A78DFA00820638 /* GoogleDataTransport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE324A78DF900820638 /* GoogleDataTransport.xcframework */; platformFilter = ios; }; + 14F2400624A78DFA00820638 /* GoogleDataTransport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE324A78DF900820638 /* GoogleDataTransport.xcframework */; }; + 14F2400724A78DFA00820638 /* GoogleDataTransport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE324A78DF900820638 /* GoogleDataTransport.xcframework */; }; + 14F2400824A78DFA00820638 /* FirebaseCrashlytics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE424A78DF900820638 /* FirebaseCrashlytics.xcframework */; platformFilter = ios; }; + 14F2400924A78DFA00820638 /* FirebaseCrashlytics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE424A78DF900820638 /* FirebaseCrashlytics.xcframework */; }; + 14F2400A24A78DFA00820638 /* FirebaseCrashlytics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE424A78DF900820638 /* FirebaseCrashlytics.xcframework */; }; + 14F2400B24A78DFA00820638 /* FirebaseCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE524A78DF900820638 /* FirebaseCore.xcframework */; platformFilter = ios; }; + 14F2400C24A78DFA00820638 /* FirebaseCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE524A78DF900820638 /* FirebaseCore.xcframework */; }; + 14F2400D24A78DFA00820638 /* FirebaseCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE524A78DF900820638 /* FirebaseCore.xcframework */; }; + 14F2400E24A78DFA00820638 /* GoogleToolboxForMac.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE624A78DF900820638 /* GoogleToolboxForMac.xcframework */; platformFilter = ios; }; + 14F2400F24A78DFA00820638 /* GoogleToolboxForMac.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE624A78DF900820638 /* GoogleToolboxForMac.xcframework */; }; + 14F2401024A78DFA00820638 /* GoogleToolboxForMac.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE624A78DF900820638 /* GoogleToolboxForMac.xcframework */; }; + 14F2401124A78DFA00820638 /* GoogleUtilities.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE724A78DF900820638 /* GoogleUtilities.xcframework */; platformFilter = ios; }; + 14F2401224A78DFA00820638 /* GoogleUtilities.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE724A78DF900820638 /* GoogleUtilities.xcframework */; }; + 14F2401324A78DFA00820638 /* GoogleUtilities.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FE724A78DF900820638 /* GoogleUtilities.xcframework */; }; + 14F2401A24A78DFA00820638 /* FirebaseInstallations.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEB24A78DFA00820638 /* FirebaseInstallations.xcframework */; platformFilter = ios; }; + 14F2401B24A78DFA00820638 /* FirebaseInstallations.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEB24A78DFA00820638 /* FirebaseInstallations.xcframework */; }; + 14F2401C24A78DFA00820638 /* FirebaseInstallations.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEB24A78DFA00820638 /* FirebaseInstallations.xcframework */; }; + 14F2401D24A78DFA00820638 /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEC24A78DFA00820638 /* FirebaseAnalytics.framework */; platformFilter = ios; }; + 14F2401E24A78DFA00820638 /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEC24A78DFA00820638 /* FirebaseAnalytics.framework */; }; + 14F2401F24A78DFA00820638 /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEC24A78DFA00820638 /* FirebaseAnalytics.framework */; }; + 14F2402024A78DFA00820638 /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FED24A78DFA00820638 /* FirebasePerformance.framework */; platformFilter = ios; }; + 14F2402124A78DFA00820638 /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FED24A78DFA00820638 /* FirebasePerformance.framework */; }; + 14F2402224A78DFA00820638 /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FED24A78DFA00820638 /* FirebasePerformance.framework */; }; + 14F2402324A78DFA00820638 /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEE24A78DFA00820638 /* GoogleAppMeasurement.framework */; platformFilter = ios; }; + 14F2402424A78DFA00820638 /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEE24A78DFA00820638 /* GoogleAppMeasurement.framework */; }; + 14F2402524A78DFA00820638 /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEE24A78DFA00820638 /* GoogleAppMeasurement.framework */; }; + 14F2402624A78DFA00820638 /* nanopb.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEF24A78DFA00820638 /* nanopb.xcframework */; platformFilter = ios; }; + 14F2402724A78DFA00820638 /* nanopb.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEF24A78DFA00820638 /* nanopb.xcframework */; }; + 14F2402824A78DFA00820638 /* nanopb.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FEF24A78DFA00820638 /* nanopb.xcframework */; }; + 14F2402924A78DFA00820638 /* PromisesObjC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF024A78DFA00820638 /* PromisesObjC.xcframework */; platformFilter = ios; }; + 14F2402A24A78DFA00820638 /* PromisesObjC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF024A78DFA00820638 /* PromisesObjC.xcframework */; }; + 14F2402B24A78DFA00820638 /* PromisesObjC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF024A78DFA00820638 /* PromisesObjC.xcframework */; }; + 14F2402C24A78DFA00820638 /* FIRAnalyticsConnector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF124A78DFA00820638 /* FIRAnalyticsConnector.framework */; platformFilter = ios; }; + 14F2402D24A78DFA00820638 /* FIRAnalyticsConnector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF124A78DFA00820638 /* FIRAnalyticsConnector.framework */; }; + 14F2402E24A78DFA00820638 /* FIRAnalyticsConnector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF124A78DFA00820638 /* FIRAnalyticsConnector.framework */; }; + 14F2402F24A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF224A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework */; platformFilter = ios; }; + 14F2403024A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF224A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework */; }; + 14F2403124A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14F23FF224A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework */; }; + 14F2EC9524A64A8F007A4C97 /* GKMatchRequest+WKR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */; }; + 14F2EC9624A64A8F007A4C97 /* GKMatchRequest+WKR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */; }; + 14F2EC9724A64A8F007A4C97 /* GKMatchRequest+WKR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -364,6 +552,13 @@ remoteGlobalIDString = 149FF7F21F362B0D000A5D96; remoteInfo = WKRUIKit; }; + 14E0CA3224AE48E50091868E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146B99F51F4F4B5600507B3F /* WKRKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 146E052424AE3E59001E1917; + remoteInfo = WKRKitCore; + }; 14E0F1AD22303FD100BFF1E9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 149FF8411F362B83000A5D96 /* Project object */; @@ -456,44 +651,21 @@ /* Begin PBXFileReference section */ 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SharedAssets.xcassets; sourceTree = ""; }; - 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameKitConnectViewController.swift; sourceTree = ""; }; - 1414280621FC394600C48788 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = ""; }; - 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameKitConnectViewController+Match.swift"; sourceTree = ""; }; - 141854DE2373666A008C988A /* MPCHostAutoInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCHostAutoInviteCell.swift; sourceTree = ""; }; - 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCConnectViewController+Invite.swift"; sourceTree = ""; }; + 1414280221FC18F600C48788 /* GKJoinViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GKJoinViewController.swift; sourceTree = ""; }; + 1414280621FC394600C48788 /* GKConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GKConnectViewController.swift; sourceTree = ""; }; + 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GKJoinViewController+Match.swift"; sourceTree = ""; }; + 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListFooterView.swift; sourceTree = ""; }; + 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultsItemContentView.swift; sourceTree = ""; }; + 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultsContentView.swift; sourceTree = ""; }; 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsViewController+Actions.swift"; sourceTree = ""; }; - 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDatabaseMetrics.swift; sourceTree = ""; }; - 1429327B242AE29000D64834 /* PointerInteractionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointerInteractionTableViewCell.swift; sourceTree = ""; }; - 1429327F242B0E5B00D64834 /* Protobuf.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Protobuf.xcframework; sourceTree = ""; }; - 14293280242B0E5B00D64834 /* FirebaseCoreDiagnostics.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseCoreDiagnostics.xcframework; sourceTree = ""; }; - 14293281242B0E5B00D64834 /* FirebaseABTesting.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseABTesting.xcframework; sourceTree = ""; }; - 14293282242B0E5B00D64834 /* FirebaseRemoteConfig.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseRemoteConfig.xcframework; sourceTree = ""; }; - 14293283242B0E5B00D64834 /* FirebaseInstanceID.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseInstanceID.xcframework; sourceTree = ""; }; - 14293284242B0E5B00D64834 /* FIRAnalyticsConnector.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FIRAnalyticsConnector.framework; sourceTree = ""; }; - 14293285242B0E5B00D64834 /* GoogleAppMeasurement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleAppMeasurement.framework; sourceTree = ""; }; - 14293286242B0E5B00D64834 /* FirebaseAnalytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseAnalytics.framework; sourceTree = ""; }; - 14293287242B0E5B00D64834 /* GoogleDataTransportCCTSupport.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleDataTransportCCTSupport.xcframework; sourceTree = ""; }; - 14293288242B0E5B00D64834 /* FirebaseInstallations.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseInstallations.xcframework; sourceTree = ""; }; - 14293289242B0E5C00D64834 /* FirebasePerformance.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebasePerformance.framework; sourceTree = ""; }; - 1429328A242B0E5C00D64834 /* GTMSessionFetcher.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GTMSessionFetcher.xcframework; sourceTree = ""; }; - 1429328B242B0E5C00D64834 /* FirebaseCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseCore.xcframework; sourceTree = ""; }; - 1429328D242B0E5C00D64834 /* nanopb.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = nanopb.xcframework; sourceTree = ""; }; - 1429328E242B0E5C00D64834 /* GoogleDataTransport.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleDataTransport.xcframework; sourceTree = ""; }; - 1429328F242B0E5C00D64834 /* GoogleUtilities.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleUtilities.xcframework; sourceTree = ""; }; - 14293290242B0E5C00D64834 /* GoogleToolboxForMac.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleToolboxForMac.xcframework; sourceTree = ""; }; - 14293291242B0E5C00D64834 /* PromisesObjC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = PromisesObjC.xcframework; sourceTree = ""; }; - 142932CB242B0F2F00D64834 /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Crashlytics.framework; sourceTree = ""; }; - 142932CF242B0F6800D64834 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Fabric.framework; sourceTree = ""; }; - 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCConnectViewController+KB.swift"; sourceTree = ""; }; + 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCloudKitStatsManager.swift; sourceTree = ""; }; 142ABDF924600559008E7F77 /* PlusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusViewController.swift; sourceTree = ""; }; 142ABDFD24600576008E7F77 /* PlusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusView.swift; sourceTree = ""; }; 142ABE012460057C008E7F77 /* ActivityButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityButton.swift; sourceTree = ""; }; 142ABE0524600A74008E7F77 /* PlusStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlusStore.swift; sourceTree = ""; }; 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagicSubscription.swift; sourceTree = ""; }; - 142ABE1124600AF0008E7F77 /* OSLog+Magic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OSLog+Magic.swift"; sourceTree = ""; }; 142ABE15246013BC008E7F77 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+KB.swift"; sourceTree = ""; }; - 142F7152210C34A300C66558 /* MPCHostViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCHostViewController+KB.swift"; sourceTree = ""; }; 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VotingViewController+KB.swift"; sourceTree = ""; }; 142F7156210C375F00C66558 /* GameViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+KB.swift"; sourceTree = ""; }; 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTableViewStatsCell.swift; sourceTree = ""; }; @@ -502,12 +674,15 @@ 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatsManager.swift; sourceTree = ""; }; 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+GameKit.swift"; sourceTree = ""; }; 143BB7321F60DE9300D00541 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; - 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCConnectViewController.swift; sourceTree = ""; }; + 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GKJoinViewController+PublicRace.swift"; sourceTree = ""; }; 144280EC1F5883AB002D977F /* WikiRaces.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WikiRaces.entitlements; sourceTree = ""; }; + 1446C1D824A334C2000A0ED3 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; + 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GKHostViewController+Match.swift"; sourceTree = ""; }; 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultRenderer.swift; sourceTree = ""; }; - 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenteredTableViewController.swift; sourceTree = ""; }; + 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualEffectViewController.swift; sourceTree = ""; }; 144A1028202FC79B003DB51A /* HelpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewController.swift; sourceTree = ""; }; 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRAppDelegate.swift; sourceTree = ""; }; + 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackingVisualEffectViewController.swift; sourceTree = ""; }; 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsViewController+KB.swift"; sourceTree = ""; }; 146B99F51F4F4B5600507B3F /* WKRKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = WKRKit.xcodeproj; path = ../WKRKit/WKRKit.xcodeproj; sourceTree = SOURCE_ROOT; }; 146B9A061F4F4B6100507B3F /* WKRUIKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = WKRUIKit.xcodeproj; path = ../WKRUIKit/WKRUIKit.xcodeproj; sourceTree = SOURCE_ROOT; }; @@ -518,12 +693,15 @@ 147593D01F609911005DFC90 /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultRenderer+Creation.swift"; sourceTree = ""; }; 14794CA8236C884A00835DC9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 147EF6F82202436600583D73 /* MPCHostContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MPCHostContext.swift; path = "Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift"; sourceTree = SOURCE_ROOT; }; 1485B6762230724A00D6800B /* MedalScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedalScene.swift; sourceTree = ""; }; 1485B67C223072AB00D6800B /* MedalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedalView.swift; sourceTree = ""; }; + 148CCBCC24A3410700538F18 /* GKHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GKHelper.swift; sourceTree = ""; }; + 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyRaceAdvertiser.swift; sourceTree = ""; }; + 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyRaceListener.swift; sourceTree = ""; }; + 148E5C4D24A2943100DD43E4 /* Nearby.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nearby.swift; sourceTree = ""; }; + 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GKHostViewController.swift; sourceTree = ""; }; + 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RaceCodeGenerator.swift; sourceTree = ""; }; 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryViewController+KB.swift"; sourceTree = ""; }; - 149357DE210E934300F6453A /* MPCConnectViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCConnectViewController+UI.swift"; sourceTree = ""; }; - 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCHostPeerStateCell.swift; sourceTree = ""; }; 149FF8491F362B83000A5D96 /* WikiRaces.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WikiRaces.app; sourceTree = BUILT_PRODUCTS_DIR; }; 149FF84C1F362B83000A5D96 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 149FF8531F362B83000A5D96 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -539,17 +717,14 @@ 149FF8901F362BF1000A5D96 /* WikiRacesScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiRacesScreenshots.swift; sourceTree = ""; }; 149FF8921F362BF1000A5D96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 14A89F671F7ABB2100C85387 /* WikiRaces (Multi-Window).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WikiRaces (Multi-Window).entitlements"; sourceTree = ""; }; + 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCloudKitLiveRaceManager.swift; sourceTree = ""; }; 14B2DD3A22212298009B8AB3 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuView+Actions.swift"; sourceTree = ""; }; 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuView+Setup.swift"; sourceTree = ""; }; - 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalRacesHelper.swift; sourceTree = ""; }; 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingPuzzleView.swift; sourceTree = ""; }; - 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCHostSoloCell.swift; sourceTree = ""; }; 14B55C981F3A49D20090E092 /* DebugWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugWindow.swift; sourceTree = ""; }; 14B8F80C222C456B006C7A06 /* GameKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameKit.framework; path = System/Library/Frameworks/GameKit.framework; sourceTree = SDKROOT; }; - 14B8F810222C47FA006C7A06 /* GKMessageImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = GKMessageImage.png; sourceTree = ""; }; 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+Debug.swift"; sourceTree = ""; }; - 14BAB5332229DC6200C5AE27 /* Firebase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Firebase.h; sourceTree = ""; }; 14C25FDD1F6F025A00CD7373 /* WikiRaces (UI Catalog).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WikiRaces (UI Catalog).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 14C25FDF1F6F025A00CD7373 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 14C25FE11F6F025A00CD7373 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -557,38 +732,65 @@ 14C25FE61F6F025A00CD7373 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14C25FE91F6F025A00CD7373 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 14C25FEB1F6F025A00CD7373 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsContentViewModel.swift; sourceTree = ""; }; 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsViewController.swift; sourceTree = ""; }; - 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsViewController+TableView.swift"; sourceTree = ""; }; - 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsTableViewCell.swift; sourceTree = ""; }; 14C4AB381F3689A90086B77F /* MenuTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuTile.swift; sourceTree = ""; }; 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; 14C4AB481F3689C60086B77F /* VotingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingViewController.swift; sourceTree = ""; }; - 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VotingViewController+TableView.swift"; sourceTree = ""; }; 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTableViewCell.swift; sourceTree = ""; }; 14C4AB581F368A050086B77F /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+Manager.swift"; sourceTree = ""; }; 14C4AB641F368A210086B77F /* GameViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+UI.swift"; sourceTree = ""; }; - 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingTableViewCell.swift; sourceTree = ""; }; - 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPCHostViewController.swift; sourceTree = ""; }; - 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPCHostSearchingCell.swift; sourceTree = ""; }; 14C6DA762462900300EC9817 /* CustomRaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRaceController.swift; sourceTree = ""; }; 14D18CDB2460CF00002E4F5D /* StatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsViewController.swift; sourceTree = ""; }; 14D18CDF2460DF9C002E4F5D /* StatsPlayersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPlayersViewController.swift; sourceTree = ""; }; - 14D7DC70245FC0D800772E6F /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+WikiRaces.swift"; sourceTree = ""; }; 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRAnimationDurationConstants.swift; sourceTree = ""; }; - 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCHostViewController+Table.swift"; sourceTree = ""; }; - 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDatabaseStat.swift; sourceTree = ""; }; + 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerUserDefaultsStat.swift; sourceTree = ""; }; 14E0F1A822303FD100BFF1E9 /* WikiRacesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WikiRacesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 14E0F1AA22303FD100BFF1E9 /* WikiRacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiRacesTests.swift; sourceTree = ""; }; 14E0F1AC22303FD100BFF1E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerAnonymousMetrics.swift; sourceTree = ""; }; + 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerFirebaseAnalytics.swift; sourceTree = ""; }; 14E1B1A01F798A520082F4FA /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; + 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingContentView.swift; sourceTree = ""; }; + 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingItemContentView.swift; sourceTree = ""; }; + 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingContentViewModel.swift; sourceTree = ""; }; + 14EBF58624A4D8260040A7C0 /* HostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostContentView.swift; sourceTree = ""; }; + 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; + 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostContentViewModel.swift; sourceTree = ""; }; + 14EBF5A524A4F4680040A7C0 /* PlayerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerImageView.swift; sourceTree = ""; }; + 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSectionView.swift; sourceTree = ""; }; 14EF51CA245FAE5500F3653F /* CustomRaceOtherController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRaceOtherController.swift; sourceTree = ""; }; 14EF51CB245FAE5500F3653F /* CustomRacePageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRacePageViewController.swift; sourceTree = ""; }; 14EF51CC245FAE5500F3653F /* CustomRaceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRaceViewController.swift; sourceTree = ""; }; 14EF51CD245FAE5600F3653F /* CustomRaceNotificationsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRaceNotificationsController.swift; sourceTree = ""; }; 14EF51CE245FAE5600F3653F /* CustomRaceNumericalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRaceNumericalViewController.swift; sourceTree = ""; }; + 14F0348024A8397A006A908E /* RaceChecksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RaceChecksViewController.swift; sourceTree = ""; }; + 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContentViewModel.swift; sourceTree = ""; }; + 14F034AC24A8480D006A908E /* LoadingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContentView.swift; sourceTree = ""; }; + 14F23FDB24A78DF900820638 /* GoogleDataTransportCCTSupport.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleDataTransportCCTSupport.xcframework; sourceTree = ""; }; + 14F23FDC24A78DF900820638 /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 14F23FDD24A78DF900820638 /* FirebaseABTesting.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseABTesting.xcframework; sourceTree = ""; }; + 14F23FDE24A78DF900820638 /* Protobuf.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Protobuf.xcframework; sourceTree = ""; }; + 14F23FDF24A78DF900820638 /* FirebaseInstanceID.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseInstanceID.xcframework; sourceTree = ""; }; + 14F23FE024A78DF900820638 /* FirebaseRemoteConfig.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseRemoteConfig.xcframework; sourceTree = ""; }; + 14F23FE124A78DF900820638 /* GTMSessionFetcher.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GTMSessionFetcher.xcframework; sourceTree = ""; }; + 14F23FE224A78DF900820638 /* Firebase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Firebase.h; sourceTree = ""; }; + 14F23FE324A78DF900820638 /* GoogleDataTransport.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleDataTransport.xcframework; sourceTree = ""; }; + 14F23FE424A78DF900820638 /* FirebaseCrashlytics.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseCrashlytics.xcframework; sourceTree = ""; }; + 14F23FE524A78DF900820638 /* FirebaseCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseCore.xcframework; sourceTree = ""; }; + 14F23FE624A78DF900820638 /* GoogleToolboxForMac.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleToolboxForMac.xcframework; sourceTree = ""; }; + 14F23FE724A78DF900820638 /* GoogleUtilities.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GoogleUtilities.xcframework; sourceTree = ""; }; + 14F23FEB24A78DFA00820638 /* FirebaseInstallations.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseInstallations.xcframework; sourceTree = ""; }; + 14F23FEC24A78DFA00820638 /* FirebaseAnalytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseAnalytics.framework; sourceTree = ""; }; + 14F23FED24A78DFA00820638 /* FirebasePerformance.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebasePerformance.framework; sourceTree = ""; }; + 14F23FEE24A78DFA00820638 /* GoogleAppMeasurement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleAppMeasurement.framework; sourceTree = ""; }; + 14F23FEF24A78DFA00820638 /* nanopb.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = nanopb.xcframework; sourceTree = ""; }; + 14F23FF024A78DFA00820638 /* PromisesObjC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = PromisesObjC.xcframework; sourceTree = ""; }; + 14F23FF124A78DFA00820638 /* FIRAnalyticsConnector.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FIRAnalyticsConnector.framework; sourceTree = ""; }; + 14F23FF224A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = FirebaseCoreDiagnostics.xcframework; sourceTree = ""; }; + 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GKMatchRequest+WKR.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -597,30 +799,29 @@ buildActionMask = 2147483647; files = ( 14E6D55D1F86A1CA005EB3B9 /* WKRKit.framework in Frameworks */, - 142932BC242B0E5C00D64834 /* nanopb.xcframework in Frameworks */, - 142932C8242B0E5C00D64834 /* PromisesObjC.xcframework in Frameworks */, - 142932C2242B0E5C00D64834 /* GoogleUtilities.xcframework in Frameworks */, - 142932CC242B0F2F00D64834 /* Crashlytics.framework in Frameworks */, - 142932B3242B0E5C00D64834 /* GTMSessionFetcher.xcframework in Frameworks */, - 14293298242B0E5C00D64834 /* FirebaseABTesting.xcframework in Frameworks */, - 142932A4242B0E5C00D64834 /* GoogleAppMeasurement.framework in Frameworks */, + 14F23FF624A78DFA00820638 /* FirebaseABTesting.xcframework in Frameworks */, + 14F2402324A78DFA00820638 /* GoogleAppMeasurement.framework in Frameworks */, + 14F2401D24A78DFA00820638 /* FirebaseAnalytics.framework in Frameworks */, + 14F2402F24A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework in Frameworks */, + 14F2402024A78DFA00820638 /* FirebasePerformance.framework in Frameworks */, + 14F2400824A78DFA00820638 /* FirebaseCrashlytics.xcframework in Frameworks */, + 14F2402624A78DFA00820638 /* nanopb.xcframework in Frameworks */, + 14F2400224A78DFA00820638 /* GTMSessionFetcher.xcframework in Frameworks */, + 14F23FFF24A78DFA00820638 /* FirebaseRemoteConfig.xcframework in Frameworks */, 14E1B1A11F798A520082F4FA /* CloudKit.framework in Frameworks */, - 142932AD242B0E5C00D64834 /* FirebaseInstallations.xcframework in Frameworks */, - 142932A1242B0E5C00D64834 /* FIRAnalyticsConnector.framework in Frameworks */, - 142932D0242B0F6800D64834 /* Fabric.framework in Frameworks */, + 14F2401A24A78DFA00820638 /* FirebaseInstallations.xcframework in Frameworks */, 14E6D5611F86A1CF005EB3B9 /* WKRUIKit.framework in Frameworks */, - 142932BF242B0E5C00D64834 /* GoogleDataTransport.xcframework in Frameworks */, - 142932B6242B0E5C00D64834 /* FirebaseCore.xcframework in Frameworks */, - 1429329E242B0E5C00D64834 /* FirebaseInstanceID.xcframework in Frameworks */, - 142932C5242B0E5C00D64834 /* GoogleToolboxForMac.xcframework in Frameworks */, + 14F2400E24A78DFA00820638 /* GoogleToolboxForMac.xcframework in Frameworks */, + 14F2400524A78DFA00820638 /* GoogleDataTransport.xcframework in Frameworks */, + 14F23FFC24A78DFA00820638 /* FirebaseInstanceID.xcframework in Frameworks */, + 14F2400B24A78DFA00820638 /* FirebaseCore.xcframework in Frameworks */, + 14F2401124A78DFA00820638 /* GoogleUtilities.xcframework in Frameworks */, + 14F2402C24A78DFA00820638 /* FIRAnalyticsConnector.framework in Frameworks */, + 14F23FF924A78DFA00820638 /* Protobuf.xcframework in Frameworks */, + 14F23FF324A78DFA00820638 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */, 142ABE16246013BC008E7F77 /* StoreKit.framework in Frameworks */, - 1429329B242B0E5C00D64834 /* FirebaseRemoteConfig.xcframework in Frameworks */, - 142932A7242B0E5C00D64834 /* FirebaseAnalytics.framework in Frameworks */, - 142932AA242B0E5C00D64834 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */, - 142932B0242B0E5C00D64834 /* FirebasePerformance.framework in Frameworks */, 14B8F80D222C456B006C7A06 /* GameKit.framework in Frameworks */, - 14293295242B0E5C00D64834 /* FirebaseCoreDiagnostics.xcframework in Frameworks */, - 14293292242B0E5C00D64834 /* Protobuf.xcframework in Frameworks */, + 14F2402924A78DFA00820638 /* PromisesObjC.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -628,29 +829,28 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 142932D1242B0F6800D64834 /* Fabric.framework in Frameworks */, - 1429329F242B0E5C00D64834 /* FirebaseInstanceID.xcframework in Frameworks */, - 14293299242B0E5C00D64834 /* FirebaseABTesting.xcframework in Frameworks */, - 142932B4242B0E5C00D64834 /* GTMSessionFetcher.xcframework in Frameworks */, - 142932A5242B0E5C00D64834 /* GoogleAppMeasurement.framework in Frameworks */, - 142932AB242B0E5C00D64834 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */, - 142932AE242B0E5C00D64834 /* FirebaseInstallations.xcframework in Frameworks */, - 14293293242B0E5C00D64834 /* Protobuf.xcframework in Frameworks */, - 142932B1242B0E5C00D64834 /* FirebasePerformance.framework in Frameworks */, - 1429329C242B0E5C00D64834 /* FirebaseRemoteConfig.xcframework in Frameworks */, - 142932B7242B0E5C00D64834 /* FirebaseCore.xcframework in Frameworks */, - 142932CD242B0F2F00D64834 /* Crashlytics.framework in Frameworks */, - 142932A2242B0E5C00D64834 /* FIRAnalyticsConnector.framework in Frameworks */, - 14293296242B0E5C00D64834 /* FirebaseCoreDiagnostics.xcframework in Frameworks */, + 14F2401E24A78DFA00820638 /* FirebaseAnalytics.framework in Frameworks */, + 14F2402724A78DFA00820638 /* nanopb.xcframework in Frameworks */, + 14F2400624A78DFA00820638 /* GoogleDataTransport.xcframework in Frameworks */, + 14F2402424A78DFA00820638 /* GoogleAppMeasurement.framework in Frameworks */, + 14F2402D24A78DFA00820638 /* FIRAnalyticsConnector.framework in Frameworks */, + 14F2400C24A78DFA00820638 /* FirebaseCore.xcframework in Frameworks */, + 14F23FFD24A78DFA00820638 /* FirebaseInstanceID.xcframework in Frameworks */, + 14F23FFA24A78DFA00820638 /* Protobuf.xcframework in Frameworks */, + 14F2400324A78DFA00820638 /* GTMSessionFetcher.xcframework in Frameworks */, 14DC66D31F9072180026C6ED /* WKRKit.framework in Frameworks */, - 142932C6242B0E5C00D64834 /* GoogleToolboxForMac.xcframework in Frameworks */, - 142932A8242B0E5C00D64834 /* FirebaseAnalytics.framework in Frameworks */, - 142932C3242B0E5C00D64834 /* GoogleUtilities.xcframework in Frameworks */, - 142932BD242B0E5C00D64834 /* nanopb.xcframework in Frameworks */, 14A89F681F7ABB2400C85387 /* CloudKit.framework in Frameworks */, + 14F2400024A78DFA00820638 /* FirebaseRemoteConfig.xcframework in Frameworks */, 14DC66D71F9072200026C6ED /* WKRUIKit.framework in Frameworks */, - 142932C0242B0E5C00D64834 /* GoogleDataTransport.xcframework in Frameworks */, - 142932C9242B0E5C00D64834 /* PromisesObjC.xcframework in Frameworks */, + 14F2401224A78DFA00820638 /* GoogleUtilities.xcframework in Frameworks */, + 14F23FF724A78DFA00820638 /* FirebaseABTesting.xcframework in Frameworks */, + 14F2401B24A78DFA00820638 /* FirebaseInstallations.xcframework in Frameworks */, + 14F2403024A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework in Frameworks */, + 14F23FF424A78DFA00820638 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */, + 14F2400924A78DFA00820638 /* FirebaseCrashlytics.xcframework in Frameworks */, + 14F2400F24A78DFA00820638 /* GoogleToolboxForMac.xcframework in Frameworks */, + 14F2402124A78DFA00820638 /* FirebasePerformance.framework in Frameworks */, + 14F2402A24A78DFA00820638 /* PromisesObjC.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -665,28 +865,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 142932A0242B0E5C00D64834 /* FirebaseInstanceID.xcframework in Frameworks */, - 142932C7242B0E5C00D64834 /* GoogleToolboxForMac.xcframework in Frameworks */, - 142932CA242B0E5C00D64834 /* PromisesObjC.xcframework in Frameworks */, - 14293297242B0E5C00D64834 /* FirebaseCoreDiagnostics.xcframework in Frameworks */, - 142932B2242B0E5C00D64834 /* FirebasePerformance.framework in Frameworks */, - 142932BE242B0E5C00D64834 /* nanopb.xcframework in Frameworks */, - 142932AF242B0E5C00D64834 /* FirebaseInstallations.xcframework in Frameworks */, - 142932C1242B0E5C00D64834 /* GoogleDataTransport.xcframework in Frameworks */, - 142932C4242B0E5C00D64834 /* GoogleUtilities.xcframework in Frameworks */, - 142932A6242B0E5C00D64834 /* GoogleAppMeasurement.framework in Frameworks */, - 14293294242B0E5C00D64834 /* Protobuf.xcframework in Frameworks */, - 142932A9242B0E5C00D64834 /* FirebaseAnalytics.framework in Frameworks */, - 142932B8242B0E5C00D64834 /* FirebaseCore.xcframework in Frameworks */, - 142932CE242B0F2F00D64834 /* Crashlytics.framework in Frameworks */, + 14F23FF524A78DFA00820638 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */, + 14F2402524A78DFA00820638 /* GoogleAppMeasurement.framework in Frameworks */, + 14F2400D24A78DFA00820638 /* FirebaseCore.xcframework in Frameworks */, + 14F2402E24A78DFA00820638 /* FIRAnalyticsConnector.framework in Frameworks */, + 14F2400424A78DFA00820638 /* GTMSessionFetcher.xcframework in Frameworks */, + 14F2401324A78DFA00820638 /* GoogleUtilities.xcframework in Frameworks */, + 14F2401C24A78DFA00820638 /* FirebaseInstallations.xcframework in Frameworks */, 14584BC7220B6BEE00D63428 /* WKRUIKit.framework in Frameworks */, - 1429329A242B0E5C00D64834 /* FirebaseABTesting.xcframework in Frameworks */, - 1429329D242B0E5C00D64834 /* FirebaseRemoteConfig.xcframework in Frameworks */, + 14F2400124A78DFA00820638 /* FirebaseRemoteConfig.xcframework in Frameworks */, + 14F23FF824A78DFA00820638 /* FirebaseABTesting.xcframework in Frameworks */, 14584BC2220B6BE700D63428 /* WKRKit.framework in Frameworks */, - 142932B5242B0E5C00D64834 /* GTMSessionFetcher.xcframework in Frameworks */, - 142932AC242B0E5C00D64834 /* GoogleDataTransportCCTSupport.xcframework in Frameworks */, - 142932A3242B0E5C00D64834 /* FIRAnalyticsConnector.framework in Frameworks */, - 142932D2242B0F6800D64834 /* Fabric.framework in Frameworks */, + 14F2400A24A78DFA00820638 /* FirebaseCrashlytics.xcframework in Frameworks */, + 14F2400724A78DFA00820638 /* GoogleDataTransport.xcframework in Frameworks */, + 14F23FFE24A78DFA00820638 /* FirebaseInstanceID.xcframework in Frameworks */, + 14F2402224A78DFA00820638 /* FirebasePerformance.framework in Frameworks */, + 14F2401024A78DFA00820638 /* GoogleToolboxForMac.xcframework in Frameworks */, + 14F2402B24A78DFA00820638 /* PromisesObjC.xcframework in Frameworks */, + 14F2402824A78DFA00820638 /* nanopb.xcframework in Frameworks */, + 14F23FFB24A78DFA00820638 /* Protobuf.xcframework in Frameworks */, + 14F2403124A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework in Frameworks */, + 14F2401F24A78DFA00820638 /* FirebaseAnalytics.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -700,24 +899,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1414280821FC3C9800C48788 /* GameKit */ = { - isa = PBXGroup; - children = ( - 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */, - 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */, - ); - path = GameKit; - sourceTree = ""; - }; - 1414280921FC3CA000C48788 /* Multipeer Connectivity */ = { + 1414280821FC3C9800C48788 /* GKJoinViewController */ = { isa = PBXGroup; children = ( - 147EF6F82202436600583D73 /* MPCHostContext.swift */, - 14EF51C6245FAE3600F3653F /* CustomRaceViewController */, - 14DFBD21210EFE8E00BD8DAF /* MPCConnectViewController */, - 14DFBD22210EFE9900BD8DAF /* MPCHostViewController */, + 1414280221FC18F600C48788 /* GKJoinViewController.swift */, + 1414280A21FC437000C48788 /* GKJoinViewController+Match.swift */, + 143CE07D24A355DC000F6833 /* GKJoinViewController+PublicRace.swift */, ); - path = "Multipeer Connectivity"; + path = GKJoinViewController; sourceTree = ""; }; 142ABDF624600550008E7F77 /* PlusViewController */ = { @@ -728,7 +917,6 @@ 142ABE0524600A74008E7F77 /* PlusStore.swift */, 142ABE012460057C008E7F77 /* ActivityButton.swift */, 142ABE0D24600ABE008E7F77 /* MagicSubscription.swift */, - 142ABE1124600AF0008E7F77 /* OSLog+Magic.swift */, ); path = PlusViewController; sourceTree = ""; @@ -736,28 +924,27 @@ 143054C01F8958DC00C0BC27 /* Analytics */ = { isa = PBXGroup; children = ( - 14BAB5332229DC6200C5AE27 /* Firebase.h */, - 14D7DC70245FC0D800772E6F /* module.modulemap */, - 142932CF242B0F6800D64834 /* Fabric.framework */, - 142932CB242B0F2F00D64834 /* Crashlytics.framework */, - 14293284242B0E5B00D64834 /* FIRAnalyticsConnector.framework */, - 14293281242B0E5B00D64834 /* FirebaseABTesting.xcframework */, - 14293286242B0E5B00D64834 /* FirebaseAnalytics.framework */, - 1429328B242B0E5C00D64834 /* FirebaseCore.xcframework */, - 14293280242B0E5B00D64834 /* FirebaseCoreDiagnostics.xcframework */, - 14293288242B0E5B00D64834 /* FirebaseInstallations.xcframework */, - 14293283242B0E5B00D64834 /* FirebaseInstanceID.xcframework */, - 14293289242B0E5C00D64834 /* FirebasePerformance.framework */, - 14293282242B0E5B00D64834 /* FirebaseRemoteConfig.xcframework */, - 14293285242B0E5B00D64834 /* GoogleAppMeasurement.framework */, - 1429328E242B0E5C00D64834 /* GoogleDataTransport.xcframework */, - 14293287242B0E5B00D64834 /* GoogleDataTransportCCTSupport.xcframework */, - 14293290242B0E5C00D64834 /* GoogleToolboxForMac.xcframework */, - 1429328F242B0E5C00D64834 /* GoogleUtilities.xcframework */, - 1429328A242B0E5C00D64834 /* GTMSessionFetcher.xcframework */, - 1429328D242B0E5C00D64834 /* nanopb.xcframework */, - 14293291242B0E5C00D64834 /* PromisesObjC.xcframework */, - 1429327F242B0E5B00D64834 /* Protobuf.xcframework */, + 14F23FF124A78DFA00820638 /* FIRAnalyticsConnector.framework */, + 14F23FE224A78DF900820638 /* Firebase.h */, + 14F23FDD24A78DF900820638 /* FirebaseABTesting.xcframework */, + 14F23FEC24A78DFA00820638 /* FirebaseAnalytics.framework */, + 14F23FE524A78DF900820638 /* FirebaseCore.xcframework */, + 14F23FF224A78DFA00820638 /* FirebaseCoreDiagnostics.xcframework */, + 14F23FE424A78DF900820638 /* FirebaseCrashlytics.xcframework */, + 14F23FEB24A78DFA00820638 /* FirebaseInstallations.xcframework */, + 14F23FDF24A78DF900820638 /* FirebaseInstanceID.xcframework */, + 14F23FED24A78DFA00820638 /* FirebasePerformance.framework */, + 14F23FE024A78DF900820638 /* FirebaseRemoteConfig.xcframework */, + 14F23FEE24A78DFA00820638 /* GoogleAppMeasurement.framework */, + 14F23FE324A78DF900820638 /* GoogleDataTransport.xcframework */, + 14F23FDB24A78DF900820638 /* GoogleDataTransportCCTSupport.xcframework */, + 14F23FE624A78DF900820638 /* GoogleToolboxForMac.xcframework */, + 14F23FE724A78DF900820638 /* GoogleUtilities.xcframework */, + 14F23FE124A78DF900820638 /* GTMSessionFetcher.xcframework */, + 14F23FDC24A78DF900820638 /* module.modulemap */, + 14F23FEF24A78DFA00820638 /* nanopb.xcframework */, + 14F23FF024A78DFA00820638 /* PromisesObjC.xcframework */, + 14F23FDE24A78DF900820638 /* Protobuf.xcframework */, ); path = Analytics; sourceTree = ""; @@ -774,17 +961,33 @@ 143BB7331F60DF0200D00541 /* Connect View Controllers */ = { isa = PBXGroup; children = ( - 1414280621FC394600C48788 /* ConnectViewController.swift */, - 1414280821FC3C9800C48788 /* GameKit */, - 1414280921FC3CA000C48788 /* Multipeer Connectivity */, + 14F0348024A8397A006A908E /* RaceChecksViewController.swift */, + 14F034A624A847FF006A908E /* LoadingContentViewModel.swift */, + 14F034AC24A8480D006A908E /* LoadingContentView.swift */, + 1414280621FC394600C48788 /* GKConnectViewController.swift */, + 148E5C3224A2883300DD43E4 /* GKHostViewController */, + 1414280821FC3C9800C48788 /* GKJoinViewController */, + 14EF51C6245FAE3600F3653F /* CustomRaceViewController */, + 1446C1DE24A337B2000A0ED3 /* Nearby */, ); path = "Connect View Controllers"; sourceTree = ""; }; + 1446C1DE24A337B2000A0ED3 /* Nearby */ = { + isa = PBXGroup; + children = ( + 148E5C4D24A2943100DD43E4 /* Nearby.swift */, + 148E5C4124A293DD00DD43E4 /* NearbyRaceAdvertiser.swift */, + 148E5C4724A293EB00DD43E4 /* NearbyRaceListener.swift */, + ); + path = Nearby; + sourceTree = ""; + }; 144A1024202FC786003DB51A /* Other */ = { isa = PBXGroup; children = ( - 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */, + 144A1027202FC79B003DB51A /* VisualEffectViewController.swift */, + 144EC2A424ADA93500F0C315 /* BackingVisualEffectViewController.swift */, 144A1028202FC79B003DB51A /* HelpViewController.swift */, 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */, ); @@ -800,6 +1003,26 @@ path = StatsViewController; sourceTree = ""; }; + 146E04F924AE3127001E1917 /* GameKit Support */ = { + isa = PBXGroup; + children = ( + 148E5C5F24A2AE7D00DD43E4 /* RaceCodeGenerator.swift */, + 148CCBCC24A3410700538F18 /* GKHelper.swift */, + 14F2EC9424A64A8F007A4C97 /* GKMatchRequest+WKR.swift */, + ); + path = "GameKit Support"; + sourceTree = ""; + }; + 148E5C3224A2883300DD43E4 /* GKHostViewController */ = { + isa = PBXGroup; + children = ( + 148E5C5324A2967600DD43E4 /* GKHostViewController.swift */, + 1446C1DF24A337DD000A0ED3 /* GKHostViewController+Match.swift */, + 14EBF5A424A4F4470040A7C0 /* SwiftUI */, + ); + path = GKHostViewController; + sourceTree = ""; + }; 149FDED51F4F4AAF003A71D0 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -845,7 +1068,6 @@ 149FF8531F362B83000A5D96 /* Assets.xcassets */, 149FF8551F362B83000A5D96 /* LaunchScreen.storyboard */, 149FF8581F362B83000A5D96 /* Info.plist */, - 14B8F810222C47FA006C7A06 /* GKMessageImage.png */, ); path = WikiRaces; sourceTree = ""; @@ -880,11 +1102,11 @@ 14A778251F6133C700823DE8 /* Race View Controllers */ = { isa = PBXGroup; children = ( - 144A1024202FC786003DB51A /* Other */, 14B55C931F3A49820090E092 /* GameViewController */, + 14B55C921F3A497B0090E092 /* VotingViewController */, 14B55C8F1F3A495C0090E092 /* ResultsViewController */, 14B55C911F3A496D0090E092 /* HistoryViewController */, - 14B55C921F3A497B0090E092 /* VotingViewController */, + 144A1024202FC786003DB51A /* Other */, ); path = "Race View Controllers"; sourceTree = ""; @@ -924,27 +1146,15 @@ path = MenuView; sourceTree = ""; }; - 14B4DB6E2224FB96007D4B54 /* Cells */ = { - isa = PBXGroup; - children = ( - 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */, - 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */, - 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */, - 141854DE2373666A008C988A /* MPCHostAutoInviteCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; 14B55C8F1F3A495C0090E092 /* ResultsViewController */ = { isa = PBXGroup; children = ( 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */, 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */, - 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */, 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */, - 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */, 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */, 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */, + 14EBF55724A4AEA00040A7C0 /* SwiftUI */, ); path = ResultsViewController; sourceTree = ""; @@ -976,9 +1186,8 @@ isa = PBXGroup; children = ( 14C4AB481F3689C60086B77F /* VotingViewController.swift */, - 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */, 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */, - 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */, + 14EBF5B724A5F5800040A7C0 /* SwiftUI */, ); path = VotingViewController; sourceTree = ""; @@ -997,10 +1206,10 @@ 14B55C951F3A499C0090E092 /* Other */ = { isa = PBXGroup; children = ( - 1429327B242AE29000D64834 /* PointerInteractionTableViewCell.swift */, - 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */, + 1446C1D824A334C2000A0ED3 /* Defaults.swift */, 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */, 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */, + 146E04F924AE3127001E1917 /* GameKit Support */, ); path = Other; sourceTree = ""; @@ -1034,36 +1243,16 @@ 14DF31B121163ABA005BA432 /* Logging */ = { isa = PBXGroup; children = ( - 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */, + 14DB8C9824AC2A8400D356DF /* OSLog+WikiRaces.swift */, + 14E1B1991F7981C70082F4FA /* PlayerFirebaseAnalytics.swift */, + 14E0F19E22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift */, 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */, - 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */, - 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */, + 14B05D2424AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift */, + 141E4CF022012B2F000A0A15 /* PlayerCloudKitStatsManager.swift */, ); path = Logging; sourceTree = ""; }; - 14DFBD21210EFE8E00BD8DAF /* MPCConnectViewController */ = { - isa = PBXGroup; - children = ( - 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */, - 149357DE210E934300F6453A /* MPCConnectViewController+UI.swift */, - 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */, - 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */, - ); - path = MPCConnectViewController; - sourceTree = ""; - }; - 14DFBD22210EFE9900BD8DAF /* MPCHostViewController */ = { - isa = PBXGroup; - children = ( - 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */, - 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */, - 142F7152210C34A300C66558 /* MPCHostViewController+KB.swift */, - 14B4DB6E2224FB96007D4B54 /* Cells */, - ); - path = MPCHostViewController; - sourceTree = ""; - }; 14E0F1A922303FD100BFF1E9 /* WikiRacesTests */ = { isa = PBXGroup; children = ( @@ -1096,11 +1285,45 @@ isa = PBXGroup; children = ( 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */, + 14E0CA3324AE48E50091868E /* WKRKitCore.framework */, 14E6D55C1F86A1B0005EB3B9 /* WKRKitTests.xctest */, ); name = Products; sourceTree = ""; }; + 14EBF55724A4AEA00040A7C0 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 14C2AEFE24A3CE9000D2378C /* ResultsContentViewModel.swift */, + 141B3ED424A3C08000BD18C7 /* ResultsContentView.swift */, + 141B3ED324A3C08000BD18C7 /* ResultsItemContentView.swift */, + 141B3ED024A3C08000BD18C7 /* ListFooterView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 14EBF5A424A4F4470040A7C0 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 14EBF59E24A4F43A0040A7C0 /* HostContentViewModel.swift */, + 14EBF58624A4D8260040A7C0 /* HostContentView.swift */, + 14EBF5AB24A4F4780040A7C0 /* HostSectionView.swift */, + 14EBF5A524A4F4680040A7C0 /* PlayerImageView.swift */, + 14EBF59824A4F3E20040A7C0 /* ActivityIndicatorView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 14EBF5B724A5F5800040A7C0 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 14EBF50A24A49BF20040A7C0 /* VotingContentViewModel.swift */, + 14EBF4FC24A49BD10040A7C0 /* VotingContentView.swift */, + 14EBF50424A49BE20040A7C0 /* VotingItemContentView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; 14EF51C6245FAE3600F3653F /* CustomRaceViewController */ = { isa = PBXGroup; children = ( @@ -1308,6 +1531,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 14E0CA3324AE48E50091868E /* WKRKitCore.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = WKRKitCore.framework; + remoteRef = 14E0CA3224AE48E50091868E /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -1346,7 +1576,6 @@ 149FF8571F362B83000A5D96 /* LaunchScreen.storyboard in Resources */, 149FF8541F362B83000A5D96 /* Assets.xcassets in Resources */, 1410DB3B1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */, - 14B8F811222C47FA006C7A06 /* GKMessageImage.png in Resources */, 14A7C9B31F65A9EB00980E4D /* Settings.bundle in Resources */, 14794CA9236C884B00835DC9 /* GoogleService-Info.plist in Resources */, ); @@ -1446,77 +1675,85 @@ files = ( 143BB7181F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */, 143A8BBC1F58746800580AA2 /* PlayerStatsManager.swift in Sources */, + 148E5C4E24A2943100DD43E4 /* Nearby.swift in Sources */, + 143CE07E24A355DC000F6833 /* GKJoinViewController+PublicRace.swift in Sources */, 14C6DA772462900300EC9817 /* CustomRaceController.swift in Sources */, 142F714F210C33FC00C66558 /* MenuViewController+KB.swift in Sources */, + 141B3ED824A3C08000BD18C7 /* ResultsItemContentView.swift in Sources */, 143948BD2144CC0F00992850 /* DebugInfoTableViewController.swift in Sources */, - 14C4AB311F36899B0086B77F /* ResultsViewController+TableView.swift in Sources */, + 14EBF58724A4D8260040A7C0 /* HostContentView.swift in Sources */, 144A5AEC1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */, - 14E1B19A1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */, + 14E1B19A1F7981C70082F4FA /* PlayerFirebaseAnalytics.swift in Sources */, 14C4AB551F3689FE0086B77F /* HistoryTableViewCell.swift in Sources */, - 1429327C242AE29000D64834 /* PointerInteractionTableViewCell.swift in Sources */, + 1475C1B324A6561400882F1F /* VotingViewController+KB.swift in Sources */, 14B2DD3B22212298009B8AB3 /* MenuView.swift in Sources */, - 14B4DB6B2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */, - 14C4AB511F3689E20086B77F /* VotingViewController+TableView.swift in Sources */, - 142F7155210C35BC00C66558 /* VotingViewController+KB.swift in Sources */, 14C4AB611F368A170086B77F /* GameViewController+Manager.swift in Sources */, 142ABDFE24600576008E7F77 /* PlusView.swift in Sources */, - 14C4AB351F3689A40086B77F /* ResultsTableViewCell.swift in Sources */, + 141B3EDB24A3C08000BD18C7 /* ResultsContentView.swift in Sources */, + 14B05D2524AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */, 14C4AB591F368A050086B77F /* HistoryViewController.swift in Sources */, + 1446C1D924A334C2000A0ED3 /* Defaults.swift in Sources */, 14DF31A8211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */, + 14EBF4FD24A49BD10040A7C0 /* VotingContentView.swift in Sources */, 142F7157210C375F00C66558 /* GameViewController+KB.swift in Sources */, - 1414280721FC394600C48788 /* ConnectViewController.swift in Sources */, + 1414280721FC394600C48788 /* GKConnectViewController.swift in Sources */, + 14F0348124A8397A006A908E /* RaceChecksViewController.swift in Sources */, + 148CCBCD24A3410700538F18 /* GKHelper.swift in Sources */, 1478B1B122095360009F2F3F /* ResultRenderer+Creation.swift in Sources */, + 14F2EC9524A64A8F007A4C97 /* GKMatchRequest+WKR.swift in Sources */, 145925DE210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */, - 143BB7351F60DF4A00D00541 /* MPCConnectViewController.swift in Sources */, + 148E5C4824A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */, + 14F034A724A847FF006A908E /* LoadingContentViewModel.swift in Sources */, 14B4DB612224809F007D4B54 /* MovingPuzzleView.swift in Sources */, 142ABE022460057C008E7F77 /* ActivityButton.swift in Sources */, - 14B2DD4C22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */, + 144EC2A524ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */, + 148E5C5424A2967600DD43E4 /* GKHostViewController.swift in Sources */, 14D18CE02460DF9C002E4F5D /* StatsPlayersViewController.swift in Sources */, 14495D8821FF9A0500CAA129 /* ResultRenderer.swift in Sources */, + 14F034AD24A8480D006A908E /* LoadingContentView.swift in Sources */, 141E4CE72200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */, - 142F7153210C34A300C66558 /* MPCHostViewController+KB.swift in Sources */, - 1414280B21FC437000C48788 /* GameKitConnectViewController+Match.swift in Sources */, + 1414280B21FC437000C48788 /* GKJoinViewController+Match.swift in Sources */, 14D18CDC2460CF00002E4F5D /* StatsViewController.swift in Sources */, - 14C4AB851F368AED0086B77F /* VotingTableViewCell.swift in Sources */, - 147EF6F92202436600583D73 /* MPCHostContext.swift in Sources */, 14C4AB391F3689A90086B77F /* MenuTile.swift in Sources */, - 14C6B1F61FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */, + 14EBF50524A49BE20040A7C0 /* VotingItemContentView.swift in Sources */, 144A102A202FC79B003DB51A /* HelpViewController.swift in Sources */, 149FF84D1F362B83000A5D96 /* AppDelegate.swift in Sources */, - 14DFBD1E210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */, 14C4AB5D1F368A0C0086B77F /* GameViewController.swift in Sources */, 14B2DD4522212B96009B8AB3 /* MenuView+Setup.swift in Sources */, + 14EBF59F24A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */, 14C4AB651F368A210086B77F /* GameViewController+UI.swift in Sources */, - 141E4CF122012B2F000A0A15 /* PlayerDatabaseMetrics.swift in Sources */, + 141E4CF122012B2F000A0A15 /* PlayerCloudKitStatsManager.swift in Sources */, 14EF51DB245FAE5600F3653F /* CustomRaceNotificationsController.swift in Sources */, 14EF51DE245FAE5600F3653F /* CustomRaceNumericalViewController.swift in Sources */, 142ABE0624600A74008E7F77 /* PlusStore.swift in Sources */, 142ABDFA24600559008E7F77 /* PlusViewController.swift in Sources */, + 14DB8C9924AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */, + 14EBF50B24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */, 1473E2A0210DAD7C00726377 /* HelpViewController+KB.swift in Sources */, - 1414280321FC18F600C48788 /* GameKitConnectViewController.swift in Sources */, + 1414280321FC18F600C48788 /* GKJoinViewController.swift in Sources */, 14B2DD412221273E009B8AB3 /* MenuView+Actions.swift in Sources */, - 142ABE1224600AF0008E7F77 /* OSLog+Magic.swift in Sources */, + 148E5C6024A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */, 14EF51D5245FAE5600F3653F /* CustomRacePageViewController.swift in Sources */, - 141892F41F60EABC006748F0 /* MPCConnectViewController+Invite.swift in Sources */, 14C4AB2D1F3689900086B77F /* ResultsViewController.swift in Sources */, + 14EBF5AC24A4F4780040A7C0 /* HostSectionView.swift in Sources */, 1485B67D223072AB00D6800B /* MedalView.swift in Sources */, - 14E0F19F22303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */, + 14E0F19F22303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift in Sources */, 14C4AB491F3689C60086B77F /* VotingViewController.swift in Sources */, + 14EBF59924A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */, + 148E5C4224A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */, 149357D9210E801A00F6453A /* HistoryViewController+KB.swift in Sources */, 14EF51D8245FAE5600F3653F /* CustomRaceViewController.swift in Sources */, 1485B6772230724A00D6800B /* MedalScene.swift in Sources */, 14C4AB3D1F3689AE0086B77F /* MenuViewController.swift in Sources */, 142ABE0E24600ABE008E7F77 /* MagicSubscription.swift in Sources */, 1437C51F22285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */, - 149357E3210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */, - 142A09BE210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */, 14EF51D2245FAE5600F3653F /* CustomRaceOtherController.swift in Sources */, - 144A1029202FC79B003DB51A /* CenteredTableViewController.swift in Sources */, - 149357DF210E934300F6453A /* MPCConnectViewController+UI.swift in Sources */, - 141854DF2373666A008C988A /* MPCHostAutoInviteCell.swift in Sources */, + 144A1029202FC79B003DB51A /* VisualEffectViewController.swift in Sources */, 143948C12144CC8C00992850 /* DebugInfoTableViewCell.swift in Sources */, + 141B3ED524A3C08000BD18C7 /* ListFooterView.swift in Sources */, + 14C2AEFF24A3CE9000D2378C /* ResultsContentViewModel.swift in Sources */, + 1446C1E024A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */, 14BA538D21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */, - 14C6B1F41FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1524,75 +1761,88 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 14DF31B721169290005BA432 /* MPCConnectViewController.swift in Sources */, - 14C4AB331F36899B0086B77F /* ResultsViewController+TableView.swift in Sources */, - 142ABE1324600AF0008E7F77 /* OSLog+Magic.swift in Sources */, + 14F034A824A847FF006A908E /* LoadingContentViewModel.swift in Sources */, 14C4AB571F3689FE0086B77F /* HistoryTableViewCell.swift in Sources */, + 141B3EDC24A3C08000BD18C7 /* ResultsContentView.swift in Sources */, 14891AA6214F6BDB001BDEB8 /* DebugInfoTableViewCell.swift in Sources */, - 14E0F1A022303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */, - 14C4AB531F3689E20086B77F /* VotingViewController+TableView.swift in Sources */, - 14DF31B321169232005BA432 /* MPCConnectViewController+KB.swift in Sources */, + 14EBF59A24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */, + 14E0F1A022303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift in Sources */, 14C4AB631F368A170086B77F /* GameViewController+Manager.swift in Sources */, - 1429327D242AE29000D64834 /* PointerInteractionTableViewCell.swift in Sources */, + 14F2EC9624A64A8F007A4C97 /* GKMatchRequest+WKR.swift in Sources */, 141E4CE82200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */, 143BB7151F60AEC900D00541 /* PlayerStatsManager.swift in Sources */, 143BB7191F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */, + 14EBF5AD24A4F4780040A7C0 /* HostSectionView.swift in Sources */, 14EF51D9245FAE5600F3653F /* CustomRaceViewController.swift in Sources */, + 148E5C4324A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */, + 144EC2A624ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */, + 148E5C6124A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */, 14DF31A9211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */, 145925DF210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */, 149357DA210E801A00F6453A /* HistoryViewController+KB.swift in Sources */, + 14EBF5A024A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */, + 14DB8C9A24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */, 1485B6782230724A00D6800B /* MedalScene.swift in Sources */, 142ABE032460057C008E7F77 /* ActivityButton.swift in Sources */, 14EF51DF245FAE5600F3653F /* CustomRaceNumericalViewController.swift in Sources */, - 14C4AB371F3689A40086B77F /* ResultsTableViewCell.swift in Sources */, - 14DF31B62116923C005BA432 /* MPCHostViewController.swift in Sources */, 142ABDFF24600576008E7F77 /* PlusView.swift in Sources */, 14B2DD4622212B96009B8AB3 /* MenuView+Setup.swift in Sources */, - 14B4DB662224F1B9007D4B54 /* GameKitConnectViewController.swift in Sources */, + 14B4DB662224F1B9007D4B54 /* GKJoinViewController.swift in Sources */, 14C4AB5B1F368A050086B77F /* HistoryViewController.swift in Sources */, - 1473F38D24650B6800F939B0 /* MPCHostAutoInviteCell.swift in Sources */, - 14DD97282202294D00AAB389 /* PlayerDatabaseMetrics.swift in Sources */, + 1475C14624A655B000882F1F /* GameViewController+KB.swift in Sources */, + 1446C1E124A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */, + 14DD97282202294D00AAB389 /* PlayerCloudKitStatsManager.swift in Sources */, 149FF87D1F362BE4000A5D96 /* ViewController.swift in Sources */, + 14EBF50624A49BE20040A7C0 /* VotingItemContentView.swift in Sources */, 142ABE0724600A74008E7F77 /* PlusStore.swift in Sources */, + 14F0348224A8397A006A908E /* RaceChecksViewController.swift in Sources */, 14B2DD422221273E009B8AB3 /* MenuView+Actions.swift in Sources */, + 148E5C4F24A2943100DD43E4 /* Nearby.swift in Sources */, + 14C2AF0024A3CE9000D2378C /* ResultsContentViewModel.swift in Sources */, 142ABDFB2460055B008E7F77 /* PlusViewController.swift in Sources */, - 14B4DB682224F1BC007D4B54 /* GameKitConnectViewController+Match.swift in Sources */, - 14DF31B421169236005BA432 /* MPCConnectViewController+UI.swift in Sources */, + 14B05D2624AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */, + 14B4DB682224F1BC007D4B54 /* GKJoinViewController+Match.swift in Sources */, 14495D8921FF9A0500CAA129 /* ResultRenderer.swift in Sources */, 14EF51DC245FAE5600F3653F /* CustomRaceNotificationsController.swift in Sources */, + 1475C18124A655F100882F1F /* StatsViewController.swift in Sources */, + 1475C18224A655F100882F1F /* StatsPlayersViewController.swift in Sources */, + 14EBF50C24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */, 142ABE0F24600ABE008E7F77 /* MagicSubscription.swift in Sources */, 14C6DA782462900300EC9817 /* CustomRaceController.swift in Sources */, - 147EF6FA2202436600583D73 /* MPCHostContext.swift in Sources */, - 14E1B19B1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */, - 14C4AB871F368AED0086B77F /* VotingTableViewCell.swift in Sources */, + 14E1B19B1F7981C70082F4FA /* PlayerFirebaseAnalytics.swift in Sources */, + 1475C17024A655DC00882F1F /* MenuViewController+KB.swift in Sources */, + 148E5C5524A2967600DD43E4 /* GKHostViewController.swift in Sources */, 14B2DD3C22212298009B8AB3 /* MenuView.swift in Sources */, 1478B1B222095360009F2F3F /* ResultRenderer+Creation.swift in Sources */, + 14EBF58824A4D8260040A7C0 /* HostContentView.swift in Sources */, 14EF51D3245FAE5600F3653F /* CustomRaceOtherController.swift in Sources */, 1437C52022285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */, - 14B4DB6C2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */, + 14F034AE24A8480D006A908E /* LoadingContentView.swift in Sources */, + 148CCBCE24A3410700538F18 /* GKHelper.swift in Sources */, 14BA538E21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */, - 14DD97252202293900AAB389 /* ConnectViewController.swift in Sources */, + 14DD97252202293900AAB389 /* GKConnectViewController.swift in Sources */, + 1446C1DA24A334C2000A0ED3 /* Defaults.swift in Sources */, 14EF51D6245FAE5600F3653F /* CustomRacePageViewController.swift in Sources */, + 14EBF4FE24A49BD10040A7C0 /* VotingContentView.swift in Sources */, 14C4AB3B1F3689A90086B77F /* MenuTile.swift in Sources */, + 143CE07F24A355DC000F6833 /* GKJoinViewController+PublicRace.swift in Sources */, 149FF87B1F362BE4000A5D96 /* AppDelegate.swift in Sources */, 14C4AB5F1F368A0C0086B77F /* GameViewController.swift in Sources */, 1485B67E223072AB00D6800B /* MedalView.swift in Sources */, - 149357E4210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */, 14C4AB671F368A210086B77F /* GameViewController+UI.swift in Sources */, 14B55C991F3A49D20090E092 /* DebugWindow.swift in Sources */, - 14D18CE12460DF9C002E4F5D /* StatsPlayersViewController.swift in Sources */, + 141B3ED624A3C08000BD18C7 /* ListFooterView.swift in Sources */, 144A5AED1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */, - 144A102B202FC7A1003DB51A /* CenteredTableViewController.swift in Sources */, + 144A102B202FC7A1003DB51A /* VisualEffectViewController.swift in Sources */, 14B4DB622224809F007D4B54 /* MovingPuzzleView.swift in Sources */, - 14D18CDD2460CF00002E4F5D /* StatsViewController.swift in Sources */, + 141B3ED924A3C08000BD18C7 /* ResultsItemContentView.swift in Sources */, 14C4AB2F1F3689900086B77F /* ResultsViewController.swift in Sources */, 14C4AB4B1F3689C60086B77F /* VotingViewController.swift in Sources */, - 14DF31B521169239005BA432 /* MPCHostViewController+Table.swift in Sources */, - 14DF31BA21169371005BA432 /* MPCConnectViewController+Invite.swift in Sources */, 14C4AB3F1F3689AE0086B77F /* MenuViewController.swift in Sources */, - 14B2DD4D22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */, - 14DF31B921169360005BA432 /* MPCHostSearchingCell.swift in Sources */, + 1475C19124A6560000882F1F /* HelpViewController+KB.swift in Sources */, + 148E5C4924A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */, 14891AA9214F6BDE001BDEB8 /* DebugInfoTableViewController.swift in Sources */, + 1475C1B224A6561400882F1F /* VotingViewController+KB.swift in Sources */, 144A102C202FC7A1003DB51A /* HelpViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1601,8 +1851,87 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1446C1DB24A334C2000A0ED3 /* Defaults.swift in Sources */, + 1475C19224A6560000882F1F /* HelpViewController+KB.swift in Sources */, + 14EBF5AE24A4F4780040A7C0 /* HostSectionView.swift in Sources */, + 1475C17424A655E900882F1F /* GKJoinViewController.swift in Sources */, + 1475C15D24A655D800882F1F /* MenuView.swift in Sources */, + 148E5C5624A2967600DD43E4 /* GKHostViewController.swift in Sources */, + 14EBF50724A49BE20040A7C0 /* VotingItemContentView.swift in Sources */, + 1475C19B24A6560800882F1F /* ResultsViewController.swift in Sources */, + 1475C17F24A655F100882F1F /* StatsViewController.swift in Sources */, + 1475C17A24A655EC00882F1F /* CustomRaceNumericalViewController.swift in Sources */, + 14EBF50D24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */, 149FF8911F362BF1000A5D96 /* WikiRacesScreenshots.swift in Sources */, + 1475C16F24A655DC00882F1F /* MenuViewController+KB.swift in Sources */, + 1475C1AC24A6560E00882F1F /* HistoryTableViewStatsCell.swift in Sources */, + 1475C1B524A6561A00882F1F /* PlayerCloudKitStatsManager.swift in Sources */, + 14F034A924A847FF006A908E /* LoadingContentViewModel.swift in Sources */, + 1475C16024A655D800882F1F /* MedalView.swift in Sources */, + 148E5C4424A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */, + 1475C18B24A655F600882F1F /* MagicSubscription.swift in Sources */, + 1475C17924A655EC00882F1F /* CustomRaceNotificationsController.swift in Sources */, + 1475C1B024A6561400882F1F /* VotingViewController.swift in Sources */, + 1475C19F24A6560800882F1F /* ResultRenderer.swift in Sources */, + 1475C18824A655F600882F1F /* PlusView.swift in Sources */, + 1475C1AA24A6560E00882F1F /* HistoryViewController+KB.swift in Sources */, + 1475C14124A6557800882F1F /* GameViewController.swift in Sources */, + 1475C15224A655CF00882F1F /* WKRAnimationDurationConstants.swift in Sources */, + 1446C1E224A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */, + 148E5C4A24A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */, + 1475C15F24A655D800882F1F /* MenuView+Actions.swift in Sources */, + 1475C16C24A655DC00882F1F /* MenuViewController.swift in Sources */, + 1475C17524A655E900882F1F /* GKJoinViewController+Match.swift in Sources */, + 1475C18A24A655F600882F1F /* ActivityButton.swift in Sources */, + 14EBF5A124A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */, + 144EC2A724ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */, + 14EBF58924A4D8260040A7C0 /* HostContentView.swift in Sources */, + 1475C15C24A655D800882F1F /* MenuTile.swift in Sources */, + 1475C15E24A655D800882F1F /* MenuView+Setup.swift in Sources */, + 1475C16224A655D800882F1F /* MovingPuzzleView.swift in Sources */, + 1475C15024A655C800882F1F /* HelpViewController.swift in Sources */, + 1475C14924A655B100882F1F /* GameViewController+KB.swift in Sources */, + 148E5C5024A2943100DD43E4 /* Nearby.swift in Sources */, + 14EBF59B24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */, 147593D11F609911005DFC90 /* SnapshotHelper.swift in Sources */, + 1475C18924A655F600882F1F /* PlusStore.swift in Sources */, + 1475C17624A655E900882F1F /* GKJoinViewController+PublicRace.swift in Sources */, + 1475C16D24A655DC00882F1F /* MenuViewController+Debug.swift in Sources */, + 1475C17724A655EC00882F1F /* CustomRaceController.swift in Sources */, + 1475C16124A655D800882F1F /* MedalScene.swift in Sources */, + 1475C13524A6554000882F1F /* ResultsContentView.swift in Sources */, + 1475C17B24A655EC00882F1F /* CustomRaceOtherController.swift in Sources */, + 14DB8C9B24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */, + 1475C13924A6555B00882F1F /* VisualEffectViewController.swift in Sources */, + 1475C19E24A6560800882F1F /* ResultsViewController+KB.swift in Sources */, + 1475C14824A655B100882F1F /* GameViewController+Manager.swift in Sources */, + 1475C18024A655F100882F1F /* StatsPlayersViewController.swift in Sources */, + 1475C13D24A6556A00882F1F /* PlayerFirebaseAnalytics.swift in Sources */, + 1475C1AB24A6560E00882F1F /* HistoryTableViewCell.swift in Sources */, + 1475C1B824A6571200882F1F /* GKHelper.swift in Sources */, + 1475C17C24A655EC00882F1F /* CustomRacePageViewController.swift in Sources */, + 1475C13424A6554000882F1F /* ResultsContentViewModel.swift in Sources */, + 1475C18F24A655FC00882F1F /* DebugInfoTableViewController.swift in Sources */, + 1475C13F24A6557200882F1F /* GKMatchRequest+WKR.swift in Sources */, + 1475C1B424A6561A00882F1F /* PlayerUserDefaultsStat.swift in Sources */, + 1475C19C24A6560800882F1F /* ResultsViewController+Actions.swift in Sources */, + 1475C13624A6554000882F1F /* ResultsItemContentView.swift in Sources */, + 1475C1A924A6560E00882F1F /* HistoryViewController.swift in Sources */, + 1475C16E24A655DC00882F1F /* MenuViewController+GameKit.swift in Sources */, + 1475C13724A6554000882F1F /* ListFooterView.swift in Sources */, + 1475C14424A6559500882F1F /* PlusViewController.swift in Sources */, + 1475C14724A655B100882F1F /* GameViewController+UI.swift in Sources */, + 1475C14E24A655C100882F1F /* PlayerStatsManager.swift in Sources */, + 1475C13B24A6556300882F1F /* GKConnectViewController.swift in Sources */, + 14F0348324A8397A006A908E /* RaceChecksViewController.swift in Sources */, + 14EBF4FF24A49BD10040A7C0 /* VotingContentView.swift in Sources */, + 1475C17824A655EC00882F1F /* CustomRaceViewController.swift in Sources */, + 1475C19024A655FC00882F1F /* DebugInfoTableViewCell.swift in Sources */, + 14F034AF24A8480D006A908E /* LoadingContentView.swift in Sources */, + 14B05D2724AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */, + 148E5C6224A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */, + 1475C1A024A6560800882F1F /* ResultRenderer+Creation.swift in Sources */, + 1475C1B124A6561400882F1F /* VotingViewController+KB.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1611,75 +1940,88 @@ buildActionMask = 2147483647; files = ( 14C25FE21F6F025A00CD7373 /* ViewController.swift in Sources */, - 147EF7122202D76000583D73 /* MPCHostContext.swift in Sources */, + 14F034AA24A847FF006A908E /* LoadingContentViewModel.swift in Sources */, 14D18CE22460DF9C002E4F5D /* StatsPlayersViewController.swift in Sources */, 145925E0210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */, - 14DF31AA211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */, + 141B3EDD24A3C08000BD18C7 /* ResultsContentView.swift in Sources */, + 14EBF59C24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */, + 1475C16824A655DC00882F1F /* MenuViewController.swift in Sources */, 142ABDFC2460055C008E7F77 /* PlusViewController.swift in Sources */, - 14E0F1A122303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */, - 149357DB210E801A00F6453A /* HistoryViewController+KB.swift in Sources */, + 14E0F1A122303A8F00BFF1E9 /* PlayerUserDefaultsStat.swift in Sources */, + 1475C15324A655D000882F1F /* WKRAnimationDurationConstants.swift in Sources */, + 14F2EC9724A64A8F007A4C97 /* GKMatchRequest+WKR.swift in Sources */, 14C25FFC1F6F02A500CD7373 /* GameViewController+UI.swift in Sources */, - 14DF31B821169291005BA432 /* MPCConnectViewController.swift in Sources */, 14C25FF11F6F028100CD7373 /* MenuTile.swift in Sources */, 14C25FFE1F6F02A500CD7373 /* GameViewController+Manager.swift in Sources */, + 14EBF5AF24A4F4780040A7C0 /* HostSectionView.swift in Sources */, 14495D8A21FF9A0500CAA129 /* ResultRenderer.swift in Sources */, + 144EC2A824ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */, 14C260071F6F02A500CD7373 /* PlayerStatsManager.swift in Sources */, - 14C260011F6F02A500CD7373 /* ResultsTableViewCell.swift in Sources */, + 148E5C4524A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */, + 148E5C6324A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */, 1485B6792230724A00D6800B /* MedalScene.swift in Sources */, 14C25FFB1F6F02A500CD7373 /* GameViewController.swift in Sources */, + 14EBF5A224A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */, + 14DB8C9C24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */, 14B2DD4722212B96009B8AB3 /* MenuView+Setup.swift in Sources */, + 1475C1A124A6560D00882F1F /* HistoryViewController.swift in Sources */, 14C25FFF1F6F02A500CD7373 /* ResultsViewController.swift in Sources */, - 14B4DB672224F1BA007D4B54 /* GameKitConnectViewController.swift in Sources */, + 14B4DB672224F1BA007D4B54 /* GKJoinViewController.swift in Sources */, + 1475C1A324A6560D00882F1F /* HistoryTableViewCell.swift in Sources */, 14EF51D7245FAE5600F3653F /* CustomRacePageViewController.swift in Sources */, 14B2DD432221273E009B8AB3 /* MenuView+Actions.swift in Sources */, - 14BA538F21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */, 14EF51E0245FAE5600F3653F /* CustomRaceNumericalViewController.swift in Sources */, - 149357E5210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */, + 1475C14A24A655B100882F1F /* GameViewController+KB.swift in Sources */, 14C6DA792462900300EC9817 /* CustomRaceController.swift in Sources */, 14EF51DD245FAE5600F3653F /* CustomRaceNotificationsController.swift in Sources */, - 14DD97292202295100AAB389 /* PlayerDatabaseMetrics.swift in Sources */, - 14B4DB692224F1BD007D4B54 /* GameKitConnectViewController+Match.swift in Sources */, - 142A09C0210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */, + 1446C1E324A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */, + 14DD97292202295100AAB389 /* PlayerCloudKitStatsManager.swift in Sources */, + 14F0348424A8397A006A908E /* RaceChecksViewController.swift in Sources */, + 14EBF50824A49BE20040A7C0 /* VotingItemContentView.swift in Sources */, + 14B4DB692224F1BD007D4B54 /* GKJoinViewController+Match.swift in Sources */, 14EF51DA245FAE5600F3653F /* CustomRaceViewController.swift in Sources */, + 14B05D2824AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */, + 148E5C5124A2943100DD43E4 /* Nearby.swift in Sources */, + 14C2AF0124A3CE9000D2378C /* ResultsContentViewModel.swift in Sources */, 14C25FE01F6F025A00CD7373 /* AppDelegate.swift in Sources */, - 14DFBD20210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */, - 142ABE1424600AF0008E7F77 /* OSLog+Magic.swift in Sources */, 14C2600A1F6F02A500CD7373 /* WKRAppDelegate.swift in Sources */, - 1429327E242AE29000D64834 /* PointerInteractionTableViewCell.swift in Sources */, + 14EBF50E24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */, 14B2DD3D22212298009B8AB3 /* MenuView.swift in Sources */, 1480F6C621FB77DE00081F58 /* DebugInfoTableViewCell.swift in Sources */, + 1475C16B24A655DC00882F1F /* MenuViewController+KB.swift in Sources */, 1480F6C321FB77D300081F58 /* DebugInfoTableViewController.swift in Sources */, + 1475C1A424A6560D00882F1F /* HistoryTableViewStatsCell.swift in Sources */, 14D8AD481F81835700914E5A /* DebugWindow.swift in Sources */, - 1437C52122285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */, + 148E5C5724A2967600DD43E4 /* GKHostViewController.swift in Sources */, 142ABE1024600ABE008E7F77 /* MagicSubscription.swift in Sources */, - 14B4DB6D2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */, + 14EBF58A24A4D8260040A7C0 /* HostContentView.swift in Sources */, 1478B1B322095360009F2F3F /* ResultRenderer+Creation.swift in Sources */, + 14F034B024A8480D006A908E /* LoadingContentView.swift in Sources */, 14EF51D4245FAE5600F3653F /* CustomRaceOtherController.swift in Sources */, - 14C260031F6F02A500CD7373 /* HistoryViewController.swift in Sources */, + 148CCBCF24A3410700538F18 /* GKHelper.swift in Sources */, 141E4CE92200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */, - 14C6B1F71FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */, - 14C260021F6F02A500CD7373 /* HistoryTableViewCell.swift in Sources */, + 1446C1DC24A334C2000A0ED3 /* Defaults.swift in Sources */, + 14EBF50024A49BD10040A7C0 /* VotingContentView.swift in Sources */, + 1475C16924A655DC00882F1F /* MenuViewController+Debug.swift in Sources */, 1485B67F223072AB00D6800B /* MedalView.swift in Sources */, - 149357E1210E934300F6453A /* MPCConnectViewController+UI.swift in Sources */, + 1475C1A224A6560D00882F1F /* HistoryViewController+KB.swift in Sources */, + 1475C16A24A655DC00882F1F /* MenuViewController+GameKit.swift in Sources */, 144A102E202FC7A2003DB51A /* HelpViewController.swift in Sources */, - 14D8AD471F81828D00914E5A /* PlayerAnonymousMetrics.swift in Sources */, - 14C25FF21F6F028100CD7373 /* MenuViewController.swift in Sources */, + 143CE08024A355DC000F6833 /* GKJoinViewController+PublicRace.swift in Sources */, + 14D8AD471F81828D00914E5A /* PlayerFirebaseAnalytics.swift in Sources */, 142ABE0824600A74008E7F77 /* PlusStore.swift in Sources */, - 14C260001F6F02A500CD7373 /* ResultsViewController+TableView.swift in Sources */, 14B4DB632224809F007D4B54 /* MovingPuzzleView.swift in Sources */, 14C260041F6F02A500CD7373 /* VotingViewController.swift in Sources */, + 141B3ED724A3C08000BD18C7 /* ListFooterView.swift in Sources */, 142ABE0024600576008E7F77 /* PlusView.swift in Sources */, - 14C260051F6F02A500CD7373 /* VotingViewController+TableView.swift in Sources */, - 14C25FF51F6F028100CD7373 /* MenuViewController+GameKit.swift in Sources */, 142ABE042460057C008E7F77 /* ActivityButton.swift in Sources */, - 14C260061F6F02A500CD7373 /* VotingTableViewCell.swift in Sources */, - 14DF31BB21169372005BA432 /* MPCConnectViewController+Invite.swift in Sources */, + 141B3EDA24A3C08000BD18C7 /* ResultsItemContentView.swift in Sources */, 14D18CDE2460CF00002E4F5D /* StatsViewController.swift in Sources */, - 14B2DD4E22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */, - 143FC31C2401BB0900AB313A /* MPCHostAutoInviteCell.swift in Sources */, - 144A102D202FC7A2003DB51A /* CenteredTableViewController.swift in Sources */, - 14BA538921FE3B1400A8CB01 /* ConnectViewController.swift in Sources */, - 14C6B1F51FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */, + 1475C19324A6560000882F1F /* HelpViewController+KB.swift in Sources */, + 148E5C4B24A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */, + 144A102D202FC7A2003DB51A /* VisualEffectViewController.swift in Sources */, + 1475C1AF24A6561300882F1F /* VotingViewController+KB.swift in Sources */, + 14BA538921FE3B1400A8CB01 /* GKConnectViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1687,7 +2029,86 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 148E5C4C24A293EB00DD43E4 /* NearbyRaceListener.swift in Sources */, + 1475C14F24A655C100882F1F /* PlayerStatsManager.swift in Sources */, + 1475C1AD24A6561200882F1F /* VotingViewController.swift in Sources */, + 1475C16324A655DB00882F1F /* MenuViewController.swift in Sources */, + 1475C15824A655D700882F1F /* MenuView+Actions.swift in Sources */, + 1446C1E424A337DD000A0ED3 /* GKHostViewController+Match.swift in Sources */, + 1475C15524A655D700882F1F /* MenuTile.swift in Sources */, + 14F034B124A8480D006A908E /* LoadingContentView.swift in Sources */, + 1475C15B24A655D700882F1F /* MovingPuzzleView.swift in Sources */, + 1475C18624A655F500882F1F /* MagicSubscription.swift in Sources */, + 1475C18524A655F500882F1F /* ActivityButton.swift in Sources */, + 1475C18324A655F500882F1F /* PlusView.swift in Sources */, + 1475C19524A6560700882F1F /* ResultsViewController.swift in Sources */, + 14EBF58B24A4D8260040A7C0 /* HostContentView.swift in Sources */, + 1475C1B624A6561B00882F1F /* PlayerUserDefaultsStat.swift in Sources */, + 1475C13C24A6556900882F1F /* PlayerFirebaseAnalytics.swift in Sources */, + 1475C1A524A6560D00882F1F /* HistoryViewController.swift in Sources */, + 1475C19924A6560700882F1F /* ResultRenderer.swift in Sources */, + 1475C12D24A6551F00882F1F /* CustomRacePageViewController.swift in Sources */, + 1475C1AE24A6561200882F1F /* VotingViewController+KB.swift in Sources */, + 1475C14024A6557800882F1F /* GameViewController.swift in Sources */, + 14F0348524A8397A006A908E /* RaceChecksViewController.swift in Sources */, + 1446C1DD24A334C2000A0ED3 /* Defaults.swift in Sources */, 14E0F1AB22303FD100BFF1E9 /* WikiRacesTests.swift in Sources */, + 14EBF50124A49BD10040A7C0 /* VotingContentView.swift in Sources */, + 14EBF59D24A4F3E20040A7C0 /* ActivityIndicatorView.swift in Sources */, + 1475C1B924A6571300882F1F /* GKHelper.swift in Sources */, + 1475C1A724A6560D00882F1F /* HistoryTableViewCell.swift in Sources */, + 14DB8C9D24AC2A8400D356DF /* OSLog+WikiRaces.swift in Sources */, + 1475C17224A655E800882F1F /* GKJoinViewController+Match.swift in Sources */, + 1475C17D24A655F000882F1F /* StatsViewController.swift in Sources */, + 1475C1A624A6560D00882F1F /* HistoryViewController+KB.swift in Sources */, + 1475C16524A655DB00882F1F /* MenuViewController+GameKit.swift in Sources */, + 1475C17E24A655F000882F1F /* StatsPlayersViewController.swift in Sources */, + 148E5C6424A2AE7D00DD43E4 /* RaceCodeGenerator.swift in Sources */, + 148E5C4624A293DD00DD43E4 /* NearbyRaceAdvertiser.swift in Sources */, + 1475C13124A6553F00882F1F /* ResultsContentView.swift in Sources */, + 148E5C5224A2943100DD43E4 /* Nearby.swift in Sources */, + 14EBF50F24A49BF20040A7C0 /* VotingContentViewModel.swift in Sources */, + 1475C14D24A655B200882F1F /* GameViewController+KB.swift in Sources */, + 1475C15124A655C900882F1F /* HelpViewController.swift in Sources */, + 1475C15924A655D700882F1F /* MedalView.swift in Sources */, + 1475C19A24A6560700882F1F /* ResultRenderer+Creation.swift in Sources */, + 14EBF5A324A4F43A0040A7C0 /* HostContentViewModel.swift in Sources */, + 14F034AB24A847FF006A908E /* LoadingContentViewModel.swift in Sources */, + 1475C19824A6560700882F1F /* ResultsViewController+KB.swift in Sources */, + 144EC2A924ADA93500F0C315 /* BackingVisualEffectViewController.swift in Sources */, + 1475C13024A6553F00882F1F /* ResultsContentViewModel.swift in Sources */, + 1475C19624A6560700882F1F /* ResultsViewController+Actions.swift in Sources */, + 1475C17124A655E800882F1F /* GKJoinViewController.swift in Sources */, + 1475C12C24A6551F00882F1F /* CustomRaceOtherController.swift in Sources */, + 1475C14B24A655B200882F1F /* GameViewController+UI.swift in Sources */, + 1475C13A24A6556200882F1F /* GKConnectViewController.swift in Sources */, + 1475C14524A6559500882F1F /* PlusViewController.swift in Sources */, + 1475C18D24A655FB00882F1F /* DebugInfoTableViewController.swift in Sources */, + 1475C15A24A655D700882F1F /* MedalScene.swift in Sources */, + 1475C12924A6551F00882F1F /* CustomRaceViewController.swift in Sources */, + 1475C16624A655DB00882F1F /* MenuViewController+KB.swift in Sources */, + 1475C1A824A6560D00882F1F /* HistoryTableViewStatsCell.swift in Sources */, + 1475C1B724A6561B00882F1F /* PlayerCloudKitStatsManager.swift in Sources */, + 1475C12824A6551F00882F1F /* CustomRaceController.swift in Sources */, + 1475C18E24A655FB00882F1F /* DebugInfoTableViewCell.swift in Sources */, + 1475C12B24A6551F00882F1F /* CustomRaceNumericalViewController.swift in Sources */, + 14EBF50924A49BE20040A7C0 /* VotingItemContentView.swift in Sources */, + 1475C17324A655E800882F1F /* GKJoinViewController+PublicRace.swift in Sources */, + 1475C12A24A6551F00882F1F /* CustomRaceNotificationsController.swift in Sources */, + 1475C18424A655F500882F1F /* PlusStore.swift in Sources */, + 14EBF5B024A4F4780040A7C0 /* HostSectionView.swift in Sources */, + 1475C15724A655D700882F1F /* MenuView+Setup.swift in Sources */, + 1475C15424A655D000882F1F /* WKRAnimationDurationConstants.swift in Sources */, + 14B05D2924AD542D0010FA07 /* PlayerCloudKitLiveRaceManager.swift in Sources */, + 1475C13324A6553F00882F1F /* ListFooterView.swift in Sources */, + 1475C16424A655DB00882F1F /* MenuViewController+Debug.swift in Sources */, + 1475C13224A6553F00882F1F /* ResultsItemContentView.swift in Sources */, + 1475C15624A655D700882F1F /* MenuView.swift in Sources */, + 1475C13E24A6557100882F1F /* GKMatchRequest+WKR.swift in Sources */, + 1475C14C24A655B200882F1F /* GameViewController+Manager.swift in Sources */, + 1475C13824A6555A00882F1F /* VisualEffectViewController.swift in Sources */, + 148E5C5824A2967600DD43E4 /* GKHostViewController.swift in Sources */, + 1475C19424A6560100882F1F /* HelpViewController+KB.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1906,7 +2327,6 @@ CODE_SIGN_ENTITLEMENTS = WikiRaces/WikiRaces.entitlements; CURRENT_PROJECT_VERSION = 6344; DEFINES_MODULE = YES; - DERIVE_UIKITFORMAC_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = 72S993BNAV; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; @@ -1916,16 +2336,17 @@ ); HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Shared/Frameworks/Analytics"; INFOPLIST_FILE = WikiRaces/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2020.05.2; - OTHER_LDFLAGS = " -ObjC"; + MARKETING_VERSION = 2020.07; + OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=300 -Xfrontend -warn-long-expression-type-checking=150"; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.wikiraces; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; SUPPORTS_UIKITFORMAC = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; @@ -1941,7 +2362,6 @@ CODE_SIGN_ENTITLEMENTS = WikiRaces/WikiRaces.entitlements; CURRENT_PROJECT_VERSION = 6344; DEFINES_MODULE = YES; - DERIVE_UIKITFORMAC_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = 72S993BNAV; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; @@ -1951,15 +2371,16 @@ ); HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Shared/Frameworks/Analytics"; INFOPLIST_FILE = WikiRaces/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2020.05.2; - OTHER_LDFLAGS = " -ObjC"; + MARKETING_VERSION = 2020.07; + OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.wikiraces; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; SUPPORTS_UIKITFORMAC = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; @@ -1979,7 +2400,7 @@ "$(PROJECT_DIR)/Shared/Frameworks/Analytics", ); INFOPLIST_FILE = "WikiRaces (Multi-Window)/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2004,7 +2425,7 @@ "$(PROJECT_DIR)/Shared/Frameworks/Analytics", ); INFOPLIST_FILE = "WikiRaces (Multi-Window)/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2021,6 +2442,10 @@ isa = XCBuildConfiguration; buildSettings = { DEVELOPMENT_TEAM = 72S993BNAV; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Shared/Frameworks/Analytics", + ); INFOPLIST_FILE = WikiRacesScreenshots/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2039,6 +2464,10 @@ isa = XCBuildConfiguration; buildSettings = { DEVELOPMENT_TEAM = 72S993BNAV; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Shared/Frameworks/Analytics", + ); INFOPLIST_FILE = WikiRacesScreenshots/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2067,7 +2496,7 @@ "$(PROJECT_DIR)/Shared/Frameworks/Analytics", ); INFOPLIST_FILE = "WikiRaces (UI Catalog)/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2093,7 +2522,7 @@ "$(PROJECT_DIR)/Shared/Frameworks/Analytics", ); INFOPLIST_FILE = "WikiRaces (UI Catalog)/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2114,8 +2543,12 @@ CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 72S993BNAV; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Shared/Frameworks/Analytics", + ); INFOPLIST_FILE = WikiRacesTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2138,8 +2571,12 @@ CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 72S993BNAV; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Shared/Frameworks/Analytics", + ); INFOPLIST_FILE = WikiRacesTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme index b19d17d..8928596 100644 --- a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme +++ b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme @@ -1,6 +1,6 @@ - - - - @@ -65,6 +56,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" enableASanStackUseAfterReturn = "YES" + disableMainThreadChecker = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesAllTests.xcscheme b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesAllTests.xcscheme index 685a3b1..d3369c6 100644 --- a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesAllTests.xcscheme +++ b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesAllTests.xcscheme @@ -1,6 +1,6 @@ - + - - + @@ -16,15 +15,6 @@ - @@ -35,8 +25,6 @@ - - diff --git a/WikiRaces/WikiRaces/GKMessageImage.png b/WikiRaces/WikiRaces/GKMessageImage.png deleted file mode 100644 index 4543b60..0000000 Binary files a/WikiRaces/WikiRaces/GKMessageImage.png and /dev/null differ diff --git a/WikiRaces/WikiRaces/GoogleService-Info.plist b/WikiRaces/WikiRaces/GoogleService-Info.plist index 7e3a6fc..d037598 100644 --- a/WikiRaces/WikiRaces/GoogleService-Info.plist +++ b/WikiRaces/WikiRaces/GoogleService-Info.plist @@ -19,18 +19,18 @@ STORAGE_BUCKET wikiraces-5b7dc.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID 1:955248951790:ios:b82db689d6855c14 DATABASE_URL https://wikiraces-5b7dc.firebaseio.com - + \ No newline at end of file diff --git a/WikiRaces/WikiRaces/Info.plist b/WikiRaces/WikiRaces/Info.plist index e005976..211b80d 100644 --- a/WikiRaces/WikiRaces/Info.plist +++ b/WikiRaces/WikiRaces/Info.plist @@ -16,22 +16,21 @@ APPL CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.andrewfinke.WikiRaces + CFBundleURLSchemes + + WikiRaces + + + CFBundleVersion - 7028 - Fabric - - APIKey - 80c3b2d37f1bca4e182e7fbf7976e6f069340b4d - Kits - - - KitInfo - - KitName - Crashlytics - - - + 8768 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/WikiRaces/WikiRacesTests/WikiRacesTests.swift b/WikiRaces/WikiRacesTests/WikiRacesTests.swift index 3a8b38f..7b6c25b 100644 --- a/WikiRaces/WikiRacesTests/WikiRacesTests.swift +++ b/WikiRaces/WikiRacesTests/WikiRacesTests.swift @@ -29,23 +29,10 @@ class WikiRacesTests: XCTestCase { } } - func testMenuStats() { - let menuView = MenuView() - - var stat = PlayerDatabaseStat.mpcPressedJoin - var value = stat.value() - menuView.joinLocalRace() - XCTAssertEqual(value + 1, stat.value()) - - stat = PlayerDatabaseStat.mpcPressedHost - value = stat.value() - menuView.createLocalRace() - XCTAssertEqual(value + 1, stat.value()) - - stat = PlayerDatabaseStat.gkPressedJoin - value = stat.value() - menuView.joinGlobalRace() - XCTAssertEqual(value + 1, stat.value()) + func testRaceCodePlayerGroupGeneration() { + for code in RaceCodeGenerator.codes { + _ = RaceCodeGenerator.playerGroup(for: code) + } } func testViewedPage() { @@ -54,12 +41,12 @@ class WikiRacesTests: XCTestCase { XCTFail("race type nil") return } - let pageStat: PlayerDatabaseStat + let pageStat: PlayerUserDefaultsStat switch raceType { - case .mpc: + case .private: pageStat = .mpcPages - case .gameKit: + case .public: pageStat = .gkPages case .solo: pageStat = .soloPages @@ -79,14 +66,14 @@ class WikiRacesTests: XCTestCase { } let playersKey: String - var uniqueStat: PlayerDatabaseStat - var totalStat: PlayerDatabaseStat + var uniqueStat: PlayerUserDefaultsStat + var totalStat: PlayerUserDefaultsStat switch raceType { - case .mpc: + case .private: playersKey = "PlayersArray" uniqueStat = .mpcUniquePlayers totalStat = .mpcTotalPlayers - case .gameKit: + case .public: playersKey = "GKPlayersArray" uniqueStat = .gkUniquePlayers totalStat = .gkTotalPlayers @@ -111,12 +98,12 @@ class WikiRacesTests: XCTestCase { return } - let raceFastestTimeStat: PlayerDatabaseStat + let raceFastestTimeStat: PlayerUserDefaultsStat switch raceType { - case .mpc: + case .private: raceFastestTimeStat = .mpcFastestTime - case .gameKit: + case .public: raceFastestTimeStat = .gkFastestTime case .solo: raceFastestTimeStat = .soloFastestTime @@ -143,7 +130,7 @@ class WikiRacesTests: XCTestCase { } func testRaceCompletionStats() { - var testedStats = Set() + var testedStats = Set() for raceIndex in 0..<600 { guard let raceType = PlayerStatsManager.RaceType(rawValue: (raceIndex % 3) + 1) else { XCTFail("race type nil") @@ -155,15 +142,15 @@ class WikiRacesTests: XCTestCase { let newTimeRaced = Double(Int.random(in: 0...100)) let newPixelsScrolled = Double(Int.random(in: 0...100000)) - let raceCountStat: PlayerDatabaseStat - let racePointsStat: PlayerDatabaseStat - let racePlaceStat: PlayerDatabaseStat - let raceTimeStat: PlayerDatabaseStat - let racePixelsScrolledStat: PlayerDatabaseStat + let raceCountStat: PlayerUserDefaultsStat + let racePointsStat: PlayerUserDefaultsStat + let racePlaceStat: PlayerUserDefaultsStat + let raceTimeStat: PlayerUserDefaultsStat + let racePixelsScrolledStat: PlayerUserDefaultsStat switch raceType { - case .mpc: + case .private: raceCountStat = .mpcRaces racePointsStat = .mpcPoints raceTimeStat = .mpcTotalTime @@ -177,7 +164,7 @@ class WikiRacesTests: XCTestCase { } else { racePlaceStat = .mpcRaceDNF } - case .gameKit: + case .public: raceCountStat = .gkRaces racePointsStat = .gkPoints raceTimeStat = .gkTotalTime diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..3653d92 --- /dev/null +++ b/data/README.md @@ -0,0 +1,3 @@ +75,000 races sorted by links viewed. + +Includes player final states, history, and time spent on each page. \ No newline at end of file diff --git a/data/lzma_races.xz b/data/lzma_races.xz new file mode 100644 index 0000000..3818417 Binary files /dev/null and b/data/lzma_races.xz differ diff --git a/data/main.py b/data/main.py new file mode 100644 index 0000000..506e5a3 --- /dev/null +++ b/data/main.py @@ -0,0 +1,52 @@ +import os +import csv +import lzma +import pickle + +DIRECTORY_PATH = 'WKRRaces' + +def files(): + directory = DIRECTORY_PATH + f = [] + for file in os.listdir(directory): + if file.endswith(".csv"): + f.append(os.path.join(directory, file)) + return f + +def history_item(item, is_null_time): + s = item.split('|') + return (s[1], None if is_null_time else s[0]) + +def obj_for_file(file): + obj = [] + with open(file, newline='') as handler: + reader = csv.reader(handler, delimiter=',') + next(reader, None) + for row in reader: + _ = row.pop(0) + state = row.pop(0) + _ = row.pop(0) + history = [] + items_count = len(row) + for index, item in enumerate(row): + history.append(history_item(item, index == items_count - 1)) + + obj.append({ + 'State': state, + 'History': history + }) + return obj + +def save_races(): + races = [] + for file in files(): + races.append(obj_for_file(file)) + + with lzma.open("lzma_races.xz", "wb") as f: + pickle.dump(races, f) + +def load_races(): + with lzma.open('lzma_races.xz') as f: + races = pickle.load(f) + print(races[0]) + # ... diff --git a/docs/Killswitch.html b/docs/Killswitch.html new file mode 100644 index 0000000..d438c9f --- /dev/null +++ b/docs/Killswitch.html @@ -0,0 +1,2 @@ + +0 diff --git a/docs/Spectator.css b/docs/Spectator.css new file mode 100644 index 0000000..6a14a71 --- /dev/null +++ b/docs/Spectator.css @@ -0,0 +1,79 @@ +body { + background-color: #f3f3f3; + font-size: 12px; + font-weight: 500; + font-family: -apple-system; + text-align: center; + display: block; +} + +#header-flex { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + color: #a9a9a9; + padding: 20px; +} + +.header-item { + width: 30%; + font-size: 16px; + font-weight: 600; +} + +#race-info { + font-size: 16px; + font-weight: 600; + color: rgb(54, 54, 54); +} + +#header-icon { + width: 80px; + margin: 0 auto; + border: 2.5px solid #e8e8e8; + border-radius: 20px; +} + +#players-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-top: 20px; +} + +.player-image { + width: 40%; + border-radius: 50%; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1); + margin-top: 20px; +} + +.player-container { + width: 250px; + min-height: 220px; + text-align: center; + margin: 25px; + background-color: #ededed; + border-radius: 32px; +} + +.player-inner-container { + margin: 10px; +} +.player-name { + font-size: 14px; + font-weight: 500; + margin-top: 10px; + color: rgb(54, 54, 54); +} + +.player-page { + font-size: 16px; + font-weight: 500; + color: rgb(54, 54, 54); + + margin-top: 10px; + padding: 0px; +} diff --git a/docs/Spectator.html b/docs/Spectator.html new file mode 100644 index 0000000..fbe9886 --- /dev/null +++ b/docs/Spectator.html @@ -0,0 +1,23 @@ + + + + + + WikiRaces Spectator View Alpha + + + +
+
PRIVATE RACE
+ + + +
SPECTATOR VIEW ALPHA
+
+
+ +
+ +
+ + diff --git a/docs/Spectator.js b/docs/Spectator.js new file mode 100644 index 0000000..47f7861 --- /dev/null +++ b/docs/Spectator.js @@ -0,0 +1,297 @@ +const CLOUD_KIT_ENV = "production"; +const CLOUD_KEY_API_TOKEN = "3bb9e19a5e4594ce75d46f98fca2c453b857d3e0ff0eceaf8be3a598cbf24e49"; + +const WKRGameState = Object.freeze({ + preMatch: 0, + voting: 1, + race: 2, + results: 3, + hostResults: 4, + points: 5, +}); + +const AssetType = Object.freeze({ + imageContainer: 0, + resultsInfo: 1, + config: 2, +}); + +var activeRaceCode = null; +var activeResultsInfo = null; +var activeImageContainerItems = null; +var activeGameState = WKRGameState.preMatch; + +var initalInterfaceLoaded = false; + +function start() { + let raceCode = raceCodeFromURL(); + if (raceCode == null) { + raceCode = prompt("Enter Race Code"); + } + startWithRaceCode(raceCode); +} + +function raceCodeFromURL() { + let urlParams = new URLSearchParams(window.location.search); + if (urlParams != null && urlParams.has("Code")) { + return urlParams.get("Code"); + } else { + return null; + } +} + +function startWithRaceCode(raceCode) { + activeRaceCode = raceCode; + activeResultsInfo = null; + activeImageContainerItems = null; + initalInterfaceLoaded = false; + fetchDataForRaceCode(raceCode); +} + +function fetchDataForRaceCode(raceCode) { + let url = + "https://api.apple-cloudkit.com/database/1/iCloud.com.andrewfinke.wikiraces/" + + CLOUD_KIT_ENV + + "/public/records/query?ckAPIToken=" + + CLOUD_KEY_API_TOKEN; + + let xhr = new XMLHttpRequest(); + xhr.responseType = "json"; + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + receivedResponse(xhr.response); + } else { + console.error(xhr.statusText); + } + setTimeout(function () { + fetchDataForRaceCode(raceCode); + }, 5000); + } + }; + + xhr.onerror = function (e) { + console.error(xhr.statusText); + }; + + xhr.send( + JSON.stringify({ + query: { + recordType: "RaceActive", + filterBy: [ + { + fieldName: "Code", + comparator: "EQUALS", + fieldValue: { + value: raceCode.toLowerCase(), + type: "STRING", + }, + }, + ], + sortBy: [ + { + systemFieldName: "modifiedTimestamp", + ascending: false, + }, + ], + }, + }) + ); +} + +function receivedResponse(response) { + if (!response.hasOwnProperty("records")) { + console.error("Invalid Response: " + response); + return; + } + + let records = response["records"]; + if (records.length == 0) { + console.error("No Records: " + response); + return; + } + + let record = records[0]; + if (!record.hasOwnProperty("modified") || !record.hasOwnProperty("fields")) { + console.error("Invalid Record: " + record); + return; + } + + let modifiedObject = record["modified"]; + if (!modifiedObject.hasOwnProperty("timestamp")) { + console.error("Invalid Modified: " + modifiedObject); + return; + } + let modifiedTimestamp = modifiedObject["timestamp"]; + + let fields = record["fields"]; + if ( + !fields.hasOwnProperty("Code") || + !fields.hasOwnProperty("Host") || + !fields.hasOwnProperty("State") || + !fields.hasOwnProperty("Version") + ) { + console.error("Invalid Fields: " + fields); + return; + } + + let code = fields["Code"]["value"]; + let host = fields["Host"]["value"]; + let version = fields["Version"]["value"]; + activeGameState = fields["State"]["value"]; + if (activeGameState != WKRGameState.race && activeGameState != WKRGameState.results) { + updateStatusLabel(statusForGameState(activeGameState)); + } + + + if (fields.hasOwnProperty("Config")) { + downloadAssetRecord(fields["Config"], AssetType.config); + } else { + console.error("No Config Info"); + } + + if (fields.hasOwnProperty("ResultsInfo")) { + downloadAssetRecord(fields["ResultsInfo"], AssetType.resultsInfo); + } else { + console.error("No Results Info"); + } + + if (activeImageContainerItems == null) { + if (fields.hasOwnProperty("ImageContainer")) { + downloadAssetRecord(fields["ImageContainer"], AssetType.imageContainer); + } else { + console.error("No Results Info"); + } + } else { + console.log("Already have images"); + } +} + +function downloadAssetRecord(asset, assetType) { + if (asset.hasOwnProperty("value") && asset["value"].hasOwnProperty("downloadURL")) { + let downloadURL = asset["value"]["downloadURL"]; + downloadAsset(downloadURL, assetType); + } else { + console.error("No asset Info"); + } +} + +function downloadAsset(url, assetType) { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.responseType = "json"; + + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + if (assetType == AssetType.imageContainer) { + receivedImageContainer(xhr.response); + } else if (assetType == AssetType.resultsInfo) { + receivedResultsInfo(xhr.response); + } else if (assetType == AssetType.config) { + receivedConfig(xhr.response); + } + } else { + console.error(xhr.statusText); + } + } + }; + + xhr.onerror = function (e) { + console.error(xhr.statusText); + }; + + xhr.send(); +} + +function receivedResultsInfo(resultsInfo) { + if (!resultsInfo.hasOwnProperty("playersSortedByPoints")) { + console.error("Invalid Results Info: " + resultsInfo); + return; + } + let players = resultsInfo["playersSortedByPoints"]; + for (const playerObject of players) { + let playerID = playerObject["profile"]["playerID"]; + let entries = playerObject["raceHistory"]["entries"]; + let lastEntry = entries[entries.length - 1]; + let title = lastEntry["page"]["title"]; + for (const element of document.getElementsByClassName("player-page")) { + if (element.getAttribute("data-player-id") == playerID) { + element.innerHTML = title; + break; + } + } + } +} + +function receivedConfig(config) { + if (!config.hasOwnProperty("endingPage")) { + console.error("Invalid Config: " + config); + return; + } + + if (activeGameState == WKRGameState.race || activeGameState == WKRGameState.results) { + updateStatusLabel((config["startingPage"]["title"] + " TO " + config["endingPage"]["title"]).toUpperCase()); + } +} + +function receivedImageContainer(imageContainer) { + if (!imageContainer.hasOwnProperty("items")) { + console.error("Invalid Image Container: " + imageContainer); + return; + } + activeImageContainerItems = imageContainer["items"]; + createInitalInterfaceIfNeeded(); +} + +function createInitalInterfaceIfNeeded() { + if (initalInterfaceLoaded) { + return; + } + + initalInterfaceLoaded = true; + + let innerHTML = ""; + for (const playerID of Object.keys(activeImageContainerItems)) { + innerHTML += playerContainerTemplateForPlayerID(playerID); + } + document.getElementById("players-container").innerHTML = innerHTML; + document.getElementById("left-header-text").innerHTML = "PRIVATE RACE: " + activeRaceCode.toUpperCase(); +} + +function playerContainerTemplateForPlayerID(playerID) { + return ` +
+
+ +
${playerID}
+
+
+
+ `; +} + +function statusForGameState(state) { + if (state == WKRGameState.preMatch) { + return "PRERACE" + } else if (state == WKRGameState.voting) { + return "VOTING" + } else if (state == WKRGameState.race) { + return "RACE" + } else if (state == WKRGameState.results) { + return "END OF RACE" + } else if (state == WKRGameState.hostResults) { + return "END OF RACE" + } else if (state == WKRGameState.points) { + return "END OF RACE" + } else { + return "N/A" + } +} + +function updateStatusLabel(text) { + document.getElementById("race-status").innerHTML = text; +} diff --git a/docs/icon.jpg b/docs/icon.jpg new file mode 100644 index 0000000..3d46062 Binary files /dev/null and b/docs/icon.jpg differ