mirror of
https://github.com/writeas/writefreely-swiftui-multiplatform.git
synced 2024-11-15 01:11:02 +00:00
Alert on error: shared code (#207)
* Initial work on presenting alert on error
* Move Account-related error handling up the hierarchy
* Handle errors on logout
* Fix for temporary debugging
* Clean up WriteFreelyModel’s published vars
* Add error handling to top-level content view
* Set current error on API call failures
* Set current error on API call handlers
* Move User Defaults errors to ErrorConstants file
* Add default values for some error strings
* Handle purging post errors
* Add FIXME to track silent failure on fetching collections
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.
* Handle errors in (most) shared code
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
* Add error handling to Mac app
* Revert "Add error handling to Mac app"
This reverts commit b1a8b8b29c
.
This commit is contained in:
parent
b790c9ff49
commit
a51bbd3abc
@ -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,13 +88,6 @@ 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"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -2,19 +2,33 @@ import SwiftUI
|
||||
|
||||
struct AccountView: View {
|
||||
@EnvironmentObject var model: WriteFreelyModel
|
||||
@EnvironmentObject var errorHandling: ErrorHandling
|
||||
|
||||
var body: some View {
|
||||
if model.account.isLoggedIn {
|
||||
HStack {
|
||||
Spacer()
|
||||
AccountLogoutView()
|
||||
.withErrorHandling()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
AccountLoginView()
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
173
Shared/ErrorHandling/ErrorConstants.swift
Normal file
173
Shared/ErrorHandling/ErrorConstants.swift
Normal file
@ -0,0 +1,173 @@
|
||||
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: - 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 {
|
||||
case couldNotSaveContext
|
||||
case couldNotFetchCollections
|
||||
case couldNotFetchPosts(String = "")
|
||||
case couldNotPurgePosts(String = "")
|
||||
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 .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):
|
||||
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(String = "")
|
||||
}
|
||||
|
||||
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(let customContent):
|
||||
if customContent.isEmpty {
|
||||
return NSLocalizedString("Something went wrong", comment: "")
|
||||
} else {
|
||||
return NSLocalizedString(customContent, comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
Shared/ErrorHandling/ErrorHandling.swift
Normal file
42
Shared/ErrorHandling/ErrorHandling.swift
Normal 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())
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
@ -47,10 +47,13 @@ extension WriteFreelyModel {
|
||||
|
||||
func fetchUserCollections() {
|
||||
if !hasNetworkConnection {
|
||||
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
|
||||
self.currentError = NetworkError.noConnectionError
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else {
|
||||
self.currentError = AppError.couldNotGetLoggedInClient
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else { return }
|
||||
// We're starting the network request.
|
||||
DispatchQueue.main.async {
|
||||
self.isProcessingRequest = true
|
||||
@ -60,10 +63,13 @@ extension WriteFreelyModel {
|
||||
|
||||
func fetchUserPosts() {
|
||||
if !hasNetworkConnection {
|
||||
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
|
||||
self.currentError = NetworkError.noConnectionError
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else {
|
||||
self.currentError = AppError.couldNotGetLoggedInClient
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else { return }
|
||||
// We're starting the network request.
|
||||
DispatchQueue.main.async {
|
||||
self.isProcessingRequest = true
|
||||
@ -75,10 +81,13 @@ extension WriteFreelyModel {
|
||||
postToUpdate = nil
|
||||
|
||||
if !hasNetworkConnection {
|
||||
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
|
||||
self.currentError = NetworkError.noConnectionError
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else {
|
||||
self.currentError = AppError.couldNotGetLoggedInClient
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else { return }
|
||||
// We're starting the network request.
|
||||
DispatchQueue.main.async {
|
||||
self.isProcessingRequest = true
|
||||
@ -120,11 +129,17 @@ extension WriteFreelyModel {
|
||||
|
||||
func updateFromServer(post: WFAPost) {
|
||||
if !hasNetworkConnection {
|
||||
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
|
||||
self.currentError = NetworkError.noConnectionError
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else {
|
||||
self.currentError = AppError.couldNotGetLoggedInClient
|
||||
return
|
||||
}
|
||||
guard let postId = post.postId else {
|
||||
self.currentError = AppError.couldNotGetPostId
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client else { return }
|
||||
guard let postId = post.postId else { return }
|
||||
// We're starting the network request.
|
||||
DispatchQueue.main.async {
|
||||
self.selectedPost = post
|
||||
@ -135,11 +150,17 @@ 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 else {
|
||||
self.currentError = AppError.couldNotGetLoggedInClient
|
||||
return
|
||||
}
|
||||
guard let postId = post.postId else {
|
||||
self.currentError = AppError.couldNotGetPostId
|
||||
return
|
||||
}
|
||||
guard let loggedInClient = client,
|
||||
let postId = post.postId else { return }
|
||||
// We're starting the network request.
|
||||
DispatchQueue.main.async {
|
||||
self.isProcessingRequest = true
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,11 +40,15 @@ extension WriteFreelyModel {
|
||||
client = nil
|
||||
DispatchQueue.main.async {
|
||||
self.account.logout()
|
||||
LocalStorageManager.standard.purgeUserCollections()
|
||||
self.posts.purgePublishedPosts()
|
||||
do {
|
||||
try LocalStorageManager.standard.purgeUserCollections()
|
||||
try self.posts.purgePublishedPosts()
|
||||
} catch {
|
||||
self.currentError = error
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Something went wrong purging the token from the Keychain.")
|
||||
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
|
||||
@ -70,11 +59,15 @@ extension WriteFreelyModel {
|
||||
client = nil
|
||||
DispatchQueue.main.async {
|
||||
self.account.logout()
|
||||
LocalStorageManager.standard.purgeUserCollections()
|
||||
self.posts.purgePublishedPosts()
|
||||
do {
|
||||
try LocalStorageManager.standard.purgeUserCollections()
|
||||
try self.posts.purgePublishedPosts()
|
||||
} catch {
|
||||
self.currentError = error
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Something went wrong purging the token from the Keychain.")
|
||||
self.currentError = KeychainError.couldNotPurgeAccessToken
|
||||
}
|
||||
} catch {
|
||||
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
|
||||
@ -113,13 +106,10 @@ 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)
|
||||
self.currentError = AppError.genericError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +131,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 {
|
||||
@ -158,16 +152,13 @@ extension WriteFreelyModel {
|
||||
LocalStorageManager.standard.saveContext()
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
self.currentError = AppError.genericError(error.localizedDescription)
|
||||
}
|
||||
} 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")
|
||||
self.currentError = LocalStoreError.couldNotFetchPosts("cached")
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,11 +202,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,7 +228,7 @@ extension WriteFreelyModel {
|
||||
LocalStorageManager.standard.saveContext()
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
self.currentError = AppError.genericError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,7 +250,7 @@ extension WriteFreelyModel {
|
||||
DispatchQueue.main.async {
|
||||
LocalStorageManager.standard.container.viewContext.rollback()
|
||||
}
|
||||
print(error)
|
||||
self.currentError = AppError.genericError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,15 +39,15 @@ 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 WFKeychainError.fetchFromKeychainFailed
|
||||
throw KeychainError.couldNotFetchAccessToken
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -23,19 +23,19 @@ final class LocalStorageManager {
|
||||
do {
|
||||
try container.viewContext.save()
|
||||
} catch {
|
||||
print("Error saving context: \(error)")
|
||||
fatalError(LocalStoreError.couldNotSaveContext.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func purgeUserCollections() {
|
||||
func purgeUserCollections() throws {
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFACollection")
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
do {
|
||||
try container.viewContext.executeAndMergeChanges(using: deleteRequest)
|
||||
} catch {
|
||||
print("Error: Failed to purge cached collections.")
|
||||
throw LocalStoreError.couldNotPurgeCollections
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,34 @@ 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()
|
||||
|
||||
// MARK: - Error handling
|
||||
@Published var hasError: Bool = false
|
||||
var currentError: Error? {
|
||||
didSet {
|
||||
#if DEBUG
|
||||
print("⚠️ currentError -> didSet \(currentError?.localizedDescription ?? "nil")")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
@Published var isLoggingIn: Bool = false
|
||||
@Published var isProcessingRequest: Bool = false
|
||||
@Published var hasNetworkConnection: Bool = true
|
||||
@ -17,17 +41,13 @@ 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?
|
||||
#if os(iOS)
|
||||
#if os(iOS)
|
||||
@Published var isPresentingSettingsView: Bool = false
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static var shared = WriteFreelyModel()
|
||||
|
||||
var loginErrorMessage: String?
|
||||
|
||||
// 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")!
|
||||
@ -48,7 +68,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 +76,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 +86,8 @@ final class WriteFreelyModel: ObservableObject {
|
||||
self.fetchUserCollections()
|
||||
self.fetchUserPosts()
|
||||
} catch {
|
||||
self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription
|
||||
self.isPresentingLoginErrorAlert = true
|
||||
self.currentError = KeychainError.couldNotFetchAccessToken
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,11 +38,13 @@ struct ContentView: View {
|
||||
}
|
||||
#else
|
||||
CollectionListView()
|
||||
.withErrorHandling()
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
ZStack {
|
||||
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
|
||||
.withErrorHandling()
|
||||
if model.isProcessingRequest {
|
||||
ZStack {
|
||||
Color(NSColor.controlBackgroundColor).opacity(0.75)
|
||||
@ -50,12 +54,23 @@ struct ContentView: View {
|
||||
}
|
||||
#else
|
||||
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
|
||||
.withErrorHandling()
|
||||
#endif
|
||||
|
||||
Text("Select a post, or create a new local draft.")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,8 @@ class CollectionListModel: NSObject, ObservableObject {
|
||||
try collectionsController.performFetch()
|
||||
list = collectionsController.fetchedObjects ?? []
|
||||
} catch {
|
||||
print("Failed to fetch collections!")
|
||||
// FIXME: Errors cannot be thrown out of the CollectionListView property initializer
|
||||
fatalError(LocalStoreError.couldNotFetchCollections.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ class PostListModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func purgePublishedPosts() {
|
||||
func purgePublishedPosts() throws {
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ struct WriteFreely_MultiPlatformApp: App {
|
||||
}
|
||||
}
|
||||
})
|
||||
.withErrorHandling()
|
||||
.environmentObject(model)
|
||||
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)
|
||||
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
|
||||
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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 = "<group>";
|
||||
};
|
||||
17275264280997BF003D0A6A /* ErrorHandling */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1727526528099802003D0A6A /* ErrorConstants.swift */,
|
||||
172752692809991A003D0A6A /* ErrorHandling.swift */,
|
||||
);
|
||||
path = ErrorHandling;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>ActionExtension-iOS.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -9,6 +9,7 @@ struct SettingsView: View {
|
||||
Form {
|
||||
Section(header: Text("Login Details")) {
|
||||
AccountView()
|
||||
.withErrorHandling()
|
||||
}
|
||||
Section(header: Text("Appearance")) {
|
||||
PreferencesView(preferences: model.preferences)
|
||||
|
Loading…
Reference in New Issue
Block a user