diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift index fc360ab..274eb45 100644 --- a/Shared/Account/AccountLoginView.swift +++ b/Shared/Account/AccountLoginView.swift @@ -1,125 +1,106 @@ import SwiftUI struct AccountLoginView: View { - @State private var username: String = "" - @State private var password: String = "" - @State private var server: String = "" + @ObservedObject var account: AccountModel + @State private var isShowingAlert: Bool = false @State private var alertMessage: String = "" - @Binding var accountModel: AccountModel - @Binding var isLoggedIn: Bool - @Binding var isLoggingIn: Bool - var body: some View { VStack { HStack { Image(systemName: "person.circle") .foregroundColor(.gray) #if os(iOS) - TextField("Username", text: $username) + TextField("Username", text: $account.username) .keyboardType(.emailAddress) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(RoundedBorderTextFieldStyle()) #else - TextField("Username", text: $username) + TextField("Username", text: $account.username) #endif } HStack { Image(systemName: "lock.circle") .foregroundColor(.gray) #if os(iOS) - SecureField("Password", text: $password) + SecureField("Password", text: $account.password) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(RoundedBorderTextFieldStyle()) #else - SecureField("Password", text: $password) + SecureField("Password", text: $account.password) #endif } HStack { Image(systemName: "link.circle") .foregroundColor(.gray) #if os(iOS) - TextField("Server URL", text: $server) + TextField("Server URL", text: $account.server) .keyboardType(.URL) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(RoundedBorderTextFieldStyle()) #else - TextField("Server URL", text: $server) + TextField("Server URL", text: $account.server) #endif } Spacer() - if isLoggingIn { + if account.isLoggingIn { ProgressView("Logging in...") + .padding() } else { Button(action: { - accountModel.login( - to: server, - as: username, password: password, + account.login( + to: account.server, + as: account.username, password: account.password, completion: loginHandler ) - isLoggingIn = true }, label: { Text("Login") - }).disabled(isLoggedIn) + }) + .disabled(account.isLoggedIn) + .padding() } } .alert(isPresented: $isShowingAlert) { Alert( title: Text("Error Logging In"), message: Text(alertMessage), dismissButton: .default(Text("OK")) ) } } func loginHandler(result: Result) { do { _ = try result.get() - isLoggedIn = true - isLoggingIn = false } catch AccountError.serverNotFound { alertMessage = """ The server could not be found. Please check that you've entered the information correctly and try again. """ - isLoggedIn = false - isLoggingIn = false isShowingAlert = true } catch AccountError.invalidCredentials { alertMessage = """ Invalid username or password. Please check that you've entered the information correctly and try again. """ - isLoggedIn = false - isLoggingIn = false isShowingAlert = true } catch { alertMessage = "An unknown error occurred. Please try again." - isLoggedIn = false - isLoggingIn = false isShowingAlert = true } } } struct AccountLoginView_LoggedOutPreviews: PreviewProvider { static var previews: some View { - AccountLoginView( - accountModel: .constant(AccountModel()), - isLoggedIn: .constant(false), - isLoggingIn: .constant(false) - ) + AccountLoginView(account: AccountModel()) } } struct AccountLoginView_LoggingInPreviews: PreviewProvider { static var previews: some View { - AccountLoginView( - accountModel: .constant(AccountModel()), - isLoggedIn: .constant(false), - isLoggingIn: .constant(true) - ) + AccountLoginView(account: AccountModel()) } } diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift index e59c46d..bc84b50 100644 --- a/Shared/Account/AccountLogoutView.swift +++ b/Shared/Account/AccountLogoutView.swift @@ -1,36 +1,29 @@ import SwiftUI struct AccountLogoutView: View { - @Binding var accountModel: AccountModel - @Binding var isLoggedIn: Bool - @Binding var isLoggingIn: Bool + @ObservedObject var account: AccountModel var body: some View { VStack { Spacer() VStack { - Text("Logged in as \(accountModel.username ?? "UNKNOWN")") - Text("on \(accountModel.server ?? "UNKNOWN")") + Text("Logged in as \(account.username)") + Text("on \(account.server)") } Spacer() Button(action: logoutHandler, label: { Text("Logout") }) } } func logoutHandler() { - isLoggedIn = false - isLoggingIn = false + account.logout() } } struct AccountLogoutView_Previews: PreviewProvider { static var previews: some View { - AccountLogoutView( - accountModel: .constant(AccountModel()), - isLoggedIn: .constant(true), - isLoggingIn: .constant(false) - ) + AccountLogoutView(account: AccountModel()) } } diff --git a/Shared/Account/AccountModel.swift b/Shared/Account/AccountModel.swift index e460f48..bde756f 100644 --- a/Shared/Account/AccountModel.swift +++ b/Shared/Account/AccountModel.swift @@ -1,49 +1,68 @@ import Foundation enum AccountError: Error { case invalidCredentials case serverNotFound } -struct AccountModel { - private(set) var id: UUID? - var username: String? - var password: String? - var server: String? +class AccountModel: ObservableObject { + @Published private(set) var id: UUID? + @Published private(set) var isLoggedIn: Bool = false + @Published private(set) var isLoggingIn: Bool = false + @Published var username: String = "" + @Published var password: String = "" + @Published var server: String = "" - mutating func login( + func login( to server: String, as username: String, password: String, completion: @escaping (Result) -> Void ) { + self.isLoggingIn = true let result: Result if server != validServer { result = .failure(.serverNotFound) } else if username == validCredentials["username"] && password == validCredentials["password"] { self.id = UUID() self.username = username self.password = password self.server = server result = .success(self.id!) } else { result = .failure(.invalidCredentials) } #if DEBUG // Delay to simulate async network call DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.isLoggingIn = false + do { + _ = try result.get() + self.isLoggedIn = true + } catch { + self.isLoggedIn = false + } completion(result) } #endif } + + func logout() { + id = nil + isLoggedIn = false + isLoggingIn = false + username = "" + password = "" + server = "" + } } #if DEBUG let validCredentials = [ "username": "name@example.com", "password": "12345" ] let validServer = "https://test.server.url" #endif diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index ef838c3..a0382d7 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -1,25 +1,25 @@ import SwiftUI struct AccountView: View { - @State private var accountModel = AccountModel() - @State private var isLoggingIn: Bool = false - @State private var isLoggedIn: Bool = false + @ObservedObject var account: AccountModel var body: some View { - if isLoggedIn { + if account.isLoggedIn { HStack { Spacer() - AccountLogoutView(accountModel: $accountModel, isLoggedIn: $isLoggedIn, isLoggingIn: $isLoggingIn) + AccountLogoutView(account: account) Spacer() } + .padding() } else { - AccountLoginView(accountModel: $accountModel, isLoggedIn: $isLoggedIn, isLoggingIn: $isLoggingIn) + AccountLoginView(account: account) + .padding() } } } struct AccountLogin_Previews: PreviewProvider { static var previews: some View { - AccountView() + AccountView(account: AccountModel()) } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index b404072..c92177a 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,25 +1,27 @@ import SwiftUI struct ContentView: View { @ObservedObject var postStore: PostStore @ObservedObject var preferences: PreferencesModel + @ObservedObject var account: AccountModel var body: some View { NavigationView { SidebarView() PostList(selectedCollection: allPostsCollection) Text("Select a post, or create a new draft.") .foregroundColor(.secondary) } .environmentObject(postStore) .environmentObject(preferences) + .environmentObject(account) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(postStore: testPostStore, preferences: PreferencesModel()) + ContentView(postStore: testPostStore, preferences: PreferencesModel(), account: AccountModel()) } } diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index feb4993..26834e3 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -1,27 +1,29 @@ import SwiftUI @main struct WriteFreely_MultiPlatformApp: App { @StateObject private var preferences = PreferencesModel() + @StateObject private var account = AccountModel() #if DEBUG @StateObject private var store = testPostStore #else @StateObject private var store = PostStore() #endif var body: some Scene { WindowGroup { - ContentView(postStore: store, preferences: preferences) + ContentView(postStore: store, preferences: preferences, account: account) .preferredColorScheme(preferences.preferredColorScheme) } #if os(macOS) Settings { SettingsView(preferences: preferences) .frame(minWidth: 300, maxWidth: 300, minHeight: 200, maxHeight: 200) .padding() + .preferredColorScheme(preferences.preferredColorScheme) } #endif } } diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift index 50ad760..a9c1d26 100644 --- a/iOS/Settings/SettingsView.swift +++ b/iOS/Settings/SettingsView.swift @@ -1,28 +1,29 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var preferences: PreferencesModel + @EnvironmentObject var account: AccountModel @Binding var isPresented: Bool var body: some View { VStack { SettingsHeaderView(isPresented: $isPresented) Form { Section(header: Text("Login Details")) { - AccountView() + AccountView(account: account) } Section(header: Text("Appearance")) { PreferencesView(preferences: preferences) } } } .preferredColorScheme(preferences.preferredColorScheme) } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView(isPresented: .constant(true)) } }