diff --git a/.gitignore b/.gitignore index 5258389..121d1db 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ CIPipeKit/TestResults # Directories potentially created on remote AFP share node_modules/ serviceKey/ -otherFiles/ \ No newline at end of file +otherFiles/ + +# Claude Code guidelines +.guidelines/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index a8297ec..6f7fd3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,4 +85,19 @@ When creating new Swift files, use "Nikolai Nobadi" as the creator name in file - Automatic cache management for optimal performance ### Language Support -Multiple languages supported via `ComicLanguage` enum with URL generation for different language versions of the comic. \ No newline at end of file +Multiple languages supported via `ComicLanguage` enum with URL generation for different language versions of the comic. + +## Project Guidelines +Project-specific guidelines are located in `.guidelines/claude/` + +## iOS Architecture +- SwiftUI-based views +- No business logic inside SwiftUI views +- Modular feature-based architecture + +## iOS Testing +- Prefer behavior-driven unit tests (Swift Testing, `@Test("…")`) +- Use `makeSUT` + `trackForMemoryLeaks` in tests +- No inline comments in test files; use `// MARK:` only for sectioning +- Default to type-safe assertions (`#expect`, `#require`) +- Use `waitUntil` for `@Published`/reactive assertions instead of sleeps \ No newline at end of file diff --git a/DBMultiverse/Sources/App/Launch/LaunchView.swift b/DBMultiverse/Sources/App/Launch/LaunchView.swift index 1c21d9a..15d350b 100644 --- a/DBMultiverse/Sources/App/Launch/LaunchView.swift +++ b/DBMultiverse/Sources/App/Launch/LaunchView.swift @@ -23,8 +23,10 @@ struct LaunchView: View { } +#if DEBUG // MARK: - Preview #Preview { LaunchView() .withPreviewModifiers() } +#endif diff --git a/DBMultiverse/Sources/App/Launch/WelcomeView.swift b/DBMultiverse/Sources/App/Launch/WelcomeView.swift index ee0d7bf..9445ef3 100644 --- a/DBMultiverse/Sources/App/Launch/WelcomeView.swift +++ b/DBMultiverse/Sources/App/Launch/WelcomeView.swift @@ -17,36 +17,17 @@ struct WelcomeView: View { var body: some View { VStack { - WelcomeHeaderView() + header Spacer() - DisclaimerView() - .showingConditionalView(when: selectingLanguage) { - VStack { - Text("Choose a language") - .padding() - .withFont() - - LanguagePicker(selection: $language) - } - } + disclaimerSection Spacer() - VStack { - Button("Select Language") { - selectingLanguage = true - } - .withFont() - .showingConditionalView(when: selectingLanguage) { - Button("Get Started", action: getStarted) - .padding() - .withFont() - } + buttonSection + .padding() .buttonStyle(.borderedProminent) - } - .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } @@ -54,8 +35,8 @@ struct WelcomeView: View { // MARK: - Header -fileprivate struct WelcomeHeaderView: View { - var body: some View { +private extension WelcomeView { + var header: some View { VStack { Text("Welcome to") .bold() @@ -74,10 +55,41 @@ fileprivate struct WelcomeHeaderView: View { .bold() } } + + @ViewBuilder + var disclaimerSection: some View { + if selectingLanguage { + VStack { + Text("Choose a language") + .padding() + .withFont() + + LanguagePicker(selection: $language) + } + } else { + DisclaimerView() + } + } + + @ViewBuilder + var buttonSection: some View { + if selectingLanguage { + Button("Get Started", action: getStarted) + .padding() + .withFont() + } else { + Button("Select Language") { + selectingLanguage = true + } + .withFont() + } + } } +#if DEBUG // MARK: - Preview #Preview { WelcomeView(language: .constant(.english), getStarted: { }) } +#endif diff --git a/DBMultiverse/Sources/App/Start/DBMultiverseApp.swift b/DBMultiverse/Sources/App/Start/DBMultiverseApp.swift index a7429f3..817b133 100644 --- a/DBMultiverse/Sources/App/Start/DBMultiverseApp.swift +++ b/DBMultiverse/Sources/App/Start/DBMultiverseApp.swift @@ -17,6 +17,6 @@ struct DBMultiverseApp: App { .withNnErrorHandling() .preferredColorScheme(.dark) } - .initializeSwiftDataModelContainer(schema: .init([SwiftDataChapter.self])) + .initializeSwiftDataModelContainer(schema: .init(versionedSchema: CurrentSchema.self)) } } diff --git a/DBMultiverse/Sources/Features/Comic/ChapterListFeatureView.swift b/DBMultiverse/Sources/Features/Comic/ChapterListFeatureView.swift index d87041c..31a210c 100644 --- a/DBMultiverse/Sources/Features/Comic/ChapterListFeatureView.swift +++ b/DBMultiverse/Sources/Features/Comic/ChapterListFeatureView.swift @@ -17,10 +17,11 @@ struct ChapterListFeatureView: View { var body: some View { ChapterListView(imageSize: imageSize, eventHandler: eventHandler) { selection in - iPhoneComicPicker(selection: selection) - .showingConditionalView(when: isPad) { - iPadComicPicker(selection: selection) - } + if isPad { + iPadComicPicker(selection: selection) + } else { + iPhoneComicPicker(selection: selection) + } } } } diff --git a/DBMultiverse/Sources/Features/Main/MainFeaturesView.swift b/DBMultiverse/Sources/Features/Main/MainFeaturesView.swift index 1e7c598..2363523 100644 --- a/DBMultiverse/Sources/Features/Main/MainFeaturesView.swift +++ b/DBMultiverse/Sources/Features/Main/MainFeaturesView.swift @@ -61,17 +61,6 @@ private extension MainFeaturesView { } -// MARK: - Preview -#Preview { - struct PreviewLoader: ChapterLoader { - func loadChapters(url: URL?) async throws -> [Chapter] { [] } - } - - return MainFeaturesView(language: .constant(.english), viewModel: .init(loader: PreviewLoader())) - .withPreviewModifiers() -} - - // MARK: - Extension Dependencies private extension SwiftDataChapterListEventHandler { static func customInit(viewModel: MainFeaturesViewModel, chapterList: SwiftDataChapterList) -> SwiftDataChapterListEventHandler { @@ -102,3 +91,17 @@ private extension ComicPageViewModel { return .init(chapter: route.chapter, currentPageNumber: currentPageNumber, delegate: manager) } } + + + +#if DEBUG +// MARK: - Preview +#Preview { + struct PreviewLoader: ChapterLoader { + func loadChapters(url: URL?) async throws -> [Chapter] { [] } + } + + return MainFeaturesView(language: .constant(.english), viewModel: .init(loader: PreviewLoader())) + .withPreviewModifiers() +} +#endif diff --git a/DBMultiverse/Sources/Features/Settings/Main/SettingsFeatureNavStack.swift b/DBMultiverse/Sources/Features/Settings/Main/SettingsFeatureNavStack.swift index 805a6e5..fe1f9a1 100644 --- a/DBMultiverse/Sources/Features/Settings/Main/SettingsFeatureNavStack.swift +++ b/DBMultiverse/Sources/Features/Settings/Main/SettingsFeatureNavStack.swift @@ -52,7 +52,9 @@ struct SettingsFeatureNavStack: View { } +#if DEBUG // MARK: - Preview #Preview { SettingsFeatureNavStack(language: .constant(.english), viewModel: .init(), canDismiss: true) } +#endif diff --git a/DBMultiverse/Sources/Features/Settings/Model/CachedChapter.swift b/DBMultiverse/Sources/Features/Settings/Model/CachedChapter.swift index 15c6ef9..18e5945 100644 --- a/DBMultiverse/Sources/Features/Settings/Model/CachedChapter.swift +++ b/DBMultiverse/Sources/Features/Settings/Model/CachedChapter.swift @@ -9,4 +9,14 @@ struct CachedChapter: Hashable { let number: String let imageCount: Int + + static var sampleList: [CachedChapter] { + [ + CachedChapter(number: "1", imageCount: 12), + CachedChapter(number: "2", imageCount: 15), + CachedChapter(number: "3", imageCount: 18), + CachedChapter(number: "4", imageCount: 14), + CachedChapter(number: "5", imageCount: 16) + ] + } } diff --git a/DBMultiverse/Sources/Features/Settings/SubViews/CacheChapterListView.swift b/DBMultiverse/Sources/Features/Settings/SubViews/CacheChapterListView.swift index e3d762e..c70d4d3 100644 --- a/DBMultiverse/Sources/Features/Settings/SubViews/CacheChapterListView.swift +++ b/DBMultiverse/Sources/Features/Settings/SubViews/CacheChapterListView.swift @@ -23,7 +23,9 @@ struct CacheChapterListView: View { } +#if DEBUG // MARK: - Preview #Preview { - CacheChapterListView(chapters: []) + CacheChapterListView(chapters: CachedChapter.sampleList) } +#endif diff --git a/DBMultiverse/Sources/Features/Settings/SubViews/LanguageSelectionView.swift b/DBMultiverse/Sources/Features/Settings/SubViews/LanguageSelectionView.swift index 83c307c..c8e7fa8 100644 --- a/DBMultiverse/Sources/Features/Settings/SubViews/LanguageSelectionView.swift +++ b/DBMultiverse/Sources/Features/Settings/SubViews/LanguageSelectionView.swift @@ -23,6 +23,26 @@ struct LanguageSelectionView: View { LanguagePicker(selection: $selection) .padding(.vertical) + languageUpdateView + } + .navigationBarBackButtonHidden(true) + .animation(.easeInOut, value: didChangeLanguage) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .trackingItemChanges(item: selection, itemDidChange: $didChangeLanguage) + .withDiscardChangesNavBarDismissButton( + message: "You've selected a new language.", + itemToModify: selection, + dismissButtonInfo: .init(prompt: "Don't change language.") + ) + } +} + + +// MARK: - Subviews +private extension LanguageSelectionView { + @ViewBuilder + var languageUpdateView: some View { + if didChangeLanguage { VStack { Spacer() Text("If you change your language, all your cached image data will be reset.") @@ -41,22 +61,14 @@ struct LanguageSelectionView: View { Spacer() } - .onlyShow(when: didChangeLanguage) } - .navigationBarBackButtonHidden(true) - .animation(.easeInOut, value: didChangeLanguage) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .trackingItemChanges(item: selection, itemDidChange: $didChangeLanguage) - .withDiscardChangesNavBarDismissButton( - message: "You've selected a new language.", - itemToModify: selection, - dismissButtonInfo: .init(prompt: "Don't change language.") - ) } } +#if DEBUG // MARK: - Preview #Preview { LanguageSelectionView(selection: .english, updateLanguage: { _ in }) } +#endif diff --git a/DBMultiverse/Sources/Features/Settings/SubViews/SettingsDisclaimerView.swift b/DBMultiverse/Sources/Features/Settings/SubViews/SettingsDisclaimerView.swift index 7fb39b9..9704d1b 100644 --- a/DBMultiverse/Sources/Features/Settings/SubViews/SettingsDisclaimerView.swift +++ b/DBMultiverse/Sources/Features/Settings/SubViews/SettingsDisclaimerView.swift @@ -17,7 +17,10 @@ struct SettingsDisclaimerView: View { } } + +#if DEBUG // MARK: - Preview #Preview { SettingsDisclaimerView() } +#endif diff --git a/DBMultiverse/Sources/Features/Settings/SubViews/SettingsFormView.swift b/DBMultiverse/Sources/Features/Settings/SubViews/SettingsFormView.swift index e2450b1..da388e4 100644 --- a/DBMultiverse/Sources/Features/Settings/SubViews/SettingsFormView.swift +++ b/DBMultiverse/Sources/Features/Settings/SubViews/SettingsFormView.swift @@ -16,26 +16,7 @@ struct SettingsFormView: View { var body: some View { Form { DynamicSection("Cached Data") { - VStack { - Text("View Cached Chapters") - .foregroundColor(.blue) - .withFont() - .tappable(withChevron: true) { - viewModel.showView(.cacheList) - } - - Divider() - - HapticButton("Clear All Cached Data", action: viewModel.clearCache) - .padding() - .tint(.red) - .buttonStyle(.bordered) - .withFont(textColor: .red) - .frame(maxWidth: .infinity) - } - .showingConditionalView(when: viewModel.cachedChapters.isEmpty) { - Text("No cached data") - } + cachedDataSectionContent } DynamicSection("Language") { @@ -50,13 +31,7 @@ struct SettingsFormView: View { DynamicSection("Web Comic Links") { ForEach(SettingsLinkItem.allCases, id: \.name) { link in - if let url = viewModel.makeURL(for: link, language: language) { - Link(link.name, destination: url) - .padding(.vertical, 10) - .textLinearGradient(.lightStarrySky) - .asRowItem(withChevron: true) - .withFont() - } + linkRow(link) } } } @@ -65,7 +40,49 @@ struct SettingsFormView: View { } +// MARK: - Subviews +private extension SettingsFormView { + @ViewBuilder + var cachedDataSectionContent: some View { + if viewModel.cachedChapters.isEmpty { + Text("No cached data") + } else { + VStack { + Text("View Cached Chapters") + .foregroundColor(.blue) + .withFont() + .tappable(withChevron: true) { + viewModel.showView(.cacheList) + } + + Divider() + + HapticButton("Clear All Cached Data", action: viewModel.clearCache) + .padding() + .tint(.red) + .buttonStyle(.bordered) + .withFont(textColor: .red) + .frame(maxWidth: .infinity) + } + } + } + + @ViewBuilder + func linkRow(_ link: SettingsLinkItem) -> some View { + if let url = viewModel.makeURL(for: link, language: language) { + Link(link.name, destination: url) + .padding(.vertical, 10) + .textLinearGradient(.lightStarrySky) + .asRowItem(withChevron: true) + .withFont() + } + } +} + + +#if DEBUG // MARK: - Preview #Preview { SettingsFormView(viewModel: .init(), language: .english) } +#endif diff --git a/DBMultiverse/Sources/SwiftData/Model/SwiftDataChapter.swift b/DBMultiverse/Sources/SwiftData/Model/SwiftDataChapter.swift index d3e7144..73a5cea 100644 --- a/DBMultiverse/Sources/SwiftData/Model/SwiftDataChapter.swift +++ b/DBMultiverse/Sources/SwiftData/Model/SwiftDataChapter.swift @@ -5,51 +5,4 @@ // Created by Nikolai Nobadi on 12/10/24. // -import SwiftData - -/// Represents a comic chapter stored in the database. -@Model -final class SwiftDataChapter { - /// The unique name of the chapter. - @Attribute(.unique) var name: String - - /// The unique number of the chapter. - @Attribute(.unique) var number: Int - - /// The starting page of the chapter. - var startPage: Int - - /// The ending page of the chapter. - var endPage: Int - - /// The universe number associated with the chapter, if applicable. - var universe: Int? - - /// The last page the user read in this chapter, if applicable. - var lastReadPage: Int? - - /// The URL for the chapter's cover image. - var coverImageURL: String - - /// Indicates whether the chapter has been completely read by the user. - var didFinishReading: Bool = false - - /// Initializes a new `SwiftDataChapter` instance. - /// - Parameters: - /// - name: The name of the chapter. - /// - number: The unique number of the chapter. - /// - startPage: The starting page of the chapter. - /// - endPage: The ending page of the chapter. - /// - universe: The universe number associated with the chapter, if any. - /// - lastReadPage: The last page read in this chapter, if any. - /// - coverImageURL: The URL for the chapter's cover image. - init(name: String, number: Int, startPage: Int, endPage: Int, universe: Int?, lastReadPage: Int?, coverImageURL: String) { - self.name = name - self.number = number - self.startPage = startPage - self.endPage = endPage - self.universe = universe - self.lastReadPage = lastReadPage - self.coverImageURL = coverImageURL - } -} +typealias SwiftDataChapter = CurrentSchema.SwiftDataChapter diff --git a/DBMultiverse/Sources/SwiftData/Schema/CurrentSchema.swift b/DBMultiverse/Sources/SwiftData/Schema/CurrentSchema.swift new file mode 100644 index 0000000..1779ef1 --- /dev/null +++ b/DBMultiverse/Sources/SwiftData/Schema/CurrentSchema.swift @@ -0,0 +1,8 @@ +// +// CurrentSchema.swift +// DBMultiverse +// +// Created by Nikolai Nobadi on 12/21/25. +// + +typealias CurrentSchema = FirstSchema diff --git a/DBMultiverse/Sources/SwiftData/Schema/FirstSchema.swift b/DBMultiverse/Sources/SwiftData/Schema/FirstSchema.swift new file mode 100644 index 0000000..5b5cde7 --- /dev/null +++ b/DBMultiverse/Sources/SwiftData/Schema/FirstSchema.swift @@ -0,0 +1,54 @@ +// +// FirstSchema.swift +// DBMultiverse +// +// Created by Nikolai Nobadi on 12/21/25. +// + +import Foundation +@preconcurrency import SwiftData + +public enum FirstSchema: VersionedSchema { + public static let versionIdentifier: Schema.Version = .init(1, 0, 0) + public static var models: [any PersistentModel.Type] { + return [ + SwiftDataChapter.self + ] + } +} + + +// MARK: - IntakeEvent +extension FirstSchema { + @Model + final class SwiftDataChapter { + @Attribute(.unique) var name: String + @Attribute(.unique) var number: Int + + var startPage: Int + var endPage: Int + var universe: Int? + var lastReadPage: Int? + var coverImageURL: String + var didFinishReading: Bool = false + + /// Initializes a new `SwiftDataChapter` instance. + /// - Parameters: + /// - name: The name of the chapter. + /// - number: The unique number of the chapter. + /// - startPage: The starting page of the chapter. + /// - endPage: The ending page of the chapter. + /// - universe: The universe number associated with the chapter, if any. + /// - lastReadPage: The last page read in this chapter, if any. + /// - coverImageURL: The URL for the chapter's cover image. + init(name: String, number: Int, startPage: Int, endPage: Int, universe: Int?, lastReadPage: Int?, coverImageURL: String) { + self.name = name + self.number = number + self.startPage = startPage + self.endPage = endPage + self.universe = universe + self.lastReadPage = lastReadPage + self.coverImageURL = coverImageURL + } + } +} diff --git a/DBMultiverse/Sources/SwiftData/ViewModifiers/PreviewModifiersViewModifier.swift b/DBMultiverse/Sources/SwiftData/ViewModifiers/PreviewModifiersViewModifier.swift index e1aa4a1..cb28b83 100644 --- a/DBMultiverse/Sources/SwiftData/ViewModifiers/PreviewModifiersViewModifier.swift +++ b/DBMultiverse/Sources/SwiftData/ViewModifiers/PreviewModifiersViewModifier.swift @@ -11,12 +11,10 @@ import NnSwiftUIKit struct PreviewModifiersViewModifier: ViewModifier { func body(content: Content) -> some View { - MainActor.assumeIsolated { - content - .withNnErrorHandling() - .environment(\.isPreview, true) - .modelContainer(PreviewSampleData.container) - } + content + .withNnErrorHandling() + .environment(\.isPreview, true) + .modelContainer(PreviewSampleData.container) } } diff --git a/DBMultiverse/Sources/SwiftData/ViewModifiers/SwiftDataChapterStorageViewModifier.swift b/DBMultiverse/Sources/SwiftData/ViewModifiers/SwiftDataChapterStorageViewModifier.swift index 09f0181..4518b6b 100644 --- a/DBMultiverse/Sources/SwiftData/ViewModifiers/SwiftDataChapterStorageViewModifier.swift +++ b/DBMultiverse/Sources/SwiftData/ViewModifiers/SwiftDataChapterStorageViewModifier.swift @@ -54,8 +54,16 @@ extension View { // MARK: - Extension Dependencies -fileprivate extension SwiftDataChapter { +private extension SwiftDataChapter { convenience init(chapter: Chapter) { - self.init(name: chapter.name, number: chapter.number, startPage: chapter.startPage, endPage: chapter.endPage, universe: chapter.universe, lastReadPage: chapter.lastReadPage, coverImageURL: chapter.coverImageURL) + self.init( + name: chapter.name, + number: chapter.number, + startPage: chapter.startPage, + endPage: chapter.endPage, + universe: chapter.universe, + lastReadPage: chapter.lastReadPage, + coverImageURL: chapter.coverImageURL + ) } } diff --git a/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/CoverImageManager.swift b/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/CoverImageManager.swift index 29cf018..f5264bb 100644 --- a/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/CoverImageManager.swift +++ b/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/CoverImageManager.swift @@ -9,12 +9,12 @@ import Foundation public struct CoverImageManager: Sendable { private let sharedContainerDirectory: URL - private let fileSystemManager: FileSystemManaging + private let fileSystemManager: FileSystem private let imageCompressor: ImageCompressing private let imageFileName = "chapterCoverImage.jpg" private let jsonFileName = "currentChapterData.json" - init(appGroupIdentifier: String, fileSystemManager: FileSystemManaging, imageCompressor: ImageCompressing) { + init(appGroupIdentifier: String, fileSystemManager: FileSystem, imageCompressor: ImageCompressing) { guard let appGroupDirectory = fileSystemManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { fatalError("Failed to get App Group directory") } @@ -151,7 +151,7 @@ public protocol ImageCompressing: Sendable { func compressImageData(_ data: Data) -> Data? } -public protocol FileSystemManaging: Sendable { +public protocol FileSystem: Sendable { func write(data: Data, to url: URL) throws func readData(from url: URL) throws -> Data func containerURL(forSecurityApplicationGroupIdentifier: String) -> URL? diff --git a/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultFileSystemManager.swift b/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultFileSystemManager.swift index 22d3267..d90c5c4 100644 --- a/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultFileSystemManager.swift +++ b/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultFileSystemManager.swift @@ -7,11 +7,17 @@ import Foundation -public final class DefaultFileSystemManager: FileSystemManaging, @unchecked Sendable { - private let fileManager = FileManager.default - - public init() {} +public final class DefaultFileSystemManager: @unchecked Sendable { + private let fileManager: FileManager + public init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } +} + + +// MARK: - FileSystem +extension DefaultFileSystemManager: FileSystem { public func containerURL(forSecurityApplicationGroupIdentifier identifier: String) -> URL? { return fileManager.containerURL(forSecurityApplicationGroupIdentifier: identifier) } diff --git a/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultImageCompressor.swift b/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultImageCompressor.swift index 366cd77..00d545f 100644 --- a/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultImageCompressor.swift +++ b/DBMultiverseComicKit/Sources/DBMultiverseComicKit/CoverImageCache/DefaultImageCompressor.swift @@ -7,7 +7,7 @@ import UIKit -public final class DefaultImageCompressor: ImageCompressing { +public struct DefaultImageCompressor: ImageCompressing { public init() {} public func compressImageData(_ data: Data) -> Data? { diff --git a/DBMultiverseComicKit/Tests/DBMultiverseComicKitTests/CoverImageManagerTests.swift b/DBMultiverseComicKit/Tests/DBMultiverseComicKitTests/CoverImageManagerTests.swift index f76f1e8..f10bb9a 100644 --- a/DBMultiverseComicKit/Tests/DBMultiverseComicKitTests/CoverImageManagerTests.swift +++ b/DBMultiverseComicKit/Tests/DBMultiverseComicKitTests/CoverImageManagerTests.swift @@ -169,7 +169,7 @@ private extension CoverImageManagerTests { // MARK: - Mocks private extension CoverImageManagerTests { - final class MockFileSystemManager: FileSystemManaging, @unchecked Sendable { + final class MockFileSystemManager: FileSystem, @unchecked Sendable { private let shouldThrowError: Bool private(set) var mockContainerURL: URL private(set) var mockReadData: Data