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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 = "