crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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:
MCrossmate/Models/PlayerPreferences.swift | 16++++++++++++++++
MCrossmate/Views/CellView.swift | 12+++++++-----
MCrossmate/Views/GridView.swift | 56+++++++++++++++++++++++++++++++++-----------------------
MCrossmate/Views/PuzzleView.swift | 78+++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
MCrossmate/Views/SettingsView.swift | 2++
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()