У меня есть следующая настройка:
- Разрешения включают iCloud с включенными документами
- Использование FileManager.default.url(forUbiquityContainerIdentifier: nil) для получения контейнера
- Убедитесь, что каталог Documents существует внутри контейнера.
- Успешная запись тестовых файлов с помощью copyItem.
- Удаление приложения и установка новой.
- Увеличение сборки и версии и новая установка.
- Использование TeamID (но это не требуется при использовании автоматически создаваемого iCloud...)
- Это тестовое приложение позволяет отображать все сохраненные файлы и создавать новые в документах.
[ERROR] couldn't fetch remote operation IDs: NSError: Cocoa 257 "The file couldn’t be opened because you don’t have permission to view it."
"Error returned from daemon: Error Domain=com.apple.accounts Code=7 "(null)"
На моем iPhone файл загружается нормально, и я вижу его загруженным на своем Mac. Но в приложении «Файлы» (iOS и macOS) папка приложения не отображается в iCloud Drive.
Вопросы:
- Нужно ли сделать шаг, чтобы открыть папку «Документы» приложения в iCloud Drive?
- Эти ошибки указывают на неверную конфигурацию (Cocoa 257 / код учетной записи 7) или они безвредны?
- Ожидается ли это при запуске на устройстве с поддержкой разработчика?
Что действительно раздражает, так это то, что я знаю, что все это синхронизируется. Если я спамлю кнопку «Загрузить тестовый файл» на своем iPhone, а затем перейду в «Настройки» > «iCloud» > «Управление хранилищем» > «Приложение» на своем Mac — размер увеличится. И если я удалю из iCloud (на Mac), список приложений iOS очистится. Итак, они разговаривают, а хранилище работает.
import SwiftUI
import UniformTypeIdentifiers
@main
struct testApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State private var cloudService: CloudService = .init()
@State private var documents: [CloudDocument] = []
@State private var isRefreshing = false
var body: some View {
NavigationView {
VStack {
Button("Upload Test File") {
uploadTestFile()
}
.padding()
Button("Refresh Documents") {
refreshDocuments()
}
.padding()
if isRefreshing {
ProgressView("Loading documents...")
.padding()
}
List(documents) { doc in
VStack(alignment: .leading) {
Text(doc.name)
.font(.headline)
Text("Status: \(doc.status)")
.font(.caption)
.foregroundColor(doc.isDownloaded ? .green : .orange)
if let size = doc.size {
Text("Size: \(ByteCountFormatter.string(fromByteCount: size, countStyle: .file))")
.font(.caption2)
}
}
}
}
.navigationTitle("iCloud Documents")
.onAppear {
// Give iCloud a moment to initialize
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
refreshDocuments()
}
}
}
}
private func uploadTestFile() {
guard cloudService.isCloudAvailable else {
print("
return
}
// Create a temporary test file
let temp = URL.temporaryDirectory.appendingPathComponent("Hello iCloud \(Date().timeIntervalSince1970).txt")
let message = "Hello! Time: \(Date())"
do {
try message.write(to: temp, atomically: true, encoding: .utf8)
// Copy to iCloud
let target = try cloudService.copyToCloud(from: temp)
print("
// Start download to ensure it's available
try cloudService.startDownloadIfNeeded(for: target)
// Monitor upload status
cloudService.monitorUpload(of: target) { uploaded, error in
if let error = error {
print("
} else if uploaded {
print("
// Refresh our document list
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
refreshDocuments()
}
}
}
// Cleanup temp file
try? FileManager.default.removeItem(at: temp)
} catch {
print("
}
}
private func refreshDocuments() {
isRefreshing = true
Task {
do {
let docs = try await cloudService.fetchDocuments()
await MainActor.run {
self.documents = docs
self.isRefreshing = false
}
} catch {
print("
await MainActor.run {
self.isRefreshing = false
}
}
}
}
}
struct CloudDocument: Identifiable {
let id = UUID()
let name: String
let url: URL
let isDownloaded: Bool
let size: Int64?
let status: String
}
@Observable
final class CloudService {
static let shared = CloudService()
private(set) var containerURL: URL?
private(set) var documentsDirectory: URL?
private var metadataQuery: NSMetadataQuery?
init() {
refreshContainer()
observeIdentityChanges()
}
var isCloudAvailable: Bool {
documentsDirectory != nil
}
// MARK: - Container Setup
private func refreshContainer() {
// Add a small delay to ensure iCloud is ready
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// Try with nil first (uses default container)
var containerURL: URL?
// First try the explicit container ID
containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
// If that fails, try with nil (uses the first/default container)
if containerURL == nil {
containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
print("
}
if let url = containerURL {
self.containerURL = url
self.documentsDirectory = url.appendingPathComponent("Documents", isDirectory: true)
self.ensureDocumentsDirectory()
print("
// Check if we can actually write to it
self.testContainerAccess()
} else {
self.containerURL = nil
self.documentsDirectory = nil
print("
print(" • iCloud Drive enabled in Settings")
print(" • App ID configured in App Store Connect")
print(" • Entitlements match container ID")
print(" • User signed into iCloud")
}
}
}
private func testContainerAccess() {
guard let docsDir = documentsDirectory else { return }
let testFile = docsDir.appendingPathComponent(".test_access")
do {
try "test".write(to: testFile, atomically: true, encoding: .utf8)
try FileManager.default.removeItem(at: testFile)
print("
} catch {
print("
print(" This indicates a permissions or configuration issue")
}
}
private func ensureDocumentsDirectory() {
guard let dir = documentsDirectory else { return }
do {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
print("
} catch {
print("
}
}
private func observeIdentityChanges() {
NotificationCenter.default.addObserver(
forName: .NSUbiquityIdentityDidChange,
object: nil,
queue: .main
) { [weak self] _ in
print("
self?.refreshContainer()
}
}
// MARK: - File Operations
func copyToCloud(from localURL: URL) throws -> URL {
guard let docs = documentsDirectory else {
throw CocoaError(.fileNoSuchFile, userInfo: [
NSLocalizedDescriptionKey: "iCloud Drive is not available"
])
}
var target = docs.appendingPathComponent(localURL.lastPathComponent)
// Remove existing file if it exists
if FileManager.default.fileExists(atPath: target.path) {
try FileManager.default.removeItem(at: target)
}
try FileManager.default.copyItem(at: localURL, to: target)
// Set the file to be stored in iCloud
var resourceValues = URLResourceValues()
resourceValues.hasHiddenExtension = false
try target.setResourceValues(resourceValues)
return target
}
// MARK: - Document Discovery
func fetchDocuments() async throws -> [CloudDocument] {
guard let docsDir = documentsDirectory else {
throw CocoaError(.fileNoSuchFile, userInfo: [
NSLocalizedDescriptionKey: "iCloud Drive is not available"
])
}
return try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
do {
// First, try to get directory contents
let contents = try FileManager.default.contentsOfDirectory(
at: docsDir,
includingPropertiesForKeys: [
.ubiquitousItemDownloadingStatusKey,
.fileSizeKey,
.ubiquitousItemHasUnresolvedConflictsKey
]
)
var documents: [CloudDocument] = []
for url in contents {
// Skip hidden files and directories
if url.lastPathComponent.hasPrefix(".") { continue }
var isDownloaded = false
var size: Int64? = nil
var status = "Unknown"
// Get resource values
let resourceValues = try? url.resourceValues(forKeys: [
.ubiquitousItemDownloadingStatusKey,
.fileSizeKey,
.ubiquitousItemHasUnresolvedConflictsKey
])
if let values = resourceValues {
size = values.fileSize.map { Int64($0) }
if let downloadStatus = values.ubiquitousItemDownloadingStatus {
switch downloadStatus {
case .current:
status = "Downloaded"
isDownloaded = true
case .downloaded:
status = "Downloaded"
isDownloaded = true
case .notDownloaded:
status = "In Cloud"
isDownloaded = false
default:
status = "Downloading"
isDownloaded = false
}
}
if values.ubiquitousItemHasUnresolvedConflicts == true {
status += " (Conflict)"
}
}
let doc = CloudDocument(
name: url.lastPathComponent,
url: url,
isDownloaded: isDownloaded,
size: size,
status: status
)
documents.append(doc)
}
continuation.resume(returning: documents)
} catch {
print("
continuation.resume(throwing: error)
}
}
}
}
// MARK: - Download Management
func startDownloadIfNeeded(for url: URL) throws {
var isDownloaded = false
if let resourceValues = try? url.resourceValues(forKeys: [.ubiquitousItemDownloadingStatusKey]) {
if let downloadStatus = resourceValues.ubiquitousItemDownloadingStatus {
switch downloadStatus {
case .current, .downloaded:
isDownloaded = true
default:
isDownloaded = false
}
}
}
if !isDownloaded {
do {
try FileManager.default.startDownloadingUbiquitousItem(at: url)
print("
} catch {
print("
throw error
}
}
}
// MARK: - Upload Monitoring
func monitorUpload(of url: URL, handler: @escaping (Bool, Error?) -> Void) {
metadataQuery?.stop()
metadataQuery = NSMetadataQuery()
guard let query = metadataQuery else { return }
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
query.predicate = NSPredicate(format: "%K == %@", NSMetadataItemFSNameKey, url.lastPathComponent)
NotificationCenter.default.addObserver(
forName: .NSMetadataQueryDidUpdate,
object: query,
queue: .main
) { [weak self] _ in
self?.handleMetadataUpdate(query: query, handler: handler)
}
query.start()
}
private func handleMetadataUpdate(query: NSMetadataQuery, handler: @escaping (Bool, Error?) -> Void) {
query.disableUpdates()
defer { query.enableUpdates() }
for item in query.results.compactMap({ $0 as? NSMetadataItem }) {
let isUploaded = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadedKey) as? Bool ?? false
let error = item.value(forAttribute: NSMetadataUbiquitousItemUploadingErrorKey) as? Error
if isUploaded || error != nil {
handler(isUploaded, error)
// Stop monitoring after completion or error
query.stop()
}
}
}
}
Подробнее здесь: https://stackoverflow.com/questions/797 ... -files-app
Мобильная версия