commit 27c0611f893c48428de21d2d81236f8c07980cbc
parent e905838f71f1da7e48cfe954ec765b45f6b76af4
Author: Michael Camilleri <[email protected]>
Date: Sat, 2 May 2026 10:36:31 +0900
Allow certain logging to be disabled
Diffstat:
5 files changed, 109 insertions(+), 55 deletions(-)
diff --git a/Crossmate/Models/PlayerPreferences.swift b/Crossmate/Models/PlayerPreferences.swift
@@ -18,6 +18,8 @@ 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
@@ -38,6 +40,18 @@ 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 }
@@ -56,6 +70,8 @@ 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/Views/CellView.swift b/Crossmate/Views/CellView.swift
@@ -30,11 +30,13 @@ struct CellView: View, Equatable {
}
var body: some View {
- let _ = performanceMonitor.markViewBodyDeferred(
- "cell",
- key: "active",
- detail: diagnosticDetail()
- )
+ let _ = preferences.isViewBodyLoggingEnabled
+ ? performanceMonitor.markViewBodyDeferred(
+ "cell",
+ key: "active",
+ detail: diagnosticDetail()
+ )
+ : ()
ZStack(alignment: .topLeading) {
background
if !cell.isBlock {
diff --git a/Crossmate/Views/GridView.swift b/Crossmate/Views/GridView.swift
@@ -3,17 +3,21 @@ 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 _ = performanceMonitor.markViewBodyDeferred(
- "grid",
- key: session.renderProbeID.map { "probe=\($0)" } ?? "idle",
- detail: bodyDetail
- )
+ 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
@@ -21,36 +25,42 @@ struct GridView: View {
// to the most recent.
let overlayStart = ContinuousClock.now
let (outlineByCell, tintByCell) = remoteOverlays()
- let _ = performanceMonitor.recordDeferred(
- "grid.remoteOverlays",
- start: overlayStart,
- detail: bodyDetail,
- thresholdMS: 2
- )
+ 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 _ = performanceMonitor.recordDeferred(
- "grid.relatedCells",
- start: relatedStart,
- detail: "\(bodyDetail) count=\(relatedCells.count)",
- thresholdMS: 2
- )
+ 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 _ = performanceMonitor.recordDeferred(
- "grid.currentWord",
- start: wordStart,
- detail: "\(bodyDetail) count=\(currentWordCells.count)",
- thresholdMS: 2
- )
+ 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
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -371,14 +371,17 @@ 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 _ = performanceMonitor.markViewBodyDeferred(
- "clueBarContent",
- key: currentKey.map { "\($0.direction)-\($0.number)" } ?? "none",
- detail: currentKey.map { "direction=\($0.direction) number=\($0.number)" } ?? "none"
- )
+ 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)
@@ -388,16 +391,7 @@ private struct ClueBarContent: View {
.textCase(.uppercase)
.foregroundStyle(.secondary)
ZStack(alignment: .leading) {
- Text(clueText)
- .font(.headline)
- .lineLimit(2, reservesSpace: reservesClueSpace)
- .multilineTextAlignment(.leading)
- .frame(maxWidth: .infinity, alignment: .leading)
- .id(currentKey)
- .transition(.asymmetric(
- insertion: .move(edge: slideEdge),
- removal: .move(edge: slideEdge == .trailing ? .leading : .trailing)
- ))
+ clueTextView
}
.alignmentGuide(.clueCenter) { d in d[VerticalAlignment.center] }
.frame(maxWidth: .infinity, alignment: .leading)
@@ -413,6 +407,28 @@ private struct ClueBarContent: View {
.padding(.horizontal, 12)
.padding(.vertical, 12)
}
+
+ @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
+ }
+ }
+
+ private var baseClueText: some View {
+ Text(clueText)
+ .font(.headline)
+ .lineLimit(2, reservesSpace: reservesClueSpace)
+ .multilineTextAlignment(.leading)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
}
private struct ClueBarIcon: View {
@@ -448,19 +464,24 @@ private struct ClueBar: View {
var body: some View {
let bodyDetail = diagnosticDetail()
- let _ = performanceMonitor.markViewBodyDeferred(
- "clueBar",
- key: session.renderProbeID.map { "probe=\($0)" } ?? "idle",
- detail: bodyDetail
- )
+ 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 _ = performanceMonitor.recordDeferred(
- "clueBar.currentClue",
- start: clueStart,
- detail: bodyDetail,
- thresholdMS: 1
- )
+ let _ = shouldLogViewBodies
+ ? performanceMonitor.recordDeferred(
+ "clueBar.currentClue",
+ start: clueStart,
+ detail: bodyDetail,
+ thresholdMS: 1
+ )
+ : ()
let currentKey = clue.map { ClueKey(direction: session.direction, number: $0.number) }
ClueBarContent(
@@ -481,7 +502,10 @@ private struct ClueBar: View {
}
)
.background(playerColor.highlightFill)
- .animation(.smooth(duration: 0.22), value: currentKey)
+ .animation(
+ preferences.isClueBarAnimationEnabled ? .smooth(duration: 0.22) : nil,
+ value: currentKey
+ )
.sheet(isPresented: $isShowingClueList) {
ClueList(session: session)
.presentationDetents([.medium, .large])
diff --git a/Crossmate/Views/SettingsView.swift b/Crossmate/Views/SettingsView.swift
@@ -23,6 +23,8 @@ 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()