crossmate

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

commit ed2353e55709dd5a5397b9f46c4b144bc990aeb1
parent abef15d53bbeb091d96513bc4821b8dfb5b4ede2
Author: Michael Camilleri <[email protected]>
Date:   Tue, 14 Apr 2026 18:52:50 +0900

Improve support for importing XD puzzles

This commit improves the importing of XD puzzles so that users can add
their own crosswords to the app.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

Diffstat:
MCrossmate.xcodeproj/project.pbxproj | 4++++
MCrossmate/CrossmateApp.swift | 33+++++++++++++++++++++++++++++++++
MCrossmate/Info.plist | 33+++++++++++++++++++++++++++++++++
ACrossmate/Models/XDFileType.swift | 5+++++
MCrossmate/Views/ImportedBrowseView.swift | 31++++++++++++++++++++++++++++++-
Mproject.yml | 13+++++++++++++
6 files changed, 118 insertions(+), 1 deletion(-)

diff --git a/Crossmate.xcodeproj/project.pbxproj b/Crossmate.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 9789150602A3321D2E1E7E81 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BF60C84D92A9024AC1A53FC /* Media.xcassets */; }; 97D77230A98330DCB757FA81 /* sample.xd in Resources */ = {isa = PBXBuildFile; fileRef = 5C63A148D98E2D37EABF2CF5 /* sample.xd */; }; 98F8FBF324ED00D53FEBB1DB /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465F2BB469EFE84CF3733398 /* Game.swift */; }; + 9CB8808193A4A106D721D767 /* XDFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC61E2582D94B1E6EC67136 /* XDFileType.swift */; }; AA28425BD26F72A9E2B58742 /* BundledBrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4B7C6A8A23C6E4CCEC759F /* BundledBrowseView.swift */; }; AA992F67F509EC8EFDDFC7CB /* morning.xd in Resources */ = {isa = PBXBuildFile; fileRef = 0B73A791FD061430AE286E11 /* morning.xd */; }; AF4F1AE2A1F94E92C785C524 /* Square.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB851649DE78AAAC5A928C52 /* Square.swift */; }; @@ -116,6 +117,7 @@ E512D95B4518EE3DE6E350C0 /* PendingChangePayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChangePayload.swift; sourceTree = "<group>"; }; E524780E360E008FACE4F213 /* PendingChange+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PendingChange+Helpers.swift"; sourceTree = "<group>"; }; E7AFD37B03A1C2E23E5766E6 /* PuzzleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PuzzleSource.swift; sourceTree = "<group>"; }; + EAC61E2582D94B1E6EC67136 /* XDFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDFileType.swift; sourceTree = "<group>"; }; F2F7D62E5E9EE2AEFC8940F4 /* NewGameSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGameSheet.swift; sourceTree = "<group>"; }; F8E50E7BA98C88B4CAB39DC1 /* CellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellView.swift; sourceTree = "<group>"; }; F97B399E89BBB37730F2F1E9 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; }; @@ -175,6 +177,7 @@ E7AFD37B03A1C2E23E5766E6 /* PuzzleSource.swift */, DB851649DE78AAAC5A928C52 /* Square.swift */, B9031A1574C21866940F6A2C /* XD.swift */, + EAC61E2582D94B1E6EC67136 /* XDFileType.swift */, F93AC31640C40FCC039570A3 /* CrossmateModel.xcdatamodeld */, ); path = Models; @@ -417,6 +420,7 @@ F46733AB3C72749A4A992667 /* SyncState+Helpers.swift in Sources */, 5AB52A0BA2934922EB94E5D1 /* UbiquityMonitor.swift in Sources */, 7FFEACFC672925A0968ACC1C /* XD.swift in Sources */, + 9CB8808193A4A106D721D767 /* XDFileType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift @@ -137,6 +137,9 @@ struct RootView: View { } await refreshSnapshot() } + .onOpenURL { url in + handleOpenURL(url) + } .onChange(of: scenePhase) { _, newPhase in if newPhase == .active { Task { @@ -152,6 +155,36 @@ struct RootView: View { } } + private func handleOpenURL(_ url: URL) { + guard url.pathExtension.lowercased() == "xd" else { return } + + let needsAccess = url.startAccessingSecurityScopedResource() + defer { + if needsAccess { + url.stopAccessingSecurityScopedResource() + } + } + + let source: String + do { + source = try String(contentsOf: url, encoding: .utf8) + } catch { + return + } + + if ubiquityMonitor.containerAvailable { + try? ubiquityMonitor.importFile(from: url) + } + + if let existing = store.findGameID(matching: source) { + navigationPath.append(existing) + return + } + if let id = try? store.createGame(from: source) { + navigationPath.append(id) + } + } + private func refreshSnapshot() async { let snapshot = await syncEngine.diagnosticSnapshot() syncMonitor.updateSnapshot(snapshot) diff --git a/Crossmate/Info.plist b/Crossmate/Info.plist @@ -6,6 +6,19 @@ <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleDisplayName</key> <string>Crossmate</string> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeName</key> + <string>Crossmate Puzzle</string> + <key>LSHandlerRank</key> + <string>Owner</string> + <key>LSItemContentTypes</key> + <array> + <string>net.inqk.crossmate.xd</string> + </array> + </dict> + </array> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> @@ -48,5 +61,25 @@ <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> + <key>UTImportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.plain-text</string> + </array> + <key>UTTypeDescription</key> + <string>Crossmate Puzzle</string> + <key>UTTypeIdentifier</key> + <string>net.inqk.crossmate.xd</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>xd</string> + </array> + </dict> + </dict> + </array> </dict> </plist> diff --git a/Crossmate/Models/XDFileType.swift b/Crossmate/Models/XDFileType.swift @@ -0,0 +1,5 @@ +import UniformTypeIdentifiers + +extension UTType { + static let xdPuzzle = UTType(importedAs: "net.inqk.crossmate.xd") +} diff --git a/Crossmate/Views/ImportedBrowseView.swift b/Crossmate/Views/ImportedBrowseView.swift @@ -1,10 +1,12 @@ import SwiftUI +import UniformTypeIdentifiers struct ImportedBrowseView: View { let onSelected: (String) -> Void @Environment(UbiquityMonitor.self) private var monitor @State private var errorMessage: String? + @State private var showingImporter = false var body: some View { Group { @@ -24,10 +26,27 @@ struct ImportedBrowseView: View { ContentUnavailableView { Label("No Imported Puzzles", systemImage: "folder") } description: { - Text("Add .xd files to the Crossmate folder in Files to import them.") + Text("Add .xd files to the Crossmate folder in Files, or tap the import button to bring one in.") } } } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showingImporter = true + } label: { + Label("Import", systemImage: "square.and.arrow.down") + } + .disabled(!monitor.containerAvailable) + } + } + .fileImporter( + isPresented: $showingImporter, + allowedContentTypes: [.xdPuzzle], + allowsMultipleSelection: false + ) { result in + handleImport(result) + } .alert( "Couldn't Open Puzzle", isPresented: .init( @@ -42,6 +61,16 @@ struct ImportedBrowseView: View { } } + private func handleImport(_ result: Result<[URL], Error>) { + do { + let urls = try result.get() + guard let url = urls.first else { return } + try monitor.importFile(from: url) + } catch { + errorMessage = error.localizedDescription + } + } + private func open(_ item: UbiquityItem) { if !item.isDownloaded { monitor.startDownloading(item) diff --git a/project.yml b/project.yml @@ -40,6 +40,19 @@ targets: NSUbiquitousContainerIsDocumentScopePublic: true NSUbiquitousContainerName: Crossmate NSUbiquitousContainerSupportedFolderLevels: Any + UTImportedTypeDeclarations: + - UTTypeIdentifier: net.inqk.crossmate.xd + UTTypeDescription: Crossmate Puzzle + UTTypeConformsTo: + - public.plain-text + UTTypeTagSpecification: + public.filename-extension: + - xd + CFBundleDocumentTypes: + - CFBundleTypeName: Crossmate Puzzle + LSHandlerRank: Owner + LSItemContentTypes: + - net.inqk.crossmate.xd UILaunchScreen: {} UISupportedInterfaceOrientations: - UIInterfaceOrientationPortrait