Initial work on presenting alert on error

This commit is contained in:
Angelo Stavrow 2022-05-01 12:06:36 -04:00
parent 185567dfed
commit 11200a01a0
No known key found for this signature in database
GPG Key ID: 1A49C7064E060EEE
14 changed files with 301 additions and 145 deletions

View File

@ -2,6 +2,7 @@ import SwiftUI
struct AccountLoginView: View { struct AccountLoginView: View {
@EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@State private var alertMessage: String = "" @State private var alertMessage: String = ""
@State private var username: String = "" @State private var username: String = ""
@ -76,8 +77,7 @@ struct AccountLoginView: View {
as: username, password: password as: username, password: password
) )
} else { } else {
model.loginErrorMessage = AccountError.invalidServerURL.localizedDescription self.errorHandling.handle(error: AccountError.invalidServerURL)
model.isPresentingLoginErrorAlert = true
} }
}, label: { }, label: {
Text("Log In") Text("Log In")
@ -88,12 +88,15 @@ struct AccountLoginView: View {
.padding() .padding()
} }
} }
.alert(isPresented: $model.isPresentingLoginErrorAlert) { .onChange(of: model.hasError) { value in
Alert( if value {
title: Text("Error Logging In"), if let error = model.currentError {
message: Text(model.loginErrorMessage ?? "An unknown error occurred while trying to login."), self.errorHandling.handle(error: error)
dismissButton: .default(Text("OK")) } else {
) self.errorHandling.handle(error: AppError.genericError)
}
model.hasError = false
}
} }
} }
} }

View File

@ -1,58 +1,6 @@
import SwiftUI import SwiftUI
import WriteFreely 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 { struct AccountModel {
@AppStorage(WFDefaults.isLoggedIn, store: UserDefaults.shared) var isLoggedIn: Bool = false @AppStorage(WFDefaults.isLoggedIn, store: UserDefaults.shared) var isLoggedIn: Bool = false
private let defaults = UserDefaults.shared private let defaults = UserDefaults.shared

View File

@ -13,6 +13,7 @@ struct AccountView: View {
.padding() .padding()
} else { } else {
AccountLoginView() AccountLoginView()
.withErrorHandling()
.padding(.top) .padding(.top)
} }
} }

View File

@ -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: "")
}
}
}

View File

@ -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())
}
}

View File

@ -4,7 +4,7 @@ import WriteFreely
extension WriteFreelyModel { extension WriteFreelyModel {
func login(to server: URL, as username: String, password: String) { func login(to server: URL, as username: String, password: String) {
if !hasNetworkConnection { if !hasNetworkConnection {
isPresentingNetworkErrorAlert = true self.currentError = NetworkError.noConnectionError
return return
} }
let secureProtocolPrefix = "https://" let secureProtocolPrefix = "https://"
@ -30,7 +30,7 @@ extension WriteFreelyModel {
func logout() { func logout() {
if !hasNetworkConnection { if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } self.currentError = NetworkError.noConnectionError
return return
} }
guard let loggedInClient = client else { guard let loggedInClient = client else {
@ -47,7 +47,7 @@ extension WriteFreelyModel {
func fetchUserCollections() { func fetchUserCollections() {
if !hasNetworkConnection { if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } self.currentError = NetworkError.noConnectionError
return return
} }
guard let loggedInClient = client else { return } guard let loggedInClient = client else { return }
@ -60,7 +60,7 @@ extension WriteFreelyModel {
func fetchUserPosts() { func fetchUserPosts() {
if !hasNetworkConnection { if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } self.currentError = NetworkError.noConnectionError
return return
} }
guard let loggedInClient = client else { return } guard let loggedInClient = client else { return }
@ -75,7 +75,7 @@ extension WriteFreelyModel {
postToUpdate = nil postToUpdate = nil
if !hasNetworkConnection { if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } self.currentError = NetworkError.noConnectionError
return return
} }
guard let loggedInClient = client else { return } guard let loggedInClient = client else { return }
@ -120,7 +120,7 @@ extension WriteFreelyModel {
func updateFromServer(post: WFAPost) { func updateFromServer(post: WFAPost) {
if !hasNetworkConnection { if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } self.currentError = NetworkError.noConnectionError
return return
} }
guard let loggedInClient = client else { return } guard let loggedInClient = client else { return }
@ -135,7 +135,7 @@ extension WriteFreelyModel {
func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) { func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) {
if !hasNetworkConnection { if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } self.currentError = NetworkError.noConnectionError
return return
} }
guard let loggedInClient = client, guard let loggedInClient = client,

View File

@ -16,33 +16,18 @@ extension WriteFreelyModel {
self.account.login(user) self.account.login(user)
} }
} catch { } catch {
DispatchQueue.main.async { self.currentError = KeychainError.couldNotStoreAccessToken
self.loginErrorMessage = "There was a problem storing your access token to the Keychain."
self.isPresentingLoginErrorAlert = true
}
} }
} catch WFError.notFound { } catch WFError.notFound {
DispatchQueue.main.async { self.currentError = AccountError.usernameNotFound
self.loginErrorMessage = AccountError.usernameNotFound.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} catch WFError.unauthorized { } catch WFError.unauthorized {
DispatchQueue.main.async { self.currentError = AccountError.invalidPassword
self.loginErrorMessage = AccountError.invalidPassword.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} catch { } catch {
if (error as NSError).domain == NSURLErrorDomain, if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == -1003 { (error as NSError).code == -1003 {
DispatchQueue.main.async { self.currentError = AccountError.serverNotFound
self.loginErrorMessage = AccountError.serverNotFound.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} else { } else {
DispatchQueue.main.async { self.currentError = error
self.loginErrorMessage = error.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} }
} }
} }
@ -59,7 +44,7 @@ extension WriteFreelyModel {
self.posts.purgePublishedPosts() self.posts.purgePublishedPosts()
} }
} catch { } catch {
print("Something went wrong purging the token from the Keychain.") print(KeychainError.couldNotPurgeAccessToken.localizedDescription)
} }
} catch WFError.notFound { } catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with // 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() self.posts.purgePublishedPosts()
} }
} catch { } catch {
print("Something went wrong purging the token from the Keychain.") print(KeychainError.couldNotPurgeAccessToken.localizedDescription)
} }
} catch { } catch {
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here, // 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() LocalStorageManager.standard.saveContext()
} }
} catch WFError.unauthorized { } catch WFError.unauthorized {
DispatchQueue.main.async { self.currentError = AccountError.genericAuthError
self.loginErrorMessage = "Something went wrong, please try logging in again."
self.isPresentingLoginErrorAlert = true
}
self.logout() self.logout()
} catch { } catch {
print(error) print(error)
@ -161,10 +143,7 @@ extension WriteFreelyModel {
print(error) print(error)
} }
} catch WFError.unauthorized { } catch WFError.unauthorized {
DispatchQueue.main.async { self.currentError = AccountError.genericAuthError
self.loginErrorMessage = "Something went wrong, please try logging in again."
self.isPresentingLoginErrorAlert = true
}
self.logout() self.logout()
} catch { } catch {
print("Error: Failed to fetch cached posts") print("Error: Failed to fetch cached posts")

View File

@ -2,12 +2,6 @@ import Foundation
extension WriteFreelyModel { extension WriteFreelyModel {
enum WFKeychainError: Error {
case saveToKeychainFailed
case purgeFromKeychainFailed
case fetchFromKeychainFailed
}
func saveTokenToKeychain(_ token: String, username: String?, server: String) throws { func saveTokenToKeychain(_ token: String, username: String?, server: String) throws {
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword, kSecClass as String: kSecClassGenericPassword,
@ -17,7 +11,7 @@ extension WriteFreelyModel {
] ]
let status = SecItemAdd(query as CFDictionary, nil) let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecDuplicateItem || status == errSecSuccess else { guard status == errSecDuplicateItem || status == errSecSuccess else {
throw WFKeychainError.saveToKeychainFailed throw KeychainError.couldNotStoreAccessToken
} }
} }
@ -29,7 +23,7 @@ extension WriteFreelyModel {
] ]
let status = SecItemDelete(query as CFDictionary) let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { guard status == errSecSuccess || status == errSecItemNotFound else {
throw WFKeychainError.purgeFromKeychainFailed throw KeychainError.couldNotPurgeAccessToken
} }
} }
@ -48,7 +42,7 @@ extension WriteFreelyModel {
return nil return nil
} }
guard status == errSecSuccess else { guard status == errSecSuccess else {
throw WFKeychainError.fetchFromKeychainFailed throw KeychainError.couldNotFetchAccessToken
} }
guard let existingSecItem = secItem as? [String: Any], guard let existingSecItem = secItem as? [String: Any],
let tokenData = existingSecItem[kSecValueData as String] as? Data, let tokenData = existingSecItem[kSecValueData as String] as? Data,

View File

@ -23,7 +23,7 @@ final class LocalStorageManager {
do { do {
try container.viewContext.save() try container.viewContext.save()
} catch { } catch {
print("Error saving context: \(error)") print(LocalStoreError.couldNotSaveContext.localizedDescription)
} }
} }
} }
@ -35,7 +35,7 @@ final class LocalStorageManager {
do { do {
try container.viewContext.executeAndMergeChanges(using: deleteRequest) try container.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch { } catch {
print("Error: Failed to purge cached collections.") print(LocalStoreError.couldNotPurgeCollections.localizedDescription)
} }
} }
@ -61,7 +61,7 @@ private extension LocalStorageManager {
container.loadPersistentStores { _, error in container.loadPersistentStores { _, error in
if let error = error { if let error = error {
fatalError("Core Data store failed to load with error: \(error)") fatalError(LocalStoreError.couldNotLoadStore(error.localizedDescription).localizedDescription)
} }
} }
migrateStore(for: container) migrateStore(for: container)
@ -88,14 +88,16 @@ private extension LocalStorageManager {
options: nil, options: nil,
withType: NSSQLiteStoreType) withType: NSSQLiteStoreType)
} catch { } catch {
fatalError("Something went wrong migrating the store: \(error)") fatalError(LocalStoreError.couldNotMigrateStore(error.localizedDescription).localizedDescription)
} }
// Attempt to delete the old store. // Attempt to delete the old store.
do { do {
try FileManager.default.removeItem(at: oldStoreURL) try FileManager.default.removeItem(at: oldStoreURL)
} catch { } catch {
fatalError("Something went wrong while deleting the old store: \(error)") fatalError(
LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription).localizedDescription
)
} }
} }

View File

@ -17,16 +17,31 @@ final class WriteFreelyModel: ObservableObject {
@Published var selectedCollection: WFACollection? @Published var selectedCollection: WFACollection?
@Published var showAllPosts: Bool = true @Published var showAllPosts: Bool = true
@Published var isPresentingDeleteAlert: Bool = false @Published var isPresentingDeleteAlert: Bool = false
@Published var isPresentingLoginErrorAlert: Bool = false
@Published var isPresentingNetworkErrorAlert: Bool = false
@Published var postToDelete: WFAPost? @Published var postToDelete: WFAPost?
@Published var hasError: Bool = false
#if os(iOS) #if os(iOS)
@Published var isPresentingSettingsView: Bool = false @Published var isPresentingSettingsView: Bool = false
#endif #endif
static var shared = WriteFreelyModel() 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 // swiftlint:disable line_length
let helpURL = URL(string: "https://discuss.write.as/c/help/5")! let helpURL = URL(string: "https://discuss.write.as/c/help/5")!
@ -48,7 +63,7 @@ final class WriteFreelyModel: ObservableObject {
self.account.restoreState() self.account.restoreState()
if self.account.isLoggedIn { if self.account.isLoggedIn {
guard let serverURL = URL(string: self.account.server) else { guard let serverURL = URL(string: self.account.server) else {
print("Server URL not found") self.currentError = AccountError.invalidServerURL
return return
} }
do { do {
@ -56,8 +71,7 @@ final class WriteFreelyModel: ObservableObject {
username: self.account.username, username: self.account.username,
server: self.account.server server: self.account.server
) else { ) else {
self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription self.currentError = KeychainError.couldNotFetchAccessToken
self.isPresentingLoginErrorAlert = true
return return
} }
@ -67,8 +81,7 @@ final class WriteFreelyModel: ObservableObject {
self.fetchUserCollections() self.fetchUserCollections()
self.fetchUserPosts() self.fetchUserPosts()
} catch { } catch {
self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription self.currentError = KeychainError.couldNotFetchAccessToken
self.isPresentingLoginErrorAlert = true
} }
} }
} }

View File

@ -41,6 +41,7 @@ struct ContentView: View {
#if os(macOS) #if os(macOS)
ZStack { ZStack {
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
if model.isProcessingRequest { if model.isProcessingRequest {
ZStack { ZStack {
Color(NSColor.controlBackgroundColor).opacity(0.75) Color(NSColor.controlBackgroundColor).opacity(0.75)
@ -50,6 +51,7 @@ struct ContentView: View {
} }
#else #else
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
#endif #endif
Text("Select a post, or create a new local draft.") Text("Select a post, or create a new local draft.")

View File

@ -3,6 +3,7 @@ import Combine
struct PostListView: View { struct PostListView: View {
@EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@Environment(\.managedObjectContext) var managedObjectContext @Environment(\.managedObjectContext) var managedObjectContext
@State private var postCount: Int = 0 @State private var postCount: Int = 0
@ -86,17 +87,6 @@ struct PostListView: View {
Spacer() Spacer()
Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
.foregroundColor(.secondary) .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() Spacer()
if model.isProcessingRequest { if model.isProcessingRequest {
ProgressView() ProgressView()
@ -138,6 +128,16 @@ struct PostListView: View {
model.selectedCollection = selectedCollection model.selectedCollection = selectedCollection
model.showAllPosts = showAllPosts 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 #else
PostListFilteredView( PostListFilteredView(
collection: selectedCollection, collection: selectedCollection,
@ -148,18 +148,6 @@ struct PostListView: View {
ToolbarItemGroup(placement: .primaryAction) { ToolbarItemGroup(placement: .primaryAction) {
if model.selectedPost != nil { if model.selectedPost != nil {
ActivePostToolbarView(activePost: model.selectedPost!) 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.selectedCollection = selectedCollection
model.showAllPosts = showAllPosts 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 #endif
} }
} }

View File

@ -24,6 +24,12 @@
171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; }; 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; };
171DC677272C7D0B002B9B8A /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171DC676272C7D0B002B9B8A /* UserDefaults+Extensions.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 */; }; 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 */; }; 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172C492D2593981900E20ADF /* MacUpdatesView.swift */; };
172E10012735B83E00061372 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 172E10002735B83E00061372 /* UniformTypeIdentifiers.framework */; platformFilter = maccatalyst; }; 172E10012735B83E00061372 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 172E10002735B83E00061372 /* UniformTypeIdentifiers.framework */; platformFilter = maccatalyst; };
172E10042735B83E00061372 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 172E10032735B83E00061372 /* Media.xcassets */; }; 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 = "<group>"; }; 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
171BFDF924D4AF8300888236 /* CollectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListView.swift; sourceTree = "<group>"; }; 171BFDF924D4AF8300888236 /* CollectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListView.swift; sourceTree = "<group>"; };
171DC676272C7D0B002B9B8A /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = "<group>"; }; 171DC676272C7D0B002B9B8A /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = "<group>"; };
1727526528099802003D0A6A /* ErrorConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorConstants.swift; sourceTree = "<group>"; };
172752692809991A003D0A6A /* ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandling.swift; sourceTree = "<group>"; };
172C492D2593981900E20ADF /* MacUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacUpdatesView.swift; sourceTree = "<group>"; }; 172C492D2593981900E20ADF /* MacUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacUpdatesView.swift; sourceTree = "<group>"; };
172E0FFF2735B83E00061372 /* ActionExtension-iOS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "ActionExtension-iOS.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
17275264280997BF003D0A6A /* ErrorHandling */ = {
isa = PBXGroup;
children = (
1727526528099802003D0A6A /* ErrorConstants.swift */,
172752692809991A003D0A6A /* ErrorHandling.swift */,
);
path = ErrorHandling;
sourceTree = "<group>";
};
172E10022735B83E00061372 /* ActionExtension-iOS */ = { 172E10022735B83E00061372 /* ActionExtension-iOS */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -476,6 +493,7 @@
17DF328324C87D3500BCE2E3 /* Assets.xcassets */, 17DF328324C87D3500BCE2E3 /* Assets.xcassets */,
17DF32D024C8B75C00BCE2E3 /* Account */, 17DF32D024C8B75C00BCE2E3 /* Account */,
1756AE7F24CB841200FD7257 /* Extensions */, 1756AE7F24CB841200FD7257 /* Extensions */,
17275264280997BF003D0A6A /* ErrorHandling */,
1762DCB124EB07680019C4EB /* Models */, 1762DCB124EB07680019C4EB /* Models */,
17DF32CC24C8B72300BCE2E3 /* Navigation */, 17DF32CC24C8B72300BCE2E3 /* Navigation */,
1739B8D324EAFAB700DA7421 /* PostEditor */, 1739B8D324EAFAB700DA7421 /* PostEditor */,
@ -871,8 +889,10 @@
172E10212735C64600061372 /* WFACollection+CoreDataProperties.swift in Sources */, 172E10212735C64600061372 /* WFACollection+CoreDataProperties.swift in Sources */,
172E101C2735C57400061372 /* LocalStorageManager.swift in Sources */, 172E101C2735C57400061372 /* LocalStorageManager.swift in Sources */,
172E10192735C3DB00061372 /* ContentView.swift in Sources */, 172E10192735C3DB00061372 /* ContentView.swift in Sources */,
1727526828099802003D0A6A /* ErrorConstants.swift in Sources */,
172E10152735C2BD00061372 /* UIHostingView.swift in Sources */, 172E10152735C2BD00061372 /* UIHostingView.swift in Sources */,
172E101F2735C64600061372 /* WFAPost+CoreDataClass.swift in Sources */, 172E101F2735C64600061372 /* WFAPost+CoreDataClass.swift in Sources */,
1727526C2809991A003D0A6A /* ErrorHandling.swift in Sources */,
172E10232735C6FF00061372 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 172E10232735C6FF00061372 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */,
172E101E2735C62F00061372 /* PostStatus.swift in Sources */, 172E101E2735C62F00061372 /* PostStatus.swift in Sources */,
); );
@ -887,6 +907,7 @@
17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */, 17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */,
17C42E622507D8E600072984 /* PostStatus.swift in Sources */, 17C42E622507D8E600072984 /* PostStatus.swift in Sources */,
1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */, 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */,
1727526A2809991A003D0A6A /* ErrorHandling.swift in Sources */,
1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */, 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */,
17C42E652509237800072984 /* PostListFilteredView.swift in Sources */, 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */,
170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */, 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */,
@ -912,6 +933,7 @@
1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */, 1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */,
17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */,
17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */, 17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */,
1727526628099802003D0A6A /* ErrorConstants.swift in Sources */,
1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */,
17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */, 17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */,
17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */, 17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */,
@ -937,6 +959,7 @@
17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */,
172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */, 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */,
1727526728099802003D0A6A /* ErrorConstants.swift in Sources */,
17479F152583D8E40072B7FB /* PostEditorSharingPicker.swift in Sources */, 17479F152583D8E40072B7FB /* PostEditorSharingPicker.swift in Sources */,
17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */,
17C42E662509237800072984 /* PostListFilteredView.swift in Sources */, 17C42E662509237800072984 /* PostListFilteredView.swift in Sources */,
@ -944,6 +967,7 @@
17D4926727947D780035BD7E /* MacUpdatesViewModel.swift in Sources */, 17D4926727947D780035BD7E /* MacUpdatesViewModel.swift in Sources */,
17466626256C0D0600629997 /* MacEditorTextView.swift in Sources */, 17466626256C0D0600629997 /* MacEditorTextView.swift in Sources */,
170A7EC226F5186A00F1CBD4 /* CollectionListModel.swift in Sources */, 170A7EC226F5186A00F1CBD4 /* CollectionListModel.swift in Sources */,
1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */,
17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */, 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */,
17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */,
1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */,

View File

@ -7,17 +7,17 @@
<key>ActionExtension-iOS.xcscheme_^#shared#^_</key> <key>ActionExtension-iOS.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>1</integer> <integer>0</integer>
</dict> </dict>
<key>WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_</key> <key>WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>1</integer>
</dict> </dict>
<key>WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_</key> <key>WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>2</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>