From f766e0893ae95fe02b0f09467e82e9c867e248ae Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sat, 18 Jan 2025 00:33:45 +0100 Subject: [PATCH] Create password attempt when submitting --- password/data/manager/PasswordManager.swift | 18 +++++ .../PasswordAttemptRepository.swift | 54 +++++++++++++ .../data/repository/PasswordRepository.swift | 4 - password/view/detail/DetailView.swift | 78 +++++++++++-------- password/view/detail/DetailViewModel.swift | 58 ++++++++++++++ password/view/list/ListView.swift | 11 ++- password/view/list/ListViewModel.swift | 7 +- 7 files changed, 183 insertions(+), 47 deletions(-) create mode 100644 password/data/repository/PasswordAttemptRepository.swift create mode 100644 password/view/detail/DetailViewModel.swift diff --git a/password/data/manager/PasswordManager.swift b/password/data/manager/PasswordManager.swift index d377cf6..4dbd143 100644 --- a/password/data/manager/PasswordManager.swift +++ b/password/data/manager/PasswordManager.swift @@ -12,6 +12,7 @@ import SwiftData class PasswordManager { let passwordRepository: PasswordRepository + let passwordAttemptRepository: PasswordAttemptRepository let passwordKeychainRepository: PasswordKeychainRepository private let context: ModelContext @@ -19,10 +20,12 @@ class PasswordManager { init( context: ModelContext, passwordRepository: PasswordRepository, + passwordAttemptRepository: PasswordAttemptRepository, passwordKeychainRepository: PasswordKeychainRepository ) { self.context = context self.passwordRepository = passwordRepository + self.passwordAttemptRepository = passwordAttemptRepository self.passwordKeychainRepository = passwordKeychainRepository } @@ -40,6 +43,10 @@ class PasswordManager { return passwordKeychainRepository.getPassword(withID: id) } + func getPasswordAttempts(passwordID id: UUID) -> [PasswordAttempt] { + return passwordAttemptRepository.getPasswordAttempts(passwordID: id) + } + @MainActor func createPassword(name: String, value: String) -> Password { @@ -65,6 +72,16 @@ class PasswordManager { return password } + @MainActor + func createPasswordAttempt(passwordID: UUID, isSuccessful: Bool, typingTime: Double) { + let attempt = PasswordAttempt( + password: passwordID, + isSuccessful: isSuccessful, + typingTime: typingTime + ) + passwordAttemptRepository.createPasswordAttempt(attempt: attempt) + } + @MainActor func deletePassword(_ password: Password) -> Bool { @@ -76,6 +93,7 @@ class PasswordManager { try context.transaction { passwordRepository.deletePassword(password) + try passwordAttemptRepository.deleteAllAttempts(withID: password.id) try passwordKeychainRepository.deletePassword(withID: password.id) } diff --git a/password/data/repository/PasswordAttemptRepository.swift b/password/data/repository/PasswordAttemptRepository.swift new file mode 100644 index 0000000..b749ac4 --- /dev/null +++ b/password/data/repository/PasswordAttemptRepository.swift @@ -0,0 +1,54 @@ +// +// PasswordAttemptRepository.swift +// password +// +// Created by Markus Thielker on 17.01.25. +// + +import Foundation +import SwiftUICore +import SwiftData + +class PasswordAttemptRepository { + + private let context: ModelContext + + init(_ context: ModelContext) { + self.context = context + } + + func getPasswordAttempts(passwordID id: UUID) -> [PasswordAttempt] { + + print("fetching attempts for password \(id)") + + var attempts: [PasswordAttempt] = [] + do { + + let request = FetchDescriptor( + predicate: #Predicate { $0.password == id }, + sortBy: [SortDescriptor(\.timestamp)] + ) + attempts = try context.fetch(request) + + print("found \(attempts.count) attempts for password \(id)") + + } catch { + print("fetching attempts failed: \(error)") + } + + return attempts + } + + @MainActor + func createPasswordAttempt(attempt: PasswordAttempt) { + context.insert(attempt) + print("inserted attempt") + } + + @MainActor + func deleteAllAttempts(withID id: UUID) throws { + let predicate = #Predicate { $0.password == id } + try context.delete(model: PasswordAttempt.self, where: predicate) + print("deleted all attempts for password \(id)") + } +} diff --git a/password/data/repository/PasswordRepository.swift b/password/data/repository/PasswordRepository.swift index 4a512e0..fbc8ab6 100644 --- a/password/data/repository/PasswordRepository.swift +++ b/password/data/repository/PasswordRepository.swift @@ -9,10 +9,6 @@ import Foundation import SwiftUICore import SwiftData -enum PasswordRepositoryError: Error { - case notFound -} - class PasswordRepository { private let context: ModelContext diff --git a/password/view/detail/DetailView.swift b/password/view/detail/DetailView.swift index e6f1d73..28caf8e 100644 --- a/password/view/detail/DetailView.swift +++ b/password/view/detail/DetailView.swift @@ -9,47 +9,61 @@ import SwiftUI struct DetailView: View { - let password: Password - let passwordKC: PasswordKC + @ObservedObject var viewModel: DetailViewModel - @State var value: String = "" - - func validateInput(input: String, password: Password) -> Bool { - return input == passwordKC.value + @State private var startTime: Date? + @State private var elapsedTime: Double = -1 + @State private var value: String = "" + + init(viewModel: DetailViewModel) { + self.viewModel = viewModel } - + + func validateInput(input: String, password: Password) -> Bool { + return input == viewModel.passwordKC.value + } + var body: some View { VStack { - Text("Enter the password for \(password.name)") + Text("Enter the password for \(viewModel.password.name) and submit with \"Enter\"") Form { SecureField("", text: $value) - Button("Submit") { - let correct = validateInput(input: value, password: password) - - if (correct) { - let alert = NSAlert() - alert.messageText = "Correct" - alert.informativeText = "That one was correct!" - alert.addButton(withTitle: "Let's go!") - alert.runModal() - } else { - let alert = NSAlert() - alert.messageText = "Not quite" - alert.informativeText = " That one was not quite right! Try again!" - alert.addButton(withTitle: "Okay") - alert.runModal() + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onChange(of: value) { _, _ in + if (value.isEmpty){ + startTime = nil + } else if startTime == nil { + startTime = Date() + } + } + .onSubmit { + if let startTime = startTime { + elapsedTime = Date().timeIntervalSince(startTime) + } + + let isSuccessful = validateInput(input: value, password: viewModel.password) + viewModel.createPasswordAttempt(isSuccessful: isSuccessful, typingTime: elapsedTime) + + if isSuccessful { + let alert = NSAlert() + alert.messageText = "Correct" + alert.informativeText = "That one was correct!" + alert.addButton(withTitle: "Let's go!") + alert.runModal() + } else { + let alert = NSAlert() + alert.messageText = "Not quite" + alert.informativeText = " That one was not quite right! Try again!" + alert.addButton(withTitle: "Okay") + alert.runModal() + } + + value = "" + startTime = nil + elapsedTime = -1 } - - value = "" - } } } .padding() } } - -#Preview { - let password = Password(name: "macbook") - let passwordKC = PasswordKC(id: password.id, value: "admin") - DetailView(password: password, passwordKC: passwordKC) -} diff --git a/password/view/detail/DetailViewModel.swift b/password/view/detail/DetailViewModel.swift new file mode 100644 index 0000000..a6f4ad3 --- /dev/null +++ b/password/view/detail/DetailViewModel.swift @@ -0,0 +1,58 @@ +// +// ListViewModel.swift +// password +// +// Created by Markus Thielker on 16.01.25. +// + +import Foundation +import SwiftData + +class DetailViewModel: ObservableObject { + + let passwordID: UUID + + private let context: ModelContext + private let passwordManager: PasswordManager + + @Published var password: Password + @Published var passwordKC: PasswordKC + @Published var passwordAttempts: [PasswordAttempt] + + @MainActor + init(context: ModelContext, passwordID id: UUID) { + self.context = context + self.passwordID = id + + let passwordRepository = PasswordRepository(context) + let passwordAttemptRepository = PasswordAttemptRepository(context) + let passwordKeychainRepository = PasswordKeychainRepository() + + self.passwordManager = PasswordManager( + context: context, + passwordRepository: passwordRepository, + passwordAttemptRepository: passwordAttemptRepository, + passwordKeychainRepository: passwordKeychainRepository + ) + + password = passwordManager.getPassword(withID: id)! + passwordKC = passwordManager.getPasswordKeychain(withID: id)! + passwordAttempts = passwordManager.getPasswordAttempts(passwordID: id) + } + + @MainActor + func getPassword(withID id: UUID) -> Password? { + return passwordManager.getPassword(withID: id) + } + + @MainActor + func getPasswordKeychain(withID id: UUID) -> PasswordKC? { + return passwordManager.getPasswordKeychain(withID: id) + } + + @MainActor + func createPasswordAttempt(isSuccessful: Bool, typingTime: Double) { + passwordManager.createPasswordAttempt(passwordID: password.id, isSuccessful: isSuccessful, typingTime: typingTime) + passwordAttempts = passwordManager.getPasswordAttempts(passwordID: password.id) + } +} diff --git a/password/view/list/ListView.swift b/password/view/list/ListView.swift index 929f71d..a6d3ae6 100644 --- a/password/view/list/ListView.swift +++ b/password/view/list/ListView.swift @@ -10,6 +10,8 @@ import _SwiftData_SwiftUI struct ListView: View { + @Environment(\.modelContext) var context + @ObservedObject var viewModel: ListViewModel @State var isAddingPassword: Bool = false @@ -17,10 +19,7 @@ struct ListView: View { NavigationView { List { ForEach(viewModel.passwords) { password in - NavigationLink(destination: DetailView( - password: password, - passwordKC: viewModel.getPasswordKeychain(withID: password.id)! - )) { + NavigationLink(destination: DetailView(viewModel: DetailViewModel(context: context, passwordID: password.id))) { Text(password.name) } } @@ -32,7 +31,7 @@ struct ListView: View { .frame(width: 20, height: 20) } Button(action: { - viewModel.getAllPasswords() + viewModel.passwords = viewModel.getAllPasswords() }) { Image(systemName: "arrow.trianglehead.clockwise") .imageScale(.medium) @@ -45,7 +44,7 @@ struct ListView: View { AddPasswordView(viewModel: viewModel) } .onAppear { - viewModel.getAllPasswords() + viewModel.passwords = viewModel.getAllPasswords() } } } diff --git a/password/view/list/ListViewModel.swift b/password/view/list/ListViewModel.swift index 9cb32fd..f211674 100644 --- a/password/view/list/ListViewModel.swift +++ b/password/view/list/ListViewModel.swift @@ -20,15 +20,15 @@ class ListViewModel: ObservableObject { self.context = context let passwordRepository = PasswordRepository(context) + let passwordAttemptRepository = PasswordAttemptRepository(context) let passwordKeychainRepository = PasswordKeychainRepository() self.passwordManager = PasswordManager( context: context, passwordRepository: passwordRepository, + passwordAttemptRepository: passwordAttemptRepository, passwordKeychainRepository: passwordKeychainRepository ) - - passwords = getAllPasswords() } @MainActor @@ -55,8 +55,5 @@ class ListViewModel: ObservableObject { @MainActor func deletePassword(_ password: Password) { let success = passwordManager.deletePassword(password) - if success { - passwords = getAllPasswords() - } } }