Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ CIPipeKit/TestResults
# Directories potentially created on remote AFP share
node_modules/
serviceKey/
otherFiles/
otherFiles/

# Claude Code guidelines
.guidelines/
17 changes: 16 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
2 changes: 2 additions & 0 deletions DBMultiverse/Sources/App/Launch/LaunchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ struct LaunchView: View {
}


#if DEBUG
// MARK: - Preview
#Preview {
LaunchView()
.withPreviewModifiers()
}
#endif
62 changes: 37 additions & 25 deletions DBMultiverse/Sources/App/Launch/WelcomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,26 @@ 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)
}
}


// MARK: - Header
fileprivate struct WelcomeHeaderView: View {
var body: some View {
private extension WelcomeView {
var header: some View {
VStack {
Text("Welcome to")
.bold()
Expand All @@ -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
2 changes: 1 addition & 1 deletion DBMultiverse/Sources/App/Start/DBMultiverseApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ struct DBMultiverseApp: App {
.withNnErrorHandling()
.preferredColorScheme(.dark)
}
.initializeSwiftDataModelContainer(schema: .init([SwiftDataChapter.self]))
.initializeSwiftDataModelContainer(schema: .init(versionedSchema: CurrentSchema.self))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
25 changes: 14 additions & 11 deletions DBMultiverse/Sources/Features/Main/MainFeaturesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ struct SettingsFeatureNavStack: View {
}


#if DEBUG
// MARK: - Preview
#Preview {
SettingsFeatureNavStack(language: .constant(.english), viewModel: .init(), canDismiss: true)
}
#endif
10 changes: 10 additions & 0 deletions DBMultiverse/Sources/Features/Settings/Model/CachedChapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ struct CacheChapterListView: View {
}


#if DEBUG
// MARK: - Preview
#Preview {
CacheChapterListView(chapters: [])
CacheChapterListView(chapters: CachedChapter.sampleList)
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ struct SettingsDisclaimerView: View {
}
}


#if DEBUG
// MARK: - Preview
#Preview {
SettingsDisclaimerView()
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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)
}
}
}
Expand All @@ -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
Loading