diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3694e57..1ecbde3 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -79,9 +79,6 @@ }, "Automatischer Sync" : { - }, - "Benutzername" : { - }, "Bookmark archivieren" : { @@ -163,12 +160,6 @@ }, "https://readeck.example.com" : { - }, - "Ihr Benutzername" : { - - }, - "Ihr Passwort" : { - }, "Ihre aktuelle Server-Verbindung und Anmeldedaten." : { @@ -240,7 +231,7 @@ "Optional: Eigener Titel" : { }, - "Passwort" : { + "Password" : { }, "readeck Bookmark Title" : { @@ -305,6 +296,9 @@ }, "URL gefunden:" : { + }, + "Username" : { + }, "Version %@" : { @@ -326,6 +320,12 @@ }, "Wird gespeichert..." : { + }, + "Your Password" : { + + }, + "Your Username" : { + }, "z.B. arbeit, wichtig, später" : { diff --git a/readeck/Domain/UseCase/AddLabelsToBookmarkUseCase.swift b/readeck/Domain/UseCase/AddLabelsToBookmarkUseCase.swift index 58869e3..34840ba 100644 --- a/readeck/Domain/UseCase/AddLabelsToBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/AddLabelsToBookmarkUseCase.swift @@ -1,6 +1,11 @@ import Foundation -class AddLabelsToBookmarkUseCase { +protocol PAddLabelsToBookmarkUseCase { + func execute(bookmarkId: String, labels: [String]) async throws + func execute(bookmarkId: String, label: String) async throws +} + +class AddLabelsToBookmarkUseCase: PAddLabelsToBookmarkUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/AddTextToSpeechQueueUseCase.swift b/readeck/Domain/UseCase/AddTextToSpeechQueueUseCase.swift index 9d86e91..951119a 100644 --- a/readeck/Domain/UseCase/AddTextToSpeechQueueUseCase.swift +++ b/readeck/Domain/UseCase/AddTextToSpeechQueueUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class AddTextToSpeechQueueUseCase { +protocol PAddTextToSpeechQueueUseCase { + func execute(bookmarkDetail: BookmarkDetail) +} + +class AddTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase { private let speechQueue: SpeechQueue init(speechQueue: SpeechQueue = .shared) { diff --git a/readeck/Domain/UseCase/CreateBookmarkUseCase.swift b/readeck/Domain/UseCase/CreateBookmarkUseCase.swift index 4c8662a..a926d90 100644 --- a/readeck/Domain/UseCase/CreateBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/CreateBookmarkUseCase.swift @@ -1,6 +1,14 @@ import Foundation -class CreateBookmarkUseCase { +protocol PCreateBookmarkUseCase { + func execute(createRequest: CreateBookmarkRequest) async throws -> String + func createFromURL(_ url: String) async throws -> String + func createFromURLWithTitle(_ url: String, title: String) async throws -> String + func createFromURLWithLabels(_ url: String, labels: [String]) async throws -> String + func createFromClipboard() async throws -> String? +} + +class CreateBookmarkUseCase: PCreateBookmarkUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/DeleteBookmarkUseCase.swift b/readeck/Domain/UseCase/DeleteBookmarkUseCase.swift index cf01232..83deeb8 100644 --- a/readeck/Domain/UseCase/DeleteBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/DeleteBookmarkUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class DeleteBookmarkUseCase { +protocol PDeleteBookmarkUseCase { + func execute(bookmarkId: String) async throws +} + +class DeleteBookmarkUseCase: PDeleteBookmarkUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/GetBookmarkArticleUseCase.swift b/readeck/Domain/UseCase/GetBookmarkArticleUseCase.swift index 54e9d5a..059d5c8 100644 --- a/readeck/Domain/UseCase/GetBookmarkArticleUseCase.swift +++ b/readeck/Domain/UseCase/GetBookmarkArticleUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class GetBookmarkArticleUseCase { +protocol PGetBookmarkArticleUseCase { + func execute(id: String) async throws -> String +} + +class GetBookmarkArticleUseCase: PGetBookmarkArticleUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/GetBookmarkUseCase.swift b/readeck/Domain/UseCase/GetBookmarkUseCase.swift index 8449376..37668c2 100644 --- a/readeck/Domain/UseCase/GetBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/GetBookmarkUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class GetBookmarkUseCase { +protocol PGetBookmarkUseCase { + func execute(id: String) async throws -> BookmarkDetail +} + +class GetBookmarkUseCase: PGetBookmarkUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/GetBookmarksUseCase.swift b/readeck/Domain/UseCase/GetBookmarksUseCase.swift index 027b259..35ed465 100644 --- a/readeck/Domain/UseCase/GetBookmarksUseCase.swift +++ b/readeck/Domain/UseCase/GetBookmarksUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class GetBookmarksUseCase { +protocol PGetBookmarksUseCase { + func execute(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?, tag: String?) async throws -> BookmarksPage +} + +class GetBookmarksUseCase: PGetBookmarksUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/GetLabelsUseCase.swift b/readeck/Domain/UseCase/GetLabelsUseCase.swift index 663fc65..0cd4ec9 100644 --- a/readeck/Domain/UseCase/GetLabelsUseCase.swift +++ b/readeck/Domain/UseCase/GetLabelsUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class GetLabelsUseCase { +protocol PGetLabelsUseCase { + func execute() async throws -> [BookmarkLabel] +} + +class GetLabelsUseCase: PGetLabelsUseCase { private let labelsRepository: PLabelsRepository init(labelsRepository: PLabelsRepository) { diff --git a/readeck/Domain/UseCase/LoadSettingsUseCase.swift b/readeck/Domain/UseCase/LoadSettingsUseCase.swift index 36e297f..03d6da2 100644 --- a/readeck/Domain/UseCase/LoadSettingsUseCase.swift +++ b/readeck/Domain/UseCase/LoadSettingsUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class LoadSettingsUseCase { +protocol PLoadSettingsUseCase { + func execute() async throws -> Settings? +} + +class LoadSettingsUseCase: PLoadSettingsUseCase { private let authRepository: PAuthRepository init(authRepository: PAuthRepository) { diff --git a/readeck/Domain/UseCase/LoginUseCase.swift b/readeck/Domain/UseCase/LoginUseCase.swift index a6e176f..ea5bb91 100644 --- a/readeck/Domain/UseCase/LoginUseCase.swift +++ b/readeck/Domain/UseCase/LoginUseCase.swift @@ -1,4 +1,9 @@ -class LoginUseCase { + +protocol PLoginUseCase { + func execute(endpoint: String, username: String, password: String) async throws -> User +} + +class LoginUseCase: PLoginUseCase { private let repository: PAuthRepository init(repository: PAuthRepository) { diff --git a/readeck/Domain/UseCase/LogoutUseCase.swift b/readeck/Domain/UseCase/LogoutUseCase.swift index c871c39..086eb2c 100644 --- a/readeck/Domain/UseCase/LogoutUseCase.swift +++ b/readeck/Domain/UseCase/LogoutUseCase.swift @@ -7,11 +7,11 @@ import Foundation -protocol LogoutUseCaseProtocol { +protocol PLogoutUseCase { func execute() async throws } -class LogoutUseCase: LogoutUseCaseProtocol { +class LogoutUseCase: PLogoutUseCase { private let settingsRepository: SettingsRepository private let tokenManager: TokenManager diff --git a/readeck/Domain/UseCase/ReadBookmarkUseCase.swift b/readeck/Domain/UseCase/ReadBookmarkUseCase.swift index 8c64f8b..89ff838 100644 --- a/readeck/Domain/UseCase/ReadBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/ReadBookmarkUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class ReadBookmarkUseCase { +protocol PReadBookmarkUseCase { + func execute(bookmarkDetail: BookmarkDetail) +} + +class ReadBookmarkUseCase: PReadBookmarkUseCase { private let addToSpeechQueue: AddTextToSpeechQueueUseCase init(addToSpeechQueue: AddTextToSpeechQueueUseCase = AddTextToSpeechQueueUseCase()) { diff --git a/readeck/Domain/UseCase/RemoveLabelsFromBookmarkUseCase.swift b/readeck/Domain/UseCase/RemoveLabelsFromBookmarkUseCase.swift index b9f3159..7d2e053 100644 --- a/readeck/Domain/UseCase/RemoveLabelsFromBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/RemoveLabelsFromBookmarkUseCase.swift @@ -1,6 +1,11 @@ import Foundation -class RemoveLabelsFromBookmarkUseCase { +protocol PRemoveLabelsFromBookmarkUseCase { + func execute(bookmarkId: String, labels: [String]) async throws + func execute(bookmarkId: String, label: String) async throws +} + +class RemoveLabelsFromBookmarkUseCase: PRemoveLabelsFromBookmarkUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift b/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift index 24bcd08..9c7c837 100644 --- a/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift +++ b/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class SaveServerSettingsUseCase { +protocol PSaveServerSettingsUseCase { + func execute(endpoint: String, username: String, password: String, token: String) async throws +} + +class SaveServerSettingsUseCase: PSaveServerSettingsUseCase { private let repository: PSettingsRepository init(repository: PSettingsRepository) { diff --git a/readeck/Domain/UseCase/SaveSettingsUseCase.swift b/readeck/Domain/UseCase/SaveSettingsUseCase.swift index c8733a4..f8c6176 100644 --- a/readeck/Domain/UseCase/SaveSettingsUseCase.swift +++ b/readeck/Domain/UseCase/SaveSettingsUseCase.swift @@ -1,6 +1,13 @@ import Foundation -class SaveSettingsUseCase { +protocol PSaveSettingsUseCase { + func execute(endpoint: String, username: String, password: String) async throws + func execute(endpoint: String, username: String, password: String, hasFinishedSetup: Bool) async throws + func execute(token: String) async throws + func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws +} + +class SaveSettingsUseCase: PSaveSettingsUseCase { private let settingsRepository: PSettingsRepository init(settingsRepository: PSettingsRepository) { diff --git a/readeck/Domain/UseCase/SearchBookmarksUseCase.swift b/readeck/Domain/UseCase/SearchBookmarksUseCase.swift index 613d2a9..e1b5e92 100644 --- a/readeck/Domain/UseCase/SearchBookmarksUseCase.swift +++ b/readeck/Domain/UseCase/SearchBookmarksUseCase.swift @@ -1,6 +1,10 @@ import Foundation -class SearchBookmarksUseCase { +protocol PSearchBookmarksUseCase { + func execute(search: String) async throws -> BookmarksPage +} + +class SearchBookmarksUseCase: PSearchBookmarksUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/Domain/UseCase/UpdateBookmarkUseCase.swift b/readeck/Domain/UseCase/UpdateBookmarkUseCase.swift index 1aa71af..9e943a4 100644 --- a/readeck/Domain/UseCase/UpdateBookmarkUseCase.swift +++ b/readeck/Domain/UseCase/UpdateBookmarkUseCase.swift @@ -1,6 +1,18 @@ import Foundation -class UpdateBookmarkUseCase { +protocol PUpdateBookmarkUseCase { + func execute(bookmarkId: String, updateRequest: BookmarkUpdateRequest) async throws + func toggleArchive(bookmarkId: String, isArchived: Bool) async throws + func toggleFavorite(bookmarkId: String, isMarked: Bool) async throws + func markAsDeleted(bookmarkId: String) async throws + func updateReadProgress(bookmarkId: String, progress: Int, anchor: String?) async throws + func updateTitle(bookmarkId: String, title: String) async throws + func updateLabels(bookmarkId: String, labels: [String]) async throws + func addLabels(bookmarkId: String, labels: [String]) async throws + func removeLabels(bookmarkId: String, labels: [String]) async throws +} + +class UpdateBookmarkUseCase: PUpdateBookmarkUseCase { private let repository: PBookmarksRepository init(repository: PBookmarksRepository) { diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift index e89f918..49e1fdd 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift @@ -3,7 +3,7 @@ import SafariServices struct BookmarkDetailView: View { let bookmarkId: String - @State private var viewModel = BookmarkDetailViewModel() + @State private var viewModel: BookmarkDetailViewModel @State private var webViewHeight: CGFloat = 300 @State private var showingFontSettings = false @State private var showingLabelsSheet = false @@ -11,6 +11,14 @@ struct BookmarkDetailView: View { private let headerHeight: CGFloat = 320 + init(bookmarkId: String, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel(), webViewHeight: CGFloat = 300, showingFontSettings: Bool = false, showingLabelsSheet: Bool = false, playerUIState: PlayerUIState = .init()) { + self.bookmarkId = bookmarkId + self.viewModel = viewModel + self.webViewHeight = webViewHeight + self.showingFontSettings = showingFontSettings + self.showingLabelsSheet = showingLabelsSheet + } + var body: some View { GeometryReader { geometry in ScrollView { @@ -156,7 +164,7 @@ struct BookmarkDetailView: View { private var contentSection: some View { if let settings = viewModel.settings, !viewModel.articleContent.isEmpty { WebView(htmlContent: viewModel.articleContent, settings: settings) { height in - withAnimation(.easeInOut(duration: 0.3)) { + withAnimation(.easeInOut(duration: 0.1)) { webViewHeight = height } } @@ -181,7 +189,7 @@ struct BookmarkDetailView: View { } .buttonStyle(.borderedProminent) .padding(.horizontal) - .padding(.top, 4) + .padding(.top, 0) } } @@ -339,6 +347,11 @@ struct BookmarkDetailView: View { #Preview { NavigationView { - BookmarkDetailView(bookmarkId: "sample-id") + BookmarkDetailView(bookmarkId: "123", + viewModel: .init(MockUseCaseFactory()), + webViewHeight: 300, + showingFontSettings: false, + showingLabelsSheet: false, + playerUIState: .init()) } } diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift index 1bad4e1..d8474bb 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift @@ -2,11 +2,11 @@ import Foundation @Observable class BookmarkDetailViewModel { - private let getBookmarkUseCase: GetBookmarkUseCase - private let getBookmarkArticleUseCase: GetBookmarkArticleUseCase - private let loadSettingsUseCase: LoadSettingsUseCase - private let updateBookmarkUseCase: UpdateBookmarkUseCase - private let addTextToSpeechQueueUseCase: AddTextToSpeechQueueUseCase + private let getBookmarkUseCase: PGetBookmarkUseCase + private let getBookmarkArticleUseCase: PGetBookmarkArticleUseCase + private let loadSettingsUseCase: PLoadSettingsUseCase + private let updateBookmarkUseCase: PUpdateBookmarkUseCase + private let addTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty var articleContent: String = "" @@ -17,8 +17,7 @@ class BookmarkDetailViewModel { var errorMessage: String? var settings: Settings? - init() { - let factory = DefaultUseCaseFactory.shared + init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.getBookmarkUseCase = factory.makeGetBookmarkUseCase() self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase() self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() diff --git a/readeck/UI/Bookmarks/BookmarksViewModel.swift b/readeck/UI/Bookmarks/BookmarksViewModel.swift index 092bf97..77b0c4f 100644 --- a/readeck/UI/Bookmarks/BookmarksViewModel.swift +++ b/readeck/UI/Bookmarks/BookmarksViewModel.swift @@ -4,9 +4,9 @@ import SwiftUI @Observable class BookmarksViewModel { - private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase() - private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase() - private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase() + private let getBooksmarksUseCase: PGetBookmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase() + private let updateBookmarkUseCase: PUpdateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase() + private let deleteBookmarkUseCase: PDeleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase() var bookmarks: BookmarksPage? var isLoading = false @@ -118,6 +118,7 @@ class BookmarksViewModel { state: currentState, limit: limit, offset: offset, + search: nil, type: currentType, tag: currentTag) bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks) diff --git a/readeck/UI/Settings/SectionHeader.swift b/readeck/UI/Components/SectionHeader.swift similarity index 83% rename from readeck/UI/Settings/SectionHeader.swift rename to readeck/UI/Components/SectionHeader.swift index 0d3af97..7b420bf 100644 --- a/readeck/UI/Settings/SectionHeader.swift +++ b/readeck/UI/Components/SectionHeader.swift @@ -14,4 +14,8 @@ struct SectionHeader: View { .fontWeight(.bold) } } -} \ No newline at end of file +} + +#Preview { + SectionHeader(title: "hello", icon: "person.circle") +} diff --git a/readeck/UI/DefaultUseCaseFactory.swift b/readeck/UI/DefaultUseCaseFactory.swift deleted file mode 100644 index 480fb87..0000000 --- a/readeck/UI/DefaultUseCaseFactory.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -protocol UseCaseFactory { - func makeLoginUseCase() -> LoginUseCase - func makeGetBookmarksUseCase() -> GetBookmarksUseCase - func makeGetBookmarkUseCase() -> GetBookmarkUseCase - func makeGetBookmarkArticleUseCase() -> GetBookmarkArticleUseCase - func makeSaveSettingsUseCase() -> SaveSettingsUseCase - func makeLoadSettingsUseCase() -> LoadSettingsUseCase - func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase - func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase - func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase - func makeLogoutUseCase() -> LogoutUseCase - func makeSearchBookmarksUseCase() -> SearchBookmarksUseCase - func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase - func makeAddLabelsToBookmarkUseCase() -> AddLabelsToBookmarkUseCase - func makeRemoveLabelsFromBookmarkUseCase() -> RemoveLabelsFromBookmarkUseCase - func makeGetLabelsUseCase() -> GetLabelsUseCase - func makeAddTextToSpeechQueueUseCase() -> AddTextToSpeechQueueUseCase -} - -class DefaultUseCaseFactory: UseCaseFactory { - private let tokenProvider = CoreDataTokenProvider() - private lazy var api: PAPI = API(tokenProvider: tokenProvider) - private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository) - private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api) - private let settingsRepository: PSettingsRepository = SettingsRepository() - - static let shared = DefaultUseCaseFactory() - - private init() {} - - func makeLoginUseCase() -> LoginUseCase { - LoginUseCase(repository: authRepository) - } - - func makeGetBookmarksUseCase() -> GetBookmarksUseCase { - GetBookmarksUseCase(repository: bookmarksRepository) - } - - func makeGetBookmarkUseCase() -> GetBookmarkUseCase { - GetBookmarkUseCase(repository: bookmarksRepository) - } - - func makeGetBookmarkArticleUseCase() -> GetBookmarkArticleUseCase { - GetBookmarkArticleUseCase(repository: bookmarksRepository) - } - - func makeSaveSettingsUseCase() -> SaveSettingsUseCase { - SaveSettingsUseCase(settingsRepository: settingsRepository) - } - - func makeLoadSettingsUseCase() -> LoadSettingsUseCase { - LoadSettingsUseCase(authRepository: authRepository) - } - - func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase { - return UpdateBookmarkUseCase(repository: bookmarksRepository) - } - - func makeLogoutUseCase() -> LogoutUseCase { - return LogoutUseCase() - } - - // Nicht mehr nötig - Token wird automatisch geladen - func refreshConfiguration() async { - // Optional: Cache löschen falls nötig - } - - func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase { - return DeleteBookmarkUseCase(repository: bookmarksRepository) - } - - func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase { - return CreateBookmarkUseCase(repository: bookmarksRepository) - } - - func makeSearchBookmarksUseCase() -> SearchBookmarksUseCase { - return SearchBookmarksUseCase(repository: bookmarksRepository) - } - - func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase { - return SaveServerSettingsUseCase(repository: SettingsRepository()) - } - - func makeAddLabelsToBookmarkUseCase() -> AddLabelsToBookmarkUseCase { - return AddLabelsToBookmarkUseCase(repository: bookmarksRepository) - } - - func makeRemoveLabelsFromBookmarkUseCase() -> RemoveLabelsFromBookmarkUseCase { - return RemoveLabelsFromBookmarkUseCase(repository: bookmarksRepository) - } - - func makeGetLabelsUseCase() -> GetLabelsUseCase { - let api = API(tokenProvider: CoreDataTokenProvider()) - let labelsRepository = LabelsRepository(api: api) - return GetLabelsUseCase(labelsRepository: labelsRepository) - } - - func makeAddTextToSpeechQueueUseCase() -> AddTextToSpeechQueueUseCase { - return AddTextToSpeechQueueUseCase() - } -} diff --git a/readeck/UI/Factory/DefaultUseCaseFactory.swift b/readeck/UI/Factory/DefaultUseCaseFactory.swift new file mode 100644 index 0000000..b3db7b2 --- /dev/null +++ b/readeck/UI/Factory/DefaultUseCaseFactory.swift @@ -0,0 +1,100 @@ +import Foundation + +protocol UseCaseFactory { + func makeLoginUseCase() -> PLoginUseCase + func makeGetBookmarksUseCase() -> PGetBookmarksUseCase + func makeGetBookmarkUseCase() -> PGetBookmarkUseCase + func makeGetBookmarkArticleUseCase() -> PGetBookmarkArticleUseCase + func makeSaveSettingsUseCase() -> PSaveSettingsUseCase + func makeLoadSettingsUseCase() -> PLoadSettingsUseCase + func makeUpdateBookmarkUseCase() -> PUpdateBookmarkUseCase + func makeDeleteBookmarkUseCase() -> PDeleteBookmarkUseCase + func makeCreateBookmarkUseCase() -> PCreateBookmarkUseCase + func makeLogoutUseCase() -> PLogoutUseCase + func makeSearchBookmarksUseCase() -> PSearchBookmarksUseCase + func makeSaveServerSettingsUseCase() -> PSaveServerSettingsUseCase + func makeAddLabelsToBookmarkUseCase() -> PAddLabelsToBookmarkUseCase + func makeRemoveLabelsFromBookmarkUseCase() -> PRemoveLabelsFromBookmarkUseCase + func makeGetLabelsUseCase() -> PGetLabelsUseCase + func makeAddTextToSpeechQueueUseCase() -> PAddTextToSpeechQueueUseCase +} + + + +class DefaultUseCaseFactory: UseCaseFactory { + private let tokenProvider = CoreDataTokenProvider() + private lazy var api: PAPI = API(tokenProvider: tokenProvider) + private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository) + private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api) + private let settingsRepository: PSettingsRepository = SettingsRepository() + + static let shared = DefaultUseCaseFactory() + + private init() {} + + func makeLoginUseCase() -> PLoginUseCase { + LoginUseCase(repository: authRepository) + } + + func makeGetBookmarksUseCase() -> PGetBookmarksUseCase { + GetBookmarksUseCase(repository: bookmarksRepository) + } + + func makeGetBookmarkUseCase() -> PGetBookmarkUseCase { + GetBookmarkUseCase(repository: bookmarksRepository) + } + + func makeGetBookmarkArticleUseCase() -> PGetBookmarkArticleUseCase { + GetBookmarkArticleUseCase(repository: bookmarksRepository) + } + + func makeSaveSettingsUseCase() -> PSaveSettingsUseCase { + SaveSettingsUseCase(settingsRepository: settingsRepository) + } + + func makeLoadSettingsUseCase() -> PLoadSettingsUseCase { + LoadSettingsUseCase(authRepository: authRepository) + } + + func makeUpdateBookmarkUseCase() -> PUpdateBookmarkUseCase { + return UpdateBookmarkUseCase(repository: bookmarksRepository) + } + + func makeLogoutUseCase() -> PLogoutUseCase { + return LogoutUseCase() + } + + func makeDeleteBookmarkUseCase() -> PDeleteBookmarkUseCase { + return DeleteBookmarkUseCase(repository: bookmarksRepository) + } + + func makeCreateBookmarkUseCase() -> PCreateBookmarkUseCase { + return CreateBookmarkUseCase(repository: bookmarksRepository) + } + + func makeSearchBookmarksUseCase() -> PSearchBookmarksUseCase { + return SearchBookmarksUseCase(repository: bookmarksRepository) + } + + func makeSaveServerSettingsUseCase() -> PSaveServerSettingsUseCase { + return SaveServerSettingsUseCase(repository: SettingsRepository()) + } + + func makeAddLabelsToBookmarkUseCase() -> PAddLabelsToBookmarkUseCase { + return AddLabelsToBookmarkUseCase(repository: bookmarksRepository) + } + + func makeRemoveLabelsFromBookmarkUseCase() -> PRemoveLabelsFromBookmarkUseCase { + return RemoveLabelsFromBookmarkUseCase(repository: bookmarksRepository) + } + + func makeGetLabelsUseCase() -> PGetLabelsUseCase { + let api = API(tokenProvider: CoreDataTokenProvider()) + let labelsRepository = LabelsRepository(api: api) + return GetLabelsUseCase(labelsRepository: labelsRepository) + } + + func makeAddTextToSpeechQueueUseCase() -> PAddTextToSpeechQueueUseCase { + return AddTextToSpeechQueueUseCase() + } +} diff --git a/readeck/UI/Factory/MockUseCaseFactory.swift b/readeck/UI/Factory/MockUseCaseFactory.swift new file mode 100644 index 0000000..a522dee --- /dev/null +++ b/readeck/UI/Factory/MockUseCaseFactory.swift @@ -0,0 +1,178 @@ +// +// MockUseCaseFactory.swift +// readeck +// +// Created by Ilyas Hallak on 18.07.25. +// + +import Foundation + +class MockUseCaseFactory: UseCaseFactory { + func makeLoginUseCase() -> any PLoginUseCase { + MockLoginUserCase() + } + + func makeGetBookmarksUseCase() -> any PGetBookmarksUseCase { + MockGetBookmarksUseCase() + } + + func makeGetBookmarkUseCase() -> any PGetBookmarkUseCase { + MockGetBookmarkUseCase() + } + + func makeGetBookmarkArticleUseCase() -> any PGetBookmarkArticleUseCase { + MockGetBookmarkArticleUseCase() + } + + func makeSaveSettingsUseCase() -> any PSaveSettingsUseCase { + MockSaveSettingsUseCase() + } + + func makeLoadSettingsUseCase() -> any PLoadSettingsUseCase { + MockLoadSettingsUseCase() + } + + func makeUpdateBookmarkUseCase() -> any PUpdateBookmarkUseCase { + MockUpdateBookmarkUseCase() + } + + func makeDeleteBookmarkUseCase() -> any PDeleteBookmarkUseCase { + MockDeleteBookmarkUseCase() + } + + func makeCreateBookmarkUseCase() -> any PCreateBookmarkUseCase { + MockCreateBookmarkUseCase() + } + + func makeLogoutUseCase() -> any PLogoutUseCase { + MockLogoutUseCase() + } + + func makeSearchBookmarksUseCase() -> any PSearchBookmarksUseCase { + MockSearchBookmarksUseCase() + } + + func makeSaveServerSettingsUseCase() -> any PSaveServerSettingsUseCase { + MockSaveServerSettingsUseCase() + } + + func makeAddLabelsToBookmarkUseCase() -> any PAddLabelsToBookmarkUseCase { + MockAddLabelsToBookmarkUseCase() + } + + func makeRemoveLabelsFromBookmarkUseCase() -> any PRemoveLabelsFromBookmarkUseCase { + MockRemoveLabelsFromBookmarkUseCase() + } + + func makeGetLabelsUseCase() -> any PGetLabelsUseCase { + MockGetLabelsUseCase() + } + + func makeAddTextToSpeechQueueUseCase() -> any PAddTextToSpeechQueueUseCase { + MockAddTextToSpeechQueueUseCase() + } + +} + + +// MARK: Mocked Use Cases + +class MockLoginUserCase: PLoginUseCase { + func execute(endpoint: String, username: String, password: String) async throws -> User { + return User(id: "123", token: "abc") + } +} + +class MockLogoutUseCase: PLogoutUseCase { + func execute() async throws {} +} + +class MockCreateBookmarkUseCase: PCreateBookmarkUseCase { + func execute(createRequest: CreateBookmarkRequest) async throws -> String { "mock-bookmark-id" } + func createFromURL(_ url: String) async throws -> String { "mock-bookmark-id" } + func createFromURLWithTitle(_ url: String, title: String) async throws -> String { "mock-bookmark-id" } + func createFromURLWithLabels(_ url: String, labels: [String]) async throws -> String { "mock-bookmark-id" } + func createFromClipboard() async throws -> String? { "mock-bookmark-id" } +} + +class MockGetLabelsUseCase: PGetLabelsUseCase { + func execute() async throws -> [BookmarkLabel] { + [BookmarkLabel(name: "Test", count: 1, href: "mock-href")] + } +} + +class MockSearchBookmarksUseCase: PSearchBookmarksUseCase { + func execute(search: String) async throws -> BookmarksPage { + BookmarksPage(bookmarks: [], currentPage: 1, totalCount: 0, totalPages: 1, links: nil) + } +} + +class MockReadBookmarkUseCase: PReadBookmarkUseCase { + func execute(bookmarkDetail: BookmarkDetail) {} +} + +class MockGetBookmarksUseCase: PGetBookmarksUseCase { + func execute(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?, tag: String?) async throws -> BookmarksPage { + BookmarksPage(bookmarks: [], currentPage: 1, totalCount: 0, totalPages: 1, links: nil) + } +} + +class MockUpdateBookmarkUseCase: PUpdateBookmarkUseCase { + func execute(bookmarkId: String, updateRequest: BookmarkUpdateRequest) async throws {} + func toggleArchive(bookmarkId: String, isArchived: Bool) async throws {} + func toggleFavorite(bookmarkId: String, isMarked: Bool) async throws {} + func markAsDeleted(bookmarkId: String) async throws {} + func updateReadProgress(bookmarkId: String, progress: Int, anchor: String?) async throws {} + func updateTitle(bookmarkId: String, title: String) async throws {} + func updateLabels(bookmarkId: String, labels: [String]) async throws {} + func addLabels(bookmarkId: String, labels: [String]) async throws {} + func removeLabels(bookmarkId: String, labels: [String]) async throws {} +} + +class MockSaveSettingsUseCase: PSaveSettingsUseCase { + func execute(endpoint: String, username: String, password: String) async throws {} + func execute(endpoint: String, username: String, password: String, hasFinishedSetup: Bool) async throws {} + func execute(token: String) async throws {} + func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws {} +} + +class MockGetBookmarkUseCase: PGetBookmarkUseCase { + func execute(id: String) async throws -> BookmarkDetail { + BookmarkDetail(id: "123", title: "Test", url: "https://www.google.com", description: "Test", siteName: "Test", authors: ["Test"], created: "2021-01-01", updated: "2021-01-01", wordCount: 100, readingTime: 100, hasArticle: true, isMarked: false, isArchived: false, labels: ["Test"], thumbnailUrl: "https://picsum.photos/30/30", imageUrl: "https://picsum.photos/400/400", lang: "en") + } +} + +class MockLoadSettingsUseCase: PLoadSettingsUseCase { + func execute() async throws -> Settings? { + Settings(endpoint: "mock-endpoint", username: "mock-user", password: "mock-pw", token: "mock-token", fontFamily: .system, fontSize: .medium, hasFinishedSetup: true) + } +} + +class MockDeleteBookmarkUseCase: PDeleteBookmarkUseCase { + func execute(bookmarkId: String) async throws {} +} + +class MockGetBookmarkArticleUseCase: PGetBookmarkArticleUseCase { + func execute(id: String) async throws -> String { + let path = Bundle.main.path(forResource: "article", ofType: "html") + return try String(contentsOfFile: path!) + } +} + +class MockAddLabelsToBookmarkUseCase: PAddLabelsToBookmarkUseCase { + func execute(bookmarkId: String, labels: [String]) async throws {} + func execute(bookmarkId: String, label: String) async throws {} +} + +class MockRemoveLabelsFromBookmarkUseCase: PRemoveLabelsFromBookmarkUseCase { + func execute(bookmarkId: String, labels: [String]) async throws {} + func execute(bookmarkId: String, label: String) async throws {} +} + +class MockSaveServerSettingsUseCase: PSaveServerSettingsUseCase { + func execute(endpoint: String, username: String, password: String, token: String) async throws {} +} + +class MockAddTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase { + func execute(bookmarkDetail: BookmarkDetail) {} +} diff --git a/readeck/UI/Factory/article.html b/readeck/UI/Factory/article.html new file mode 100644 index 0000000..24c99c8 --- /dev/null +++ b/readeck/UI/Factory/article.html @@ -0,0 +1,135 @@ +
+

 

Why SwiftData Should Be Isolated

While SwiftData provides a smooth developer experience thanks to its macro-based integration and built-in support for @Model@Query, and @Environment(\.modelContext), it introduces a major architectural concern: tight coupling between persistence and the UI layer.

When you embed SwiftData directly into your views or view models, you violate clean architecture principles like separation of concerns and dependency inversion. This makes your code:

To preserve the testabilityflexibility, and scalability of your app, it’s critical to isolate SwiftData behind an abstraction.

+ +

+In this tutorial, we’ll focus on how to achieve this isolation by applying SOLID principles, with a special emphasis on the Dependency Inversion Principle. We’ll show how to decouple SwiftData from the view and the view model, making your app cleaner, safer, and future-proof, ensuring your app's scalability. +

+ +
View the full source code on GitHub: https://github.com/belkhadir/SwiftDataApp/

Defining the Boundaries

The example you'll see shortly is intentionally simple. The goal is clarity, so you can follow along easily and fully grasp the key concepts. But before diving into the code, let's understand what we mean by boundaries, as clearly defined by Uncle Bob (Robert C. Martin):

+ +
+

“Those boundaries separate software elements from one another, and restrict those on one side from knowing about those on the other.”

+ +
+ +
Figure 1: Diagram from Clean Architecture by Robert C. Martin showing the separation between business rules and database access

In our app, when a user taps the “+” button, we add a new Person. The UI layer should neither know nor care about how or where the Person is saved. Its sole responsibility is straightforward: display a list of persons.

Using something like @Query directly within our SwiftUI views violates these boundaries. Doing so tightly couples the UI to the persistence mechanism (in our case, SwiftData). This breaks the fundamental principle of Single Responsibility, as our views now know too much specific detail about data storage and retrieval.

In the following sections, we’ll show how to respect these boundaries by carefully isolating the persistence logic from the UI, ensuring each layer remains focused, clean, and maintainable.

Abstracting the Persistence Layer

First, let’s clearly outline our requirements. Our app needs to perform three main actions:

  1. Add a new person
  2. Fetch all persons
  3. Delete a specific person

To ensure these operations are not directly tied to any specific storage framework (like SwiftData), we encapsulate them inside a protocol. We’ll name this protocol PersonDataStore:

public protocol PersonDataStore {
+func fetchAll() throws -> [Person]
+func save(_ person: Person) throws
+func delete(_ person: Person) throws
+}
+
+

Next, we define our primary entity, Person, as a simple struct. Notice it doesn’t depend on SwiftData or any other framework:

public struct Person: Identifiable {
+public var id: UUID = UUID()
+public let name: String
+
+public init(name: String) {
+self.name = name
+}
+}
+
+

These definitions (PersonDataStore and Person) become the core of our domain, forming a stable abstraction for persistence that other layers can depend upon.

Implementing SwiftData in the Infra Layer

Now that we have our Domain layer clearly defined, let’s implement the persistence logic using SwiftData. We’ll encapsulate the concrete implementation in a dedicated framework called SwiftDataInfra.

Defining the Local Model

First, we define a local model called LocalePerson. You might wonder why we create a separate model rather than directly using our domain Person entity. The reason is simple:

import SwiftData
+
+@Model
+final class LocalePerson: Identifiable {
+@Attribute(.unique) var name: String
+
+init(name: String) {
+self.name = name
+}
+}
+
+

Note that we annotate it with @Model and specify @Attribute(.unique) on the name property, signaling to SwiftData that each person’s name must be unique.

Implementing the Persistence Logic

To implement persistence operations (fetch, save, delete), we’ll use SwiftData’s ModelContext. We’ll inject this context directly into our infrastructure class (SwiftDataPersonDataStore) via constructor injection:

import Foundation
+import SwiftData
+import SwiftDataDomain
+
+public final class SwiftDataPersonDataStore {
+private let modelContext: ModelContext
+
+public init(modelContext: ModelContext) {
+self.modelContext = modelContext
+}
+}
+
+

Conforming to PersonDataStore

Our infrastructure class will now conform to our domain protocol PersonDataStore. Here’s how each operation is implemented:

1. Fetching all persons:

public func fetchAll() throws -> [Person] {
+let request = FetchDescriptor<LocalePerson>(sortBy: [SortDescriptor(\.name)])
+let results = try modelContext.fetch(request)
+
+return results.map { Person(name: $0.name) }
+}
+
+

2. Saving a person:

public func save(_ person: Person) throws {
+let localPerson = LocalePerson(name: person.name)
+
+modelContext.insert(localPerson)
+try modelContext.save()
+}
+
+

3. Deleting a person:

public func delete(_ person: Person) throws {
+let request = FetchDescriptor<LocalePerson>(sortBy: [SortDescriptor(\.name)])
+let results = try modelContext.fetch(request)
+guard let localPerson = results.first else { return }
+
+modelContext.delete(localPerson)
+try modelContext.save()
+}
+
+

ViewModel That Doesn’t Know About SwiftData

Our ViewModel is placed in a separate framework called SwiftDataPresentation, which depends only on the Domain layer (SwiftDataDomain). Crucially, this ViewModel knows nothing about SwiftData specifics or any persistence details. Its sole responsibility is managing UI state and interactions, displaying persons when the view appears, and handling the addition or deletion of persons through user actions.

SwiftUI list view displaying people added using a modular SwiftData architecture, with a clean decoupled ViewModel.

Here’s the ViewModel implementation, highlighting dependency injection clearly:

public final class PersonViewModel {
+// Dependency injected through initializer
+private let personDataStore: PersonDataStore
+
+// UI state management using ViewState
+public private(set) var viewState: ViewState<[Person]> = .idle
+
+public init(personDataStore: PersonDataStore) {
+self.personDataStore = personDataStore
+}
+}
+
+

Explanation of the Injection and Usage

public func onAppear() {
+viewState = .loaded(allPersons())
+}
+
+
public func addPerson(_ person: Person) {
+perform { try personDataStore.save(person) }
+}
+
+

The ViewModel delegates saving the new person to the injected store, without knowing how or where it happens.

public func deletePerson(at offsets: IndexSet) {
+switch viewState {
+case .loaded(let people) where !people.isEmpty:
+for index in offsets {
+    let person = people[index]
+    perform { try personDataStore.delete(person) }
+}
+default:
+break
+}
+}
+
+

Similarly, deletion is entirely delegated to the injected store, keeping persistence details completely hidden from the ViewModel.

Composing the App Without Breaking Boundaries

Now that we've built clearly defined layers, Domain, Infrastructure, and Presentation, it's time to tie everything together into our application. But there's one important rule: the way we compose our application shouldn't compromise our carefully crafted boundaries.

Clean architecture dependency graph for a SwiftUI app using SwiftData, showing separated App, Presentation, Domain, and Infra layers

Application Composition (SwiftDataAppApp)

Our application's entry point, SwiftDataAppApp, acts as the composition root. It has full knowledge of every module, enabling it to wire dependencies together without letting those details leak into the inner layers:

import SwiftUI
+import SwiftData
+import SwiftDataInfra
+import SwiftDataPresentation
+
+@main
+struct SwiftDataAppApp: App {
+let container: ModelContainer
+
+init() {
+// Creating our SwiftData ModelContainer through a factory method.
+do {
+    container = try SwiftDataInfraContainerFactory.makeContainer()
+} catch {
+    fatalError("Failed to initialize ModelContainer: \(error)")
+}
+}
+
+var body: some Scene {
+WindowGroup {
+    // Constructing the view with dependencies injected.
+    ListPersonViewContructionView.construct(container: container)
+}
+}
+}
+
+

Benefits of This Isolation

By encapsulating SwiftData logic within the Infrastructure layer and adhering strictly to the PersonDataStore protocol, we’ve achieved a powerful separation:

 References and Further Reading

+
diff --git a/readeck/UI/Settings/FontSettingsView.swift b/readeck/UI/Settings/FontSettingsView.swift index 23dcc2f..df0cc30 100644 --- a/readeck/UI/Settings/FontSettingsView.swift +++ b/readeck/UI/Settings/FontSettingsView.swift @@ -8,7 +8,11 @@ import SwiftUI struct FontSettingsView: View { - @State private var viewModel = FontSettingsViewModel() + @State private var viewModel: FontSettingsViewModel + + init(viewModel: FontSettingsViewModel = FontSettingsViewModel()) { + self.viewModel = viewModel + } var body: some View { VStack(alignment: .leading, spacing: 24) { @@ -96,5 +100,7 @@ struct FontSettingsView: View { } #Preview { - FontSettingsView() -} + FontSettingsView(viewModel: .init( + factory: MockUseCaseFactory()) + ) +} diff --git a/readeck/UI/Settings/FontSettingsViewModel.swift b/readeck/UI/Settings/FontSettingsViewModel.swift index d872582..cf7f45e 100644 --- a/readeck/UI/Settings/FontSettingsViewModel.swift +++ b/readeck/UI/Settings/FontSettingsViewModel.swift @@ -11,8 +11,8 @@ import SwiftUI @Observable class FontSettingsViewModel { - private let saveSettingsUseCase: SaveSettingsUseCase - private let loadSettingsUseCase: LoadSettingsUseCase + private let saveSettingsUseCase: PSaveSettingsUseCase + private let loadSettingsUseCase: PLoadSettingsUseCase // MARK: - Font Settings var selectedFontFamily: FontFamily = .system @@ -63,8 +63,7 @@ class FontSettingsViewModel { } } - init() { - let factory = DefaultUseCaseFactory.shared + init(factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.saveSettingsUseCase = factory.makeSaveSettingsUseCase() self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() } diff --git a/readeck/UI/Settings/SettingsGeneralView.swift b/readeck/UI/Settings/SettingsGeneralView.swift index 5fbd9ae..ba96bac 100644 --- a/readeck/UI/Settings/SettingsGeneralView.swift +++ b/readeck/UI/Settings/SettingsGeneralView.swift @@ -9,7 +9,11 @@ import SwiftUI // SectionHeader wird jetzt zentral importiert struct SettingsGeneralView: View { - @State private var viewModel = SettingsGeneralViewModel() + @State private var viewModel: SettingsGeneralViewModel + + init(viewModel: SettingsGeneralViewModel = SettingsGeneralViewModel()) { + self.viewModel = viewModel + } var body: some View { VStack(spacing: 20) { @@ -169,5 +173,7 @@ enum Theme: String, CaseIterable { #Preview { - SettingsGeneralView() -} + SettingsGeneralView(viewModel: .init( + MockUseCaseFactory() + )) +} diff --git a/readeck/UI/Settings/SettingsGeneralViewModel.swift b/readeck/UI/Settings/SettingsGeneralViewModel.swift index fef1987..6aadead 100644 --- a/readeck/UI/Settings/SettingsGeneralViewModel.swift +++ b/readeck/UI/Settings/SettingsGeneralViewModel.swift @@ -4,8 +4,8 @@ import SwiftUI @Observable class SettingsGeneralViewModel { - private let saveSettingsUseCase: SaveSettingsUseCase - private let loadSettingsUseCase: LoadSettingsUseCase + private let saveSettingsUseCase: PSaveSettingsUseCase + private let loadSettingsUseCase: PLoadSettingsUseCase // MARK: - UI Settings var selectedTheme: Theme = .system @@ -26,8 +26,7 @@ class SettingsGeneralViewModel { // func clearCache() async {} // func resetSettings() async {} - init() { - let factory = DefaultUseCaseFactory.shared + init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.saveSettingsUseCase = factory.makeSaveSettingsUseCase() self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() } diff --git a/readeck/UI/Settings/SettingsServerView.swift b/readeck/UI/Settings/SettingsServerView.swift index d7a3bde..33fcfc3 100644 --- a/readeck/UI/Settings/SettingsServerView.swift +++ b/readeck/UI/Settings/SettingsServerView.swift @@ -11,6 +11,11 @@ struct SettingsServerView: View { @State private var viewModel = SettingsServerViewModel() @State private var showingLogoutAlert = false + init(viewModel: SettingsServerViewModel = SettingsServerViewModel(), showingLogoutAlert: Bool = false) { + self.viewModel = viewModel + self.showingLogoutAlert = showingLogoutAlert + } + var body: some View { VStack(spacing: 20) { SectionHeader(title: viewModel.isSetupMode ? "Server-Einstellungen" : "Server-Verbindung", icon: "server.rack") @@ -42,9 +47,9 @@ struct SettingsServerView: View { } } VStack(alignment: .leading, spacing: 6) { - Text("Benutzername") + Text("Username") .font(.headline) - TextField("Ihr Benutzername", text: $viewModel.username) + TextField("Your Username", text: $viewModel.username) .textFieldStyle(.roundedBorder) .autocapitalization(.none) .disableAutocorrection(true) @@ -56,9 +61,9 @@ struct SettingsServerView: View { } } VStack(alignment: .leading, spacing: 6) { - Text("Passwort") + Text("Password") .font(.headline) - SecureField("Ihr Passwort", text: $viewModel.password) + SecureField("Your Password", text: $viewModel.password) .textFieldStyle(.roundedBorder) .disabled(!viewModel.isSetupMode) .onChange(of: viewModel.password) { @@ -90,6 +95,7 @@ struct SettingsServerView: View { .font(.caption) } } + if let successMessage = viewModel.successMessage { HStack { Image(systemName: "checkmark.circle.fill") @@ -165,5 +171,7 @@ struct SettingsServerView: View { } #Preview { - SettingsServerView() + SettingsServerView(viewModel: .init( + MockUseCaseFactory() + )) } diff --git a/readeck/UI/Settings/SettingsServerViewModel.swift b/readeck/UI/Settings/SettingsServerViewModel.swift index 14fe848..ecdc552 100644 --- a/readeck/UI/Settings/SettingsServerViewModel.swift +++ b/readeck/UI/Settings/SettingsServerViewModel.swift @@ -7,10 +7,10 @@ class SettingsServerViewModel { // MARK: - Use Cases - private let loginUseCase: LoginUseCase - private let logoutUseCase: LogoutUseCase - private let saveServerSettingsUseCase: SaveServerSettingsUseCase - private let loadSettingsUseCase: LoadSettingsUseCase + private let loginUseCase: PLoginUseCase + private let logoutUseCase: PLogoutUseCase + private let saveServerSettingsUseCase: PSaveServerSettingsUseCase + private let loadSettingsUseCase: PLoadSettingsUseCase // MARK: - Server Settings var endpoint = "" @@ -27,8 +27,7 @@ class SettingsServerViewModel { SettingsRepository().hasFinishedSetup } - init() { - let factory = DefaultUseCaseFactory.shared + init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.loginUseCase = factory.makeLoginUseCase() self.logoutUseCase = factory.makeLogoutUseCase() self.saveServerSettingsUseCase = factory.makeSaveServerSettingsUseCase() @@ -69,7 +68,6 @@ class SettingsServerViewModel { successMessage = "Server-Einstellungen gespeichert und erfolgreich angemeldet." try await SettingsRepository().saveHasFinishedSetup(true) NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil) - await DefaultUseCaseFactory.shared.refreshConfiguration() } catch { errorMessage = "Verbindung oder Anmeldung fehlgeschlagen: \(error.localizedDescription)" isLoggedIn = false