commit fde29e2799e42bd38804e5c99ffa4d7cfa5f6e18
parent ddb3e953d67455b295bd929595e742c18f25524b
Author: Michael Camilleri <[email protected]>
Date: Sat, 2 May 2026 17:25:51 +0900
Remove performance logging
At the risk of tempting the universe, this commit preemptively
removes the performance logging. A debugging device is now available
that makes this less necessary.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
10 files changed, 9 insertions(+), 423 deletions(-)
diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift
@@ -21,7 +21,6 @@ struct CrossmateApp: App {
.environment(\.managedObjectContext, services.persistence.viewContext)
.environment(services.driveMonitor)
.environment(services.syncMonitor)
- .environment(services.performanceMonitor)
.environment(\.syncEngine, services.syncEngine)
.environment(services.nytAuth)
.environment(\.nytPuzzleFetcher, services.nytFetcher)
@@ -314,7 +313,6 @@ private struct PuzzleDisplayView: View {
do {
let (game, mutator) = try store.loadGame(id: gameID)
let newSession = PlayerSession(game: game, mutator: mutator)
- newSession.performanceMonitor = services.performanceMonitor
if mutator.isShared && preferences.isICloudSyncEnabled {
Task { await AppDelegate.requestNotificationAuthorizationIfNeeded() }
roster = services.makePlayerRoster(for: gameID, preferences: preferences)
diff --git a/Crossmate/Models/PlayerPreferences.swift b/Crossmate/Models/PlayerPreferences.swift
@@ -18,8 +18,6 @@ final class PlayerPreferences {
static let colorID = "playerColorID"
static let name = "playerName"
static let isICloudSyncEnabled = "isICloudSyncEnabled"
- static let isClueBarAnimationEnabled = "isClueBarAnimationEnabled"
- static let isViewBodyLoggingEnabled = "isViewBodyLoggingEnabled"
}
private let local: UserDefaults
@@ -40,18 +38,6 @@ final class PlayerPreferences {
didSet { local.set(isICloudSyncEnabled, forKey: Keys.isICloudSyncEnabled) }
}
- /// Debugging switch for testing whether clue text transitions are
- /// contributing to input jank. Stored locally only.
- var isClueBarAnimationEnabled: Bool {
- didSet { local.set(isClueBarAnimationEnabled, forKey: Keys.isClueBarAnimationEnabled) }
- }
-
- /// Debugging switch for the relatively noisy SwiftUI body-count probes.
- /// The core input/render duration probes remain active.
- var isViewBodyLoggingEnabled: Bool {
- didSet { local.set(isViewBodyLoggingEnabled, forKey: Keys.isViewBodyLoggingEnabled) }
- }
-
var color: PlayerColor {
get { PlayerColor.color(for: colorID) }
set { colorID = newValue.id }
@@ -70,8 +56,6 @@ final class PlayerPreferences {
?? local.string(forKey: Keys.name)
?? "Player"
self.isICloudSyncEnabled = local.object(forKey: Keys.isICloudSyncEnabled) as? Bool ?? true
- self.isClueBarAnimationEnabled = local.object(forKey: Keys.isClueBarAnimationEnabled) as? Bool ?? true
- self.isViewBodyLoggingEnabled = local.object(forKey: Keys.isViewBodyLoggingEnabled) as? Bool ?? true
cloud.synchronize()
NotificationCenter.default.addObserver(
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
diff --git a/Crossmate/Models/PlayerSession.swift b/Crossmate/Models/PlayerSession.swift
@@ -34,11 +34,6 @@ final class PlayerSession {
@ObservationIgnored
var onCompletionStateChanged: ((Game.CompletionState) -> Void)?
- @ObservationIgnored
- var performanceMonitor: PerformanceMonitor?
-
- var renderProbeID: Int?
-
/// Rebus mode lets the player type a multi-character value into a single
/// cell (e.g. "STAR" or "♥"). While active, keyboard input accumulates in
/// `rebusBuffer` rather than going straight to `Game.squares`; on commit
@@ -213,34 +208,19 @@ final class PlayerSession {
// MARK: - Input
func enter(_ letter: String) {
- let start = ContinuousClock.now
let inputRow = selectedRow
let inputCol = selectedCol
let cell = puzzle.cells[inputRow][inputCol]
guard !cell.isBlock else { return }
- let expectedEntry = letter.uppercased()
- startRenderProbe(
- "render.enter",
- row: inputRow,
- col: inputCol,
- expectedEntry: expectedEntry,
- detail: "letter=\(expectedEntry)"
- )
mutator.setLetter(letter, atRow: inputRow, atCol: inputCol, pencil: isPencilMode)
let completionState = game.completionState
publishTerminalCompletionState(completionState)
if completionState != .solved {
advance()
}
- recordInputDuration(
- "input.enter",
- start: start,
- detail: inputDetail(row: inputRow, col: inputCol, extra: "letter=\(letter)")
- )
}
func deleteBackward() {
- let start = ContinuousClock.now
// If the cursor is on an empty cell or a revealed (locked) cell,
// retreat first — revealed cells can't be cleared in place, so delete
// tunnels past them to the previous editable cell. `clearLetter`
@@ -251,20 +231,7 @@ final class PlayerSession {
if currentEmpty || currentMark.isRevealed {
retreat()
}
- let inputRow = selectedRow
- let inputCol = selectedCol
- startRenderProbe(
- "render.delete",
- row: inputRow,
- col: inputCol,
- expectedEntry: ""
- )
mutator.clearLetter(atRow: selectedRow, atCol: selectedCol)
- recordInputDuration(
- "input.delete",
- start: start,
- detail: inputDetail(row: inputRow, col: inputCol)
- )
}
// MARK: - Rebus
@@ -286,31 +253,17 @@ final class PlayerSession {
}
func commitRebus() {
- let start = ContinuousClock.now
let value = rebusBuffer
let inputRow = selectedRow
let inputCol = selectedCol
isRebusActive = false
rebusBuffer = ""
- let expectedEntry = value.uppercased()
- startRenderProbe(
- "render.rebusCommit",
- row: inputRow,
- col: inputCol,
- expectedEntry: expectedEntry,
- detail: "length=\(expectedEntry.count)"
- )
mutator.setLetter(value, atRow: inputRow, atCol: inputCol, pencil: isPencilMode)
let completionState = game.completionState
publishTerminalCompletionState(completionState)
if completionState != .solved {
advance()
}
- recordInputDuration(
- "input.rebusCommit",
- start: start,
- detail: inputDetail(row: inputRow, col: inputCol, extra: "length=\(value.count)")
- )
}
// MARK: - Word geometry
@@ -370,45 +323,6 @@ final class PlayerSession {
onCompletionStateChanged?(state)
}
- private func recordInputDuration(
- _ name: String,
- start: ContinuousClock.Instant,
- detail: String = ""
- ) {
- let duration = start.duration(to: .now)
- let milliseconds = Double(duration.components.seconds) * 1000
- + Double(duration.components.attoseconds) / 1_000_000_000_000_000
- performanceMonitor?.record(
- name,
- durationMS: milliseconds,
- detail: detail,
- thresholdMS: 6
- )
- }
-
- private func startRenderProbe(
- _ name: String,
- row: Int,
- col: Int,
- expectedEntry: String,
- detail: String = ""
- ) {
- guard let performanceMonitor else { return }
- renderProbeID = performanceMonitor.beginRenderProbe(
- name: name,
- position: GridPosition(row: row, col: col),
- expectedEntry: expectedEntry,
- detail: detail
- )
- }
-
- private func inputDetail(row: Int, col: Int, extra: String = "") -> String {
- let probePrefix = renderProbeID.flatMap { performanceMonitor?.renderProbeDetailPrefix(for: $0) }
- ?? "row=\(row) col=\(col)"
- guard !extra.isEmpty else { return probePrefix }
- return "\(probePrefix) \(extra)"
- }
-
private func advance() {
let (dr, dc) = step(for: direction)
let r = selectedRow + dr
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -8,7 +8,6 @@ final class AppServices {
let store: GameStore
let syncEngine: SyncEngine
let syncMonitor: SyncMonitor
- let performanceMonitor: PerformanceMonitor
let nytAuth: NYTAuthService
let driveMonitor: DriveMonitor
let nytFetcher: NYTPuzzleFetcher
@@ -39,8 +38,6 @@ final class AppServices {
let syncEngine = SyncEngine(container: self.ckContainer, persistence: persistence)
self.syncEngine = syncEngine
self.syncMonitor = SyncMonitor()
- let performanceMonitor = PerformanceMonitor()
- self.performanceMonitor = performanceMonitor
self.nytAuth = NYTAuthService()
self.driveMonitor = DriveMonitor()
self.nytFetcher = NYTPuzzleFetcher { NYTAuthService.currentCookie() }
@@ -60,46 +57,17 @@ final class AppServices {
guard isEnabled else { return }
await syncEngine.enqueueMoves(moves)
},
- performanceSink: { [performanceMonitor] name, durationMS, detail, thresholdMS in
- await performanceMonitor.record(
- name,
- durationMS: durationMS,
- detail: detail,
- thresholdMS: thresholdMS
- )
- },
- afterFlush: { [performanceMonitor] gameIDs in
+ afterFlush: { gameIDs in
let isEnabled = await MainActor.run { preferences.isICloudSyncEnabled }
guard isEnabled else { return }
- let snapshotStart = ContinuousClock.now
let result = await store.createSnapshotsIfNeeded(for: gameIDs)
- await performanceMonitor.record(
- "moveBuffer.snapshotCheck",
- durationMS: Self.milliseconds(from: snapshotStart.duration(to: .now)),
- detail: "games=\(gameIDs.count) snapshots=\(result.snapshotNames.count) prunedMoves=\(result.prunedMoveNames.count)",
- thresholdMS: 8
- )
- let snapshotEnqueueStart = ContinuousClock.now
for name in result.snapshotNames {
await syncEngine.enqueueSnapshot(ckRecordName: name)
}
- await performanceMonitor.record(
- "moveBuffer.snapshotEnqueue",
- durationMS: Self.milliseconds(from: snapshotEnqueueStart.duration(to: .now)),
- detail: "snapshots=\(result.snapshotNames.count)",
- thresholdMS: 4
- )
- let deleteEnqueueStart = ContinuousClock.now
await syncEngine.enqueueDeleteRecords(result.prunedMoveNames)
- await performanceMonitor.record(
- "moveBuffer.deleteEnqueue",
- durationMS: Self.milliseconds(from: deleteEnqueueStart.duration(to: .now)),
- detail: "moves=\(result.prunedMoveNames.count)",
- thresholdMS: 4
- )
},
sessionPingSink: { [preferences] gameID, authorID in
guard await MainActor.run(body: { preferences.isICloudSyncEnabled }) else { return }
@@ -414,11 +382,6 @@ final class AppServices {
syncMonitor.updateSnapshot(snapshot)
}
- private static func milliseconds(from duration: Duration) -> Double {
- Double(duration.components.seconds) * 1000
- + Double(duration.components.attoseconds) / 1_000_000_000_000_000
- }
-
/// Builds the `GameStore.onGameDeleted` callback. Extracted so tests can
/// drive the exact same closure that production wires up — keeps the
/// colour-cleanup branch from drifting silently.
diff --git a/Crossmate/Sync/MoveBuffer.swift b/Crossmate/Sync/MoveBuffer.swift
@@ -32,7 +32,6 @@ actor MoveBuffer {
private let sessionPingStaleInterval: TimeInterval
private let persistence: PersistenceController
private let sink: @Sendable ([Move]) async -> Void
- private let performanceSink: (@Sendable (String, Double, String, Double) async -> Void)?
private let afterFlush: (@Sendable (Set<UUID>) async -> Void)?
private let sessionPingSink: (@Sendable (UUID, String) async -> Void)?
@@ -60,7 +59,6 @@ actor MoveBuffer {
sessionPingStaleInterval: TimeInterval = 30 * 60,
persistence: PersistenceController,
sink: @escaping @Sendable ([Move]) async -> Void,
- performanceSink: (@Sendable (String, Double, String, Double) async -> Void)? = nil,
afterFlush: (@Sendable (Set<UUID>) async -> Void)? = nil,
sessionPingSink: (@Sendable (UUID, String) async -> Void)? = nil
) {
@@ -68,7 +66,6 @@ actor MoveBuffer {
self.sessionPingStaleInterval = sessionPingStaleInterval
self.persistence = persistence
self.sink = sink
- self.performanceSink = performanceSink
self.afterFlush = afterFlush
self.sessionPingSink = sessionPingSink
}
@@ -165,57 +162,23 @@ actor MoveBuffer {
order.removeAll(keepingCapacity: true)
lastCell = nil
- let persistStart = ContinuousClock.now
let persistedMoves = persistAndAssignLamports(
snapshot: snapshot,
order: snapshotOrder
)
pendingSinkMoves.append(contentsOf: persistedMoves)
- await recordPerformance(
- "moveBuffer.persist",
- start: persistStart,
- detail: "moves=\(persistedMoves.count) pendingCloudKit=\(pendingSinkMoves.count)",
- thresholdMS: 4
- )
}
guard runAfterFlush, !pendingSinkMoves.isEmpty else { return }
let moves = pendingSinkMoves
pendingSinkMoves.removeAll(keepingCapacity: true)
- let sinkStart = ContinuousClock.now
await sink(moves)
- await recordPerformance(
- "moveBuffer.cloudKitEnqueue",
- start: sinkStart,
- detail: "moves=\(moves.count)",
- thresholdMS: 4
- )
if let afterFlush {
- let afterFlushStart = ContinuousClock.now
await afterFlush(Set(moves.map { $0.gameID }))
- await recordPerformance(
- "moveBuffer.afterFlush",
- start: afterFlushStart,
- detail: "games=\(Set(moves.map { $0.gameID }).count)",
- thresholdMS: 8
- )
}
}
- private func recordPerformance(
- _ name: String,
- start: ContinuousClock.Instant,
- detail: String,
- thresholdMS: Double
- ) async {
- guard let performanceSink else { return }
- let duration = start.duration(to: .now)
- let milliseconds = Double(duration.components.seconds) * 1000
- + Double(duration.components.attoseconds) / 1_000_000_000_000_000
- await performanceSink(name, milliseconds, detail, thresholdMS)
- }
-
/// Allocates lamports from each game's `lamportHighWater`, writes
/// `MoveEntity` rows, and bumps the high-water — all inside a single
/// background-context transaction so a crash can't leave the high-water
diff --git a/Crossmate/Views/CellView.swift b/Crossmate/Views/CellView.swift
@@ -10,10 +10,8 @@ struct CellView: View, Equatable {
let specialKind: Puzzle.Special?
var remoteWordTint: Color? = nil
var remoteOutline: Color? = nil
- var diagnosticPosition: GridPosition?
@Environment(PlayerPreferences.self) private var preferences
- @Environment(PerformanceMonitor.self) private var performanceMonitor
private var playerColor: PlayerColor { preferences.color }
nonisolated static func == (lhs: CellView, rhs: CellView) -> Bool {
@@ -26,17 +24,9 @@ struct CellView: View, Equatable {
&& lhs.specialKind == rhs.specialKind
&& lhs.remoteWordTint == rhs.remoteWordTint
&& lhs.remoteOutline == rhs.remoteOutline
- && lhs.diagnosticPosition == rhs.diagnosticPosition
}
var body: some View {
- let _ = preferences.isViewBodyLoggingEnabled
- ? performanceMonitor.markViewBodyDeferred(
- "cell",
- key: "active",
- detail: diagnosticDetail()
- )
- : ()
ZStack(alignment: .topLeading) {
background
if !cell.isBlock {
@@ -75,24 +65,6 @@ struct CellView: View, Equatable {
.contentShape(Rectangle())
}
- private func diagnosticDetail() -> String {
- var parts: [String] = []
- if let diagnosticPosition {
- parts.append("lastRow=\(diagnosticPosition.row)")
- parts.append("lastCol=\(diagnosticPosition.col)")
- }
- if isSelected {
- parts.append("selected=1")
- }
- if isHighlighted {
- parts.append("highlighted=1")
- }
- if !entry.isEmpty {
- parts.append("entry=1")
- }
- return parts.joined(separator: " ")
- }
-
/// Foreground style for the main entry letter. Pencil entries use the
/// hierarchical `.secondary` style so they render lighter and respect
/// dark mode; everything else — including revealed and checkedWrong
diff --git a/Crossmate/Views/DiagnosticsView.swift b/Crossmate/Views/DiagnosticsView.swift
@@ -273,96 +273,3 @@ struct ShareDiagnosticsView: View {
return lines.joined(separator: "\n")
}
}
-
-struct PerformanceDiagnosticsView: View {
- @Environment(PerformanceMonitor.self) private var performanceMonitor
-
- private static let timestampFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .none
- formatter.timeStyle = .medium
- return formatter
- }()
-
- var body: some View {
- List {
- Section("Summary") {
- row("Recorded Slow Events", String(performanceMonitor.entries.count))
- row("Measured Events", String(performanceMonitor.totalEvents))
- row("Suppressed Fast Events", String(performanceMonitor.suppressedEvents))
- }
-
- Section("Actions") {
- Button("Clear Log", role: .destructive) {
- performanceMonitor.clear()
- }
- }
-
- Section("Recent Slow Events") {
- if performanceMonitor.entries.isEmpty {
- Text("No slow events captured yet.")
- .foregroundStyle(.secondary)
- } else {
- ForEach(performanceMonitor.entries.reversed()) { entry in
- VStack(alignment: .leading, spacing: 4) {
- Text(
- "\(Self.timestampFormatter.string(from: entry.timestamp)) \(entry.name) \(formatMS(entry.durationMS))"
- )
- .font(.caption.monospaced())
- .foregroundStyle(.secondary)
-
- if !entry.detail.isEmpty {
- Text(entry.detail)
- .font(.caption.monospaced())
- .textSelection(.enabled)
- }
- }
- .padding(.vertical, 2)
- }
- }
- }
- }
- .navigationTitle("Performance Diagnostics")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .topBarTrailing) {
- Button("Copy") {
- UIPasteboard.general.string = diagnosticDump
- }
- }
- }
- }
-
- @ViewBuilder
- private func row(_ title: String, _ value: String) -> some View {
- VStack(alignment: .leading, spacing: 4) {
- Text(title)
- .font(.caption)
- .foregroundStyle(.secondary)
- Text(value)
- .font(.body.monospaced())
- .textSelection(.enabled)
- }
- .padding(.vertical, 2)
- }
-
- private func formatMS(_ value: Double) -> String {
- value == 0 ? "note" : String(format: "%.2f ms", value)
- }
-
- private var diagnosticDump: String {
- var lines: [String] = []
- lines.append("Recorded Slow Events: \(performanceMonitor.entries.count)")
- lines.append("Measured Events: \(performanceMonitor.totalEvents)")
- lines.append("Suppressed Fast Events: \(performanceMonitor.suppressedEvents)")
- lines.append("")
- lines.append("Recent Slow Events:")
- for entry in performanceMonitor.entries {
- let detail = entry.detail.isEmpty ? "" : " \(entry.detail)"
- lines.append(
- "\(Self.timestampFormatter.string(from: entry.timestamp)) \(entry.name) \(formatMS(entry.durationMS))\(detail)"
- )
- }
- return lines.joined(separator: "\n")
- }
-}
diff --git a/Crossmate/Views/GridView.swift b/Crossmate/Views/GridView.swift
@@ -3,64 +3,26 @@ import SwiftUI
struct GridView: View {
@Bindable var session: PlayerSession
var roster: PlayerRoster? = nil
- @Environment(PlayerPreferences.self) private var preferences
- @Environment(PerformanceMonitor.self) private var performanceMonitor
private let spacing: CGFloat = 1
var body: some View {
- let bodyDetail = diagnosticDetail()
- let shouldLogViewBodies = preferences.isViewBodyLoggingEnabled
- let _ = shouldLogViewBodies
- ? performanceMonitor.markViewBodyDeferred(
- "grid",
- key: session.renderProbeID.map { "probe=\($0)" } ?? "idle",
- detail: bodyDetail
- )
- : ()
let width = session.puzzle.width
let height = session.puzzle.height
// Index remote selections by cell so each CellView only receives the
// value it needs. Multiple peers landing on the same cell collapse
// to the most recent.
- let overlayStart = ContinuousClock.now
let (outlineByCell, tintByCell) = remoteOverlays()
- let _ = shouldLogViewBodies
- ? performanceMonitor.recordDeferred(
- "grid.remoteOverlays",
- start: overlayStart,
- detail: bodyDetail,
- thresholdMS: 2
- )
- : ()
- let relatedStart = ContinuousClock.now
let relatedCells = session.puzzle.relatedCells(
atRow: session.selectedRow,
col: session.selectedCol,
direction: session.direction
)
- let _ = shouldLogViewBodies
- ? performanceMonitor.recordDeferred(
- "grid.relatedCells",
- start: relatedStart,
- detail: "\(bodyDetail) count=\(relatedCells.count)",
- thresholdMS: 2
- )
- : ()
- let wordStart = ContinuousClock.now
let currentWordCells = Set(session.puzzle.wordCells(
atRow: session.selectedRow,
col: session.selectedCol,
direction: session.direction
).map { GridPosition(row: $0.row, col: $0.col) })
- let _ = shouldLogViewBodies
- ? performanceMonitor.recordDeferred(
- "grid.currentWord",
- start: wordStart,
- detail: "\(bodyDetail) count=\(currentWordCells.count)",
- thresholdMS: 2
- )
- : ()
PuzzleGridLayout(columns: width, rows: height, spacing: spacing) {
ForEach(0..<(width * height), id: \.self) { index in
let r = index / width
@@ -75,31 +37,17 @@ struct GridView: View {
isRelatedToFocus: relatedCells.contains(pos),
specialKind: session.puzzle.specialKind,
remoteWordTint: tintByCell[pos],
- remoteOutline: outlineByCell[pos],
- diagnosticPosition: pos
+ remoteOutline: outlineByCell[pos]
)
.equatable()
.onTapGesture {
session.select(row: r, col: c)
}
- .onChange(of: entryProbeValue(id: session.renderProbeID, entry: session.game.squares[r][c].entry)) { _, value in
- guard let id = value.id else { return }
- performanceMonitor.completeRenderProbe(id: id, position: pos, entry: value.entry)
- }
}
}
.background(Color.black)
}
- private struct EntryProbeValue: Equatable {
- let id: Int?
- let entry: String
- }
-
- private func entryProbeValue(id: Int?, entry: String) -> EntryProbeValue {
- EntryProbeValue(id: id, entry: entry)
- }
-
/// Builds the focused-cell outline map and the word-tint map from each
/// peer's selection. Conflicts (two peers on the same cell or word) are
/// resolved by keeping the most recent `updatedAt`.
@@ -127,18 +75,6 @@ struct GridView: View {
return (outline.mapValues { $0.1 }, tint.mapValues { $0.1 })
}
- private func diagnosticDetail() -> String {
- var parts: [String] = []
- if let probeID = session.renderProbeID {
- parts.append("probe=\(probeID)")
- }
- parts.append("selected=\(session.selectedRow),\(session.selectedCol)")
- parts.append("direction=\(session.direction)")
- if let roster {
- parts.append("remoteSelections=\(roster.remoteSelections.count)")
- }
- return parts.joined(separator: " ")
- }
}
// MARK: - Layout
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -371,17 +371,7 @@ private struct ClueBarContent: View {
var onNext: (() -> Void)?
var onClueTap: (() -> Void)?
- @Environment(PlayerPreferences.self) private var preferences
- @Environment(PerformanceMonitor.self) private var performanceMonitor
-
var body: some View {
- let _ = preferences.isViewBodyLoggingEnabled
- ? performanceMonitor.markViewBodyDeferred(
- "clueBarContent",
- key: currentKey.map { "\($0.direction)-\($0.number)" } ?? "none",
- detail: currentKey.map { "direction=\($0.direction) number=\($0.number)" } ?? "none"
- )
- : ()
HStack(alignment: .clueCenter, spacing: 12) {
ClueBarIcon(systemName: "chevron.left", action: onPrevious)
@@ -410,16 +400,12 @@ private struct ClueBarContent: View {
@ViewBuilder
private var clueTextView: some View {
- if preferences.isClueBarAnimationEnabled {
- baseClueText
- .id(currentKey)
- .transition(.asymmetric(
- insertion: .move(edge: slideEdge),
- removal: .move(edge: slideEdge == .trailing ? .leading : .trailing)
- ))
- } else {
- baseClueText
- }
+ baseClueText
+ .id(currentKey)
+ .transition(.asymmetric(
+ insertion: .move(edge: slideEdge),
+ removal: .move(edge: slideEdge == .trailing ? .leading : .trailing)
+ ))
}
private var baseClueText: some View {
@@ -456,32 +442,13 @@ private struct ClueBarIcon: View {
private struct ClueBar: View {
@Bindable var session: PlayerSession
@Environment(PlayerPreferences.self) private var preferences
- @Environment(PerformanceMonitor.self) private var performanceMonitor
@State private var slideEdge: Edge = .trailing
@State private var isShowingClueList = false
private var playerColor: PlayerColor { preferences.color }
var body: some View {
- let bodyDetail = diagnosticDetail()
- let shouldLogViewBodies = preferences.isViewBodyLoggingEnabled
- let _ = shouldLogViewBodies
- ? performanceMonitor.markViewBodyDeferred(
- "clueBar",
- key: session.renderProbeID.map { "probe=\($0)" } ?? "idle",
- detail: bodyDetail
- )
- : ()
- let clueStart = ContinuousClock.now
let clue = session.currentClue()
- let _ = shouldLogViewBodies
- ? performanceMonitor.recordDeferred(
- "clueBar.currentClue",
- start: clueStart,
- detail: bodyDetail,
- thresholdMS: 1
- )
- : ()
let currentKey = clue.map { ClueKey(direction: session.direction, number: $0.number) }
ClueBarContent(
@@ -502,10 +469,7 @@ private struct ClueBar: View {
}
)
.background(playerColor.highlightFill)
- .animation(
- preferences.isClueBarAnimationEnabled ? .smooth(duration: 0.22) : nil,
- value: currentKey
- )
+ .animation(.smooth(duration: 0.22), value: currentKey)
.sheet(isPresented: $isShowingClueList) {
ClueList(session: session)
.presentationDetents([.medium, .large])
@@ -513,16 +477,6 @@ private struct ClueBar: View {
}
}
- private func diagnosticDetail() -> String {
- var parts: [String] = []
- if let probeID = session.renderProbeID {
- parts.append("probe=\(probeID)")
- }
- parts.append("selected=\(session.selectedRow),\(session.selectedCol)")
- parts.append("direction=\(session.direction)")
- return parts.joined(separator: " ")
- }
-
private func label(for clue: Puzzle.Clue?) -> String {
let direction = session.direction == .across ? "Across" : "Down"
if let clue {
diff --git a/Crossmate/Views/SettingsView.swift b/Crossmate/Views/SettingsView.swift
@@ -23,8 +23,6 @@ struct SettingsView: View {
Section("Debugging") {
Toggle("Enable iCloud Sync", isOn: $preferences.isICloudSyncEnabled)
- Toggle("Animate Clue Bar", isOn: $preferences.isClueBarAnimationEnabled)
- Toggle("Log View Body Counts", isOn: $preferences.isViewBodyLoggingEnabled)
NavigationLink("iCloud Diagnostics") {
DiagnosticsView()
@@ -32,9 +30,6 @@ struct SettingsView: View {
NavigationLink("Share Diagnostics") {
ShareDiagnosticsView()
}
- NavigationLink("Performance Diagnostics") {
- PerformanceDiagnosticsView()
- }
Button("Reset Database", role: .destructive) {
showResetConfirmation = true