crossmate

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

commit 7f29b31e961964c461d10298b8877c8d16b0083f
parent cceaeae51810ad4f7a86b326597ebe3d3507c6ec
Author: Michael Camilleri <[email protected]>
Date:   Sat, 13 Jun 2026 13:54:39 +0900

Suggest user set profile name on first launch

Diffstat:
MCrossmate/Models/PlayerPreferences.swift | 16+++++++++++++---
MCrossmate/Views/GameListView.swift | 41+++++++++++++++++++++++++++++++++++++----
2 files changed, 50 insertions(+), 7 deletions(-)

diff --git a/Crossmate/Models/PlayerPreferences.swift b/Crossmate/Models/PlayerPreferences.swift @@ -32,9 +32,14 @@ final class PlayerPreferences { } var name: String { - didSet { write(Keys.name, name) } + didSet { + hasName = Self.isUsableName(name) + write(Keys.name, name) + } } + var hasName: Bool + /// Debugging switch for comparing on-device responsiveness with CloudKit /// work paused. Stored locally only so disabling sync on one device does /// not silently disable it everywhere. @@ -78,9 +83,10 @@ final class PlayerPreferences { self.colorID = cloud.string(forKey: Keys.colorID) ?? local.string(forKey: Keys.colorID) ?? PlayerColor.blue.id - self.name = cloud.string(forKey: Keys.name) + let storedName = cloud.string(forKey: Keys.name) ?? local.string(forKey: Keys.name) - ?? "Player" + self.name = storedName ?? "Player" + self.hasName = storedName.map(Self.isUsableName) ?? false self.isICloudSyncEnabled = local.object(forKey: Keys.isICloudSyncEnabled) as? Bool ?? true self.notifiesSessionBegin = local.object(forKey: Keys.notifiesSessionBegin) as? Bool ?? true self.notifiesSessionEnd = local.object(forKey: Keys.notifiesSessionEnd) as? Bool ?? true @@ -111,4 +117,8 @@ final class PlayerPreferences { name = newName } } + + private static func isUsableName(_ name: String) -> Bool { + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } } diff --git a/Crossmate/Views/GameListView.swift b/Crossmate/Views/GameListView.swift @@ -35,6 +35,7 @@ struct GameListView: View { @Environment(\.declineInvite) private var declineInvite @Environment(\.blockFriend) private var blockFriend @Environment(\.sendResignPings) private var sendResignPings + @Environment(PlayerPreferences.self) private var preferences @Environment(AnnouncementCenter.self) private var announcements @State private var acceptingInviteID: NSManagedObjectID? @State private var blockTarget: InviteEntity? @@ -46,6 +47,8 @@ struct GameListView: View { @State private var resignTarget: GameSummary? @State private var leaveTarget: GameSummary? @State private var leaveError: Error? + @State private var showingNamePrompt = false + @State private var nameDraft = "" @State private var summaryCache = GameSummaryCache() @State private var completedVisibleCount = completedPageSize @@ -196,6 +199,22 @@ struct GameListView: View { let name = blockTarget?.resolvedInviterName ?? "this player" Text("You won't receive further invites from \(name), and any games they currently share with you will be removed from this device.") } + .alert("Set Profile Name", isPresented: $showingNamePrompt) { + TextField("Name", text: $nameDraft) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + Button("Cancel", role: .cancel) {} + Button("Save") { + let trimmed = nameDraft.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + preferences.name = trimmed + nameDraft = trimmed + } + } + .keyboardShortcut(.defaultAction) + } message: { + Text("Enter the name other players will see.") + } } @ViewBuilder @@ -243,10 +262,24 @@ struct GameListView: View { } .overlay { if games.isEmpty { - ContentUnavailableView { - Label("No Puzzles", systemImage: "square.grid.3x3") - } description: { - Text("Tap the + button to start a new puzzle, or pull down to refresh.") + if preferences.hasName { + ContentUnavailableView { + Label("No Puzzles", systemImage: "square.grid.3x3") + } description: { + Text("Tap the + button to start a new puzzle, or pull down to refresh.") + } + } else { + ContentUnavailableView { + Label("Set Your Profile Name", systemImage: "person.text.rectangle") + } description: { + Text("Choose the name other players will see.") + } actions: { + Button { + nameDraft = "" + showingNamePrompt = true + } label: { Text("Set Profile Name") } + .buttonStyle(.borderedProminent) + } } } }