crossmate

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

commit da4140cf18b644af269f1a71dc379317e929cc04
parent 99db6f67d273c85c0b335be5305bea5bc9d630ba
Author: Michael Camilleri <[email protected]>
Date:   Mon, 27 Apr 2026 09:31:48 +0900

Revert to previous share sheet

Diffstat:
MCrossmate.xcodeproj/project.pbxproj | 4----
MCrossmate/Sync/ShareController.swift | 26++------------------------
DCrossmate/Views/CloudSharingPopover.swift | 81-------------------------------------------------------------------------------
MCrossmate/Views/GameListView.swift | 33+++++++++------------------------
MCrossmate/Views/PuzzleView.swift | 36++++++++++++------------------------
5 files changed, 23 insertions(+), 157 deletions(-)

diff --git a/Crossmate.xcodeproj/project.pbxproj b/Crossmate.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ 4A89595E3F6AB50E1D9E6BA8 /* ImportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 462CE0FD356F6137C9BFD30F /* ImportService.swift */; }; 503229FF89FF7C29CEF4C16D /* Puzzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C8064F04FC6177D987ACA2 /* Puzzle.swift */; }; 54464FDFB8C71B0D3B4B61A2 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FEFF257CDDD3EF0E77CBF7 /* SettingsView.swift */; }; - 69B090CECDD8360288CE10DC /* CloudSharingPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8ECFCF3E5647F6121369963 /* CloudSharingPopover.swift */; }; 6BE7E91158F4DF1F71247C6D /* CellMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = B135C285570F91181595B405 /* CellMark.swift */; }; 6BFB5945FCCDEC64C431C2AC /* SyncDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C90BA83B6DC7F435A7CF24 /* SyncDiagnosticsView.swift */; }; 765B50552B13175F91A25EA1 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4BB9E160C3A59C653E7A9 /* GridView.swift */; }; @@ -155,7 +154,6 @@ DB55FC337CF72C650373210A /* PlayerColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerColor.swift; sourceTree = "<group>"; }; DB851649DE78AAAC5A928C52 /* Square.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Square.swift; sourceTree = "<group>"; }; E7AFD37B03A1C2E23E5766E6 /* PuzzleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PuzzleSource.swift; sourceTree = "<group>"; }; - E8ECFCF3E5647F6121369963 /* CloudSharingPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSharingPopover.swift; sourceTree = "<group>"; }; E9BD3F7EAFD344D8E10E8C3B /* ClueList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClueList.swift; sourceTree = "<group>"; }; EAC61E2582D94B1E6EC67136 /* XDFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDFileType.swift; sourceTree = "<group>"; }; EBC4C0246B2BCE686A3516DB /* GamePlayerColorStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamePlayerColorStoreTests.swift; sourceTree = "<group>"; }; @@ -279,7 +277,6 @@ 9A4B7C6A8A23C6E4CCEC759F /* BundledBrowseView.swift */, C0CAA5E17BD406AFEEF96196 /* CalendarDayCell.swift */, F8E50E7BA98C88B4CAB39DC1 /* CellView.swift */, - E8ECFCF3E5647F6121369963 /* CloudSharingPopover.swift */, E9BD3F7EAFD344D8E10E8C3B /* ClueList.swift */, 38DDAD9D6470A894C3FD6F90 /* GameListView.swift */, 5ABB557BA10CBE9909056882 /* GameShareItem.swift */, @@ -468,7 +465,6 @@ 6BE7E91158F4DF1F71247C6D /* CellMark.swift in Sources */, CFCA3C2C3CF6D88AE844D7AD /* CellView.swift in Sources */, CC250D6BA9B41CB722D8A62E /* CloudService.swift in Sources */, - 69B090CECDD8360288CE10DC /* CloudSharingPopover.swift in Sources */, B94919176DEC6EC31637B037 /* ClueList.swift in Sources */, DE9E4FAB098731A650F2D306 /* CrossmateApp.swift in Sources */, C30C0C4E54E4209A22843872 /* CrossmateModel.xcdatamodeld in Sources */, diff --git a/Crossmate/Sync/ShareController.swift b/Crossmate/Sync/ShareController.swift @@ -1,21 +1,15 @@ import CloudKit import CoreData import Foundation -import Observation /// Manages the lifecycle of `CKShare` objects for per-game zones. Responsible /// for creating zone-scoped shares and saving them to CloudKit, refreshing /// existing shares on re-present, and letting participants leave a shared game. @MainActor -@Observable final class ShareController { let container: CKContainer - @ObservationIgnored private let persistence: PersistenceController - @ObservationIgnored private let syncEngine: SyncEngine - - /// Latest known iCloud account status. `nil` until the first refresh. - /// Drives whether share UI can actually present anything. - private(set) var accountStatus: CKAccountStatus? + private let persistence: PersistenceController + private let syncEngine: SyncEngine enum ShareError: Error { case gameNotFound @@ -27,22 +21,6 @@ final class ShareController { self.container = container self.persistence = persistence self.syncEngine = syncEngine - Task { await self.refreshAccountStatus() } - Task { await self.observeAccountChanges() } - } - - func refreshAccountStatus() async { - do { - accountStatus = try await container.accountStatus() - } catch { - accountStatus = .couldNotDetermine - } - } - - private func observeAccountChanges() async { - for await _ in NotificationCenter.default.notifications(named: .CKAccountChanged) { - await refreshAccountStatus() - } } /// Returns the `CKShare` and container for `UICloudSharingController`'s diff --git a/Crossmate/Views/CloudSharingPopover.swift b/Crossmate/Views/CloudSharingPopover.swift @@ -1,81 +0,0 @@ -import CloudKit -import SwiftUI -import UIKit - -/// `UIViewControllerRepresentable` over `UICloudSharingController`, intended to -/// be the content of a SwiftUI `.popover`. Pair with -/// `.presentationCompactAdaptation(.popover)` to keep the popover anchored to -/// its source on iPhone instead of adapting to a bottom sheet. -struct CloudSharingPopover: UIViewControllerRepresentable { - let gameID: UUID - let title: String - let shareController: ShareController - let onDismiss: () -> Void - - func makeUIViewController(context: Context) -> UICloudSharingController { - let csc = UICloudSharingController { [shareController, gameID] (_, completion) in - Task { @MainActor in - do { - let (share, container) = try await shareController.prepareShare(for: gameID) - completion(share, container, nil) - } catch { - completion(nil, nil, error) - } - } - } - csc.delegate = context.coordinator - csc.availablePermissions = [.allowReadWrite, .allowPrivate] - return csc - } - - func updateUIViewController(_ uiViewController: UICloudSharingController, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator( - title: title, - gameID: gameID, - shareController: shareController, - onDismiss: onDismiss - ) - } - - @MainActor - final class Coordinator: NSObject, UICloudSharingControllerDelegate { - let title: String - let gameID: UUID - let shareController: ShareController - let onDismiss: () -> Void - - init( - title: String, - gameID: UUID, - shareController: ShareController, - onDismiss: @escaping () -> Void - ) { - self.title = title - self.gameID = gameID - self.shareController = shareController - self.onDismiss = onDismiss - } - - func itemTitle(for csc: UICloudSharingController) -> String? { title } - - func cloudSharingController( - _ csc: UICloudSharingController, - failedToSaveShareWithError error: Error - ) { - onDismiss() - } - - func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) { - if let share = csc.share { - try? shareController.persistShareName(share.recordID.recordName, for: gameID) - } - onDismiss() - } - - func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { - onDismiss() - } - } -} diff --git a/Crossmate/Views/GameListView.swift b/Crossmate/Views/GameListView.swift @@ -177,9 +177,6 @@ private struct GameRowView: View { var onResign: () -> Void = {} var onDelete: () -> Void = {} - @State private var showSharePopover = false - @State private var showSignInAlert = false - var body: some View { HStack(spacing: 12) { GridThumbnailView( @@ -209,13 +206,15 @@ private struct GameRowView: View { } Spacer() if game.isOwned { - Button { - if shareController.accountStatus == .available { - showSharePopover = true - } else { - showSignInAlert = true - } - } label: { + ShareLink( + item: GameShareItem( + gameID: game.id, + title: game.title, + container: shareController.container, + shareController: shareController + ), + preview: SharePreview(game.title) + ) { Image(systemName: "square.and.arrow.up") .font(.body) .frame(width: 32, height: 32) @@ -224,20 +223,6 @@ private struct GameRowView: View { .buttonStyle(.borderless) .tint(.secondary) .compositingGroup() - .popover(isPresented: $showSharePopover) { - CloudSharingPopover( - gameID: game.id, - title: game.title, - shareController: shareController, - onDismiss: { showSharePopover = false } - ) - .presentationCompactAdaptation(.popover) - } - .alert("Sign in to iCloud", isPresented: $showSignInAlert) { - Button("OK", role: .cancel) {} - } message: { - Text("Sharing requires an iCloud account. Sign in via Settings to share games.") - } } Menu { Button { onLeave() } label: { Label("Leave", systemImage: "rectangle.portrait.and.arrow.right") } diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift @@ -14,8 +14,6 @@ struct PuzzleView: View { @State private var isConfirmingLeave = false @State private var leaveError: String? @State private var isRevokedBannerDismissed = false - @State private var showSharePopover = false - @State private var showSignInAlert = false private func swatchImage(for color: PlayerColor) -> Image { let tint = UIColor(color.tint) @@ -185,34 +183,24 @@ struct PuzzleView: View { } .disabled(!(session.mutator.isShared && !session.mutator.isOwned) || shareController == nil) - Button("Share Game") { - if shareController?.accountStatus == .available { - showSharePopover = true - } else { - showSignInAlert = true + if let shareController { + ShareLink( + item: GameShareItem( + gameID: session.mutator.gameID, + title: session.puzzle.title, + container: shareController.container, + shareController: shareController + ), + preview: SharePreview(session.puzzle.title) + ) { + Text("Share Game") } + .disabled(!session.mutator.isOwned) } - .disabled(!session.mutator.isOwned || shareController == nil) } } label: { Label("Players", systemImage: "person.2") } - .popover(isPresented: $showSharePopover) { - if let shareController { - CloudSharingPopover( - gameID: session.mutator.gameID, - title: session.puzzle.title, - shareController: shareController, - onDismiss: { showSharePopover = false } - ) - .presentationCompactAdaptation(.popover) - } - } - .alert("Sign in to iCloud", isPresented: $showSignInAlert) { - Button("OK", role: .cancel) {} - } message: { - Text("Sharing requires an iCloud account. Sign in via Settings to share games.") - } } } .task {