From b469e1130b06322dd77cd345c516661bb918024a Mon Sep 17 00:00:00 2001 From: Chung Shing Hin Date: Thu, 25 May 2023 15:45:21 +0800 Subject: [PATCH 1/2] app: Handle iCloud files gracefully #840 --- Code.xcodeproj/project.pbxproj | 6 +++ .../Local/LocalFileSystemProvider.swift | 22 +++++++-- CodeApp/Managers/MainApp.swift | 19 +++++-- CodeApp/Modifiers/deferredRendering.swift | 49 +++++++++++++++++++ CodeApp/Views/EditorView.swift | 12 ++--- CodeApp/Views/InfiniteProgressView.swift | 4 +- 6 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 CodeApp/Modifiers/deferredRendering.swift diff --git a/Code.xcodeproj/project.pbxproj b/Code.xcodeproj/project.pbxproj index 716f78aed..aad1efa90 100644 --- a/Code.xcodeproj/project.pbxproj +++ b/Code.xcodeproj/project.pbxproj @@ -661,6 +661,8 @@ 9F046C3B29223D1600BDE4E9 /* RemoteExecutionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F046C3929223D1600BDE4E9 /* RemoteExecutionExtension.swift */; }; 9F046C3D29223E1500BDE4E9 /* CodeAppExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F046C3C29223E1500BDE4E9 /* CodeAppExtension.swift */; }; 9F046C3E29223E1500BDE4E9 /* CodeAppExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F046C3C29223E1500BDE4E9 /* CodeAppExtension.swift */; }; + 9F29759C2A1F488E007FDB3D /* deferredRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F29759B2A1F488E007FDB3D /* deferredRendering.swift */; }; + 9F29759D2A1F488E007FDB3D /* deferredRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F29759B2A1F488E007FDB3D /* deferredRendering.swift */; }; 9F37391E293F37F8006886C1 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37391D293F37F8006886C1 /* ThemeManager.swift */; }; 9F37391F293F37F8006886C1 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37391D293F37F8006886C1 /* ThemeManager.swift */; }; 9F3C2DC82918A31000BFF14C /* hiddenScrollableContentBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3C2DC72918A31000BFF14C /* hiddenScrollableContentBackground.swift */; }; @@ -1693,6 +1695,7 @@ 9F046C3429222D8E00BDE4E9 /* ExtensionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionManager.swift; sourceTree = ""; }; 9F046C3929223D1600BDE4E9 /* RemoteExecutionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteExecutionExtension.swift; sourceTree = ""; }; 9F046C3C29223E1500BDE4E9 /* CodeAppExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeAppExtension.swift; sourceTree = ""; }; + 9F29759B2A1F488E007FDB3D /* deferredRendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = deferredRendering.swift; sourceTree = ""; }; 9F37391D293F37F8006886C1 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; 9F3C2DC72918A31000BFF14C /* hiddenScrollableContentBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = hiddenScrollableContentBackground.swift; sourceTree = ""; }; 9F3C2DE92918E19B00BFF14C /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; @@ -2464,6 +2467,7 @@ children = ( 9F3C2DC72918A31000BFF14C /* hiddenScrollableContentBackground.swift */, 94A347BC293DE24B00A59658 /* hiddenSystemOverlays.swift */, + 9F29759B2A1F488E007FDB3D /* deferredRendering.swift */, ); path = Modifiers; sourceTree = ""; @@ -3008,6 +3012,7 @@ 9F3C2DEB2918E19B00BFF14C /* Theme.swift in Sources */, 94196957280316C7008AAEB2 /* SettingsView.swift in Sources */, 94196958280316C7008AAEB2 /* wasm.swift in Sources */, + 9F29759D2A1F488E007FDB3D /* deferredRendering.swift in Sources */, 94196959280316C7008AAEB2 /* SearchManager.swift in Sources */, 9419695A280316C7008AAEB2 /* ArchiveDir.swift in Sources */, 9419695B280316C7008AAEB2 /* MarkdownView.swift in Sources */, @@ -3162,6 +3167,7 @@ 9F3C2DEA2918E19B00BFF14C /* Theme.swift in Sources */, 94A777F6257B9260008FE7B2 /* SettingsView.swift in Sources */, 94CF58E1265E8A4B00CB6A4B /* wasm.swift in Sources */, + 9F29759C2A1F488E007FDB3D /* deferredRendering.swift in Sources */, 947BF349262453040015DAEB /* SearchManager.swift in Sources */, 94A7781E257BC473008FE7B2 /* ArchiveDir.swift in Sources */, 94A5682B257CBDE4008A6530 /* MarkdownView.swift in Sources */, diff --git a/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift b/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift index 22c3588b9..a1a0343f5 100644 --- a/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift +++ b/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift @@ -83,10 +83,24 @@ class LocalFileSystemProvider: FileSystemProvider { } func contents(at: URL, completionHandler: @escaping (Data?, Error?) -> Void) { - do { - let data = try Data(contentsOf: at) - completionHandler(data, nil) - } catch { + // Using a FileCoordinator allows downloading iCloud file using a completion handler pattern + // Reference: https://developer.apple.com/forums/thread/681520 + var error: NSError? + let fileCoordinator = NSFileCoordinator() + + fileCoordinator.coordinate( + readingItemAt: at, options: .withoutChanges, error: &error + ) { newURL in + do { + let data = try Data(contentsOf: newURL) + completionHandler(data, nil) + } catch { + completionHandler(nil, error) + } + return + } + + if let error { completionHandler(nil, error) } } diff --git a/CodeApp/Managers/MainApp.swift b/CodeApp/Managers/MainApp.swift index 484ab23e5..b23444d42 100644 --- a/CodeApp/Managers/MainApp.swift +++ b/CodeApp/Managers/MainApp.swift @@ -753,17 +753,28 @@ class MainApp: ObservableObject { urlQueue.append(url) throw AppError.editorIsNotReady } + var url = url + if url.pathExtension == "icloud" { + let originalFileName = String( + url.lastPathComponent.dropFirst(".".count).dropLast(".icloud".count)) + url = url.deletingLastPathComponent().appendingPathComponent(originalFileName) + } if let existingEditor = try? openEditorForURL(url: url) { return existingEditor } // TODO: Avoid reading the same file twice - if let textEditor = try? await createTextEditorFromURL(url: url) { + do { + let textEditor = try await createTextEditorFromURL(url: url) appendAndFocusNewEditor(editor: textEditor, alwaysInNewTab: alwaysInNewTab) return textEditor + } catch NSFileProviderError.serverUnreachable { + throw NSFileProviderError(.serverUnreachable) + } catch { + // Otherwise, fallback to using extensions + let editor = try createExtensionEditorFromURL(url: url) + appendAndFocusNewEditor(editor: editor, alwaysInNewTab: alwaysInNewTab) + return editor } - let editor = try createExtensionEditorFromURL(url: url) - appendAndFocusNewEditor(editor: editor, alwaysInNewTab: alwaysInNewTab) - return editor } @MainActor diff --git a/CodeApp/Modifiers/deferredRendering.swift b/CodeApp/Modifiers/deferredRendering.swift new file mode 100644 index 000000000..4dec89659 --- /dev/null +++ b/CodeApp/Modifiers/deferredRendering.swift @@ -0,0 +1,49 @@ +// +// deferredRendering.swift +// Code +// +// Created by Ken Chung on 25/5/2023. +// + +import SwiftUI + +// Reference: https://stackoverflow.com/questions/59731724/swiftui-show-custom-view-with-delay + +/// A ViewModifier that defers its rendering until after the provided threshold surpasses +private struct DeferredViewModifier: ViewModifier { + + // MARK: API + + let threshold: Double + + // MARK: - ViewModifier + + func body(content: Content) -> some View { + _content(content) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + threshold) { + self.shouldRender = true + } + } + } + + // MARK: - Private + + @ViewBuilder + private func _content(_ content: Content) -> some View { + if shouldRender { + content + } else { + content + .hidden() + } + } + + @State private var shouldRender = false +} + +extension View { + func deferredRendering(for seconds: Double) -> some View { + modifier(DeferredViewModifier(threshold: seconds)) + } +} diff --git a/CodeApp/Views/EditorView.swift b/CodeApp/Views/EditorView.swift index 083e7dc85..1a3fe17fb 100644 --- a/CodeApp/Views/EditorView.swift +++ b/CodeApp/Views/EditorView.swift @@ -39,18 +39,16 @@ struct EditorView: View { .background(Color.init(id: "editor.background")) } } else if let editor = App.activeEditor { - editor.view - - VStack { - InfinityProgressView(enabled: App.workSpaceStorage.editorIsBusy) - Spacer() - } - } else { DescriptionText("You don't have any open editor.") } + VStack { + InfinityProgressView(enabled: App.workSpaceStorage.editorIsBusy) + Spacer() + } + } .onReceive( NotificationCenter.default.publisher( diff --git a/CodeApp/Views/InfiniteProgressView.swift b/CodeApp/Views/InfiniteProgressView.swift index 565295a10..ddef2b66b 100644 --- a/CodeApp/Views/InfiniteProgressView.swift +++ b/CodeApp/Views/InfiniteProgressView.swift @@ -10,7 +10,7 @@ import SwiftUI struct InfinityProgressView: View { @State private var atLeading: Bool = false - let enabled: Bool + var enabled: Bool private var repeatingAnimation: Animation { Animation @@ -26,10 +26,12 @@ struct InfinityProgressView: View { .frame(maxWidth: 30, maxHeight: 3) .offset(x: atLeading ? geometry.size.width - 30 : 0, y: 0) .onAppear { + atLeading = false withAnimation(repeatingAnimation) { atLeading.toggle() } } + .deferredRendering(for: 0.5) } } .frame(height: 8) From 51aa6ff0e9e601085a4efde645c4bc07a5fd1a5b Mon Sep 17 00:00:00 2001 From: Chung Shing Hin Date: Thu, 25 May 2023 15:53:59 +0800 Subject: [PATCH 2/2] ci: Fix formatting for latest release of swift-format --- CodeApp/Managers/WebViewBase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeApp/Managers/WebViewBase.swift b/CodeApp/Managers/WebViewBase.swift index 58a489ce9..7cec60566 100644 --- a/CodeApp/Managers/WebViewBase.swift +++ b/CodeApp/Managers/WebViewBase.swift @@ -82,7 +82,7 @@ class WebViewBase: KBWebViewBase { let newMethod = class_getInstanceMethod( WebViewBase.self, #selector(WebViewBase.getCustomInputAccessoryView)) class_addMethod( - newClass.self, #selector(getter:UIResponder.inputAccessoryView), + newClass.self, #selector(getter: UIResponder.inputAccessoryView), method_getImplementation(newMethod!), method_getTypeEncoding(newMethod!)) objc_registerClassPair(newClass!)