From 11200a01a0cf2ad453a7a2f6d4004e129d8d2ca6 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sun, 1 May 2022 12:06:36 -0400 Subject: [PATCH 01/15] Initial work on presenting alert on error --- Shared/Account/AccountLoginView.swift | 19 ++- Shared/Account/AccountModel.swift | 52 ------ Shared/Account/AccountView.swift | 1 + Shared/ErrorHandling/ErrorConstants.swift | 150 ++++++++++++++++++ Shared/ErrorHandling/ErrorHandling.swift | 42 +++++ Shared/Extensions/WriteFreelyModel+API.swift | 14 +- .../WriteFreelyModel+APIHandlers.swift | 39 ++--- .../WriteFreelyModel+Keychain.swift | 12 +- Shared/LocalStorageManager.swift | 12 +- Shared/Models/WriteFreelyModel.swift | 29 +++- Shared/Navigation/ContentView.swift | 2 + Shared/PostList/PostListView.swift | 44 +++-- .../project.pbxproj | 24 +++ .../xcschemes/xcschememanagement.plist | 6 +- 14 files changed, 301 insertions(+), 145 deletions(-) create mode 100644 Shared/ErrorHandling/ErrorConstants.swift create mode 100644 Shared/ErrorHandling/ErrorHandling.swift diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift index 8284b95..c7ea5ea 100644 --- a/Shared/Account/AccountLoginView.swift +++ b/Shared/Account/AccountLoginView.swift @@ -2,6 +2,7 @@ import SwiftUI struct AccountLoginView: View { @EnvironmentObject var model: WriteFreelyModel + @EnvironmentObject var errorHandling: ErrorHandling @State private var alertMessage: String = "" @State private var username: String = "" @@ -76,8 +77,7 @@ struct AccountLoginView: View { as: username, password: password ) } else { - model.loginErrorMessage = AccountError.invalidServerURL.localizedDescription - model.isPresentingLoginErrorAlert = true + self.errorHandling.handle(error: AccountError.invalidServerURL) } }, label: { Text("Log In") @@ -88,12 +88,15 @@ struct AccountLoginView: View { .padding() } } - .alert(isPresented: $model.isPresentingLoginErrorAlert) { - Alert( - title: Text("Error Logging In"), - message: Text(model.loginErrorMessage ?? "An unknown error occurred while trying to login."), - dismissButton: .default(Text("OK")) - ) + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError) + } + model.hasError = false + } } } } diff --git a/Shared/Account/AccountModel.swift b/Shared/Account/AccountModel.swift index 94171ec..25103ba 100644 --- a/Shared/Account/AccountModel.swift +++ b/Shared/Account/AccountModel.swift @@ -1,58 +1,6 @@ import SwiftUI import WriteFreely -enum AccountError: Error { - case invalidPassword - case usernameNotFound - case serverNotFound - case invalidServerURL - case couldNotSaveTokenToKeychain - case couldNotFetchTokenFromKeychain - case couldNotDeleteTokenFromKeychain -} - -extension AccountError: LocalizedError { - public var errorDescription: String? { - switch self { - case .serverNotFound: - return NSLocalizedString( - "The server could not be found. Please check the information you've entered and try again.", - comment: "" - ) - case .invalidPassword: - return NSLocalizedString( - "Invalid password. Please check that you've entered your password correctly and try logging in again.", - comment: "" - ) - case .usernameNotFound: - return NSLocalizedString( - "Username not found. Did you use your email address by mistake?", - comment: "" - ) - case .invalidServerURL: - return NSLocalizedString( - "Please enter a valid instance domain name. It should look like \"https://example.com\" or \"write.as\".", // swiftlint:disable:this line_length - comment: "" - ) - case .couldNotSaveTokenToKeychain: - return NSLocalizedString( - "There was a problem trying to save your access token to the device, please try logging in again.", - comment: "" - ) - case .couldNotFetchTokenFromKeychain: - return NSLocalizedString( - "There was a problem trying to fetch your access token from the device, please try logging in again.", - comment: "" - ) - case .couldNotDeleteTokenFromKeychain: - return NSLocalizedString( - "There was a problem trying to delete your access token from the device, please try logging out again.", - comment: "" - ) - } - } -} - struct AccountModel { @AppStorage(WFDefaults.isLoggedIn, store: UserDefaults.shared) var isLoggedIn: Bool = false private let defaults = UserDefaults.shared diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index 4ff4527..9241026 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -13,6 +13,7 @@ struct AccountView: View { .padding() } else { AccountLoginView() + .withErrorHandling() .padding(.top) } } diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift new file mode 100644 index 0000000..eb1f037 --- /dev/null +++ b/Shared/ErrorHandling/ErrorConstants.swift @@ -0,0 +1,150 @@ +import Foundation + +// MARK: - Network Errors + +enum NetworkError: Error { + case noConnectionError +} + +extension NetworkError: LocalizedError { + public var errorDescription: String? { + switch self { + case .noConnectionError: + return NSLocalizedString( + "There is no internet connection at the moment. Please reconnect or try again later.", + comment: "" + ) + } + } +} + +// MARK: - Keychain Errors + +enum KeychainError: Error { + case couldNotStoreAccessToken + case couldNotPurgeAccessToken + case couldNotFetchAccessToken +} + +extension KeychainError: LocalizedError { + public var errorDescription: String? { + switch self { + case .couldNotStoreAccessToken: + return NSLocalizedString("There was a problem storing your access token in the Keychain.", comment: "") + case .couldNotPurgeAccessToken: + return NSLocalizedString("Something went wrong purging the token from the Keychain.", comment: "") + case .couldNotFetchAccessToken: + return NSLocalizedString("Something went wrong fetching the token from the Keychain.", comment: "") + } + } +} + +// MARK: - Account Errors + +enum AccountError: Error { + case invalidPassword + case usernameNotFound + case serverNotFound + case invalidServerURL + case unknownLoginError + case genericAuthError +} + +extension AccountError: LocalizedError { + public var errorDescription: String? { + switch self { + case .serverNotFound: + return NSLocalizedString( + "The server could not be found. Please check the information you've entered and try again.", + comment: "" + ) + case .invalidPassword: + return NSLocalizedString( + "Invalid password. Please check that you've entered your password correctly and try logging in again.", + comment: "" + ) + case .usernameNotFound: + return NSLocalizedString( + "Username not found. Did you use your email address by mistake?", + comment: "" + ) + case .invalidServerURL: + return NSLocalizedString( + "Please enter a valid instance domain name. It should look like \"https://example.com\" or \"write.as\".", // swiftlint:disable:this line_length + comment: "" + ) + case .genericAuthError: + return NSLocalizedString("Something went wrong, please try logging in again.", comment: "") + case .unknownLoginError: + return NSLocalizedString("An unknown error occurred while trying to login.", comment: "") + } + } +} + +// MARK: - Local Store Errors + +enum LocalStoreError: Error { + case couldNotSaveContext + case couldNotFetchCollections + case couldNotFetchPosts(String) + case couldNotPurgePublishedPosts + case couldNotPurgeCollections + case couldNotLoadStore(String) + case couldNotMigrateStore(String) + case couldNotDeleteStoreAfterMigration(String) + case genericError(String) +} + +extension LocalStoreError: LocalizedError { + public var errorDescription: String? { + switch self { + case .couldNotSaveContext: + return NSLocalizedString("Error saving context", comment: "") + case .couldNotFetchCollections: + return NSLocalizedString("Failed to fetch blogs from local store.", comment: "") + case .couldNotFetchPosts(let postFilter): + if postFilter.isEmpty { + return NSLocalizedString("Failed to fetch posts from local store.", comment: "") + } else { + return NSLocalizedString("Failed to fetch \(postFilter) posts from local store.", comment: "") + } + case .couldNotPurgePublishedPosts: + return NSLocalizedString("Failed to purge published posts from local store.", comment: "") + case .couldNotPurgeCollections: + return NSLocalizedString("Failed to purge cached collections", comment: "") + case .couldNotLoadStore(let errorDescription): + return NSLocalizedString("Something went wrong loading local store: \(errorDescription)", comment: "") + case .couldNotMigrateStore(let errorDescription): + return NSLocalizedString("Something went wrong migrating local store: \(errorDescription)", comment: "") + case .couldNotDeleteStoreAfterMigration(let errorDescription): + return NSLocalizedString("Something went wrong deleting old store: \(errorDescription)", comment: "") + case .genericError(let customContent): + if customContent.isEmpty { + return NSLocalizedString("Something went wrong accessing device storage", comment: "") + } else { + return NSLocalizedString(customContent, comment: "") + } + } + } +} + +// MARK: - Application Errors + +enum AppError: Error { + case couldNotGetLoggedInClient + case couldNotGetPostId + case genericError +} + +extension AppError: LocalizedError { + public var errorDescription: String? { + switch self { + case .couldNotGetLoggedInClient: + return NSLocalizedString("Something went wrong trying to access the WriteFreely client.", comment: "") + case .couldNotGetPostId: + return NSLocalizedString("Something went wrong trying to get the post's unique ID.", comment: "") + case .genericError: + return NSLocalizedString("Something went wrong", comment: "") + } + } +} diff --git a/Shared/ErrorHandling/ErrorHandling.swift b/Shared/ErrorHandling/ErrorHandling.swift new file mode 100644 index 0000000..8644b33 --- /dev/null +++ b/Shared/ErrorHandling/ErrorHandling.swift @@ -0,0 +1,42 @@ +// Based on https://www.ralfebert.com/swiftui/generic-error-handling/ + +import SwiftUI + +struct ErrorAlert: Identifiable { + var id = UUID() + var message: String + var dismissAction: (() -> Void)? +} + +class ErrorHandling: ObservableObject { + @Published var currentAlert: ErrorAlert? + + func handle(error: Error) { + currentAlert = ErrorAlert(message: error.localizedDescription) + } +} + +struct HandleErrorByShowingAlertViewModifier: ViewModifier { + @StateObject var errorHandling = ErrorHandling() + + func body(content: Content) -> some View { + content + .environmentObject(errorHandling) + .background( + EmptyView() + .alert(item: $errorHandling.currentAlert) { currentAlert in + Alert(title: Text("Error"), + message: Text(currentAlert.message), + dismissButton: .default(Text("OK")) { + currentAlert.dismissAction?() + }) + } + ) + } +} + +extension View { + func withErrorHandling() -> some View { + modifier(HandleErrorByShowingAlertViewModifier()) + } +} diff --git a/Shared/Extensions/WriteFreelyModel+API.swift b/Shared/Extensions/WriteFreelyModel+API.swift index 939cf76..23e3b61 100644 --- a/Shared/Extensions/WriteFreelyModel+API.swift +++ b/Shared/Extensions/WriteFreelyModel+API.swift @@ -4,7 +4,7 @@ import WriteFreely extension WriteFreelyModel { func login(to server: URL, as username: String, password: String) { if !hasNetworkConnection { - isPresentingNetworkErrorAlert = true + self.currentError = NetworkError.noConnectionError return } let secureProtocolPrefix = "https://" @@ -30,7 +30,7 @@ extension WriteFreelyModel { func logout() { if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + self.currentError = NetworkError.noConnectionError return } guard let loggedInClient = client else { @@ -47,7 +47,7 @@ extension WriteFreelyModel { func fetchUserCollections() { if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + self.currentError = NetworkError.noConnectionError return } guard let loggedInClient = client else { return } @@ -60,7 +60,7 @@ extension WriteFreelyModel { func fetchUserPosts() { if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + self.currentError = NetworkError.noConnectionError return } guard let loggedInClient = client else { return } @@ -75,7 +75,7 @@ extension WriteFreelyModel { postToUpdate = nil if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + self.currentError = NetworkError.noConnectionError return } guard let loggedInClient = client else { return } @@ -120,7 +120,7 @@ extension WriteFreelyModel { func updateFromServer(post: WFAPost) { if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + self.currentError = NetworkError.noConnectionError return } guard let loggedInClient = client else { return } @@ -135,7 +135,7 @@ extension WriteFreelyModel { func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) { if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + self.currentError = NetworkError.noConnectionError return } guard let loggedInClient = client, diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift index 804dd41..6d0d147 100644 --- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -16,33 +16,18 @@ extension WriteFreelyModel { self.account.login(user) } } catch { - DispatchQueue.main.async { - self.loginErrorMessage = "There was a problem storing your access token to the Keychain." - self.isPresentingLoginErrorAlert = true - } + self.currentError = KeychainError.couldNotStoreAccessToken } } catch WFError.notFound { - DispatchQueue.main.async { - self.loginErrorMessage = AccountError.usernameNotFound.localizedDescription - self.isPresentingLoginErrorAlert = true - } + self.currentError = AccountError.usernameNotFound } catch WFError.unauthorized { - DispatchQueue.main.async { - self.loginErrorMessage = AccountError.invalidPassword.localizedDescription - self.isPresentingLoginErrorAlert = true - } + self.currentError = AccountError.invalidPassword } catch { if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == -1003 { - DispatchQueue.main.async { - self.loginErrorMessage = AccountError.serverNotFound.localizedDescription - self.isPresentingLoginErrorAlert = true - } + self.currentError = AccountError.serverNotFound } else { - DispatchQueue.main.async { - self.loginErrorMessage = error.localizedDescription - self.isPresentingLoginErrorAlert = true - } + self.currentError = error } } } @@ -59,7 +44,7 @@ extension WriteFreelyModel { self.posts.purgePublishedPosts() } } catch { - print("Something went wrong purging the token from the Keychain.") + print(KeychainError.couldNotPurgeAccessToken.localizedDescription) } } catch WFError.notFound { // The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with @@ -74,7 +59,7 @@ extension WriteFreelyModel { self.posts.purgePublishedPosts() } } catch { - print("Something went wrong purging the token from the Keychain.") + print(KeychainError.couldNotPurgeAccessToken.localizedDescription) } } catch { // We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here, @@ -113,10 +98,7 @@ extension WriteFreelyModel { LocalStorageManager.standard.saveContext() } } catch WFError.unauthorized { - DispatchQueue.main.async { - self.loginErrorMessage = "Something went wrong, please try logging in again." - self.isPresentingLoginErrorAlert = true - } + self.currentError = AccountError.genericAuthError self.logout() } catch { print(error) @@ -161,10 +143,7 @@ extension WriteFreelyModel { print(error) } } catch WFError.unauthorized { - DispatchQueue.main.async { - self.loginErrorMessage = "Something went wrong, please try logging in again." - self.isPresentingLoginErrorAlert = true - } + self.currentError = AccountError.genericAuthError self.logout() } catch { print("Error: Failed to fetch cached posts") diff --git a/Shared/Extensions/WriteFreelyModel+Keychain.swift b/Shared/Extensions/WriteFreelyModel+Keychain.swift index f039e31..4984675 100644 --- a/Shared/Extensions/WriteFreelyModel+Keychain.swift +++ b/Shared/Extensions/WriteFreelyModel+Keychain.swift @@ -2,12 +2,6 @@ import Foundation extension WriteFreelyModel { - enum WFKeychainError: Error { - case saveToKeychainFailed - case purgeFromKeychainFailed - case fetchFromKeychainFailed - } - func saveTokenToKeychain(_ token: String, username: String?, server: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, @@ -17,7 +11,7 @@ extension WriteFreelyModel { ] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecDuplicateItem || status == errSecSuccess else { - throw WFKeychainError.saveToKeychainFailed + throw KeychainError.couldNotStoreAccessToken } } @@ -29,7 +23,7 @@ extension WriteFreelyModel { ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { - throw WFKeychainError.purgeFromKeychainFailed + throw KeychainError.couldNotPurgeAccessToken } } @@ -48,7 +42,7 @@ extension WriteFreelyModel { return nil } guard status == errSecSuccess else { - throw WFKeychainError.fetchFromKeychainFailed + throw KeychainError.couldNotFetchAccessToken } guard let existingSecItem = secItem as? [String: Any], let tokenData = existingSecItem[kSecValueData as String] as? Data, diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index b644faf..af62660 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -23,7 +23,7 @@ final class LocalStorageManager { do { try container.viewContext.save() } catch { - print("Error saving context: \(error)") + print(LocalStoreError.couldNotSaveContext.localizedDescription) } } } @@ -35,7 +35,7 @@ final class LocalStorageManager { do { try container.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { - print("Error: Failed to purge cached collections.") + print(LocalStoreError.couldNotPurgeCollections.localizedDescription) } } @@ -61,7 +61,7 @@ private extension LocalStorageManager { container.loadPersistentStores { _, error in if let error = error { - fatalError("Core Data store failed to load with error: \(error)") + fatalError(LocalStoreError.couldNotLoadStore(error.localizedDescription).localizedDescription) } } migrateStore(for: container) @@ -88,14 +88,16 @@ private extension LocalStorageManager { options: nil, withType: NSSQLiteStoreType) } catch { - fatalError("Something went wrong migrating the store: \(error)") + fatalError(LocalStoreError.couldNotMigrateStore(error.localizedDescription).localizedDescription) } // Attempt to delete the old store. do { try FileManager.default.removeItem(at: oldStoreURL) } catch { - fatalError("Something went wrong while deleting the old store: \(error)") + fatalError( + LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription).localizedDescription + ) } } diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index ecb575f..634aa50 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -17,16 +17,31 @@ final class WriteFreelyModel: ObservableObject { @Published var selectedCollection: WFACollection? @Published var showAllPosts: Bool = true @Published var isPresentingDeleteAlert: Bool = false - @Published var isPresentingLoginErrorAlert: Bool = false - @Published var isPresentingNetworkErrorAlert: Bool = false @Published var postToDelete: WFAPost? + @Published var hasError: Bool = false #if os(iOS) @Published var isPresentingSettingsView: Bool = false #endif static var shared = WriteFreelyModel() - var loginErrorMessage: String? + var currentError: Error? { + didSet { + #if DEBUG + print("⚠️ currentError -> didSet \(currentError)") + print(" > hasError was: \(self.hasError)") + #endif + DispatchQueue.main.async { + #if DEBUG + print(" > self.currentError != nil: \(self.currentError != nil)") + #endif + self.hasError = self.currentError != nil + #if DEBUG + print(" > hasError is now: \(self.hasError)") + #endif + } + } + } // swiftlint:disable line_length let helpURL = URL(string: "https://discuss.write.as/c/help/5")! @@ -48,7 +63,7 @@ final class WriteFreelyModel: ObservableObject { self.account.restoreState() if self.account.isLoggedIn { guard let serverURL = URL(string: self.account.server) else { - print("Server URL not found") + self.currentError = AccountError.invalidServerURL return } do { @@ -56,8 +71,7 @@ final class WriteFreelyModel: ObservableObject { username: self.account.username, server: self.account.server ) else { - self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription - self.isPresentingLoginErrorAlert = true + self.currentError = KeychainError.couldNotFetchAccessToken return } @@ -67,8 +81,7 @@ final class WriteFreelyModel: ObservableObject { self.fetchUserCollections() self.fetchUserPosts() } catch { - self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription - self.isPresentingLoginErrorAlert = true + self.currentError = KeychainError.couldNotFetchAccessToken } } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 4001266..a3addfe 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -41,6 +41,7 @@ struct ContentView: View { #if os(macOS) ZStack { PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) + .withErrorHandling() if model.isProcessingRequest { ZStack { Color(NSColor.controlBackgroundColor).opacity(0.75) @@ -50,6 +51,7 @@ struct ContentView: View { } #else PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) + .withErrorHandling() #endif Text("Select a post, or create a new local draft.") diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index d59a706..2158aa4 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -3,6 +3,7 @@ import Combine struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel + @EnvironmentObject var errorHandling: ErrorHandling @Environment(\.managedObjectContext) var managedObjectContext @State private var postCount: Int = 0 @@ -86,17 +87,6 @@ struct PostListView: View { Spacer() Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") .foregroundColor(.secondary) - .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { - Alert( - title: Text("Connection Error"), - message: Text(""" - There is no internet connection at the moment. Please reconnect or try again later. - """), - dismissButton: .default(Text("OK"), action: { - model.isPresentingNetworkErrorAlert = false - }) - ) - }) Spacer() if model.isProcessingRequest { ProgressView() @@ -138,6 +128,16 @@ struct PostListView: View { model.selectedCollection = selectedCollection model.showAllPosts = showAllPosts } + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError) + } + model.hasError = false + } + } #else PostListFilteredView( collection: selectedCollection, @@ -148,18 +148,6 @@ struct PostListView: View { ToolbarItemGroup(placement: .primaryAction) { if model.selectedPost != nil { ActivePostToolbarView(activePost: model.selectedPost!) - .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { - Alert( - title: Text("Connection Error"), - message: Text(""" - There is no internet connection at the moment. \ - Please reconnect or try again later. - """), - dismissButton: .default(Text("OK"), action: { - model.isPresentingNetworkErrorAlert = false - }) - ) - }) } } } @@ -172,6 +160,16 @@ struct PostListView: View { model.selectedCollection = selectedCollection model.showAllPosts = showAllPosts } + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError) + } + model.hasError = false + } + } #endif } } diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 742d691..c2f33e2 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -24,6 +24,12 @@ 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; }; 171DC677272C7D0B002B9B8A /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171DC676272C7D0B002B9B8A /* UserDefaults+Extensions.swift */; }; 171DC678272C7D0B002B9B8A /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171DC676272C7D0B002B9B8A /* UserDefaults+Extensions.swift */; }; + 1727526628099802003D0A6A /* ErrorConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1727526528099802003D0A6A /* ErrorConstants.swift */; }; + 1727526728099802003D0A6A /* ErrorConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1727526528099802003D0A6A /* ErrorConstants.swift */; }; + 1727526828099802003D0A6A /* ErrorConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1727526528099802003D0A6A /* ErrorConstants.swift */; }; + 1727526A2809991A003D0A6A /* ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172752692809991A003D0A6A /* ErrorHandling.swift */; }; + 1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172752692809991A003D0A6A /* ErrorHandling.swift */; }; + 1727526C2809991A003D0A6A /* ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172752692809991A003D0A6A /* ErrorHandling.swift */; }; 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172C492D2593981900E20ADF /* MacUpdatesView.swift */; }; 172E10012735B83E00061372 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 172E10002735B83E00061372 /* UniformTypeIdentifiers.framework */; platformFilter = maccatalyst; }; 172E10042735B83E00061372 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 172E10032735B83E00061372 /* Media.xcassets */; }; @@ -181,6 +187,8 @@ 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = ""; }; 171BFDF924D4AF8300888236 /* CollectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListView.swift; sourceTree = ""; }; 171DC676272C7D0B002B9B8A /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = ""; }; + 1727526528099802003D0A6A /* ErrorConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorConstants.swift; sourceTree = ""; }; + 172752692809991A003D0A6A /* ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandling.swift; sourceTree = ""; }; 172C492D2593981900E20ADF /* MacUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacUpdatesView.swift; sourceTree = ""; }; 172E0FFF2735B83E00061372 /* ActionExtension-iOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "ActionExtension-iOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 172E10002735B83E00061372 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; @@ -325,6 +333,15 @@ path = Settings; sourceTree = ""; }; + 17275264280997BF003D0A6A /* ErrorHandling */ = { + isa = PBXGroup; + children = ( + 1727526528099802003D0A6A /* ErrorConstants.swift */, + 172752692809991A003D0A6A /* ErrorHandling.swift */, + ); + path = ErrorHandling; + sourceTree = ""; + }; 172E10022735B83E00061372 /* ActionExtension-iOS */ = { isa = PBXGroup; children = ( @@ -476,6 +493,7 @@ 17DF328324C87D3500BCE2E3 /* Assets.xcassets */, 17DF32D024C8B75C00BCE2E3 /* Account */, 1756AE7F24CB841200FD7257 /* Extensions */, + 17275264280997BF003D0A6A /* ErrorHandling */, 1762DCB124EB07680019C4EB /* Models */, 17DF32CC24C8B72300BCE2E3 /* Navigation */, 1739B8D324EAFAB700DA7421 /* PostEditor */, @@ -871,8 +889,10 @@ 172E10212735C64600061372 /* WFACollection+CoreDataProperties.swift in Sources */, 172E101C2735C57400061372 /* LocalStorageManager.swift in Sources */, 172E10192735C3DB00061372 /* ContentView.swift in Sources */, + 1727526828099802003D0A6A /* ErrorConstants.swift in Sources */, 172E10152735C2BD00061372 /* UIHostingView.swift in Sources */, 172E101F2735C64600061372 /* WFAPost+CoreDataClass.swift in Sources */, + 1727526C2809991A003D0A6A /* ErrorHandling.swift in Sources */, 172E10232735C6FF00061372 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 172E101E2735C62F00061372 /* PostStatus.swift in Sources */, ); @@ -887,6 +907,7 @@ 17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */, 17C42E622507D8E600072984 /* PostStatus.swift in Sources */, 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */, + 1727526A2809991A003D0A6A /* ErrorHandling.swift in Sources */, 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */, 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */, 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */, @@ -912,6 +933,7 @@ 1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */, 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, 17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */, + 1727526628099802003D0A6A /* ErrorConstants.swift in Sources */, 1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */, 17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */, @@ -937,6 +959,7 @@ 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */, + 1727526728099802003D0A6A /* ErrorConstants.swift in Sources */, 17479F152583D8E40072B7FB /* PostEditorSharingPicker.swift in Sources */, 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, 17C42E662509237800072984 /* PostListFilteredView.swift in Sources */, @@ -944,6 +967,7 @@ 17D4926727947D780035BD7E /* MacUpdatesViewModel.swift in Sources */, 17466626256C0D0600629997 /* MacEditorTextView.swift in Sources */, 170A7EC226F5186A00F1CBD4 /* CollectionListModel.swift in Sources */, + 1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */, 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, diff --git a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist index 33a3444..155f2da 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,17 +7,17 @@ ActionExtension-iOS.xcscheme_^#shared#^_ orderHint - 1 + 0 WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_ orderHint - 2 + 1 WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_ orderHint - 0 + 2 From 01ba57ae759c87a6ac40050f39bfa9e85e46c103 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sun, 8 May 2022 09:16:46 -0400 Subject: [PATCH 02/15] Move Account-related error handling up the hierarchy --- Shared/Account/AccountLoginView.swift | 10 ---------- Shared/Account/AccountView.swift | 12 ++++++++++++ iOS/Settings/SettingsView.swift | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift index c7ea5ea..64e7971 100644 --- a/Shared/Account/AccountLoginView.swift +++ b/Shared/Account/AccountLoginView.swift @@ -88,16 +88,6 @@ struct AccountLoginView: View { .padding() } } - .onChange(of: model.hasError) { value in - if value { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError) - } - model.hasError = false - } - } } } diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index 9241026..0a2546e 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -2,6 +2,7 @@ import SwiftUI struct AccountView: View { @EnvironmentObject var model: WriteFreelyModel + @EnvironmentObject var errorHandling: ErrorHandling var body: some View { if model.account.isLoggedIn { @@ -16,6 +17,17 @@ struct AccountView: View { .withErrorHandling() .padding(.top) } + EmptyView() + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError) + } + model.hasError = false + } + } } } diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift index e64bc94..28c55a6 100644 --- a/iOS/Settings/SettingsView.swift +++ b/iOS/Settings/SettingsView.swift @@ -9,6 +9,7 @@ struct SettingsView: View { Form { Section(header: Text("Login Details")) { AccountView() + .withErrorHandling() } Section(header: Text("Appearance")) { PreferencesView(preferences: model.preferences) From bf3573895726bfc69cde3fc44960b5d81d29a473 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sun, 8 May 2022 09:17:05 -0400 Subject: [PATCH 03/15] Handle errors on logout --- Shared/Account/AccountLogoutView.swift | 3 ++- Shared/Account/AccountView.swift | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift index 22df2da..d972f70 100644 --- a/Shared/Account/AccountLogoutView.swift +++ b/Shared/Account/AccountLogoutView.swift @@ -2,6 +2,7 @@ import SwiftUI struct AccountLogoutView: View { @EnvironmentObject var model: WriteFreelyModel + @EnvironmentObject var errorHandling: ErrorHandling @State private var isPresentingLogoutConfirmation: Bool = false @State private var editedPostsWarningString: String = "" @@ -66,7 +67,7 @@ struct AccountLogoutView: View { editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited posts. " } } catch { - print("Error: failed to fetch cached posts") + self.errorHandling.handle(error: LocalStoreError.couldNotFetchPosts("cached")) } self.isPresentingLogoutConfirmation = true } diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index 0a2546e..5843048 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -9,6 +9,7 @@ struct AccountView: View { HStack { Spacer() AccountLogoutView() + .withErrorHandling() Spacer() } .padding() From aefcd0d7994393be7cafbde556cda29b8dad423d Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sun, 8 May 2022 10:18:21 -0400 Subject: [PATCH 04/15] Fix for temporary debugging --- Shared/Models/WriteFreelyModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 634aa50..801d842 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -28,7 +28,7 @@ final class WriteFreelyModel: ObservableObject { var currentError: Error? { didSet { #if DEBUG - print("⚠️ currentError -> didSet \(currentError)") + print("⚠️ currentError -> didSet \(currentError?.localizedDescription ?? "nil")") print(" > hasError was: \(self.hasError)") #endif DispatchQueue.main.async { From 3a53bec1845d25853bae6c05fe858fcaa195c7eb Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Mon, 9 May 2022 08:55:43 -0400 Subject: [PATCH 05/15] =?UTF-8?q?Clean=20up=20WriteFreelyModel=E2=80=99s?= =?UTF-8?q?=20published=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Shared/Models/WriteFreelyModel.swift | 33 +++++++++++-------- .../xcschemes/xcschememanagement.plist | 24 -------------- 2 files changed, 19 insertions(+), 38 deletions(-) delete mode 100644 WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 801d842..63b9695 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -6,25 +6,15 @@ import Network // MARK: - WriteFreelyModel final class WriteFreelyModel: ObservableObject { + + // MARK: - Models @Published var account = AccountModel() @Published var preferences = PreferencesModel() @Published var posts = PostListModel() @Published var editor = PostEditorModel() - @Published var isLoggingIn: Bool = false - @Published var isProcessingRequest: Bool = false - @Published var hasNetworkConnection: Bool = true - @Published var selectedPost: WFAPost? - @Published var selectedCollection: WFACollection? - @Published var showAllPosts: Bool = true - @Published var isPresentingDeleteAlert: Bool = false - @Published var postToDelete: WFAPost? + + // MARK: - Error handling @Published var hasError: Bool = false - #if os(iOS) - @Published var isPresentingSettingsView: Bool = false - #endif - - static var shared = WriteFreelyModel() - var currentError: Error? { didSet { #if DEBUG @@ -43,6 +33,21 @@ final class WriteFreelyModel: ObservableObject { } } + // MARK: - State + @Published var isLoggingIn: Bool = false + @Published var isProcessingRequest: Bool = false + @Published var hasNetworkConnection: Bool = true + @Published var selectedPost: WFAPost? + @Published var selectedCollection: WFACollection? + @Published var showAllPosts: Bool = true + @Published var isPresentingDeleteAlert: Bool = false + @Published var postToDelete: WFAPost? +#if os(iOS) + @Published var isPresentingSettingsView: Bool = false +#endif + + static var shared = WriteFreelyModel() + // swiftlint:disable line_length let helpURL = URL(string: "https://discuss.write.as/c/help/5")! let howToURL = URL(string: "https://discuss.write.as/t/using-the-writefreely-ios-app/1946")! diff --git a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 155f2da..0000000 --- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - SchemeUserState - - ActionExtension-iOS.xcscheme_^#shared#^_ - - orderHint - 0 - - WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_ - - orderHint - 1 - - WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_ - - orderHint - 2 - - - - From a3b805a31907fe045f47f41da54a1169df653834 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 13 May 2022 07:20:47 -0400 Subject: [PATCH 06/15] Add error handling to top-level content view --- Shared/Navigation/ContentView.swift | 15 +++++++++++++++ Shared/WriteFreely_MultiPlatformApp.swift | 1 + 2 files changed, 16 insertions(+) diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index a3addfe..f1e82a0 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -2,11 +2,13 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel + @EnvironmentObject var errorHandling: ErrorHandling var body: some View { NavigationView { #if os(macOS) CollectionListView() + .withErrorHandling() .toolbar { Button( action: { @@ -36,6 +38,7 @@ struct ContentView: View { } #else CollectionListView() + .withErrorHandling() #endif #if os(macOS) @@ -56,6 +59,18 @@ struct ContentView: View { Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) + + EmptyView() + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError) + } + model.hasError = false + } + } } .environmentObject(model) } diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index 4378ada..76ff61b 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -48,6 +48,7 @@ struct WriteFreely_MultiPlatformApp: App { } } }) + .withErrorHandling() .environmentObject(model) .environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext) // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. From faa557c2b48e3998ace6745a0b1e035ccc320a40 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 13 May 2022 08:01:11 -0400 Subject: [PATCH 07/15] Set current error on API call failures --- Shared/Extensions/WriteFreelyModel+API.swift | 37 +++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Shared/Extensions/WriteFreelyModel+API.swift b/Shared/Extensions/WriteFreelyModel+API.swift index 23e3b61..36e657a 100644 --- a/Shared/Extensions/WriteFreelyModel+API.swift +++ b/Shared/Extensions/WriteFreelyModel+API.swift @@ -38,7 +38,7 @@ extension WriteFreelyModel { try purgeTokenFromKeychain(username: account.username, server: account.server) account.logout() } catch { - fatalError("Failed to log out persisted state") + self.currentError = KeychainError.couldNotPurgeAccessToken } return } @@ -50,7 +50,10 @@ extension WriteFreelyModel { self.currentError = NetworkError.noConnectionError return } - guard let loggedInClient = client else { return } + guard let loggedInClient = client else { + self.currentError = AppError.couldNotGetLoggedInClient + return + } // We're starting the network request. DispatchQueue.main.async { self.isProcessingRequest = true @@ -63,7 +66,10 @@ extension WriteFreelyModel { self.currentError = NetworkError.noConnectionError return } - guard let loggedInClient = client else { return } + guard let loggedInClient = client else { + self.currentError = AppError.couldNotGetLoggedInClient + return + } // We're starting the network request. DispatchQueue.main.async { self.isProcessingRequest = true @@ -78,7 +84,10 @@ extension WriteFreelyModel { self.currentError = NetworkError.noConnectionError return } - guard let loggedInClient = client else { return } + guard let loggedInClient = client else { + self.currentError = AppError.couldNotGetLoggedInClient + return + } // We're starting the network request. DispatchQueue.main.async { self.isProcessingRequest = true @@ -123,8 +132,14 @@ extension WriteFreelyModel { self.currentError = NetworkError.noConnectionError return } - guard let loggedInClient = client else { return } - guard let postId = post.postId else { return } + guard let loggedInClient = client else { + self.currentError = AppError.couldNotGetLoggedInClient + return + } + guard let postId = post.postId else { + self.currentError = AppError.couldNotGetPostId + return + } // We're starting the network request. DispatchQueue.main.async { self.selectedPost = post @@ -138,8 +153,14 @@ extension WriteFreelyModel { self.currentError = NetworkError.noConnectionError return } - guard let loggedInClient = client, - let postId = post.postId else { return } + guard let loggedInClient = client else { + self.currentError = AppError.couldNotGetLoggedInClient + return + } + guard let postId = post.postId else { + self.currentError = AppError.couldNotGetPostId + return + } // We're starting the network request. DispatchQueue.main.async { self.isProcessingRequest = true From 223ebf5b7cc1e34e98cc65b980673cbfdbbe527c Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 13 May 2022 08:33:32 -0400 Subject: [PATCH 08/15] Set current error on API call handlers --- Shared/Account/AccountView.swift | 2 +- Shared/ErrorHandling/ErrorConstants.swift | 10 +++++--- .../WriteFreelyModel+APIHandlers.swift | 24 +++++++++++-------- Shared/Navigation/ContentView.swift | 2 +- Shared/PostList/PostListView.swift | 2 +- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index 5843048..fe22f19 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -24,7 +24,7 @@ struct AccountView: View { if let error = model.currentError { self.errorHandling.handle(error: error) } else { - self.errorHandling.handle(error: AppError.genericError) + self.errorHandling.handle(error: AppError.genericError("")) } model.hasError = false } diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift index eb1f037..2962246 100644 --- a/Shared/ErrorHandling/ErrorConstants.swift +++ b/Shared/ErrorHandling/ErrorConstants.swift @@ -133,7 +133,7 @@ extension LocalStoreError: LocalizedError { enum AppError: Error { case couldNotGetLoggedInClient case couldNotGetPostId - case genericError + case genericError(String) } extension AppError: LocalizedError { @@ -143,8 +143,12 @@ extension AppError: LocalizedError { return NSLocalizedString("Something went wrong trying to access the WriteFreely client.", comment: "") case .couldNotGetPostId: return NSLocalizedString("Something went wrong trying to get the post's unique ID.", comment: "") - case .genericError: - return NSLocalizedString("Something went wrong", comment: "") + case .genericError(let customContent): + if customContent.isEmpty { + return NSLocalizedString("Something went wrong", comment: "") + } else { + return NSLocalizedString(customContent, comment: "") + } } } } diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift index 6d0d147..c1d19d9 100644 --- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -44,7 +44,7 @@ extension WriteFreelyModel { self.posts.purgePublishedPosts() } } catch { - print(KeychainError.couldNotPurgeAccessToken.localizedDescription) + self.currentError = KeychainError.couldNotPurgeAccessToken } } catch WFError.notFound { // The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with @@ -59,7 +59,7 @@ extension WriteFreelyModel { self.posts.purgePublishedPosts() } } catch { - print(KeychainError.couldNotPurgeAccessToken.localizedDescription) + self.currentError = KeychainError.couldNotPurgeAccessToken } } catch { // We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here, @@ -101,7 +101,7 @@ extension WriteFreelyModel { self.currentError = AccountError.genericAuthError self.logout() } catch { - print(error) + self.currentError = AppError.genericError(error.localizedDescription) } } @@ -123,7 +123,11 @@ extension WriteFreelyModel { if let fetchedPostUpdatedDate = fetchedPost.updatedDate, let localPostUpdatedDate = managedPost.updatedDate { managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate - } else { print("Error: could not determine which copy of post is newer") } + } else { + self.currentError = AppError.genericError( + "Error updating post: could not determine which copy of post is newer." + ) + } postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId }) } } else { @@ -140,13 +144,13 @@ extension WriteFreelyModel { LocalStorageManager.standard.saveContext() } } catch { - print(error) + self.currentError = AppError.genericError(error.localizedDescription) } } catch WFError.unauthorized { self.currentError = AccountError.genericAuthError self.logout() } catch { - print("Error: Failed to fetch cached posts") + self.currentError = LocalStoreError.couldNotFetchPosts("cached") } } @@ -190,11 +194,11 @@ extension WriteFreelyModel { LocalStorageManager.standard.saveContext() } } catch { - print("Error: Failed to fetch cached posts") + self.currentError = LocalStoreError.couldNotFetchPosts("cached") } } } catch { - print(error) + self.currentError = AppError.genericError(error.localizedDescription) } } @@ -216,7 +220,7 @@ extension WriteFreelyModel { LocalStorageManager.standard.saveContext() } } catch { - print(error) + self.currentError = AppError.genericError(error.localizedDescription) } } @@ -238,7 +242,7 @@ extension WriteFreelyModel { DispatchQueue.main.async { LocalStorageManager.standard.container.viewContext.rollback() } - print(error) + self.currentError = AppError.genericError(error.localizedDescription) } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index f1e82a0..ca13ff5 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -66,7 +66,7 @@ struct ContentView: View { if let error = model.currentError { self.errorHandling.handle(error: error) } else { - self.errorHandling.handle(error: AppError.genericError) + self.errorHandling.handle(error: AppError.genericError("")) } model.hasError = false } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 2158aa4..16e1f45 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -133,7 +133,7 @@ struct PostListView: View { if let error = model.currentError { self.errorHandling.handle(error: error) } else { - self.errorHandling.handle(error: AppError.genericError) + self.errorHandling.handle(error: AppError.genericError("")) } model.hasError = false } From dfb3a08608ed8ec2e2e62ad3010c9d2a67691e51 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 13 May 2022 08:44:13 -0400 Subject: [PATCH 09/15] Move User Defaults errors to ErrorConstants file --- Shared/ErrorHandling/ErrorConstants.swift | 15 +++++++++++++++ Shared/Extensions/UserDefaults+Extensions.swift | 13 +------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift index 2962246..676f299 100644 --- a/Shared/ErrorHandling/ErrorConstants.swift +++ b/Shared/ErrorHandling/ErrorConstants.swift @@ -81,6 +81,21 @@ extension AccountError: LocalizedError { } } +// MARK: - User Defaults Errors + +enum UserDefaultsError: Error { + case couldNotMigrateStandardDefaults +} + +extension UserDefaultsError: LocalizedError { + public var errorDescription: String? { + switch self { + case .couldNotMigrateStandardDefaults: + return NSLocalizedString("Could not migrate user defaults to group container", comment: "") + } + } +} + // MARK: - Local Store Errors enum LocalStoreError: Error { diff --git a/Shared/Extensions/UserDefaults+Extensions.swift b/Shared/Extensions/UserDefaults+Extensions.swift index f40a824..b010fdc 100644 --- a/Shared/Extensions/UserDefaults+Extensions.swift +++ b/Shared/Extensions/UserDefaults+Extensions.swift @@ -17,17 +17,6 @@ enum WFDefaults { extension UserDefaults { - private enum DefaultsError: Error { - case couldNotMigrateStandardDefaults - - var description: String { - switch self { - case .couldNotMigrateStandardDefaults: - return "Could not migrate user defaults to group container." - } - } - } - private static let appGroupName: String = "group.com.abunchtell.writefreely" private static let didMigrateDefaultsToAppGroup: String = "didMigrateDefaultsToAppGroup" private static let didRemoveStandardDefaults: String = "didRemoveStandardDefaults" @@ -61,7 +50,7 @@ extension UserDefaults { groupDefaults.set(true, forKey: UserDefaults.didMigrateDefaultsToAppGroup) return groupDefaults } else { - throw DefaultsError.couldNotMigrateStandardDefaults + throw UserDefaultsError.couldNotMigrateStandardDefaults } } From 11d2e41ab5d58c405160b81dcdd8597d1e2c552c Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Mon, 23 May 2022 15:12:33 -0400 Subject: [PATCH 10/15] Add default values for some error strings --- Shared/Account/AccountView.swift | 2 +- Shared/ErrorHandling/ErrorConstants.swift | 6 ++--- .../WriteFreelyModel+Keychain.swift | 4 ++-- Shared/Models/WriteFreelyModel.swift | 1 + Shared/Navigation/ContentView.swift | 22 +++++++++---------- Shared/PostList/PostListView.swift | 4 ++-- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index fe22f19..0b54f0e 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -24,7 +24,7 @@ struct AccountView: View { if let error = model.currentError { self.errorHandling.handle(error: error) } else { - self.errorHandling.handle(error: AppError.genericError("")) + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift index 676f299..b412fdb 100644 --- a/Shared/ErrorHandling/ErrorConstants.swift +++ b/Shared/ErrorHandling/ErrorConstants.swift @@ -101,13 +101,13 @@ extension UserDefaultsError: LocalizedError { enum LocalStoreError: Error { case couldNotSaveContext case couldNotFetchCollections - case couldNotFetchPosts(String) + case couldNotFetchPosts(String = "") case couldNotPurgePublishedPosts case couldNotPurgeCollections case couldNotLoadStore(String) case couldNotMigrateStore(String) case couldNotDeleteStoreAfterMigration(String) - case genericError(String) + case genericError(String = "") } extension LocalStoreError: LocalizedError { @@ -148,7 +148,7 @@ extension LocalStoreError: LocalizedError { enum AppError: Error { case couldNotGetLoggedInClient case couldNotGetPostId - case genericError(String) + case genericError(String = "") } extension AppError: LocalizedError { diff --git a/Shared/Extensions/WriteFreelyModel+Keychain.swift b/Shared/Extensions/WriteFreelyModel+Keychain.swift index 4984675..f6555aa 100644 --- a/Shared/Extensions/WriteFreelyModel+Keychain.swift +++ b/Shared/Extensions/WriteFreelyModel+Keychain.swift @@ -39,7 +39,7 @@ extension WriteFreelyModel { var secItem: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &secItem) guard status != errSecItemNotFound else { - return nil + throw KeychainError.couldNotFetchAccessToken } guard status == errSecSuccess else { throw KeychainError.couldNotFetchAccessToken @@ -47,7 +47,7 @@ extension WriteFreelyModel { guard let existingSecItem = secItem as? [String: Any], let tokenData = existingSecItem[kSecValueData as String] as? Data, let token = String(data: tokenData, encoding: .utf8) else { - return nil + throw KeychainError.couldNotFetchAccessToken } return token } diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 63b9695..617385f 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -87,6 +87,7 @@ final class WriteFreelyModel: ObservableObject { self.fetchUserPosts() } catch { self.currentError = KeychainError.couldNotFetchAccessToken + return } } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index ca13ff5..f57a541 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -59,20 +59,18 @@ struct ContentView: View { Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) - - EmptyView() - .onChange(of: model.hasError) { value in - if value { - if let error = model.currentError { - self.errorHandling.handle(error: error) - } else { - self.errorHandling.handle(error: AppError.genericError("")) - } - model.hasError = false - } - } } .environmentObject(model) + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) + } + model.hasError = false + } + } } } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 16e1f45..c1bb662 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -133,7 +133,7 @@ struct PostListView: View { if let error = model.currentError { self.errorHandling.handle(error: error) } else { - self.errorHandling.handle(error: AppError.genericError("")) + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } @@ -165,7 +165,7 @@ struct PostListView: View { if let error = model.currentError { self.errorHandling.handle(error: error) } else { - self.errorHandling.handle(error: AppError.genericError) + self.errorHandling.handle(error: AppError.genericError()) } model.hasError = false } From b017e21e066dbcf86a3166fc06d19222047b78b9 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Mon, 23 May 2022 15:52:20 -0400 Subject: [PATCH 11/15] Handle purging post errors --- Shared/ErrorHandling/ErrorConstants.swift | 10 +++++++--- Shared/Extensions/WriteFreelyModel+APIHandlers.swift | 12 ++++++++++-- Shared/PostList/PostListModel.swift | 4 ++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift index b412fdb..88ae92b 100644 --- a/Shared/ErrorHandling/ErrorConstants.swift +++ b/Shared/ErrorHandling/ErrorConstants.swift @@ -102,7 +102,7 @@ enum LocalStoreError: Error { case couldNotSaveContext case couldNotFetchCollections case couldNotFetchPosts(String = "") - case couldNotPurgePublishedPosts + case couldNotPurgePosts(String = "") case couldNotPurgeCollections case couldNotLoadStore(String) case couldNotMigrateStore(String) @@ -123,8 +123,12 @@ extension LocalStoreError: LocalizedError { } else { return NSLocalizedString("Failed to fetch \(postFilter) posts from local store.", comment: "") } - case .couldNotPurgePublishedPosts: - return NSLocalizedString("Failed to purge published posts from local store.", comment: "") + case .couldNotPurgePosts(let postFilter): + if postFilter.isEmpty { + return NSLocalizedString("Failed to purge \(postFilter) posts from local store.", comment: "") + } else { + return NSLocalizedString("Failed to purge posts from local store.", comment: "") + } case .couldNotPurgeCollections: return NSLocalizedString("Failed to purge cached collections", comment: "") case .couldNotLoadStore(let errorDescription): diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift index c1d19d9..b6dc7f3 100644 --- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -41,7 +41,11 @@ extension WriteFreelyModel { DispatchQueue.main.async { self.account.logout() LocalStorageManager.standard.purgeUserCollections() - self.posts.purgePublishedPosts() + do { + try self.posts.purgePublishedPosts() + } catch { + self.currentError = error + } } } catch { self.currentError = KeychainError.couldNotPurgeAccessToken @@ -56,7 +60,11 @@ extension WriteFreelyModel { DispatchQueue.main.async { self.account.logout() LocalStorageManager.standard.purgeUserCollections() - self.posts.purgePublishedPosts() + do { + try self.posts.purgePublishedPosts() + } catch { + self.currentError = error + } } } catch { self.currentError = KeychainError.couldNotPurgeAccessToken diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift index db0ff4a..edd545c 100644 --- a/Shared/PostList/PostListModel.swift +++ b/Shared/PostList/PostListModel.swift @@ -9,7 +9,7 @@ class PostListModel: ObservableObject { } } - func purgePublishedPosts() { + func purgePublishedPosts() throws { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "WFAPost") fetchRequest.predicate = NSPredicate(format: "status != %i", 0) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) @@ -17,7 +17,7 @@ class PostListModel: ObservableObject { do { try LocalStorageManager.standard.container.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { - print("Error: Failed to purge cached posts.") + throw LocalStoreError.couldNotPurgePosts("cached") } } From c5b611b39e16bd339282310ab3a65fdafa98e939 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Thu, 26 May 2022 07:31:11 -0400 Subject: [PATCH 12/15] Add FIXME to track silent failure on fetching collections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As collections are fetched and added to the `list` property in the CollectionListModel’s initializer, it’s tricky to throw an error here: we call it as a property initializer in CollectionListView, which cannot throw. Consider refactoring this logic such that we’re using, for example, a @FetchRequest in CollectionListView instead. --- Shared/PostCollection/CollectionListModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift index 5e107bb..86e4088 100644 --- a/Shared/PostCollection/CollectionListModel.swift +++ b/Shared/PostCollection/CollectionListModel.swift @@ -19,6 +19,7 @@ class CollectionListModel: NSObject, ObservableObject { try collectionsController.performFetch() list = collectionsController.fetchedObjects ?? [] } catch { + // FIXME: Errors cannot be thrown out of the CollectionListView property initializer print("Failed to fetch collections!") } } From 15f84b04c026749f88404a099df3fe840e2a13a0 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Thu, 26 May 2022 08:08:12 -0400 Subject: [PATCH 13/15] Handle errors in (most) shared code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two outliers to come back to are: - the LocalStoreManager, where we can’t set a current error in the WriteFreelyModel in methods that can’t throw - the CollectionListModel, where the initializer can’t throw because we use it as a property initializer in CollectionListView --- Shared/Extensions/WriteFreelyModel+APIHandlers.swift | 4 ++-- Shared/LocalStorageManager.swift | 6 +++--- Shared/PostCollection/CollectionListModel.swift | 2 +- Shared/PostCollection/CollectionListView.swift | 11 +++++++++++ 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift index b6dc7f3..3163676 100644 --- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -40,8 +40,8 @@ extension WriteFreelyModel { client = nil DispatchQueue.main.async { self.account.logout() - LocalStorageManager.standard.purgeUserCollections() do { + try LocalStorageManager.standard.purgeUserCollections() try self.posts.purgePublishedPosts() } catch { self.currentError = error @@ -59,8 +59,8 @@ extension WriteFreelyModel { client = nil DispatchQueue.main.async { self.account.logout() - LocalStorageManager.standard.purgeUserCollections() do { + try LocalStorageManager.standard.purgeUserCollections() try self.posts.purgePublishedPosts() } catch { self.currentError = error diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index af62660..ae074b4 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -23,19 +23,19 @@ final class LocalStorageManager { do { try container.viewContext.save() } catch { - print(LocalStoreError.couldNotSaveContext.localizedDescription) + fatalError(LocalStoreError.couldNotSaveContext.localizedDescription) } } } - func purgeUserCollections() { + func purgeUserCollections() throws { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "WFACollection") let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { try container.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { - print(LocalStoreError.couldNotPurgeCollections.localizedDescription) + throw LocalStoreError.couldNotPurgeCollections } } diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift index 86e4088..b2ac884 100644 --- a/Shared/PostCollection/CollectionListModel.swift +++ b/Shared/PostCollection/CollectionListModel.swift @@ -20,7 +20,7 @@ class CollectionListModel: NSObject, ObservableObject { list = collectionsController.fetchedObjects ?? [] } catch { // FIXME: Errors cannot be thrown out of the CollectionListView property initializer - print("Failed to fetch collections!") + fatalError(LocalStoreError.couldNotFetchCollections.localizedDescription) } } } diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index 29e84b1..0975fff 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -2,6 +2,7 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel + @EnvironmentObject var errorHandling: ErrorHandling @ObservedObject var collections = CollectionListModel( managedObjectContext: LocalStorageManager.standard.container.viewContext ) @@ -40,6 +41,16 @@ struct CollectionListView: View { self.model.editor.showAllPostsFlag = model.showAllPosts } } + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) + } + model.hasError = false + } + } } } From 230f7a10762266f843b55279695a82d26d062e39 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sat, 28 May 2022 07:17:33 -0400 Subject: [PATCH 14/15] Delete CollectionListModel in favour of FetchRequest in CollectionListView --- .../PostCollection/CollectionListModel.swift | 41 ------------------- .../PostCollection/CollectionListView.swift | 9 ++-- .../project.pbxproj | 6 --- 3 files changed, 5 insertions(+), 51 deletions(-) delete mode 100644 Shared/PostCollection/CollectionListModel.swift diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift deleted file mode 100644 index b2ac884..0000000 --- a/Shared/PostCollection/CollectionListModel.swift +++ /dev/null @@ -1,41 +0,0 @@ -import SwiftUI -import CoreData - -class CollectionListModel: NSObject, ObservableObject { - @Published var list: [WFACollection] = [] - private let collectionsController: NSFetchedResultsController - - init(managedObjectContext: NSManagedObjectContext) { - collectionsController = NSFetchedResultsController(fetchRequest: WFACollection.collectionsFetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil) - - super.init() - - collectionsController.delegate = self - - do { - try collectionsController.performFetch() - list = collectionsController.fetchedObjects ?? [] - } catch { - // FIXME: Errors cannot be thrown out of the CollectionListView property initializer - fatalError(LocalStoreError.couldNotFetchCollections.localizedDescription) - } - } -} - -extension CollectionListModel: NSFetchedResultsControllerDelegate { - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - guard let collections = controller.fetchedObjects as? [WFACollection] else { return } - self.list = collections - } -} - -extension WFACollection { - static var collectionsFetchRequest: NSFetchRequest { - let request: NSFetchRequest = WFACollection.createFetchRequest() - request.sortDescriptors = [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)] - return request - } -} diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index 0975fff..f1bcadf 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -3,9 +3,10 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling - @ObservedObject var collections = CollectionListModel( - managedObjectContext: LocalStorageManager.standard.container.viewContext - ) +// @ObservedObject var collections = CollectionListModel( +// managedObjectContext: LocalStorageManager.standard.container.viewContext +// ) + @FetchRequest(sortDescriptors: []) var collections: FetchedResults @State var selectedCollection: WFACollection? var body: some View { @@ -14,7 +15,7 @@ struct CollectionListView: View { NavigationLink("All Posts", destination: PostListView(selectedCollection: nil, showAllPosts: true)) NavigationLink("Drafts", destination: PostListView(selectedCollection: nil, showAllPosts: false)) Section(header: Text("Your Blogs")) { - ForEach(collections.list, id: \.self) { collection in + ForEach(collections, id: \.self) { collection in NavigationLink(destination: PostListView(selectedCollection: collection, showAllPosts: false), tag: collection, selection: $selectedCollection, diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index c2f33e2..51aae28 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 170A7EC126F5186A00F1CBD4 /* CollectionListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170A7EC026F5186A00F1CBD4 /* CollectionListModel.swift */; }; - 170A7EC226F5186A00F1CBD4 /* CollectionListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170A7EC026F5186A00F1CBD4 /* CollectionListModel.swift */; }; 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */; }; 170DFA35251BBC44001D82A0 /* PostEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */; }; 17120DA124E19839002B9F6C /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; }; @@ -179,7 +177,6 @@ /* Begin PBXFileReference section */ 1709ADDF251B9A110053AF79 /* EditorLaunchingPolicy.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = EditorLaunchingPolicy.md; sourceTree = ""; }; - 170A7EC026F5186A00F1CBD4 /* CollectionListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListModel.swift; sourceTree = ""; }; 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorModel.swift; sourceTree = ""; }; 17120DA424E19CBF002B9F6C /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLogoutView.swift; sourceTree = ""; }; @@ -603,7 +600,6 @@ 17DF32D224C8B78D00BCE2E3 /* PostCollection */ = { isa = PBXGroup; children = ( - 170A7EC026F5186A00F1CBD4 /* CollectionListModel.swift */, 171BFDF924D4AF8300888236 /* CollectionListView.swift */, ); path = PostCollection; @@ -926,7 +922,6 @@ 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, - 170A7EC126F5186A00F1CBD4 /* CollectionListModel.swift in Sources */, 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */, 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */, 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, @@ -966,7 +961,6 @@ 17120DAD24E1B99F002B9F6C /* AccountLoginView.swift in Sources */, 17D4926727947D780035BD7E /* MacUpdatesViewModel.swift in Sources */, 17466626256C0D0600629997 /* MacEditorTextView.swift in Sources */, - 170A7EC226F5186A00F1CBD4 /* CollectionListModel.swift in Sources */, 1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */, 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, From 2eba4c5c0483b8779068c52e9e6536b121e59d58 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sat, 28 May 2022 07:22:27 -0400 Subject: [PATCH 15/15] Remove commented-out code --- Shared/PostCollection/CollectionListView.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index f1bcadf..589903b 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -3,9 +3,6 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var errorHandling: ErrorHandling -// @ObservedObject var collections = CollectionListModel( -// managedObjectContext: LocalStorageManager.standard.container.viewContext -// ) @FetchRequest(sortDescriptors: []) var collections: FetchedResults @State var selectedCollection: WFACollection?